以轮询模式出名的是USB,但是NDIS本身也有轮询模式,这是一种操作系统控制的轮询执行模型,用于驱动网络接口数据路径。
以前,NDIS 对数据路径执行上下文没有正式定义。 NDIS 驱动程序通常依赖于延迟过程调用 (DPC) 来实现其执行模型。 但是,当发出长指示链时,使用 DPC 可能会使系统不堪重负,而避免此问题需要大量代码,这些代码很难正确处理。 NDIS 轮询模式提供了 DPC 和类似执行工具的替代方法。
NDIS 轮询模式将计划决策的复杂性从 NIC 驱动程序转移到 NDIS 中,NDIS 在 NDIS 中设置每次迭代的工作限制。 为了实现此轮询模式,提供:
- OS 对 NIC 施加反向压力的机制;
- 操作系统精细控制中断的一种机制;
NDIS 轮询模式适用于 NDIS 6.85 及更高版本的微型端口驱动程序,NDIS 6.85 包含在 Windows 10 版本 21H2 和 Windows Server 2022 及更高版本中。
DPC 模型的问题
以下序列图演示了 NDIS 微型端口驱动程序如何使用 DPC 处理 Rx 数据包突发的典型示例。 在此示例中,硬件是 PCIe NIC 的标准硬件。 它具有接收硬件队列和该队列的中断掩码。
当没有网络活动时,硬件已启用 Rx 中断。 当 Rx 数据包到达时:
- 硬件生成中断,NDIS (ISR) 调用驱动程序的 MiniportInterrupt 函数;
- 驱动程序在 ISR 中执行的工作很少,因为它们以非常高的 IRQL 运行。 驱动程序禁用来自 ISR 的中断,并将硬件处理延迟到 MiniportInterruptDPC 函数 (DPC) ;
- NDIS 最终调用驱动程序的 DPC,驱动程序会从硬件队列中排出所有完成,并将其指示给 OS;
当驱动程序将 I/O 操作延迟到 DPC 时,有两个痛点可能会影响网络堆栈:
- 驱动程序不知道系统是否能够处理所指示的所有数据,因此驱动程序别无选择,只能从其硬件队列中清空尽可能多的元素,并在堆栈上指示它们;
- 由于驱动程序使用 DPC 来延迟其 ISR 的工作,因此所有指示都DISPATCH_LEVEL。 当发出长指示链时,这会使系统不堪重负,并导致 bug 检查0x133 DPC_WATCHDOG_VIOLATION;
- 避免这些痛点需要在驱动程序中使用大量棘手的代码。 虽然可以检查如果 DPC 监视器接近 KeQueryDpcWatchdogInformation 函数的限制并突破 DPC,但仍需要在驱动程序中围绕此情况构建基础结构:需要以某种方式暂停一点,然后继续指示数据包,同时需要将所有这一切与数据路径的生存期同步;
轮询对象简介
NDIS 轮询模式引入了轮询对象来解决与 DPC 关联的痛点。 Poll 对象是执行上下文构造。 微型端口驱动程序在处理数据路径操作时可以使用 Poll 对象代替 DPC。
Poll 对象提供以下内容:
它为 NDIS 提供了一种设置每次迭代工作限制的方法;
它与通知机制密切相关。 这可使 OS 和 NIC 在何时需要处理工作方面保持同步;
它具有内置迭代和中断的概念。 使用 DPC 时,驱动程序在每次完成 DPC 时强制重新启用中断。 使用轮询对象时,驱动程序不需要重新启用每次轮询迭代的中断,因为轮询模式会让你的驱动程序知道何时完成轮询,并且需要再次重新启用中断;
在制定计划决策时,系统可以明智地确定是在DISPATCH_LEVEL还是PASSIVE_LEVEL运行。 这可以允许微调来自不同 NIC 的流量的优先级,并在计算机上实现更公平的工作负载分布;
它具有序列化保证。 从 Poll 对象的执行上下文中运行代码后,可以保证不会运行与同一执行上下文相关的其他代码。 这允许 NIC 驱动程序具有其数据路径的无锁实现;
NDIS 轮询模式模型
下面的序列图演示了同一假设 PCIe NIC 驱动程序如何使用 Poll 对象而不是 DPC 处理 Rx 数据包的突发情况:
与 DPC 模型一样,当 Rx 数据包到达时,硬件会生成中断,NDIS 会调用驱动程序的 ISR,驱动程序会禁用来自 ISR 的中断。 此时,轮询模式模型出现分歧:
- 驱动程序不会对 DPC 进行排队,而是将轮 询对象 (之前从 ISR 创建的) 排队 ,以通知 NDIS 新工作已准备好处理;
- 在未来的某个时间点,NDIS 会调用驱动程序的 轮询迭代处理程序 来处理工作。 与 DPC 不同,驱动程序不允许指示其硬件队列中已准备好的元素数量多于 Rx NBR。 驱动程序应改为检查处理程序的轮询数据参数,以获取它可以指示的最大 NBR 数;
- 一旦驱动程序提取到最大数量的 Rx 数据包,它应初始化 NBL,将它们添加到轮询处理程序提供的 NBL 队列,然后退出回调。 驱动程序不应在退出之前启用中断;
- NDIS 会继续轮询驱动程序,直到它评估驱动程序不再向前推进。 此时,NDIS 将停止轮询并要求驱动程序 重新启用中断;
INF的修改
NdisPoll 枚举标准化 INF 关键字具有以下属性:
- SubkeyName:必须在 INF 文件中指定且显示在注册表中的关键字 (keyword) 的名称;
- ParamDesc:与 SubkeyName 关联的显示文本;
- Value: 与列表中的每个选项关联的枚举整数值。 此值存储在 NDI\params\ SubkeyName\Value 中;
- EnumDesc: 与菜单中显示的每个值关联的显示文本;
- Default: 菜单的默认值;
创建 Poll 对象
若要创建 Poll 对象,微型端口驱动程序在其 MiniportInitializeEx 回调函数中执行以下操作:
- 分配专用微型端口上下文;
- 分配 NDIS_POLL_CHARACTERISTICS 结构,以指定 NdisPoll 和 NdisSetPollNotification 回调函数的入口点;
- 调用 NdisRegisterPoll 以创建 Poll 对象并将其存储在微型端口上下文中;
以下代码演示微型端口驱动程序如何为接收队列流创建 Poll 对象:
// 代码中忽略了错误和异常处理
NDIS_SET_POLL_NOTIFICATION NdisSetPollNotification;
NDIS_POLL NdisPoll;
NDIS_STATUS
MiniportInitialize(
_In_ NDIS_HANDLE NdisAdapterHandle,
_In_ NDIS_HANDLE MiniportDriverContext,
_In_ NDIS_MINIPORT_INIT_PARAMETERS * MiniportInitParameters
)
{
// Allocate a private miniport context
MINIPORT_CONTEXT * miniportContext = ...;
NDIS_POLL_CHARACTERISTICS pollCharacteristics;
pollCharacteristics.Header.Type = NDIS_OBJECT_TYPE_DEFAULT;
pollCharacteristics.Header.Revision = NDIS_POLL_CHARACTERISTICS_REVISION_1;
pollCharacteristics.Header.Size = NDIS_SIZEOF_NDIS_POLL_CHARACTERISTICS_REVISION_1;
pollCharacteristics.SetPollNotificationHandler = NdisSetPollNotification;
pollCharacteristics.PollHandler = NdisPoll;
// Create a Poll object and store it in the miniport context
NdisRegisterPoll(
NdisAdapterHandle,
miniportContext,
&pollCharacteristics,
&miniportContext->RxPoll);
return NDIS_STATUS_SUCCESS;
}
将 Poll 对象排队等待执行
从 ISR 中,微型端口驱动程序调用 NdisRequestPoll 以将 Poll 对象排队等待执行。 下面的示例演示了接收处理,但为简单起见,忽略了基于行的中断的共享处理和其他处理:
BOOLEAN
MiniportIsr(
KINTERRUPT * Interrupt,
void * Context
)
{
auto miniportContext = static_cast<MINIPORT_CONTEXT *>(Context);
auto hardwareContext = miniportContext->HardwareContext;
// Check if this interrupt is due to a received packet
if (hardwareContext->ISR & RX_OK)
{
// Disable the receive interrupt and queue the Poll
hardwareContext->IMR &= ~RX_OK;
NdisRequestPoll(miniportContext->RxPoll, nullptr);
}
return TRUE;
}
实现轮询迭代处理程序
NDIS 调用微型端口驱动程序的 NdisPoll 回调来轮询接收指示并发送完成。 当驱动程序调用 NdisRequestPoll 将 Poll 对象排队时, NDIS 首先调用 NdisPoll 。 当驱动程序在接收指示或传输完成方面向前推进时,NDIS 将继续调用 NdisPoll 。
对于接收指示,驱动程序应在 NdisPoll 中执行以下操作:
- 检查 NDIS_POLL_DATA 结构的接收参数,以获取它可以指示的最大 NBR 数;
- 提取最大 Rx 数据包数;
- 初始化 NBR;
- 将它们添加到由位于NdisPollPollData 参数) NDIS_POLL_DATA结构中的 NDIS_POLL_RECEIVE_DATA 结构 提供的 NBL 队列;
- 退出回调;
对于传输完成,驱动程序应在 NdisPoll 中执行以下操作:
- 检查 NDIS_POLL_DATA 结构的传输参数,以获取它可以完成的最大 NBR 数;
在退出 NdisPoll 函数之前,驱动程序不应启用 Poll 对象的中断。 NDIS 将继续轮询驱动程序,直到它评估没有取得任何进展。 此时,NDIS 将停止轮询并要求驱动程序 重新启用中断; - 提取最大 Tx 数据包数;
- 完成 NBR;
- 将它们添加到 NDIS_POLL_TRANSMIT_DATA 结构提供的 NBL 队列, (位于 NdisPollPollData 参数 ) 的 NDIS_POLL_DATA 结构中;
- 退出回调;
下面是驱动程序如何为接收队列流实现 NdisPoll 。
_Use_decl_annotations_
void
NdisPoll(
void * Context,
NDIS_POLL_DATA * PollData
)
{
auto miniportContext = static_cast<MINIPORT_CONTEXT *>(Context);
auto hardwareContext = miniportContext->HardwareContext;
// Drain received frames
auto & receive = PollData->Receive;
receive.NumberOfRemainingNbls = NDIS_ANY_NUMBER_OF_NBLS;
receive.Flags = NDIS_RECEIVE_FLAGS_SHARED_MEMORY_VALID;
while (receive.NumberOfIndicatedNbls < receive.MaxNblsToIndicate)
{
auto rxDescriptor = HardwareQueueGetNextDescriptorToCheck(hardwareContext->RxQueue);
// If this descriptor is still owned by hardware stop draining packets
if ((rxDescriptor->Status & HW_OWN) != 0)
break;
auto nbl = MakeNblFromRxDescriptor(miniportContext->NblPool, rxDescriptor);
AppendNbl(&receive.IndicatedNblChain, nbl);
receive.NumberOfIndicatedNbls++;
// Move to next descriptor
HardwareQueueAdvanceNextDescriptorToCheck(hardwareContext->RxQueue);
}
}
管理中断
微型端口驱动程序实现 NdisSetPollNotification 回调以启用或禁用与 Poll 对象关联的中断。 当检测到微型端口驱动程序未在 NdisPoll 中向前推进时,NDIS 通常会调用 NdisSetPollNotification 回调。 NDIS 使用 NdisSetPollNotification 告知驱动程序它将停止调用 NdisPoll。 当准备好处理新工作时,驱动程序应调用 NdisRequestPoll 。
下面是驱动程序如何为接收队列流实现 NdisSetPollNotification:
_Use_decl_annotations_
void
NdisSetPollNotification(
void * Context,
NDIS_POLL_NOTIFICATION * Notification
)
{
auto miniportContext = static_cast<MINIPORT_CONTEXT *>(Context);
auto hardwareContext = miniportContext->HardwareContext;
if (Notification->Enabled)
{
hardwareContext->IMR |= RX_OK;
}
else
{
hardwareContext->IMR &= ~RX_OK;
}
}