Freigeben über


Server-Stub-Speicherverwaltung

Einführung in Server-Stub Speicherverwaltung

MIDL-generierte Stubs fungieren als Schnittstelle zwischen einem Clientprozess und einem Serverprozess. Ein Clientstub marshallt alle Daten, die an parameter übergeben werden, die mit dem [ in] -Attribut gekennzeichnet sind, und sendet sie an den Serverstub. Beim Empfang dieser Daten rekonstruiert der Serverstub den Aufrufstapel und führt dann die entsprechende vom Benutzer implementierte Serverfunktion aus. Der Serverstub marshallt auch die mit dem Attribut [out] markierten Parameterdaten und gibt sie an die Clientanwendung zurück.

Das von MSRPC verwendete 32-Bit-Marshalldatenformat ist eine kompatible Version der NDR-Übertragungssyntax (Network Data Representation). Weitere Informationen zu diesem Format finden Sie unter The Open Group-Website. Für 64-Bit-Plattformen kann eine Microsoft-64-Bit-Erweiterung für die NDR-Übertragungssyntax namens NDR64 verwendet werden, um eine bessere Leistung zu erzielen.

Aufheben desMarmarshalings eingehender Daten

In MSRPC marshallt der Client-Stub alle als [in] markierten Parameterdaten in einem kontinuierlichen Puffer für die Übertragung an den Serverstub. Ebenso marshallt der Server-Stub alle Daten, die mit dem Attribut [out] markiert sind, in einem fortlaufenden Puffer für die Rückgabe an den Client-Stub. Während die Netzwerkprotokollschicht unterhalb von RPC den Puffer für die Übertragung fragmentieren und paketieren kann, ist die Fragmentierung für die RPC-Stubs transparent.

Die Speicherzuweisung zum Erstellen des Serveraufrufrahmens kann ein teurer Vorgang sein. Der Serverstub versucht, die unnötige Arbeitsspeicherauslastung nach Möglichkeit zu minimieren, und es wird davon ausgegangen, dass die Serverroutine daten, die mit den Attributen [in] oder [in, out] gekennzeichnet sind, nicht freigibt oder neu zugeordnet. Der Server-Stub versucht, Daten im Puffer nach Möglichkeit wiederzuverwenden, um unnötige Duplizierungen zu vermeiden. Die allgemeine Regel lautet: Wenn das gemarstete Datenformat mit dem Speicherformat übereinstimmt, verwendet RPC Zeiger auf die ge marshallten Daten, anstatt zusätzlichen Arbeitsspeicher für identisch formatierte Daten zuzuweisen.

Der folgende RPC-Aufruf wird beispielsweise mit einer Struktur definiert, deren Marshallformat mit dem speicherinternen Format identisch ist.

typedef struct RpcStructure
{
    long val;
    long val2;
}

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

In diesem Fall weist RPC keinen zusätzlichen Arbeitsspeicher für die Daten zu, auf die von plInStructure verwiesen wird. stattdessen wird der Zeiger auf die gemarsten Daten einfach an die serverseitige Funktionsimplementierung übergeben. Der RPC-Server-Stub überprüft den Puffer während des Entmarshalings, wenn der Stub mithilfe des Flags "-robust" kompiliert wird (dies ist eine Standardeinstellung in der neuesten Version des MIDL-Compilers). RPC garantiert, dass die an die serverseitige Funktionsimplementierung übergebenen Daten gültig sind.

Beachten Sie, dass arbeitsspeicher für plOutStructure zugewiesen ist, da keine Daten an den Server übergeben werden.

Speicherzuordnung für eingehende Daten

Es kann vorkommen, dass der Serverstub Arbeitsspeicher für Parameterdaten zuweist, die mit den Attributen [in] oder [in, out] gekennzeichnet sind. Dies tritt auf, wenn sich das gemarselte Datenformat vom Speicherformat unterscheidet oder wenn die Strukturen, aus denen die gemarsten Daten bestehen, ausreichend komplex sind und vom RPC-Server-Stub atomar gelesen werden müssen. Im Folgenden sind mehrere häufige Fälle aufgeführt, in denen Arbeitsspeicher für daten zugewiesen werden muss, die vom Server-Stub empfangen werden.

  • Die Daten sind ein unterschiedliches Array oder ein konformes unterschiedliches Array. Hierbei handelt es sich um Arrays (oder Zeiger auf Arrays), für die das Attribut [length_is()] oder [first_is()] festgelegt ist. In NDR wird nur das erste Element dieser Arrays gemarst und übertragen. Im folgenden Codeausschnitt wird beispielsweise für die im Parameter pv übergebenen Daten Arbeitsspeicher zugewiesen.

    void RpcFunction
    (
        [in] long size,
        [in, out] long *pLength,
        [in, out, size_is(size), length_is(*pLength)] long *pv
    );
    
  • Die Daten sind eine große Zeichenfolge oder eine nicht konforme Zeichenfolge. Diese Zeichenfolgen sind in der Regel Zeiger auf Zeichendaten, die mit dem Attribut [size_is()] markiert sind. Im folgenden Beispiel wird für die Zeichenfolge, die an die serverseitige Funktion SizedString übergeben wird, Arbeitsspeicher zugewiesen, während die an die NormalString-Funktion übergebene Zeichenfolge wiederverwendet wird.

    void SizedString
    (
        [in] long size,
        [in, size_is(size), string] char *str
    );
    
    void NormalString
    (
        [in, string] char str
    );
    
  • Bei den Daten handelt es sich um einen einfachen Typ, dessen Arbeitsspeichergröße sich von der gemarshallten Größe unterscheidet, z. B. enum16 und __int3264.

  • Die Daten werden durch eine Struktur definiert, deren Speicherausrichtung kleiner als die natürliche Ausrichtung ist, einen der oben genannten Datentypen enthält oder eine nachfolgende Bytefüllung aufweist. Die folgende komplexe Datenstruktur hat beispielsweise eine 2-Byte-Ausrichtung erzwungen und verfügt über eine Auffüllung am Ende.

#pragma Pack(2) typedef struct ComplexPackedStructure { char c;
long l; Die Ausrichtung wird am zweiten Bytezeichen c2 erzwungen; Es wird ein nachfolgendes Ein-Byte-Pad vorhanden sein, um die Ausrichtung von 2 Byte } ''' beizubehalten.

  • Die Daten enthalten eine Struktur, die Feld für Feld gemarst werden muss. Diese Felder umfassen Schnittstellenzeiger, die in DCOM-Schnittstellen definiert sind. Ignorierte Zeiger; ganzzahlige Werte, die mit dem [range] -Attribut festgelegt sind; Elemente von Arrays, die mit den Attributen [wire_marshal], [user_marshal], [transmit_as] und [represent_as] definiert sind; und eingebettete komplexe Datenstrukturen.
  • Die Daten enthalten eine Union, eine Struktur, die eine Union enthält, oder ein Array von Gewerkschaften. Nur der spezifische Zweig der Union wird auf dem Draht gemarst.
  • Die Daten enthalten eine Struktur mit einem mehrdimensionalen konformen Array, das mindestens eine nicht feste Dimension aufweist.
  • Die Daten enthalten ein Array komplexer Strukturen.
  • Die Daten enthalten ein Array einfacher Datentypen wie enum16 und __int3264.
  • Die Daten enthalten ein Array von Ref- und Schnittstellenzeigern.
  • Die Daten verfügen über ein [force_allocate] -Attribut, das auf einen Zeiger angewendet wird.
  • Die Daten verfügen über ein [allocate(all_nodes)]- Attribut, das auf einen Zeiger angewendet wird.
  • Die Daten verfügen über ein [byte_count] -Attribut, das auf einen Zeiger angewendet wird.

64-Bit-Daten und NDR64-Übertragungssyntax

Wie bereits erwähnt, werden 64-Bit-Daten mithilfe einer bestimmten 64-Bit-Übertragungssyntax namens NDR64 ge marshallt. Diese Übertragungssyntax wurde entwickelt, um das spezifische Problem zu beheben, das auftritt, wenn Zeiger unter 32-Bit-NDR gemarst und an einen Server-Stub auf einer 64-Bit-Plattform übertragen werden. In diesem Fall stimmt ein gemarselter 32-Bit-Datenzeiger nicht mit einem 64-Bit-Datenzeiger überein, und die Speicherzuordnung tritt ausnahmslos auf. Um ein konsistentes Verhalten auf 64-Bit-Plattformen zu erstellen, hat Microsoft eine neue Übertragungssyntax namens NDR64 entwickelt.

Dieses Problem wird in einem Beispiel veranschaulicht:

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

Diese Struktur wird beim Marshallen vom Serverstub auf einem 32-Bit-System wiederverwendet. Wenn sich der Serverstub jedoch auf einem 64-Bit-System befindet, sind die NDR-gemarselten Daten 4 Bytes lang, aber die erforderliche Arbeitsspeichergröße beträgt 8. Daher wird die Speicherzuordnung erzwungen, und die Pufferwiederverwendung erfolgt selten. NDR64 behebt dieses Problem, indem die gemarselte Größe eines Zeigers 64-Bit festgelegt wird.

Im Gegensatz zu 32-Bit-NDR machen einfache Daten tyes wie enum16 und __int3264 eine Struktur oder ein Array unter NDR64 nicht komplex. Ebenso machen nachfolgende Padwerte eine Struktur nicht komplex. Schnittstellenzeiger werden auf der obersten Ebene als eindeutige Zeiger behandelt. Daher werden Strukturen und Arrays, die Schnittstellenzeiger enthalten, nicht als komplex betrachtet und erfordern für ihre Verwendung keine spezifische Speicherzuordnung.

Initialisieren ausgehender Daten

Nachdem alle eingehenden Daten aufgehoben wurden, muss der Serverstub die nur ausgehenden Zeiger initialisieren, die mit dem Attribut [out] markiert sind.

typedef struct RpcStructure
{
    long val;
    long val2;
}

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

Im obigen Aufruf muss der Serverstub plOutStructure initialisieren, da er nicht in den ge marshallten Daten vorhanden war, und es sich um einen impliziten [ref] -Zeiger handelt, der für die Serverfunktionsimplementierung zur Verfügung gestellt werden muss. Der RPC-Serverstub initialisiert und nulliert alle Verweiszeiger der obersten Ebene mit dem Attribut [out] auf null. Alle [out]- Verweiszeiger darunter werden ebenfalls rekursiv initialisiert. Die Rekursion stoppt bei allen Zeigern, auf denen die Attribute [eindeutig] oder [ptr] festgelegt sind.

Die Serverfunktionsimplementierung kann zeigerwerte der obersten Ebene nicht direkt ändern und kann sie daher nicht neu zuordnen. In der obigen Implementierung von ProcessRpcStructure ist beispielsweise der folgende Code ungültig:

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

plOutStructure ist ein Stapelwert, dessen Änderung nicht an RPC weitergegeben wird. Die Implementierung der Serverfunktion kann versuchen, die Zuordnung zu vermeiden, indem sie versucht, plOutStructure freizugeben, was zu Einer Speicherbeschädigung führen kann. Der Serverstub weist dann Speicherplatz für den Zeiger der obersten Ebene im Arbeitsspeicher (im Fall von Zeiger auf Zeiger) und eine einfache Struktur der obersten Ebene zu, deren Größe auf dem Stapel kleiner als erwartet ist.

Der Client kann unter bestimmten Umständen die Speicherbelegungsgröße der Serverseite angeben. Im folgenden Beispiel gibt der Client die Größe der ausgehenden Daten im Parameter für eingehende Größe an.

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

Nach dem Aufheben des Zusammenfügens der eingehenden Daten, einschließlich der Größe, weist der Serverstub einen Puffer für pv mit der Größe "sizeof(char)*size" zu. Nachdem der Speicherplatz zugewiesen wurde, stellt der Serverstub den Puffer auf Null. Beachten Sie, dass in diesem fall der Stub den Arbeitsspeicher mit MIDL_user_allocate()ordnet, da die Größe des Puffers zur Laufzeit bestimmt wird.

Beachten Sie, dass bei DCOM-Schnittstellen die von MIDL generierten Stubs möglicherweise überhaupt nicht beteiligt sind, wenn Client und Server dasselbe COM-Apartment teilen oder ICallFrame implementiert ist. In diesem Fall kann der Server nicht vom Zuordnungsverhalten abhängen und muss den Clientspeicher unabhängig überprüfen.

Serverseitige Funktionsimplementierungen und Outbound Data Marshaling

Unmittelbar nach der Entmarshallung für eingehende Daten und der Initialisierung des Arbeitsspeichers, der für ausgehende Daten zugewiesen ist, führt der RPC-Serverstub die serverseitige Implementierung der vom Client aufgerufenen Funktion aus. Zu diesem Zeitpunkt kann der Server die daten ändern, die speziell mit dem Attribut [in, out] gekennzeichnet sind, und er kann den Speicher auffüllen, der für nur ausgehende Daten zugewiesen ist (die mit [out]) markierten Daten).

Die allgemeinen Regeln für die Bearbeitung von Marshallparameterdaten sind einfach: Der Server kann nur neuen Arbeitsspeicher zuweisen oder den speziell vom Server-Stub zugewiesenen Arbeitsspeicher ändern. Die Neuzuweisung oder Freigabe des vorhandenen Arbeitsspeichers für Daten kann sich negativ auf die Ergebnisse und die Leistung des Funktionsaufrufs auswirken und kann sehr schwierig zu debuggen sein.

Logischerweise befindet sich der RPC-Server in einem anderen Adressraum als der Client, und es kann im Allgemeinen davon ausgegangen werden, dass er keinen gemeinsamen Arbeitsspeicher verwendet. Daher ist es für die Serverfunktionsimplementierung sicher, die daten zu verwenden, die mit dem [ in] -Attribut als "Scratch"-Arbeitsspeicher gekennzeichnet sind, ohne dass sich dies auf die Clientspeicheradressen auswirkt. Allerdings sollte der Server nicht versuchen, Daten neu zuzulagern oder freizugeben , sodass die Kontrolle über diese Leerzeichen dem RPC-Server-Stub selbst überlassen wird.

Im Allgemeinen muss die Serverfunktionsimplementierung keine Mit dem Attribut [in, out] markierten Daten neu zugeordnet oder freigegeben werden. Bei Daten mit fester Größe kann die Logik der Funktionsimplementierung die Daten direkt ändern. Ebenso darf die Funktionsimplementierung für Daten mit variabler Größe auch nicht den Feldwert ändern, der für das [size_is()] -Attribut bereitgestellt wird. Wenn Sie den Feldwert ändern, der für die Größe der Daten verwendet wird, wird ein kleinerer oder größerer Puffer an den Client zurückgegeben, der möglicherweise nicht für die ungewöhnliche Länge geeignet ist.

Wenn Situationen auftreten, in denen die Serverroutine den Speicher neu zuordnen muss, der von Daten verwendet wird, die mit dem Attribut [in, out] gekennzeichnet sind, ist es durchaus möglich, dass die serverseitige Funktionsimplementierung nicht weiß, ob der vom Stub bereitgestellte Zeiger auf den speicher ist , der mit MIDL_user_allocate() oder dem Gemarstdrahtpuffer zugeordnet ist. Um dieses Problem zu umgehen, kann MS RPC sicherstellen, dass kein Speicherverlust oder eine Beschädigung auftritt, wenn das Attribut [force_allocate] für die Daten festgelegt ist. Wenn [force_allocate] festgelegt ist, weist der Serverstub immer Arbeitsspeicher für den Zeiger zu, obwohl die Einschränkung besteht, dass die Leistung bei jeder Verwendung abnimmt.

Wenn der Aufruf von der serverseitigen Funktionsimplementierung zurückgegeben wird, verteilt der Serverstub die mit dem Attribut [out] markierten Daten und sendet sie an den Client. Beachten Sie, dass der Stub die Daten nicht marshallt, wenn die serverseitige Funktionsimplementierung eine Ausnahme auslöst.

Freigeben des zugewiesenen Arbeitsspeichers

Der RPC-Serverstub gibt den Stapelspeicher frei, nachdem der Aufruf von der serverseitigen Funktion zurückgegeben wurde, unabhängig davon, ob eine Ausnahme auftritt oder nicht. Der Serverstub gibt den gesamten vom Stub zugewiesenen Arbeitsspeicher sowie den mit MIDL_user_allocate()zugeordneten Arbeitsspeicher frei. Die serverseitige Funktionsimplementierung muss RPC immer einen konsistenten Zustand verleihen, indem entweder eine Ausnahme ausgelöst oder ein Fehlercode zurückgegeben wird. Wenn die Funktion während der Auffüllung komplizierter Datenstrukturen fehlschlägt, muss sie sicherstellen, dass alle Zeiger auf gültige Daten zeigen oder auf NULL festgelegt sind.

Während dieses Durchlaufs gibt der Serverstub den gesamten Arbeitsspeicher frei, der nicht Teil des gemarselten Puffers ist, der die [in] -Daten enthält. Eine Ausnahme von diesem Verhalten sind Daten, für die das Attribut [allocate(dont_free)] festgelegt ist. Der Serverstub gibt keinen Speicher frei, der diesen Zeigern zugeordnet ist.

Nachdem der Serverstub den durch den Stub und die Funktionsimplementierung zugewiesenen Arbeitsspeicher freigegeben hat, ruft der Stub eine bestimmte Benachrichtigungsfunktion auf, wenn das [notify_flag] -Attribut für bestimmte Daten angegeben ist.

Marshallen einer verknüpften Liste über RPC – Ein Beispiel

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

Im obigen Beispiel ist das Speicherformat für LINKEDLIST identisch mit dem Marshall-Wire-Format. Daher weist der Serverstub keinen Arbeitsspeicher für die gesamte Kette von Datenzeigern unter pIn zu. Stattdessen verwendet RPC den Drahtpuffer für die gesamte verknüpfte Liste wieder. Ebenso weist der Stub keinen Arbeitsspeicher für pInOut zu, sondern verwendet stattdessen den vom Client gemarsten Drahtpuffer wieder.

Da die Funktionssignatur den ausgehenden Parameter pOut enthält, weist der Serverstub Arbeitsspeicher zu, um die zurückgegebenen Daten zu enthalten. Der zugewiesene Arbeitsspeicher wird zunächst auf Null gesetzt, wobei pNext auf NULL festgelegt ist. Die Anwendung kann den Arbeitsspeicher für eine neue verknüpfte Liste zuordnen und pOut-pNächst> darauf verweisen.pIn und die darin enthaltene verknüpfte Liste können als Scratchbereich verwendet werden, aber die Anwendung sollte keine pNext-Zeiger ändern.

Die Anwendung kann den Inhalt der verknüpften Liste, auf die pInOut verweist, frei ändern, darf aber keinen der pNext-Zeiger ändern, geschweige denn den Link der obersten Ebene selbst. Wenn die Anwendung beschließt, die verknüpfte Liste zu verkürzen, kann sie nicht wissen, ob ein gegebener pNext-Zeiger t mit einem internen RPC-Puffer oder einem Puffer verknüpft ist , der speziell mit MIDL_user_allocate()zugeordnet ist. Um dieses Problem zu umgehen, fügen Sie eine bestimmte Typdeklaration für verknüpfte Listenzeiger hinzu, die die Benutzerzuordnung erzwingt, wie im folgenden Code dargestellt.

typedef [force_allocate] PLINKEDLIST;

Dieses Attribut zwingt den Serverstub, jeden Knoten der verknüpften Liste separat zuzuordnen, und die Anwendung kann den gekürzten Teil der verknüpften Liste freigeben, indem sie MIDL_user_free()aufruft. Die Anwendung kann dann den pNext-Zeiger am Ende der neu gekürzten verknüpften Liste sicher auf NULL festlegen.