服务器存根内存管理

Server-Stub内存管理简介

MIDL 生成的存根充当客户端进程和服务器进程之间的接口。 客户端存根封送传递给使用 [in] 属性标记的参数的所有数据,并将其发送到服务器存根。 服务器存根在收到此数据后重建调用堆栈,然后执行相应的用户实现的服务器函数。 服务器存根还会封送用 [out] 属性标记的参数数据,并将其返回到客户端应用程序。

MSRPC 使用的 32 位封送数据格式是网络数据表示 (NDR) 传输语法的合规版本。 有关此格式的详细信息,请参阅 开放组网站。 对于 64 位平台,Microsoft 64 位扩展为 NDR64 的 NDR 传输语法可用于提高性能。

取消对入站数据进行封送

在 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 引用的数据分配额外的内存;相反,它只是将指向封送数据的指针传递到服务器端函数实现。 RPC 服务器存根在取消封存过程中,如果使用“-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 struct ComplexPackedStructure { char c;
long l;对齐在第二个字节字符 c2 处强制对齐;将有一个尾随的 1 字节垫,以保持 2 字节对齐 } “””

  • 数据包含一个结构,该结构必须按字段封送字段。 这些字段包括 DCOM 接口中定义的接口指针;忽略的指针;使用 [range] 属性设置的整数值;使用 [wire_marshal][user_marshal][transmit_as] 和 [represent_as] 属性定义的数组的元素;和嵌入的复杂数据结构。
  • 数据包含联合、包含联合的结构或联合数组。 只有联盟的特定分支在网络上封送。
  • 数据包含具有至少一个非固定维度的多维一致性数组的结构。
  • 数据包含复杂结构的数组。
  • 数据包含简单数据类型的数组,例如 enum16__int3264
  • 数据包含 ref 和 interface 指针的数组。
  • 数据具有应用于指针 的 [force_allocate] 属性。
  • 数据具有应用于指针 的 [allocate (all_nodes) ] 属性。
  • 数据具有应用于指针 的 [byte_count] 属性。

64 位数据和 NDR64 传输语法

如前所述,使用名为 NDR64 的特定 64 位传输语法封送 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 来尝试避免分配,这可能会导致内存损坏。 然后,服务器存根将为内存中的顶级指针分配空间, (指针到指针的情况) ,以及堆栈上大小小于预期的顶级简单结构。

在某些情况下,客户端可以指定服务器端的内存分配大小。 在以下示例中,客户端在入站 size 参数中指定出站数据 的大小

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

取消对入站数据(包括 大小)进行封存后,服务器存根会为 pv 分配一个大小为“sizeof (char) *size”的缓冲区。 分配空间后,服务器存根会将缓冲区归零。 请注意,在此特定情况下,存根使用 MIDL_user_allocate () 分配内存,因为缓冲区的大小是在运行时确定的。

请注意,对于 DCOM 接口,如果客户端和服务器共享同一 COM 单元或实现 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