Linux Bridge源码分析
Linux Bridge是Linux内核中实现的二层虚拟网络设备,它工作在数据链路层,可以连接多个网络接口,实现类似物理交换机的功能。在容器网络、虚拟化等场景中,Linux Bridge扮演着重要角色,为虚拟网络提供了基础的二层转发能力。
本文将从源码层面深入分析Linux Bridge的实现原理,包括Bridge的创建、网络接口的添加、数据帧的接收和转发等核心流程。通过对内核源码的剖析,理解Linux Bridge的工作机制和实现细节。
1. 创建Bridge
Linux Bridge在内核中是以net_device
结构体表示的,这体现了Linux内核将所有网络设备抽象为统一的net_device
结构的设计理念。创建Bridge的过程实际上是分配并初始化一个特殊的网络设备,这个设备除了通用的net_device
信息外,还包含了Bridge特有的net_bridge
结构体信息。
1.1 创建过程
Bridge设备的初始化过程包括:
分配内存空间(同时包含
net_device
和net_bridge
两部分)设置Bridge的基本属性(如MAC地址、网络命名空间等)
注册Bridge特定的操作函数集(通过
br_netdev_ops
实现)将设备注册到内核网络子系统
1.2 内存分配
alloc_netdev()
宏通过调用alloc_netdev_mqs()
函数来实现具体的内存分配和初始化工作。这个函数不仅分配了设备结构体的内存,还初始化了设备的各种队列、列表和基本属性。
1.3 设备初始化
alloc_netdev_mqs()
函数是网络设备创建的核心函数,它完成了从内存分配到设备初始化的完整流程。对于Bridge设备,该函数执行了以下关键步骤:
内存分配与对齐 :函数首先计算所需的内存大小,包括
net_device
结构体本身和私有数据区(对于Bridge是net_bridge
结构体)。为了优化缓存性能,内存按照32字节(NETDEV_ALIGN)对齐。per-CPU引用计数初始化 :通过
alloc_percpu()
为每个CPU分配独立的引用计数,避免在多核系统中的缓存竞争,提高性能。地址初始化 :调用
dev_addr_init()
初始化设备地址结构,为后续的MAC地址分配做准备。同时初始化多播(dev_mc_init()
)和单播(dev_uc_init()
)地址列表。网络命名空间设置 :将设备关联到初始网络命名空间(
init_net
),支持网络隔离。链表和队列初始化 :初始化各种链表结构(NAPI列表、注销列表、链路监视列表等),这些链表用于管理设备的不同状态和功能。
调用设备特定的setup函数 :这是关键的一步,对于Bridge设备,这里会调用
br_dev_setup()
函数,完成Bridge特有的初始化工作,包括设置设备类型为Ethernet 、初始化设备操作函数指针等。队列分配 :分配发送和接收队列。即使是虚拟设备如Bridge,也需要这些队列结构来处理数据包。
1.4 Bridge操作函数集
Bridge设备通过br_netdev_ops
结构体定义了一组特定的操作函数,这些函数覆盖了网络设备的各种操作,包括打开/关闭设备、发送数据、获取统计信息、添加/删除从设备等。这种设计使得Bridge设备能够实现其特定的网络功能。
1.5 Bridge核心数据结构
net_bridge
结构体是Bridge的核心数据结构,它包含了Bridge运行所需的所有信息,包括端口列表、MAC地址转发表、STP(生成树协议)相关参数、多播组管理信息等。其中最重要的是port_list
字段,它维护了所有接入到Bridge的网络接口,比如veth pair
等设备。
2. Bridge添加网络接口
2.1 添加过程
向Bridge添加网络接口是Bridge实现其转发功能的关键步骤。这个过程涉及到多个重要操作:
接口验证 :确保要添加的接口符合Bridge的要求(必须是以太网设备,不能是回环设备等)
创建端口结构 :为新接口创建
net_bridge_port
结构,建立接口与Bridge的关联设置混杂模式 :将接口设置为混杂模式,使其能够接收所有经过的数据帧
注册rx_handler :这是最关键的一步,通过注册
br_handle_frame
函数,使得该接口收到的所有数据帧都会交给Bridge处理更新转发表 :将接口的MAC地址加入Bridge的转发表
启用STP :如果Bridge启用了生成树协议,则在该端口上启用相应功能
2.2 rx_handler注册机制
在这个函数中,有下面这么一行代码,它注册了这个网络接口的rx_handler
为br_handle_frame
,kernel在skb(可以理解为kernel中数据包的结构)的处理过程中会用到各种函数指针,因此会有很多注册逻辑,这里先记住注册了这个处理函数。
2.3 net_device结构
net_device
是Linux内核中表示网络设备的核心数据结构。它包含了网络设备的所有信息,包括设备名称、硬件地址、MTU、队列信息、操作函数指针等。对于Bridge来说,每个加入的接口都是一个net_device
实例。
3. Linux数据帧处理过程
在继续分析Bridge之前,有必要先介绍下内核对数据包的处理过程,因为大概过程基本一致,只是Bridge设备有特殊逻辑。
3.1 Linux收包概览
Linux网络收包是一个复杂的过程,涉及硬件中断、软中断、协议栈处理等多个阶段。整个过程采用了中断驱动的异步处理模式,通过硬中断快速响应,软中断延迟处理的方式,既保证了及时性,又避免了长时间占用CPU。
当网卡上有数据到达时,Linux处理数据包的路径如上图。
网卡以DMA的方式把数据帧写入内存。
网卡向CPU发起一个硬中断,通知CPU有数据到达,要紧急处理。
CPU调用内核中网络驱动注册的中断处理函数。
中断处理函数发出软中断。
ksoftirqd处理软中断。
ksoftirqd调用网卡驱动的
poll()
函数处理数据帧。网卡驱动的
poll()
从RingBuffer上取出数据包,保存为skb
结构。内核对
skb
进行处理,比如设备层处理,协议层处理等,对于TCP包来说,最后会放到用户空间的socket
等待队列中。
3.2 Linux网络初始化
Linux kernel在启动的过程中,需要执行一系列操作,以做好接收网络数据的准备。
3.2.1 创建ksoftirqd
系统启动时会通过early_initcall(spawn_ksoftirqd)
创建专门用于处理软中断的内核线程ksoftirqd。每个CPU核心都有一个对应的ksoftirqd线程,保证了软中断处理的并行性。这些线程会循环检查是否有软中断需要处理,并调用相应的处理函数。
3.2.2 初始化网络设备
网络设备初始化过程中会创建per-CPU的数据结构,注册软中断处理函数,初始化协议类型哈希表等。这些准备工作为后续的网络数据处理奠定了基础。
通过subsys_initcall(net_dev_init)
执行网络设备初始化。
初始化
softnet_data
等per cpu数据结构,网卡驱动的pool()
函数后面会注册到softnet_data
结构体中的poll_list
字段。调用
open_softirq()
注册RX_SOFTIRQ和TX_SOFTIRQ对应的中断处理函数。初始化
packet_type
哈希表,为所有可能的协议类型创建并初始化哈希表(ptype_all
和ptype_base
),这些哈希表用于快速查找特定协议的数据包处理器。
3.2.3 注册协议栈
Linux支持多种网络协议,每种协议都有自己的处理函数。协议栈注册过程将这些处理函数注册到相应的数据结构中,使得内核能够根据数据包的协议类型调用正确的处理函数。比如IP,TCP,UDP等协议,对应的实现函数为ip_rcv()
, tcp_v4_rcv()
和udp_rcv()
,将这些函数注册到了inet_protos
和ptype_base
数据结构中了。
inet_init()
完整代码如下。
inet_protos
数组记录了各种传输层协议的处理函数,比如UDP和TCP的处理函数是udp_rcv()
与tcp_v4_rcv()
,通过协议号作为索引可以快速找到对应的处理函数。
ptype_base
哈希表存储了各种网络层协议的处理函数,比如IP的处理函数ip_rcv()
,通过协议类型(如ETH_P_IP)可以找到对应的处理函数。
3.2.4 注册网卡驱动
网卡驱动注册是硬件与内核交互的关键步骤。驱动注册过程会设置中断处理函数、初始化DMA、分配RingBuffer等资源,为网卡正常工作做好准备。以igb
网卡驱动为例,通过module_init(igb_init_module)
向内核注册网卡驱动初始化函数,不同网卡的初始化函数不一样。
驱动的probe函数是设备初始化的核心,它会执行硬件检测、资源分配、中断注册等一系列操作。对于网卡驱动来说,这包括设置MAC地址、初始化RingBuffer、注册网络设备操作函数等。
pci_register_driver()
执行完成后,Linux就知道了驱动的信息,接下来就会调用驱动的probe()
方法,igb的probe
函数是igb_probe
,这个函数非常长,贴部分代码理解下。
3.2.5 启动网卡
前面的初始化都完成后,内核就可以调用上面net_device_ops
结构体中对应的函数执行各种网卡操作,比如启动,关闭,设置MAC等。
网卡启动过程是网络设备真正开始工作的关键步骤。在这个过程中,会分配RingBuffer、注册中断处理函数、启动硬件等。特别是RingBuffer的分配和中断处理函数的注册,直接决定了网卡能否正常接收数据。
在启动网卡的过程中,会调用igb_open()
-> __igb_open()
-> igb_setup_all_tx_resources()
/igb_setup_all_rx_resources()
。在 igb_setup_all_rx_resources()
调用中,分配了RingBuffer,建立内存和Rx队列的映射关系。在igb_request_irq()
中注册了中断处理函数,在发生中断时调用igb_request_msix()
进行处理。
多队列网卡会创建多个接收队列,每个队列都有独立的RingBuffer和中断处理。可以充分利用多核CPU的并行处理能力,提高网络处理性能。从igb_setup_all_rx_resources()
中可以看到,一个循环中创建了多个队列。
经过上述处理后,Linux就做好了接收数据包的准备。当数据帧从网线到达网卡后,经过网卡驱动执行DMA,发出硬中断,内核执行硬中断处理函数,再发出软中断,最后触发ksoftirqd执行软中断处理函数net_rx_action()
,接着执行网卡驱动注册的poll()
方法,把数据帧从RingBuffer上取下来,然后进入GRO(Generic Receive Offload)处理逻辑,最后会进入netif_receive_skb()
函数进行处理,这个函数是设备层进入协议层前的处理逻辑,二层相关的处理会在这里体现。
4. 进入Bridge处理逻辑
netif_receive_skb()
是网络设备层与协议栈的分界点。在这个函数中,会根据设备是否注册了rx_handler来决定数据包的处理路径。对于Bridge设备来说,之前注册的br_handle_frame
函数会在这里被调用,从而进入Bridge的处理逻辑。
netif_receive_skb()
逻辑比较简单,主要是对数据包进行了RPS的处理,然后调用了__netif_receive_skb()
。
__netif_receive_skb()
做了个特殊类型判断后就调用了__netif_receive_skb_core()
。
__netif_receive_skb_core()
是网络接收路径的核心函数,它负责:
处理VLAN标签
调用rx_handler(如果存在)
根据协议类型分发数据包
处理各种特殊情况(如tcpdump抓包、netfilter等)
在__netif_receive_skb_core()
函数中,调用rx_handler()
函数(在br_add_if()
中注册的),也就是br_handle_frame()
,这里就和Bridge对数据帧的处理逻辑关联上了,通过如下代码进入了Bridge处理逻辑。
接下来就要看看Bridge入口函数即br_handle_frame()
的处理逻辑了。
5. Bridge处理入口
br_handle_frame()
是Bridge处理数据帧的入口函数。这个函数执行了一系列的检查和判断,包括:
验证源MAC地址的有效性
处理链路本地地址(如STP协议报文)
根据端口状态决定是否转发
调用Netfilter钩子进行过滤
br_handle_frame()
的作用,可以理解为“ 在Bridge的port收到frame时调用这个函数进行处理 ”,逻辑大概如下。
如果是loopback的packet,返回RX_HANDLER_PASS ,表示应该由上层处理。
检查二层源MAC,如果无效则drop。
从Bridge上获取结构为
net_bridge_port
的port信息。特殊MAC处理,这里不分析。
转发处理 ,根据网桥端口状态(br_state_forwarding或br_state_learning)来决定如何处理数据包。
BR_STATE_FORWARDING :如果端口处于转发状态并且存在自定义hook则交给hook处理,否则继续交给BR_STATE_LEARNING逻辑处理,因为这里没有
break
。BR_STATE_LEARNING: 如果目标MAC地址与网桥设备MAC地址相同 ,将数据包标记为发往本地主机(PACKET_HOST类型)。调用Netfilter的NF_HOOK宏,执行NFPROTO_BRIDGE协议族的NF_BR_PRE_ROUTING钩子链,最后会调用br_handle_frame_finish函数。
默认情况(即不满足上述条件时):丢弃数据包并释放内存资源,返回RX_HANDLER_CONSUMED。
正常情况下都会执行到br_handle_frame_finish()
。
6. br_handle_frame_finish()
br_handle_frame_finish()
是Bridge转发逻辑的核心函数。它实现了:
MAC地址学习 :更新转发表,记录源MAC地址与端口的对应关系
转发决策 :根据目标MAC地址决定如何转发
广播/多播处理 :处理特殊类型的数据帧
本地交付 :将需要本地处理的数据帧送到上层协议栈
这个函数主要对以太网数据帧执行进一步的决策和执行,主要逻辑如下。
获取当前接收设备对应的网桥端口结构体,并检查端口是否启用。若未启用,则丢弃数据包并返回。
调用
br_allowed_ingress()
检查数据包是否满足入站过滤规则,即是否允许其进入网桥设备。如果不满足,则丢弃数据包。更新MAC地址学习表 ,将源MAC地址与当前端口关联起来,以便后续的数据包可以基于MAC地址表进行快速转发,避免MAC地址欺骗。
根据目标MAC地址进行广播,多播,单播等操作:
如果目标MAC地址为广播地址,则创建skb2指向原始skb,并准备将其传递给本地主机。
如果目标MAC地址为多播地址,则查找多播数据库条目,并根据配置判断是否需要转发至多播组或本地主机。同时增加多播统计计数。
如果目标MAC地址为单播地址且存在于本地 (通过
__br_fdb_get()
查询),则同样创建skb2指向原始skb,并跳过转发 ,因为该数据包应发送到本地主机。
对于需要转发的数据包(即skb非空)进行处理:
若存在对应的目标单播MAC地址条目dst,则更新条目的最后使用时间,并调用
br_forward(dst->dst, skb, skb2)
函数直接转发至相应端口。若不存在目标单播MAC地址条目 ,则调用
br_flood_forward(br, skb, skb2)
对数据包进行泛洪转发 ,即将数据包转发至除接收端口之外的所有端口。
如果此时skb2还存在(即需要发送给本地主机的数据包),则调用
br_pass_frame_up(skb2)
将数据包传递给上层协议栈。在所有情况结束后,清理资源并返回相应的结果状态。
在容器环境中,Bridge上会接入很多veth pair
,经过veth pair
的单播数据帧要么通过第5步转发给另一个veth pair
,要么通过第6步送到上层协议栈处理,接下来分析看一下。
7. 送到上层协议栈
当数据帧的目标MAC地址是Bridge自身的MAC地址时,或者Bridge处于混杂模式时,数据帧需要送到本地主机的上层协议栈处理。 br_pass_frame_up()
函数负责将数据帧从Bridge层传递到网络层,主要工作包括:
更新统计信息
检查VLAN和出站规则
修改skb的设备指针,避免再次进入Bridge处理
调用
netif_receive_skb()
重新进入协议栈处理流程
根据前面的分析,单播frame会通过br_pass_frame_up()
将skb传递给上层网络协议栈处理。这个函数主要执行如下操作:
更新Bridge统计信息。
更新skb的dev为bridge设备,作用是避免再次进入bridge处理逻辑。因为bridge设备的rx_handler函数没有被设置,所以就不会再次进入bridge逻辑,而是直接进入上层协议栈处理,在TCP/IP网络中就是送给IP协议处理。
8. 转发
Bridge的转发功能是其核心功能之一。当Bridge在转发表中找到目标MAC地址对应的端口时,会执行直接转发;否则会执行泛洪转发。
8.1 直接转发
br_forward()
函数实现了直接转发功能。它会检查是否应该转发到目标端口(避免发送回源端口),然后调用__br_forward()
执行实际的转发操作。转发过程中会经过Netfilter的FORWARD钩子,最终通过br_forward_finish()
完成转发。
8.2 泛洪转发
当Bridge不知道目标MAC地址对应的端口时,会执行泛洪转发,即将数据帧发送到除源端口外的所有端口。 br_flood_forward()
函数遍历所有端口,对每个符合条件的端口调用转发函数。
转发表管理
Bridge维护了一个MAC地址转发表(FDB - Forwarding Database),记录了MAC地址与端口的对应关系。这个表通过哈希表实现,支持快速查找。每次收到数据帧时,Bridge都会更新这个表,记录源MAC地址来自哪个端口。表项有老化时间,长时间未使用的表项会被删除。
总结
通过对Linux Bridge源码的深入分析,我们了解了Bridge的完整工作流程:
Bridge创建 :Bridge作为一个特殊的网络设备被创建,拥有自己的
net_device
和net_bridge
结构。接口管理 :通过注册rx_handler机制,Bridge能够接管加入其中的网络接口的所有接收流量。
数据处理流程:
网卡通过DMA将数据写入内存,触发中断
内核通过软中断机制处理数据包
数据包经过
netif_receive_skb()
进入协议栈Bridge端口的rx_handler(
br_handle_frame()
)被调用根据端口状态和目标地址进行转发决策
转发机制:
MAC地址学习:动态维护MAC地址与端口的映射关系
单播转发:查表直接转发到目标端口
广播/多播:复制到多个端口
未知单播:泛洪到所有端口
本地交付:目标是Bridge自身时送到上层协议栈
性能优化:
使用RCU机制保证并发访问的安全性
哈希表实现快速的MAC地址查找
per-CPU数据结构减少锁竞争
NAPI机制批量处理数据包
Linux Bridge的设计充分体现了内核网络子系统的优雅架构。通过抽象的设备模型、灵活的钩子机制、高效的数据结构,Bridge实现了一个功能完整、性能优异的二层转发设备。这种设计不仅满足了虚拟化和容器网络的需求,也为网络功能的扩展提供了良好的基础。
在容器网络场景中,Bridge配合veth pair使用,为容器提供了二层网络连接能力。理解Bridge的工作原理,对于深入理解容器网络、解决网络问题、优化网络性能都有重要意义。
参考
深入理解Linux网络
Linux Kernel Development Third Edition