共用方式為


伺服器存根記憶體管理

記憶體管理Server-Stub簡介

MIDL 產生的存根可作為用戶端進程與伺服器進程之間的介面。 用戶端存根會封送處理傳遞至以 [in] 屬性標示之參數的所有資料,並將它傳送至伺服器存根。 接收此資料時,伺服器存根會重新建構呼叫堆疊,然後執行對應的使用者實作伺服器函式。 伺服器存根也會封送處理標示 為 [out] 屬性的參數資料,並將它傳回給用戶端應用程式。

MSRPC 所使用的 32 位封送處理資料格式是符合規範的網路資料標記法版本, (NDR) 傳輸語法。 如需此格式的詳細資訊,請參閱 Open Group 網站。 針對 64 位平臺,適用于 NDR64 的 Microsoft 64 位延伸模組,稱為 NDR64 的傳輸語法可用於更佳的效能。

取消封送輸入資料

在 MSRPC 中,用戶端存根會將一個連續緩衝區中標記為 [in] 的所有參數資料封送處理,以傳輸至伺服器存根。 同樣地,伺服器存根會封送處理以連續緩衝區中 [out] 屬性標示的所有資料,以返回用戶端存根。 雖然 RPC 底下的網路通訊協定層可以片段並封包處理緩衝區以進行傳輸,但 RPC 存根會透明分散。

用於建立伺服器呼叫框架的記憶體配置可能是昂貴的作業。 伺服器存根會盡可能嘗試將不必要的記憶體使用量降到最低,並假設伺服器常式不會釋放或重新配置標示 為 [in][in, out] 屬性的資料。 伺服器存根會盡可能嘗試重複使用緩衝區中的資料,以避免不必要的重複。 一般規則是,如果封送處理的資料格式與記憶體格式相同,RPC 會使用封送處理資料的指標,而不是為相同格式的資料配置額外的記憶體。

例如,下列 RPC 呼叫是使用封送處理格式與其記憶體內部格式相同的結構來定義。

typedef struct RpcStructure
{
    long val;
    long val2;
}

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

在此情況下,RPC 不會為 plInStructure所參考的資料配置額外的記憶體;相反地,它只會將封送處理資料的指標傳遞至伺服器端函式實作。 如果存根是使用 「-robust」 旗標編譯,則 RPC 伺服器存根會在取消封存程式期間驗證緩衝區, (這是最新版 MIDL 編譯器) 中的預設設定。 RPC 保證傳遞至伺服器端函式實作的資料有效。

請注意,記憶體會配置給 plOutStructure,因為不會將任何資料傳遞給伺服器。

輸入資料的記憶體配置

當伺服器存根為以 [in][in, out] 屬性標示的參數資料配置記憶體時,可能會發生的情況。 當封送處理的資料格式與記憶體格式不同,或組成封送處理資料的結構夠複雜,而且必須由 RPC 伺服器存根以不可部分完成的方式讀取時,就會發生這種情況。 以下是數個常見的案例,當記憶體必須配置給伺服器存根所接收的資料時。

  • 資料是不同的陣列或一致性的變異陣列。 這些是陣列 (或陣列) 指標,這些陣列上已設定 [length_is () ][first_is () ] 屬性。 在 NDR 中,只會封送處理和傳輸這些陣列的第一個專案。 例如,在下面的程式碼片段中,參數 pv 中傳遞的資料會為其配置記憶體。

    void RpcFunction
    (
        [in] long size,
        [in, out] long *pLength,
        [in, out, size_is(size), length_is(*pLength)] long *pv
    );
    
  • 資料是大小字串或不符合規範的字串。 這些字串通常是以 [size_is () ] 屬性標記的字元資料指標。 在下列範例中,傳遞至 SizedString 伺服器端函式的字串會配置記憶體,而傳遞至 NormalString 函式的字串將會重複使用。

    void SizedString
    (
        [in] long size,
        [in, size_is(size), string] char *str
    );
    
    void NormalString
    (
        [in, string] char str
    );
    
  • 資料是簡單類型,其記憶體大小與其封送處理大小不同,例如 enum16__int3264

  • 資料是由記憶體對齊小於自然對齊、包含上述任何資料類型或具有尾端位元組填補的結構所定義。 例如,下列複雜資料結構已強制 2 位元組對齊,並在結尾填補。

#pragma pack (2) typedef 結構 ComplexPackedStructure { char c;
long l;對齊會在第二個位元組字元 c2 強制執行;會有一個尾端的一位元組面板,以保留 2 位元組對齊 } '''

  • 資料包含結構,必須依欄位封送處理欄位。 這些欄位包括 DCOM 介面中定義的介面指標;忽略的指標;以[range]屬性設定的整數值;使用[wire_marshal][user_marshal]、[transmit_as] 和 [represent_as]屬性定義的陣列元素;和內嵌的複雜資料結構。
  • 資料包含等位、包含等位的結構或等位陣列。 只有聯集的特定分支會線上路上封送處理。
  • 資料包含具有多維度一致性陣列且至少有一個非固定維度的結構。
  • 資料包含複雜結構的陣列。
  • 資料包含單一資料型別的陣列,例如 enum16__int3264
  • 資料包含 ref 和介面指標的陣列。
  • 資料具有套用至指標 的 [force_allocate] 屬性。
  • 資料具有套用至指標 的 [allocate (all_nodes) ] 屬性。
  • 資料具有套用至指標 的 [byte_count] 屬性。

64 位資料和 NDR64 傳輸語法

如先前所述,64 位資料會使用稱為 NDR64 的特定 64 位傳輸語法封送處理。 此傳輸語法是開發來解決在 32 位 NDR 下封送處理指標並傳輸至 64 位平臺上的伺服器存根時所發生的特定問題。 在此情況下,封送處理的 32 位資料指標與 64 位資料指標不符,而且記憶體配置不一定會發生。 為了在 64 位平臺上建立更一致的行為,Microsoft 開發了稱為 NDR64 的新傳輸語法。

說明此問題的範例如下:

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

當封送處理時,伺服器存根會在 32 位系統上重複使用此結構。 不過,如果伺服器存根位於 64 位系統上,則 NDR 封送處理的資料長度為 4 位元組,但所需的記憶體大小會是 8。 因此,會強制配置記憶體,而且很少會發生緩衝區重複使用。 NDR64 藉由將指標封送處理的大小設定為 64 位來解決此問題。

相較于 32 位 NDR,列舉 16__int3264 等簡單資料原則不會使 NDR64 下的結構或陣列變得複雜。 同樣地,尾端填補值不會讓結構變得複雜。 介面指標會被視為最上層的唯一指標;因此,包含介面指標的結構和陣列不會被視為複雜,而且不需要特定的記憶體配置以供其使用。

初始化輸出資料

解除封送所有輸入資料之後,伺服器存根必須初始化標示為 [out] 屬性的僅限輸出指標。

typedef struct RpcStructure
{
    long val;
    long val2;
}

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

在上述呼叫中,伺服器存根必須初始化 plOutStructure ,因為它不存在於封送處理的資料中,而且它是必須提供給伺服器函式實作使用的隱含 [ref] 指標。 RPC 伺服器存根會使用 [out] 屬性,初始化並零出任何最上層參考的指標。 其下方的任何 [out] 參考指標也會以遞迴方式初始化。 遞迴會停止在任何指標上設定 [unique][ptr] 屬性的任何指標。

伺服器函式實作無法直接改變最上層指標值,因此無法重新配置它們。 例如,在上述 ProcessRpcStructure 的實作中,下列程式碼無效:

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

plOutStructure 是堆疊值,而且其變更不會傳播回 RPC。 伺服器函式實作可以嘗試釋放 plOutStructure來嘗試避免配置,這可能會導致記憶體損毀。 然後,伺服器存根會配置記憶體中最上層指標的空間, (指標對指標案例中) 以及堆疊大小小於預期的最上層簡單結構。

在某些情況下,用戶端可以指定伺服器端的記憶體配置大小。 在下列範例中,用戶端會在輸入大小參數中指定輸出資料 的大小

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

取消封存輸入資料,包括 size之後,伺服器存根會為 pv 配置緩衝區,其大小為 「sizeof (char) *size」。 配置空間之後,伺服器存根會將緩衝區零出。 請注意,在此特定情況下,存根會以 MIDL_user_allocate () 配置記憶體,因為緩衝區的大小是在執行時間決定。

請注意,在 DCOM 介面的情況下,如果用戶端和伺服器共用相同的 COM Apartment,或實作 ICallFrame ,MIDL 產生的存根可能完全不涉及。 在此情況下,伺服器無法相依于配置行為,而且必須獨立驗證用戶端大小的記憶體。

伺服器端函式實作和輸出資料封送處理

緊接在輸入資料的取消封存,以及配置給包含輸出資料的記憶體初始化之後,RPC 伺服器存根會執行用戶端所呼叫之函式的伺服器端實作。 此時,伺服器可以修改以 [in, out] 屬性特別標示的資料,並填入配置給輸出專用資料的記憶體, (標記 [out] 的資料) 。

處理封送處理參數資料的一般規則很簡單:伺服器只能配置新的記憶體或修改伺服器存根所特別配置的記憶體。 重新配置或釋放資料的現有記憶體可能會對函式呼叫的結果和效能造成負面影響,而且可能非常難以偵錯。

邏輯上,RPC 伺服器位於與用戶端不同的位址空間中,而且通常假設它們不會共用記憶體。 因此,伺服器函式實作可以放心地使用標示為 [in] 屬性的資料作為「臨時」記憶體,而不會影響用戶端記憶體位址。 也就是說,伺服器不應該嘗試重新配置或釋放 [in] 資料,將這些空間的控制權保留給 RPC 伺服器存根本身。

一般而言,伺服器函式實作不需要重新配置或釋放標示 為 [in, out] 屬性的資料。 對於固定大小資料,函式實作邏輯可以直接修改資料。 同樣地,對於可變大小的資料,函式實作不得修改提供給 [size_is () ] 屬性的域值。 變更用來調整資料大小的域值,會導致傳回給用戶端的較小或較大的緩衝區,而該緩衝區可能配備不當來處理異常長度。

如果伺服器常式必須重新配置標示 為 [in, out] 屬性之資料所使用的記憶體,則伺服器端函式實作可能會完全不知道存根所提供的指標是否為配置 MIDL_user_allocate () 或封送處理線路緩衝區所配置的記憶體。 若要解決此問題,MS RPC 可確保在資料上設定 [force_allocate] 屬性時,不會發生記憶體流失或損毀。 設定 [force_allocate]時,伺服器存根一律會配置指標的記憶體,不過請注意,每次使用的效能都會降低。

當呼叫從伺服器端函式實作傳回時,伺服器存根會封送處理標示 為 [out] 屬性的資料,並將其傳送至用戶端。 請注意,如果伺服器端函式實作擲回例外狀況,存根不會封送處理資料。

釋放配置的記憶體

RPC 伺服器存根會在呼叫從伺服器端函式傳回之後釋放堆疊記憶體,不論是否發生例外狀況。 伺服器存根釋放存根配置的所有記憶體,以及配置MIDL_user_allocate () 的任何記憶體。 伺服器端函式實作必須一律授與 RPC 一致狀態,方法是擲回例外狀況或傳回錯誤碼。 如果函式在擴展複雜的資料結構期間失敗,則必須確定所有指標都指向有效的資料或設定為 Null

在此階段期間,伺服器存根會釋放不屬於包含 [in] 資料之封送處理緩衝區的所有記憶體。 此行為有一個例外狀況,就是在它們上設定 [allocate (dont_free) ] 屬性的資料 - 伺服器存根不會釋放與這些指標相關聯的任何記憶體。

在伺服器存根釋放存根和函式實作所配置的記憶體之後,如果為特定資料指定 [notify_flag] 屬性,則存根會呼叫特定的通知函式。

透過 RPC 封送連結清單 -- 範例

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

在上述範例中, LINKEDLIST 的記憶體格式會與封送處理的線路格式相同。 因此,伺服器存根不會配置 pIn下整個資料指標鏈結的記憶體。 相反地,RPC 會針對整個連結清單重複使用線路緩衝區。 同樣地,存根不會配置 pInOut的記憶體,而是會重複使用用戶端封送處理的線路緩衝區。

因為函式簽章包含輸出參數 pOut,所以伺服器存根會配置記憶體以包含傳回的資料。 配置記憶體一開始會以零為零, 並將 pNext 設定為 Null。 應用程式可以配置新連結清單的記憶體,並將pOut-pNext> 指向它。pIn及其包含的連結清單可用來作為臨時區域,但應用程式不應該變更任何 pNext 指標。

應用程式可以自由地變更 pInOut所指向的連結清單內容,但不能變更任何 pNext 指標,而不只是最上層連結本身。 如果應用程式決定縮短連結清單,則無法知道是否有任何指定的 pNext 指標連結至 RPC 內部緩衝區或特別配置 MIDL_user_allocate () 的緩衝區。 若要解決此問題,您可以新增連結清單指標的特定類型宣告,以強制使用者配置,如下列程式碼所示。

typedef [force_allocate] PLINKEDLIST;

此屬性會強制服務器存根個別配置連結清單的每個節點,而且應用程式可以呼叫 MIDL_user_free () 來釋放連結清單的縮短部分。 然後,應用程式就可以安全地將新縮短連結清單結尾的 pNext 指標設定為 Null