Server-Stub-Speicherverwaltung
Eine Einführung in Server-Stub Speicherverwaltung
MIDL-generierte Stubs fungieren als Schnittstelle zwischen einem Clientprozess und einem Serverprozess. Ein Client stub marshallt alle Daten, die an Parameter übergeben werden, die mit dem [in] Attribut gekennzeichnet sind, und sendet sie an den Server-Stub. Der Server-Stub rekonstruiert beim Empfang dieser Daten den Aufrufstapel und führt dann die entsprechende vom Benutzer implementierte Serverfunktion aus. Der Server-Stub marshallt auch die Parameterdaten, die mit dem [out] Attribut gekennzeichnet sind, und gibt sie an die Clientanwendung zurück.
Das von MSRPC verwendete 32-Bit-Marshall-Datenformat ist eine kompatible Version der NDR-Übertragungssyntax (Network Data Representation). Weitere Informationen zu diesem Format finden Sie unter Die Open Group-Website. Für 64-Bit-Plattformen kann eine Microsoft 64-Bit-Erweiterung auf die NDR-Übertragungssyntax namens NDR64 für eine bessere Leistung verwendet werden.
Unmarshaling eingehender Daten
In MSRPC marshallt der Client alle Parameterdaten, die als [in] gekennzeichnet sind, in einem fortlaufenden Puffer für die Übertragung an den Server stub. Ebenso übergibt der Server alle Daten, die mit dem [out] gekennzeichnet sind, Attribut in einem fortlaufenden Puffer, um zum Client-Stub zurückzukehren. Während die Netzwerkprotokollschicht unter RPC den Puffer für die Übertragung fragmentieren und packen kann, ist die Fragmentierung für die RPC-Stubs transparent.
Die Speicherzuweisung zum Erstellen des Serveraufrufframes kann ein teurer Vorgang sein. Der Server-Stub versucht, unnötige Speicherauslastung nach Möglichkeit zu minimieren, und es wird davon ausgegangen, dass die Serverroutine keine mit dem [in] oder [in, out] gekennzeichneten Daten freigibt oder neu zuordnet. Der Server-Stub versucht, Daten im Puffer nach Möglichkeit wiederzuverwenden, um unnötige Duplizierungen zu vermeiden. Die allgemeine Regel besteht darin, dass rpc Zeiger auf die gemarsteten Daten verwendet, wenn das Marshall-Datenformat identisch ist, anstatt zusätzlichen Speicher für identisch formatierte Daten zu zuordnen.
Beispielsweise wird der folgende RPC-Aufruf mit einer Struktur definiert, deren Marshal-Format 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 Speicher für die Daten zu, auf die von plInStructureverwiesen wird; Vielmehr übergibt er einfach den Zeiger an die gemarsteten Daten an die serverseitige Funktionsimplementierung. Der RPC-Server-Stub überprüft den Puffer während des Unmarshaling-Prozesses, wenn der Stub mithilfe des Flags "-robust" kompiliert wird (dies ist eine Standardeinstellung in der aktuellsten Version des MIDL-Compilers). RPC garantiert, dass die an die serverseitige Funktionsimplementierung übergebenen Daten gültig sind.
Beachten Sie, dass speicher für plOutStructurezugewiesen wird, da keine Daten an den Server übergeben werden.
Speicherzuordnung für eingehende Daten
Fälle können auftreten, in denen der Server-Stub Speicher für Parameterdaten zuordnet, die mit dem [in] oder [in, out] Attributen gekennzeichnet sind. Dies tritt auf, wenn sich das Marshal-Datenformat vom Speicherformat unterscheidet oder wenn die Strukturen, die die gemarstischen Daten umfassen, ausreichend komplex sind und vom RPC-Server-Stub atomisch gelesen werden müssen. Nachfolgend 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 Array. Hierbei handelt es sich um Arrays (oder Zeiger auf Arrays), die die [length_is()] oder [first_is()] Attributs festgelegt haben. Im NDR werden nur das erste Element dieser Arrays gemarstet und übertragen. Im folgenden Codeausschnitt werden z. B. die im Parameter übergebenen Daten pv- arbeitsspeichergebunden.
void RpcFunction ( [in] long size, [in, out] long *pLength, [in, out, size_is(size), length_is(*pLength)] long *pv );
Die Daten sind eine Zeichenfolge oder eine nicht konforme Zeichenfolge. Diese Zeichenfolgen sind in der Regel Zeiger auf Zeichendaten, die mit dem [size_is()] Attribut gekennzeichnet sind. Im folgenden Beispiel weist die an die SizedString serverseitige Funktion übergebene Zeichenfolge Arbeitsspeicher zu, 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 gemarstischen Größe unterscheidet, z. B. Enumeration16 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 nachgestellten Byteabstand aufweist. Die folgende komplexe Datenstruktur hat z. B. die Ausrichtung von 2 Byte erzwungen und den Abstand am Ende.
#pragma pack(2) typedef struct ComplexPackedStructure { char c;
long l; Die Ausrichtung wird beim zweiten Bytezeichen c2 erzwungen; es wird ein nachfolgendes Ein-Byte-Pad geben, um die Ausrichtung von 2 Byte } ''' beizubehalten.
- Die Daten enthalten eine Struktur, die nach Feld gemarstet werden muss. Zu diesen Feldern gehören Schnittstellenzeiger, die in DCOM-Schnittstellen definiert sind; ignorierte Zeiger; ganzzahlige Werte, die mit dem attribut [range] festgelegt sind; Elemente von Arrays, die mit dem [wire_marshal], [user_marshal], [transmit_as] und [represent_as] attributen definiert sind; und eingebettete komplexe Datenstrukturen.
- Die Daten enthalten eine Vereinigung, eine Struktur, die eine Gewerkschaft oder ein Array von Gewerkschaften enthält. Nur der spezifische Zweig der Vereinigung wird auf dem Draht gemarstet.
- Die Daten enthalten eine Struktur mit einem multidimensionalen 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 Bezugs- und Schnittstellenzeigern.
- Die Daten weisen ein [force_allocate] Attribut auf, das auf einen Zeiger angewendet wird.
- Die Daten haben ein [allocate(all_nodes)] Attribut auf einen Zeiger angewendet.
- Die Daten weisen ein [byte_count] Attribut auf, 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 gemarstet und an einen Server-Stub auf einer 64-Bit-Plattform übertragen werden. In diesem Fall stimmt ein gemarsteter 32-Bit-Datenzeiger nicht mit einer 64-Bit-Bit-1 überein, und die Speicherzuweisung erfolgt in jeder Hinsicht. Um ein konsistentes Verhalten auf 64-Bit-Plattformen zu schaffen, hat Microsoft eine neue Übertragungssyntax namens NDR64 entwickelt.
Ein Beispiel, das dieses Problem veranschaulicht, ist wie folgt:
typedef struct PtrStruct
{
long l;
long *pl;
}
Diese Struktur wird beim Marshallen vom Serverstub auf einem 32-Bit-System wiederverwendet. Wenn sich der Server-Stub jedoch auf einem 64-Bit-System befindet, beträgt die Länge der NDR-Marshaldaten 4 Byte, die erforderliche Speichergröße beträgt jedoch 8. Daher wird die Speicherzuweisung erzwungen, und die Pufferwiederverwendung tritt selten auf. NDR64 behebt dieses Problem, indem die gemarstische Größe eines Zeigers 64-Bit festgelegt wird.
Im Gegensatz zu 32-Bit-NDR machen einfache Datenbinden wie enum16 und __int3264 keine Struktur oder Arraykomplexe unter NDR64. Ebenso machen nachfolgende Padwerte keine Struktur komplex. Schnittstellenzeiger werden als eindeutige Zeiger auf oberster Ebene behandelt; Daher werden Strukturen und Arrays, die Schnittstellenzeiger enthalten, nicht als komplex betrachtet und erfordern keine spezifische Speicherzuweisung für ihre Verwendung.
Initialisieren ausgehender Daten
Nachdem alle eingehenden Daten entmarsiert wurden, muss der Server-Stub die nur ausgehenden Zeiger initialisieren, die mit dem [out] Attribut gekennzeichnet sind.
typedef struct RpcStructure
{
long val;
long val2;
}
void ProcessRpcStructure
(
[in] RpcStructure *plInStructure;
[out] RpcStructure *plOutStructure;
);
Im obigen Aufruf muss der Server-Stub plOutStructure initialisieren, da er nicht in den gemarstischen Daten vorhanden war, und es handelt sich um einen implizierten [ref] Zeiger, der der Serverfunktionsimplementierung zur Verfügung gestellt werden muss. Der RPC-Server initialisiert und 000 Verweise auf oberster Ebene mit dem attribut [out] auf oberster Ebene. Alle [out] Referenzzeiger darunter werden rekursiv initialisiert. Die Rekursion hält an allen Zeigern mit dem [eindeutig] oder [ptr] Attributen, die für sie festgelegt sind.
Die Serverfunktionsimplementierung kann zeigerwerte der obersten Ebene nicht direkt ändern und kann daher nicht neu zuordnen. In der Implementierung von ProcessRpcStructure oben ist beispielsweise der folgende Code ungültig:
void ProcessRpcStructure(RpcStructure *plInStructure, rpcStructure *plOutStructure)
{
plOutStructure = MIDL_user_allocate(sizeof(RpcStructure));
Process(plOutStructure);
}
plOutStructure- ein Stapelwert ist und seine Änderung nicht an RPC weitergegeben wird. Die Serverfunktionsimplementierung kann versuchen, die Zuordnung zu vermeiden, indem versucht wird, plOutStructurefreizugeben, was zu Speicherbeschädigungen führen kann. Der Serverstub weist dann Platz für den Zeiger der obersten Ebene im Arbeitsspeicher (im Zeiger-zu-Zeiger-Fall) und eine einfache Struktur auf oberster Ebene zu, deren Größe im Stapel kleiner als erwartet ist.
Der Client kann unter bestimmten Umständen die Größe der Speicherzuweisung auf der Serverseite angeben. Im folgenden Beispiel gibt der Client die Größe der ausgehenden Daten in der eingehenden Größe Parameter an.
void VariableSizeData
(
[in] long size,
[out, size_is(size)] char *pv
);
Nach dem Entmarsen der eingehenden Daten, einschließlich Größe, weist der Serverstub einen Puffer für PV- mit einer Größe von "sizeof(char)*size" zu. Nachdem der Speicherplatz zugewiesen wurde, wird der Server stub null aus dem Puffer herausgestellt. Beachten Sie, dass der Stub in diesem Fall den Speicher mit MIDL_user_allocate()zuordnet, da die Größe des Puffers zur Laufzeit bestimmt wird.
Beachten Sie, dass im Falle von DCOM-Schnittstellen möglicherweise midL-generierte Stubs überhaupt nicht beteiligt sind, wenn der Client und der Server dasselbe COM-Apartment gemeinsam nutzen oder wenn ICallFrame implementiert ist. In diesem Fall kann der Server nicht vom Zuordnungsverhalten abhängig sein und muss den Arbeitsspeicher der Clientgröße unabhängig überprüfen.
Serverseitige Funktionsimplementierungen und Outbound Data Marshaling
Unmittelbar nach dem Entmarshalling bei eingehenden Daten und der Initialisierung des Speichers, der für ausgehende Daten zugeordnet 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 [in, out] Attribut gekennzeichnet sind, und er kann den speicherauffüllen, der für nur ausgehende Daten zugeordnet ist (die mit [out] markierten Daten).
Die allgemeinen Regeln für die Manipulation von Marshall-Parameterdaten sind einfach: Der Server kann nur neuen Speicher zuweisen oder den vom Server stub speziell zugewiesenen Speicher ändern. Das Neuspeichern oder Freigeben vorhandener Speicher für Daten kann sich negativ auf die Ergebnisse und leistung des Funktionsaufrufs auswirken und kann sehr schwierig zu debuggen sein.
Logischerweise befindet sich der RPC-Server in einem anderen Adressbereich als der Client, und es kann in der Regel davon ausgegangen werden, dass sie keinen Arbeitsspeicher teilen. Daher ist es für die Serverfunktionsimplementierung sicher, die mit dem [in] gekennzeichneten Daten zu verwenden, Attribut als "Scratch"-Speicher ohne Auswirkungen auf die Clientspeicheradressen zu verwenden. Der Server sollte jedoch nicht versuchen, [in] Daten neu zuzuweisen oder freizugeben und die Kontrolle über diese Leerzeichen an den RPC-Server-Stub selbst zu lassen.
Im Allgemeinen muss die Serverfunktionsimplementierung keine mit dem [in, out] gekennzeichneten Daten neu zuordnen oder freigeben, Attribut. Bei Daten mit fester Größe kann die Funktionsimplementierungslogik die Daten direkt ändern. Ebenso darf die Funktionsimplementierung für Daten mit variabler Größe nicht den Feldwert ändern, der dem [size_is()] Attribut bereitgestellt wird. Ändern Sie den Feldwert, der verwendet wird, um die Größe der Daten zu ändern, was zu einem kleineren oder größeren Puffer führt, der an den Client zurückgegeben wird, der möglicherweise schlecht ausgestattet ist, um mit der ungewöhnlichen Länge umzugehen.
Wenn Situationen auftreten, in denen die Serverroutine den von Daten verwendeten Speicher neu zuordnen muss, der mit dem [in, out] Attribut gekennzeichnet ist, ist es völlig möglich, dass die serverseitige Funktionsimplementierung nicht weiß, ob der vom Stub bereitgestellte Zeiger auf den Speicher liegt, der mit MIDL_user_allocate() oder dem gemarsteten Drahtpuffer zugeordnet ist. Um dieses Problem zu umgehen, kann MS RPC sicherstellen, dass kein Speicherverlust oder keine Beschädigung auftritt, wenn das [force_allocate] Attributs für die Daten festgelegt ist. Wenn [force_allocate]- festgelegt ist, weist der Serverstub immer Speicher für den Zeiger zu, obwohl die Leistung für jede Verwendung verringert wird.
Wenn der Aufruf von der serverseitigen Funktionsimplementierung zurückgegeben wird, marshallt der Server die mit dem [out] gekennzeichneten Daten Attribut 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-Server-Stub gibt den Stapelspeicher frei, nachdem der Aufruf von der serverseitigen Funktion zurückgegeben wurde, unabhängig davon, ob eine Ausnahme auftritt oder nicht. Der Server-Stub gibt alle vom Stub zugewiesenen Arbeitsspeicher sowie alle Speicher frei, die mit MIDL_user_allocate()zugeordnet sind. Die serverseitige Funktionsimplementierung muss rpc immer einen konsistenten Zustand geben, entweder durch Auslösen einer Ausnahme oder Zurückgeben eines Fehlercodes. Wenn die Funktion während der Population komplizierter Datenstrukturen fehlschlägt, muss sichergestellt werden, dass alle Zeiger auf gültige Daten zeigen oder auf NULL-festgelegt sind.
Während dieses Durchlaufs gibt der Server-Stub den gesamten Speicher frei, der nicht Teil des gemarsteten Puffers ist, der die [in] Daten enthält. Eine Ausnahme dieses Verhaltens sind Daten mit dem [allocate(dont_free)] Attributs, das darauf festgelegt ist – der Server-Stub gibt keine Speicher frei, die diesen Zeigern zugeordnet sind.
Nachdem der Server-Stub den vom Stub und der Funktionsimplementierung zugeordneten Speicher freigegeben hat, ruft der Stub eine bestimmte Benachrichtigungsfunktion auf, wenn das [notify_flag] Attribut für bestimmte Daten angegeben wird.
Marshalling 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- mit dem Marshall-Drahtformat identisch. Daher weist der Server-Stub keine Speicher für die gesamte Kette von Datenzeigern unter pInzu. Stattdessen verwendet RPC den Drahtpuffer für die gesamte verknüpfte Liste wieder. Ebenso weist der Stub keinen Speicher für pInOut-zu, sondern verwendet stattdessen den Drahtpuffer, der vom Client gemarstet wird.
Da die Funktionssignatur einen ausgehenden Parameter enthält, pOut-, weist der Server-Stub Speicher zu, um die zurückgegebenen Daten zu enthalten. Der zugewiesene Speicher wird anfangs nulliert, wobei pNext- auf NULL-festgelegt ist. Die Anwendung kann den Speicher für eine neue verknüpfte Liste zuordnen und pOut-–>pNext- darauf verweisen. pIn- und die verknüpfte Liste, die sie enthält, kann als Entwurfsbereich verwendet werden, die Anwendung sollte jedoch keinen der pNext-Zeiger ändern.
Die Anwendung kann den Inhalt der verknüpften Liste, auf die durch pInOutverwiesen wird, frei ändern, darf jedoch keinen der pNext Zeiger ändern, ganz allein der Link der obersten Ebene selbst. Wenn die Anwendung entscheidet, die verknüpfte Liste zu kürzen, kann sie nicht wissen, ob eine angegebene pNext Zeiger mit einem RPC-internen 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 Benutzerzuweisung erzwingen, wie im folgenden Code dargestellt.
typedef [force_allocate] PLINKEDLIST;
Dieses Attribut erzwingt den Server-Stub, jeden Knoten der verknüpften Liste separat zuzuweisen, und die Anwendung kann den gekürzten Teil der verknüpften Liste durch Aufrufen von MIDL_user_free()freigeben. Die Anwendung kann dann den pNext Zeiger am Ende der neu gekürzten verknüpften Liste sicher auf NULL-festlegen.