net_device 和 sk_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 vethbond0br0lo
内核里基本都抽象成一个 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()
也就是说:
- 应用发数据
- 内核构造一个
skb - 路由决定从哪个网卡出去
- 设置:
skb->dev = 某个 net_device
例如:
skb->dev = eth0_dev;
- 然后调用:
dev_queue_xmit(skb);
- 最终进入:
skb->dev->netdev_ops->ndo_start_xmit(skb, dev);
所以发送本质上是:
把一个
skb交给某个net_device去发
接收时的关系
接收路径里:
网卡收到包 -> 驱动分配/填充 skb -> skb->dev = 收包设备 -> netif_receive_skb()
例如物理网卡 eth0 收到包:
- 网卡 DMA 到内存
- 驱动构造
skb - 设置:
skb->dev = eth0_dev;
- 调用:
netif_receive_skb(skb);
这样协议栈就知道:
这个包是从
eth0进来的
如果是 tap0、veth0、geneve0 收到,也一样。
一张图理解
+-------------------+
| 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收到包:生成一个skbbr0转发包:处理这个skbveth0发送包:继续处理这个skbgeneve0封装包:修改这个skbeth0最终发出:发送这个skb
所以在 Linux 网络栈中,你经常看到的是:
一个
skb在不同net_device之间被传递和加工
一个包在不同设备间流转的例子
比如:
VM -> tap0 -> bridge -> veth-pod -> veth-host -> OVS -> geneve -> eth0
在这个过程中:
tap0、veth-pod、veth-host、eth0都是net_device- 这个包在内核里通常一直表现为一个
skb
只是中间会不断修改:
skb->devskb->dataskb的头部指针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_device | sk_buff |
|---|---|---|
| 表示什么 | 网络接口/设备 | 数据包 |
| 生命周期 | 设备存在期间长期存在 | 一个包处理期间短暂存在 |
| 数量 | 很少,按接口个数 | 很多,按包个数 |
| 关注点 | 设备属性与操作 | 报文内容与处理状态 |
| 典型例子 | eth0、tap0、veth0 | 一个 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 只是抽象,很多是纯软件设备:
lovethtapbridgegenevevxlan
误区 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/veth0skb:数据包对象,代表网络栈里流动的一个包- 二者通过
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 结构体内部布局图
也就是详细解释:
headdatatailendmac_headernetwork_headertransport_header
这个对理解封装/解封装特别有帮助。