传输和接收队列

概述

数据包队列数据路径队列 是 NetAdapterCx 中引入的对象,使客户端驱动程序能够在软件驱动程序中更明确地为其硬件功能(如硬件传输和接收队列)建模。 本主题说明如何在 NetAdapterCx 中使用传输和接收队列。

当客户端驱动程序调用 NET_ADAPTER_DATAPATH_CALLBACKS_INIT(通常从其 EVT_WDF_DRIVER_DEVICE_ADD 事件回调函数)时,它会提供两个队列创建回调: EVT_NET_ADAPTER_CREATE_TXQUEUEEVT_NET_ADAPTER_CREATE_RXQUEUE。 客户端分别在这些回调中创建传输和接收队列。

框架在转换为低功耗状态之前清空队列,并在删除适配器之前将其删除。

创建数据包队列

创建数据包队列(传输队列或接收队列)时,客户端必须提供指向以下三个回调函数的指针:

此外,客户端可以在初始化队列配置结构后提供这些可选的回调函数:

创建传输队列

NetAdapterCx 在启动序列的末尾调用EVT_NET_ADAPTER_CREATE_TXQUEUE。 在此回调期间,客户端驱动程序通常执行以下操作:

  • (可选)为队列注册开始和停止回调。
  • 调用 NetTxQueueInitGetQueueId 以检索要设置的传输队列的标识符。
  • 调用 NetTxQueueCreate 以分配队列。
    • 如果 NetTxQueueCreate 失败, EvtNetAdapterCreateTxQueue 回调函数应返回错误代码。
  • 查询数据包扩展偏移量。

以下示例演示这些步骤在代码中的外观。 为清楚起见,此示例中已排除错误处理代码。

NTSTATUS
EvtAdapterCreateTxQueue(
    _In_    NETADAPTER          Adapter,
    _Inout_ NETTXQUEUE_INIT *   TxQueueInit
)
{
    NTSTATUS status = STATUS_SUCCESS;

    // Prepare the configuration structure
    NET_PACKET_QUEUE_CONFIG txConfig;
    NET_PACKET_QUEUE_CONFIG_INIT(
        &txConfig,
        EvtTxQueueAdvance,
        EvtTxQueueSetNotificationEnabled,
        EvtTxQueueCancel);

    // Optional: register the queue's start and stop callbacks
    txConfig.EvtStart = EvtTxQueueStart;
    txConfig.EvtStop = EvtTxQueueStop;

    // Get the queue ID
    const ULONG queueId = NetTxQueueInitGetQueueId(TxQueueInit);

    // Create the transmit queue
    NETPACKETQUEUE txQueue;
    status = NetTxQueueCreate(
        TxQueueInit,
        &txAttributes,
        &txConfig,
        &txQueue);

    // Get the queue context for storing the queue ID and packet extension offset info
    PMY_TX_QUEUE_CONTEXT queueContext = GetMyTxQueueContext(txQueue);

    // Store the queue ID in the context
    queueContext->QueueId = queueId;

    // Query checksum packet extension offset and store it in the context
    NET_EXTENSION_QUERY extension;
    NET_EXTENSION_QUERY_INIT(
        &extension,
        NET_PACKET_EXTENSION_CHECKSUM_NAME,
        NET_PACKET_EXTENSION_CHECKSUM_VERSION_1);

    NetTxQueueGetExtension(txQueue, &extension, &queueContext->ChecksumExtension);

    // Query Large Send Offload packet extension offset and store it in the context
    NET_EXTENSION_QUERY_INIT(
        &extension,
        NET_PACKET_EXTENSION_LSO_NAME,
        NET_PACKET_EXTENSION_LSO_VERSION_1);
    
    NetTxQueueGetExtension(txQueue, &extension, &queueContext->LsoExtension);

    return status;
}

创建接收队列

若要从 EVT_NET_ADAPTER_CREATE_RXQUEUE 创建接收队列,请使用与传输队列相同的模式并调用 NetRxQueueCreate

以下示例演示如何在代码中创建接收队列。 为清楚起见,此示例中已排除错误处理代码。

NTSTATUS
EvtAdapterCreateRxQueue(
    _In_ NETADAPTER NetAdapter,
    _Inout_ PNETRXQUEUE_INIT RxQueueInit
)
{
    NTSTATUS status = STATUS_SUCCESS;

    // Prepare the configuration structure
    NET_PACKET_QUEUE_CONFIG rxConfig;
    NET_PACKET_QUEUE_CONFIG_INIT(
        &rxConfig,
        EvtRxQueueAdvance,
        EvtRxQueueSetNotificationEnabled,
        EvtRxQueueCancel);

    // Optional: register the queue's start and stop callbacks
    rxConfig.EvtStart = EvtRxQueueStart;
    rxConfig.EvtStop = EvtRxQueueStop;

    // Get the queue ID
    const ULONG queueId = NetRxQueueInitGetQueueId(RxQueueInit);

    // Create the receive queue
    NETPACKETQUEUE rxQueue;
    status = NetRxQueueCreate(
        RxQueueInit,
        &rxAttributes,
        &rxConfig,
        &rxQueue);

    // Get the queue context for storing the queue ID and packet extension offset info
    PMY_RX_QUEUE_CONTEXT queueContext = GetMyRxQueueContext(rxQueue);

    // Store the queue ID in the context
    queueContext->QueueId = queueId;

    // Query the checksum packet extension offset and store it in the context
    NET_EXTENSION_QUERY extension;
    NET_EXTENSION_QUERY_INIT(
        &extension,
        NET_PACKET_EXTENSION_CHECKSUM_NAME,
        NET_PACKET_EXTENSION_CHECKSUM_VERSION_1); 
          
    NetRxQueueGetExtension(rxQueue, &extension, &queueContext->ChecksumExtension);

    return status;
}

轮询模型

NetAdapter 数据路径是一个轮询模型,对一个数据包队列的轮询操作完全独立于其他队列。 轮询模型是通过调用客户端驱动程序的队列提前回调来实现的,如下图所示:

显示 NetAdapterCx 中的轮询流的示意图。

推进数据包队列

对数据包队列执行轮询操作的顺序如下所示:

  1. OS 为客户端驱动程序提供用于传输或接收的缓冲区。
  2. 客户端驱动程序将数据包程序化到硬件。
  3. 客户端驱动程序将已完成的数据包返回到 OS。

轮询操作发生在客户端驱动程序的 EvtPacketQueueAdvance 回调函数中。 客户端驱动程序中的每个数据包队列都由称为 “网络环”的基础数据结构提供支持,该结构包含或链接到系统内存中的实际网络数据缓冲区。 在 EvtPacketQueueAdvance 期间,客户端驱动程序通过控制环内的索引,在传输或接收数据时在硬件和 OS 之间传输缓冲区所有权,在网络环上执行发送和接收操作。

有关网络环的详细信息,请参阅 Net Ring 简介

有关为传输队列实现 EvtPacketQueueAdvance 的示例,请参阅 使用网络环发送网络数据。 有关为接收队列实现 EvtPacketQueueAdvance 的示例,请参阅 使用网络通道接收网络数据

启用和禁用数据包队列通知

当客户端驱动程序在数据包队列的网环中收到新数据包时,NetAdapterCx 会调用客户端驱动程序的 EvtPacketQueueSetNotificationEnabled 回调函数。 此回调向客户端驱动程序指示,轮询 EvtPacketQueueAdvanceEvtPacketQueueCancel) (将停止,并且不会继续,直到客户端驱动程序调用 NetTxQueueNotifyMoreCompletedPacketsAvailableNetRxQueueNotifyMoreReceivedPacketsAvailable。 通常,PCI 设备使用此回调来启用 Tx 或 Rx 中断。 收到中断后,可以再次禁用中断,客户端驱动程序调用 NetTxQueueNotifyMoreCompletedPacketsAvailableNetRxQueueNotifyMoreReceivedPacketsAvailable 以触发框架再次开始轮询。

为传输队列启用和禁用通知

对于 PCI NIC,启用传输队列通知通常意味着启用传输队列的硬件中断。 当硬件中断触发时,客户端从其 DPC 调用 NetTxQueueNotifyMoreCompletedPacketsAvailable

同样,对于 PCI NIC,禁用队列通知意味着禁用与队列关联的中断。

对于具有异步 I/O 模型的设备,客户端通常使用内部标志来跟踪已启用状态。 异步操作完成后,完成处理程序会检查此标志,并调用 NetTxQueueNotifyMoreCompletedPacketsAvailable (如果已设置)。

如果 NetAdapterCx 调用 EvtPacketQueueSetNotificationEnabled 并将 NotificationEnabled 设置为 FALSE,则客户端不得调用 NetTxQueueNotifyMoreCompletedPacketsAvailable ,直到 NetAdapterCx 接下来调用 NotificationEnabled 设置为 TRUE 的此回调函数。

例如:

VOID
MyEvtTxQueueSetNotificationEnabled(
    _In_ NETPACKETQUEUE TxQueue,
    _In_ BOOLEAN NotificationEnabled
)
{
    // Optional: retrieve queue's WDF context
    MY_TX_QUEUE_CONTEXT *txContext = GetTxQueueContext(TxQueue);

    // If NotificationEnabled is TRUE, enable transmit queue's hardware interrupt
    ...
}

VOID
MyEvtTxInterruptDpc(
    _In_ WDFINTERRUPT Interrupt,
    _In_ WDFOBJECT AssociatedObject
    )
{
    MY_INTERRUPT_CONTEXT *interruptContext = GetInterruptContext(Interrupt);

    NetTxQueueNotifyMoreCompletedPacketsAvailable(interruptContext->TxQueue);
}

为接收队列启用和禁用通知

对于 PCI NIC,启用接收队列通知与 Tx 队列非常相似。 这通常意味着启用接收队列的硬件中断。 当硬件中断触发时,客户端从其 DPC 调用 NetRxQueueNotifyMoreReceivedPacketsAvailable

例如:

VOID
MyEvtRxQueueSetNotificationEnabled(
    _In_ NETPACKETQUEUE RxQueue,
    _In_ BOOLEAN NotificationEnabled
)
{
    // optional: retrieve queue's WDF Context
    MY_RX_QUEUE_CONTEXT *rxContext = GetRxQueueContext(RxQueue);

    // If NotificationEnabled is TRUE, enable receive queue's hardware interrupt
    ...
}

VOID
MyEvtRxInterruptDpc(
    _In_ WDFINTERRUPT Interrupt,
    _In_ WDFOBJECT AssociatedObject
    )
{
    MY_INTERRUPT_CONTEXT *interruptContext = GetInterruptContext(Interrupt);

    NetRxQueueNotifyMoreReceivedPacketsAvailable(interruptContext->RxQueue);
}

对于 USB 设备或任何其他具有软件接收完成机制的队列,客户端驱动程序应在自己的上下文中跟踪是否启用了队列的通知。 从完成例程 (触发,例如,当某个消息在 USB 连续读取器) 中可用时,如果启用了通知,请调用 NetRxQueueNotifyMoreReceivedPacketsAvailable 。 以下示例演示如何执行此操作。

VOID
UsbEvtReaderCompletionRoutine(
    _In_ WDFUSBPIPE Pipe,
    _In_ WDFMEMORY Buffer,
    _In_ size_t NumBytesTransferred,
    _In_ WDFCONTEXT Context
)
{
    UNREFERENCED_PARAMETER(Pipe);

    PUSB_RCB_POOL pRcbPool = *((PUSB_RCB_POOL*) Context);
    PUSB_RCB pRcb = (PUSB_RCB) WdfMemoryGetBuffer(Buffer, NULL);

    pRcb->DataOffsetCurrent = 0;
    pRcb->DataWdfMemory = Buffer;
    pRcb->DataValidSize = NumBytesTransferred;

    WdfObjectReference(pRcb->DataWdfMemory);

    ExInterlockedInsertTailList(&pRcbPool->ListHead,
                                &pRcb->Link,
                                &pRcbPool->ListSpinLock);

    if (InterlockedExchange(&pRcbPool->NotificationEnabled, FALSE) == TRUE)
    {
        NetRxQueueNotifyMoreReceivedPacketsAvailable(pRcbPool->RxQueue);
    }
}

取消数据包队列

当 OS 停止数据路径时,首先调用客户端驱动程序的 EvtPacketQueueCancel 回调函数。 在此回调中,客户端驱动程序在框架删除数据包队列之前执行所需的任何处理。 取消传输队列是可选的,具体取决于硬件是否支持正在进行的传输取消,但需要取消接收队列。

EvtPacketQueueCancel 期间,驱动程序会根据需要将数据包返回到 OS。 有关传输队列和接收队列取消的代码示例,请参阅 使用网络环取消网络数据

调用驱动程序的 EvtPacketQueueCancel 回调后,框架将继续轮询驱动程序的 EvtPacketQueueAdvance 回调,直到所有数据包和缓冲区都返回到 OS。