NIC 中的描述符概念
descriptor concept in NIC
我正在尝试理解网络驱动程序代码中使用的 Rx 和 Tx 描述符的概念。
- 是软件(RAM)或硬件(NIC 卡)中的描述符。
- 它们是如何填充的。
编辑:所以在 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 opts2
和 DescOwn
之类的卡是特定的吗?制造商会在数据表中定义它们吗?
快速回答:
- 它们是遵循 NIC 硬件定义的软件结构,因此两者都能理解并可以相互交谈。
- 根据供应商定义的合同,它们可以通过任何一种方式填充。可能的场景可能包括但不限于:
- 由驱动程序(例如,对于由驱动程序准备的空缓冲区由硬件 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 */
....
}
驱动程序在 RAM 中分配一些数据包缓冲区(存储在 packet_buffer_pool 数据结构中)。
pool = alloc_packet_buffer_pool(buffer_size=2048, num_buffer=512);
驱动程序将每个数据包缓冲区的地址放在描述符字段中,如
rx_ring[i]->read.pkt_addr = pool.get_free_buffer();
驱动告诉网卡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 */
现在 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 */
}
驱动程序读取新的尾指针偏移量,发现{x,y,z}有新的数据包。它将从 pkt_addr_backup[{x,y,z}] 和相关预处理结果中读取数据包。
当上层软件处理完数据包后,{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。
您还可以检查开源驱动程序代码(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 流程的非常简短的摘要。
我正在尝试理解网络驱动程序代码中使用的 Rx 和 Tx 描述符的概念。
- 是软件(RAM)或硬件(NIC 卡)中的描述符。
- 它们是如何填充的。
编辑:所以在 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 opts2
和 DescOwn
之类的卡是特定的吗?制造商会在数据表中定义它们吗?
快速回答:
- 它们是遵循 NIC 硬件定义的软件结构,因此两者都能理解并可以相互交谈。
- 根据供应商定义的合同,它们可以通过任何一种方式填充。可能的场景可能包括但不限于:
- 由驱动程序(例如,对于由驱动程序准备的空缓冲区由硬件 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 */
....
}
驱动程序在 RAM 中分配一些数据包缓冲区(存储在 packet_buffer_pool 数据结构中)。
pool = alloc_packet_buffer_pool(buffer_size=2048, num_buffer=512);
驱动程序将每个数据包缓冲区的地址放在描述符字段中,如
rx_ring[i]->read.pkt_addr = pool.get_free_buffer();
驱动告诉网卡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 */
现在 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 */ }
驱动程序读取新的尾指针偏移量,发现{x,y,z}有新的数据包。它将从 pkt_addr_backup[{x,y,z}] 和相关预处理结果中读取数据包。
当上层软件处理完数据包后,{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。
您还可以检查开源驱动程序代码(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 优先级,以便驱动程序可以首先获取最重要的数据包。
--编辑3--
Intel 通常会公开 NIC datasheet 供 public 下载,而其他供应商可能只向 ODM 公开。在他们的 Intel 82599 系列数据表的第 1.8 节架构和基本操作中描述了 Tx/Rx 流程的非常简短的摘要。