一、概念

Intel® DPDK全称Intel Data Plane Development Kit,是intel提供的数据平面开发工具集,为Intel architecture(IA)处理器架构下用户空间高效的数据包处理提供库函数和驱动的支持,它不同于Linux系统以通用性设计为目的,而是专注于网络应用中数据包的高性能处理。目前已经验证可以运行在大多数Linux操作系统上。DPDK使用了BSDLicense,极大的方便了企业在其基础上来实现自己的协议栈或者应用。目前出现了很多基于 dpdk 的高性能网络框架,OVS 和 VPP 是常用的数据面框架,mTCP 和 f-stack 是常用的用户态协议栈。

需要强调的是,DPDK应用程序是运行在用户空间上利用自身提供的数据平面库来收发数据包,绕过了Linux内核协议栈对数据包处理过程。Linux内核将DPDK应用程序看作是一个普通的用户态进程,包括它的编译、连接和加载方式和普通程序没有什么两样。如下图2所示DPDK包处理流程绕过了内核直接到用户层进行处理,区别于传统的数据包先到内核最后再到用户层。
基于DPDK的高效包处理系统_用户态
Kni(Kernel NIC Interface)内核网卡接口,是DPDK允许用户态和内核态交换报文的解决方案,例如DPDK的协议栈是专门处理DNS报文,其余的报文通过KNI接口返回给内核来处理。KNI模拟了一个虚拟的网口,提供dpdk的应用程序和linux内核之间通讯。用于DPDK和内核的交互,kni接口允许报文从用户态接收后转发到内核协议栈去。DPDK的包全部在用户空间使用内存池管理,内核空间与用户空间的内存交互不用进行拷贝,只做控制权转移。DPDK的主要对外函数接口都以rte_作为前缀,抽象化函数接口是典型软件设计思路,rte是指runtime environment,eal是指environmentabstraction layer. 下图3是kni的mbuf使用流程图,可以看出报文的流向,因为报文在代码中其实就是一个个内存指针。其中rx_q右边是用户态,左边是内核态。最后通过调用netif_rx()将报文送入linux协议栈,这其中需要将dpdk的mbuf转换成skb_buf。当linux向kni端口发送报文时,调用回调函数kni_net_tx(),然后报文经过转换之后发送到端口上。rte_pktmbut_free()把内存重新释放到mbuf内存池中。
基于DPDK的高效包处理系统_用户态_02

二、DPDK的Helloworld代码示例

HelloWorld是最基础的入门程序,代码简短,功能也不复杂。它建立了一个多核(线程)运行的基础环境,每个线程会打印“hello from core #”,core # 是由操作系统管理的。

int  main(int    argc, char **argv) 
{
int ret;
unsigned lcore_id;
ret = rte_eal_init(argc, argv);

if (ret < 0)
rte_panic("Cannot init EAL\n");

/* call lcore_hello() on every slave lcore */
RTE_LCORE_FOREACH_SLAVE(lcore_id) {
rte_eal_remote_launch(lcore_hello, NULL, lcore_id);
}

/* call it on master lcore too */
lcore_hello(NULL);
rte_eal_mp_wait_lcore();
return 0;
}

对于HelloWorld这个实例,最需要的参数是“-c ”,线程掩码(coremask)指定了需要参与运行的线程(核)集合。rte_eal_init本身所完成的工作是复杂的,它读取入口参数,解析并保存作为DPDK运行的系统信息,依赖这些信息,构建一个针对包处理设计的运行环境。主要动作分解为:配置初始化–>内存初始化–>内存池初始化–>队列初始化–>告警初始化–>中断初始化–>PCI初始化–>定时器初始化–>检测内存本地化(NUMA)–>插件初始化–>主线程初始化–>轮询设备初始化–>建立主从线程通道–>将从线程设置在等待模式–>PCI设备的探测与初始化…对于DPDK库的使用者,这些操作已经被EAL封装起来,接口清晰。如果需要对DPDK进行深度定制,二次开发,需要仔细研究内部操作,详见官方网站https://doc.dpdk.org/guides/prog_guide/。

DPDK面向多核设计,程序会试图独占运行在逻辑核(lcore)上。Main函数里重要部分是启动多核运行环境,RTE_LCORE_FOREACH_SLAVE(lcore_id)如名所示,遍历所有EAL指定可以使用的lcore,然后通过rte_eal_remote_launch在每个lcore上,启动被指定的线程。

​int rte_eal_remote_launch(int (*f)(void *)​​​,
​​​void *arg, unsignedslave_id)​​;

第一个参数是从线程,是被征召的线程,第二个参数是传给从线程的参数,第三个参数是指定的逻辑核,从线程会执行在这个core上。具体来说,int rte_eal_remote_launch(lcore_hello,NULL, lcore_id);参数lcore_id指定了从线程ID,运行入口函数lcore_hello.
运行函数lcore_hello,它读取自己的逻辑核编号(lcore_id), 打印出“hellofrom core #”

static    int 
lcore_hello(__attribute__((unused)) void *arg)
{

unsigned lcore_id;

lcore_id = rte_lcore_id();

printf("hello from core %u\n", lcore_id);

return 0;

}

以上仅是个简单示例,在真实的DPDK处理场景中,该处理函数会是一个循环运行的处理过程。

DPDK也有自身的劣势,对于低负荷的场景不建议使用DPDK:

内核栈转移至用户层增加了开发成本.
低负荷服务器不实用,会造成内核空转.


基于DPDK的高效包处理系统_DPDK_03

学习地址:​​https://ke.qq.com/course/5066203?flowToken=1043068​​​ B站教学:​​https://www.bilibili.com/video/BV1Ju411z773?spm_id_from=333.999.0.0​