
简介《深入浅出DPDK》全书读书笔记是一份针对DPDK的要点型学习资料适合DPDK初学者、网络平台开发者以及关注NFV/SDN的架构师快速了解高性能网络I/O框架。压缩包内只有一个PDF文件大小6.57MB携带与阅读都很方便当前已有3849人学习下载。笔记从Linux传统网卡驱动的中断处理讲起对比NAPI轮询与Netmap共享包池两种改进思路再系统梳理DPDK用户态驱动、大页内存、无锁环、多队列流分类及PMD、Classify、QoS等核心库能力同时覆盖rte_eal_init的完整初始化动作、数据包收包到路由查找再转发的链路并将SR-IOV、virtio多虚拟化支持与NFV/SDN演进纳入讨论。作者对全书知识点做了模块化归纳既有原理澄清也有实例说明便于开发前建立整体认知也可用于备考冲刺或技术分享前快速回顾重点。1. DPDK 到底是什么先从中断开销扛不住讲起做网络后端的人大概都有过这种经历网卡流量一上来top里看到软中断si占掉好几个核应用进程反而抢不到 CPU。这不是应用写得不好而是传统内核网络路径的设计假设已经过时了。《深入浅出 DPDK》读书笔记的第一章就把这个问题讲得很透早期 CPU 运行速度远高于外设访问速度所以用中断唤醒处理器完全够用可当网卡从千兆迈入万兆每个报文都触发一次中断光中断开销就能把系统拖垮。DPDK 的思路是反过来的——把网卡驱动从内核态搬到用户态用轮询代替中断用大页内存降低 TLB miss用多队列配合多核分摊负载。这套组合拳解决的就是通用服务器能否扛住高速网络处理的问题。如果你在做 NFV、SDN 数据面、或者单纯想把报文转发延迟压下去这份笔记值得逐章读。2. 从中断到轮询用户态驱动与 NAPI 的演进脉络2.1 传统收包路径为什么慢一次报文要过几道关先看传统 Linux 网卡驱动的收包路径。读书笔记里概括了七个动作数据包到达网卡网卡做 DMA网卡发中断唤醒处理器驱动填充读写缓冲区报文进入内核协议栈做高层处理如果应用在用户态还得从内核搬一次数据。这条路径的问题不在于某个单点特别慢而在于每个报文都要完整走一遍中断唤醒有上下文切换成本内核态到用户态的数据拷贝有内存带宽成本协议栈处理还有锁竞争和调度延迟。为了解决中断频繁的问题内核引入了 NAPI 机制。它的策略是系统被中断唤醒后尽量用轮询方式一次处理多个数据包直到网络空闲再重新转入中断等待。这个机制在高吞吐场景下确实提升了效率但它仍然没有解决内核态和用户态之间的数据拷贝问题。Netmap 走了另一条路——用共享数据包池来减少内核到用户空间的包复制。DPDK 则更进一步把整个驱动逻辑放到用户态用 PMDPolling Mode Driver轮询模式驱动来收发报文既规避了不必要的内存拷贝又避免了系统调用。换句话说数据面完全可以避开内核协议栈由应用程序直接控制网卡。2.2 为什么说轮询 用户态是合理的工程取舍很多第一次接触 DPDK 的人会问轮询不是浪费 CPU 吗空闲的时候也死循环这个问题要分场景看。在 NFV 或高速转发场景下网卡几乎永远有包处理轮询模式的收益远大于消耗。即使偶尔空闲DPDK 也提供了rte_eth_rx_burst返回 0 的快速路径这时候 CPU 可以去做其他事情。更关键的是用户态驱动意味着不需要每次收包都陷入内核不需要copy_to_user这类操作cache 的利用率也会高很多。取舍的代价是DPDK 应用程序需要自己管理内存池、自己处理队列、自己保证多核之间的负载均衡。这就是为什么读书笔记里反复强调那些基础组件——核心库提供系统抽象、大页内存、缓存池、定时器和无锁环PMD 库提供全用户态驱动Classify 库支持精确匹配、LPM 最长匹配和 ACL 通配符匹配QoS 库提供限速和调度。这一套组合下来才能支撑用户态收包、查表、转发的完整数据面。2.3 从收发动作看 DPDK 的性能预算读书笔记里给了一组很有价值的数字DPDK 一个核大约每秒能处理 33M 个报文也就是每 30 纳秒处理一个报文假设主频 2.7GHz则每 80 个 CPU 时钟周期就要处理一个报文。而处理一个报文的内存读取动作笔记里列了八步其中带内存读标记的就有六次。如果每次访存都打到 DRAM几百个时钟周期就没了80 个周期的预算根本不够用。所以 DPDK 性能优化的核心其实不在轮询本身而在于怎么让这六次内存读取尽量命中 Cache。这就要靠大页内存降低 TLB miss、靠内存多通道交错访问提高有效带宽、靠 Cache Line 对齐避免 false sharing、靠 DDIO 让网卡直接和 LLC Cache 交换数据。笔记的第二章正是在讲这些底层机制接下来的篇幅我会拆开说。3. 内存子系统的那些玄学TLB、Cache Line、DDIO 与 NUMA3.1 Cache 和 TLB为什么页表查找会拖慢收包处理器从 Cache 读数据只需要几个时钟周期而从内存读要几百个周期。这就是 Cache 存在的意义——匹配处理器和内存之间的速度鸿沟。但 Cache 不是万能的它有两个关键短板一是容量有限二是地址映射需要查表。虚拟地址到物理地址的转换依赖页表页表放在内存中且被频繁访问所以处理器专门设计了 TLB 来缓存页表项。x86 上 4KB 小页的 TLB 覆盖范围很有限一个报文处理过程涉及的数据结构和缓冲区很容易就把 TLB 打穿。DPDK 的解法是用大页内存。2MB 大页让同样数量的 TLB 条目覆盖的内存空间扩大了 512 倍TLB miss 率显著下降。在实际部署时我会在/etc/default/grub里加default_hugepagesz1G hugepagesz1G hugepages16让系统预留 16 个 1G 大页。注意1G 大页需要 CPU 和内核都支持如果要在容器里用还得在 Pod 的resources.limits里声明hugepages-1Gi的用量。3.2 Cache 一致性MESI 协议和 false sharing 的坑多核处理器里每个核都有独占的一级、二级 Cache多个核可能同时读写同一个内存块这就是 Cache 一致性问题。解决机制有两种基于目录的协议和总线窥探协议。MESI 是总线窥探协议的代表每个 Cache Line 有 Modified、Exclusive、Shared、Invalid 四种状态。这四种状态的迁移是有成本的尤其在多个核频繁读写共享数据时一致性协议的交互会拖慢所有核。DPDK 的应对思路很直接尽量避免多个核访问同一个内存地址或数据结构。笔记里给了两个例子。第一个例子是数据结构定义每个核一份自己的lcore_conf并且用__rte_cache_aligned强制 Cache Line 对齐防止结构体横跨两个 Cache Line。第二个例子是网卡队列——网卡支持多队列DPDK 会让每个核绑定独立的接收队列和发送队列避免竞争也就从源头上消除了一致性开销。这个思路在实际编程中要时刻记住不要图省事让多个核共享计数器或标志位那点共享可能比加锁还贵。3.3 DDIO让网卡绕过内存直接和 LLC 对话DDIO 是英特尔处理器提供的一项技术让外部网卡和 CPU 通过 LLC Cache最后一级 Cache直接交换数据绕开内存这个相对慢速的部件。网卡收包时报文和控制结构体通过 PCIe 总线直接写入 CacheCPU 读包时直接从 Cache 拿省掉了内存访问和 Cache 写回的等待。笔记里对比了有无 DDIO 的读数据、写数据流程结论是 DDIO 在理想状况下NIC 和处理器可以完全不用访问内存。但 DDIO 也带来一个副作用报文直接占用 LLC让 LLC 的容量压力变大。这也是为什么英特尔的 E5 系列把 LLC 做到了 20MB。实际调优时如果发现 LLC 命中率异常或者转发延迟抖动可以查一下 DDIO 是否开启。在某些平台上BIOS 里叫Data Direct I/O如果机器是用于存储场景关闭 DDIO 反而可能让性能更稳定。3.4 NUMA 结构内存和 CPU 的距离是真实存在的NUMA 是从 SMP 演化而来的。SMP 系统里所有处理器通过一条总线共享内存和其他硬件资源处理器一多总线就成了瓶颈。NUMA 让每个处理器拥有本地内存和本地 PCIe 总线访问本地资源延迟低、带宽大访问远程资源则要走 QPI 总线延迟高且要和其他处理器竞争。对一个双路服务器来说node0 上的核访问 node0 的内存可能只要 70ns访问 node1 的内存可能要 130ns 甚至更高。DPDK 对 NUMA 的处理原则是本地设备本地处理PCI 设备在 node0 上就用 node0 上的核来处理数据结构和数据缓冲区都从 node0 分配。代码里通常会调rte_zmalloc_socket并传入socket_id参数比如/* 在指定的 NUMA 节点上分配队列结构体 */ q rte_zmalloc_socket(fm10k, sizeof(*q), RTE_CACHE_LINE_SIZE, socket_id);socket_id来自rte_eth_dev_socket_id(port_id)也就是网卡所在的 NUMA 节点。如果网卡在 node0你却在 node1 上分配 rx ring 和 mbuf pool那么每次 DMA 和 CPU 访问都可能跨 NUMA延迟和带宽都会受到不小的影响。这一点在调优时比较容易踩坑因为程序能跑但性能只有理论值的六七成。/* 用 lcore 的 socket_id 创建内存池保证本地化 */ struct rte_mempool *mp rte_pktmbuf_pool_create( mbuf_pool, nb_mbufs, MBUF_CACHE_SIZE, 0, RTE_MBUF_DEFAULT_BUF_SIZE, rte_socket_id());rte_socket_id()返回当前线程所在核的 NUMA 节点创建内存池时明确指定能避免分配在远端内存上。这里还有个容易忽略的参数MBUF_CACHE_SIZE它决定每个核从内存池批量取对象时的本地缓存数量设置太小会让核频繁回池子取对象设置太大又可能浪费内存一般按256或512起步调。4. 数据面主干逻辑从 rte_eal_init 到三层转发查表4.1 rte_eal_init 到底做了什么rte_eal_init是每个 DPDK 程序都要调的第一个函数。它做的事情非常多解析入口参数保存系统运行信息然后构建一个针对包处理设计的运行环境。笔记里列的动作包括配置初始化、内存初始化、内存池初始化、队列初始化、告警初始化、中断初始化、PCI 初始化、定时器初始化、NUMA 检测、插件初始化、主线程初始化、轮询设备初始化、建立主从线程通道、将从线程设为等待模式、PCI 设备探测与初始化。简单说EAL 把DPDK 运行环境这件复杂的事情封装成了一个函数调用。但它在底层做了很多有讲究的取舍比如会检查大页是否可用、检查igb_uio或vfio-pci驱动是否已经绑定到网卡、绑定 CPU 核并设置亲和性等。如果你在启动日志里看到EAL: No available hugepages reported那说明大页没配置好程序会在初始化阶段直接退出。另一个常见问题是/var/run/dpdk目录的权限问题多进程场景下第二个进程会因为锁文件无法创建而失败。在编写 DPDK 应用时rte_eal_init的返回值需要做完整检查。我一般会在调用后立刻判断返回值是否小于 0并打印rte_strerror(rte_errno)这样初始化失败时能快速定位是内存、PCI 还是参数的问题。4.2 网卡多队列让每个核都有活干的关键网卡多队列是高速网卡的通用技术。一个物理网卡可以有多个接收队列和多个发送队列每个队列由不同的核独立收发。DPDK 里通过rte_eth_dev_configure配置队列数量然后对每个队列调用rte_eth_rx_queue_setup绑定到指定 socket 的内存池。要注意配置队列数之前要先查网卡的能力不是你想设多少个就能设多少个。/* 配置网卡为多队列模式 */ struct rte_eth_conf port_conf; memset(port_conf, 0, sizeof(port_conf)); port_conf.rxmode.mq_mode ETH_MQ_RX_NONE; /* 先不开启 RSS按队列绑定核 */ port_conf.txmode.mq_mode ETH_MQ_TX_NONE; uint16_t nb_rx_queue 4; /* 4 个接收队列 */ uint16_t nb_tx_queue 4; /* 4 个发送队列 */ ret rte_eth_dev_configure(port_id, nb_rx_queue, nb_tx_queue, port_conf); if (ret 0) rte_exit(EXIT_FAILURE, Cannot configure device: err%d, port%u\n, ret, port_id);ETH_MQ_RX_NONE表示不开启硬件多队列分流那么每个队列都需要一个核去轮询它。如果开启ETH_MQ_RX_RSS网卡会根据哈希值把报文自动分布到不同队列这通常配合 Flow Director 或 RSS 哈希使用。更常见的做法是让每个核只轮询自己绑定的一组队列这样做的核心收益是避免多核竞争同一个队列的锁或环形缓冲区。笔记里那张四核双网卡的图值得反复看每个核有自己的接收队列和发送队列核 0 从网卡 1 的接收队列 0 收包可以发到网卡 1 的发送队列 0 或网卡 2 的发送队列 0。这种核-队列绑定就是 DPDK 多核扩展的基础。队列配置还有一个细节rte_eth_rx_queue_setup的socket_id参数要和运行该队列的 lcore 所在 NUMA 节点一致。否则队列描述符和 DMA 缓冲区就落到远端内存收包路径上每次访问都要跨 NUMA。4.3 三层转发实例LPM 和精确匹配怎么选笔记里讲了一个三层转发的实例代码有 2700 多行但整体逻辑其实就是 HelloWorld 和 Skeleton 的结合体收包查表转发。关键在查表这一步。路由查找一般有两种方式基于目标 IP 地址的完全匹配exact match和基于路由表的最长前缀匹配Longest Prefix Match, LPM。先看 exact match 的实现。依据 IP 头部的五元组信息构造 key然后调rte_hash_lookup查询目标端口。笔记里给出了一段用 SIMD 指令提取五元组的代码这里我把它拆开解释/* 用 SSE 指令一次性提取五元组 */ mask0 _mm_set_epi32(ALL_32_BITS, ALL_32_BITS, ALL_32_BITS, BIT_8_TO_15); ipv4_hdr (uint8_t *)ipv4_hdr offsetof(struct ipv4_hdr, time_to_live); __m128i data _mm_loadu_si128((__m128i*)(ipv4_hdr)); /* 与操作后得到 dst port, src port, dst IP, src IP, protocol */ key.xmm _mm_and_si128(data, mask0); /* 在哈希表中查找目标端口 */ ret rte_hash_lookup(ipv4_l3fwd_lookup_struct, (const void *)key); return (uint8_t)((ret 0) ? portid : ipv4_l3fwd_out_if[ret]);这段代码的精髓在_mm_loadu_si128和_mm_and_si128一条指令把报文头部从 TTL 字段开始读 128 位再和掩码做与运算直接拿到五元组。掩码BIT_8_TO_15表示 TTL 后的那一个字节是高层协议号ALL_32_BITS则覆盖源目 IP 和端口。这样做的收益是减少访存次数把原来逐字段拷贝的十几条指令压缩到两条。性能调优时如果发现 hash lookup 的命中率低多半是 key 构造不对而不是 hash 库本身的问题。/* LPM 路由查找示例 */ struct rte_lpm *lpm; uint32_t next_hop; uint32_t dst_ip rte_be_to_cpu_32(ipv4_hdr-dst_addr); /* 按目的 IP 查 LPM 表 */ ret rte_lpm_lookup(lpm, dst_ip, next_hop); if (ret 0) { /* 命中路由表从 next_hop 对应的端口转发 */ out_port next_hop; } else { /* 未命中走默认路由或丢弃 */ out_port default_port; }LPM 适合前缀路由场景比如典型的路由表exact match 适合流表场景比如 OpenFlow 的精确流表项。DPDK 的rte_lpm库在内存使用上做了优化用两级表结构压缩条目占用。实际选型时我一般这样判断规则数少于几万条且要求严格前缀匹配用 LPM规则数大且 key 是五元组用 hash。二者没有谁绝对好关键看规则规模和匹配语义。4.4 无锁环与内存池收发包的骨架DPDK 的收发包流程离不开rte_ring和rte_mempool。rte_ring是一个无锁环形队列支持单生产者单消费者和多生产者多消费者两种模式。在单核收包、另一个核处理、第三个核发包的流水线模型里队列作为核间通信的通道非常合适。/* 创建无锁环容量 1024单生产者单消费者模式 */ struct rte_ring *ring rte_ring_create(worker_ring, 1024, rte_socket_id(), RING_F_SC_DEQ | RING_F_SP_ENQ); /* 收包线程向 ring 中生产报文 */ n rte_ring_enqueue_burst(ring, (void **)bufs, nb_rx); /* 处理线程从 ring 中消费报文 */ n rte_ring_dequeue_burst(ring, (void **)bufs, nb_dequeue);RING_F_SP_ENQ和RING_F_SC_DEQ分别表示单生产者入队、单消费者出队。如果多个核同时入队就必须去掉SP_ENQ标志使用多生产者模式。多生产者模式性能稍差因为它需要处理 CAS 竞争但功能上是安全的。内存池rte_mempool则负责报文缓冲区的分配与回收。每次收包前从池里rte_pktmbuf_alloc处理完后rte_pktmbuf_free归还避免频繁的malloc/free系统调用。5. 避坑与调优常见问题5.1 绑定网卡后设备不见了lspci也看不到现象用dpdk-devbind.py绑定网卡到igb_uio或vfio-pci后ip link下网卡消失lspci里也看不到设备。原因绑定的本质是把网卡从原驱动解绑再挂到 DPDK 支持的驱动上。这一操作会让网卡在内核网络栈中消失属于正常现象。但如果你连lspci都看不到设备本身说明 PCI 枚举层面出了问题大概率是驱动没加载成功或者设备被其他驱动占用。解决先看dmesg里有没有igb_uio或vfio-pci的错误日志。然后确认modprobe vfio-pci是否执行。如果用的是vfio方式还要确认 BIOS 里虚化相关的选项如 VT-d已经开启并且内核启动参数里加了iommupt intel_iommuon。我遇到过一次因为没开 VT-d 导致 vfio 绑定后设备彻底消失的情况改完 BIOS 重进才恢复。5.2 大页配置正确但 DPDK 启动仍报没有大页现象/proc/meminfo里能看到HugePages_Total不为零但 DPDK 初始化时仍然报EAL: No available hugepages reported。原因大页有两种来源——预留的静态大页和动态分配的mmap大页。DPDK 默认从/mnt/huge挂载点分配大页文件。如果没有挂载hugetlbfs或者挂载的大小不对EAL 就发现不了可用的大页。解决执行mount -t hugetlbfs nodev /mnt/huge并在/etc/fstab里加一行持久化配置。另外在系统里同时存在多个 NUMA 节点时需要注意每个节点上的大页分配是否均衡。建议在启动应用前看一下/sys/devices/system/node/node0/hugepages/hugepages-1048576kB/nr_hugepages和node1对应的文件确保目标网卡所在的节点有大页。5.3 多核收包时同一个队列被多个核访问现象程序跑起来后某几个核的 CPU 占用特别高但吞吐没上去收包速率波动很大。原因多个核同时轮询同一个接收队列。DPDK 的轮询模型里队列不是锁保护的多核同时读同一个 rx ring 会导致描述符竞争甚至出现丢包。这和 Cache 一致性的开销叠加在一起性能会断崖式下降。解决每个核只绑定自己的队列通过rte_eth_dev_rx_queue_setup为每个队列分配独立的内存池并确保队列的socket_id与核所在 NUMA 节点一致。同时可以设置rte_eth_dev_set_rx_queue_stats_reset定期重置队列统计观察rx_errors和rx_missed的增长情况来定位问题。5.4 开了超线程但没绑核性能反而下降现象在开启了超线程的机器上跑 DPDK 转发性能比关闭超线程时还差。原因超线程的两个逻辑核共享执行单元和 Cache。DPDK 这种 I/O 密集负载的 IPC 本就不高如果两个逻辑核同时跑收包任务会因为共享资源产生互相干扰。而且如果 DPDK 把两个逻辑核都绑定到了同一个物理核上那相当于自己和自己抢资源。解决绑核时优先选择不同的物理核。可以通过lscpu查看Core和Socket列确认逻辑核的物理映射关系。DPDK 的-l参数指定核列表时可以手动绕开同一个物理核上的另一个逻辑核。还有一种做法是只使用物理核在 EAL 初始化时用--no-hpet和--no-shconf减少干扰但根本还是在核的选择上。5.5 mbuf pool 大小设置不合理导致丢包现象压力测试时吞吐先涨后跌rte_eth_stats里rx_missed持续增长。原因rte_pktmbuf_pool_create的nb_mbufs参数设置得太小收包速率瞬间高峰时缓冲区被耗尽网卡因为没有可用的描述符缓冲区而丢包。反过来设置太大又会浪费内存因为每个 mbuf 都要占用连续内存。解决按期望的收包速率 × 单包占用时间来估算。一个万兆网卡在满速时大约 14.88Mpps每个包 2KB 的话10ms 内需要约 300M 内存。池子大小建议按这个量估算再乘 1.5 的余量。另外MBUF_CACHE_SIZE也要配合核的数量和 lcore 的轮询深度rx_burst的大小来调一般设 256 或 512。/* 一个相对合理的 mbuf pool 配置示例 */ #define NB_MBUF 8192 * 2 /* 根据报文大小和核数调整 */ struct rte_mempool *mbuf_pool rte_pktmbuf_pool_create(mbuf_pool, NB_MBUF, 256, 0, RTE_MBUF_DEFAULT_BUF_SIZE, rte_socket_id());5.6 编译出来的程序在别的机器上跑不了现象在一台服务器上编译好的 DPDK 应用拷到另一台同架构机器上报EAL: unsupported cpu或直接段错误。原因DPDK 的编译选项和 CPU 指令集绑定。默认meson build会启用本机 CPU 支持的指令集比如 SSE4.2、AVX2。如果换到的机器 CPU 型号较老不具备这些指令集程序就跑不起来。另一个常见原因是目标机器的大页配置、IOMMU 设置和源机器不一致。解决交叉编译或打包时用-Dc_args-marchx86-64让二进制兼容性更广牺牲少量指令集优化换取可移植性。用于生产分发时我一般会在 CI 里用基础x86-64微架构级别构建一个通用版本然后在每台机器上用dpdk-testpmd做一次冒烟测试确认能实际收发包后再切换流量。6. 性能验证方法先测基线再谈优化效果判断 DPDK 优化是否有效一定要有对照试验不能只看转发延迟的均值还要看方差和尾延迟。我的习惯是先用testpmd测出硬件基线再对比自己的程序。第一步启动 testpmd 并绑定网卡dpdk-testpmd -l 0-3 -n 4 -- -i --nb-ports2 --nb-cores4进入交互模式后start开始转发show port stats all查看每个端口的收发统计。用流量发生器打满速记录Tx-pps和Rx-pps。如果 testpmd 也达不到线速那么问题的根源在网卡、PCIe 带宽或 BIOS 配置而不是你的业务代码。第二步验证内存分配是否跨 NUMA。执行numactl --hardware查看节点拓扑然后用dpdk-testpmd的--socket-num参数强制指定 socket和默认配置对比吞吐差异。如果差异大于 10%说明存在跨节点访问需要在代码里显式调用rte_zmalloc_socket或rte_pktmbuf_pool_create时传入正确的 socket_id。第三步用perf top观察热点函数的分布。如果rte_eth_rx_burst的占比超过 60%说明瓶颈在收包路径上如果rte_hash_lookup或rte_lpm_lookup占比高说明查表逻辑需要优化。此时可以用--lcore参数做核的负载均衡确保没有某个核因为队列绑定的问题被拖死。# 查看每个核的收包分布 dpdk-testpmd -l 0-3 -n 4 -- --port-topologypaired \ --rxq4 --txq4 --nb-cores4这里--rxq和--txq要和网卡实际的队列数一致。如果队列数配多了会有队列空转浪费 CPU配少了单个队列的吞吐会顶到上限。从那以后我每次接手一个 DPDK 项目第一件事都是先跑一轮 testpmd 基线把网卡能力、NUMA 拓扑和 BIOS 设置记录下来再改自己的代码。这个习惯帮我避开了很多明明代码没问题但性能上不去的坑。希望这份笔记也能帮你少走一些弯路。本文还有配套的精品资源点击获取