virt-report虚拟化社区动态

DPDK vhost-user 的报文转发与缓冲区管理

本文由 OpenAI Codex (GPT-6-Astra) 辅助编写。

一、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 这一步。

从客户机 TX virtqueue 到物理网卡
虚线为初始化和通知,实线为报文访问及交接。vhost-user socket 传递控制消息和文件描述符;数据通过共享内存进入 vhost,再复制到本例的 mbuf。
虚线为初始化和通知,实线为报文访问及交接。vhost-user socket 传递控制消息和文件描述符;数据通过共享内存进入 vhost,再复制到本例的 mbuf。 打开原图
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

初始化中的内存、队列与通知配置

  1. DPDK 应用初始化 EAL、mempool、网卡端口及 RX/TX 队列,安排数据线程与队列的对应关系。
  2. 通过 rte_vhost_driver_register() 注册 Unix socket 路径,设置支持的特性和设备生命周期回调,再启动控制连接。客户端或服务端角色只决定连接由谁建立。
  3. QEMU 传递共享 RAM 的文件描述符及内存区域信息;后端建立自己的虚拟地址映射。随后配置 virtqueue 大小、描述符和环地址、消费位置,以及 kick/call eventfd。
  4. 设备和队列就绪后,应用才将该队列加入数据线程的轮询集合。停止或断开时先退出这个集合,再处理在途访问与映射释放。

共享的物理页可以映射在 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 消费

四、请求经过的结构与缓冲区归属

P1 的三个表示与两次缓冲区交接
客户机链 0 → 2、DPDK mbuf M 和物理网卡 TX 描述符属于三个不同对象。本例复制完成后可以归还客户机链;M 必须保留到 PMD 确认硬件用完。
客户机链 0 → 2、DPDK mbuf M 和物理网卡 TX 描述符属于三个不同对象。本例复制完成后可以归还客户机链;M 必须保留到 PMD 确认硬件用完。 打开原图
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 virtqueuedesc.addr / len / flags / next;avail.idx / ring;used.idx / {id, len}驱动观察到对应 used 完成后,才能回收整条链及原报文缓冲区
vhost_virtqueuedesc / avail / used 指针、size、last_avail_idx、last_used_idx、shadow used 状态队列访问停止后才能销毁映射及本地状态;共享 idx 与本地进度需要保持一致
rte_mbufbuf_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 队列定义

五、完成与背压:报文缓冲区的所有权

客户机完成、PMD 接受和硬件完成的先后关系
本例的同步复制使客户机链可以提前归还。PMD 只接受返回数量对应的 mbuf;未接受部分仍由应用重试或释放。
本例的同步复制使客户机链可以提前归还。PMD 只接受返回数量对应的 mbuf;未接受部分仍由应用重试或释放。 打开原图
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 的请求与完成路径对照。

八、参考资料