Windows 网络驱动程序使用 OID 请求将控制消息发送到 NDIS 绑定堆栈。 协议驱动程序(如 TCPIP 或 vSwitch)依赖于数十个 OID 来配置基础 NIC 驱动程序的每个功能。 在 Windows 10 版本 1709 之前,OID 请求以两种方式发送:常规请求和直接请求。
本主题介绍第三种 OID 调用:同步。 同步调用旨在低延迟、非阻塞、可缩放且可靠。 从 NDIS 6.80 开始提供同步 OID 请求接口,该接口包含在 Windows 10 版本 1709 及更高版本中。
常规和直接 OID 请求之比较
使用同步 OID 请求时,调用的负载(即 OID 本身)与常规和直接 OID 请求完全相同。 唯一的区别在于调用本身。 因此,在所有三种类型的 OID 中,内容 是相同的;只是 实现方式 不同。
下表描述了常规 OID、Direct OID 和同步 OID 之间的差异。
| 特征 | 常规 OID | 直接 OID | 同步 OID |
|---|---|---|---|
| 有效负载 | NDIS_OID_REQUEST | NDIS_OID_REQUEST | NDIS_OID_REQUEST |
| OID 类型 | 统计信息、查询、设置、方法 | 统计信息、查询、设置、方法 | 统计信息、查询、设置、方法 |
| 可通过以下方式发出 | 协议、筛选器 | 协议、筛选器 | 协议、筛选器 |
| 可以由...来完成 | 微型端口、筛选器 | 微型端口、筛选器 | 微型端口、筛选器 |
| 筛选器可以修改 | 是的 | 是的 | 是的 |
| NDIS 分配内存 | 对于每个过滤器(OID 克隆) | 对于每个筛选器(OID 克隆) | 仅在使用异常多的筛选器时(调用上下文) |
| 可以暂停 | 是的 | 是的 | 否 |
| 可以阻止 | 是的 | 否 | 否 |
| IRQL | == 被动 | <= DISPATCH | <= DISPATCH |
| 由 NDIS 序列化 | 是的 | 否 | 否 |
| 筛选器被调用 | 递 归 | 递 归 | 迭代 |
| 过滤器克隆 OID | 是的 | 是的 | 否 |
筛选
与其他两种类型的 OID 调用一样,筛选器驱动程序在同步调用中完全控制 OID 请求。 筛选器驱动程序可以观察、截获、修改和发出同步 OID。 但是,为了提高效率,同步 OID 的机制有些不同。
直通、拦截和起源
从概念上讲,所有 OID 请求都从较高驱动程序发出,并由较低驱动程序完成。 在此过程中,OID 请求可能会通过任意数量的筛选器驱动程序。
在最常见的情况下,协议驱动程序发出 OID 请求,所有筛选器只需向下传递 OID 请求,未修改。 下图说明了此常见方案。
但是,允许任何筛选器模块截获 OID 请求并完成它。 在这种情况下,请求不会传递到较低的驱动程序,如下图所示。
在某些情况下,筛选器模块可能决定发起其自己的 OID 请求。 此请求从筛选器模块的级别开始,仅遍历较低的驱动程序,如下图所示。
所有 OID 请求都有此基本流:更高的驱动程序(协议或筛选器驱动程序)发出请求,较低的驱动程序(微型端口或筛选器驱动程序)完成该请求。
常规和直接 OID 请求的工作原理
常规或直接 OID 请求以递归方式派发。 下图显示了函数调用序列。 请注意,序列本身与上一部分的关系图中所述的序列非常类似,但按顺序显示请求的递归性质。
如果已安装足够的筛选器,则 NDIS 将强制分配新的线程堆栈,以保持更深层次的递归。
NDIS 认为 NDIS_OID_REQUEST 结构仅对堆栈上的单个跃点有效。 如果筛选器驱动程序想要将请求向下传递到下一个较低驱动程序(这是绝大多数 OID 的情况),筛选器驱动程序 必须 插入几十行样板代码以克隆 OID 请求。 此样板存在以下几个问题:
- 它强制进行内存分配以克隆 OID。 访问内存池既很慢,又无法保证 OID 请求的顺利推进。
- 随着时间的推移,OID 结构设计必须保持不变,因为所有筛选器驱动程序都对复制一个 NDIS_OID_REQUEST 内容到另一个的机制进行了硬编码。
- 需要这么多样板代码会掩盖筛选器的实际作用。
同步 OID 请求的筛选模型
同步 OID 请求的筛选模型利用调用的同步性质,以解决上一节中讨论的问题。
问题和完成处理程序
与常规和直接 OID 请求不同,同步 OID 请求有两个筛选器挂钩:问题处理程序和完整处理程序。 筛选器驱动程序既不能注册一个挂钩,也不能注册两个挂钩。
为每个筛选器驱动程序调用问题调用,从堆栈顶部向下到堆栈底部。 过滤器的Issue调用可以阻止OID继续向下传递,并通过一些状态代码完成OID。 如果没有筛选器决定截获 OID,则 OID 会到达 NIC 驱动程序,该驱动程序必须同步完成 OID。
完成 OID 后,将为每个筛选器驱动程序调用完整的调用,从 OID 在堆栈中完成的位置开始,到堆栈顶部。 完整调用可以检查或修改 OID 请求,并检查或修改 OID 的完成状态代码。
下图演示了典型情况,其中协议发出同步 OID 请求,筛选器不会截获请求。
请注意,同步 OID 的调用模型是迭代的。 这样,堆栈的使用将受常量限制,无需扩展堆栈。
如果筛选器驱动程序在其问题处理程序中截获同步 OID,则 OID 不会提供给较低筛选器或 NIC 驱动程序。 但是,仍会调用更高层过滤器的完整处理程序,如下图所示:
最小内存分配
常规和直接 OID 请求需要筛选器驱动程序才能克隆NDIS_OID_REQUEST。 相比之下,不允许同步 OID 请求被克隆。 此设计的优点是同步 OID 的延迟较低(OID 请求在运行筛选器堆栈时不会重复克隆),并且失败机会更少。
但是,这确实引发了一个新问题。 如果 OID 无法克隆,筛选器驱动程序会将每个请求的状态存储在哪里? 例如,假设筛选器驱动程序将一个 OID 转换为另一个 OID。 在堆栈向下过程中,筛选器需要保存旧的 OID。 在返回堆栈的过程中,过滤器需要恢复旧的 OID。
若要解决此问题,NDIS 为每个筛选器驱动程序分配一个指针大小的槽,用于每个正在进行的同步 OID 请求。 NDIS 在从筛选器的 Issue 处理程序到其 Complete 处理程序的调用过程中保持该槽位不变。 这样,问题处理程序可以保存以后由完成处理程序使用的状态。 以下代码片段演示了一个示例。
NDIS_STATUS
MyFilterSynchronousOidRequest(
_In_ NDIS_HANDLE FilterModuleContext,
_Inout_ NDIS_OID_REQUEST *OidRequest,
_Outptr_result_maybenull_ PVOID *CallContext)
{
if ( . . . should intercept this OID . . . )
{
// preserve the original buffer in the CallContext
*CallContext = OidRequest->DATA.SET_INFORMATION.InformationBuffer;
// replace the buffer with a new one
OidRequest->DATA.SET_INFORMATION.InformationBuffer = . . . something . . .;
}
return NDIS_STATUS_SUCCESS;
}
VOID
MyFilterSynchronousOidRequestComplete(
_In_ NDIS_HANDLE FilterModuleContext,
_Inout_ NDIS_OID_REQUEST *OidRequest,
_Inout_ NDIS_STATUS *Status,
_In_ PVOID CallContext)
{
// if the context is not null, we must have replaced the buffer.
if (CallContext != null)
{
// Copy the data from the miniport back into the protocol’s original buffer.
RtlCopyMemory(CallContext, OidRequest->DATA.SET_INFORMATION.InformationBuffer,...);
// restore the original buffer into the OID request
OidRequest->DATA.SET_INFORMATION.InformationBuffer = CallContext;
}
}
NDIS 为每个调用的每个筛选器保存一个 PVOID。 NDIS 启发式地在堆栈上分配合理的槽数,以便在常见情况下存在零池分配。 这通常不超过七个筛选器。 如果用户设置了异常情况,NDIS 会回退到内存池分配。
减少重复代码
考虑 处理常规或直接 OID 请求的示例模板。 该代码只是为了注册 OID 处理程序而输入的成本。 如果要分配自己的 OID,则必须再添加十几行模板代码。 使用同步 OID 时,无需处理异步完成的额外复杂性。 因此,你可以切掉大部分样板代码。
下面是同步 OID 的最小问题处理程序:
NDIS_STATUS
MyFilterSynchronousOidRequest(
NDIS_HANDLE FilterModuleContext,
NDIS_OID_REQUEST *OidRequest,
PVOID *CallContext)
{
return NDIS_STATUS_SUCCESS;
}
如果要截获或修改特定 OID,只需添加几行代码即可执行此作。 最简的 Complete 处理程序更简单:
VOID
MyFilterSynchronousOidRequestComplete(
NDIS_HANDLE FilterModuleContext,
NDIS_OID_REQUEST *OidRequest,
NDIS_STATUS *Status,
PVOID CallContext)
{
return;
}
同样,筛选器驱动程序可以使用一行代码发出自己的新同步 OID 请求:
status = NdisFSynchronousOidRequest(binding->NdisBindingHandle, &oid);
相比之下,需要发出常规或 Direct OID 的筛选器驱动程序必须设置异步完成处理程序并实现一些代码,以便将自己的 OID 完成与刚克隆的 OID 完成区分开来。 此样板示例显示在 用于发出常规 OID 请求的示例样板。
互操作性
尽管常规、直接和同步调用样式都使用相同的数据结构,但管道不会转到微型端口中的相同处理程序。 此外,某些 OID 不能用于某些管道。 例如,OID_PNP_SET_POWER 需要仔细同步,并且通常会强制小型端口进行阻塞调用。 这使得在 Direct OID 回调中处理它变得困难,并阻止其在同步 OID 回调中使用。
因此,与直接 OID 请求一样,同步 OID 调用只能与 OID 子集一起使用。 在 Windows 10 版本 1709 中,同步 OID 路径仅支持在接收端缩放版本 2(RSSv2)中使用的OID_GEN_RSS_SET_INDIRECTION_TABLE_ENTRIES OID。
实现同步 OID 请求
有关在驱动程序中实现同步 OID 请求接口的详细信息,请参阅以下主题: