OpenStack neutron vlan+bridge构建vpc网络时同子网虚拟机跨节点互通过程细节分析

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 就是它们之间的"交换机"

相关推荐
几许1 小时前
高并发有序顺序号生成中间件 - 架构设计文档
java·后端
小码哥_常1 小时前
Spring Boot 邂逅Elasticsearch:打造搜索“光速引擎”
后端
程序员Terry2 小时前
Docker 部署 RocketMQ 5.1.0 踩坑实录:从超时到 Console 连不上的完整解决之路
java·后端
tsyjjOvO2 小时前
SpringMVC 从入门到精通(续)
java·后端·spring
Binary-Jeff2 小时前
MySQL MVCC 原理解析:Undo Log、ReadView 与版本可见性机制
java·数据库·后端·mysql·spring
木易 士心2 小时前
AI辅助开发:前端“加速器”还是后端“稳定器”?——基于项目类型与用户规模的实战指南
人工智能·后端
逆境不可逃2 小时前
【后端新手谈 04】Spring 依赖注入所有方式 + 构造器注入成官方推荐的原因
java·开发语言·spring boot·后端·算法·spring·注入方式
程序员爱钓鱼2 小时前
Go并发同步核心库:syn 包深度指南
后端·面试·go