DPDK——如何获取数据包 UIO+PMD

(绕蒙圈了已经)可直接跳到后面看结论.目前已经不影响项目进展,中间部分当作素材,留着以后看看能不能整理出来吧。

这里主要是想搞清楚具体的,从网卡拿到数据包的流程是什么样的。正常来说,数据包进入网卡队列,由网卡驱动将数据包拷贝到内核缓冲区,然后进行封装,封装成sk_buffer然后就上交内核协议栈了。那么在我们的程序中,我们想要直接拿到网卡队列中的原始数据包。

0.DPDK为我们提供的接口

想要达到这样的目的很简单,因为dpdk已经为我们提供了这样的接口:

1
2
3
static inline uint16_t
rte_eth_rx_burst(uint16_t port_id, uint16_t queue_id,
struct rte_mbuf **rx_pkts, const uint16_t nb_pkts)

其中,第一个参数代表我们想要从哪个网卡端口获取数据包,第二个参数代表我们想要从哪个队列获取数据包,第三个参数是存放数据包数组指针,这个结构体用于存放原始数据包,第四个参数用于限制依次读取数据包的大小。

1.接口中重要的两步

在上一部分提到的接口函数的定义中找到两句比较重要的。

1
2
3
struct rte_eth_dev *dev = &rte_eth_devices[port_id];
nb_rx = (*dev->rx_pkt_burst)(dev->data->rx_queues[queue_id],
rx_pkts, nb_pkts);

通过port_id拿到我们具体想要在哪个网卡端口获取数据包。代表网卡端口设备的结构体定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
struct rte_eth_dev {
eth_rx_burst_t rx_pkt_burst; /**< Pointer to PMD receive function. */
eth_tx_burst_t tx_pkt_burst; /**< Pointer to PMD transmit function. */
eth_tx_prep_t tx_pkt_prepare; /**< Pointer to PMD transmit prepare function. */

eth_rx_queue_count_t rx_queue_count; /**< Get the number of used RX descriptors. */
eth_rx_descriptor_done_t rx_descriptor_done; /**< Check rxd DD bit. */
eth_rx_descriptor_status_t rx_descriptor_status; /**< Check the status of a Rx descriptor. */
eth_tx_descriptor_status_t tx_descriptor_status; /**< Check the status of a Tx descriptor. */

/**
* Next two fields are per-device data but *data is shared between
* primary and secondary processes and *process_private is per-process
* private. The second one is managed by PMDs if necessary.
*/
struct rte_eth_dev_data *data; /**< Pointer to device data. */
void *process_private; /**< Pointer to per-process device data. */
const struct eth_dev_ops *dev_ops; /**< Functions exported by PMD */
struct rte_device *device; /**< Backing device */
struct rte_intr_handle *intr_handle; /**< Device interrupt handle */
/** User application callbacks for NIC interrupts */
struct rte_eth_dev_cb_list link_intr_cbs;
/**
* User-supplied functions called from rx_burst to post-process
* received packets before passing them to the user
*/
struct rte_eth_rxtx_callback *post_rx_burst_cbs[RTE_MAX_QUEUES_PER_PORT];
/**
* User-supplied functions called from tx_burst to pre-process
* received packets before passing them to the driver for transmission.
*/
struct rte_eth_rxtx_callback *pre_tx_burst_cbs[RTE_MAX_QUEUES_PER_PORT];
enum rte_eth_dev_state state; /**< Flag indicating the port state */
void *security_ctx; /**< Context for security ops */

uint64_t reserved_64s[4]; /**< Reserved for future fields */
void *reserved_ptrs[4]; /**< Reserved for future fields */
} __rte_cache_aligned;

这个结构体与每一个以太网设备关联。可以这样理解,这就是网卡的用户态驱动程序。可以看到其中就定义了我们接收数据包所需要的rx_pkt_burst。也就是说,根据我们实际使用的网卡型号,我们在初始化网络设备的时候,会对这个数据结构进行相应的赋值。以实现对应网卡的对应功能。这个在运行dpdk程序之前需要我们去绑定用户态驱动。这个不用多说,绑定之后由dpdk去为我们检测。比如在程序中我们调用

1
2
3
int rte_eth_dev_count_avail() //就可以查看当前绑定了用户态驱动的网卡设备数量。
rte_eth_macaddr_get((uint8_t) m_Id, &etherAddr); //获取mac地址
rte_eth_dev_get_mtu((uint8_t) m_Id, &m_DeviceMtu);//获取最大帧长

等等等等。这些信息都源自下面这个结构体,通过下面的结构体存放这绑定了用户态驱动的网卡端口任何信息,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
struct rte_eth_dev_info {
struct rte_device *device; /** Generic device information */
const char *driver_name; /**< Device Driver name. */
unsigned int if_index; /**< Index to bound host interface, or 0 if none.
Use if_indextoname() to translate into an interface name. */
uint16_t min_mtu; /**< Minimum MTU allowed */
uint16_t max_mtu; /**< Maximum MTU allowed */
const uint32_t *dev_flags; /**< Device flags */
uint32_t min_rx_bufsize; /**< Minimum size of RX buffer. */
uint32_t max_rx_pktlen; /**< Maximum configurable length of RX pkt. */
/** Maximum configurable size of LRO aggregated packet. */
uint32_t max_lro_pkt_size;
uint16_t max_rx_queues; /**< Maximum number of RX queues. */
uint16_t max_tx_queues; /**< Maximum number of TX queues. */
uint32_t max_mac_addrs; /**< Maximum number of MAC addresses. */
uint32_t max_hash_mac_addrs;
/** Maximum number of hash MAC addresses for MTA and UTA. */
uint16_t max_vfs; /**< Maximum number of VFs. */
uint16_t max_vmdq_pools; /**< Maximum number of VMDq pools. */
struct rte_eth_rxseg_capa rx_seg_capa; /**< Segmentation capability.*/
uint64_t rx_offload_capa;
/**< All RX offload capabilities including all per-queue ones */
uint64_t tx_offload_capa;
/**< All TX offload capabilities including all per-queue ones */
uint64_t rx_queue_offload_capa;
/**< Device per-queue RX offload capabilities. */
uint64_t tx_queue_offload_capa;
/**< Device per-queue TX offload capabilities. */
uint16_t reta_size;
/**< Device redirection table size, the total number of entries. */
uint8_t hash_key_size; /**< Hash key size in bytes */
/** Bit mask of RSS offloads, the bit offset also means flow type */
uint64_t flow_type_rss_offloads;
struct rte_eth_rxconf default_rxconf; /**< Default RX configuration */
struct rte_eth_txconf default_txconf; /**< Default TX configuration */
uint16_t vmdq_queue_base; /**< First queue ID for VMDQ pools. */
uint16_t vmdq_queue_num; /**< Queue number for VMDQ pools. */
uint16_t vmdq_pool_base; /**< First ID of VMDQ pools. */
struct rte_eth_desc_lim rx_desc_lim; /**< RX descriptors limits */
struct rte_eth_desc_lim tx_desc_lim; /**< TX descriptors limits */
uint32_t speed_capa; /**< Supported speeds bitmap (ETH_LINK_SPEED_). */
/** Configured number of rx/tx queues */
uint16_t nb_rx_queues; /**< Number of RX queues. */
uint16_t nb_tx_queues; /**< Number of TX queues. */
/** Rx parameter recommendations */
struct rte_eth_dev_portconf default_rxportconf;
/** Tx parameter recommendations */
struct rte_eth_dev_portconf default_txportconf;
/** Generic device capabilities (RTE_ETH_DEV_CAPA_). */
uint64_t dev_capa;
/**
* Switching information for ports on a device with a
* embedded managed interconnect/switch.
*/
struct rte_eth_switch_info switch_info;

uint64_t reserved_64s[2]; /**< Reserved for future fields */
void *reserved_ptrs[2]; /**< Reserved for future fields */
};

应用程序中往往执行下面的两行代码就可以拿到,

1
2
rte_eth_dev_info portInfo;
rte_eth_dev_info_get(Id, &portInfo);

id一般从0开始,如果系统中有两个网卡绑定了用户态驱动,那么通过0,1就可以获取这两张网卡设备的信息。

查了一下自己用的igb_uio驱动的源码,比较重要的映射功能实现主要是下面的probe函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
static int
#endif
igbuio_pci_probe(struct pci_dev *dev, const struct pci_device_id *id)
{
struct rte_uio_pci_dev *udev;
dma_addr_t map_dma_addr;
void *map_addr;
int err;

#ifdef HAVE_PCI_IS_BRIDGE_API
if (pci_is_bridge(dev)) {
dev_warn(&dev->dev, "Ignoring PCI bridge device\n");
return -ENODEV;
}
#endif

udev = kzalloc(sizeof(struct rte_uio_pci_dev), GFP_KERNEL);
if (!udev)
return -ENOMEM;

/*
* enable device: ask low-level code to enable I/O and
* memory
*/
err = pci_enable_device(dev);
if (err != 0) {
dev_err(&dev->dev, "Cannot enable PCI device\n");
goto fail_free;
}

/* enable bus mastering on the device */
pci_set_master(dev);

/* remap IO memory */
err = igbuio_setup_bars(dev, &udev->info);
if (err != 0)
goto fail_release_iomem;

/* set 64-bit DMA mask */
err = pci_set_dma_mask(dev, DMA_BIT_MASK(64));
if (err != 0) {
dev_err(&dev->dev, "Cannot set DMA mask\n");
goto fail_release_iomem;
}

err = pci_set_consistent_dma_mask(dev, DMA_BIT_MASK(64));
if (err != 0) {
dev_err(&dev->dev, "Cannot set consistent DMA mask\n");
goto fail_release_iomem;
}

/* fill uio infos */
udev->info.name = "igb_uio";
udev->info.version = "0.1";
udev->info.irqcontrol = igbuio_pci_irqcontrol;
udev->info.open = igbuio_pci_open;
udev->info.release = igbuio_pci_release;
udev->info.priv = udev;
udev->pdev = dev;
atomic_set(&udev->refcnt, 0);

err = sysfs_create_group(&dev->dev.kobj, &dev_attr_grp);
if (err != 0)
goto fail_release_iomem;

/* register uio driver */
err = uio_register_device(&dev->dev, &udev->info);
if (err != 0)
goto fail_remove_group;

pci_set_drvdata(dev, udev);

/*
* Doing a harmless dma mapping for attaching the device to
* the iommu identity mapping if kernel boots with iommu=pt.
* Note this is not a problem if no IOMMU at all.
*/
map_addr = dma_alloc_coherent(&dev->dev, 1024, &map_dma_addr,
GFP_KERNEL);
if (map_addr)
memset(map_addr, 0, 1024);

if (!map_addr)
dev_info(&dev->dev, "dma mapping failed\n");
else {
dev_info(&dev->dev, "mapping 1K dma=%#llx host=%p\n",
(unsigned long long)map_dma_addr, map_addr);

dma_free_coherent(&dev->dev, 1024, map_addr, map_dma_addr);
dev_info(&dev->dev, "unmapping 1K dma=%#llx host=%p\n",
(unsigned long long)map_dma_addr, map_addr);
}

return 0;

fail_remove_group:
sysfs_remove_group(&dev->dev.kobj, &dev_attr_grp);
fail_release_iomem:
igbuio_pci_release_iomem(&udev->info);
pci_disable_device(dev);
fail_free:
kfree(udev);

return err;
}

1
err = igbuio_setup_bars(dev, &udev->info);

函数的作用是读取当前设备的所有PCI BAR的信息,并存储到uio_info结构体中。PCI BAR是一种存储基地址的寄存器,它可以指示设备内部的内存或I/O端口的位置和大小。这些信息在后续注册UIO设备时需要使用,以便用户程序可以通过mmap函数映射/dev/uioX文件到用户空间的虚拟地址,并根据偏移量访问不同的PCI BAR区域。

看到下面这两个指针的定义就觉得不简单

1
2
3
4
5
6
dma_addr_t map_dma_addr;
void *map_addr;
...

//之后
map_addr = dma_alloc_coherent(&dev->dev, 1024, &map_dma_addr,GFP_KERNEL);

dma_alloc_coherent是一个用于分配一致性DMA内存的函数。一致性DMA内存是指可以被设备和CPU同时访问而不需要考虑缓存效应的内存。

dma_alloc_coherent函数接受四个参数:第一个参数dev是一个struct device指针,用于指定要执行DMA的设备;第二个参数size是要分配的内存大小;第三个参数是一个dma_addr_t指针,用于返回分配的内存的DMA地址;flag是一个gfp_t类型,用于指定分配内存时的标志位,如GFP_KERNEL等。

dma_alloc_coherent函数返回两个值:用于CPU访问的虚拟地址cpu_addr和用于设备访问的DMA地址dma_addr_t。这两个地址可能不相同,因为可能存在物理地址和总线地址之间的转换。因此,在CPU端要使用cpu_addr来操作分配的内存,在设备端要使用dma_addr_t来作为DMA源或目标地址

经历这步之后,打印信息

1
2
3
dev_info(&dev->dev, "mapping 1K dma=%#llx host=%p\n",
(unsigned long long)map_dma_addr, map_addr);

合理推测(不一定对哈),到时候数据包从网卡队列里头拿出来应该就是拿到这里面。

可以将PCI设备的内存空间映射到用户空间,并提供中断处理功能。而PMD驱动程序由具体的网卡驱动程序支持。

那直接总结好了:

首先运行dpdk程序会要求我们将网卡绑定UIO驱动。我绑定的是igb_uio

UIO驱动是一种用户空间驱动,它可以让用户空间的程序直接访问和控制某些类型的设备,而不需要编写复杂的内核模块。UIO驱动的优点是简化了驱动程序的开发和维护,降低了内核崩溃的风险,提高了驱动程序的更新效率。UIO驱动适用于那些具有可映射的内存空间,可以通过内存写入来控制,通常会产生中断,并且不属于标准内核子系统范畴的设备。

UIO驱动的工作原理是通过一个字符设备文件和一些sysfs属性文件来访问和管理设备。字符设备文件通常命名为/dev/uio0, /dev/uio1等等。用户空间的程序可以通过mmap()函数将设备文件映射到自己的地址空间,并通过读写该地址空间来访问和控制设备的寄存器或RAM位置。用户空间的程序还可以通过read()或select()函数来等待和处理设备产生的中断。sysfs属性文件则提供了一些关于设备名称、版本、映射大小、中断号等信息。

(Poll Mode Drivers)网卡驱动。该驱动由用户态的 API 以及 PMD Driver 构成,内核态的 UIO Driver 屏蔽了网卡发出的中断信号,然后由用户态的 PMD Driver 采用主动轮询的方式。除了链路状态通知仍必须采用中断方式以外,均使用无中断方式直接操作网卡设备的接收和发送队列。

PMD Driver 从网卡上接收到数据包后,会直接通过 DMA 方式传输到预分配的内存中,同时更新无锁环形队列中的数据包指针,不断轮询的应用程序很快就能感知收到数据包,并在预分配的内存地址上直接处理数据包,这个过程非常简洁。

PMD 极大提升了网卡 I/O 性能。此外,PMD 还同时支持物理和虚拟两种网络接口,支持 Intel、Cisco、Broadcom、Mellanox、Chelsio 等整个行业生态系统的网卡设备,以及支持基于 KVM、VMware、 Xen 等虚拟化网络接口。PMD 实现了 Intel 1GbE、10GbE 和 40GbE 网卡下基于轮询收发包。

UIO+PMD,前者零拷贝,后者主动轮询避免了硬中断,DPDK 从而可以在用户态进行收发包的处理。带来了零拷贝(Zero Copy)、无系统调用(System call)的优化。同时,还避免了软中断的异步处理,也减少了上下文切换带来的 Cache Miss。

igb_uio是一种PCI驱动,它可以将网卡设备绑定到用户态,并绕开内核的网络栈。它的主要工作流程如下:

  • igb_uio模块初始化时,向内核注册一个pci_driver结构体,但不指定任何设备id。
  • 用户通过dpdk-devbind.py脚本将某个网卡设备绑定到igb_uio驱动,触发probe回调函数。
  • probe回调函数中,首先使能当前设备,并设置DMA模式。
  • 然后调用igbuio_setup_bars函数,读取当前设备的所有PCI BAR的信息,并存储到uio_info结构体中。PCI BAR是一种存储基地址的寄存器,它可以指示设备内部的内存或I/O端口的位置和大小。
  • 接着初始化UIO设备的文件操作函数,如open、release、irqcontrol等。
  • 最后调用uio_register_device函数,将当前设备注册为UIO设备,并在sysfs文件系统下创建/dev/uioX文件接口。
  • 用户程序可以通过mmap函数,映射/dev/uioX文件到用户空间的虚拟地址,并根据偏移量访问不同的PCI BAR区域。这样就可以直接与网卡设备进行通信了。

DPDK是一种数据平面开发工具包,它可以实现绕过内核的功能,提高网络I/O性能。DPDK的主要方法是:

  • 使用UIO或VFIO等机制,将网卡设备从内核驱动转移到用户态驱动,这样可以避免中断和系统调用的开销,直接访问网卡寄存器和描述符123
  • 使用大页内存和物理连续的内存池,分配和管理数据包缓冲区,这样可以减少TLB的开销,提高缓存命中率,降低延迟45
  • 使用轮询模式的驱动,不依赖于中断,而是主动检查网卡的状态,并处理数据包3

通过这些方法,DPDK可以实现不在内核的高性能网络I/O。

结论!!!!

网卡设备绑定igb_uio之后会将自己的硬件内部的内存空间 保存在sysfs的目录下,用户可以将这部分空间映射到用户空间。

1
2
3
static inline uint16_t
rte_eth_rx_burst(uint16_t port_id, uint16_t queue_id,
struct rte_mbuf **rx_pkts, const uint16_t nb_pkts)

所以说这个接口底层逻辑就是通过PMD直接从网卡的硬件获取数据包保存在rte_mbuf

至于PMD的好处!

PMD可以直接把网卡上的数据包存放到用户空间,是因为它使用了一些特殊的机制,如:

  • UIO或VFIO等机制,可以将网卡设备从内核驱动转移到用户态驱动,这样可以避免中断和系统调用的开销,直接访问网卡寄存器和描述符
  • 大页内存和物理连续的内存池,可以分配和管理数据包缓冲区,这样可以减少TLB的开销,提高缓存命中率,降低延迟。

通过这些机制,PMD可以实现不经过内核的高效数据传输。