NDIS 轮询模式

NDIS 轮询模式概述

NDIS 轮询模式是一种操作系统控制的轮询执行模型,可驱动网络接口数据路径。

以前,NDIS 没有对数据路径执行上下文进行正式定义。 NDIS 驱动程序通常依赖于延迟过程调用(DPC)来实现其执行模型。 但是,当发出长指示链并避免此问题时,使用 DPC 可能会使系统不知所措,这需要大量的代码来正确操作。 NDIS 轮询模式提供 DPC 和类似的执行工具的替代方法。

NDIS 轮询模式将计划决策的复杂性从 NIC 驱动程序转移到 NDIS 中,其中 NDIS 设置每次迭代的工作限制。 若要实现此轮询模式,请执行以下操作:

  1. OS 对 NIC 施加回压的机制。

  2. OS 可精细控制中断的机制。

NDIS 轮询模式适用于 NDIS 6.85 及更高版本的微型端口驱动程序。

DPC 模型的问题

下面的序列图演示了 NDIS 微型端口驱动程序如何使用 DPC 处理 Rx 数据包的突发的典型示例。 在此示例中,硬件是 PCIe NIC 的标准。 它具有接收硬件队列和该队列的中断掩码。

显示包含 Rx 数据包和接收硬件队列的 NDIS DPC 模型的示意图。

如果没有网络活动,硬件将启用 Rx 中断。 Rx 数据包到达时:

  1. 硬件生成中断,NDIS 调用驱动程序的 MiniportInterrupt 函数 (ISR)。

  2. 驱动程序在 ISR 中工作很少,因为它们在 IRQL 非常高的情况下运行。 驱动程序禁用 ISR 中的中断,并将硬件处理延迟到 MiniportInterruptDPC 函数 (DPC)。

  3. NDIS 最终调用驱动程序的 DPC,驱动程序会从硬件队列中清空所有完成,并将其指示给 OS。

当驱动程序将 I/O 操作延迟到 DPC 时,两个痛点可能会影响网络堆栈:

  1. 驱动程序不知道系统是否能够处理所指示的所有数据,因此驱动程序别无选择,只能从其硬件队列中清空尽可能多的元素,并指示它们向上堆栈。

  2. 由于驱动程序使用 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 数据包的突发。

显示包含 Rx 数据包和接收硬件队列的 NDIS 轮询模式的关系图。

与 DPC 模型一样,当 Rx 数据包到达硬件时,NDIS 会调用驱动程序的 ISR,驱动程序会禁用 ISR 中的中断。 此时,轮询模式模型会分歧:

  1. 驱动程序 将轮询对象以前创建)从 ISR 排队,而不是对 DPC 进行排队,以通知 NDIS 已准备好处理新工作。

  2. 在将来的某个时候,NDIS 调用驱动程序的 轮询迭代处理程序 来处理工作。 与 DPC 不同,驱动程序不允许指示任意数量的 Rx NBL,因为其硬件队列中已准备好元素。 驱动程序应检查处理程序的轮询数据参数,以获取它可以指示的最大 NBL 数。

    驱动程序提取到它应初始化 NBL 的最大 Rx 数据包数后,将其添加到轮询处理程序提供的 NBL 队列,然后退出回调。 在退出之前,驱动程序不应启用中断。

  3. 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 回调函数中执行以下操作:

  1. 分配专用微型端口上下文。
  2. 分配NDIS_POLL_CHARACTERISTICS结构以指定 NdisPoll 和 NdisSetPollNotification 回调函数的入口点。
  3. 调用 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执行以下操作:

  1. 检查NDIS_POLL_DATA结构的接收参数以获取它可以指示的最大 NBL 数。
  2. 最多提取 Rx 数据包数。
  3. 初始化 NBL。
  4. 将它们添加到NDIS_POLL_RECEIVE_DATA结构提供的 NBL 队列(位于 NdisPoll PollData 参数的NDIS_POLL_DATA结构中)。
  5. 退出回调。

对于传输完成,驱动程序应在 NdisPoll执行以下操作:

  1. 检查NDIS_POLL_DATA结构的传输参数以获取其可完成的最大 NBL 数。
  2. 最多提取 Tx 数据包数。
  3. 完成 NBL。
  4. 将它们添加到NDIS_POLL_TRANSMIT_DATA结构提供的 NBL 队列(位于 NdisPoll PollData 参数NDIS_POLL_DATA结构中)。
  5. 退出回调。

驱动程序不应在退出 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; 
    } 
}