首页
关于
Search
1
PacketForge 博客部署文档
16 阅读
2
深入 SONiC 与 SAI:从命令行到芯片的完整数据之旅
14 阅读
3
DPU 与 CPO:AI 时代的网络基础设施革命
11 阅读
4
Broadcom芯片
10 阅读
5
DPDK高性能网络
10 阅读
默认分类
交换芯片
高性能网络
AI算力网络
WebSocket高性能项目
DPDK 成长路线
①性能工程基础
②DPDK 源码深读
③用户态协议栈
④软件交换与 SONiC
⑤PCIe、虚拟化与 DPU
⑥面试与复盘
登录
Search
标签搜索
DPDK→DPU 学习路线
P0
P1
P2
P3
P5
PCIe
P4
P6
libmtcp
NUMA
mTCP
OVS
SONiC
SAI
DPU
perf
gdb
EAL
mempool
PacketForge
累计撰写
53
篇文章
累计收到
3
条评论
首页
栏目
页面
关于
最新文章
热门文章
评论最多
点赞最多
2026-08-18
P5-03 · virtio 与 vhost-user:虚拟化的数据面协议
对应路线图:Phase 5 (Day 141-168) | DPDK 24.11.6 LTS | 内核对照 v6.6代码出处:source/linux-stable/drivers/net/virtio_net.c、source/linux-stable/drivers/vhost/net.c、source/dpdk-stable-24.11.6/lib/vhost/vhost_user.c上一篇:P5-02 VFIO | 下一篇:P5-04 vDPA一句话定位虚拟机没有真网卡,于是 virtio 造了台"纸面网卡":guest 里跑一个标准驱动,host 上跑一个后端,中间用三段共享内存队列(virtqueue)通信。协议只有两个动作——guest 往 avail ring 塞条目并 kick,后端往 used ring 写结果并注入中断。vhost-user 则是把这个后端从内核挪进 DPDK 用户态进程。协议骨架(virtio 规范,公开知识)Split virtqueue 三段(全在 guest 内存里,GPA 对 host 可见):Descriptor Ring(guest 描述每段缓冲的 addr/len/flags)、Avail Ring(guest 公布"哪些描述符可用")、Used Ring(后端公布"哪些已完成")。单方向通信靠下标(idx)推进,双向通知靠 kick(guest→host)和中断注入(host→guest)。packed virtqueue 把三段压成一个环,用环内标志位代替 avail/used 两段,省 cache 访问。真实源码精读① Guest 侧:收包靠"预填空 buffer"(drivers/net/virtio_net.c:2044-2071)static bool try_fill_recv(struct virtnet_info *vi, struct receive_queue *rq, gfp_t gfp) { int err; bool oom; do { if (vi->mergeable_rx_bufs) err = add_recvbuf_mergeable(vi, rq, gfp); else if (vi->big_packets) err = add_recvbuf_big(vi, rq, gfp); else err = add_recvbuf_small(vi, rq, gfp); oom = err == -ENOMEM; if (err) break; } while (rq->vq->num_free); if (virtqueue_kick_prepare(rq->vq) && virtqueue_notify(rq->vq)) { // ...省略统计计数 ... } return !oom; }和 P1-05 的 ixgbe refill 一个哲学:buffer 是提前挂进队列的。收包路径 virtnet_receive(:2140)只做"从 used ring 取条目 → 交给协议栈",然后在空闲过半时补货: // virtio_net.c:2166-2173,节选 if (rq->vq->num_free > min((unsigned int)budget, virtqueue_get_vring_size(rq->vq)) / 2) { if (!try_fill_recv(vi, rq, GFP_ATOMIC)) { spin_lock(&vi->refill_lock); if (vi->refill_enabled) schedule_delayed_work(&vi->refill, 0); spin_unlock(&vi->refill_lock); } }注意这个和 DPDK PMD 的区别:virtio-net 是软中断驱动(virtnet_poll 在 :2218),受 budget 约束;它只是"和真网卡驱动长得一样"的前端驱动,真正的包搬运在后端。② 内核后端 vhost-net:数据面线程 + 双模式(drivers/vhost/net.c:973-997)static void handle_tx(struct vhost_net *net) { struct vhost_net_virtqueue *nvq = &net->vqs[VHOST_NET_VQ_TX]; struct vhost_virtqueue *vq = &nvq->vq; struct socket *sock; mutex_lock_nested(&vq->mutex, VHOST_NET_VQ_TX); sock = vhost_vq_get_backend(vq); if (!sock) goto out; if (!vq_meta_prefetch(vq)) goto out; vhost_disable_notify(&net->dev, vq); vhost_net_disable_vq(net, vq); if (vhost_sock_zcopy(sock)) handle_tx_zerocopy(net, sock); else handle_tx_copy(net, sock); out: mutex_unlock(&vq->mutex); }TX 方向(guest 发包)从 avail ring 取描述符,把数据拷进 host 的内核 socket(handle_tx_copy,:774)或走 zerocopy(:864)。注意"先 disable notify 再干活":批处理期间关掉 guest 的 kick 通知,防止每包一次 VM exit。RX 方向 handle_rx(:1120)先 peek socket 队列长度(:1167),凑齐 vhost_hlen + sock_hlen(:1159-1160)后 get_rx_bufs 拿 guest 的空 buffer 填包。两条路径都是内核线程在忙,每包要过一次内核 socket。③ vhost-user:后端搬到用户态,直接映射 guest 内存(lib/vhost/vhost_user.c:841-895,节选)translate_ring_addresses(struct virtio_net **pdev, struct vhost_virtqueue **pvq) { // ...省略 log_guest_addr 处理 ... if (vq_is_packed(dev)) { len = sizeof(struct vring_packed_desc) * vq->size; vq->desc_packed = (struct vring_packed_desc *)(uintptr_t) ring_addr_to_vva(dev, vq, vq->ring_addrs.desc_user_addr, &len); if (vq->desc_packed == NULL || len != sizeof(struct vring_packed_desc) * vq->size) { // ...省略错误日志 ... return; } // ...省略 numa_realloc ... len = sizeof(struct vring_packed_desc_event); vq->driver_event = (struct vring_packed_desc_event *) (uintptr_t)ring_addr_to_vva(dev, vq, vq->ring_addrs.avail_user_addr, &len); // ...省略校验与 device_event ...QEMU 通过 vhost-user 协议把三段 ring 的 guest 物理地址发给 DPDK 进程,DPDK 侧把 GPA 翻译成自己的虚拟地址(ring_addr_to_vva),之后直接读写——没有 copy,没有内核 socket。协议消息的处理入口是 vhost_user_msg_handler(:3105-3146):read_vhost_message(:3133)读 fd,按 request 编号查 vhost_message_handlers 表(:3139-3143)分发;表里 SET_MEM_TABLE / SET_VRING_NUM / SET_VRING_ADDR 三个消息的注册在 :75-79。🐧 内核对照:vhost-net vs vhost-user维度内核 vhost-netDPDK vhost-user后端位置内核线程(handle_tx/handle_rx,net.c:973/1120)用户态 lcore 直接读写 ring数据路径copy 进内核 socket(net.c:774 起)零拷贝,GPA→VVA 后直接操作kick 处理vhost 线程被唤醒eventfd → 用户态轮询每包成本内核 socket 出入栈与 DPDK 收发包同量级中断注入内核负责DPDK 写 used ring 后 kick eventfd,QEMU 注入同一份 virtio 协议,内核实现守"通用",用户态实现冲"线速"。vhost-user 是 OVS-DPDK 云网络的默认形态,也是理解 DPU 上 virtio 卸载的起点。面试 3 问virtqueue 为什么是"三段式"而不是一个环? → desc 环描述缓冲(多段 scatter-gather),avail 环让 guest 批量公布可用描述符(可以攒批再 kick),used 环让后端批量回写结果(攒批再中断)——三段分离是批量化和双向通知的结构基础。vhost-user 怎么做到零拷贝? → QEMU 只传 GPA(SET_VRING_ADDR),DPDK 用 ring_addr_to_vva 翻译成进程内地址直接读写(vhost_user.c:861-883);对比内核 vhost-net 要走 socket copy。为什么 handle_tx 开头要先 vhost_disable_notify? → 批处理期间避免 guest 每提交一个包就 kick 一次(每次 VM exit 都是成本),处理完再恢复通知(net.c:987-988)。🔧 动手实验# 1. 启动 DPDK vhost-user 后端(testpmd 起一个 vhost 端口) ./testpmd -c 0xf -n 2 --vdev 'net_vhost0,iface=/tmp/vhost-user.sock' -- -i testpmd> start # 2. QEMU 启动 guest,后端走 vhost-user socket qemu-system-x86_64 -netdev vhost-user,id=n1,chardev=c1,queues=1 \ -chardev socket,id=c1,path=/tmp/vhost-user.sock,server=off ... # 3. guest 内 iperf3 打流,对比两个后端: # a) 同规格 QEMU 换成 -netdev tap + vhost=on(内核 vhost-net) # b) 关闭 vhost(QEMU 纯软件模拟) # 记录三种后端的吞吐/CPU # 4. testpmd 里 show port stats 0,看 virtqueue 方向流量数据落点:perf-data/P5/03-vhost-user.md(三种后端 iperf3 吞吐对比表、testpmd 端口统计、CPU 占用)。J3455 上跑 QEMU 会比较吃力,重点看相对差异而不是绝对值。下一篇:P5-04 · vDPA 框架:把 virtqueue 卸载进硬件
2026年08月18日
0 阅读
0 评论
0 点赞
第 1/1 页 · 本页 1 条 · 共 1 条