Transmit and receive queues

Overview

Packet queues, or datapath queues are objects introduced in NetAdapterCx to enable client drivers to model their hardware features, such as hardware transmit and receive queues, more explicitly in software drivers. This topic explains how to work with transmit and receive queues in NetAdapterCx.

When your client driver calls NET_ADAPTER_DATAPATH_CALLBACKS_INIT, typically from its EVT_WDF_DRIVER_DEVICE_ADD event callback function, it provides two queue creation callbacks: EVT_NET_ADAPTER_CREATE_TXQUEUE and EVT_NET_ADAPTER_CREATE_RXQUEUE. The client creates transmit and receive queues in these callbacks respectively.

The framework empties queues before transitioning to a low power state and deletes them before deleting the adapter.

Creating packet queues

When creating a packet queue, either a transmit queue or a receive queue, the client must provide pointers to the following three callback functions:

In addition, the client can provide these optional callback functions after initializing the queue configuration structure:

Creating a transmit queue

NetAdapterCx calls EVT_NET_ADAPTER_CREATE_TXQUEUE at the very end of the power-up sequence. During this callback, client drivers typically do the following:

  • Optionally register start and stop callbacks for the queue.
  • Call NetTxQueueInitGetQueueId to retrieve the identifier of the transmit queue to set up.
  • Call NetTxQueueCreate to allocate a queue.
    • If NetTxQueueCreate fails, the EvtNetAdapterCreateTxQueue callback function should return an error code.
  • Query for packet extension offsets.

The following example shows how these steps might look in code. Error handling code has been left out of this example for clarity.

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;
}

Creating a receive queue

To create a receive queue from EVT_NET_ADAPTER_CREATE_RXQUEUE, use the same pattern as a transmit queue and call NetRxQueueCreate.

The following example shows how creating a receive queue might look in code. Error handling code has been left out of this example for clarity.

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;
}

Polling model

The NetAdapter data path is a polling model, and the polling operation on one packet queue is completely independent of other queues. The polling model is implemented by calling the client driver's queue advance callbacks, as shown in the following figure:

Diagram that shows the polling flow in NetAdapterCx.

Advancing packet queues

The sequence of a polling operation on a packet queue is as follows:

  1. The OS gives buffers to the client driver for either transmitting or receiving.
  2. The client driver programs the packets to hardware.
  3. The client driver returns the completed packets to the OS.

Polling operations occur within the client driver's EvtPacketQueueAdvance callback function. Each packet queue in a client driver is backed by underlying data structures called net rings, which contain or link to the actual network data buffers in system memory. During EvtPacketQueueAdvance, client drivers carry out send and receive operations on the net rings by controlling indices within the rings, transferring buffer ownership between hardware and the OS as data is transmitted or received.

For more information about net rings, see Introduction to net rings.

For an example of implementing EvtPacketQueueAdvance for a transmit queue, see Sending network data with net rings. For an example of implementing EvtPacketQueueAdvance for a receive queue, see Receiving network data with net rings.

Enabling and disabling packet queue notification

When a client driver receives new packets in a packet queue's net rings, NetAdapterCx invokes the client driver's EvtPacketQueueSetNotificationEnabled callback function. This callback indicates to a client driver that polling (of EvtPacketQueueAdvance or EvtPacketQueueCancel) will stop and will not continue until the client driver calls NetTxQueueNotifyMoreCompletedPacketsAvailable or NetRxQueueNotifyMoreReceivedPacketsAvailable. Typically, a PCI device uses this callback to enable Tx or Rx interrupts. Once an interrupt is received, interrupts can be disabled again and the client driver calls NetTxQueueNotifyMoreCompletedPacketsAvailable or NetRxQueueNotifyMoreReceivedPacketsAvailable to trigger the framework to begin polling again.

Enabling and disabling notification for a transmit queue

For a PCI NIC, enabling transmit queue notification typically means enabling the transmit queue's hardware interrupt. When the hardware interrupt fires, the client calls NetTxQueueNotifyMoreCompletedPacketsAvailable from its DPC.

Similarly, for a PCI NIC, disabling queue notification means disabling the interrupt associated with the queue.

For a device that has an asynchronous I/O model, the client typically uses an internal flag to track the enabled state. When an asynchronous operation completes, the completion handler checks this flag and calls NetTxQueueNotifyMoreCompletedPacketsAvailable if it is set.

If NetAdapterCx calls EvtPacketQueueSetNotificationEnabled with NotificationEnabled set to FALSE, the client must not call NetTxQueueNotifyMoreCompletedPacketsAvailable until NetAdapterCx next calls this callback function with NotificationEnabled set to TRUE.

For example:

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);
}

Enabling and disabling notification for a receive queue

For a PCI NIC, enabling receive queue notification looks very similar to a Tx queue. This typically means enabling the receive queue's hardware interrupt. When the hardware interrupt fires, the client calls NetRxQueueNotifyMoreReceivedPacketsAvailable from its DPC.

For example:

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);
}

For a USB device, or any other queue with a software receive completion mechanism, the client driver should track in its own Context whether the queue's notification is enabled. From the completion routine (triggered for example when a message becomes available in the USB continuous reader), call NetRxQueueNotifyMoreReceivedPacketsAvailable if the notification is enabled. The following example shows how you might do this.

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);
    }
}

Canceling packet queues

When the OS stops the data path, it begins by invoking the client driver's EvtPacketQueueCancel callback function. This callback is where client drivers perform any processing needed before the framework deletes the packet queues. Canceling for a transmit queue is optional and depends on whether the hardware supports in-flight transmit cancellation, but canceling for a receive queue is required.

During EvtPacketQueueCancel, drivers return packets to the OS as needed. For code examples of transmit queue and receive queue cancellation, see Canceling network data with net rings.

After calling the driver's EvtPacketQueueCancel callback, the framework continues to poll the driver's EvtPacketQueueAdvance callback until all packets and buffers have been returned to the OS.