各种虚拟网络设备的收发包流程

4 阅读9分钟

虚拟网卡的收发包流程

总览:虚拟网卡 vs 物理网卡收发包对比

物理网卡:  应用 ↔ 内核协议栈 ↔ 网卡驱动 ↔ DMA ↔ 网卡硬件 ↔ 物理线缆
虚拟网卡:  应用 ↔ 内核协议栈 ↔ 虚拟驱动 ↔ 内核函数调用/共享内存 ↔ 另一个软件组件

虚拟网卡没有真实硬件,数据的"发送"本质上是把数据交给内核中的另一个软件模块,"接收"本质上是另一个软件模块把数据注入到该接口


一、TAP 设备收发包流程

TAP 是 KubeVirt 中 VM 连接 Host 的核心设备。

架构

┌───────────────────────────────────────────────────────────────┐
│                        Host 内核                              │
│                                                               │
│   ┌──────────────┐         ┌──────────┐                      │
│   │ 内核协议栈    │◄───────▶│  tap0    │◄────────┐            │
│   │ / Bridge     │  sk_buff │ net_device│         │            │
│   │ / OVS        │         └──────────┘         │            │
│   └──────────────┘              ▲                │            │
│                                 │                │            │
│                     ┌───────────┴────────┐       │            │
│                     │  字符设备           │       │            │
│                     │  /dev/net/tun      │       │            │
│                     └───────────┬────────┘       │            │
│                                 │                │            │
└─────────────────────────────────┼────────────────┼────────────┘
                                  │ fd             │
                           ┌──────┴──────┐   ┌────┴─────┐
                           │ 用户空间进程  │   │ vhost-net│
                           │ (QEMU 等)   │   │ (内核态)  │
                           └─────────────┘   └──────────┘

发送流程(用户空间进程 → 内核网络)

QEMU / 用户空间进程
    │
    │ ① write(fd, packet_data, len)
    │   系统调用进入内核
    ▼
/dev/net/tun 字符设备
    │
    │ ② tun_chr_write_iter()
    │   - 从用户空间复制数据到内核  ← [数据复制 #1]
    │   - 分配 sk_buff
    │   - 填充以太网帧头信息
    ▼
tap0 net_device
    │
    │ ③ netif_rx() / netif_receive_skb()
    │   - 将 sk_buff 注入内核网络收包路径
    │   - 触发软中断 (NET_RX_SOFTIRQ)
    │   - 此刻对内核来说,就像 tap0 "收到" 了一个包
    ▼
内核网络栈正常处理
    │
    │ ④ 根据 tap0 的连接决定去向:
    │   - 挂在 Bridge 上 → Bridge 转发
    │   - 挂在 OVS 上   → OVS 流表匹配
    │   - 独立接口       → 内核路由
    ▼
下一跳设备

接收流程(内核网络 → 用户空间进程)

内核网络栈 / Bridge / OVS
    │
    │ ① 将 sk_buff 转发到 tap0 设备
    │   调用 tap0 的 ndo_start_xmit()
    ▼
tap0 net_device
    │
    │ ② tun_net_xmit()
    │   - 将 sk_buff 放入 tap 设备的接收队列 (socket 收包队列)
    │   - 唤醒在 read()/poll() 上等待的用户空间进程
    ▼
/dev/net/tun 字符设备
    │
    │ ③ 用户空间进程被唤醒
    │   tun_chr_read_iter()
    │   - 从内核 sk_buff 复制数据到用户空间缓冲区  ← [数据复制 #1]
    │   - 释放 sk_buff
    ▼
QEMU / 用户空间进程
    │
    │ ④ read(fd, buf, len) 返回
    │   用户空间拿到完整以太网帧
    ▼
进程处理数据

关键代码路径

// 发送 (用户空间 → 内核)
write(fd)
  → tun_chr_write_iter()
    → tun_get_user()
      → alloc_skb()              // 分配 sk_buff
      → copy_from_user()         // 用户空间 → 内核 [复制]
      → netif_rx(skb)            // 注入内核收包路径
        → __netif_receive_skb()  // 触发协议栈处理

// 接收 (内核 → 用户空间)
read(fd)
  → tun_chr_read_iter()
    → tun_do_read()
      → skb_dequeue()            // 从队列取出 sk_buff
      → copy_to_user()           // 内核 → 用户空间 [复制]
      → kfree_skb()              // 释放 sk_buff

二、veth pair 收发包流程

架构

┌──────── Namespace A ──────────┐     ┌──────── Namespace B ──────────┐
│                               │     │                               │
│   ┌────────────┐              │     │              ┌────────────┐   │
│   │ 协议栈 A    │              │     │              │ 协议栈 B    │   │
│   └──────┬─────┘              │     │              └──────┬─────┘   │
│          │                    │     │                     │         │
│   ┌──────┴─────┐              │     │              ┌──────┴─────┐   │
│   │  veth-a    │──────────────┼─────┼─────────────▶│  veth-b    │   │
│   │ net_device │  内核函数调用  │     │              │ net_device │   │
│   └────────────┘              │     │              └────────────┘   │
│                               │     │                               │
└───────────────────────────────┘     └───────────────────────────────┘

发送流程(veth-a → veth-b)

Namespace A 中的应用或协议栈
    │
    │ ① 数据包到达 veth-a 的发送函数
    │   调用 veth-a 的 ndo_start_xmit()
    ▼
veth_xmit() 
    │
    │ ② 找到 peer 设备
    │   veth-a 和 veth-b 互相持有对方的指针
    │   struct net_device *peer = rcu_dereference(priv->peer)
    │
    │ ③ 处理 sk_buff
    │   - skb_clone() 或直接传递 sk_buff     ← [可能的数据复制]
    │   - 修改 skb->dev = peer (指向 veth-b)
    │   - 重置 skb 的收包相关字段
    ▼
veth-b 的接收路径
    │
    │ ④ netif_rx(skb)
    │   - 将包注入 veth-b 所在 Namespace B 的收包路径
    │   - 触发 Namespace B 的软中断 (NET_RX_SOFTIRQ)
    │   - 对 Namespace B 来说,就像 veth-b "收到" 了一个包
    ▼
Namespace B 协议栈正常处理

关键代码路径

// veth 发送核心函数
static netdev_tx_t veth_xmit(struct sk_buff *skb, struct net_device *dev)
{
    struct veth_priv *priv = netdev_priv(dev);
    struct net_device *peer;

    rcu_read_lock();
    peer = rcu_dereference(priv->peer);     // 获取对端设备

    if (!peer || !pskb_may_pull(skb, ETH_HLEN)) {
        kfree_skb(skb);
        goto out;
    }

    skb->dev = peer;                         // 切换到对端设备
    skb->pkt_type = PACKET_HOST;             // 标记为本机包

    if (likely(dev_forward_skb(peer, skb) == NET_RX_SUCCESS)) {
        // dev_forward_skb 内部调用:
        // ① skb_scrub_packet()   — 清理 skb 元数据
        // ② skb->protocol 重新设置
        // ③ netif_rx(skb)        — 注入对端收包路径
    }

out:
    rcu_read_unlock();
    return NETDEV_TX_OK;
}

veth 的特殊性

                    veth-a 发送 = veth-b 接收
                    ──────────────────────────

  普通物理网卡:   发送 → DMA → 线缆 → 对端网卡 DMA → 接收
  veth pair:     发送 → 内核函数调用 → 对端 netif_rx() → 接收

  ┌──────────────────────────────────────────────┐
  │  veth-a 的 ndo_start_xmit()                  │
  │       ↓                                      │
  │  直接调用 dev_forward_skb(veth-b, skb)        │
  │       ↓                                      │
  │  触发 veth-b 的 netif_rx()                    │
  │                                              │
  │  没有 DMA,没有中断,只有函数调用 + 软中断       │
  └──────────────────────────────────────────────┘

三、Linux Bridge 收发包流程

架构

┌──────────────────────────────────────────────────────────────┐
│                    Linux Bridge (br0)                        │
│                                                              │
│    FDB 转发表 (MAC → Port 映射)                               │
│    ┌───────────────────────────────────────────┐             │
│    │  MAC Address        │  Port    │  Aging   │             │
│    │  aa:bb:cc:dd:ee:01  │  tap0    │  300s    │             │
│    │  aa:bb:cc:dd:ee:02  │  veth0   │  120s    │             │
│    └───────────────────────────────────────────┘             │
│                                                              │
│    ┌────────┐     ┌────────┐     ┌────────┐                 │
│    │ port0  │     │ port1  │     │ port2  │                 │
│    │ (tap0) │     │ (tap1) │     │ (veth) │                 │
│    └───┬────┘     └───┬────┘     └───┬────┘                 │
└────────┼──────────────┼──────────────┼───────────────────────┘
         │              │              │
         ▼              ▼              ▼
      VM-A 的         VM-B 的       容器/veth
      虚拟网卡        虚拟网卡

收包流程(某端口收到包 → Bridge 处理 → 转发)

某个 bridge port (如 tap0) 收到数据包
    │
    │ ① netif_receive_skb()
    │   内核收包路径识别到 tap0 是 bridge port
    │   调用 br_handle_frame()
    ▼
br_handle_frame()
    │
    │ ② 入口检查
    │   - 检查源 MAC 是否合法
    │   - ebtables BROUTING 链
    │   - 如果是 STP BPDU → 交给 STP 处理
    ▼
br_handle_frame_finish()
    │
    │ ③ MAC 地址学习 (源 MAC)
    │   br_fdb_update()
    │   - 将 {源MAC, 入端口, 时间戳} 写入 FDB 表
    │   - 如果 MAC 已存在但端口变了 → 更新(MAC 漂移)
    │
    │ ④ ebtables / nftables 过滤
    │   - NF_BR_PRE_ROUTING 钩子
    │   - 可以在这里做 MAC 防欺骗等
    ▼
br_forward_finish() / br_flood()
    │
    │ ⑤ 查找 FDB (目的 MAC)
    │   br_fdb_find_rcu(目的MAC)
    │
    │   ┌─────────────────────────┬──────────────────────┐
    │   │ FDB 命中 (单播已知)      │  FDB 未命中 (未知单播) │
    │   │                         │  或广播/组播           │
    │   ▼                         ▼                      │
    │   br_forward()              br_flood()             │
    │   转发到特定端口              泛洪到所有端口          │
    │   (除了入端口)               (除了入端口)            │
    └─────────────────────────────┴──────────────────────┘
    │
    │ ⑥ 出端口发送
    │   br_forward_finish()
    │   → NF_BR_POST_ROUTING 钩子
    │   → dev_queue_xmit(skb)    // 调用出端口的发送函数
    ▼
目的端口的 ndo_start_xmit()
    │
    │ 如果出端口是 tap  → tun_net_xmit()  → 用户空间进程
    │ 如果出端口是 veth → veth_xmit()     → 对端 namespace
    │ 如果出端口是 eth0 → 物理网卡驱动     → DMA → 线缆
    ▼
数据到达目的设备

关键代码路径

// Bridge 收包入口
br_handle_frame(skb)
  → br_handle_frame_finish(skb)
    → br_fdb_update(源MAC, 入端口)        // MAC 学习
    → NF_HOOK(NF_BR_PRE_ROUTING)          // ebtables
    → br_fdb_find_rcu(目的MAC)            // FDB 查找
      → 命中:  br_forward(目的端口, skb)   // 单播转发
      → 未命中: br_flood(br, skb)          // 泛洪

// Bridge 端口发送
br_forward(to_port, skb)
  → br_forward_finish(skb)
    → NF_HOOK(NF_BR_POST_ROUTING)         // ebtables
    → br_dev_queue_push_xmit(skb)
      → dev_queue_xmit(skb)               // 调用出端口发送

四、virtio-net / vhost-net 收发包流程

这是 KubeVirt 中 VM 网络的核心路径。

架构

┌──────────── Guest VM ─────────────┐
│                                   │
│  ┌─────────────┐                  │
│  │ Guest App   │                  │
│  └──────┬──────┘                  │
│         ▼                         │
│  ┌─────────────┐                  │
│  │ Guest Kernel │                  │
│  │ TCP/IP Stack │                  │
│  └──────┬──────┘                  │
│         ▼                         │
│  ┌─────────────────────────┐      │
│  │ virtio-net 驱动 (前端)   │      │
│  │                         │      │
│  │  ┌──────────────────┐   │      │
│  │  │  TX vring (发送)  │   │      │
│  │  │  ┌──┬──┬──┬──┐   │   │      │
│  │  │  │d0│d1│d2│..│   │   │  d = descriptor   
│  │  │  └──┴──┴──┴──┘   │   │      │
│  │  │  avail ring       │   │      │
│  │  │  used ring        │   │      │
│  │  └──────────────────┘   │      │
│  │                         │      │
│  │  ┌──────────────────┐   │      │
│  │  │  RX vring (接收)  │   │      │
│  │  │  (同样的结构)      │   │      │
│  │  └──────────────────┘   │      │
│  │                         │      │
│  └───────────┬─────────────┘      │
│              │ 共享内存             │
└──────────────┼────────────────────┘
               │
    ┌──────────┴──────────┐
    │   vring 共享内存区域  │  ← Guest 和 Host 都可以直接访问
    └──────────┬──────────┘
               │
┌──────────────┼────────────────────────────────┐
│  Host 内核   │                                │
│              ▼                                │
│  ┌──────────────────────────┐                 │
│  │  vhost-net 内核模块       │                 │
│  │  (virtio 后端)           │                 │
│  │                          │                 │
│  │  vhost worker 内核线程    │                 │
│  │  持续处理 vring           │                 │
│  └────────────┬─────────────┘                 │
│               ▼                               │
│  ┌──────────────────────────┐                 │
│  │  tap0 (net_device)       │                 │
│  └──────────────────────────┘                 │
│                                               │
└───────────────────────────────────────────────┘

发送流程(Guest App → Host 网络)

Guest 应用程序
    │
    │ ① send() 系统调用
    │   [复制 #1] 用户空间 → Guest 内核 sk_buff[Guest 上下文切换] 用户态 → 内核态
    ▼
Guest 内核协议栈
    │
    │ ② TCP/IP 封装
    │   添加 TCP头 → IP头 → 以太网帧头
    ▼
virtio-net 驱动 ndo_start_xmit()
    │
    │ ③ 填充 TX vring[复制 #2] sk_buff 数据 → vring descriptor 指向的共享内存
    │
    │   具体步骤:
    │   a. 从 TX vring 获取空闲 descriptorb. 将 sk_buff 的数据地址填入 descriptor
    │      (或复制数据到 descriptor 指向的 buffer)
    │   c. 更新 avail ring (告知 Host 有新数据)
    │   d. 写 virtio 通知寄存器
    │
    │   ┌─────────── TX vring 结构 ───────────────┐
    │   │                                         │
    │   │  Descriptor Table:                      │
    │   │  ┌─────┬────────┬─────┬───────┐        │
    │   │  │ idxaddrlenflags │        │
    │   │  ├─────┼────────┼─────┼───────┤        │
    │   │  │  00xA000128 │  ...  │        │
    │   │  │  10xB000256 │  ...  │        │
    │   │  └─────┴────────┴─────┴───────┘        │
    │   │                                         │
    │   │  Avail Ring: [0, 1, ...]Guest 写    │
    │   │  Used Ring:  [...]Host 写     │
    │   │                                         │
    │   └─────────────────────────────────────────┘
    │
    │ ④ 通知 Host(触发 VM Exit ⚡)
    │   写入 MMIO 通知寄存器
    │   → 触发 VM Exit
    │   → KVM 捕获退出原因
    │   → 通知 vhost-net 内核线程
    ▼

═══════════ VM Exit 边界 ════════════

vhost-net 内核线程被唤醒
    │
    │ ⑤ 处理 TX vringhandle_tx()
    │   a. 读取 avail ring,获取 Guest 新提交的 descriptorb. 从共享内存读取数据包
    │   c. 分配 Hostsk_buffd. 复制数据: 共享内存 → sk_buff[复制 #3]e. 设置 skb->dev = tap0f. 更新 used ring (告知 Guest 该 buffer 已处理)
    ▼
tap0 net_device
    │
    │ ⑥ netif_receive_skb(skb)
    │   注入 Host 内核收包路径
    │   触发软中断
    ▼
Host 内核网络栈 / Bridge / OVS
    (后续正常处理)

接收流程(Host 网络 → Guest App)

Host 内核网络栈 / Bridge / OVS
    │
    │ ① 数据包到达 tap0
    │   调用 tap0 的 ndo_start_xmit()
    │   → tun_net_xmit()
    │   → 将 sk_buff 放入 tap 的 socket 接收队列
    │   → 唤醒 vhost-net 内核线程
    ▼
vhost-net 内核线程
    │
    │ ② 处理 RX vring
    │   handle_rx()
    │   a. 从 tap 的 socket 队列取出 sk_buff
    │   b. 读取 RX vring 的 avail ring
    │      (Guest 预先提交的空 buffer)
    │   c. 复制数据: sk_buff → 共享内存    ← [复制]
    │   d. 更新 used ring
    │   e. 注入虚拟中断 (通过 irqfd / KVM)
    ▼

═══════════ VM Enter 边界 ════════════

Guest 收到虚拟中断
    │
    │ ③ virtio-net 驱动中断处理
    │   virtnet_poll() — NAPI 轮询
    │   a. 读取 used ring,获取 Host 填充好的 descriptor
    │   b. 从共享内存取出数据
    │   c. 构建 Guest 侧 sk_buff
    │   d. 提交新的空 buffer 到 avail ring
    ▼
Guest 内核协议栈
    │
    │ ④ TCP/IP 解封装
    │   解析以太网帧 → IP → TCP
    │   放入 socket 接收缓冲区
    ▼
Guest 应用程序
    │
    │ ⑤ recv() 系统调用
    │   [复制] 内核 socket 缓冲区 → 用户空间
    ▼
应用获得数据

五、KubeVirt 完整发送路径的函数调用链

Guest 用户空间: send(sockfd, buf, len)
    │
    ▼
Guest 内核:
    sys_sendto()
      → sock_sendmsg()
        → tcp_sendmsg() / udp_sendmsg()
          → ip_queue_xmit()
            → ip_output()
              → dev_queue_xmit()
                → virtio-net: start_xmit()
                  → virtqueue_add_outbuf()     // 填充 TX vring
                  → virtqueue_kick()           // 通知 Host
                    → VM Exit ⚡
    │
    ▼
Host 内核:
    KVM: vmx_handle_exit()
      → vhost_net: handle_tx_kick()
        → handle_tx()
          → skb = alloc_skb()
          → memcpy_from_vring()                // 从共享内存复制
          → tun_sendmsg()
            → tun_get_user()
              → netif_rx(skb)                  // 注入 tap0 收包路径
    │
    ▼
Host 内核 (Bridge):
    __netif_receive_skb()
      → br_handle_frame()                     // tap0 是 bridge port
        → br_fdb_update()                     // MAC 学习
        → br_forward()                        // FDB 查表转发
          → dev_queue_xmit()                  // 转发到 veth 出端口
    │
    ▼
Host 内核 (veth):
    veth_xmit()
      → dev_forward_skb(peer, skb)            // 跨 namespace
        → netif_rx(skb)                       // 在 Host netns 触发收包
    │
    ▼
Host 内核 (OVS):
    __netif_receive_skb()
      → ovs_vport_receive()
        → ovs_flow_tbl_lookup()               // 流表匹配
          → 命中: ovs_execute_actions()
            → Geneve 封装
            → ip_local_out()                   // 进入 Host 路由
              → dev_queue_xmit(物理网卡)
                → ixgbe_xmit_frame()           // 网卡驱动
                  → DMA 传输到网卡

六、各虚拟设备开销对比

设备数据复制中断/通知上下文切换单包延迟
tap (QEMU read/write)1次内核↔用户空间系统调用2次~5-10 μs
tap (vhost-net)1次 vring↔sk_buffeventfd0次用户态切换~2-5 μs
veth pairskb_clone(轻量)软中断0次~2-5 μs
Linux Bridge0次(指针传递)无额外0次~1-3 μs
virtio-net + VM Exit1次VM Exit ⚡Guest→Host 模式切换~3-8 μs

一句话总结

虚拟网卡的收发包本质是用内核函数调用和共享内存替代了物理网卡的 DMA 和硬件中断。TAP 通过字符设备连接用户空间进程,veth 通过直接函数调用连接对端,Bridge 通过 FDB 表在端口间转发 sk_buff 指针,而 virtio/vhost-net 通过 vring 共享内存 + VM Exit 通知实现 Guest 与 Host 之间的高效数据传递。