kubevirt vm的流量包skb在不同net_device中流转全流程图

2 阅读11分钟

net_devicesk_buff(skb)

by GPT 5.4

如果你想快速抓住 Linux 网络栈,这两个结构一定要先分清

  • net_device:表示网络设备
  • sk_buff(简称 skb):表示网络数据包

一句话理解

net_device 是“网卡/接口”,skb 是“在网络栈里流动的数据包对象”。

可以把它们类比成:

  • net_device = 公路收费站 / 站点
  • skb = 在路上跑的车

或者:

  • net_device = 快递站点
  • skb = 快递包裹

它们分别是什么

1. net_device:设备抽象

Linux 里不管是:

  • 物理网卡 eth0
  • 虚拟网卡 tap0
  • veth
  • bond0
  • br0
  • lo

内核里基本都抽象成一个 struct net_device

它描述的是:

  • 设备名字:eth0
  • MAC 地址
  • MTU
  • 状态(UP/DOWN)
  • 发送函数 ndo_start_xmit
  • 所属 namespace
  • 统计信息

所以你可以把它理解成:

“这个包要从哪个接口进来/出去”


2. sk_buff:数据包抽象

skb 是 Linux 网络栈里表示一个包的核心对象。

它里面包含:

  • 包的数据内容
  • 各层头部的位置指针
  • 包长度
  • 来自哪个设备 skb->dev
  • 协议类型
  • 校验和状态
  • 路由信息
  • conntrack / tc / ovs 等附加元数据

所以它可以理解成:

“这个包本身,以及内核为了处理它附带的上下文信息”


最核心关系

skb->dev 把两者连起来

最重要的一点:

skb 里面有一个字段 dev,指向一个 net_device

也就是:

struct sk_buff {
    ...
    struct net_device *dev;
    ...
};

这表示:

  • 这个包是从哪个设备收到的
  • 或者这个包准备从哪个设备发出去

发送时的关系

发送路径里:

应用 -> TCP/IP -> IP路由 -> 选出出接口 -> skb->dev = eth0 -> dev_queue_xmit()

也就是说:

  1. 应用发数据
  2. 内核构造一个 skb
  3. 路由决定从哪个网卡出去
  4. 设置:
skb->dev = 某个 net_device

例如:

skb->dev = eth0_dev;
  1. 然后调用:
dev_queue_xmit(skb);
  1. 最终进入:
skb->dev->netdev_ops->ndo_start_xmit(skb, dev);

所以发送本质上是:

把一个 skb 交给某个 net_device 去发


接收时的关系

接收路径里:

网卡收到包 -> 驱动分配/填充 skb -> skb->dev = 收包设备 -> netif_receive_skb()

例如物理网卡 eth0 收到包:

  1. 网卡 DMA 到内存
  2. 驱动构造 skb
  3. 设置:
skb->dev = eth0_dev;
  1. 调用:
netif_receive_skb(skb);

这样协议栈就知道:

这个包是从 eth0 进来的

如果是 tap0veth0geneve0 收到,也一样。


一张图理解

             +-------------------+
             |   net_device      |
             |-------------------|
             | name = "eth0"     |
             | mtu = 1500        |
             | mac = xx:xx       |
             | ndo_start_xmit()  |
             +---------+---------+
                       ^
                       |
                  skb->dev
                       |
             +---------+---------+
             |    sk_buff        |
             |-------------------|
             | data = 报文内容     |
             | len = 128         |
             | protocol = IP     |
             | dev = eth0        |
             +-------------------+

你可以怎么记

net_device 关注“接口”

它关心:

  • 我是谁
  • 我能不能发
  • 我怎么发
  • 我的 MTU 是多少
  • 我的 MAC 是什么

skb 关注“包”

它关心:

  • 我这个包的数据是什么
  • 我从哪来
  • 我要去哪
  • 我现在解析到哪一层了
  • 我有没有被 NAT/conntrack/OVS 处理过

在代码里的典型配合

1. 发送

int dev_queue_xmit(struct sk_buff *skb);

意思就是:

把这个 skb 发出去

发到哪里去?

看:

skb->dev

2. 驱动发送函数

netdev_tx_t ndo_start_xmit(struct sk_buff *skb, struct net_device *dev);

这里两个参数同时出现,意思很明确:

  • skb:要发的包
  • dev:通过哪个设备发

通常满足:

dev == skb->dev

3. 接收

netif_receive_skb(skb);

协议栈收到 skb 后,会看:

skb->dev

来决定:

  • 这是从哪个口来的
  • 要不要走 bridge
  • 要不要走 OVS
  • 要不要做路由
  • 要不要进 netfilter

一个非常实用的理解方式

net_device 是“处理者”

skb 是“被处理对象”

例如:

  • eth0 收到包:生成一个 skb
  • br0 转发包:处理这个 skb
  • veth0 发送包:继续处理这个 skb
  • geneve0 封装包:修改这个 skb
  • eth0 最终发出:发送这个 skb

所以在 Linux 网络栈中,你经常看到的是:

一个 skb 在不同 net_device 之间被传递和加工


一个包在不同设备间流转的例子

比如:

VM -> tap0 -> bridge -> veth-pod -> veth-host -> OVS -> geneve -> eth0

在这个过程中:

  • tap0veth-podveth-hosteth0 都是 net_device
  • 这个包在内核里通常一直表现为一个 skb

只是中间会不断修改:

  • skb->dev
  • skb->data
  • skb 的头部指针
  • skb 的元数据

例如:

在 tap0 收到时

skb->dev = tap0;

bridge 转发到 veth-pod 时

skb->dev = veth-pod;

veth 到 host 后

skb->dev = veth-host;

OVS 决定走 Geneve 后

skb->dev = geneve_sys;

最后外层路由决定从 eth0 出去

skb->dev = eth0;

所以你可以把 skb->dev 理解成:

这个包当前“挂靠”在哪个设备上


二者最本质的区别

维度net_devicesk_buff
表示什么网络接口/设备数据包
生命周期设备存在期间长期存在一个包处理期间短暂存在
数量很少,按接口个数很多,按包个数
关注点设备属性与操作报文内容与处理状态
典型例子eth0tap0veth0一个 TCP 包、ARP 包、ICMP 包
核心作用提供收发能力承载待处理的数据

生命周期对比

net_device

创建接口 -> 注册到内核 -> UP/DOWN -> 持续存在 -> 删除接口

例如:

ip link add veth0 type veth
ip link set veth0 up
ip link del veth0

skb

收到/构造一个包 -> 在网络栈中流转 -> 发出/上送应用 -> 释放

通常:

  • 收一个包,分配一个 skb
  • 发一个包,构造一个 skb
  • 处理完就 kfree_skb()

常见误区

误区 1:把 skb 当成“报文数据本身”

不完全对。

skb 不只是报文数据,还包括很多控制信息:

  • 哪个设备来的
  • 路由缓存
  • netfilter 状态
  • GSO/GRO 信息
  • VLAN 标记
  • 隧道信息

所以更准确地说:

skb = 报文数据 + 内核处理元数据


误区 2:以为 net_device 一定对应真实网卡

不对。

net_device 只是抽象,很多是纯软件设备:

  • lo
  • veth
  • tap
  • bridge
  • geneve
  • vxlan

误区 3:一个设备只处理一种 skb

不对。

同一个 net_device 可以处理很多不同类型的包:

  • ARP
  • IPv4
  • IPv6
  • TCP
  • UDP
  • ICMP
  • VLAN
  • Geneve/VXLAN

你只要记住这条主线

Linux 网络栈里最核心的一句话就是:

包用 skb 表示,口用 net_device 表示,包通过 skb->dev 与口关联。

也可以进一步压缩成:

net_device 是“口”,skb 是“包”。


超短总结

  • net_device:网络设备对象,代表 eth0/tap0/veth0
  • skb:数据包对象,代表网络栈里流动的一个包
  • 二者通过 skb->dev 关联
  • 发送:把 skb 交给 net_device
  • 接收:net_device 产生/提交 skb

如果你愿意,我下一条可以继续帮你画一张:

skb 在 net_device / bridge / veth / ovs / tap 之间流转的全流程图

这样会更直观。

skb 流转全流程图

我给你画一张从 VM 发包到物理网卡发出的完整 skb 流转图,尽量把:

  • skb 在哪里生成
  • skb->dev 怎么变化
  • 哪些地方是复制
  • 哪些地方只是转发指针

都串起来。


总览图

VM用户态
  │
  │ send()
  ▼
VM内核协议栈
  │
  │ [Guest skb #G1]
  │ skb->dev = virtio-net
  ▼
virtio-net
  │
  │ 拷贝到 TX vring
  │ [复制 #1: Guest skb -> vring]
  ▼
══════════════ Guest / Host 边界 ══════════════
  ▼
vhost-net
  │
  │ 从 vring 取数据,构造 Host skb
  │ [Host skb #H1]
  │ skb->dev = tap0
  │ [复制 #2: vring -> Host skb]
  ▼
tap0
  │
  │ netif_receive_skb(H1)
  ▼
Linux Bridge (Pod netns)
  │
  │ 查 FDB,决定从 veth-pod 发出
  │ H1 不一定复制,通常继续复用
  │ skb->dev = veth-pod
  ▼
veth-pod
  │
  │ veth_xmit()
  │ 跨 namespace 交给 peer
  ▼
veth-host
  │
  │ dev_forward_skb()
  │ skb->dev = veth-host
  ▼
OVS br-int (Host netns)
  │
  │ 流表匹配 / ACL / LR / NAT
  │ 若跨节点:Geneve 封装
  ▼
geneve port
  │
  │ 在原 skb 前面 push 外层头
  │ Outer IP/UDP/Geneve
  │ skb->dev = eth0
  ▼
内核路由 / 邻居子系统
  │
  │ 查路由、ARP/邻居
  ▼
eth0
  │
  │ ndo_start_xmit()
  │ DMA 发包
  ▼
物理网络

更细的分层图

1. VM 内部

┌──────────────────────── Guest VM ────────────────────────┐
│                                                          │
│  应用程序                                                │
│    │                                                     │
│    │ send()                                              │
│    ▼                                                     │
│  TCP/UDP/IP                                              │
│    │                                                     │
│    │ 构造 Guest skb                                      │
│    │ [Guest skb #G1]                                     │
│    │ skb->data = Inner Eth + IP + TCP + Payload          │
│    │ skb->dev  = virtio-net                              │
│    ▼                                                     │
│  virtio-net 驱动                                         │
│    │                                                     │
│    │ 把包描述到 TX vring                                 │
│    │ [复制 #1: Guest skb -> vring共享内存]               │
│    ▼                                                     │
└──────────────────────────────────────────────────────────┘

这里的关键点:

  • Guest 里有它自己的 skb
  • Host 并不会直接使用 Guest 的 skb
  • Guest/Host 之间共享的是 vring buffer,不是 skb 对象本身

2. Host 上 tap / vhost-net

┌──────────────────────── Host 内核 ────────────────────────┐
│                                                           │
│  vhost-net                                                │
│    │                                                      │
│    │ 从 vring 取出数据                                    │
│    │ 分配新的 Host skb                                    │
│    │ [Host skb #H1]                                       │
│    │ skb->data = Inner Eth + IP + TCP + Payload           │
│    │ skb->dev  = tap0                                     │
│    │ [复制 #2: vring -> Host skb]                         │
│    ▼                                                      │
│  tap0                                                     │
│    │                                                      │
│    │ netif_receive_skb(H1)                                │
│    ▼                                                      │
└───────────────────────────────────────────────────────────┘

这里要特别注意:

到了 Host 侧,已经是一个新的 skb 了。


3. Pod netns 中的 Linux Bridge

                    Pod Network Namespace
┌──────────────────────────────────────────────────────────────┐
│                                                              │
│   tap0  ─────►  Linux Bridge(k6t-net0)  ─────►  veth-pod    │
│                                                              │
│   skb #H1 进入 bridge                                        │
│   skb->dev = tap0                                            │
│                                                              │
│   bridge 做的事:                                            │
│   - 学习源 MAC                                               │
│   - 查目的 MAC                                               │
│   - 决定出端口 = veth-pod                                     │
│                                                              │
│   然后:                                                     │
│   skb #H1 继续往前走                                          │
│   skb->dev = veth-pod                                        │
│                                                              │
└──────────────────────────────────────────────────────────────┘

这一步通常没有“重新造一个包”,主要是:

  • 查表
  • skb->dev
  • 调用下一跳设备发送函数

所以 bridge 你可以理解为:

skb 的软件交换机中转站


4. veth 跨 namespace

┌──────────── Pod netns ────────────┐    ┌────────── Host netns ──────────┐
│                                    │    │                                │
│   veth-pod                         │    │                        veth-host│
│      │                             │    │                           ▲    │
│      │ ndo_start_xmit(H1)          │    │                           │    │
│      ▼                             │    │                           │    │
│   veth_xmit()                      │────┼──── dev_forward_skb() ────┘    │
│                                    │    │                                │
│   skb #H1 或其克隆继续流转          │    │   skb->dev = veth-host         │
│                                    │    │                                │
└────────────────────────────────────┘    └────────────────────────────────┘

你可以把 veth 理解成:

一根虚拟网线,两端各是一个 net_device

特点:

  • 一端发,就是另一端收
  • 没有物理 DMA
  • 本质是内核函数调用转交 skb

5. OVS 里 skb 的处理

┌──────────────────────── OVS br-int ────────────────────────┐
│                                                            │
│   入端口: veth-host                                         │
│   skb #H1                                                  │
│   skb->dev = veth-host                                      │
│                                                            │
│   OVS 处理:                                                │
│   - 提取五元组 / MAC / tunnel metadata                      │
│   - 查 datapath flow                                        │
│   - 命中后执行 actions                                      │
│                                                            │
│   可能的动作:                                              │
│   - output: 本地另一个 port                                 │
│   - push/pop vlan                                           │
│   - ct / nat                                                │
│   - output: geneve                                          │
│                                                            │
└────────────────────────────────────────────────────────────┘

如果目标 VM 在本机:

  • OVS 可能直接 output 到另一个 veth-host
  • 然后再走 bridge/tap 到目标 VM

如果目标 VM 在远端节点:

  • OVS 会让这个 skb 走 Geneve 封装

6. Geneve 封装时 skb 的变化

这是最值得看的一段。

封装前

skb #H1
skb->dev = veth-host

+---------------------------------------------------+
| Inner Eth | Inner IP | TCP/UDP | Payload          |
+---------------------------------------------------+

OVS 决定走隧道

action:
  set tunnel dst = remote_node_ip
  set tun_id     = VNI
  output: geneve

封装后

skb #H1 仍然是这个 skb(通常不是新建)
只是前面被 push 了外层头

+-----------------------------------------------------------------------+
| Outer Eth | Outer IP | UDP | Geneve | Inner Eth | Inner IP | Payload  |
+-----------------------------------------------------------------------+

也就是说:

Geneve 封装很多时候不是“新造一个 skb”,而是在原 skb 头部前面继续塞数据

如果 headroom 不够,才可能触发重新分配。


skb 在封装过程中的 dev 变化

tap0 收到时        skb->dev = tap0
bridge 转发时      skb->dev = veth-pod
跨 veth 后         skb->dev = veth-host
OVS 处理时         skb->dev = veth-host / geneve port
路由决定出 eth0    skb->dev = eth0
最终网卡发送       eth0->ndo_start_xmit(skb)

全流程详细图

┌─────────────────────────────────────────────────────────────────────┐
│ 1. Guest VM                                                        │
└─────────────────────────────────────────────────────────────────────┘

[应用 send()]
      │
      ▼
[Guest TCP/IP]
      │
      │ 构造 Guest skb #G1
      │ skb->dev = virtio-net
      ▼
[virtio-net]
      │
      │ copy to TX vring
      │ [复制 #1]
      ▼

══════════════════════ Guest / Host 边界 ══════════════════════

┌─────────────────────────────────────────────────────────────────────┐
│ 2. Host: vhost-net / tap                                           │
└─────────────────────────────────────────────────────────────────────┘

[vhost-net]
      │
      │ 从 vring 读数据
      │ 分配 Host skb #H1
      │ skb->dev = tap0
      │ [复制 #2]
      ▼
[tap0]
      │
      │ netif_receive_skb(H1)
      ▼

┌─────────────────────────────────────────────────────────────────────┐
│ 3. Pod netns: Linux Bridge                                         │
└─────────────────────────────────────────────────────────────────────┘

[bridge k6t-net0]
      │
      │ 入端口 tap0
      │ 学习 src MAC
      │ 查 dst MAC
      │ 决定出端口 veth-pod
      ▼
[veth-pod]
      │
      │ skb->dev = veth-pod
      ▼

┌─────────────────────────────────────────────────────────────────────┐
│ 4. veth pair                                                       │
└─────────────────────────────────────────────────────────────────────┘

[veth_xmit()]
      │
      │ peer = veth-host
      │ dev_forward_skb()
      ▼
[veth-host]
      │
      │ skb->dev = veth-host
      ▼

┌─────────────────────────────────────────────────────────────────────┐
│ 5. OVS br-int                                                      │
└─────────────────────────────────────────────────────────────────────┘

[OVS ingress: veth-host]
      │
      │ flow lookup
      │ ACL / LR / NAT / output
      │
      ├──────── 若目标在本机 ───────► output: 另一个本地端口
      │
      └──────── 若目标在远端 ───────► output: geneve

┌─────────────────────────────────────────────────────────────────────┐
│ 6. Geneve encapsulation                                            │
└─────────────────────────────────────────────────────────────────────┘

[geneve_xmit()]
      │
      │ push Geneve hdr
      │ push UDP hdr
      │ push Outer IP hdr
      │ 路由查找
      │ 邻居解析
      │ push Outer Eth hdr
      │ skb->dev = eth0
      ▼

┌─────────────────────────────────────────────────────────────────────┐
│ 7. Physical NIC                                                    │
└─────────────────────────────────────────────────────────────────────┘

[eth0]
      │
      │ ndo_start_xmit()
      │ DMA
      ▼
[物理网络]

你最该关注的 4 个点

1. skb 是“包对象”,不是设备

设备是 net_device 包是 skb

所以流转时本质是:

同一个 skb 在不同 net_device 之间被转交


2. skb->dev 会一路变化

这是理解全流程最关键的字段。

例如一个包可能经历:

tap0 -> veth-pod -> veth-host -> eth0

对应:

skb->dev = tap0;
skb->dev = veth-pod;
skb->dev = veth-host;
skb->dev = eth0;

3. 不是每一跳都会复制数据

很多地方只是:

  • skb->dev
  • 查表
  • 调函数转交

真正容易发生复制的地方是:

  • Guest skb -> vring
  • vring -> Host skb
  • skb headroom 不够导致 expand
  • 某些 clone/copy 场景

4. OVS / bridge / veth 本质上都在“处理 skb”

  • bridge:根据 MAC 转发 skb
  • veth:把 skb 交给对端设备
  • OVS:根据流表处理 skb
  • eth0:把 skb 交给真实网卡 DMA 发出去

一个超短记忆版

你可以把整条链路记成一句:

Guest 里先造一个 skb,经过 virtio/vhost 变成 Host skb,然后这个 Host skb 在 tap、bridge、veth、OVS、Geneve、eth0 之间不断切换 skb->dev,最终被真实网卡发出去。


如果你愿意,我下一条可以继续给你画这两张中的任意一张

选项 1:skb 接收路径全流程图

也就是:

eth0 收包 → Geneve 解封装 → OVS → veth → bridge → tap → VM

选项 2:skb 结构体内部布局图

也就是详细解释:

  • head
  • data
  • tail
  • end
  • mac_header
  • network_header
  • transport_header

这个对理解封装/解封装特别有帮助。