简介:《深入浅出DPDK》全书读书笔记以PDF整理了DPDK高性能网络I/O框架的知识脉络,面向需要理解用户态驱动、多队列流分类、内存管理的开发者与网络工程师。资源包仅1个PDF文档,约6.57MB,便于系统阅读。已有3849人学习下载。笔记从传统内核中断切入,逐步讲解NAPI轮询、Netmap共享数据包池、用户态驱动规避内存拷贝与系统调用的原理;随后覆盖核心库(大页内存、缓存池、定时器、无锁环)、PMD驱动、精确/最长/通配符匹配查表、rte_eal_init初始化及三层转发流程。还结合虚拟化、NFV/SDN讨论了PCIe带宽与降低访存开销的优化思路。这份笔记既可当作《深入浅出DPDK》的浓缩导读,也能作为日常开发排错时快速定位知识点的参考。
1. 一份《深入浅出DPDK》的读书笔记,为什么值得花一个晚上精读
软中断打满 100%,带宽卡在 3 Gbps 上不去,翻出那份攒了很久的《深入浅出DPDK》读书笔记,顺着大页配置和收包测试一步步重跑,才意识到瓶颈根本不在 CPU 主频,而在内核收包路径上。这份读书笔记解决的就是这件事:DPDK 是什么、数据面为什么要绕开内核、怎么在一台物理机上把第一包数据收进来。它不像 API 手册那样按函数罗列,而是把大页内存、UIO/VFIO、轮询模型这些零散概念串成一条可落地的路径。网上 dpdk 中文资料不算少,但大多是单点讲解;这份笔记的价值在于按读书顺序把原理和安装串起来,读完能直接动手。适合正在做网关、接入层、NFV,或者被高并发收包卡住的开发者;也适合想系统学 DPDK、但不想一上来就啃源码的人——先把这份笔记读透,再决定要不要深挖。
2. 先立骨架:数据面绕开内核只是表象,读书笔记里的三条主线才是重点
读这份笔记,第一件要做的事不是从头抄到尾。大多数人翻几页就卡在“轮询与中断”的对比上,以为 DPDK 的核心就是“不用中断”。实际上这只是表象。笔记真正反复出现的是三条主线:收包路径、内存模型、设备隔离。先抓住这三条线,后面每一章都能挂上去,读起来会顺很多。
2.1 轮询代替中断:性能来源和它换来的 CPU 代价
为什么中断在高速小包下撑不住?万兆口满速率每秒约 1488 万个小包,每个包一次中断,就意味着每秒钟上千万次上下文切换和 cache 污染。CPU 大量时间在“跑去处理中断再跑回来”,真正用在收包上的时间很少。中断合并(NAPI)能缓解一部分,但收包延迟会抖动。DPDK 选择轮询:一个核固定转圈检查收包队列,省掉切换,包处理时间更稳定。这是性能的第一来源,但不是免费的。
轮询的代价是“没有包时也在消费 CPU”,所以笔记里把 CPU 隔离放在很靠前的位置。实际操作上,要在内核启动参数里加isolcpus=2,3,让这两个核不被调度器随手分配任务,还要关掉irqbalance,否则中断随时会把线程赶来赶去。这一条经常被当成“性能玄学”跳过,实际影响比 DPDK 的任何编译选项都大。验证方法也简单:cat /proc/interrupts看网卡中断是否落在隔离核以外,再用top观察 DPDK 进程是不是稳定占满指定核。如果发现进程在一个核上,但中断和软中断还在别的核上跳动,说明前面几步没做。
2.2 收包路径与内存模型:读笔记时要抓的两根主线
收包路径这条主线从头到尾不经过内核协议栈,也不经过 socket。链路是:网卡收到包,DMA 把数据写到内存,驱动程序把这段内存包装成mbuf,放入无锁 ring,应用在另一个核上从 ring 里取走。笔记里每讲一个组件,都是在为这条路径上的某个节点服务。读的时候先在纸上画出这条线,再往上面挂细节,不然很容易迷失在函数名里。
内存模型是第二条主线。DMA 要求物理连续,所以大页内存是起点;包要被反复分配和释放,所以有mempool;每个包的数据区要预留头部空间并做对齐,所以有mbuf结构。两条主线的合流点就是rte_eth_rx_burst返回一个mbuf数组。这份笔记之所以总在讲驱动之前先讲内存,就是因为收包的本质是“把数据放到一段预先规划好的内存里”,而不是“收到数据再临时找地方”。
读的时候有个技巧:把每个章节标题挂到线上。比如大页是“让 DMA 有地方落脚”,UIO 是“让用户态能访问设备”,ring 是“在核与核之间传递包”。挂完钩再决定要不要深挖源码,效率高很多。不建议一开始就从rte_ring的源码读起,那会陷入实现细节,忘了它到底解决什么问题。
2.3 UIO 还是 VFIO:先做完三个检查再选型
两个驱动框架的作用都是把设备直接交给用户态。UIO 通过 sysfs 把设备内存映射给用户态,模块少、实现简单,但能力有限,没有设备隔离。VFIO 依赖 IOMMU 做 DMA 重映射和隔离,安全性好,支持中断和直通,但要求 CPU 和 BIOS 都开了相关虚拟化特性。怎么选?学习阶段用 UIO 更省事;生产环境、要做多租户隔离、要配合虚拟机直通网卡时,考虑 VFIO。不少人在笔记本上直接绑 VFIO 翻车,原因就是没开 VT-d,这时退回 UIO 就是后悔药。
# 选驱动前先做三个检查 lscpu | grep -E 'vmx|svm' # CPU是否支持虚拟化扩展 cat /proc/cmdline | tr ' ' '\n' | grep -E 'intel_iommu=on|amd_iommu=on' # IOMMU是否已在内核启动参数里开启 lspci -nn | grep -i ethernet # 确认网卡的PCI地址和设备型号第一条没有vmx或svm,VFIO 基本不用考虑;第二条没有intel_iommu=on,BIOS 开了也白搭;第三条用来确认你要绑的是哪块网卡,避免把管理口绑走。把这三条跑完再选驱动,能省掉后面一整个晚上的排查时间。
| 对比项 | UIO (igb_uio) | VFIO (vfio-pci) |
|---|---|---|
| 内核模块 | igb_uio,需单独加载 | vfio-pci,内核自带 |
| 中断支持 | 轮询为主,中断支持弱 | 支持中断,配合更好的轮询模型 |
| 设备隔离 | 无 | IOMMU 隔离,DMA 重映射 |
| IOMMU 依赖 | 不依赖 | 强依赖 |
| 学习成本 | 低 | 中 |
| 典型场景 | 学习、单机收包 | 生产、虚拟化直通、多租户 |
提示:绑定网卡前,先在系统里记录这块网卡的 IP 和管理方式。这条后面讨论绑定网卡时还会再踩一次。
3. dpdk安装后的最小验证环境:从大页配置到 testpmd 收包
骨架立住之后,下一步是“真的收到一包”。dpdk 安装完成后,第一件事不是写业务代码,而是先搭一个最小验证环境。这里假设已经用常见方式编译好了 DPDK(源码 meson 构建或者发行版打包都可),并且有 root 权限。下面三步做完,就能看到第一包数据从网卡流进 DPDK。
3.1 大页内存配置:让 DMA 有连续物理内存可写
网卡 DMA 要把数据写进内存,需要物理上连续的地址;另外 4KB 普通页在大量收包时会产生大量 TLB miss。大页内存同时解决这两个问题,默认用 2MB 大页就够了。配置方式不是临时echo一下,而是写进 sysctl 配置,让系统在开机时预留。
# 预留 1024 个 2MB 大页,共 2GB,写入配置并立即生效 echo 'vm.nr_hugepages=1024' | sudo tee /etc/sysctl.d/99-hugepages.conf sudo sysctl -p /etc/sysctl.d/99-hugepages.conf # 确认是否真的分配成功 grep -E 'HugePages_Total|HugePages_Free' /proc/meminfo1024是个适合学习阶段的值,2GB 预留给 DPDK,剩余内存还能跑系统。如果机器内存小,改成 512 也够跑 testpmd;如果后面要上 1GB 大页,需要在内核启动参数里加hugepagesz=1G,然后 sysctl 里写vm.nr_hugepages=2。注意大页的内存是直接预留的,配置后不重启,系统可能因为内存碎片凑不出连续空间,所以最稳的做法是:写入/etc/sysctl.d/后重启一次,让内核在开机阶段预留干净内存。
接着要挂载 hugetlbfs,DPDK 启动时会从这个文件系统里映射大页:
# 挂载大页文件系统,DPDK 从这里取内存 sudo mkdir -p /mnt/huge sudo mount -t hugetlbfs -o pagesize=2M nodev /mnt/huge还有一件事容易被忽略:透明大页(THP)。它是给通用场景用的后台合并机制,和 DPDK 自己管理大页的方式冲突,运行时会带来不确定的缺页行为。学习阶段建议关掉:
# 关闭透明大页,避免和 DPDK 内存管理抢地盘 echo never | sudo tee /sys/kernel/mm/transparent_hugepage/enabled这一步不做,之后的报错会非常诡异:/proc/meminfo里明明有大页,DPDK 却报无法分配内存。先把大页和 THP 搞定,后面很多问题都不会出现。
3.2 绑定网卡到用户态驱动:一条命令和一个回退方案
大页就绪后,要把网卡从内核驱动切到用户态驱动。使用 UIO 方式,先加载igb_uio模块,再用 DPDK 自带的dpdk-devbind.py脚本绑定。这里的顺序很重要:先确认网卡没在跑业务,再绑定。
# 加载 UIO 内核模块 sudo modprobe uio sudo insmod /path/to/dpdk/build/kernel/linux/igb_uio/igb_uio.ko # 查看当前网卡状态,确认 PCI 地址 sudo dpdk-devbind.py --status # 绑定:把 PCI 地址上的内核驱动换成 igb_uio sudo dpdk-devbind.py --bind=igb_uio 0000:02:00.0 # 再查一次状态,确认驱动栏变成 igb_uio sudo dpdk-devbind.py --status0000:02:00.0是 PCI 地址,从lspci或--status的输出里抄。绑定前务必用ip addr show看下这块网卡是不是管理口,--status也会显示当前内核驱动,如果是ixgbe、e1000这类驱动,说明这块卡还在被内核使用。绑卡后原来的 IP 会消失,SSH 会话如果走了这张网卡,会当场断连。这是血泪经验。
回退方案同样是脚本一行搞定:
# 回退到内核驱动,后续可重新配 IP sudo dpdk-devbind.py --bind=ixgbe 0000:02:00.0 sudo ip link set up dev eth0ixgbe要换成你这块网卡原本的内核驱动名,回退后原 IP 一般需要重新配置。绑定这种事,别在远程会话里对着管理网卡做,真要做就用本地控制台或者先准备好另外一条管理通路。
3.3 testpmd 跑通收包:验证环境可用的最小命令
绑定成功后,用testpmd做冒烟测试。它是 DPDK 自带的收发包测试程序,不用写一行 C 代码就能验证大页、UIO、网卡整条链路是否通畅。
# 启动 testpmd:使用 0、1 两个逻辑核,2 个内存通道 sudo ./build/app/dpdk-testpmd -l 0-1 -n 2 -- -i --total-num-mbufs=65535-l 0-1指定使用的核心列表,-n 2是内存通道数,必须和主板实际配置一致(一般 2 或 4 是安全的),这个值填错会导致启动失败。--total-num-mbufs=65535是预分配的 mbuf 数,学习阶段用默认或这个值都行。进入交互模式后,执行:
testpmd> start testpmd> show port stats allstart让网卡开始收包并按默认 io 模式转发,show port stats all会打印每个端口统计。期望看到RX-packets持续增长,如果对端没发包,可以再开一个终端用dpdk-testpmd从同一个网卡的另一端口发流,或者用简单工具打流。如果统计里RX-packets一直为 0,先看testpmd> show port info all确认端口状态是 UP,再回去查绑定和大页。退出时执行quit即可。
| testpmd 参数 | 作用 | 常见误设 |
|---|---|---|
-l 0-1 | 指定逻辑核 | 核号和 NUMA 不对齐,影响吞吐 |
-n 2 | 内存通道数 | 填 1 或 8 启动直接失败 |
-i | 进入交互模式 | 不加则启动后直接转发,不好观察 |
--total-num-mbufs | 预分配 mbuf 数量 | 太小导致高吞吐下丢包 |
4. 读书笔记里的三个机制:mempool、ring、mbuf 怎么在运行中验证
骨架和环境都有了,接下来要解决的是“为什么这么设计”。笔记后半部分的高频主角是 mempool、ring、mbuf。这三个机制最难的地方不在概念,而在“跑起来怎么验证”。只看不跑,过了两周就忘。
4.1 mempool:为什么“分配一次、反复复用”才是性能关键
如果每个包都走malloc/free,锁竞争和内存碎片会直接杀死收包性能。mempool 的解法是预先分配一大块内存,取包和还包都走池子,配合每核本地缓存,绝大多数场景连锁都不用碰。这也是为什么rte_pktmbuf_pool_create的cache_size参数值得仔细调:它决定了每个核本地缓存多少个 mbuf,太小会有频繁回池请求,太大浪费内存。
testpmd 里可以直接观察池子消耗:
testpmd> show mempool mem0这行命令在 testpmd 交互界面里查看名为mem0的 mbuf 池状态,输出里有used_count和free_count。开始收包后,used_count会上升并稳定在一个水位,说明 mbuf 在收发路径上被反复复用。如果free_count降到 0,说明池子不够用,位置高一点的--total-num-mbufs就是给你的后悔药。这块池子分配在 HugePages 上,这也是为什么前面说大页是整个内存模型的起点。
4.2 ring:SPSC/MPSC 的边界和笔记里常被忽略的细节
rte_ring 是无锁环形队列,但“无锁”是有条件的。单生产者单消费者模式下,无锁且几乎零开销;多生产者参与时,入队要处理竞争;多消费者时,出队也要处理竞争。笔记里最常见的误区是把“无锁”理解成“随便并发”。真实情况是:能设计成单生产者单消费者,就别轻易用多生产者模式,成本不在功能上,而在性能上。
还有一块容易被略过:ABA 问题。在 32 位环境或旧版本 DPDK 里,无锁队列的头部操作会涉及到指针的重复比较,稍有不慎就会取出已释放的节点。新版 DPDK 通过扩展头部字段规避了这个问题,笔记未必跟到那么细。真要验证 SPSC 和 MPMC 的性能差异,最直接的办法是用rte_ring_create配合rte_ring_enqueue/dequeue写个一百行内的基准测试,分别跑单生产和双生产场景,对比吞吐和时延。这个实验做完,对“无锁”三个字的理解会比读十篇文章都深。
4.3 mbuf 对齐与 cacheline:false sharing 是最隐蔽的掉速点
mbuf 不只描述一个包,它还承担了对齐和头部预留的职责。结构上要求对齐到 cacheline(通常 64 字节),头部预留 headroom,这样协议栈每加一层头,不需要搬移整个数据。笔记里如果只强调“headroom 预留”,很容易漏掉另一个更隐蔽的问题:false sharing。
多核场景下,两个核如果各自写同一个结构体的不同字段,而这些字段恰好落在同一条 cacheline 上,一次写入就会导致整条 cacheline 在核间反复失效。表现很魔幻:单核跑性能正常,加到多核反而掉速。RTE 提供的__rte_cache_aligned就是干这个用的:
/* 每个核一个统计变量,按 cacheline 对齐,避免 false sharing */ struct lcore_stats { uint64_t rx_pkts; uint64_t tx_pkts; } __rte_cache_aligned; /* 在收发路径里:stats[lcore].rx_pkts++; */不加对齐时,两个核的rx_pkts可能落在同一条 cacheline 上,互相拖累。实际排查时可以先用perf stat看 cache-misses,如果多核吞吐明显低于单核累加,优先怀疑 false sharing,而不是去调队列长度。这一条在笔记里往往只有一句话,但它在真实业务里能让人排查一整天。
5. 上手踩坑记录:配置看着没问题却收不到包的 5 个排查点
环境搭完,真正的考验才来。下面这些坑,大半不是 DPDK 本身的问题,而是环境准备和认知偏差。每一条都按“现象 → 原因 → 解决”写,照着排查能省下大量时间。
5.1 HugePages 配置了,testpmd 却报无法分配内存
现象:testpmd 一启动就报EAL: Cannot get hugepage,或者启动后收包异常卡顿。原因:/proc/meminfo里HugePages_Total是 0,预留没成功。常见情况是系统运行一段时间后内存碎片化,echo大页数量时内核凑不出连续内存;或者配置写到了临时参数里,重启后失效。解决:把vm.nr_hugepages写进/etc/sysctl.d/后重启,开机阶段预留最干净。还有另一种情况:多 NUMA 节点机器默认从 node 0 分配内存,node 0 剩余不足也会报错,此时用--socket-mem给每个节点显式指定大页数量。另外,容器和 cgroup 的内存限制会挡住大页申请,学 DPDK 尽量用物理机或特权容器。
5.2 绑定网卡后 SSH 断连
现象:执行dpdk-devbind.py --bind=igb_uio后,终端卡住,连接断开。原因:把正在跑管理业务的网卡绑给了用户态驱动,原来的 IP 随内核驱动一起卸载了。解决:绑卡前先ip addr show,确认这块网卡不是你的 SSH 管理口;如果只有一块可用网卡,先给另一块网卡配好管理地址再动手。绑卡操作最好在本地控制台或 IPMI 里做。回退命令要记熟:dpdk-devbind.py --bind=原驱动名加上ip link set up。这条是我踩得最实在的一次,当时在机房折腾到半夜,只能跑过去插显示器。
5.3 VFIO 绑定报权限或设备找不到
现象:执行--bind=vfio-pci时提示Operation not permitted或者No such device。原因:IOMMU 没开。要么 BIOS 里 VT-d 是 disabled,要么内核启动参数里少了intel_iommu=on,要么两者都没配。还有一类是权限问题:普通用户不在 vfio 允许的用户组里。解决:按 2.3 节的三条命令逐项检查;BIOS 打开 VT-d,内核参数加上intel_iommu=on或amd_iommu=on;权限不够就把用户加进 vfio 组,或者临时用 root 验证。对于没有 IOMMU 的机器,直接退回igb_uio,学习阶段性能差距可以忽略。
5.4 单核吞吐上不去,CPU 看起来忙但包没少
现象:top看 DPDK 进程占满一个核,但网卡吞吐只有两三百万 pps,和网卡标称相差很远。原因:CPU 隔离没做。irqbalance还在跑,内核调度器把其他任务塞到 DPDK 用的核上;或者收包核和网卡不在同一个 NUMA 节点,访问内存走远端路径,延迟翻倍。解决:内核启动参数加isolcpus=2,3并关闭irqbalance;然后把 DPDK 进程绑定到隔离核,用--socket-mem指定与网卡同节点的内存。网卡挂在哪个 NUMA 节点,看/sys/bus/pci/devices/0000:02:00.0/numa_node就能确认。这个坑排查顺序错了会很痛苦,先查隔离,再查 NUMA,不要一上来就调 DPDK 编译选项。
5.5 按笔记抄的代码段编译不过
现象:照着读书笔记粘贴rte_eth_dev_start、rte_eth_rx_queue_setup等代码,编译时大量报错,不是未声明就是函数签名不对。原因:DPDK 版本差异太大。笔记可能基于 20.11 或更早,而当前环境装的是 23.x;很多 API 的返回值、参数顺序、字段名在不同版本里都有调整。解决:以当前安装版本的头文件为准,用grep在/usr/include或 DPDK 源码里搜函数原型,直接看定义再改代码。不要盲目加-m编译选项,那不是根本问题。读书笔记的价值在于给思路和框架,不是给你一份能直接交差的代码。这个观念摆正了,后面学习会顺利很多。
6. 把读书笔记变成排查手册:一张验证清单和两个习惯
读完 PDF 只是开始,真正留下的是这套验证动作。我现在每次调 DPDK 环境,都走同一张清单,十分钟内能定位是环境问题还是代码问题。整份笔记读完后,建议把它压缩成下面这张表,贴在工位旁边:
| 验证项 | 命令 | 期望结果 |
|---|---|---|
| 大页已预留 | grep HugePages_Total /proc/meminfo | 数值等于配置数,如 1024 |
| IOMMU 状态(选 VFIO 时) | cat /proc/cmdline | 含intel_iommu=on |
| 网卡绑定状态 | dpdk-devbind.py --status | 驱动为igb_uio或vfio-pci |
| 最小收包链路可用 | testpmd 中show port stats all | RX-packets持续增长 |
| NUMA 一致 | cat /sys/bus/pci/devices/.../numa_node | 与lscpu中 lcore 所在节点一致 |
两个习惯对你后续看任何底层技术书都有用。第一个:改内核参数前先记录 baseline。把/proc/meminfo、--status、testpmd 的 stats 存一行日志,避免调完参数后不知道是变好了还是变差了。第二个:每读完一节写一个三行验证动作。读完大页章节就写个检查大页的脚本,读完 ring 章节就写条 testpmd 收包统计命令。“读过”和“跑通”之间,差的往往只是一条命令的执行。
前两年我读这类笔记总是“眼睛会了,手不会”,后来改成“每个机制必须有一个对应的验证动作”,效率高了很多。最贵的是时间,《深入浅出DPDK》这份笔记能帮你把坑提前标出来,剩下的就是自己动手,把清单里的每一项真实跑一遍。希望帮到你。
本文还有配套的精品资源,点击获取