DPDK是data plane developme kit的缩写,是一个c语言编写的软件开发框架,常用于高性能网络的开发。它的主要功能就是让用户绕过linux内核协议栈,将网卡收到的数据包直接在用户态空间内使用用户自定义的逻辑去处理数据包,或者将用户态空间的数据包绕过一系列的内核协议栈封装直接从网卡发送出去。这种用户直接和网卡进行交互的模式会减少内核和用户空间之间的内存拷贝,然而这只是其中一个优点。
出现dpdk的原因
linux操作系统是一个集合了计算存储网络等功能的复杂系统,网络对于linux来说只是其中一个功能模块。在早期的操作系统发展阶段,由于网卡和网络传输速度等限制,linux内核处理网络数据包并未出现太大瓶颈。然而随着网卡升级迭代,规格不断从10G到40G再到如今的100G网卡,网络数据包传输速率的瓶颈已经不在网卡。另外由于网络带宽增加,到达网卡的流量也会逐渐增大。在这种情况下操作系统处理网络数据包就会是整个链路的瓶颈。
为什么操作系统内核成为瓶颈
先介绍网卡收包部分之前是如何优化的
网卡收到数据包要经过一系列的操作处理,最后到达用户空间。早期网卡收到数据包会触发硬中断
硬中断是一个注册到cpu的信号,每一个网卡在初始化时,都会注册一个硬中断到cpu,发送信号给cpu,触发内核去进行收包操作。
如果cpu收到硬中断信号就会停下当前处理的任务,切换上下文去处理网卡发送的硬中断。如果每一次网卡触发硬中断cpu都需要立即处理,那cpu可能会话费大量时间去处理这些任务,甚至其他任务会阻塞。因此触发硬中断之后会注册一个软中断去处理,然后迅速去处理之前的任务,这里内核调用raise_softirq()来触发软中断。触发硬中断通常被称为上半部分。下半部分都是软中断处理的。软中断处理有一系列的注册函数,将每个软中断信号和一些函数关联起来。调用open_softirq()函数可以注册函数到某一个中断信号的函数集合softirq_vec中。
为什么用软中断:在内核中硬中断和软中断的优先级不同,一个硬中断信号必须立刻处理,而一个软中断信号优先级较低,可以放到一个软中断队列中稍后处理。这里的立即和稍后都是从cpu的角度来看的。由于cpu的操作时间最小单位都是时钟周期,因此对于cpu来说这个稍后操作也是在若干时钟周期内,谈不上非常的慢。以当前主流cpu的频率为4GHz为例,一秒内包含400亿个时钟周期。
ksoftirqd处理软中断
ksoftirq线程是一个内核级别的线程,每个cpu都有单独的一个ksoftirq线程用于处理本cpu的软中断。当然ksoftirqd也可以处理来自其他核的软中断,然而从性能方面考虑这种方式并不推荐,因为涉及到cpu间通信,有额外的开销。然而如果我们想实现某个类型的中断都由某个cpu处理,可以使用中断亲和性来进行绑定。这种情况下对软中断信号设置了中断亲和性,某一个软中断信号会被调度到指定的CPU上处理,即中断亲和性绑定。
ksoftirqd功能:处理软中断,是一个常驻线程,不会进行创建和销毁。这里会去调用__do_softirq()去遍历被触发的软中断信号,并顺序执行信号对应注册的函数。
__do_softirq功能:真正去处理软中断和执行对应函数,这一步并不会去创建一个子线程,所有函数都在ksoftirqd中执行
引入napi
如果每次收到数据包都经过硬中断和软中断,在流量没那么大情况下不会对负载造成太大问题。当流量到达一定程度时这种两个中断结合在一起的方式也会造成不小的开销。比如收到很多小数据包(当然可以尽量避免小包传输)。为了应对这种情况可以使用napi机制,采取轮询方式处理数据包。网卡设备如果要支持napi,需要在初始化时初始化napi_struct,这个结构体包含了驱动自定义的poll函数用于收包。在收到硬中断之后,并不会调用raise_softirq触发软中断,而是使用ISR(中断服务例程)调用__napi_schedule()函数,将网卡的poll函数注册到poll_list中。在__napi_schedule()中会调用__raise_softirq_irqoff()触发软中断,net_rx_action()会被调用,这个函数会遍历poll_list,执行每个napi_struct函数的poll函数来轮询收包。
问题1:怎么判断poll函数已经执行完成,如果没有数据包了会发生什么
napi_struct中有一个字段budget表示可以接受多少个数据包。这个是为了防止长时间陷入软中断。如果到达budget就会停下。另外一种情况没有数据包就会停止。当这一轮轮询结束之后,会调用napi_complete()函数。这个函数会将设备从poll list移除,同时使能硬中断,cpu不会忽略下一次的硬中断。下一次收到数据包会重复上述步骤。因此设备不会一直陷入poll状态中。
ISR: 一段硬件设备在初始化时注册到内核的代码,在硬中断之后会执行硬件自己的逻辑
上述过程中经过不断优化,napi成为了最终处理数据包的方案。网卡高效的处理数据包之后是如何到达操作系统的呢
网卡的DMA操作会将数据包拷贝到操作系统的内存空间,这一动作之后才触发软中断。在通过DMA之后poll函数会从DMA内存读取数据并转换成内核协议栈可以识别的数据包sk_buff,然后调用__netif_receive_skb()函数将sk_buff送入内核
内核操作
内核收到数据包会进行一系列处理,通常在OSI四层模型中从下向上处理,在物理链路层会进行二层数据包处理,二层主要信息包括目的mac地址48位,源mac地址48位,以及协议类型,内核会根据目的mac和源mac做一系列判断。协议类型是上层协议的类型,即IP层协议类型,在ip层有ip协议和arp协议等。如果数据包携带了vlan信息,需要对vlan进行处理。vlan通常携带一个12位的vlanid,因此vlan号的范围在0到4095之间。如果携带vlan信息,那么这部分就在报文中目的mac之后,上层协议信息之前。
为什么dpdk性能更高
在dpdk出现之前,一个普遍认知是intel architecture不适合处理大量网络数据包。然而dpdk并没有从本质上优化因特尔架构,而是从工程角度找到可以优化收发包的点
比如:
- 如果网卡驱动一直去轮询收包,cpu不会去一直切换上下文
- 驱动是运行在内核态的,如果使用用户态驱动可以避免内存拷贝或者系统调用,优化DMA和mbuf格式
- 优化cache和内存访问。如果在cpu执行任务时一直发生cache miss就需要发生cache交换,如何避免大量的cache miss和将内存指令数据载入到cache中
- 传统UNA架构导致cpu访问内存的速度一致,如果使用NUMA架构,并让cpu尽量访问本地内存就可以大大加速内存访问速度
- 亲和性与独占 如果cpu只需要处理网络数据包并将处理任务固定在某些cpu上,就可以避免cpu之间切换任务,单一cpu也不会切换上下文去处理其他任务
还有一些可以优化的点都会在后面介绍
dpdk主要模块
核心库 core libs 提供大页,线程池,定时器,无锁队列,缓存池等功能 EAL timer ring mbuf mempool malloc
用户态驱动 PMD 支持轮询和线程绑定
精确匹配 LPM 精确匹配 最长匹配 通配符匹配
限速 Qos 原子操作包括限速等
下面以skeleton为例解释如何运行dpdk
eal初始化入口
rte_eal_init()初始化 -> rte_eal_remote_launch()启动线程并绑定到核 -> 每个核都执行传入的函数
rte_eal_init函数 主要做了如下操作
eal_create_cpu_map 获取numa信息并读取cpu信息创建逻辑核映射,读取亲和性信息
rte_eal_cpu_init 初始化cpu和numa相关信息
eal_parse_args 解析启动时的命令行参数
rte_eal_intr_init 初始化中断处理,创建一个中断处理函数
rte_eal_timer_init 初始化定时器
rte_bus_scan 扫描总线设备
后面还有一系列的初始化操作