什么是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-scl
sudo yum install devtoolset-7-gcc*
scl enable devtoolset-7 bash
which gcc
gcc -version
//2. 安装libpcap
yum install libpcap
yum install dnf-plugins-core
yum 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:be
set 0 count 10000
start 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 = ð_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,更新RDT
eth_dev->rx_pkt_burst = (eth_rx_burst_t)ð_em_recv_pkts;
//发包函数
eth_dev->tx_pkt_burst = (eth_tx_burst_t)ð_em_xmit_pkts;
//发包准备函数,offload设置校验以及检验和
eth_dev->tx_pkt_prepare = (eth_tx_prep_t)ð_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多队列收包
DPDK源码分析之l2fwd
原文链接: https://zhuanlan.zhihu.com/p/454346807 原文作者:于顾而言