By Opus 4.6
主要包括两块分析:
1.arp请求和回复的过程
2.后续二层包单播发送的过程
同子网跨节点 ARP 泛洪过程
场景回顾
text
brq-net1 是一个linux网桥,
tap-vm1 是挂在linux网桥上的一个tap口
eth0.201 是挂在linux网桥上的一个vlan子接口
Compute-1 Compute-2
┌──────────────────────┐ ┌──────────────────────┐
│ VM-1 (10.0.1.10) │ │ VM-2 (10.0.1.12) │
│ MAC: aa:bb:cc:11 │ │ MAC: aa:bb:cc:22 │
│ │ │ │ ▲ │
│ tap-vm1 │ │ tap-vm2 │
│ │ │ │ │ │
│ brq-net1 │ │ brq-net1 │
│ │ │ │ │ │ │ │
│ tap-vm1 eth0.201 │ │ tap-vm2 eth0.201 │
│ │ │ │ │ │
│ eth0 │ │ eth0 │
└──────────────┼───────┘ └──────────────┼───────┘
│ │
└──────────── 物理交换机 ────────────────────┘
(trunk, 允许 VLAN 201)
VM-1 想 ping VM-2,但不知道 VM-2 的 MAC 地址,所以第一步是发 ARP 请求。
第一阶段:VM-1 发出 ARP 请求
text
VM-1 内核:
目的 IP 10.0.1.12 和自己同子网 (10.0.1.0/24)
ARP 缓存里没有 10.0.1.12 的 MAC
→ 构造 ARP Request 广播帧
┌──────────────────────────────────────────────────────────┐
│ Ethernet Header │
│ Dst MAC: ff:ff:ff:ff:ff:ff (广播) │
│ Src MAC: aa:bb:cc:11 (VM-1 MAC) │
│ Type: 0x0806 (ARP) │
│ │
│ ARP Payload │
│ Operation: Request (1) │
│ Sender MAC: aa:bb:cc:11 │
│ Sender IP: 10.0.1.10 │
│ Target MAC: 00:00:00:00:00:00 (未知,要问的) │
│ Target IP: 10.0.1.12 │
└──────────────────────────────────────────────────────────┘
这是一个二层广播帧,dst MAC = ff:ff:ff:ff:ff:ff
第二阶段:ARP 帧在 Compute-1 内部的流转
text
VM-1
│
│ 发出 ARP 广播帧
▼
tap-vm1
│
│ tap 只是一个管道,帧直接进入 bridge
▼
brq-net1 (Linux Bridge)
│
│ 💡 Bridge 收到帧,做两件事:
│
│ ① 学习源 MAC
│ 记录: aa:bb:cc:11 → 来自端口 tap-vm1
│
│ ② 查目的 MAC: ff:ff:ff:ff:ff:ff
│ → 广播地址!
│ → Bridge 的行为: 向所有其他端口泛洪
│
│ brq-net1 的端口列表:
│ - tap-vm1 (入端口,不回发)
│ - tap-vm5 (如果还有同网络的 VM)
│ - eth0.201 (上行口) ← 💡 也会收到泛洪
│
▼
向 eth0.201 泛洪发出
这里是关键:
Linux Bridge 对广播帧做泛洪,会发往所有端口,包括上行口
eth0.201
第三阶段:从 eth0.201 到 eth0(核心问题)
这是你问的重点。
eth0.201 不是一个独立的物理设备
text
eth0.201 是 eth0 的 VLAN 子接口
它们的关系:
┌──────────────────────────────────────────────────────┐
│ │
│ eth0.201 不是一块独立的网卡 │
│ 它是内核在 eth0 之上创建的一个"逻辑视图" │
│ │
│ eth0.201 和 eth0 共享同一块物理网卡 │
│ eth0.201 只是告诉内核: │
│ "凡是从我这里发出的帧,请加上 VLAN 201 tag" │
│ "凡是 eth0 收到的带 VLAN 201 tag 的帧,请交给我" │
│ │
└──────────────────────────────────────────────────────┘
发送方向:eth0.201 → eth0
text
Bridge 泛洪到 eth0.201 的帧:
┌──────────────────────────────────────────────────────┐
│ Dst: ff:ff:ff:ff:ff:ff │ Src: aa:bb:cc:11 │ ARP ... │
└──────────────────────────────────────────────────────┘
(普通以太网帧,没有 VLAN tag)
│
│ Bridge 调用 eth0.201 的发送函数
│ dev_queue_xmit(skb), skb->dev = eth0.201
▼
eth0.201 的发送函数 vlan_dev_hard_start_xmit():
│
│ 💡 核心操作: 插入 VLAN tag
│
│ 在以太网帧里插入 4 字节的 802.1Q 头:
│
│ 原始帧:
│ ┌────────┬────────┬──────┬─────────┐
│ │ Dst MAC│ Src MAC│ Type │ Payload │
│ │ 6B │ 6B │ 2B │ │
│ └────────┴────────┴──────┴─────────┘
│
│ 插入后:
│ ┌────────┬────────┬──────────────┬──────┬─────────┐
│ │ Dst MAC│ Src MAC│ 0x8100│VID201│ Type │ Payload │
│ │ 6B │ 6B │ 2B │ 2B │ 2B │ │
│ └────────┴────────┴──────────────┴──────┴─────────┘
│ ▲ ▲
│ └── 802.1Q ────┘
│ 4 bytes
│
│ 然后把 skb 交给真正的物理设备 eth0 发送:
│ skb->dev = eth0 ← 💡 dev 从 eth0.201 变成 eth0
│ dev_queue_xmit(skb)
▼
eth0 (物理网卡)
│
│ ndo_start_xmit()
│ DMA 发送
▼
物理线缆上的帧:
┌──────────────────┬──────────────────┬──────┬─────┬──────┬─────────┐
│ ff:ff:ff:ff:ff:ff│ aa:bb:cc:11 │0x8100│ 201 │0x0806│ ARP ... │
│ Dst MAC │ Src MAC │ TPID │VLAN │ Type │ │
└──────────────────┴──────────────────┴──────┴─────┴──────┴─────────┘
用代码的视角理解
c
// eth0.201 的发送函数 (简化)
static netdev_tx_t vlan_dev_hard_start_xmit(struct sk_buff *skb,
struct net_device *dev)
{
// dev = eth0.201
// ① 获取 VLAN 信息
struct vlan_dev_priv *vlan = vlan_dev_priv(dev);
// vlan->vlan_id = 201
// vlan->real_dev = eth0 (底层物理设备)
// ② 在 skb 中插入 VLAN tag
// 实际上可能是设置 skb->vlan_tci 让网卡硬件插入
// 或者软件在帧头里插入 4 字节
__vlan_hwaccel_put_tag(skb, htons(ETH_P_8021Q), vlan->vlan_id);
// 或者: vlan_insert_tag(skb, ETH_P_8021Q, vlan->vlan_id);
// ③ 把 skb 的发送设备改为底层物理设备
skb->dev = vlan->real_dev; // skb->dev = eth0
// ④ 调用底层设备发送
dev_queue_xmit(skb);
return NETDEV_TX_OK;
}
本质就三步
text
┌────────────────────────────────────────────────────────┐
│ │
│ eth0.201 发送帧时做了什么: │
│ │
│ ① 在帧里插入 VLAN 201 tag(或标记让网卡硬件插入) │
│ ② 把 skb->dev 从 eth0.201 改成 eth0 │
│ ③ 调用 dev_queue_xmit(skb) 让 eth0 发出去 │
│ │
│ 就这么简单。 │
│ eth0.201 自己没有发送能力。 │
│ 它只是一个"加 tag 的中间层"。 │
│ 真正发包的永远是 eth0。 │
│ │
└────────────────────────────────────────────────────────┘
第四阶段:物理交换机转发
text
物理交换机收到帧:
┌──────────────────┬──────────────────┬──────┬─────┬──────┬─────────┐
│ ff:ff:ff:ff:ff:ff│ aa:bb:cc:11 │0x8100│ 201 │0x0806│ ARP ... │
└──────────────────┴──────────────────┴──────┴─────┴──────┴─────────┘
交换机处理:
① 目的 MAC = ff:ff:ff:ff:ff:ff → 广播
② VLAN = 201
③ 在 VLAN 201 的所有端口上泛洪
- 其他 trunk 口 (连其他计算节点): 保留 tag 转发
- access 口 (如果有): 剥除 tag 转发
- 入端口: 不回发
→ 帧被转发到 Compute-2 的 eth0 (trunk 口)
第五阶段:Compute-2 收包
eth0 收到带 VLAN tag 的帧
text
Compute-2 的 eth0 收到:
┌──────────────────┬──────────────────┬──────┬─────┬──────┬─────────┐
│ ff:ff:ff:ff:ff:ff│ aa:bb:cc:11 │0x8100│ 201 │0x0806│ ARP ... │
└──────────────────┴──────────────────┴──────┴─────┴──────┴─────────┘
eth0 网卡驱动收包:
│
│ ① DMA 写入 ring buffer
│ ② 构造 skb
│ ③ skb->dev = eth0
│ ④ 检测到帧中有 802.1Q tag
│
▼
内核 VLAN 处理:
│
│ 💡 内核看到 VLAN tag = 201
│ 💡 检查是否有 eth0.201 这个子接口
│ 💡 如果有 → 剥离 VLAN tag,把 skb 交给 eth0.201
│
│ 具体过程:
│ vlan_do_receive(skb)
│ - 从 skb 中提取 VLAN ID = 201
│ - 查找 eth0 上注册的 VLAN 子接口: eth0.201
│ - 剥离 4 字节 802.1Q 头
│ - skb->dev = eth0.201 ← 💡 dev 从 eth0 变成 eth0.201
│ - 继续走 netif_receive_skb() 流程
│
▼
skb 现在的状态:
skb->dev = eth0.201
帧内容 (VLAN tag 已剥离):
┌──────────────────┬──────────────────┬──────┬─────────┐
│ ff:ff:ff:ff:ff:ff│ aa:bb:cc:11 │0x0806│ ARP ... │
└──────────────────┴──────────────────┴──────┴─────────┘
(干净的普通以太网帧)
收包方向的代码视角
c
// 内核收包路径 (简化)
static int __netif_receive_skb_core(struct sk_buff *skb)
{
// skb->dev = eth0
// ... 各种处理 ...
// 检查是否有 VLAN tag
if (skb->vlan_tci) {
// 或者帧头里有 0x8100
vlan_id = skb->vlan_tci & VLAN_VID_MASK; // = 201
// 查找 eth0 上 VLAN 201 对应的子接口
vlan_dev = vlan_find_dev(skb->dev, vlan_id); // = eth0.201
if (vlan_dev) {
// 剥离 VLAN tag
skb->vlan_tci = 0;
// 切换设备
skb->dev = vlan_dev; // skb->dev = eth0.201
// 重新走一遍收包流程,这次 dev 是 eth0.201
return __netif_receive_skb_core(skb);
}
}
}
第六阶段:进入 Compute-2 的 Bridge
text
eth0.201 "收到"帧 (实际是内核把 eth0 收到的帧转交过来)
│
│ eth0.201 是 brq-net1 的端口
│ 所以帧进入 brq-net1
▼
brq-net1 (Linux Bridge)
│
│ ① 学习源 MAC
│ 记录: aa:bb:cc:11 → 来自端口 eth0.201
│
│ ② 查目的 MAC: ff:ff:ff:ff:ff:ff → 广播
│ 泛洪到所有其他端口:
│ - tap-vm2 ← VM-2 会收到
│ - (其他本地同网络 VM 的 tap)
│
▼
tap-vm2
│
▼
VM-2 (10.0.1.12)
│
│ VM-2 内核收到 ARP Request
│ "谁是 10.0.1.12?"
│ "是我!"
│
│ 构造 ARP Reply:
│ ┌──────────────────────────────────────────┐
│ │ Dst MAC: aa:bb:cc:11 (VM-1 MAC, 单播) │
│ │ Src MAC: aa:bb:cc:22 (VM-2 MAC) │
│ │ ARP: 10.0.1.12 is at aa:bb:cc:22 │
│ └──────────────────────────────────────────┘
│
│ 💡 ARP Reply 是单播,不是广播
▼
(ARP Reply 按反向路径回到 VM-1)
完整往返流程总结
text
ARP Request (广播):
VM-1
│ ARP Who has 10.0.1.12?
▼
tap-vm1
│
▼
brq-net1 [Compute-1]
│ 广播 → 泛洪到所有端口
├──► 本地其他 tap (如果有)
│
▼
eth0.201 [Compute-1]
│ ┌──────────────────────────────┐
│ │ 💡 插入 VLAN 201 tag │
│ │ skb->dev = eth0 │
│ │ dev_queue_xmit(skb) │
│ └──────────────────────────────┘
▼
eth0 [Compute-1]
│ DMA 发送
▼
═══════ 物理线缆 ═══════
│ 帧带 VLAN 201 tag
▼
物理交换机
│ VLAN 201 广播泛洪
▼
═══════ 物理线缆 ═══════
│
▼
eth0 [Compute-2]
│ DMA 收包
│ ┌──────────────────────────────┐
│ │ 💡 检测 VLAN tag = 201 │
│ │ 找到子接口 eth0.201 │
│ │ 剥离 VLAN tag │
│ │ skb->dev = eth0.201 │
│ └──────────────────────────────┘
▼
eth0.201 [Compute-2]
│ (eth0.201 是 brq-net1 的端口)
▼
brq-net1 [Compute-2]
│ 广播 → 泛洪到所有端口
▼
tap-vm2
│
▼
VM-2
收到 ARP Request ✅
═══════════════════════════════════════════════
ARP Reply (单播):
VM-2
│ ARP Reply: 10.0.1.12 is at aa:bb:cc:22
│ dst MAC = aa:bb:cc:11 (单播)
▼
tap-vm2
│
▼
brq-net1 [Compute-2]
│ 查 FDB: aa:bb:cc:11 → eth0.201
│ (之前 ARP Request 进来时已经学到了)
▼
eth0.201 [Compute-2]
│ 加 VLAN 201 tag
▼
eth0 [Compute-2]
│
▼
═══════ 物理交换机 ═══════
│ VLAN 201 单播转发
▼
eth0 [Compute-1]
│ 剥 VLAN 201 tag
▼
eth0.201 [Compute-1]
│
▼
brq-net1 [Compute-1]
│ 查 FDB: aa:bb:cc:11 → tap-vm1
▼
tap-vm1
│
▼
VM-1
收到 ARP Reply ✅
学到: 10.0.1.12 → aa:bb:cc:22
现在可以发 ICMP / TCP 了
最核心的问题:为什么 eth0.201 的帧能到达 eth0
text
┌────────────────────────────────────────────────────────────────────┐
│ │
│ eth0.201 和 eth0 的关系不是 "两个独立设备通过某种方式连接" │
│ │
│ 而是: eth0.201 是 eth0 的一个"寄生"子接口 │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ eth0 (物理网卡) │ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ eth0.201 │ │ eth0.202 │ │ eth0.203 │ │ │
│ │ │ (VLAN子) │ │ (VLAN子) │ │ (VLAN子) │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ │ │ │
│ │ 这些子接口住在 eth0 "里面" │ │
│ │ 它们没有自己的物理硬件 │ │
│ │ 它们只是内核里的一层软件逻辑 │ │
│ │ │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ 发送时: │
│ eth0.201 收到帧 → 加 VLAN tag → 直接调用 eth0 发送 │
│ 中间没有经过任何"连线" │
│ 因为 eth0.201 就是 eth0 的一部分 │
│ eth0.201 的代码里直接引用了 eth0: vlan->real_dev = eth0 │
│ │
│ 接收时: │
│ eth0 收到帧 → 看 VLAN tag → 找到 eth0.201 → 剥 tag → 交过去 │
│ 中间也没有经过任何"连线" │
│ 就是内核代码里的一个 if 判断和函数调用 │
│ │
│ 所以正确的理解是: │
│ │
│ eth0.201 → eth0 不是 "包从一个设备传到另一个设备" │
│ 而是 "VLAN 子接口调用了底层物理设备的发送函数" │
│ 就像你调用一个函数一样直接 │
│ │
└────────────────────────────────────────────────────────────────────┘
类比
text
你可以把它理解成:
eth0 = 一个快递公司的物理卡车
eth0.201 = 卡车上标着 "201号线路" 的货舱
eth0.202 = 卡车上标着 "202号线路" 的货舱
往 eth0.201 放一个包裹:
→ 包裹被贴上 "201" 标签
→ 放进卡车
→ 卡车开走
卡车不需要 "从货舱到卡车" 的额外搬运
因为货舱本身就是卡车的一部分
同理:
eth0.201 不需要 "连接到" eth0
因为 eth0.201 本身就是 eth0 的一部分
与 veth 的对比(帮助理解)
text
veth pair:
┌────────┐ ┌────────┐
│ veth-a │ ──────► │ veth-b │
└────────┘ peer └────────┘
两个独立的 net_device
通过 peer 指针关联
一端发 = 另一端收
有明确的 "从 A 到 B" 的过程
VLAN 子接口:
┌─────────────────────┐
│ eth0 │
│ ┌─────────┐ │
│ │eth0.201 │ │
│ └─────────┘ │
└─────────────────────┘
不是两个独立设备
eth0.201 寄生在 eth0 上
eth0.201 发送 = 加 tag + 调用 eth0 发送
没有 "从 eth0.201 到 eth0" 的传输过程
只是一次函数调用
物理交换机视角的mac表变化
css
┌──────────────────────────────────────────────────────────────────┐
│ │
│ 物理交换机 MAC 表 (CAM Table): │
│ │
│ MAC Address │ VLAN │ Port │ 学习时机 │
│ ────────────────┼──────┼────────┼─────────── │
│ aa:bb:cc:11 │ 201 │ Port 1 │ VM-1 发 ARP Request 时学到 │
│ (VM-1) │ │ │ │
│ │ │ │ │
│ aa:bb:cc:22 │ 201 │ Port 2 │ VM-2 发 ARP Reply 时学到 │
│ (VM-2) │ │ │ │
│ │
└──────────────────────────────────────────────────────────────────┘
进一步:如果一个 Compute 节点上有很多 VM
less
Compute-1 上有 50 个 VM,都在 VLAN 201
物理交换机的 MAC 表:
MAC Address │ VLAN │ Port
────────────────┼──────┼────────
aa:bb:cc:01 │ 201 │ Port 1 ← 全都指向 Port 1
aa:bb:cc:02 │ 201 │ Port 1 因为它们都从同一个物理口出来
aa:bb:cc:03 │ 201 │ Port 1
... │ │
aa:bb:cc:50 │ 201 │ Port 1
💡 物理交换机 Port 1 背后有 50 个 MAC
这完全正常,和一个端口接一个二层交换机效果一样
实际上 Linux Bridge 就扮演了那个 "下游交换机" 的角色:
物理交换机 Port 1
│
▼
Compute-1 eth0
│
▼
Linux Bridge (brq-net1) ← 就像一个接入层交换机
│
├── tap-vm1 (aa:bb:cc:01)
├── tap-vm2 (aa:bb:cc:02)
├── tap-vm3 (aa:bb:cc:03)
...
└── tap-vm50 (aa:bb:cc:50)
这带来一个实际问题:
ini
┌──────────────────────────────────────────────────────────────┐
│ │
│ VLAN 模型下物理交换机的 MAC 表可能会很大 │
│ │
│ 假设: │
│ - 10 个计算节点 │
│ - 每个节点 50 个 VM │
│ - 每个 VM 一个 MAC │
│ │
│ 物理交换机需要学习: 10 × 50 = 500 条 MAC 记录 │
│ │
│ 如果多个 VLAN: 500 × N 个 VLAN │
│ │
│ 普通接入层交换机的 MAC 表通常 8K-32K 条 │
│ 大规模场景下可能溢出 │
│ │
│ 这就是为什么大规模云用 VXLAN/Geneve 而不用 VLAN: │
│ - VXLAN 把 VM MAC 封装在隧道内层 │
│ - 物理交换机只需要学习宿主机的 MAC │
│ - 10 个节点 = 10 条 MAC 记录 │
│ │
└──────────────────────────────────────────────────────────────┘
一句话总结
eth0.201不是一个独立的设备,它是eth0上面的一层软件逻辑。发送时eth0.201在帧里插入 VLAN 201 tag 然后直接调用eth0的发送函数 ;接收时eth0看到 VLAN 201 tag 就剥掉 tag 直接把 skb 转交给eth0.201。中间没有任何"传输",只是内核代码里的函数调用。所以 ARP 广播帧从 Bridge 泛洪到eth0.201,就等于直接从eth0发出去了(带上了 VLAN tag)。
VM-1 发包到 VM-2 单播发包过程
当vm-1已经学习到vm-2的mac地址,后续就许可以正常二层发包了,那包是如何通过vm-1 的tap口发送到eth0.201这个vlan子接口的呢? 其实这个和arp请求和回复的过程是分不开的,因为arp回包,最终是通过eth0.201这个口回到linux 网桥的,所以网桥就会认为后续目标mac是vm-2 mac地址的二层包,就会从eth0.201口出,进而打上vlan tag从eth0发出,进入真实的物理交换机trunk口,到达对端物理机的eth0,然后继续下发.
其实linux bridge和真实的物理交换机一样,在学习mac地址的时候,只关心请求是从哪个端口过来的,以及arp回复是从哪个端口回复过来的,后续在单播发包的时候,就能知道对应的帧要从哪个端口发出来的,要匹配从哪个端口发出去。
前提
ARP 已完成,VM-1 知道了:
text
VM-1 ARP 缓存:
10.0.1.12 → aa:bb:cc:22 (VM-2 的 MAC)
先看结论
text
tap-vm1 和 eth0.201 之间没有直接连接
它们之间隔着一个 Linux Bridge (brq-net1)
tap-vm1 ──────► brq-net1 ──────► eth0.201
(Bridge)
│
│ 查 FDB 转发表
│ 决定从哪个端口出去
VM-1 构造数据帧
text
VM-1 应用程序: ping 10.0.1.12
VM-1 内核:
① 构造 ICMP Echo Request
② 构造 IP 头: src=10.0.1.10, dst=10.0.1.12
③ 查路由表: 10.0.1.12 和自己同子网 → 直接发送,不需要网关
④ 查 ARP 缓存: 10.0.1.12 → aa:bb:cc:22 (已有)
⑤ 构造以太网帧头:
┌────────────────────────────────────────────────────────────┐
│ Dst MAC: aa:bb:cc:22 (VM-2, 单播) │
│ Src MAC: aa:bb:cc:11 (VM-1) │
│ Type: 0x0800 (IPv4) │
│ │
│ IP: src=10.0.1.10, dst=10.0.1.12, proto=ICMP │
│ ICMP: Echo Request │
└────────────────────────────────────────────────────────────┘
⑥ 调用 virtio-net 驱动发送
从 VM 到 tap-vm1
text
VM-1 (Guest)
│
│ Guest 内核 → virtio-net 驱动 → TX vring
│ [复制 #1: Guest skb → vring 共享内存]
│
│ 通知 Host (VM Exit 或 eventfd)
▼
vhost-net (Host)
│
│ 从 vring 读取数据
│ 分配 Host skb
│ [复制 #2: vring → Host skb]
│
│ skb->dev = tap-vm1
│ skb->data = 完整以太网帧
│
│ 调用 netif_receive_skb(skb)
│ 意思是: "tap-vm1 这个网口收到了一个帧"
▼
tap-vm1 "收到" 了这个帧
从 tap-vm1 到 brq-net1:核心机制
tap-vm1 是 bridge 的一个端口
text
brctl show:
bridge name bridge id STP interfaces
brq-net1 8000.xxxxxxxxxxxx no tap-vm1
tap-vm5 (其他VM)
eth0.201
tap-vm1 不是一个"独立的设备"在自由工作,它已经被加入了 bridge。
这意味着:
tap-vm1 收到的帧不会走正常的协议栈,而是直接进入 bridge 的转发逻辑
内核里发生了什么
text
netif_receive_skb(skb)
│
│ skb->dev = tap-vm1
│
▼
__netif_receive_skb_core(skb)
│
│ 💡 关键检查:
│ tap-vm1 是否属于某个 bridge?
│
│ if (skb->dev->priv_flags & IFF_BRIDGE_PORT) {
│ // 是 bridge 端口!
│ // 不走正常协议栈
│ // 转入 bridge 处理
│ return br_handle_frame(skb);
│ }
│
▼
br_handle_frame(skb)
│
│ 帧进入了 Linux Bridge 的处理流程
▼
用一句话说:
因为 tap-vm1 被
brctl addif brq-net1 tap-vm1加入了 bridge,所以内核在收包路径上检测到它是 bridge port,自动把帧转交给 bridge 处理,而不是送到上层协议栈。
brq-net1 内部的处理过程
text
br_handle_frame(skb)
│
│ skb->dev = tap-vm1 (入端口)
│
▼
br_handle_frame_finish(skb)
│
│ ① 学习源 MAC
│ br_fdb_update(bridge, tap-vm1, src_mac=aa:bb:cc:11)
│ FDB 记录: aa:bb:cc:11 → tap-vm1
│
│ ② 查找目的 MAC
│ dst = br_fdb_find(bridge, dst_mac=aa:bb:cc:22)
│
│ 💡 这里有两种情况:
│
├─── 情况A: FDB 里有 aa:bb:cc:22 的记录
│ │
│ │ 之前 VM-2 的 ARP Reply 经过这个 bridge 时
│ │ bridge 已经学到了:
│ │ aa:bb:cc:22 → eth0.201 (从 eth0.201 端口进来的)
│ │
│ │ 所以直接单播转发到 eth0.201
│ ▼
│ br_forward(eth0.201, skb)
│
└─── 情况B: FDB 里没有 aa:bb:cc:22
│
│ 泛洪到所有端口 (除了入端口 tap-vm1)
│ → eth0.201, tap-vm5, ...
▼
br_flood(bridge, skb, 排除 tap-vm1)
FDB 是什么时候学到 aa:bb:cc:22 → eth0.201 的?
text
回忆之前的 ARP 过程:
VM-2 的 ARP Reply 从 Compute-2 发来
→ 物理网络 (VLAN 201)
→ Compute-1 eth0
→ eth0.201 (剥 VLAN tag)
→ brq-net1 收到帧,入端口是 eth0.201
→ bridge 学习: 帧的 src MAC = aa:bb:cc:22,入端口 = eth0.201
→ FDB 记录: aa:bb:cc:22 → eth0.201 ✅
所以现在 VM-1 发单播帧给 aa:bb:cc:22 时
bridge 查 FDB 就知道该往 eth0.201 发
br_forward:Bridge 转发到 eth0.201
text
br_forward(eth0.201, skb)
│
│ ① 检查出端口状态
│ eth0.201 是 UP 的? → 是
│
│ ② 修改 skb->dev
│ skb->dev = eth0.201 ← 💡 从 tap-vm1 变成 eth0.201
│
│ ③ 调用 br_forward_finish(skb)
│
▼
br_forward_finish(skb)
│
│ 可能经过 netfilter FORWARD 链 (如果开了 bridge-nf-call-iptables)
│
▼
br_dev_queue_push_xmit(skb)
│
│ 💡 最终调用:
│ dev_queue_xmit(skb)
│
│ 此时 skb->dev = eth0.201
│ 所以这等于: "请 eth0.201 把这个帧发出去"
│
▼
eth0.201 的发送函数被调用
代码视角
c
// bridge 转发 (简化)
static void br_forward(struct net_bridge_port *to, struct sk_buff *skb)
{
// to->dev = eth0.201
// 修改 skb 的出设备
skb->dev = to->dev; // skb->dev = eth0.201
// ... netfilter hook ...
// 最终发送
dev_queue_xmit(skb);
}
后续:eth0.201 → eth0 → 物理网络
这部分在上一次已经详细讲过,简要回顾:
text
dev_queue_xmit(skb)
│
│ skb->dev = eth0.201
▼
eth0.201 的发送函数: vlan_dev_hard_start_xmit()
│
│ ① 插入 VLAN 201 tag
│ ② skb->dev = eth0 (底层物理设备)
│ ③ dev_queue_xmit(skb)
▼
eth0 的发送函数: ndo_start_xmit()
│
│ DMA 发送到物理网络
▼
物理线缆 → 交换机 → Compute-2
skb->dev 的完整变化链
text
阶段 skb->dev 谁改的
──── ──────── ──────
vhost-net 构造 skb tap-vm1 vhost-net
netif_receive_skb() tap-vm1 (不变)
br_handle_frame() tap-vm1 (不变)
br_forward() eth0.201 bridge 转发逻辑
vlan_dev_hard_start_xmit() eth0 VLAN 子接口发送函数
ndo_start_xmit() eth0 (不变,最终发送)
一共改了两次:
text
tap-vm1 → eth0.201 → eth0
│ │ │
│ │ └── VLAN 子接口改的 (加 tag,交给物理网卡)
│ └─────────── Bridge 转发改的 (查 FDB,选出端口)
└─────────────────────── vhost-net 创建 skb 时设的 (入端口)
完整流程一张图
text
VM-1 应用: ping 10.0.1.12
│
│ Guest 内核封装: Eth + IP + ICMP
│ dst MAC = aa:bb:cc:22 (单播)
▼
virtio-net TX vring
│
│ [复制: Guest → vring]
▼
════════════ Guest/Host 边界 ════════════
│
vhost-net
│ [复制: vring → Host skb]
│ skb->dev = tap-vm1
▼
netif_receive_skb(skb)
│
│ 检查: tap-vm1 是 bridge port? → 是!
▼
br_handle_frame(skb) ┐
│ │
│ 学习: aa:bb:cc:11 → tap-vm1 │
│ 查找: aa:bb:cc:22 → eth0.201 (FDB命中) │ Linux Bridge
▼ │ 内部处理
br_forward(to=eth0.201, skb) │
│ │
│ skb->dev = eth0.201 │
▼ │
dev_queue_xmit(skb) ┘
│
│ skb->dev = eth0.201
▼
vlan_dev_hard_start_xmit(skb) ┐
│ │
│ 插入 VLAN 201 tag │ VLAN 子接口
│ skb->dev = eth0 │ 处理
▼ │
dev_queue_xmit(skb) ┘
│
│ skb->dev = eth0
▼
eth0 ndo_start_xmit(skb) ┐
│ │
│ 填 TX descriptor │ 物理网卡
│ DMA │ 驱动
│ doorbell │
▼ ┘
物理线缆
│
│ 帧内容:
│ ┌────────┬────────┬──────┬─────┬──────┬──────────────┐
│ │ dst:22 │ src:11 │0x8100│ 201 │0x0800│ IP+ICMP │
│ └────────┴────────┴──────┴─────┴──────┴──────────────┘
▼
物理交换机 (VLAN 201 转发)
│
▼
Compute-2 eth0
│ 剥 VLAN tag → eth0.201 → brq-net1 → tap-vm2 → VM-2 ✅
所以回答你的问题
包是如何从 tap-vm1 到达 eth0.201 的?
text
┌────────────────────────────────────────────────────────────┐
│ │
│ 不是 tap-vm1 "直接连接到" eth0.201 │
│ │
│ 而是: │
│ │
│ tap-vm1 和 eth0.201 都是 brq-net1 (Linux Bridge) 的端口 │
│ │
│ 帧从 tap-vm1 进入 bridge │
│ bridge 查 FDB 转发表 │
│ 发现目的 MAC 对应的出端口是 eth0.201 │
│ bridge 把帧从 eth0.201 端口转发出去 │
│ │
│ 本质上就是一个软件交换机在做二层转发 │
│ 和物理交换机的工作原理一模一样: │
│ │
│ 物理交换机: 端口1收到帧 → 查 MAC 表 → 从端口3转发 │
│ Linux Bridge: tap-vm1收到帧 → 查 FDB → 从 eth0.201转发 │
│ │
└────────────────────────────────────────────────────────────┘
一句话总结
tap-vm1 和 eth0.201 都是 Linux Bridge (
brq-net1) 的端口。帧从 tap-vm1 进入 bridge 后,bridge 根据目的 MAC 查询 FDB 转发表,发现aa:bb:cc:22是从eth0.201端口学到的,于是把skb->dev改为eth0.201并调用dev_queue_xmit()转发出去。Bridge 就是它们之间的"交换机"。