Verwaltung des Serverstubspeichers

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 ] markiert sind, und sendet sie an den Serverstub. Der Serverstub rekonstruiert nach dem Empfang dieser Daten die Aufrufliste und führt dann die entsprechende vom Benutzer implementierte Serverfunktion aus. Der Serverstub marshallt auch die mit dem [ ] out-Attribut markierten Parameterdaten und gibt sie an die Clientanwendung zurück.

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

Unmarshaling eingehender Daten

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

Die Speicherzuordnung zum Erstellen des Serveraufrufrahmens kann ein aufwendiger Vorgang sein. Der Serverstub versucht, die unnötige Speicherauslastung nach Möglichkeit zu minimieren, und es wird davon ausgegangen, dass die Serverroutine keine Daten frei gibt oder neu zuzeichnet, die mit den Attributen [ in ] oder [ in, out ] gekennzeichnet sind. Der Serverstub versucht nach Möglichkeit, Daten im Puffer wiederzuverwenden, um unnötige Duplizierung zu vermeiden. Die allgemeine Regel ist, dass RPC Zeiger auf die gemarshallten Daten verwendet, anstatt zusätzlichen Arbeitsspeicher für identisch formatierte Daten zu verwenden, wenn das formatierte Datenformat mit dem Speicherformat identisch ist.

Beispielsweise wird der folgende RPC-Aufruf mit einer -Struktur definiert, deren gemarshallte Format mit dem In-Memory-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 plInStructure verweist. stattdessen wird der Zeiger einfach auf die gemarshallten Daten an die serverseitige Funktionsimplementierung übergeben. Der RPC-Serverstub überprüft den Puffer während des Unmarshalingprozesses, wenn der Stub mit dem Flag "-robust" kompiliert wird (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 speicher für plOutStructure zugeordnet wird, da dafür keine Daten an den Server übergeben werden.

Speicherzuordnung für eingehende Daten

Fälle können auftreten, in denen der Serverstub Speicher für Parameterdaten zuteilen, die mit den [ Attributen in ] oder [ in, out gekennzeichnet ] sind. Dies tritt auf, wenn sich das gemarshallte Datenformat vom Speicherformat unterscheidet oder wenn die Strukturen, aus denen die gemarshallten Daten bestehen, komplex sind und vom RPC-Serverstub atomisch gelesen werden müssen. Unten sind mehrere häufige Fälle aufgeführt, in denen Arbeitsspeicher für Daten zugeordnet werden muss, die vom Serverstub empfangen werden.

  • Die Daten sind ein variierende Array oder ein konformes variierende 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 gemarshallt und übertragen. Im folgenden Codeausschnitt wird den daten, die im Parameter pv übergeben werden, beispielsweise Arbeitsspeicher zugeordnet.

    void RpcFunction
    (
        [in] long size,
        [in, out] long *pLength,
        [in, out, size_is(size), length_is(*pLength)] long *pv
    );
    
  • Die Daten sind eine Zeichenfolge mit einer Größe oder eine nicht konforme Zeichenfolge. Diese Zeichenfolgen sind in der Regel Zeiger auf Zeichendaten, die mit dem [ Attribut size _ is() gekennzeichnet ] sind. Im folgenden Beispiel wird der Zeichenfolge, die an die serverseitige SizedString-Funktion übergeben wird, Arbeitsspeicher zugeordnet, 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
    );
    
  • Die Daten sind ein einfacher Typ, dessen Arbeitsspeichergröße sich von der gemarshallten Größe unterscheidet, z. B. enum16 und _ _ int3264.

  • Die Daten werden durch eine -Struktur definiert, deren Arbeitsspeicherausrichtung kleiner als die natürliche Ausrichtung ist, einen der oben genannten Datentypen enthält oder über eine nach trailing-Byteauf padding verfügt. Die folgende komplexe Datenstruktur hat z. B. eine 2-Byte-Ausrichtung erzwungen und am Ende eine Auf padding-Struktur.

#pragma pack(2) typedef struct ComplexPackedStructure { char c;
long l; // alignment is forced at the second byte char c2; // there will be a trailing one-byte pad to keep 2-byte alignment } ```

  • Die Daten enthalten eine -Struktur, die Feld für Feld gemarshallt werden muss. Diese Felder enthalten Schnittstellenzeker, die in DCOM-Schnittstellen definiert sind. ignorierte Zeiger; ganzzahlige [ ] Werte, die mit dem Bereichsattribut festgelegt werden, Elemente von Arrays, die mit dem [ Wire _ Marshal ]definiert [ _ ] sind, [ _ ]benutzeremarshallieren, [ _ ] als Attribute übertragen und darstellen, und eingebettete komplexe Datenstrukturen.
  • Die Daten enthalten eine Union, eine Struktur, die eine Union enthält, oder ein Array von Unions. Nur der spezifische Branch der Union wird auf dem Kabel gemarshallt.
  • Die Daten enthalten eine -Struktur mit einem mehrdimensionalen konformen Array, das über mindestens eine nicht feste Dimension verfügt.
  • Die Daten enthalten ein Array komplexer Strukturen.
  • Die Daten enthalten ein Array einfacher Datentypen wie enum16 und _ _ int3264.
  • Die Daten enthalten ein Array von Verweis- und Schnittstellenze0ern.
  • Auf die Daten wird ein [ Force _ Allocate-Attribut ] angewendet, das auf einen Zeiger angewendet wird.
  • Für die Daten wird [ ein attribut allocate(all _ nodes) ] auf einen Zeiger angewendet.
  • Für die Daten wird ein [ _ Byteanzahlattribut ] auf einen Zeiger angewendet.

64-Bit-Daten- und NDR64-Übertragungssyntax

Wie bereits erwähnt, werden 64-Bit-Daten mithilfe einer bestimmten 64-Bit-Übertragungssyntax namens NDR64 marshallt. Diese Übertragungssyntax wurde entwickelt, um das spezifische Problem zu beheben, das auftritt, wenn Zeiger unter 32-Bit-NDR gemarshallt und an einen Serverstub auf einer 64-Bit-Plattform übertragen werden. In diesem Fall passt ein gemarshallter 32-Bit-Datenzeiger nicht zu einem 64-Bit-Datenzeiger, und die Speicherzuweisung erfolgt immer. Um ein konsistentes Verhalten auf 64-Bit-Plattformen zu erstellen, hat Microsoft eine neue Übertragungssyntax namens NDR64 entwickelt.

Ein Beispiel zur Veranschaulichung dieses Problems ist:

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

Wenn diese Struktur gemarshallt wird, wird sie vom Serverstub auf einem 32-Bit-System wiederverwendet. Wenn sich der Serverstub jedoch auf einem 64-Bit-System befindet, haben die NDR-gemarshallten Daten eine Länge von 4 Bytes, aber die erforderliche Arbeitsspeichergröße beträgt 8. Dies führt dazu, dass die Speicherzuweisung erzwungen wird und die Pufferwiederverwendung nur selten erfolgt. NDR64 löst dieses Problem, indem die gemarshallte Größe eines Zeigers 64 Bit beträgt.

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

Initialisieren ausgehender Daten

Nachdem alle eingehenden Daten nicht mehr vorhanden sind, muss der Serverstub 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 Serverstub plOutStructure initialisieren, da er nicht in [ ] den gemarshallten Daten vorhanden war, und es handelt sich um einen impliziten Verweiszeiger, der der Serverfunktionsimplementierung zur Verfügung gestellt werden muss. Der RPC-Serverstub initialisiert und 0 (null) aller verweisbasierten Zeiger auf oberster Ebene mit dem [ ] out-Attribut. Alle [ ] out-Verweiszeiger darunter werden auch rekursiv initialisiert. Die Rekursion hält an allen Zeigern an, für die [ die eindeutigen ] attribute oder [ ptr-Attribute ] festgelegt sind.

Die Serverfunktionsimplementierung kann Zeigerwerte der obersten Ebene nicht direkt ändern und daher nicht neu zugewiesen werden. 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, und seine Änderung wird nicht an RPC zurückverwandelt. Die Implementierung der Serverfunktion kann versuchen, die Zuordnung zu vermeiden, indem versucht wird, plOutStructure frei zu geben, was zu Speicherbeschädigungen führen kann. Der Serverstub weist dann Speicherplatz für den Zeiger der obersten Ebene im Arbeitsspeicher (im Zeiger-zu-Zeiger-Fall) 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 Speicherzuweisungsgröße der Serverseite angeben. Im folgenden Beispiel gibt der Client die Größe der ausgehenden Daten im Parameter für die eingehende Größe an.

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

Nach dem Unmarshalling der eingehenden Daten, einschließlich der Größe, ordnet der Serverstub einen Puffer für pv mit einer Größe von "sizeof(char)" * zu. Nachdem der Speicherplatz zugeordnet wurde, wird der Puffer vom Serverstub auf Null gesetzt. Beachten Sie, dass in diesem speziellen Fall der Stub den Arbeitsspeicher mit _ MIDL-Benutzer _ allocate() zuteilen kann, da die Größe des Puffers zur Laufzeit bestimmt wird.

Beachten Sie, dass im Fall von DCOM-Schnittstellen von MIDL generierte Stubs möglicherweise überhaupt nicht beteiligt sind, wenn client und server das gleiche COM-Apartment verwenden oder ICallFrame implementiert ist. In diesem Fall kann der Server nicht vom Zuordnungsverhalten abhängig sein und muss den Arbeitsspeicher in Clientgröße unabhängig überprüfen.

Serverseitige Funktionsimplementierung und ausgehendes Datenarshalling

Unmittelbar nach dem Unmarshalling eingehender Daten und der Initialisierung des Arbeitsspeichers, 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 [ Attribut in, ] out gekennzeichnet sind, und den Arbeitsspeicher auffüllen, der nur für ausgehende Daten zugeordnet ist (die Daten, die mit out gekennzeichnet [ sind). ]

Die allgemeinen Regeln für die Bearbeitung von Marshallingparameterdaten sind einfach: Der Server kann nur neuen Arbeitsspeicher zuordnen oder den speziell vom Serverstub zugewiesenen Arbeitsspeicher ändern. Die Neuverteilung oder Freigabe von vorhandenem Arbeitsspeicher für Daten kann sich negativ auf die Ergebnisse und die Leistung des Funktionsaufrufs auswirken und kann sehr schwierig zu debuggen sein.

Logischerweise ist der RPC-Server in einem anderen Adressraum als der Client gespeichert, und im Allgemeinen kann davon ausgegangen werden, dass er keinen Arbeitsspeicher gemeinsam verwendet. Daher ist es sicher, dass die Serverfunktionsimplementierung die daten verwendet, die mit dem [ ] in-Attribut als "Scratch"-Arbeitsspeicher markiert sind, ohne die Clientspeicheradressen zu beeinträchtigen. Der Server sollte jedoch nicht [ ] versuchen, Daten neu zu erstellen oder frei zu geben, und die Kontrolle über diese Leerzeichen dem RPC-Serverstub selbst überlässt.

Im Allgemeinen muss die Serverfunktionsimplementierung keine Daten neu auffinden oder veröffentlichen, die mit dem [ Attribut in, out ] markiert sind. Bei Daten fester Größe kann die Funktionsimplementierungslogik die Daten direkt ändern. Ebenso darf die Funktionsimplementierung bei Daten variabler Größe auch nicht den Feldwert ändern, der für das [ size _ is()-Attribut ] bereitgestellt wird. Ändern Sie den Feldwert, der für die Größe der Daten verwendet wird, zu einem kleineren oder größeren Puffer, der an den Client zurückgegeben wird, der möglicherweise nicht mit der ungewöhnlichen Länge umgehen kann.

Wenn Situationen auftreten, in denen die Serverroutine den von daten, die mit dem [ in, ] out-Attribut markierten Daten verwendet werden, neu zuordnen muss, ist es möglich, dass die serverseitige Funktionsimplementierung nicht weiß, ob der vom Stub bereitgestellte Zeiger auf den mit MIDL-Benutzer _ _ allocate() oder dem gemarshallten Wire Buffer zugeordneten Arbeitsspeicher ist. Um dieses Problem zu beheben, kann MS RPC sicherstellen, dass kein Arbeitsspeicherverlust oder keine Beschädigung auftritt, wenn das [ Attribut force _ allocate ] für die Daten festgelegt ist. Wenn [ _ "Force Allocate" ] festgelegt ist, weist der Serverstub dem Zeiger immer Arbeitsspeicher zu. Der Nachteil ist jedoch, dass die Leistung bei jeder Verwendung des Zeigers abnimmt.

Wenn der Aufruf von der serverseitigen Funktionsimplementierung zurückgegeben wird, marshallt der Serverstub die mit dem [ ] out-Attribut 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 zugeordneten 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 durch den Stub belegten Arbeitsspeicher sowie den mit midl _ user _ allocate() belegten Arbeitsspeicher frei. Die serverseitige Funktionsimplementierung muss RPC immer einen konsistenten Zustand geben, 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 gemarshallten Puffers ist, der die [ in ] den Daten enthält. Eine Ausnahme dieses Verhaltens sind Daten, für die das Attribut [ allocate(don't _ free) ] festgelegt ist. Der Serverstub gibt keinen Speicher frei, der diesen Zeigern zugeordnet ist.

Nachdem der Serverstub den durch den Stub und die Funktionsimplementierungen belegten Arbeitsspeicher freigibt, ruft der Stub eine bestimmte Benachrichtigungsfunktion auf, wenn das Flagattribut [ notify _ ] 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 mit dem gemarshallten Kabelformat identisch. Daher belegt der Serverstub keinen Speicher für die gesamte Kette von Datenzeigern unter pIn. Stattdessen verwendet RPC den Kabelpuffer für die gesamte verknüpfte Liste. Auf ähnliche Weise belegt der Stub keinen Speicher für pInOut, sondern verwendet stattdessen den vom Client gemarshallten Kabelpuffer.

Da die Funktionssignatur einen ausgehenden Parameter pOut enthält, weist der Serverstub Speicher zu, der die zurückgegebenen Daten enthält. Der zugeordnete Arbeitsspeicher wird anfänglich auf Null gesetzt, wobei pNext auf NULL festgelegt ist. Die Anwendung kann den Arbeitsspeicher für eine neue verknüpfte Liste zuordnen und pOut -> pNext darauf verweisen. pIn und die darin enthaltene verknüpfte Liste können als Ab scratch-Bereich verwendet werden, die Anwendung sollte jedoch keine pNext-Zeiger ändern.

Die Anwendung kann den Inhalt der verknüpften Liste, auf die von pInOut gezeigt wird, frei ändern, darf jedoch keinen der pNext-Zeiger ändern, ganz zu vergessen den Link der obersten Ebene selbst. Wenn die Anwendung entscheidet, die verknüpfte Liste zu kürzen, kann sie nicht wissen, ob ein angegebener pNext-Zeiger mit einem internen RPC-Puffer oder einem Puffer verknüpft ist, der speziell dem MIDL-Benutzer _ _ 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 zu sehen.

typedef [force_allocate] PLINKEDLIST;

Dieses Attribut erzwingt, dass der Serverstub jeden Knoten der verknüpften Liste separat zuordnet, und die Anwendung kann den gekürzten Teil der verknüpften Liste freigeben, indem der _ MIDL-Benutzer _ free() aufgerufen wird. Die Anwendung kann dann den pNext-Zeiger am Ende der neu verkürzten verknüpften Liste sicher auf NULL festlegen.