DPDK vhost-user 的报文转发与缓冲区管理
一、DPDK vhost-user 的网络转发路径
virtio-net 定义客户机与虚拟设备之间的请求格式,报文的处理线程和物理出口则由后端实现。DPDK vhost-user 后端在独立用户态进程中消费客户机 virtqueue,将报文组织为 rte_mbuf,再交给网卡的轮询模式驱动(PMD)。QEMU 保留设备呈现和控制连接,通常不参与这条路径上的逐包处理。
本文跟踪一个 60 B 以太网帧 P1:客户机 TX 队列 → DPDK vhost → rte_mbuf → 物理网卡 TX 队列。分析采用 DPDK 24.11 的同步 split-ring 路径、单个队列对、普通复制模式,不启用分段和校验和卸载。客户机使用 modern virtio-net,12 B 网络头与帧分别放在两个只读描述符中。关键在于两次所有权交接:客户机缓冲区何时可以复用,以及交给 PMD 的 mbuf 何时可以释放。
源码版本:DPDK 24.11、QEMU 10.2.0。
二、整体路径:控制连接、共享队列与 PMD
DPDK 提供的是数据面基础设施,应用负责交换、路由、过滤或转发策略。vhost 库读写客户机共享队列,mempool 管理报文对象,ethdev 与 PMD 把统一的收发接口接到具体网卡。本文假设应用把 P1 转发到一个物理端口;同一后端也可以把报文送到另一台虚拟机,此时没有物理网卡 TX 这一步。
Mermaid 源码
flowchart TB
accTitle: DPDK vhost-user 控制连接与报文路径
accDescr: QEMU 用 vhost-user 控制消息把客户机内存和队列映射到 DPDK 进程;客户机发送队列经 vhost 复制到 mbuf,再由应用提交 PMD 和物理网卡。通知不携带报文。
Q["QEMU<br/>virtio PCI 呈现与控制连接"]
subgraph G[客户机共享 RAM]
V["virtio-net 驱动<br/>TX virtqueue:desc + avail + used"]
B["网络头与报文 P1<br/>描述符 addr 指向的缓冲区"]
end
subgraph D[DPDK 用户态进程]
C["vhost 控制消息处理<br/>共享内存 / 队列地址 / kick 与 call"]
H["数据线程:rte_vhost_dequeue_burst<br/>消费 TX 队列,复制帧到 mbuf"]
M["rte_mbuf M<br/>应用转发策略"]
P["rte_eth_tx_burst → PMD<br/>物理网卡 TX 描述符"]
end
N["物理网卡<br/>DMA 读取 M 的数据 → 网络"]
Q -. Unix socket:消息与文件描述符 .-> C
C -. 建立映射与队列访问条件 .-> H
V -->|链首与描述符| H
V -->|addr| B
B -->|复制本例 60 B 帧| H
H --> M --> P --> N
H -. used 完成与可选 call .-> V
初始化中的内存、队列与通知配置
- DPDK 应用初始化 EAL、mempool、网卡端口及 RX/TX 队列,安排数据线程与队列的对应关系。
- 通过
rte_vhost_driver_register()注册 Unix socket 路径,设置支持的特性和设备生命周期回调,再启动控制连接。客户端或服务端角色只决定连接由谁建立。 - QEMU 传递共享 RAM 的文件描述符及内存区域信息;后端建立自己的虚拟地址映射。随后配置 virtqueue 大小、描述符和环地址、消费位置,以及 kick/call eventfd。
- 设备和队列就绪后,应用才将该队列加入数据线程的轮询集合。停止或断开时先退出这个集合,再处理在途访问与映射释放。
共享的物理页可以映射在 QEMU 和 DPDK 进程的不同虚拟地址。描述符中的设备地址需要经过已协商的内存映射或 IOTLB 转换,不能直接作为后端 C 指针使用。vhost-user 协议和DPDK 控制消息处理分别定义和实现这些关系。
轮询数据线程可以按协议请求客户机抑制 kick,直接检查 avail 索引;后端完成请求后仍须单独判断客户机是否需要中断。需要通知时触发 call eventfd,QEMU 配置的 KVM irqfd 路径可把它转换为客户机中断。eventfd 传递的是事件计数,描述符和帧内容仍保存在各自的内存缓冲区。split 完成通知判断
三、发送实例:从链 0 → 2 到物理出口
使用一个队列对时,virtio 队列 0 为客户机 RX,队列 1 为客户机 TX。应用调用 dequeue 从客户机取包,调用 enqueue 向客户机送包;函数命名以 vhost 接口为视角。以下状态从 TX 队列 1 的一次普通提交开始:
desc[0]: addr=0x1000, len=12, flags=NEXT, next=2 # virtio-net 头
desc[2]: addr=0x2000, len=60, flags=0 # P1,以太网帧
avail.ring[0] = 0; avail.idx = 1
last_avail_idx = 0; used.idx = 0
rte_vhost_dequeue_burst(vid, 1, pool, pkts, 32)
→ 返回 1,pkts[0] = M,M.pkt_len = 60
→ used.ring[0] = {id: 0, len: 0}; used.idx = 1
rte_eth_tx_burst(port, txq, pkts, 1)
→ 返回 1:M 交给 PMD
→ 返回 0:M 仍由应用负责
1. 消费与复制
rte_vhost_dequeue_burst() 检查设备、队列和协商状态,split 分支进入 virtio_dev_tx_split()。后者通过 acquire 读取 avail.idx,以与 last_avail_idx 的差计算新增请求数,再限定本批处理数量并从 mempool 分配 mbuf。
fill_vec_buf_split() 从 available 项取得头索引,验证链和缓冲区地址,整理客户机内存片段。desc_to_mbuf() 处理 virtio-net 头,将帧内容复制到 mbuf,并按协商的卸载语义填入元数据。本例 P1 为 60 B,因此 mbuf 中保存 60 B 帧;12 B virtio-net 头不作为以太网数据发送。描述符到 mbuf
2. 先归还客户机链,再交给物理网卡
vhost 先暂存 shadow used 项,在完成延后执行的复制操作后,批量写入共享 used ring,再以规定的内存顺序发布 used.idx。必要时触发完成通知。本例 used.len=0,表示后端没有向 TX 描述符缓冲区写入数据;完成的含义是客户机原缓冲区已用完。同步 split TX 路径
应用此时持有内容独立的 M,可以执行转发规则,然后调用 rte_eth_tx_burst()。返回 1 表示 PMD 接受 M 并把报文放入发送描述符;随后网卡 DMA 读取数据,PMD 在确认硬件完成后回收相应 mbuf。客户机链在复制完成后归还,M 在物理发送完成后归还,远端接收另由网络协议确认。ethdev TX 接口与缓冲区回收
DPDK 的 vhost 示例在 drain_virtio_tx() 取包,交给 virtio_tx_route(),再由 do_drain_mbuf_table() 批量提交物理端口。真实交换机可以增加查表、隧道、限速等步骤,这些工作与 virtqueue 消费本身应分别计量。物理发送、客户机 TX 消费
四、请求经过的结构与缓冲区归属
Mermaid 源码
flowchart TB
accTitle: 客户机链、DPDK mbuf 与物理发送环
accDescr: 客户机链和 mbuf 之间有复制边界;mbuf 数据被物理网卡 DMA 读取。客户机链在复制结束后归还,mbuf 则保留到硬件发送完成并由 PMD 清理。
A["avail.ring[0] = 0<br/>avail.idx = 1"]
D0["desc[0]<br/>addr 0x1000 · len 12<br/>NEXT → 2"]
D2["desc[2]<br/>addr 0x2000 · len 60<br/>链结束"]
H["virtio-net header<br/>卸载元数据"]
B["P1 客户机缓冲区<br/>60 B 以太网帧"]
M["rte_mbuf M<br/>buf_addr + data_off → 帧<br/>data_len = pkt_len = 60<br/>nb_segs = 1 · next = NULL"]
U["used.ring[0] = id 0, len 0<br/>客户机可回收原链"]
T["物理 TX 描述符<br/>DMA 地址 · 长度 · 控制位"]
R["硬件发送完成<br/>PMD 回收 M → mempool"]
A --> D0 -->|next| D2
D0 -->|addr| H
D2 -->|addr| B
H -. 解析元数据 .-> M
B -->|CPU 复制| M
M -. 复制结束后归还原链 .-> U
M -->|PMD 接受并引用数据| T
T --> R
| 对象 | 关键内容 | 何时可以复用 |
|---|---|---|
| 客户机 split virtqueue | desc.addr / len / flags / next;avail.idx / ring;used.idx / {id, len} | 驱动观察到对应 used 完成后,才能回收整条链及原报文缓冲区 |
vhost_virtqueue | desc / avail / used 指针、size、last_avail_idx、last_used_idx、shadow used 状态 | 队列访问停止后才能销毁映射及本地状态;共享 idx 与本地进度需要保持一致 |
rte_mbuf | buf_addr、data_off、data_len、pkt_len、nb_segs、next、ol_flags、pool | 未提交的由应用处理;PMD 接受后,由 PMD 在硬件完成时释放所持引用 |
| 物理网卡 TX 环 | PMD 按网卡格式填写 DMA 地址、长度和控制位 | 硬件完成条件满足、PMD 清理该发送项后重新使用 |
buf_addr + data_off 定位 mbuf 当前数据起点;data_len 是本段长度,pkt_len 是整个报文长度。一个大报文可以由多个 mbuf 用 next 串起来,此时 nb_segs 大于 1。mbuf 的 next 与 virtio 描述符的 next 分属两个对象层次,不共享编号或所有权。mbuf 定义、vhost 队列定义
五、完成与背压:报文缓冲区的所有权
Mermaid 源码
sequenceDiagram
accTitle: 客户机缓冲区与 DPDK mbuf 的回收时刻
accDescr: DPDK 复制客户机数据到 mbuf 后归还客户机链,物理 PMD 接受 mbuf 后还要等待硬件完成。短发送时剩余 mbuf 仍由应用管理。
participant G as 客户机驱动
participant V as DPDK vhost
participant A as 转发应用
participant P as PMD / 物理网卡
G->>V: 发布 TX 链 0→2,avail.idx=1
A->>V: dequeue_burst,最多 32 个
V->>V: 分配 M,复制 P1,完成待执行复制
V-->>G: used={id:0,len:0},used.idx=1
Note over G,V: 客户机可回收原缓冲区
V-->>A: 返回 1 个 mbuf:M
A->>P: tx_burst(M, 1)
alt 接受 1 个
P-->>A: 返回 1,PMD 持有 M
P->>P: DMA 读取 → 发送 → 完成清理
Note over A,P: PMD 回收 M,不能由应用提前释放
else 接受 0 个
P-->>A: 返回 0
A->>A: 有界重试或丢弃并释放 M
end
1. 发送环满时,used 完成无法撤销
假设本批从客户机取得 32 个 mbuf,而物理 TX 只接受 20 个。前 20 个的引用交给 PMD,后 12 个仍归应用。应用为这 12 个安排有界队列等待重试,或计数后丢弃并释放;已经交给 PMD 的 20 个继续由其完成路径回收。
普通同步 vhost 路径已经归还了这批客户机描述符,因此物理端口拥塞不会自动撤销客户机完成。若要把背压向前传播,需要应用控制下一批 dequeue 的速度或数量,并安排有界队列。增大缓冲只能吸收短期突发,不能修复持续的入口速率高于出口速率。
2. 反向收包有另一组所有权
物理网卡收到报文后,rte_eth_rx_burst() 把 mbuf 交给应用。应用对客户机 RX 队列调用 rte_vhost_enqueue_burst(),后端把数据复制到客户机预先提供的可写缓冲区,发布 used 项。同步调用返回后,应用仍负责释放自己的原 mbuf;enqueue 没有接管这个 mbuf 的生命周期。
客户机 RX 缓冲区不足时,enqueue 返回值会小于请求数,应用决定重试或丢弃。异步 DMA copy 使用独立 API 和完成检查,提交后不能按同步路径立即释放原数据,必须等相应复制完成。接收、重试与释放、异步接口约定
六、性能收益与资源成本
DPDK 数据线程通过持续轮询和 burst 批处理减少逐包唤醒、系统调用及部分共享同步开销。收益依赖流量规模、队列分配、NUMA 和网卡能力;在低负载或 CPU 资源紧张时,持续轮询的成本可能更突出。
| 设计选择 | 解决的问题 | 需要付出的代价或进一步检查 |
|---|---|---|
| 轮询与 burst | 减少频繁唤醒、摊薄逐包固定成本 | 空闲仍消耗 CPU;只取“最多 32 个”不要求凑满,但应用主动等待凑批会增加延迟 |
| 普通 vhost 复制 | 客户机可在物理发送前复用缓冲区,后端独立管理 mbuf | 消耗内存带宽和 CPU;本例不属于端到端零拷贝 |
| 队列与核心亲和性 | 减少线程争用和跨核迁移 | 需同时匹配客户机队列、后端线程、网卡队列及 NUMA 内存;队列越多并不一定越快 |
| 多段 mbuf 与卸载 | 承载大报文,利用网卡分段或校验和能力 | 各层必须一致协商;PMD 的段数、长度和卸载限制可能要求额外整理 |
| 独立进程与共享 RAM | 设备控制与网络转发可分别调度和维护 | 后端拥有客户机内存访问权限;连接恢复、内存变化及迁移时的队列状态需协调 |
可先核对一个不依赖具体网卡的计数关系:本批取包数等于 PMD 接受数与应用保留数之和。上面的 32 个报文应分成 20 个交给 PMD、12 个由应用重试或释放;客户机 used 已完成,无法再用它判断这 12 个报文的去向。检查瓶颈时,继续记录客户机 avail 积压、mbuf 分配失败和软件队列积压,再看 CPU cycles/packet、吞吐、丢包与尾延迟。在相同包长和卸载条件下比较 QEMU TAP、内核 vhost-net 与 DPDK,才能说明某一步处理位置的改变带来了什么收益。
七、从用户态后端到队列与复制优化
把 virtio 后端移到独立用户态进程,使交换机可以统一调度多个虚拟机和物理端口,代价是必须管理共享内存、队列状态与进程生命周期。DPDK 16.07 加入 vhost-user 客户端模式,重连能力进一步减少了固定启动顺序的约束;重连后仍需恢复一致的特性和队列状态,socket 恢复不等于在途报文自动恢复。vhost-user 连接模式
split 到 packed 的队列演进把发布与完成标记集中到一张描述符环,改善元数据访问局部性,同时引入 wrap counter 和不同的所有权判断。双方在初始化时选定格式,后端据此进入各自的数据处理分支;本文使用 split 的 desc、avail、used 三个共享区域。split / packed 数据路径选择
当复制成为瓶颈,可以考虑 DMA copy 卸载。复制仍然存在,只是执行者从 CPU 转为 DMA 设备;提交、完成轮询、缓冲区存活和 DMA 通道资源变得更重要。DPDK 18.11 已提供 vDPA 示例,利用硬件实现兼容 virtio 的数据面,减少专门的 vhost 轮询工作;可迁移状态、特性组合和硬件支持随之成为约束。18.11 的 vDPA 示例
这些演进分别针对进程调度、元数据访问、内存复制和数据面执行位置。先用 P1 的路径确认时间和 CPU 花在哪里,再选择改变哪一个环节。块设备的 SPDK vhost 后端采用不同的请求格式和存储完成语义,可结合SPDK vhost-blk 的请求与完成路径对照。
八、参考资料
- DPDK 24.11 virtio_net.c:split 队列消费、desc_to_mbuf、used 发布及收发 API。
- vhost.h、rte_mbuf_core.h、rte_ethdev.h:队列、报文对象和 PMD 收发接口。
- DPDK vhost 示例:客户机与物理端口之间的批量转发、短发送和回收处理。
- DPDK vhost 库说明、PMD 设计说明、异步复制接口。
- QEMU 10.2.0 vhost-user 协议:内存区域、vring、通知与控制连接。