本节书摘来自华章出版社《深入浅出dpdk》一书中的第1章,第1.7节实例,作者朱河清,梁存铭,胡雪焜,曹水 等,更多章节内容可以访问云栖社区“华章计算机”公众号查看。
1.7 实例
在对dpdk的原理和代码展开进一步解析之前,先看一些小而简单的例子,建立一个形象上的认知。
1)helloworld,启动基础运行环境,dpdk构建了一个基于操作系统的,但适合包处理的软件运行环境,你可以认为这是个mini-os。最早期dpdk,可以完全运行在没有操作系统的物理核(bare-metal)上,这部分代码现在不在主流的开源包中。
2)skeleton,最精简的单核报文收发骨架,也许这是当前世界上运行最快的报文进出测试程序。
3)l3fwd,三层转发是dpdk用于发布性能测试指标的主要应用。
1.7.1 helloworld
dpdk里的helloworld是最基础的入门程序,代码简短,功能也不复杂。它建立了一个多核(线程)运行的基础环境,每个线程会打印“hello from core #”,core #是由操作系统管理的。如无特别说明,本文里的dpdk线程与硬件线程是一一对应的关系。从代码角度,rte是指runtime environment,eal是指environment abstraction layer。dpdk的主要对外函数接口都以rte_作为前缀,抽象化函数接口是典型软件设计思路,可以帮助dpdk运行在多个操作系统上,dpdk官方支持linux与freebsd。和多数并行处理系统类似,dpdk也有主线程、从线程的差异。
1.初始化基础运行环境
主线程运行入口是main函数,调用了rte_eal_init入口函数,启动基础运行环境。
int rte_eal_init(int argc, char **argv);
入口参数是启动dpdk的命令行,可以是长长的一串很复杂的设置,需要深入了解的读者可以查看dpdk相关的文档与源代码liblibrte_ealcommoneal_common_options.c。对于helloworld这个实例,最需要的参数是“-c ”,线程掩码(core mask)指定了需要参与运行的线程(核)集合。rte_eal_init本身所完成的工作很复杂,它读取入口参数,解析并保存作为dpdk运行的系统信息,依赖这些信息,构建一个针对包处理设计的运行环境。主要动作分解如下
配置初始化
内存初始化
内存池初始化
队列初始化
告警初始化
中断初始化
pci初始化
定时器初始化
检测内存本地化(numa)
插件初始化
主线程初始化
轮询设备初始化
建立主从线程通道
将从线程设置在等待模式
pci设备的探测与初始化
对于dpdk库的使用者,这些操作已经被eal封装起来,接口清晰。如果需要对dpdk进行深度定制,二次开发,需要仔细研究内部操作,这里不做详解。
2.多核运行初始化
dpdk面向多核设计,程序会试图独占运行在逻辑核(lcore)上。main函数里重要的是启动多核运行环境,rte_lcore_foreach_slave(lcore_id)如名所示,遍历所有eal指定可以使用的lcore,然后通过rte_eal_remote_launch在每个lcore上,启动被指定的线程。
int rte_eal_remote_launch(int (f)(void ),
第一个参数是从线程,是被征召的线程;
第二个参数是传给从线程的参数;
第三个参数是指定的逻辑核,从线程会执行在这个core上。
具体来说,int rte_eal_remote_launch(lcore_hello, null, lcore_id);
参数lcore_id指定了从线程id,运行入口函数lcore_hello。
运行函数lcore_hello,它读取自己的逻辑核编号(lcore_id),打印出“hello from core #”
这是个简单示例,从线程很快就完成了指定工作,在更真实的场景里,这个从线程会是一个循环运行的处理过程。
1.7.2 skeleton
dpdk为多核设计,但这是单核实例,设计初衷是实现一个最简单的报文收发示例,对收入报文不做任何处理直接发送。整个代码非常精简,可以用于平台的单核报文出入性能测试。
主要处理函数main的处理逻辑如下(伪码),调用rte_eal_init初始化运行环境,检查网络接口数,据此分配内存池rte_pktmbuf_pool_create,入口参数是指定rte_socket_id(),考虑了本地内存使用的范例。调用port_init(portid, mbuf_pool)初始化网口的配置,最后调用lcore_main()进行主处理流程。
网口初始化流程:
port_init(uint8_t port, struct rte_mempool *mbuf_pool)
首先对指定端口设置队列数,基于简单原则,本例只指定单队列。在收发两个方向上,基于端口与队列进行配置设置,缓冲区进行关联设置。如不指定配置信息,则使用默认配置。
网口设置:对指定端口设置接收、发送方向的队列数目,依据配置信息来指定端口功能
int rte_eth_dev_configure(uint8_t port_id, uint16_t nb_rx_q,
队列初始化:对指定端口的某个队列,指定内存、描述符数量、报文缓冲区,并且对队列进行配置
网口设置:初始化配置结束后,启动端口int rte_eth_dev_start(uint8_t port_id);
完成后,读取mac地址,打开网卡的混杂模式设置,允许所有报文进入。
网口收发报文循环收发在lcore_main中有个简单实现,因为是示例,为保证性能,首先检测cpu与网卡的socket是否最优适配,建议使用本地cpu就近操作网卡,后续章节有详细说明。数据收发循环非常简单,为高速报文进出定义了burst的收发函数如下,4个参数意义非常直观:端口,队列,报文缓冲区以及收发包数。
基于端口队列的报文收发函数:
这就构成了最基本的dpdk报文收发展示。可以看到,此处不涉及任何具体网卡形态,软件接口对硬件没有依赖。
1.7.3 l3fwd
这是dpdk中最流行的例子,也是发布dpdk性能测试的例子。如果将pcie插槽上填满高速网卡,将网口与大流量测试仪表连接,它能展示在双路服务器平台具备200gbit/s的转发能力。数据包被收入系统后,会查询ip报文头部,依据目标地址进行路由查找,发现目的端口,修改ip头部后,将报文从目的端口送出。路由查找有两种方式,一种方式是基于目标ip地址的完全匹配(exact match),另一种方式是基于路由表的最长掩码匹配(longest prefix match,lpm)。三层转发的实例代码文件有2700多行(含空行与注释行),整体逻辑其实很简单,是前续helloworld与skeleton的结合体。
启动这个例子,指定命令参数格式如下:
./build/l3fwd [eal options] -- -p portmask [-p]
--config(port,queue,lcore)[,(port,queue,lcore)]
命令参数分为两个部分,以“--”为分界线,分界线右边的参数是三层转发的私有命令选项。左边则是dpdk的eal options。
[eal options]是dpdk运行环境的输入配置选项,输入命令会交给rte_eal_init处理;
portmask依据掩码选择端口,dpdk启动时会搜索系统认识的pcie设备,依据黑白名单原则来决定是否接管,早期版本可能会接管所有端口,断开网络连接。
config选项指定(port,queue,lcore),用指定线程处理对应的端口的队列。要实现200gbit/s的转发,需要大量线程(核)参与,并行转发。

先来看主线程流程main的处理流程,因为和helloworld与skeleton类似,不详细叙述。
初始化运行环境: rte_eal_init(argc, argv);
分析入参: parse_args(argc, argv)
初始化lcore与port配置
端口与队列初始化,类似skeleton处理
端口启动,使能混杂模式
启动从线程,令其运行main_loop()
从线程执行main_loop()的主要步骤如下:
读取自己的lcore信息完成配置;
读取关联的接收与发送队列信息;
进入循环处理:
{
向指定队列批量发送报文;
从指定队列批量接收报文;
批量转发接收到报文;
}
向指定队列批量发送报文,从指定队列批量接收报文,此前已经介绍了dpdk的收发函数。批量转发接收到的报文是处理的主体,提供了基于hash的完全匹配转发,也可以基于最长匹配原则(lpm)进行转发。转发路由查找方式可以由编译配置选择。除了路由转发算法的差异,下面的例子还包括基于multi buffer原理的代码实现。在#if (enable_multi_buffer_optimize == 1)的路径下,一次处理8个报文。和普通的软件编程不同,初次见到的程序员会觉得奇怪。它的实现有效利用了处理器内部的乱序执行和并行处理能力,能显著提高转发性能。
依据ip头部的五元组信息,利用rte_hash_lookup来查询目标端口。
这段代码在读取报文头部信息时,将整个头部导入了基于sse的矢量寄存器(128位宽),并对内部进行了掩码mask0运算,得到key,然后把key作为入口参数送入rte_hash_lookup运算。同样的操作运算还展示在对ipv6的处理上,可以在代码中参考。
我们并不计划在本节将读者带入代码陷阱中,实际上本书总体上也没有偏重代码讲解,而是在原理上进行解析。如果读者希望了解详细完整的编程指南,可以参考dpdk的网站。