NDIS 轮询模式
NDIS 轮询模式概述
NDIS 轮询模式是一种操作系统控制的轮询执行模型,可驱动网络接口数据路径。
以前,NDIS 没有对数据路径执行上下文进行正式定义。 NDIS 驱动程序通常依赖于延迟过程调用(DPC)来实现其执行模型。 但是,当发出长指示链并避免此问题时,使用 DPC 可能会使系统不知所措,这需要大量的代码来正确操作。 NDIS 轮询模式提供 DPC 和类似的执行工具的替代方法。
NDIS 轮询模式将计划决策的复杂性从 NIC 驱动程序转移到 NDIS 中,其中 NDIS 设置每次迭代的工作限制。 若要实现此轮询模式,请执行以下操作:
OS 对 NIC 施加回压的机制。
OS 可精细控制中断的机制。
NDIS 轮询模式适用于 NDIS 6.85 及更高版本的微型端口驱动程序。
DPC 模型的问题
下面的序列图演示了 NDIS 微型端口驱动程序如何使用 DPC 处理 Rx 数据包的突发的典型示例。 在此示例中,硬件是 PCIe NIC 的标准。 它具有接收硬件队列和该队列的中断掩码。
如果没有网络活动,硬件将启用 Rx 中断。 Rx 数据包到达时:
硬件生成中断,NDIS 调用驱动程序的 MiniportInterrupt 函数 (ISR)。
驱动程序在 ISR 中工作很少,因为它们在 IRQL 非常高的情况下运行。 驱动程序禁用 ISR 中的中断,并将硬件处理延迟到 MiniportInterruptDPC 函数 (DPC)。
NDIS 最终调用驱动程序的 DPC,驱动程序会从硬件队列中清空所有完成,并将其指示给 OS。
当驱动程序将 I/O 操作延迟到 DPC 时,两个痛点可能会影响网络堆栈:
驱动程序不知道系统是否能够处理所指示的所有数据,因此驱动程序别无选择,只能从其硬件队列中清空尽可能多的元素,并指示它们向上堆栈。
由于驱动程序使用 DPC 从 ISR 延迟工作,因此所有指示都在DISPATCH_LEVEL发出。 当发出长指示链并导致 bug 检查0x133 DPC_WATCHDOG_VIOLATION时,系统可能会不堪重负。
避免这些难题需要驱动程序中的大量棘手代码。 虽然可以使用 KeQueryDpcWatchdogInformation 函数检查 DPC 监视器是否接近限制并突破 DPC,但仍需要在驱动程序中构建一个基础结构:需要一些方法来暂停一点,然后继续指示数据包,同时需要将所有这些内容与数据路径的生存期同步。
轮询对象简介
NDIS 轮询模式引入了轮询对象来解决与 DPC 关联的痛点。 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 中的中断。 此时,轮询模式模型会分歧:
驱动程序 将轮询对象 ( 以前创建)从 ISR 排队,而不是对 DPC 进行排队,以通知 NDIS 已准备好处理新工作。
在将来的某个时候,NDIS 调用驱动程序的 轮询迭代处理程序 来处理工作。 与 DPC 不同,驱动程序不允许指示任意数量的 Rx NBL,因为其硬件队列中已准备好元素。 驱动程序应检查处理程序的轮询数据参数,以获取它可以指示的最大 NBL 数。
驱动程序提取到它应初始化 NBL 的最大 Rx 数据包数后,将其添加到轮询处理程序提供的 NBL 队列,然后退出回调。 在退出之前,驱动程序不应启用中断。
NDIS 继续轮询驱动程序,直到评估驱动程序不再向前推进。 此时,NDIS 将停止轮询,并要求驱动程序 重新启用中断。
NDIS 轮询模式的标准化 INF 关键字
以下关键字必须用于启用或禁用对 NDIS 轮询模式的支持:
*NdisPoll 枚举标准化 INF 关键字具有以下属性:
SubkeyName
必须在 INF 文件中指定且出现在注册表中的关键字名称。
ParamDesc
与 SubkeyName 关联的显示文本。
值
与列表中的每个选项关联的枚举整数值。 此值存储在 NDI\params\ SubkeyName\ 值中。
EnumDesc
与菜单中显示的每个值关联的显示文本。
默认
菜单的默认值
SubkeyName | ParamDesc | 值 | EnumDesc |
---|---|---|---|
*NdisPoll | Ndis 轮询模式 | 0 | 已禁用 |
1(默认值) | Enabled |
有关使用枚举关键字的详细信息,请参阅枚举关键字。
创建 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;
}
排队轮询对象以供执行
从 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结构的接收参数以获取它可以指示的最大 NBL 数。
- 最多提取 Rx 数据包数。
- 初始化 NBL。
- 将它们添加到NDIS_POLL_RECEIVE_DATA结构提供的 NBL 队列(位于 NdisPoll PollData 参数的NDIS_POLL_DATA结构中)。
- 退出回调。
对于传输完成,驱动程序应在 NdisPoll 中执行以下操作:
- 检查NDIS_POLL_DATA结构的传输参数以获取其可完成的最大 NBL 数。
- 最多提取 Tx 数据包数。
- 完成 NBL。
- 将它们添加到NDIS_POLL_TRANSMIT_DATA结构提供的 NBL 队列(位于 NdisPoll PollData 参数NDIS_POLL_DATA结构中)。
- 退出回调。
驱动程序不应在退出 NdisPoll 函数之前启用 Poll 对象的中断。 NDIS 将继续轮询驱动程序,直到评估是否没有取得进展。 此时,NDIS 将停止轮询,并要求驱动程序 重新启用中断。
下面是驱动程序如何为接收队列流实现 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 对象关联的中断。 NDIS 通常会在检测到微型端口驱动程序未在 NdisPoll 中向前推进时调用 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;
}
}