写点什么

DPDK 源码分析之 l2fwd

作者:于顾而言
  • 2022 年 9 月 17 日
    江苏
  • 本文字数:4193 字

    阅读完需:约 14 分钟

什么是 L2 转发

2 层转发,即对应 OSI 模型中的数据链路层,该层以 Mac 帧进行传输,运行在 2 层的比较有代表性的设备就是交换机了。

当交换机收到数据时,它会检查它的目的 MAC 地址,然后把数据从目的主机所在的接口转发出去。

交换机之所以能实现这一功能,是因为交换机内部有一个 MAC 地址表,MAC 地址表记录了网络中所有 MAC 地址与该交换机各端口的对应信息。某一数据帧需要转发时,交换机根据该数据帧的目的 MAC 地址来查找 MAC 地址表,从而得到该地址对应的端口,即知道具有该 MAC 地址的设备是连接在交换机的哪个端口上,然后交换机把数据帧从该端口转发出去。

1.交换机根据收到数据帧中的源 MAC 地址建立该地址同交换机端口的映射,并将其写入 MAC 地址表中。2.交换机将数据帧中的目的 MAC 地址同已建立的 MAC 地址表进行比较,以决定由哪个端口进行转发。3.如数据帧中的目的 MAC 地址不在 MAC 地址表中,则向所有端口转发。这一过程称为泛洪(flood)。4.接到广播帧或组播帧的时候,它立即转发到除接收端口之外的所有其他端口。

DPDK-l2fwd 做了什么

该实例中代码写死的网卡为 promiscuous 混杂模式,我的虚拟机的两个网卡是直连的,拓扑如下:



因此,dpdk-l2fwd 中,port 0 收到包会转发给 port 1,port 1 收到包也会转发给相邻端口 port 0,下图 port 0 混杂模式收到 29694508 个包然后会把这些包都 sent 给 port 1,port 1 同样收到其他包后也会转发给 port 0。

因此,dpdk l2 fwd 这个例子展示了两个网卡在 mac 层成功的转发了数据包,后续我们会阅读源码并调试程序来看,dpdk 是如何实现这一功能的。



Pktgen 安装

pktgen-dpdk 是用于对 DPDK 进行高速数据包测试的工具

使用的命令行参数如下:

-c  : 用于指定运行程序的CPU内核掩码。-n  : 用来指定内存通道。-s  : 如果你想用pktgen发送pcap文件  例如 [-s P:PCAP_file]  -s 0 : 1.pcap    0表示在第0个网卡,1.pcap及文件名-P  : 启动所有网卡,并进入混杂模式,想指定特定网卡 用 -p mask-m  : 指定lcore和port的映射关系 [1].0, [2].1 core1->port0 core2->port2
复制代码

由于 pktgen 是基于 dpdk 进行开发的,因此选取的 pktgen 的版本和 dpdk 的版本一定是相互兼容的,我这边版本如下:

pktgen version:pktgen-dpdk-pktgen-21.03.1
dpdk version:20.11.4-rc1
复制代码

安装过程严格按照 install.md 即可:

//1. 先升级一下gcc版本sudo yum install centos-release-sclsudo yum install devtoolset-7-gcc*scl enable devtoolset-7 bashwhich gcc gcc -version//2. 安装libpcapyum install libpcapyum install dnf-plugins-coreyum install libpcap-devel//3. 环境变量设置export RTE_SDK=<DPDKinstallDir>export RTE_TARGET=x86_64-native-linux-gcc//4. make
复制代码

这样就可以通过参数配置,指定 port 去发包了,下面我通过 port0 发包 10000pkts/s,由于 port0 和 port1 直连,可以看到 port 1 收包 10000pkts/s。

//core0为主控负责命令行接受,流量显示,消息调度//core1->port0 core2->port1./pktgen -l 0-2 -n 3 -- -P -m "[1].0, [2].1"set 0 dst mac 00:0c:29:93:e6:beset 0 count 10000start 0
复制代码




源码阅读



无情 GDB

gdb 并打印一些关键信息,加深 l2fwd 源码理解

l2fwd_parse_args

解析 l2fwd 转发的一些命令行配置,我这边配置的是 set args -- -q 1 -p 0x3 即:



l2fwd_rx_queue_per_lcore:每个逻辑核负责处理一个 rx 队列,后续可以看到网卡配置时一个网卡配置一个 rx 队列和 tx 队列,因此这个参数可以理解为一个逻辑核负责处理一个网卡。

l2fwd_enabled_port_mask:可用的的网卡 port 的掩码

timer_period:多长时间将统计信息输出到 stdout 中,缺省为 10s

转发端口配置

两两一组互为转发,因为我这边就两个 port0 和 port1:



port 0 转给 port 1, port 1 转给 port 0

这里面用到了 rte_eth_devices[portid]->data.owner.id 与 RTE_ETH_DEV_NO_OWNER 进行比较,代表该接管的网卡还没有被使用。

网卡接管

我这边是虚拟机网卡 E1000,因此是在 eal 初始化时调用的是 eth_em_devinit,内容很多后面用到哪个在详细看一下,这边就是 dpdk 通过 igb_uio 用户态驱动接管网卡,并对其进行一些参数的初始化。

//pmd驱动一系列函数,包括设备的启动,停止,混杂模式,广播模式,MTU设置以及Mac地址设置eth_dev->dev_ops = &eth_em_ops;eth_dev->rx_queue_count = eth_em_rx_queue_count;//DD位(Descriptor Done Status)用于标志标识一个描述符buf是否可用。eth_dev->rx_descriptor_done   = eth_em_rx_descriptor_done;eth_dev->rx_descriptor_status = eth_em_rx_descriptor_status;eth_dev->tx_descriptor_status = eth_em_tx_descriptor_status;//收包函数//1、网卡使用DMA写Rx FIFO中的Frame到Rx Ring Buffer中的mbuf,设置desc的DD为1//2、网卡驱动取走mbuf后,设置desc的DD为0,更新RDTeth_dev->rx_pkt_burst = (eth_rx_burst_t)&eth_em_recv_pkts;//发包函数eth_dev->tx_pkt_burst = (eth_tx_burst_t)&eth_em_xmit_pkts;//发包准备函数,offload设置校验以及检验和eth_dev->tx_pkt_prepare = (eth_tx_prep_t)&eth_em_prep_pkts;//mac地址字符串eth_dev->data->mac_addrs = rte_zmalloc("e1000", RTE_ETHER_ADDR_LEN * hw->mac.rar_entry_count, 0);
复制代码



为逻辑核分配 port

根据之前配置的 core 最大可以处理几个网卡 port,我这边是 1 所以,每个 core 赋值一个网卡,可以看到 n_rx_port 都是 1



rte_pktmbuf_pool_create

这个 mbuf pool 主要是给网卡接收数据包提供 mbuf 的,换句话说网卡通过 DMA 收到数据需要把数据包通过 DMA 传送到一块内存,正是这个 mbuf pool 中的内存。这里会创建一个 mbuf 的内存池,每个 muf 的大小为( sizeof(struct rte_mbuf) + priv_size + data_room_size ,总共有 nb_mbufs 个 mbuf。

//收队列数量+发队列数量+一次最大收报文数量+核数*它的cache中报文的数量nb_mbufs = RTE_MAX(nb_ports * (nb_rxd + nb_txd + MAX_PKT_BURST + nb_lcores * MEMPOOL_CACHE_SIZE), 8192U);
复制代码

pktbuf pool 创建使用了下面几个函数,我们逐一 debug:

rte_mempool_create_empty

->rte_mempool_populate_default

->rte_mempool_obj_iter

  • rte_mempool_create_empty

在 memzone 申请内存,这块内存包含 sizeof(struct rte_mempool),每个逻辑核的 cache size 大小,以及私有数据的大小。然后会创建一个空的 mempool 头指针指向这块内存,并挂载到 rte_mempool_tailq 中,这个头包含 sizeof(struct rte_mempool)以及每个逻辑核的 cache 大小,然后会在 memzone 申请所有 pool 内元素所需要的内存,这个内存地址赋给 mp 指针。总结如下:



debug 如下:







  • rte_mempool_populate_default

ring 队列默认为多生产者多消费者模式,eal 初始化时会生成一个 ring 队列 table,用于规定每种 ring 队列的一些函数操作,包括元素入队,出队,遍历等。

为 mp 创建一个 ring 队列,后续会存 mbuf 的指针。mp->flags |= MEMPOOL_F_POOL_CREATED

为 mp 每一个元素分配空间,这里面会做一个 page-aligned address 的操作如下:



然后寻找最大的连续页面进行分配元素,将这些元素指针加入到 mp->elmlist 中,分配的内存块信息记录在 mp->memlist 中,并把这些 mbuf 指针入队 ring,然后会继续寻找连续页面分配元素直到 elt_size。总结如下:



debug 如下:





  • rte_mempool_obj_iter

初始化 mbuf 信息,包括所属内存池、缓存起始地址等。

网卡启动

rte_eth_dev_info_get 获取对应 port 的网卡信息,包括网卡驱动,发送和接收队列个数,mtu 值以及 rx 和 tx 的 hw descriptor(后序给 dma 用的, 里面包括了 内存搬运的起始地址 结束地址 什么的,dma 分析这个结构体完成数据传输。)



然后会通过 eth_dev_rx_queue_config 为 dev->data->tx_queues 和 dev->data->rx_queues 分配指向队列的指针,这里面代码写死了,1 个接收队列 1 个发送队列,然后会调用 setup 函数对收发队列进行初始化。

收队列初始化:

网卡驱动的 rx_queue_setup 函数,由于我是虚拟网卡 e1000,所以这边调用的是 eth_igb_rx_queuesetup,这个函数主要分配了 2 个队列,sw_ring 和 rx_ring,以及网卡相关的寄存器设置。

rx_ring 包含了 E1000_MAX_RING_DESC 个网卡描述符,里面含有 dma 传输的报文数据地址以及报文头部地址,rss hash 值以及报文的校验和,长度等等。

sw_ring 包含了 nb_desc 个 struct igb_rx_entry,也就是 mbuf 地址。

rx_ring 主要存储报文数据的物理地址,物理地址供网卡 DMA 使用,也称为 DMA 地址(硬件使用物理地址,将报文 copy 到报文物理位置上)。sw_ring 主要存储报文数据的虚拟地址,虚拟地址供应用使用(软件使用虚拟地址,读取报文)。

发队列初始化:

和接收队列类似,调用 eth_em_tx_queue_setup。

启动网卡前:

设置报文发送失败的回调函数,这个实例是失败计数并 free 的操作,rte_eth_tx_buffer_count_callback.

启动网卡:

我这边调用的是 e1000 的 pmd 驱动,eth_em_start,函数极其复杂,原谅太菜的我过早的遇到了它,不过这边有个函数比较关键 em_alloc_rx_queue_mbufs。它将 sw_ring 队列和用户的 mbuf pool 内存池关联,并设置 rx_ring 的 dma 地址。总结如下:



debug 如下:



lcore_worker 启动

主核负责统计各个网卡 port 收发包的数量

其他子核负责读取对应的网卡 port 的 rx queue,然后如果有数据包的话就,就循环转发到配置的另一个网卡上。

代码极其简单,关键收发包 api:

(*dev->tx_pkt_burst)(dev->data->tx_queues[queue_id], tx_pkts, nb_pkts);
(*dev->rx_pkt_burst)(dev->data->rx_queues[queue_id], rx_pkts, nb_pkts);
复制代码





我这边就一个核,所以它接收后会自己发出去。

Reference

dpdk应用基础 (豆瓣)

理解物理网卡、网卡接口、内核、IP等属性的关系-CSDN博客

交换机-CSDN博客

dpdk多队列机制-CSDN博客

混杂模式和非混杂模式-CSDN博客

DPDK L2FWD使用 - 简书

在CentOS中升级gcc4.8到gcc5并修改默认设置 - it610.com

dpdk环境搭建+创建dpdk项目,并连接dpdk库_linggang_123的博客-CSDN博客

DPDK PKTGEN使用 - 简书

DPDK 示例之 L2FWD - 知乎

DPDK-Pktgen的使用

网卡基础概念扫盲-CSDN博客

交换机的工作原理-CSDN博客

c/c++ signal(信号)解析-CSDN博客

dpdk网卡收发包分析-ChinaUnix博客

DPDK总结网卡初始化-CSDN博客

网卡offload功能介绍-CSDN博客

dpdk 网卡队列初始化 + 收发包 - tycoon3 - 博客园 (cnblogs.com)

内存池之rte_mempool-CSDN博客

DPDK内存管理-mempool、mbuf-CSDN博客

DPDK数据包与内存专题-mempool内存池 - AISEED - 博客园 (cnblogs.com)

dpdk mbuf之概念理解_ych的专栏-CSDN博客_mbuf

DPDK 网卡收包流程_RToax-CSDN博客_dpdk多队列收包

搞懂Linux零拷贝,DMA_RToax-CSDN博客


发布于: 刚刚阅读数: 3
用户头像

于顾而言

关注

| 且将新火试新茶 × 诗酒趁年华诗酒 2022.09.10 加入

| SASE | SangFor | Senior Developer | 知乎专栏:https://www.zhihu.com/people/whisper-of-the-Koo

评论

发布
暂无评论
DPDK源码分析之l2fwd_DPDK_于顾而言_InfoQ写作社区