Condividi tramite


Gestione della memoria Stub server

Introduzione alla gestione della memoria Server-Stub

Gli stub generati da MIDL fungono da interfaccia tra un processo client e un processo server. Uno stub client esegue il marshalling di tutti i dati passati ai parametri contrassegnati con l'attributo [in] e lo invia allo stub del server. Lo stub del server, dopo aver ricevuto questi dati, ricostruisce lo stack di chiamate e quindi esegue la funzione server implementata dall'utente corrispondente. Lo stub del server esegue anche il marshalling dei dati dei parametri contrassegnati con l'attributo [out] e lo restituisce all'applicazione client.

Il formato di dati con marshalling a 32 bit usato da MSRPC è una versione conforme della sintassi di trasferimento NDR (Network Data Representation). Per altre informazioni su questo formato, vedere Il sito Web Open Group. Per le piattaforme a 64 bit, è possibile usare una sintassi di trasferimento NDR di Microsoft a 64 bit denominata NDR64 per migliorare le prestazioni.

Annullare l'associazione dei dati in ingresso

In MSRPC il client esegue il marshalling di tutti i dati dei parametri contrassegnati come [in] in un buffer continuo per la trasmissione al server stub. Analogamente, lo stub del server esegue il marshalling di tutti i dati contrassegnati con l'attributo [out] in un buffer continuo per tornare allo stub del client. Mentre il livello del protocollo di rete sotto RPC può frammentare e pacchettizzare il buffer per la trasmissione, la frammentazione è trasparente agli stub RPC.

L'allocazione della memoria per la creazione del frame di chiamata del server può essere un'operazione costosa. Lo stub del server tenterà di ridurre al minimo l'utilizzo della memoria non necessario quando possibile e si presuppone che la routine del server non possa liberare o riallocare i dati contrassegnati con gli attributi [in] o [in, out]. Lo stub del server tenta di riutilizzare i dati nel buffer ogni volta che è possibile evitare la duplicazione non necessaria. La regola generale è che se il formato dei dati con marshalling è uguale al formato di memoria, RPC userà puntatori ai dati marshalling anziché allocare memoria aggiuntiva per dati formattati in modo identico.

Ad esempio, la chiamata RPC seguente viene definita con una struttura il cui formato di marshalling è identico al formato in memoria.

typedef struct RpcStructure
{
    long val;
    long val2;
}

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

In questo caso, RPC non alloca memoria aggiuntiva per i dati a cui fa riferimento plInStructure; piuttosto, passa semplicemente il puntatore ai dati di marshalling all'implementazione della funzione lato server. Lo stub del server RPC verifica il buffer durante il processo di nonmarshaling se lo stub viene compilato usando il flag "-robust" (ovvero un'impostazione predefinita nella versione più recente del compilatore MIDL). RPC garantisce che i dati passati all'implementazione della funzione lato server siano validi.

Tenere presente che la memoria viene allocata per plOutStructure, poiché non vengono passati dati al server.

Allocazione di memoria per i dati in ingresso

I casi possono verificarsi in cui lo stub del server alloca la memoria per i dati dei parametri contrassegnati con gli attributi [in] o [in, out]. Ciò si verifica quando il formato di dati con marshalling differisce dal formato di memoria o quando le strutture che comprendono i dati di marshalling sono sufficienti e devono essere letti atomicamente dallo stub del server RPC. Di seguito sono elencati diversi casi comuni in cui la memoria deve essere allocata per i dati ricevuti dallo stub del server.

  • I dati sono una matrice variabile o una matrice diversa. Queste sono matrici (o puntatori a matrici) che hanno l'attributo [length_is()] o [first_is()] impostato su di essi. In NDR, solo il primo elemento di queste matrici viene eseguito il marshalling e la trasmissione. Nel frammento di codice seguente, ad esempio, i dati passati nel parametro pv avranno la memoria allocata.

    void RpcFunction
    (
        [in] long size,
        [in, out] long *pLength,
        [in, out, size_is(size), length_is(*pLength)] long *pv
    );
    
  • I dati sono una stringa di dimensioni o una stringa non conforme. Queste stringhe sono in genere puntatori ai dati dei caratteri contrassegnati con l'attributo [size_is()]. Nell'esempio seguente, la stringa passata alla funzione lato server SizeString avrà la memoria allocata, mentre la stringa passata alla funzione NormalString verrà riutilizzata.

    void SizedString
    (
        [in] long size,
        [in, size_is(size), string] char *str
    );
    
    void NormalString
    (
        [in, string] char str
    );
    
  • I dati sono un tipo semplice le cui dimensioni di memoria differiscono dalle dimensioni del marshalling, ad esempio enum16 e __int3264.

  • I dati sono definiti da una struttura il cui allineamento della memoria è minore dell'allineamento naturale, contiene uno dei tipi di dati precedenti o ha un riempimento di byte finale. Ad esempio, la struttura di dati complessa seguente ha un allineamento forzato a 2 byte e ha riempimento alla fine.

#pragma pack(2) typedef struct ComplexPackedStructure { char c;
long l; l'allineamento è forzato al secondo byte char c2; ci sarà un pad a un byte finale per mantenere l'allineamento di 2 byte } '''

  • I dati contengono una struttura che deve essere sottoposto a marshalling per campo. Questi campi includono puntatori di interfaccia definiti nelle interfacce DCOM; puntatori ignorati; valori integer impostati con l'attributo [intervallo]; elementi di matrici definite con gli attributi [wire_marshal], [user_marshal], [transmit_as] e [represent_as]; e strutture di dati complesse incorporate.
  • I dati contengono un'unione, una struttura contenente un'unione o una matrice di unioni. Solo il ramo specifico dell'unione viene marshallato sul filo.
  • I dati contengono una struttura con una matrice conforme a multidimensionale con almeno una dimensione non fissa.
  • I dati contengono una matrice di strutture complesse.
  • I dati contengono una matrice di tipi di dati semplici, ad esempio enum16 e __int3264.
  • I dati contengono una matrice di puntatori di riferimento e interfaccia.
  • I dati hanno un attributo [force_allocate] applicato a un puntatore.
  • I dati hanno un attributo [allocate(all_nodes)] applicato a un puntatore.
  • I dati hanno un attributo [byte_count] applicato a un puntatore.

Sintassi di trasferimento a 64 bit e NDR64

Come accennato in precedenza, i dati a 64 bit vengono marshallati usando una sintassi di trasferimento a 64 bit specifica denominata NDR64. Questa sintassi di trasferimento è stata sviluppata per risolvere il problema specifico che si verifica quando i puntatori vengono marshallati sotto nDR a 32 bit e trasmessi a uno stub server in una piattaforma a 64 bit. In questo caso, un puntatore dati a 32 bit non corrisponde a uno a 64 bit e l'allocazione della memoria si verificherà invariabilmente. Per creare un comportamento più coerente sulle piattaforme a 64 bit, Microsoft ha sviluppato una nuova sintassi di trasferimento denominata NDR64.

Un esempio che illustra questo problema è il seguente:

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

Questa struttura, quando si esegue il marshalling, verrà riutilizzata dallo stub del server in un sistema a 32 bit. Tuttavia, se il stub del server risiede in un sistema a 64 bit, i dati con marshalling NDR sono 4 byte di lunghezza, ma le dimensioni di memoria necessarie saranno 8. Di conseguenza, l'allocazione della memoria è forzata e il riutilizzo del buffer si verificherà raramente. NDR64 risolve questo problema eseguendo il marshalling delle dimensioni di un puntatore a 64 bit.

Al contrario di NDR a 32 bit, i semplici tipi di dati come enum16 e __int3264 non rendono una struttura o una matrice complessa in NDR64. Analogamente, i valori del pad finale non rendono complessa una struttura. I puntatori di interfaccia vengono considerati puntatori univoci a livello superiore; di conseguenza, le strutture e le matrici contenenti puntatori di interfaccia non sono considerate complesse e non richiedono allocazioni di memoria specifiche per il loro uso.

Inizializzazione dei dati in uscita

Dopo che tutti i dati in ingresso sono stati annullati, lo stub del server deve inizializzare i puntatori solo in uscita contrassegnati con l'attributo [out] .

typedef struct RpcStructure
{
    long val;
    long val2;
}

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

Nella chiamata precedente il server stub deve inizializzare plOutStructure perché non era presente nei dati di marshalling e si tratta di un puntatore [ref] implicito che deve essere reso disponibile per l'implementazione della funzione server. Il server RPC inizializza e zerosce tutti i puntatori di riferimento di primo livello con l'attributo [out] . Tutti i puntatori di riferimento [out] sotto di esso vengono inizializzati in modo ricorsivo. La ricorsione si arresta in qualsiasi puntatore con gli attributi [univoci] o [ptr] impostati su di essi.

L'implementazione della funzione server non può modificare direttamente i valori del puntatore di primo livello e quindi non può riallocarli. Ad esempio, nell'implementazione di ProcessRpcStructure precedente, il codice seguente non è valido:

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

plOutStructure è un valore dello stack e la relativa modifica non viene propagata nuovamente a RPC. L'implementazione della funzione server può tentare di evitare l'allocazione tentando di liberare plOutStructure, che potrebbe causare il danneggiamento della memoria. Lo stub del server allocherà quindi spazio per il puntatore di primo livello in memoria (nel caso puntatore a puntatore) e una struttura semplice di primo livello la cui dimensione nello stack è inferiore rispetto al previsto.

Il client può, in determinate circostanze, specificare le dimensioni di allocazione della memoria del lato server. Nell'esempio seguente il client specifica le dimensioni dei dati in uscita nel parametro dimensioni in ingresso.

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

Dopo aver scollegato i dati in ingresso, incluse le dimensioni, lo stub del server alloca un buffer per pv con dimensioni "sizeof(char)*size". Dopo l'allocazione dello spazio, lo stub del server esce dal buffer. Si noti che in questo caso specifico lo stub alloca la memoria con MIDL_user_allocate(), poiché le dimensioni del buffer vengono determinate in fase di esecuzione.

Tenere presente che nel caso di interfacce DCOM, gli stub generati da MIDL potrebbero non essere coinvolti se il client e il server condividono lo stesso appartamento COM o se ICallFrame viene implementato. In questo caso, il server non può dipendere dal comportamento di allocazione e deve verificare in modo indipendente la memoria con dimensioni client.

Implementazioni delle funzioni lato server e marshalling dei dati in uscita

Immediatamente dopo l'annullamento delmarshalling sui dati in ingresso e l'inizializzazione della memoria allocata per contenere i dati in uscita, lo stub del server RPC esegue l'implementazione lato server della funzione chiamata dal client. A questo punto, il server può modificare i dati contrassegnati in modo specifico con l'attributo [in, out] e può popolare la memoria allocata per i dati solo in uscita (i dati contrassegnati con [out]).

Le regole generali per la manipolazione dei dati dei parametri marshalling sono semplici: il server può allocare solo nuova memoria o modificare la memoria allocata in modo specifico dallo stub del server. La riallocazione o il rilascio della memoria esistente per i dati possono avere un impatto negativo sui risultati e sulle prestazioni della chiamata di funzione e possono essere molto difficili da eseguire il debug.

Logicamente, il server RPC si trova in uno spazio di indirizzi diverso rispetto al client e può in genere essere assunto che non condividono memoria. Di conseguenza, è sicuro che l'implementazione della funzione server usi i dati contrassegnati con l'attributo [in] come memoria "scratch" senza influire sugli indirizzi di memoria client. Detto questo, il server non deve tentare di riallocare o rilasciare i dati [in] lasciando il controllo di tali spazi allo stub del server RPC stesso.

In genere, l'implementazione della funzione server non deve riallocare o rilasciare i dati contrassegnati con l'attributo [in, out] . Per i dati di dimensioni fisse, la logica di implementazione della funzione può modificare direttamente i dati. Analogamente, per i dati di dimensione variabile, l'implementazione della funzione non deve modificare il valore del campo fornito all'attributo [size_is()] , entrambi. Modificare il valore del campo usato per ridimensionare i risultati dei dati in un buffer più piccolo o maggiore restituito al client che potrebbe non essere dotato di problemi per gestire la lunghezza anormale.

Se si verificano circostanze in cui la routine del server deve riallocare la memoria utilizzata dai dati contrassegnati con l'attributo [in, out] , è completamente possibile che l'implementazione della funzione lato server non sappia se il puntatore fornito dal stub è allocato dalla memoria allocata con MIDL_user_allocate() o il buffer di filo con marshalling. Per risolvere questo problema, MS RPC può assicurarsi che non si verifichino perdite di memoria o danneggiamento se l'attributo [force_allocate] è impostato sui dati. Quando [force_allocate] è impostato, lo stub del server alloca sempre la memoria per il puntatore, anche se le prestazioni diminuiscono per ogni uso.

Quando la chiamata viene restituita dall'implementazione della funzione lato server, il server esegue il marshalling dei dati contrassegnati con l'attributo [out] e lo invia al client. Tenere presente che lo stub non esegue il marshalling dei dati se l'implementazione della funzione lato server genera un'eccezione.

Rilascio della memoria allocata

Lo stub del server RPC rilascia la memoria dello stack dopo che la chiamata viene restituita dalla funzione lato server, indipendentemente dal fatto che si verifichi o meno un'eccezione. Lo stub del server libera tutta la memoria allocata dallo stub e qualsiasi memoria allocata con MIDL_user_allocate(). L'implementazione della funzione lato server deve sempre assegnare uno stato coerente a RPC, generando un'eccezione o restituendo un codice di errore. Se la funzione ha esito negativo durante la popolazione di strutture di dati complicate, deve assicurarsi che tutti i puntatori puntino a dati validi o siano impostati su NULL.

Durante questo passaggio, il server stub libera tutta la memoria che non fa parte del buffer marshalling contenente i dati [in] . Un'eccezione a questo comportamento è costituita da dati con l'attributo [allocate(dont_free)] impostato su di essi. Lo stub del server non libera alcuna memoria associata a questi puntatori.

Dopo che il server stub rilascia la memoria allocata dallo stub e dall'implementazione della funzione, lo stub chiama una funzione di notifica specifica se l'attributo [notify_flag] viene specificato per dati specifici.

Marshalling di un elenco collegato su RPC - Un esempio

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
);

Nell'esempio precedente, il formato di memoria per LINKEDLIST sarà identico al formato di filo marshalling. Di conseguenza, lo stub del server non alloca la memoria per l'intera catena di puntatori dati in pIn. Invece, RPC riutilizza il buffer di fili per l'intero elenco collegato. Analogamente, lo stub non alloca la memoria per pInOut, ma riutilizza il marshalling del buffer di filo dal client.

Poiché la firma della funzione contiene un parametro in uscita, pOut, il server stub alloca la memoria per contenere i dati restituiti. La memoria allocata è inizialmente zero, con pNext impostata su NULL. L'applicazione può allocare la memoria per un nuovo elenco collegato e puntare pOut-pNext>. pIn e l'elenco collegato che contiene possono essere usati come area zero, ma l'applicazione non deve modificare nessuno dei puntatori pNext.

L'applicazione può modificare liberamente il contenuto dell'elenco collegato a cui punta da pInOut, ma non deve modificare nessuno dei puntatori pNext , quindi solo il collegamento di primo livello. Se l'applicazione decide di abbreviare l'elenco collegato, non può sapere se un puntatore pNext specificato collega un buffer interno RPC o un buffer allocato in modo specifico con MIDL_user_allocate(). Per risolvere questo problema, aggiungere una dichiarazione di tipo specifica per i puntatori di elenco collegati che forzano l'allocazione dell'utente, come illustrato nel codice seguente.

typedef [force_allocate] PLINKEDLIST;

Questo attributo impone al server stub di allocare ogni nodo dell'elenco collegato separatamente e l'applicazione può liberare la parte abbreviata dell'elenco collegato chiamando MIDL_user_free(). L'applicazione può quindi impostare in modo sicuro il puntatore pNext alla fine dell'elenco collegato appena abbreviato su NULL.