NDIS 6.80 中的同步 OID 请求接口

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、直接 OID 和同步 OID 之间的差异。

Attribute 常规 OID 直接 OID 同步 OID
有效负载 NDIS_OID_REQUEST NDIS_OID_REQUEST NDIS_OID_REQUEST
OID 类型 统计信息、查询、设置、方法 统计信息、查询、设置、方法 统计信息、查询、设置、方法
颁发者 协议、筛选器 协议、筛选器 协议、筛选器
可通过以下方式完成 微型端口、筛选器 微型端口、筛选器 微型端口、筛选器
筛选器可以修改
NDIS 分配内存 对于每个筛选器 (OID 克隆) 对于每个筛选器 (OID 克隆) 仅当大量筛选器 (调用上下文)
可以笔
可以阻止
IRQL == PASSIVE <= DISPATCH <= DISPATCH
由 NDIS 序列化
调用筛选器 Recursively Recursively 迭 代
筛选器克隆 OID

Filtering

与其他两种类型的 OID 调用一样,筛选器驱动程序可以完全控制同步调用中的 OID 请求。 筛选器驱动程序可以观察、截获、修改和发出同步 OID。 但是,为了提高效率,同步 OID 的机制略有不同。

直通、拦截和发起

从概念上讲,所有 OID 请求都是从较高级别的驱动程序发出的,并由较低级别的驱动程序完成。 在此过程中,OID 请求可能会通过任意数量的筛选器驱动程序。

在最常见的情况下,协议驱动程序会发出 OID 请求,所有筛选器只需向下传递 OID 请求(未修改)。 下图说明了此常见方案。

典型的 OID 路径源自协议。

但是,允许任何筛选器模块截获并完成 OID 请求。 在这种情况下,请求不会传递到较低级驱动程序,如下图所示。

典型的 OID 路径源自协议,并被筛选器截获。

在某些情况下,筛选器模块可能会决定发起自己的 OID 请求。 此请求从筛选器模块的级别开始,仅遍历较低的驱动程序,如下图所示。

典型的 OID 路径源自筛选器。

所有 OID 请求都具有此基本流:) 发出请求的较高驱动程序 (协议或筛选器驱动程序,而较低的驱动程序 (微型端口或筛选器驱动程序) 完成请求。

常规和直接 OID 请求的工作原理

常规或直接 OID 请求以递归方式调度。 下图显示了函数调用序列。 请注意,序列本身与上一部分中的关系图中描述的序列非常类似,但排列方式显示请求的递归性质。

常规和直接 OID 请求的函数调用序列。

如果安装了足够的筛选器,则将强制 NDIS 分配新的线程堆栈,以继续深入递归。

NDIS 认为 NDIS_OID_REQUEST 结构仅对堆栈上的单个跃点有效。 如果筛选器驱动程序想要将请求向下传递到下一个较低的驱动程序 (大多数 OID) ,则筛选器驱动程序 必须 插入几十行样本代码来克隆 OID 请求。 此样本有几个问题:

  1. 它强制进行内存分配以克隆 OID。 命中内存池的速度很慢,因此无法保证 OID 请求向前推进。
  2. 随着时间的推移,OID 结构设计必须保持不变,因为所有筛选器驱动将一个NDIS_OID_REQUEST的内容复制到另一个NDIS_OID_REQUEST的机制。
  3. 需要如此多的样本会掩盖筛选器实际执行的操作。

同步 OID 请求的筛选模型

同步 OID 请求的筛选模型利用调用的同步性质来解决上一部分中讨论的问题。

问题和完成处理程序

与常规和直接 OID 请求不同,同步 OID 请求有两个筛选器挂钩:问题处理程序和完整处理程序。 筛选器驱动程序不能注册两个挂钩、一个或两个挂钩。

从堆栈顶部到堆栈底部,为每个筛选器驱动程序调用问题调用。 任何筛选器的 Issue 调用都可以阻止 OID 继续向下,并使用一些状态代码完成 OID。 如果没有筛选器决定截获 OID,则 OID 会到达 NIC 驱动程序,该驱动程序必须同步完成 OID。

OID 完成后,将为每个筛选器驱动程序调用 Complete 调用,从堆栈中完成 OID 的任意位置开始,一直调用到堆栈顶部。 Complete 调用可以检查或修改 OID 请求,并检查或修改 OID 的完成状态代码。

下图演示了协议发出同步 OID 请求且筛选器不截获请求的典型情况。

同步 OID 请求的函数调用序列。

请注意,同步 OID 的调用模型是迭代的。 这样,堆栈使用就受常量限制,无需扩展堆栈。

如果筛选器驱动程序在其问题处理程序中截获同步 OID,则不会向较低筛选器或 NIC 驱动程序提供 OID。 但是,仍会调用更高筛选器的完整处理程序,如下图所示:

截获的同步 OID 请求的函数调用序列。筛选器

最小内存分配

常规和直接 OID 请求需要筛选器驱动程序来克隆NDIS_OID_REQUEST。 相反,不允许克隆同步 OID 请求。 此设计的优点是同步 OID 的延迟较低(OID 请求在筛选器堆栈中传输时不会重复克隆),并且失败的可能性也较少。

但是,这确实引发了一个新的问题。 如果无法克隆 OID,筛选器驱动程序将其每个请求的状态存储在何处? 例如,假设筛选器驱动程序将一个 OID 转换为另一个 OID。 在堆栈下行时,筛选器需要保存旧的 OID。 在备份堆栈时,筛选器需要还原旧的 OID。

为了解决此问题,NDIS 为每个筛选器驱动程序为每个正在进行的同步 OID 请求分配指针大小的槽。 NDIS 在从筛选器的问题处理程序到其 Complete 处理程序的调用中保留此槽。 这允许问题处理程序保存稍后由 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 在堆栈上以启发方式分配合理数量的槽,以便在常见情况下没有池分配。 这通常不超过 7 个筛选器。 如果用户设置了病态情况,则 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);

相比之下,需要发出常规或直接 OID 的筛选器驱动程序必须设置异步完成处理程序并实现一些代码,以将其自己的 OID 完成与刚克隆的 OID 完成区分开来。 用于 发出常规 OID 请求的示例样本中显示了此样本的示例。

互操作性

尽管常规、直接和同步调用样式都使用相同的数据结构,但管道不会转到微型端口中的同一处理程序。 此外,某些 OID 不能在某些管道中使用。 例如, OID_PNP_SET_POWER 需要仔细同步,并且通常会强制微型端口进行阻止调用。 这使得在直接 OID 回调中处理它变得困难,并阻止它在同步 OID 回调中使用。

因此,与直接 OID 请求一样,同步 OID 调用只能与一部分 OID 一起使用。 在 Windows 10版本 1709 中,同步 OID 路径中仅支持接收方缩放版本 2 (RSSv2) 中使用的OID_GEN_RSS_SET_INDIRECTION_TABLE_ENTRIES OID。

实现同步 OID 请求

有关在驱动程序中实现同步 OID 请求接口的详细信息,请参阅以下主题: