Compartir a través de


Administración de memoria de código auxiliar del servidor

Introducción a la administración de memoria de Server-Stub

Los códigos auxiliares generados por MIDL actúan como la interfaz entre un proceso de cliente y un proceso de servidor. Un código auxiliar de cliente serializa todos los datos pasados a parámetros marcados con el atributo [in] y los envía al código auxiliar del servidor. El código auxiliar del servidor, al recibir estos datos, reconstruye la pila de llamadas y, a continuación, ejecuta la función de servidor implementada por el usuario correspondiente. El código auxiliar del servidor también serializa los datos de parámetro marcados con el atributo [out] y los devuelve a la aplicación cliente.

El formato de datos serializado de 32 bits usado por MSRPC es una versión compatible de la sintaxis de transferencia de representación de datos de red (NDR). Para obtener más información sobre este formato, vea El sitio web de Open Group. Para las plataformas de 64 bits, se puede usar una extensión de Microsoft de 64 bits a la sintaxis de transferencia de NDR denominada NDR64 para mejorar el rendimiento.

Desmarshaling Inbound Data

En MSRPC, el código auxiliar del cliente serializa todos los datos de parámetro etiquetados como [in] en un búfer continuo para la transmisión al código auxiliar del servidor. Del mismo modo, el código auxiliar del servidor serializa todos los datos marcados con el atributo [out] en un búfer continuo para volver al código auxiliar del cliente. Aunque la capa de protocolo de red debajo de RPC puede fragmentar y agrupar el búfer para la transmisión, la fragmentación es transparente para los códigos auxiliares RPC.

La asignación de memoria para crear el marco de llamada del servidor puede ser una operación costosa. El código auxiliar del servidor intentará minimizar el uso innecesario de memoria cuando sea posible y se supone que la rutina del servidor no liberará ni reasignará los datos marcados con los atributos [in] o [in, out]. El código auxiliar del servidor intenta reutilizar los datos en el búfer siempre que sea posible para evitar la duplicación innecesaria. La regla general es que si el formato de datos serializado es el mismo que el formato de memoria, RPC usará punteros a los datos serializado en lugar de asignar memoria adicional para datos con formato idéntico.

Por ejemplo, la siguiente llamada RPC se define con una estructura cuyo formato serializado es idéntico a su formato en memoria.

typedef struct RpcStructure
{
    long val;
    long val2;
}

void ProcessRpcStructure
(
    [in]  RpcStructure *plInStructure;
    [out] RpcStructure *plOutStructure;
);

En este caso, RPC no asigna memoria adicional para los datos a los que hace referencia plInStructure; en su lugar, simplemente pasa el puntero a los datos serializado a la implementación de la función del lado servidor. El código auxiliar del servidor RPC comprueba el búfer durante el proceso de desajuste si el código auxiliar se compila con la marca "-robust" (que es un valor predeterminado en la versión más reciente del compilador MIDL). RPC garantiza que los datos pasados a la implementación de la función del lado servidor son válidos.

Tenga en cuenta que la memoria se asigna para plOutStructure, ya que no se pasa ningún dato al servidor para él.

Asignación de memoria para datos de entrada

Pueden surgir casos en los que el código auxiliar del servidor asigna memoria para los datos de parámetro marcados con los atributos [in] o [in, out]. Esto ocurre cuando el formato de datos serializado difiere del formato de memoria, o cuando las estructuras que componen los datos serializado son suficientes complejas y deben ser leídas atómicamente por el código auxiliar del servidor RPC. A continuación se enumeran varios casos comunes cuando la memoria debe asignarse para los datos recibidos por el código auxiliar del servidor.

  • Los datos son una matriz variable o una matriz compatible. Estas son matrices (o punteros a matrices) que tienen el atributo [length_is()] o [first_is()] establecido en ellas. En NDR, solo se serializa y transmite el primer elemento de estas matrices. Por ejemplo, en el fragmento de código siguiente, los datos pasados en el parámetro pv tendrán memoria asignada.

    void RpcFunction
    (
        [in] long size,
        [in, out] long *pLength,
        [in, out, size_is(size), length_is(*pLength)] long *pv
    );
    
  • Los datos son una cadena de tamaño o una cadena no compatible. Normalmente, estas cadenas son punteros a datos de caracteres etiquetados con el atributo [size_is()]. En el ejemplo siguiente, la cadena que se pasa a la función del lado servidor SizedString tendrá asignada memoria, mientras que la cadena pasada a la función NormalString se reutilizará.

    void SizedString
    (
        [in] long size,
        [in, size_is(size), string] char *str
    );
    
    void NormalString
    (
        [in, string] char str
    );
    
  • Los datos son un tipo simple cuyo tamaño de memoria difiere de su tamaño serializado, como enum16 y __int3264.

  • Los datos se definen mediante una estructura cuya alineación de memoria es menor que la alineación natural, contiene cualquiera de los tipos de datos anteriores o tiene relleno de bytes final. Por ejemplo, la siguiente estructura de datos compleja ha forzado la alineación de 2 bytes y tiene relleno al final.

#pragma pack(2) typedef struct ComplexPackedStructure { char c;
long l; la alineación se fuerza en el segundo byte char c2; habrá un panel de un byte final para mantener la alineación de 2 bytes } '''

  • Los datos contienen una estructura que debe serializarse campo por campo. Estos campos incluyen punteros de interfaz definidos en interfaces DCOM; punteros omitido; valores enteros establecidos con el atributo [range] ; elementos de matrices definidas con los atributos [wire_marshal], [user_marshal], [transmit_as] y [represent_as] ; y estructuras de datos complejas incrustadas.
  • Los datos contienen una unión, una estructura que contiene una unión o una matriz de uniones. Solo la rama específica de la unión se serializa en el cable.
  • Los datos contienen una estructura con una matriz de conformidad multidimensional que tiene al menos una dimensión no fija.
  • Los datos contienen una matriz de estructuras complejas.
  • Los datos contienen una matriz de tipos de datos simples, como enum16 y __int3264.
  • Los datos contienen una matriz de punteros de referencia e interfaz.
  • Los datos tienen un atributo [force_allocate] aplicado a un puntero.
  • Los datos tienen un atributo [allocate(all_nodes)] aplicado a un puntero.
  • Los datos tienen un atributo [byte_count] aplicado a un puntero.

Sintaxis de transferencia de datos de 64 bits y NDR64

Como se mencionó anteriormente, los datos de 64 bits se serializarán mediante una sintaxis de transferencia de 64 bits específica denominada NDR64. Esta sintaxis de transferencia se desarrolló para solucionar el problema específico que surge cuando los punteros se serializan en NDR de 32 bits y se transmiten a un código auxiliar de servidor en una plataforma de 64 bits. En este caso, un puntero de datos de 32 bits serializado no coincide con uno de 64 bits y la asignación de memoria se producirá invariablemente. Para crear un comportamiento más coherente en plataformas de 64 bits, Microsoft desarrolló una nueva sintaxis de transferencia denominada NDR64.

Un ejemplo que ilustra este problema es el siguiente:

typedef struct PtrStruct
{
  long l;
  long *pl;
}

Esta estructura, cuando se serializa, será reutilizada por el código auxiliar del servidor en un sistema de 32 bits. Sin embargo, si el código auxiliar del servidor reside en un sistema de 64 bits, los datos serializado por NDR son de 4 bytes de longitud, pero el tamaño de memoria necesario será 8. Como resultado, la asignación de memoria se fuerza y la reutilización del búfer rara vez se producirá. NDR64 soluciona este problema haciendo que el tamaño serializado de un puntero de 64 bits.

A diferencia de NDR de 32 bits, los valores de datos simples como enum16 y __int3264 no hacen que una estructura o matriz sea compleja en NDR64. Del mismo modo, los valores de relleno final no hacen que una estructura sea compleja. Los punteros de interfaz se tratan como punteros únicos en el nivel superior; Como resultado, las estructuras y matrices que contienen punteros de interfaz no se consideran complejas y no requerirán una asignación de memoria específica para su uso.

Inicialización de datos salientes

Una vez que todos los datos entrantes se han desmarizado, el código auxiliar del servidor debe inicializar los punteros de solo salida marcados con el atributo [out].

typedef struct RpcStructure
{
    long val;
    long val2;
}

void ProcessRpcStructure
(
    [in]  RpcStructure *plInStructure;
    [out] RpcStructure *plOutStructure;
);

En la llamada anterior, el código auxiliar del servidor debe inicializar plOutStructure porque no estaba presente en los datos serializado y es un puntero implícito [ref] que debe estar disponible para la implementación de la función del servidor. El código auxiliar del servidor RPC inicializa y cero los punteros de solo referencia de nivel superior con el atributo [out]. Los punteros de referencia [out] debajo también se inicializan recursivamente. La recursividad se detiene en cualquier puntero con los atributos [únicos] o [ptr] establecidos en ellos.

La implementación de la función de servidor no puede modificar directamente los valores de puntero de nivel superior y, por tanto, no puede reasignarlos. Por ejemplo, en la implementación de ProcessRpcStructure anterior, el código siguiente no es válido:

void ProcessRpcStructure(RpcStructure *plInStructure, rpcStructure *plOutStructure)
{
    plOutStructure = MIDL_user_allocate(sizeof(RpcStructure));
    Process(plOutStructure);
}

plOutStructure es un valor de pila y su cambio no se propaga a RPC. La implementación de la función de servidor puede intentar evitar la asignación intentando liberar plOutStructure, lo que puede provocar daños en la memoria. A continuación, el código auxiliar del servidor asignará espacio para el puntero de nivel superior en memoria (en el caso de puntero a puntero) y una estructura simple de nivel superior cuyo tamaño en la pila sea menor de lo esperado.

El cliente puede, en determinadas circunstancias, especificar el tamaño de asignación de memoria del lado servidor. En el ejemplo siguiente, el cliente especifica el tamaño de los datos de salida en el parámetro de tamaño de entrada.

void VariableSizeData
(
    [in] long size,
    [out, size_is(size)] char *pv
);

Después de desasignar los datos entrantes, incluido el tamaño, el código auxiliar del servidor asigna un búfer para pv con un tamaño de "sizeof(char)*size". Una vez asignado el espacio, el código auxiliar del servidor cero el búfer. Tenga en cuenta que, en este caso concreto, el código auxiliar asigna la memoria con MIDL_user_allocate(), ya que el tamaño del búfer se determina en tiempo de ejecución.

Tenga en cuenta que, en el caso de las interfaces DCOM, es posible que los códigos auxiliares generados por MIDL no estén implicados en absoluto si el cliente y el servidor comparten el mismo apartamento COM o si se implementa ICallFrame . En este caso, el servidor no puede depender del comportamiento de asignación y debe comprobar de forma independiente la memoria de tamaño del cliente.

Implementaciones de funciones del lado servidor y serialización de datos salientes

Inmediatamente después de la desenlazamiento en los datos entrantes y la inicialización de la memoria asignada para contener datos salientes, el código auxiliar del servidor RPC ejecuta la implementación del lado servidor de la función a la que llama el cliente. En este momento, el servidor puede modificar los datos marcados específicamente con el atributo [in, out] y puede rellenar la memoria asignada para los datos de solo salida (los datos etiquetados con [out]).

Las reglas generales para la manipulación de datos de parámetros serializado son sencillas: el servidor solo puede asignar nueva memoria o modificar la memoria asignada específicamente por el código auxiliar del servidor. La reasignación o liberación de la memoria existente para los datos puede tener un impacto negativo en los resultados y el rendimiento de la llamada de función, y puede ser muy difícil de depurar.

Lógicamente, el servidor RPC reside en un espacio de direcciones diferente al cliente y, por lo general, se puede suponer que no comparten memoria. Como resultado, es seguro que la implementación de la función del servidor use los datos marcados con el atributo [in] como memoria "temporal" sin afectar a las direcciones de memoria del cliente. Dicho esto, el servidor no debe intentar reasignar ni liberar [in] datos, dejando el control de esos espacios en el código auxiliar del servidor RPC.

Por lo general, la implementación de la función de servidor no necesita reasignar ni liberar datos marcados con el atributo [in, out]. Para los datos de tamaño fijo, la lógica de implementación de funciones puede modificar directamente los datos. Del mismo modo, para los datos de tamaño variable, la implementación de la función no debe modificar el valor de campo proporcionado al atributo [size_is()], tampoco. Cambie el valor de campo utilizado para cambiar el tamaño de los datos da como resultado un búfer más pequeño o mayor devuelto al cliente que puede estar mal equipado para tratar con la longitud anómala.

Si se producen circunstancias en las que la rutina de servidor tiene que reasignar la memoria usada por los datos marcados con el atributo [in, out], es totalmente posible que la implementación de la función del lado servidor no sepa si el puntero proporcionado por el código auxiliar es la memoria asignada con MIDL_user_allocate() o el búfer de conexión serializado. Para solucionar este problema, MS RPC puede asegurarse de que no se produzca ninguna pérdida de memoria o daños si el atributo [force_allocate] está establecido en los datos. Cuando se establece [force_allocate], el código auxiliar del servidor siempre asignará memoria para el puntero, aunque la advertencia es que el rendimiento disminuirá para cada uso de él.

Cuando la llamada vuelve desde la implementación de la función del lado servidor, el código auxiliar del servidor serializa los datos marcados con el atributo [out] y los envía al cliente. Tenga en cuenta que el código auxiliar no serializa los datos si la implementación de la función del lado servidor produce una excepción.

Liberar memoria asignada

El código auxiliar del servidor RPC liberará la memoria de pila después de que se haya devuelto la llamada desde la función del lado servidor, independientemente de si se produce o no una excepción. El código auxiliar del servidor libera toda la memoria asignada por el código auxiliar, así como cualquier memoria asignada con MIDL_user_allocate(). La implementación de la función del lado servidor siempre debe proporcionar a RPC un estado coherente, ya sea iniciando una excepción o devolviendo un código de error. Si se produce un error en la función durante el rellenado de estructuras de datos complicadas, debe asegurarse de que todos los punteros apuntan a datos válidos o se establecen en NULL.

Durante este paso, el código auxiliar del servidor libera toda la memoria que no forma parte del búfer serializado que contiene los datos [in]. Una excepción a este comportamiento es los datos con el atributo [allocate(dont_free)] establecido en ellos: el código auxiliar del servidor no libera ninguna memoria asociada a estos punteros.

Una vez que el código auxiliar del servidor libera la memoria asignada por el código auxiliar y la implementación de la función, el código auxiliar llama a una función de notificación específica si se especifica el atributo [notify_flag] para datos concretos.

Serialización de una lista vinculada a través de RPC: un ejemplo

typedef struct _LINKEDLIST
{
    long lSize;
    [size_is(lSize)] char *pData;
    struct _LINKEDLIST *pNext;
} LINKEDLIST, *PLINKEDLIST;

void Test
(
    [in] LINKEDLIST *pIn,
    [in, out] PLINKEDLIST *pInOut,
    [out] LINKEDLIST *pOut
);

En el ejemplo anterior, el formato de memoria de LINKEDLIST será idéntico al formato de conexión serializado. Como resultado, el código auxiliar del servidor no asigna memoria para toda la cadena de punteros de datos en pIn. En su lugar, RPC reutiliza el búfer de conexión para toda la lista vinculada. Del mismo modo, el código auxiliar no asigna memoria para pInOut, sino que reutiliza el búfer de conexión serializado por el cliente.

Dado que la firma de la función contiene un parámetro de salida, pOut, el código auxiliar del servidor asigna memoria para contener los datos devueltos. La memoria asignada se limita inicialmente a ceros, con pNext establecido en NULL. La aplicación puede asignar la memoria para una nueva lista vinculada y apuntar pOut-pNext> a ella.pIn y la lista vinculada que contiene se pueden usar como área temporal, pero la aplicación no debe cambiar ninguno de los punteros pNext.

La aplicación puede cambiar libremente el contenido de la lista vinculada a la que apunta pInOut, pero no debe cambiar ninguno de los punteros pNext , por ejemplo, el propio vínculo de nivel superior. Si la aplicación decide acortar la lista vinculada, no puede saber si algún puntero pNext determinado vincula un búfer interno RPC o un búfer asignado específicamente con MIDL_user_allocate().. Para solucionar este problema, agregue una declaración de tipo específica para punteros de lista vinculados que fuerzan la asignación de usuarios, como se muestra en el código siguiente.

typedef [force_allocate] PLINKEDLIST;

Este atributo obliga al código auxiliar del servidor a asignar cada nodo de la lista vinculada por separado y la aplicación puede liberar la parte abreviada de la lista vinculada llamando a MIDL_user_free(). Después, la aplicación puede establecer de forma segura el puntero pNext al final de la lista vinculada recién abreviada en NULL.