NDIS 6.85的轮询模式

以轮询模式出名的是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; 
    } 
}
  • 46
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值