Megosztás a következőn keresztül:


Kiszolgálói csonk memóriakezelése

Bevezetés a Server-Stub memóriakezelésbe

A MIDL által létrehozott csonkok az ügyfélfolyamat és a kiszolgálói folyamat közötti interfészként működnek. Az ügyfélcsonk az összes adatot a [in] attribútummal megjelölt paramétereknek továbbítja, és elküldi a kiszolgálói csonknak. A kiszolgálói csonk az adatok fogadása után rekonstruálja a hívásvermet, majd végrehajtja a megfelelő, felhasználó által implementált kiszolgálófüggvényt. A kiszolgálói csonk emellett a [out] attribútummal megjelölt paraméteradatokat is rögzíti, és visszaadja az ügyfélalkalmazásnak.

Az MSRPC által használt 32 bites marshaled adatformátum a hálózati adatábrázolás (NDR) átviteli szintaxisának megfelelő verziója. További információ erről a formátumról: A Csoport megnyitása webhely. A 64 bites platformok esetében az NDR64 nevű NDR-átvitel szintaxisának Microsoft 64 bites bővítménye használható a jobb teljesítmény érdekében.

Bejövő adatok leválasztása

Az MSRPC-ben az ügyfélcsomó az összes paraméteradatot [in] címkével egy folyamatos pufferben a kiszolgálói csonknak való átvitelhez. Hasonlóképpen, a kiszolgálói csonk a [out] attribútummal jelölt összes adatot egy folyamatos pufferben rögzíti az ügyfélcsomóhoz való visszatéréshez. Bár az RPC alatti hálózati protokollréteg széttöredezheti és csomagba csomagolhatja a puffert az átvitelhez, a töredezettség transzparens az RPC-csonkok számára.

A kiszolgálóhívási keret létrehozásához szükséges memóriafoglalás költséges művelet lehet. A kiszolgálói csonk lehetőség szerint megkísérli minimalizálni a szükségtelen memóriahasználatot, és feltételezzük, hogy a kiszolgálói rutin nem szabadít fel vagy helyez át [in] vagy [in, out] attribútumokkal jelölt adatokat. A kiszolgálói csonk lehetőség szerint megpróbálja újra felhasználni a pufferben lévő adatokat a szükségtelen duplikációk elkerülése érdekében. Az általános szabály az, hogy ha a megőrzött adatformátum megegyezik a memóriaformátummal, az RPC a rendező adatokra mutató mutatókat használ ahelyett, hogy további memóriát ad az azonos formátumú adatokhoz.

A következő RPC-hívás például olyan struktúrával van definiálva, amelynek a rögzített formátuma megegyezik a memóriában lévő formátumával.

typedef struct RpcStructure
{
    long val;
    long val2;
}

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

Ebben az esetben az RPC nem foglal le további memóriát a plInStructureáltal hivatkozott adatokhoz; ehelyett egyszerűen átadja az egérmutatót a marshaled adatoknak a kiszolgálóoldali függvény implementációjának. Az RPC-kiszolgáló csonkja ellenőrzi a puffert a leválasztási folyamat során, ha a csonk a "robusztus" jelzővel van lefordítva (ez a MIDL-fordító legújabb verziójának alapértelmezett beállítása). Az RPC garantálja, hogy a kiszolgálóoldali függvény implementációjának átadott adatok érvényesek.

Vegye figyelembe, hogy a rendszer memóriát foglal le plOutStructureszámára, mivel a rendszer nem ad át adatokat a kiszolgálónak.

Memóriafoglalás bejövő adatokhoz

Előfordulhatnak olyan esetek, amikor a kiszolgálói csonk memóriát foglal le a [in] vagy [be, ki] attribútumokkal megjelölt paraméteradatokhoz. Ez akkor fordul elő, ha a marshaled adatformátum eltér a memóriaformátumtól, vagy ha a marsallt adatokat alkotó struktúrák elegendő összetettek, és az RPC-kiszolgáló csonkjának atomi olvasást kell végeznie. Az alábbiakban felsorolunk néhány gyakori esetet, amikor a kiszolgálói csonk által fogadott adatokhoz memóriát kell lefoglalni.

  • Az adatok változó tömbök vagy egy változó tömbök. Ezek olyan tömbök (vagy tömbökre mutató mutatók), amelyeken a [length_is()] vagy [first_is()] attribútum van beállítva. Az NDR-ben a rendszer csak ezeknek a tömböknek az első elemét továbbítja és továbbítja. Az alábbi kódrészletben például a pv paraméterben átadott adatokhoz memória lesz lefoglalva.

    void RpcFunction
    (
        [in] long size,
        [in, out] long *pLength,
        [in, out, size_is(size), length_is(*pLength)] long *pv
    );
    
  • Az adatok méretes vagy nem megfelelő sztringek. Ezek a sztringek általában a [size_is()] attribútummal címkézett karakteradatokra mutatnak. Az alábbi példában a SizedString kiszolgálóoldali függvénynek átadott sztring memóriával rendelkezik, míg a NormalString függvénynek átadott sztring újra felhasználható lesz.

    void SizedString
    (
        [in] long size,
        [in, size_is(size), string] char *str
    );
    
    void NormalString
    (
        [in, string] char str
    );
    
  • Az adatok olyan egyszerű típusok, amelyek memóriamérete eltér a marshaled méretétől, például enum16 és __int3264.

  • Az adatokat olyan struktúra határozza meg, amelynek a memória igazítása kisebb, mint a természetes igazítás, a fenti adattípusok bármelyikét tartalmazza, vagy záró bájtpárnával rendelkezik. Az alábbi összetett adatstruktúra például 2 bájtos igazítást kényszerített, és a végén párnázott.

#pragma pack(2) typedef struct ComplexPackedStructure { char c;
hosszú l; az igazítás a második bájt karakter c2-nél van kényszerítve; lesz egy záró egy bájtos pad, hogy megtartsa a 2 bájtos igazítás } """

  • Az adatok olyan struktúrát tartalmaznak, amelyet mezőnként kell létrehozni. Ezek a mezők tartalmazzák a DCOM-felületekben definiált illesztőmutatókat; figyelmen kívül hagyott mutatók; az [tartomány] attribútummal beállított egész számértékek; a [wire_marshal], [user_marshal], [transmit_as] és [represent_as] attribútumokkal meghatározott tömbök elemei; és beágyazott összetett adatstruktúrák.
  • Az adatok egy egyesítést, egy egyesítést tartalmazó struktúrát vagy egy egyesítő tömböt tartalmaznak. Csak az unió adott ága van a vezetéken.
  • Az adatok olyan struktúrát tartalmaznak, amelynek többdimenziós megfelelő tömbje legalább egy nem rögzített dimenzióval rendelkezik.
  • Az adatok összetett struktúrák tömböt tartalmaznak.
  • Az adatok egyszerű adattípusokból álló tömböt tartalmaznak, például enum16 és __int3264.
  • Az adatok ref és interfészmutatók tömböt tartalmaznak.
  • Az adatok egy mutatóra alkalmazott [force_allocate] attribútummal vannak alkalmazva.
  • Az adatok egy mutatóra alkalmazott [allocate(all_nodes)] attribútummal vannak alkalmazva.
  • Az adatok egy mutatóra alkalmazott [byte_count] attribútummal vannak alkalmazva.

64 bites adatok és NDR64 átviteli szintaxis

Ahogy korábban említettük, a 64 bites adatok az NDR64 nevű 64 bites átviteli szintaxissal lesznek rendezve. Ezt az átviteli szintaxist úgy fejlesztettük ki, hogy elhárítsa azokat a problémákat, amelyek akkor merülnek fel, amikor a mutatókat 32 bites NDR alatt helyezik el, és egy 64 bites platformon továbbítják egy kiszolgálói csonkra. Ebben az esetben a marshaled 32 bites adatmutató nem egyezik meg a 64 bites mutatóval, és a memóriafoglalás mindig megtörténik. A 64 bites platformokon konzisztensebb viselkedés érdekében a Microsoft kifejlesztett egy új, NDR64 nevű átviteli szintaxist.

A problémát szemléltető példa a következő:

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

Ezt a struktúrát a kiszolgálói csonk egy 32 bites rendszeren fogja újra felhasználni. Ha azonban a kiszolgálói csonk egy 64 bites rendszeren található, az NDR által felügyelt adatok 4 bájt hosszúak, de a szükséges memóriaméret 8 lesz. Ennek eredményeképpen a memóriafoglalás kényszerítve van, és a puffer újrafelhasználása ritkán fordul elő. Az NDR64 úgy oldja meg ezt a problémát, hogy a mutató 64 bites lesz.

A 32 bites NDR-sel ellentétben az egyszerű adattűnések, például enum16 és __int3264 nem teszik összetettsé a struktúrát vagy a tömböt az NDR64 alatt. Hasonlóképpen, a záró pad értékei nem teszik bonyolultsá a szerkezetet. Az illesztőmutatók a felső szinten egyedi mutatóként vannak kezelve; Ennek eredményeképpen az illesztőmutatókat tartalmazó struktúrák és tömbök nem tekinthetők összetettnek, és nem igényelnek konkrét memóriafoglalást a használatukhoz.

Kimenő adatok inicializálása

Az összes bejövő adat kibontása után a kiszolgálói csonknak inicializálnia kell a [kimenő] attribútummal megjelölt csak kimenő mutatókat.

typedef struct RpcStructure
{
    long val;
    long val2;
}

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

A fenti hívásban a kiszolgálói csonknak inicializálnia kell plOutStructure, mert nem volt jelen a marshaled adatokban, és ez egy hallgatólagos [ref] mutató, amelyet elérhetővé kell tenni a kiszolgálófüggvény implementációja számára. Az RPC-kiszolgáló csonkja inicializálja és nulláz minden legfelső szintű hivatkozási mutatót a [out] attribútummal. Az alatta található [ki] referenciamutatók is rekurzívan inicializálódnak. A rekurzió minden olyan mutatónál leáll, amelyen a [egyedi] vagy [ptr] attribútumok vannak beállítva.

A kiszolgálófüggvény implementációja közvetlenül nem módosíthatja a felső szintű mutató értékeit, ezért nem tudja újratelepíteni őket. Például a fenti ProcessRpcStructure implementációjában a következő kód érvénytelen:

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

plOutStructure veremérték, és a módosítás nem lesz újra propagálása RPC-re. A kiszolgálófüggvény implementációja megpróbálhatja elkerülni a lefoglalást, ha megkísérli felszabadítani plOutStructure, ami memóriasérülést okozhat. A kiszolgálócsomópont ezután helyet foglal le a legfelső szintű mutató számára a memóriában (a mutató–mutató esetén), valamint egy olyan felső szintű egyszerű struktúrához, amelynek mérete a veremen kisebb a vártnál.

Az ügyfél bizonyos körülmények között megadhatja a kiszolgálóoldal memóriafoglalási méretét. Az alábbi példában az ügyfél megadja a kimenő adatok méretét a bejövő paraméterben.

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

A bejövő adatok (beleértve méret) kibontása után a kiszolgálói csonk egy puffert foglal le pv "sizeof(char)*size" mérettel. A hely lefoglalása után a kiszolgáló csonkja nullára állítja a puffert. Vegye figyelembe, hogy ebben a konkrét esetben a csonk MIDL_user_allocate()foglalja le a memóriát, mivel a puffer mérete futásidőben van meghatározva.

Vegye figyelembe, hogy DCOM-interfészek esetén előfordulhat, hogy a MIDL által generált csonkok egyáltalán nem vesznek részt, ha az ügyfél és a kiszolgáló ugyanazt a COM-lakást használja, vagy ha ICallFrame implementálva van. Ebben az esetben a kiszolgáló nem függhet a foglalási viselkedéstől, és függetlenül kell ellenőriznie az ügyfélméretű memóriát.

Kiszolgálóoldali függvények implementációi és kimenő adat-marshaling

Közvetlenül azután, hogy a bejövő adatok nem jelennek meg, és a kimenő adatokat tartalmazó memória inicializálása után az RPC-kiszolgáló csonkja végrehajtja az ügyfél által hívott függvény kiszolgálóoldali implementációját. A kiszolgáló jelenleg módosíthatja a kifejezetten a [be, ki] attribútummal megjelölt adatokat, és feltöltheti a csak kimenő adatokhoz lefoglalt memóriát (a [kimenő]címkével ellátott adatokat).

A rendező paraméteradatok kezelésének általános szabályai egyszerűek: a kiszolgáló csak új memóriát foglalhat le, vagy módosíthatja a kiszolgálói csonk által lefoglalt memóriát. Az adatok meglévő memóriájának újraelosztása vagy felszabadítása negatív hatással lehet a függvényhívás eredményeire és teljesítményére, és nagyon nehéz lehet hibakeresést végezni.

Logikailag az RPC-kiszolgáló más címtérben él, mint az ügyfél, és általában feltételezhető, hogy nem osztják meg a memóriát. Ennek eredményeképpen a kiszolgálói függvény implementációja biztonságosan használhatja a [in] attribútummal megjelölt adatokat "semmis" memóriaként anélkül, hogy ez befolyásolná az ügyfél memóriacímeit. Ennek ellenére a kiszolgálónak nem szabad [in] adatokat újratelepítenie vagy felszabadítani, a szóközök vezérlését pedig magának az RPC-kiszolgálócsomónak kell hagynia.

A kiszolgálófüggvény implementációjának általában nem kell áttelepítenie vagy felszabadítania a [be, ki] attribútummal megjelölt adatokat. Rögzített méretű adatok esetén a függvény implementálási logikája közvetlenül módosíthatja az adatokat. Hasonlóképpen, a változó méretű adatok esetében a függvény implementációja sem módosíthatja a [size_is()] attribútumnak megadott mezőértéket. Módosítsa az adatok méretének mezőértékét, és az ügyfélnek visszaadott kisebb vagy nagyobb puffert eredményez, amely esetleg nem alkalmas a rendellenes hossz kezelésére.

Ha olyan körülmények merülnek fel, amikor a kiszolgálói rutinnak újra kell helyeznie a [in, out] attribútummal megjelölt adatok által használt memóriát, teljesen lehetséges, hogy a kiszolgálóoldali függvény implementációja nem fogja tudni, hogy a csonk által biztosított mutató a MIDL_user_allocate() vagy a vezetékes pufferrel lefoglalt memóriára mutat-e. A probléma megoldásához az MS RPC biztosíthatja, hogy ne történjen memóriavesztés vagy sérülés, ha a [force_allocate] attribútum be van állítva az adatokon. Ha [force_allocate] be van állítva, a kiszolgálói csonk mindig lefoglalja a mutató memóriáját, bár a kikötés az, hogy a teljesítmény minden használatnál csökken.

Amikor a hívás a kiszolgálóoldali függvény implementációjából tér vissza, a kiszolgáló csonkja a [out] attribútummal jelölt adatokat attribútummal továbbítja az ügyfélnek. Vegye figyelembe, hogy a csonk nem menti az adatokat, ha a kiszolgálóoldali függvény megvalósítása kivételt okoz.

Lefoglalt memória felszabadítása

Az RPC-kiszolgáló csonkja a hívás kiszolgálóoldali függvényből való visszatérése után felszabadítja a verem memóriáját, függetlenül attól, hogy kivétel történik-e vagy sem. A kiszolgálói csonk felszabadítja a csonk által lefoglalt összes memóriát, valamint a MIDL_user_allocate()lefoglalt memóriát. A kiszolgálóoldali függvények implementálásának mindig konzisztens állapotot kell biztosítania az RPC-nek egy kivétel beírásával vagy hibakód visszaadásával. Ha a függvény a bonyolult adatstruktúrák sokasága során meghiúsul, győződjön meg arról, hogy az összes mutató érvényes adatokra mutat, vagy a NULL van beállítva.

Ezen átengedés során a kiszolgálói csonk felszabadít minden memóriát, amely nem része a [in] adatokat tartalmazó marshaled puffernek. Ez alól a viselkedés alól kivételt képeznek a [allocate(dont_free)] attribútummal rendelkező adatok – a kiszolgálói csonk nem szabadít fel memóriát ezekhez a mutatókhoz.

Miután a kiszolgálói csonk felszabadította a csonk által lefoglalt memóriát és a függvény implementációját, a csonk egy adott értesítési függvényt hív meg, ha az adott adatokhoz meg van adva a [notify_flag] attribútum.

Csatolt lista rendezése RPC-n keresztül – Példa

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

A fenti példában a LINKEDLIST memóriaformátuma megegyezik a kötött vezeték formátumával. Ennek eredményeképpen a kiszolgálói csonk nem foglal le memóriát a pInalatt található adatmutatók teljes láncához. Ehelyett az RPC újra felhasználja a vezetékpuffert a teljes csatolt lista esetében. Hasonlóképpen, a csonk nem foglal le memóriát pInOutszámára, hanem újra felhasználja az ügyfél által rögzített vezetékpuffert.

Mivel a függvény aláírása egy kimenő paramétert tartalmaz, pOut, a kiszolgálói csonk memóriát foglal le a visszaadott adatokhoz. A lefoglalt memória kezdetben nullára van állítva, és pNextNULLértékre van állítva. Az alkalmazás lefoglalhatja a memóriát egy új csatolt lista számára, és pont pOut->pNext hozzá. pIn és a benne található csatolt lista használható üres területként, de az alkalmazás nem módosíthatja a pNext mutatóit.

Az alkalmazás szabadon módosíthatja az pInOutáltal mutatott csatolt lista tartalmát, de nem módosíthatja a pNext mutatóit, nem beszélve magáról a legfelső szintű hivatkozásról. Ha az alkalmazás úgy dönt, hogy lerövidíti a csatolt listát, nem tudja, hogy egy adott pNext mutató egy belső RPC-pufferhez vagy egy kifejezetten MIDL_user_allocate()lefoglalt pufferhez kapcsolódik-e. A probléma megoldásához adjon hozzá egy adott típusdeklarációt a csatolt listamutatókhoz, amelyek kényszerítik a felhasználó lefoglalását, ahogyan az az alábbi kódban látható.

typedef [force_allocate] PLINKEDLIST;

Ez az attribútum arra kényszeríti a kiszolgálói csonkot, hogy külön foglalja le a csatolt lista összes csomópontját, és az alkalmazás felszabadíthatja a csatolt lista rövidített részét a MIDL_user_free()meghívásával. Az alkalmazás ezután biztonságosan beállíthatja a pNext mutatót az újonnan rövidített csatolt lista végén, hogy NULL.