服务器存根内存管理
Server-Stub 内存管理简介
MIDL 生成的存根充当客户端进程和服务器进程之间的接口。 客户端存根封送传递给使用 [in] 属性标记的参数的所有数据,并将其发送到服务器存根。 接收此数据后,服务器存根会重新构造调用堆栈,然后执行相应的用户实现的服务器函数。 服务器存根还会封送使用 [out] 属性标记的参数数据,并将其返回到客户端应用程序。
MSRPC 使用的 32 位封送数据格式是网络数据表示形式(NDR)传输语法的合规版本。 有关此格式的详细信息,请参阅 Open Group 网站。 对于 64 位平台,可以使用名为 NDR64 的 NDR 传输语法Microsoft 64 位扩展来提高性能。
取消分流入站数据
在 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”标志编译的(这是 MIDL 编译器最新版本中的默认设置),则 RPC 服务器存根在取消合并过程中验证缓冲区。 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 );
数据是一种简单类型,其内存大小与其封送大小不同,例如 枚举 16 和 __int3264。
数据由内存对齐小于自然对齐、包含上述任何数据类型或尾随字节填充的结构定义。 例如,以下复杂数据结构强制进行 2 字节对齐,并在末尾进行填充。
#pragma pack(2) typedef 结构 ComplexPackedStructure { char c;
long l;对齐在第二个字节字符 c2 处强制对齐;将有一个尾随的一字节垫来保持 2 字节对齐 } '''
- 数据包含必须按字段封送字段的结构。 这些字段包括 DCOM 接口中定义的接口指针;忽略的指针;使用 [range] 属性设置的整数值;使用 [wire_marshal]、[user_marshal]、[transmit_as] 和 [represent_as] 属性定义的数组元素;和嵌入的复杂数据结构。
- 数据包含联合、包含联合的结构或联合数组。 只有工会的特定分支在网络上封送。
- 数据包含一个具有多维一致性数组的结构,该数组至少有一个非固定维度。
- 数据包含复杂结构的数组。
- 数据包含简单数据类型的数组,如 枚举 16 和 __int3264。
- 数据包含 ref 数组和接口指针。
- 数据具有应用于指针的 [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] 引用指针也以递归方式初始化。 递归停止在任何指针处,[唯一] 或 [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
);
在取消对入站数据(包括 大小)后,服务器存根会为大小为大小为“sizeof(char)*size”的 pv 分配缓冲区。 分配空间后,服务器存根会从缓冲区中零。 请注意,在这种情况下,存根使用 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 指针链接 tto RPC 内部缓冲区或专门分配有 MIDL_user_allocate()的缓冲区。 若要解决此问题,请为强制用户分配的链接列表指针添加特定的类型声明,如下面的代码所示。
typedef [force_allocate] PLINKEDLIST;
此属性强制服务器存根单独分配链接列表的每个节点,应用程序可以通过调用 MIDL_user_free()释放链接列表的缩短部分。 然后,应用程序可以安全地将新缩短的链接列表末尾的 pNext 指针设置为 NULL。