由来
单核 -> 多核
2005年以来CPU的发展已经从提升频率变为增加核心, ARM, MIPS, Power处理器也是如此. 同时高速网卡技术(如40Gbps, 100Gbps)也成为主流.
网络处理器 -> x86通用多核硬件平台
x86通用服务器上单核小包收发已达57Mp/s.
内核态+中断 -> 用户态+轮询
传统上的网卡驱动程序运行在内核态, 当时CPU运行速度远高于外设访问, 所以中断方式很有效; 但目前网卡速度已达40Gbps甚至100Gbps, CPU主频仍在3GHz左右, I/O超越CPU的运行速率, 是行业面临的技术挑战. 用轮询来处理高速端口开始成为必然, 构成了DPDK运行的基础.
硬件为主 -> 软件为主
之前的通信设备一般采用嵌入式的实现, 但x86的高性能可以降低成本, 提高硬件通用化, 令以软件为主体的网络设备成为可能. SDN / NFV / 虚拟化.
开源, 主流公司与厂商支持
基于IA多核处理器的dpdk, 可以很好地解决高性能数据包处理的问题, 而解决这个问题, 更多的是从工程优化角度的迭代和最佳实践的融合. 这些技术可大致总结如下:
轮询
避免中断上下文切换的开销, 缺点是CPU占用持续过高.
用户态驱动
避免内核态到用户态不必要的内存拷贝和系统调用, 对mbuf结构的重新定义, 对网卡DMA操作的重新优化可以获得更好的性能.
亲和性与独占
利用线程的CPU亲和绑定, 避免线程在不同核心间频繁切换; 限制某些核心不参与Linux系统调试, 可使线程独占该核心, 避免cache miss和多任务切换开销.
降低访存开销
有效利用cache, 利用大页内存降低TLB miss, 利用内存多通道的交错访问提高内存访问的带宽, 利用对于内存非对称性的感知避免额外的访存延迟等.
软件调优
一系列调优实践, 如cache line对齐, 避免多核间共享, prefetch, 多元数据批量操作等等.
利用IA新硬件技术
利用IA最新指令集及其他新功能, 如DDIO, SMID, 超标量(superscalar)技术, 数据层面和指令层面的深度并行化等.
充分挖掘网卡潜能
充分利用现代网卡所支持的分流(如RSS, FDIR等)和卸载(如Chksum, TSO等)等功能
DPDK, 主要以IA(Intel Architecture) 多核处理器为目标平台。 在IA上, 网络数据包处理远早于DPDK而存在。 从商业版的Windows到开源的Linux操作系统, 所有跨主机通信几乎都会涉及网络协议栈以及底层网卡驱动对于数据包的处理。 然而, 低速网络与高速网络处理对系统的要求完全不一样。
操作系统中,最容易造成性能下降的是线程的调度,尤其是核间线程的切换,最容易造成cache miss和cache write back。所以在DPDK中利用的是线程的CPU亲和绑定的方式,来指定任务到不同的核上。再进一步,可以限制一些核不参与Linux的系统调度,这样就可以达到任务独占的目的,最大限度地避免了cache不命中带来的性能下降。
查阅DPDK资料,发现DPDK中的多线程是基于linux系统里的pthread实现的,lcore指的是EAL线程,并且在命令行参数中使用“-c”带十六进制参数作为coremask,该掩码的意义是为二进制数上为1的一位即表示将要绑定独占的线程,例如:掩码是16进制的f,二进制对应为1111,即表示cpu0、cpu1、cpu2、cpu3作为逻辑核为程序所用。
eal_short_options[] =
"b:" /* pci-blacklist */
"c:" /* coremask */
"d:" /* driver */
"h" /* help */
"l:" /* corelist */
"m:" /* memory size */
"n:" /* memory channels */
"r:" /* memory ranks */
"v" /* version */
"w:" /* pci-whitelist */ ;
lcore的初始化如下:
rte_eal_cpu_init()函数中,通过读取/sys/devices/system/cpu/cpuX/下的相关信息,确定当前系统有哪些核,以及分别属于哪些socket(这里的socket是NUMA架构中socket,不是网络中的套接字)。
eal_parse_args()函数,解析-c参数,确认哪些核是可以用的,并且设置第一个核为MASTER。
为每一个SLAVE核创建线程,并调用eal_thread_set_affinity()绑定CPU,每个线程的执行的其实是一个主体是while死循环的调用不同模块注册到lcore_config[lcore_id].f的回调函数eal_thread_loop()。
*注:在eal_thread_loop()中,将线程绑定核,然后置于了等待的状态。绑定核函数基于linux原型函数f_pthread_setaffinity_np,在pthread_shim.c中有对各种pthread函数封装的实现。
在DPDK代码中,
rte(runtime environment)开头的函数是作为给开发者直接调用的接口
eal(environment abstraction layer)是DPDK核心库中提供系统抽象的部分
因为虽然现在的源码是基于linux或者FreeBSD系统运行,但它最早期的代码是不依赖于操作系统的,就像自己本身就是个mini-os一样。
rte_eal_init()进行一系列很复杂的初始化工作,
然后RTE_LCORE_FOREACH_SLAVE遍历所有EAL指定可以使用lcore,通过rte_eal_remote_launch在每个lcore上,启动指定的线程。
需要注意的是lcore_id是一个unsigned变量,其实际作用就相当于循环变量i,因为宏RTE_LCORE_FOREACH_SLAVE里会启动for循环来遍历所有可用的核。
在函数rte_eal_remote_launch(int (*f)(void *), void *arg, unsigned slave_id))中,第一个参数是从线程要调用的函数,第二个参数是调用的函数的参数,第三个参数是指定的逻辑核
lcore_config中的pipe_master2slave[2]和pipe_slave2master[2]分别是主线程到从线程核从线程到主线程的管道,与linux中的管道一样,是一个大小为2的数组,数组的第一个元素为读打开,第二个元素为写打开。在这调用了linux库函数read核write,把c作为消息传递。管道的模型如下图所示:
这样,每个从线程通过rte_eal_remote_launch函数运行了自定义函数lcore_hello就打印出了“hello from core #”的输出。