NIC 中的描述符概念

descriptor concept in NIC

我正在尝试理解网络驱动程序代码中使用的 Rx 和 Tx 描述符的概念。

  1. 是软件(RAM)或硬件(NIC 卡)中的描述符。
  2. 它们是如何填充的。

编辑:所以在 Realtek 卡驱动程序代码中。我定义了以下结构。

struct Desc
{
        uint32_t opts1;
        uint32_t opts2;
        uint64_t addr;
};

txd->addr = cpu_to_le64(mapping);
txd->opts2 = cpu_to_le32(opts2);
txd->opts1 = cpu_to_le32(opts1 & ~DescOwn);

那么 opts1 and opts2DescOwn 之类的卡是特定的吗?制造商会在数据表中定义它们吗?

快速回答:

  1. 它们是遵循 NIC 硬件定义的软件结构,因此两者都能理解并可以相互交谈。
  2. 根据供应商定义的合同,它们可以通过任何一种方式填充。可能的场景可能包括但不限于:
    • 由驱动程序(例如,对于由驱动程序准备的空缓冲区由硬件 Rx 接收;对于由驱动程序准备的数据包缓冲区由硬件 Tx 发送)
    • 通过 NIC(例如,对于由硬件为完成的 Rx 数据包写回的数据包缓冲区;对于由硬件指示的已传输的完成的 Tx 数据包缓冲区)

更多建筑细节:

注意:我假设您了解环形数据结构和 DMA 的概念。 https://en.wikipedia.org/wiki/Circular_buffer
https://en.wikipedia.org/wiki/Direct_memory_access

Descriptor,顾名思义,描述一个数据包。它不直接包含数据包数据(据我所知对于 NIC),而是描述数据包,即数据包字节存储在哪里,数据包的长度等。

我将以 RX 路径为例来说明它为什么有用。收到数据包后,NIC 将线路上的 electronic/optical/radio 信号转换为二进制数据字节。然后 NIC 需要通知 OS 它已经收到了一些东西。在过去,这是通过中断完成的,OS 会将字节从 NIC 上的预定义位置读取到 RAM。然而,这很慢,因为 1) CPU 需要参与从 NIC 到 RAM 的数据传输 2) 可能有很多数据包,因此很多中断可能太多而无法处理 CPU。然后 DMA 出现并解决了第一个问题。此外,人们设计了轮询模式驱动程序(或混合模式,如 Linux NAPI),因此 CPU 可以从中断处理中解放出来并一次轮询许多数据包,从而解决第二个问题。

NIC 完成了到字节的信号转换,想要对 RAM 执行 DMA。但在此之前,NIC 需要知道 DMA 到哪里,因为它不能随机将数据放入 RAM 中 CPU 不知道在哪里并且不安全。

因此在 RX 队列初始化期间,NIC 驱动程序预先分配了一些数据包缓冲区,以及一个数据包描述符数组。它根据 NIC 定义初始化每个数据包描述符。

以下是 Intel XL710 NIC 使用的约定(名称已简化以便更好地理解):

/*
   Rx descriptor used by XL710 is filled by both driver and NIC,
 * but at different stage of operations. Thus to save space, it's
 * defined as a union of read (by NIC) and writeback (by NIC).
 *
 * It must follow the description from the data sheet table above.
 * 
 * __leXX below means little endian XX bit field.
 * The endianness and length has to be explicit, the NIC can be used by different CPU with different word size and endianness.
 */

union rx_desc {
    struct {
        __le64 pkt_addr; /* Packet buffer address, points to a free packet buffer in packet_buffer_pool */
        __le64 hdr_addr; /* Header buffer address, normally isn't used */
    } read; /* initialized by driver */

    struct {
        struct {
            struct {
                union {
                    __le16 mirroring_status;
                    __le16 fcoe_ctx_id;
                } mirr_fcoe;
                __le16 l2tag1;
            } lo_dword;
            union {
                __le32 rss; /* RSS Hash */
                __le32 fd_id; /* Flow director filter id */
                __le32 fcoe_param; /* FCoE DDP Context id */
            } hi_dword;
        } qword0;
        struct {
            /* ext status/error/pktype/length */
            __le64 status_error_len;
        } qword1;
    } wb;  /* writeback by NIC */
};

/*
 * Rx Queue defines a circular ring of Rx descriptors
 */
struct rx_queue {
    volatile rx_desc rx_ring[RING_SIZE]; /* RX ring of descriptors */
    struct packet_buffer_pool *pool;     /* packet pool */
    struct packet_buffer *pkt_addr_backup; /* save a copy of packet buffer address for writeback descriptor reuse */
....
}

  1. 驱动程序在 RAM 中分配一些数据包缓冲区(存储在 packet_buffer_pool 数据结构中)。

    pool = alloc_packet_buffer_pool(buffer_size=2048, num_buffer=512);
    
  2. 驱动程序将每个数据包缓冲区的地址放在描述符字段中,如

    rx_ring[i]->read.pkt_addr = pool.get_free_buffer(); 
    
  3. 驱动告诉网卡rx_ring的起始位置,它的长度和head/tail。所以 NIC 会知道哪些描述符是空闲的(因此这些描述符指向的数据包缓冲区是空闲的)。这个过程是通过驱动程序将这些信息写入 NIC 寄存器来完成的(固定的,可以在 NIC 数据表中找到)。

    rx_ring_addr_reg = &rx_ring;
    rx_ring_len_reg = sizeof(rx_ring);
    rx_ring_head = 0; /* meaning all free at start */
    /* rx_ring_tail is a register in NIC as NIC updates it */
    
  4. 现在 NIC 知道描述符 rx_ring[{x,y,z}] 是空闲的并且 {x,y,z}.pkt_addr 可以放入新数据包数据。它继续并将新数据包 DMA 到 {x,y,z}.pkt_addr。同时,NIC 可以预处理(卸载)数据包处理(如校验和验证、提取 VLAN 标记),因此它还需要一些地方来为软件保留这些信息。在这里,描述符在 writeback 上被重用(参见描述符联合中的第二个结构)。然后 NIC 前进 rx_ring 尾指针偏移量,表明 NIC 已经写回了一个新的描述符。[这里的问题是,由于描述符被重新用于预处理结果,驱动程序必须保存 {x,y ,z}.pkt_addr 备份数据结构].

    /* below is done in hardware, shown just for illustration purpose */
    if (rx_ring_head != rx_ring_tail) { /* ring not full */
        copy(rx_ring[rx_ring_tail].read.pkt_addr, raw_packet_data);
        result = do_offload_procesing();
    
        if (pre_processing(raw_packet_data) & BAD_CHECKSUM))      
            rx_ring[rx_ring_tail].writeback.qword1.stats_error_len |= RX_BAD_CHECKSUM_ERROR;
        rx_ring_tail++; /* actually driver sets a Descriptor done indication flag */
                        /* along in writeback descriptor so driver can figure out */
                        /* current HEAD, thus saving a PCIe write message */
    }
    
  5. 驱动程序读取新的尾指针偏移量,发现{x,y,z}有新的数据包。它将从 pkt_addr_backup[{x,y,z}] 和相关预处理结果中读取数据包。

  6. 当上层软件处理完数据包后,{x,y,z} 将放回 rx_ring 并且环头指针将更新以指示空闲描述符。

RX 路径到此结束。 TX 路径几乎是相反的:上层产生数据包,驱动程序将数据包数据复制到 packet_buffer_pool 并让 tx_ring[x].buffer_addr 指向它。驱动程序还在 TX 描述符中准备了一些 TX 卸载标志(例如硬件校验和,TSO)。 NIC 从 RAM 读取 TX 描述符和 DMA tx_ring[x].buffer_addr 到 NIC.

此信息通常出现在 NIC 数据表中,例如 Intel XL710 xl710-10-40-controller-datasheet, Chapter 8.3 & 8.4 LAN RX/TX Data Path。

http://www.intel.com/content/www/us/en/embedded/products/networking/xl710-10-40-controller-datasheet.html

您还可以检查开源驱动程序代码(Linux 内核或某些用户 space 库,如 DPDK PMD),其中包含描述符结构定义。

-- 编辑 1 --

关于 Realtek 驱动程序的其他问题: 是的,这些位是特定于 NIC 的。提示是

这样的行
    desc->opts1 = cpu_to_le32(DescOwn | RingEnd | cp->rx_buf_sz);

DescOwn 是一个位标志,通过设置它告诉 NIC 它现在拥有这个描述符和关联的缓冲区。它还需要从 CPU endianness(可能是 power CPU,即 BE)转换为 NIC 同意理解的 Little Endian。

您可以在 http://realtek.info/pdf/rtl8139cp.pdf 中找到相关信息(例如 DescOwn 的第 70 页),虽然它不像 XL710 那样完整,但至少包含所有 register/descriptor 信息。

-- 编辑 2 --

NIC 描述符是一个非常依赖供应商的定义。如上所示,Intel 的 NIC 描述符使用 same RX 描述符环来提供要写入的 NIC 缓冲区,并让 NIC 写回 RX 信息。还有其他实现,如拆分 RX submission/completion 队列(在 NVMe 技术中更为普遍)。例如,Broadcom 的某些 NIC 具有单个提交环(为 NIC 提供缓冲区)和多个完成环。它专为 NIC 决定并将数据包放在不同的环中,例如不同的流量 class 优先级,以便驱动程序可以首先获取最重要的数据包。 (来自 BCM5756M NIC 程序员指南)

--编辑3--

Intel 通常会公开 NIC datasheet 供 public 下载,而其他供应商可能只向 ODM 公开。在他们的 Intel 82599 系列数据表的第 1.8 节架构和基本操作中描述了 Tx/Rx 流程的非常简短的摘要。