1 . 前言
本文是參考附錄上的資料整理而成,以幫助讀者更好的理解kernel中brdige 模塊代碼。
2. 網橋的原理
2.1 橋接的概念
簡單來說,橋接就是把一台機器上的若干個網絡接口“連接”起來。其結果是,其中一個網口收到的報文會被復制給其他網口並發送出去。以使得網口之間的報文能夠互相轉發。
交換機就是這樣一個設備,它有若干個網口,並且這些網口是橋接起來的。於是,與交換機相連的若干主機就能夠通過交換機的報文轉發而互相通信。
如下圖:主機A發送的報文被送到交換機S1的eth0口,由於eth0與eth1、eth2橋接在一起,故而報文被復制到eth1和eth2,並且發送出 去,然后被主機B和交換機S2接收到。而S2又會將報文轉發給主機C、D。
交換機在報文轉發的過程中並不會篡改報文數據,只是做原樣復制。然而橋接卻並不是在物理層實現的,而是在數據鏈路層。交換機能夠理解數據鏈路層的報文,所以實際上橋接卻又不是單純的報文轉發。
交換機會關心填寫在報文的數據鏈路層頭部中的Mac地址信息(包括源地址和目的地址),以便了解每個Mac地址所代表的主機都在什么位置(與本交換機的哪個網口相連)。在報文轉發時,交換機就只需要向特定的網口轉發即可,從而避免不必要的網絡互。這個
就是交換機的“地址學習”。但是如果交換機遇到一個自己未學習到的地址,就不會知道這個報文應該從哪個網口轉發,則只好將報文轉發給所有網口(接收報文的那個網口除外)。比如主機C向主機A發送一個報文,報文來到了交換機S1的eth2網口上。假設S1剛剛啟
動,還沒有學習到任何地址,則它會將報文轉發給eth0和 eth1。同時,S1會根據報文的源Mac地址,記錄下“主機C是通過eth2網口接入的”。於是當主機A向C發送報文時,S1 只需要將報文轉發到 eth2網口即可。而當主機D向C發送報文時,假設交換機S2將報文轉發
到了S1的eth2網口(實際上S2也多半會因為地址學習而不這么做),則S1會 直接將報文丟棄而不做轉發(因為主機C就是從eth2接入的)。然而,網絡拓撲不可能是永不改變的。假設我們將主機B和主機C換個位置,當主機C發出報文時(不管發給誰),交換機S1的
eth1口收到報文,於是交換機 S1會更新其學習到的地址,將原來的“主機C是通過eth2網口接入的”改為“主機C是通過eth1網口接入的”。但是如果主機C一直不發送報文呢?S1將一直認為“主機C是通過eth2網口接入的”,於是將其他主機發送給C的報文都從eth2轉
發出去,結果報文就發 丟了。所以交換機的地址學習需要有超時策略。對於交換機S1來說,如果距離最后一次收到主機C的報文已經過去一定時間了(默認為5分鍾),則S1需要忘記 “主機C是通過eth2網口接入的”這件事情。這樣一來,發往主機C的報文又會被轉發到
所有網口上去,而其中從eth1轉發出去的報文將被主機C收到。
2.2 linux的橋接實現
linux內核支持網口的橋接(目前只支持以太網接口)。但是與單純的交換機不同,交換機只是一個二層設備,對於接收到的報文,要么轉發、要么丟棄。小型的交換機里面只需要一塊交換芯片即可,並不需要CPU。而運行着linux內核的機器本身就是一台主機,有可
能就是網絡報文的目的地。其收到的報文除了轉 發和丟棄,還可能被送到網絡協議棧的上層(網絡層),從而被自己消化。
linux內核是通過一個虛擬的網橋設備來實現橋接的。這個虛擬設備可以綁定若干個以太網接口設備,從而將它們橋接起來。如下圖(摘自ULNI):
網橋設備br0綁定了eth0和eth1。對於網絡協議棧的上層來說,只看得到br0,因為橋接是在數據鏈路層實現的,上層不需要關心橋接的細節。於是協議棧上層需要發送的報文被送到br0,網橋設備的處理代碼再來判斷報文該被轉發到eth0或是eth1,或者兩者皆是;反
過來,從eth0或從eth1接收到的報文被提交給網橋的處理代碼,在這里會判斷報文該轉發、丟棄、或提交到協議棧上層。而有時候eth0、eth1也可能會作為報文的源地址或目的地址,直接參與報文的發送與接收(從而繞過網橋)。
2.3 網橋的功能
a. MAC學習:學習MAC地址,起初,網橋是沒有任何地址與端口的對應關系的,它發送數據,還是得想HUB一樣,但是每發送一個數據,它都會關心數據包的來源MAC是從自己的哪個端口來的,由於學習,建立地址-端口的對照表(CAM表)。
b. 報文轉發:每發送一個數據包,網橋都會提取其目的MAC地址,從自己的地址-端口對照表(CAM表)中查找由哪個端口把數據包發送出去。
3. 網橋的配置
在Linux里面使用網橋非常簡單,僅需要做兩件事情就可以配置了。其一是在編譯內核里把CONFIG_BRIDGE或CONDIG_BRIDGE_MODULE編譯選項打開;其二是安裝brctl工具。第一步是使內核協議棧支持網橋,第二步是安裝用戶空間工具,通過一系列的ioctl調用
來配置網橋。下面以一個相對簡單的實例來貫穿全文,以便分析代碼。
Linux機器有4個網卡,分別是eth0~eth4,其中eth0用於連接外網,而eth1, eth2, eth3都連接到一台PC機,用於配置網橋。只需要用下面的命令就可以完成網橋的配置:
Brctl addbr br0 (建立一個網橋br0, 同時在Linux內核里面創建虛擬網卡br0)
Brctl addif br0 eth1
Brctl addif br0 eth2
Brctl addif br0 eth3 (分別為網橋br0添加接口eth1, eth2和eth3)
其中br0作為一個網橋,同時也是虛擬的網絡設備,它即可以用作網橋的管理端口,也可作為網橋所連接局域網的網關,具體情況視你的需求而定。要使用br0接口時,必需為它分配IP地址。為正常工作,PC1, PC2,PC3和br0的IP地址分配在同一個網段。
4. 網橋數據結構
網橋最主要有三個數據結構:struct net_bridge,struct net_bridge_port,struct net_bridge_fdb_entry,他們之間的關系如下圖:
展開來如下圖:
說明:
a. 其中最左邊的net_device是一個代表網橋的虛擬設備結構,它關聯了一個net_bridge結構,這是網橋設備所特有的數據結構。
b. 在net_bridge結構中,port_list成員下掛一個鏈表,鏈表中的每一個節點(net_bridge_port結構)關聯到一個真實的網口設備的net_device。網口設備也通過其br_port指針做反向的關聯(那么顯然,一個網口最多只能同時被綁定到一個網橋)。
c. net_bridge結構中還維護了一個hash表,是用來處理地址學習的。當網橋准備轉發一個報文時,以報文的目的Mac地址為key,如果可以在 hash表中索引到一個net_bridge_fdb_entry結構,通過這個結構能找到一個網口設備的net_device,於是報文就應該從這個
網 口轉發出去;否則,報文將從所有網口轉發。
各個結構體具體內容如下:
網橋私有數據:net_bridge{}
虛擬的網橋本身對於Kernel也是一個網絡設備,自然擁有net_device{},而網橋操作相關的信息保存在net_bridge{}中。net_bridge{}作為(對dev而言)私有信息附屬在net_device{}之后。創建網橋類型設備的時候net_bridge{}作為附屬信息由alloc_netdev()一起分配。
1
2
3
4
5
6
7
8
9
|
struct
net_bridge
{
spinlock_t
lock
;
struct
list_head port_list;
// net_bridge_port{}鏈表
struct
net_device *dev;
// 指向網橋設備的net_device{}
struct
pcpu_sw_netstats __percpu *stats;
// 統計值,TX/Rx Packet Byte之類
spinlock_t hash_lock;
struct
hlist_head hash[BR_HASH_SIZE];
// 轉發數據庫(FDB)哈希表
|
其中端口設備由port_list連接,FDB是per-bridge的數據庫(而且per-vlan),而非Per-port的,故保存在br結構中。考慮到FDB條目數量會比較多,查詢頻繁,使用Hash表保存。
IGMP Snooping和Netfilter相關的不關注。
... Netfilter 相關... u16 group_fwd_mask; /* STP */ bridge_id designated_root; bridge_id bridge_id; u32 root_path_cost; unsigned long max_age; unsigned long hello_time; unsigned long forward_delay; unsigned long bridge_max_age; unsigned long ageing_time; unsigned long bridge_hello_time; unsigned long bridge_forward_delay; u8 group_addr[ETH_ALEN]; u16 root_port; enum { BR_NO_STP, /* no spanning tree */ BR_KERNEL_STP, /* old STP in kernel */ BR_USER_STP, /* new RSTP in userspace */ } stp_enabled; unsigned char topology_change; unsigned char topology_change_detected; ... IGMP Snooping ... struct timer_list hello_timer; struct timer_list tcn_timer; struct timer_list topology_change_timer; struct timer_list gc_timer;
指定端口、網橋ID,路徑成本,之類都能在STP協議中找到。我們從stp_enabled標識中看到STP(802.1D)的實現仍然放在Kernel中,而RSTP(Rapid STP)的實現被放在了UserSpace(Kernel以前也沒有RSTP的實現)。RSTP的實現可以在這里找到:
git://git.kernel.org/pub/scm/linux/kernel/git/shemminger/rstp.git。事實上把某些數據量不大但邏輯相對復雜的控制協議放到應用層的例子還是比較多的,例如IPv6的ND,DHCPv4/DHCPv6,以及未來某些nftables的某些部分。RSTP需要Kernel和Userspace“合作”完成。
struct kobject *ifobj; u32 auto_cnt; #ifdef CONFIG_BRIDGE_VLAN_FILTERING u8 vlan_enabled; __be16 vlan_proto; u16 default_pvid; struct net_port_vlans __rcu *vlan_info; // 網橋設備和網橋端口設備一樣,也可視為一個(對L3的)端口,也需要VLAN信息 #endif };
網橋端口:net_bridge_port{}
struct net_bridge_port {
首先是Layout信息,
1
2
3
|
struct
net_bridge *br;
// 所屬網橋(反向引用)
struct
net_device *dev;
// 網橋端口自己的net_device{}結構。
struct
list_head list;
// 同一個Bridge的各個Port組織在鏈表dev.port_list中。
|
STP相關信息。STP中定義了端口的優先級,STP的各個狀態(Disabled,Blocking,Learning,Forwarding)。還有“指定端口”,“根端口”,“指定網橋”的概念。同時還定義了幾個定時器。這里保存了這寫信息。這里不再復述STP。
/* STP */ u8 priority;// 端口優先級 u8 state; // 端口STP狀態:Disabled,Blocking,Learning,Forwarding u16 port_no; // 端口號,每個Bridge上各個端口的端口號不能改變(不能配置) unsigned char topology_change_ack;// TCA ? unsigned char config_pending; port_id port_id; // 端口ID:Prio+端口號 port_id designated_port; bridge_id designated_root; bridge_id designated_bridge; u32 path_cost; u32 designated_cost; unsigned long designated_age; struct timer_list forward_delay_timer;// 轉發延遲定時器,默認15s struct timer_list hold_timer; // 控制BPDU發送最大速率的定時器 struct timer_list message_age_timer;// BPDU老化定時器
Kernel通用信息
struct kobject kobj; // Kernel為了方便一些常用對象操作(添加刪除等)建立的基本對象 struct rcu_head rcu; unsigned long flags; // 是否處於Flooding,是否Learning,是否被管理員設置了cost等 ... IGMP Snooping & Netpoll ... struct net_port_vlans __rcu *vlan_info;// 在此端口上配置的VLAN信息,例如PVID,VLAN Bitmap, Tag/Untag Map };
網橋端口設備本身對應的net_device{}結構中有一些字段會指示此設備為網橋端口,原先是br_port(v2.6.11)指針,新版的內核則看priv_flag是否設置 IFF_BRIDGE_PORT。如果是網橋端口的話,rx_handler_data指向net_bridge_port{}。這么做的原因自然是盡量讓net_device不要放入功能特定的字段。
struct net_device { ... ... // 如果是網橋端口IFF_BRIDGE_PORT會被設置。 unsigned int priv_flags; /* Like 'flags' but invisible to userspace. * See if.h for definitions. */ ... ... rx_handler_func_t __rcu *rx_handler;// 創建網橋設備的時候注冊為br_handle_frame() void __rcu *rx_handler_data; // 如果是網橋端口,指向net_bridge_port{} ... ... };
rx_handler是各個per-net_device的入口幀特殊處理的hook點,dev向協議棧(L3)遞交skb過程,即netif_receive_skb()的處理過程中,在查詢ptype_base完成L2/L3遞交前,先檢查各個net_device的rx_handler是不是被設置,設置的話會先調用rx_handler。而網橋端口設備的rx_handler是被設置的。
這個是虛擬網橋如何通過端口設備收包的方式。
轉發數據庫條目:net_bridge_fdb_entry{}
Bridge維護一個轉發數據庫(Forwarding Data Base),包含了端口號,在此端口上學習到的MAC地址等信息,用於數據轉發(Forwarding)。整個數據庫使用Hash表組織,便於快速查找。每個網橋設備的FDB保存在其 net_bridge->hash中。每個學到(或靜態配置)的MAC由一個數據庫的條目,即
net_bridge_fdb_entry{}結構表示。FDB是per-vlan的,因為不同的VLAN的數據轉發路徑(可由STP生成)可能是不一樣的,FDB需要記錄VLAN信息。is_local表示MAC地址來自本地某個端口,is_static表示MAC地址是靜態的(由用戶配置或來自本地端口),這些地址不會老化。且所有本地的
MAC(is_local為1)的MAC總是“靜態的”。
struct net_bridge_fdb_entry { struct hlist_node hlist; // 哈希表沖突鏈表節點,頭是&net_bridge.hash[i] struct net_bridge_port *dst; // 條目對應的網橋端口 unsigned long updated; unsigned long used; // 引用計數 mac_addr addr; __u16 vlan_id; // MAC屬於哪個VLAN unsigned char is_local:1, // 是否是來自某個本地端口的MAC,本地端口總是is_static is_static:1, // 是否是靜態配置的MAC added_by_user:1, // 用戶配置 added_by_external_learn:1; // 外部學習 struct rcu_head rcu; };
5. 網橋初始化
5.1 bridge init
橋接部分初始化和退出的代碼定義在net/bridge/br.c中,這還有一些事件處理函數。Bridging作為一個內核模塊進行初始化。
module_init(br_init) static int __init br_init(void) { ... ... err = stp_proto_register(&br_stp_proto); ... ... err = br_fdb_init(); ... ... err = register_pernet_subsys(&br_net_ops); ... ... err = br_netfilter_init(); ... ... err = register_netdevice_notifier(&br_device_notifier); ... ... err = br_netlink_init(); ... ... brioctl_set(br_ioctl_deviceless_stub); ... ATM 相關 ... return 0; ... 出錯處理 ... }
br_init()函數完成的工作有:
-
注冊STP協議處理函數br_stp_rcv:在net/802/stp.c中實現了個通用的STP框架,這個框架又是建立在llc之上(net/llc/),LLC顯然是用來處理802.2 LLC層的,我們知道Ethernet II Packet常用於數據傳輸(尤其是PC端)而802.3 with 802.2 LLC協議通常用來承載STP等控制協議。LLC本身的處理和其他Ethernet PacketType(ARP, IP, IPv6..)沒有不同,都是通過dev_add_pack()向netdev的ptype_base注冊rcv函數。
netif_receive_skb + |- llc_rcv <= ptype_base[ETH_P_802_2] + |- br_stp_rcv <= llc_sap->rcv_func
-
轉發數據庫初始化為了效率的考慮net_bridge_fdb_entry{}的分配會在kernel cache中進行。這里使用kmem_cache_create()初始化一個br_fdb_cache。另外,之前提到FDB Etnry保存在net_bridge.hash,為了防止DoS攻擊,計算Hash的時候引入一個隨機因子讓其計算不可預測。該因子也在此處初始化。
-
注冊pernet_operationspernet_operation只注冊了.exit函數,作用是在某個網絡實例清理的時候,將所有"net"內的的bridge設備、相關Port結構、VLAN結構、Timer和FDB等清理干凈。
-
初始化橋接Netfilter:略。
-
注冊通告鏈netdev_chain網橋設備是建立其他網絡設備之上的,那些設備的狀態(UP/DOWN),地址改變等消息會影響網橋設備(內部數據結構,如端口表,FBD等)。因此需要關注netdev_chain。對這些Event的處理由br_device_event()完成。
-
netlink操作初始化Bridging注冊了兩組Netlink的Operations,分別是AF(AF_BRIDGE)和Link級別的ops。
5.2 bridge create
一般創建一個新的網絡設備分成2個基本步驟:
- 分配net_device{}並setup
也就是調用alloc_netdev_mqs(SIZE, NAME, xxx_setup)。其中 SIZE 是附着在net_device{}內存后面的特定數據,對於網橋設備而言就是net_bridge{}的大小。xxx_setup則是特有設備的初始化過程。NAME作為創建接口名的模板,如"eth%d"、"br%d"等,稍后由
register_netdevice()生成eth1, br0等設備名,也可直接指定。alloc_netdev()是alloc_netdev_mqs的wrapper,創建TX/RX隊列各一個。分配時注冊的xxx_setup會在alloc_netdev_mqs中被立即調用,用來初始化設備特定數據,我們之前見過ether_setup。 網橋對應的setup函數為br_dev_setup()。
和ether_setup簡單設置一些ethernet參數不同,br_dev_setup完成了許多對網橋設備至關重要的工作,例如為設備指定netdev_ops(即"dev->ndo_xxx",用於后續的open/close/xmit)等。稍后會詳細介紹。
- 注冊網絡設備
函數register_netdevice()生成dev->name、dev->ifindex, 調用dev.netdev_ops.ndo_init()初始化設備,初始化輸入輸出隊列,將設備添加到全局(net{})設備列表,一個name為key的Hash net.dev_name_head,一個ifindex為key的Hash net.dev_index_head和,全局鏈表
net.dev_base_head。
而創建網橋設備同樣遵循上面的步驟
在網橋的初始化函數中,注冊了網橋操作的ioctl 函數br_ioctl_deviceless_stub ,當添加網橋的時候,通過該函數調用br_add_bridge來實現網橋的創建 。
br_dev_setup()函數
不論使用netlink還是傳統的ioctl都會調用alloc_netdev_mqs,后者會調用setup函數br_dev_setup。它的實現在net/bridge/br_device.c中。
void br_dev_setup(struct net_device *dev) { struct net_bridge *br = netdev_priv(dev); eth_hw_addr_random(dev); //生成一個隨機的MAC地址 ether_setup(dev);// 虛擬的Bridge是Ethernet類型,進行ethernet初始化(type, MTU,broadcast等)。 dev->netdev_ops = &br_netdev_ops; // 網橋設備的netdev_ops dev->destructor = br_dev_free; dev->ethtool_ops = &br_ethtool_ops; SET_NETDEV_DEVTYPE(dev, &br_type);// br_type.name = "bridge" dev->tx_queue_len = 0; dev->priv_flags = IFF_EBRIDGE;// 標識此設備為Bridge dev->features = COMMON_FEATURES | NETIF_F_LLTX | NETIF_F_NETNS_LOCAL | NETIF_F_HW_VLAN_CTAG_TX | NETIF_F_HW_VLAN_STAG_TX; dev->hw_features = COMMON_FEATURES | NETIF_F_HW_VLAN_CTAG_TX | NETIF_F_HW_VLAN_STAG_TX; dev->vlan_features = COMMON_FEATURES; br->dev = dev; spin_lock_init(&br->lock); INIT_LIST_HEAD(&br->port_list);//初始化網橋端口鏈表和鎖 spin_lock_init(&br->hash_lock); br->bridge_id.prio[0] = 0x80; // 默認優先級 br->bridge_id.prio[1] = 0x00; // STP相關初始化 ether_addr_copy(br->group_addr, eth_reserved_addr_base);// 802.1D(STP)組播01:80:C2:00:00:00 br->stp_enabled = BR_NO_STP;// 默認沒有打開STP,不阻塞任何組播包。 br->group_fwd_mask = BR_GROUPFWD_DEFAULT; br->group_fwd_mask_required = BR_GROUPFWD_DEFAULT; br->designated_root = br->bridge_id; br->bridge_max_age = br->max_age = 20 * HZ; // 20sec BPDU老化時間 br->bridge_hello_time = br->hello_time = 2 * HZ;// 2sec HELLO定時器時間 br->bridge_forward_delay = br->forward_delay = 15 * HZ;// 15sec 轉發延時(用於Block->Learning->Forwardnig) br->ageing_time = 300 * HZ;// FDB 中保存的MAC地址的老化時間(5分鍾) br_netfilter_rtable_init(br); // Netfilter (ebtables) br_stp_timer_init(br); br_multicast_init(br);// 多播轉發相關初始化 }
先為網橋設備生成一個隨機的MAC地址,當bridge的第一個接口被binding的時候,bridge的MAC字段自動轉為第一個接口的地址。虛擬網橋設備上ethernet類型,因此會調用ether_setup()。
每個net_device有一組netdev_ops用來處理設備打開、關閉,傳輸等,Bridge的net_device_ops內容則更豐富一些,需要ndo_add_save, ndo_fdb_add稍后詳細介紹。ethtool可用來查看鏈接是否UP,以及設備的信息(驅動類型,版本,固件版本,總線等)。
開始的時候網橋總是認為自己是根網橋,所有designeated_root設置成自己網橋ID。而一些STP的定時器也需要設置成默認值。有些定時器是雙份的,原因是STP的Timer是由Root Bridge通告,而不是使用自己的值。但是自己也可能會成為Root,所以要維護一份自己的定時器值。
5.3 bridge port create
和創建網橋設備一樣,為網橋設備添加端口設備,也可以使用ioctl和netlink兩種方式。兩種方式最終會調用br_add_if()。
br_add_if()函數
int br_add_if(struct net_bridge *br, struct net_device *dev)
端口資格檢查,有幾類設備不能作為網橋端口:
- loopback設備
- 非Ethernet設備
- 網橋設備,即不支持“網橋的網橋”
- 本身是另一個網橋設備端口。每個設備只能有一個Master,否則數據去哪里呢
- 配置為IFF_DONT_BRIDGE的設備
/* Don't allow bridging non-ethernet like devices */ if ((dev->flags & IFF_LOOPBACK) || dev->type != ARPHRD_ETHER || dev->addr_len != ETH_ALEN || !is_valid_ether_addr(dev->dev_addr)) return -EINVAL; /* No bridging of bridges */ if (dev->netdev_ops->ndo_start_xmit == br_dev_xmit) return -ELOOP; /* Device is already being bridged */ if (br_port_exists(dev)) return -EBUSY; /* No bridging devices that dislike that (e.g. wireless) */ if (dev->priv_flags & IFF_DONT_BRIDGE) return -EOPNOTSUPP;
如果新的端口設備沒有問題,就可以進行分配和初始化net_bridge_port{},這些工作由new_nbp()完成。
- 分配一個net_bridge_port{}結構;
- 分配端口ID。
- 初始化端口成本(協議規定萬兆、千兆,百兆和十兆的默認成本為2, 4, 19和100),
- 設置端口默認優先級,
- 初始化端口角色(dp)狀態(blocking)。
- 啟動STP定時器等。
網橋設備需要接收所有的組播包,原來此處調用的是 dev_set_promiscuity(dev, 1)讓網橋端口(可能是實際設備)工作在混雜模式,這樣才能接收目的MAC非此設備的Unicast以及(未join的)所有的Multicast。
p = new_nbp(br, dev); if (IS_ERR(p)) return PTR_ERR(p); call_netdevice_notifiers(NETDEV_JOIN, dev); err = dev_set_allmulti(dev, 1); if (err) goto put_back;
sysfs和kobj
Kernel為所有的網橋端口建立一個kobj,這樣一來可以方便的使用sysfs_ops設置sysfs參數,以及其他對象操作(例如刪除對象的時候,release_nbp被調用以刪除net_bridge_port結構。通過,kobject_init_and_add/br_sysfs_addif實現p->kobj的初始化和注冊等。一旦注冊,就可以
在/sys/class/net//brif//找到它相應的目錄。
err = kobject_init_and_add(&p->kobj, &brport_ktype, &(dev->dev.kobj), SYSFS_BRIDGE_PORT_ATTR); if (err) goto err1; err = br_sysfs_addif(p); if (err) goto err2;
既然是網橋端口那么dev->priv_flags被設置上IFF_BRIDGE_PORT。同時網橋端口不支持LRO,原因是LRO(Large Receive Offload)適用於目的為Host的Packet,而網橋端口可能會轉發數據到其他端口,自然就不能啟用這個功能(啟用了還會影響GSO)。
dev->priv_flags |= IFF_BRIDGE_PORT; dev_disable_lro(dev);
添加端口設備到網橋設備端口列表新建完一個新的端口設備,該初始化的也初始化了,現在可以加入到網橋中了。
list_add_rcu(&p->list, &br->port_list);
更新FDB,初始化VLAN
網橋設備端口的MAC需要“靜態”配置到FDB中,is_local和is_static同時置1。這回答了網橋端口是否有MAC地址的問題
if (br_fdb_insert(br, p, dev->dev_addr, 0)) netdev_err(dev, "failed insert local address bridge forwarding table\n");
初始化網橋端口的VLAN配置,如果Bridge設備有“Default PVID",就將默認PVID設置為端口的PVID並且Untag。
if (nbp_vlan_init(p)) netdev_err(dev, "failed to initialize vlan filtering on this port\n");
重新計算網橋MAC,Bridge ID
當一個網橋設備(不是端口設備)剛剛創建的時候,其MAC地址是隨機的(見 br_dev_setup,舊實現是空MAC),這也會影響網橋ID(Prio+MAC),沒有端口時網橋ID的MAC部分為0。當有個設備作為其端口后,是個合適的機會重新為網橋選一個MAC,並重新計算網橋ID。前提是如果這個端口的
MAC合適的話,例如不是0,長度是48Bits,並且值比原來的小(STP中ID小好事,因為其他因素一樣的情況下MAC愈小ID愈小,優先級就越高),就用這個端口的MAC。
changed_addr = br_stp_recalculate_bridge_id(br); ... ... if (changed_addr) call_netdevice_notifiers(NETDEV_CHANGEADDR, br->dev);
設置設備狀態,MTU
如果網橋端口設備是UP的,就使能它,設置狀態等(如果STP沒打開就沒有這些步驟了)。
- 狀態設置為Blocking,
- 認為自己是Designated Port(暫時)
- 對所有端口重新進行端口角色選擇
- 創建端口ID 這些通過br_stp_enable_port完成,
if (netif_running(dev) && netif_oper_up(dev) && (br->dev->flags & IFF_UP)) br_stp_enable_port(p);
接下來為新的端口設置MTU,將它設置為整個Bridge設備各個端口的最小MTU;將新端口的MAC地址記錄到bridge的FDB中(per VLAN)。通過函數br_fdb_insert插入的fdb表項的is_local和is_static都是1(本地端口嘛)。
dev_set_mtu(br->dev, br_min_mtu(br)); kobject_uevent(&p->kobj, KOBJ_ADD); return 0; ... 出錯處理,各種rollback ... }
br_del_if基本上是br_add_if的逆過程,就不再細說了。注意一下一個端口從Bridge移走的話Bridge的ID也需要重新計算。
5.4 打開關閉網橋
現在已經知道創建、刪除網橋設備以及添加、刪除網橋端口時內核都發生了什么。接下來再看看打開關閉網橋設備(例如ifconfig xxx up或ip link set up)時都有哪些動作發生。網橋設備也是網絡設備,也有dev->ndo_open/close,所以不管是ioctl(brctl)還是netlink(ip),最終被調用的是之前在
br_netdev_ops里面所注冊的br_dev_open和br_dev_close。其實Bridge的net_device_ops很多函數都已經看過了。
static const struct net_device_ops br_netdev_ops = { .ndo_open = br_dev_open, // 本節講這個 .ndo_stop = br_dev_stop, // 本節講這個 .ndo_init = br_dev_init, // 本節講這個 .ndo_start_xmit = br_dev_xmit, // 數據傳輸 .ndo_get_stats64 = br_get_stats64, // 統計,好理解 .ndo_set_mac_address = br_set_mac_address,// 這個好理解 .ndo_set_rx_mode = br_dev_set_multicast_list, .ndo_change_mtu = br_change_mtu, .ndo_do_ioctl = br_dev_ioctl, // 已經提過了 ... netpoll 相關... .ndo_add_slave = br_add_slave, // 已經提過了 .ndo_del_slave = br_del_slave, // 已經提過了 .ndo_fix_features = br_fix_features, // 已經提過了,見br_add_if .ndo_fdb_add = br_fdb_add, .ndo_fdb_del = br_fdb_delete, .ndo_fdb_dump = br_fdb_dump, .ndo_bridge_getlink = br_getlink, .ndo_bridge_setlink = br_setlink, .ndo_bridge_dellink = br_dellink, };
br_dev_open自然是用戶“up”了這個設備后被調用的。netdev_update_features之前遇到過。netif_start_queue打開輸出隊列,這個和普通設備沒有區別(具體參考《UNLI》)。然后是Multicast和STP部分,這就不細說了。br_dev_close是br_dev_open的反過程。
static int br_dev_open(struct net_device *dev) { struct net_bridge *br = netdev_priv(dev); netdev_update_features(dev); netif_start_queue(dev); br_stp_enable_bridge(br); br_multicast_open(br); return 0; }
6. 網橋數據轉發
6.1 網橋數據包入口
網橋是一種2層網絡互連設備,而不是一種網絡協議。它在協議結構上並沒有占有一席之地,因此不能通過向協議棧注冊協議的方式來申請網橋數據包的處理。相 反,網橋接口(如上述的eth1)的數據包和一般接口(如eth0)在格式上完全是一樣的,不同之處是網橋在2層上就對它進行了轉了,而一
般接口要在3層 才能根據路由信息來決定是否要轉發,如何轉發。那么一個網絡接口,在驅動處理完數據包后,怎么才知道該接口分配在一個網橋里面呢?其實很簡單,當 brctl工具通過ioctl系統調用時,kernel為該添加的設備生成一個bridge_port結構並放到port_list鏈中,同時將該 bridge_port的值賦
予設備net_device的br_port指針。因此,要識別接口是否屬於某個網橋,只需判斷net_device的 br_port指針是否不為空即可。
現假設PC1向PC2發送其個數據包,數據首先會由eth1網卡接收,此后網卡向CPU發送接收中斷。當CPU執行當前指令后(如果開中斷的話),馬上跳 到網卡的驅動程去。Eth1的網卡驅動首先生成一個skb結構,然后對以太網層進行分析,最后驅動將該skb結構放到當前CPU的輸入隊列中,喚醒軟中
斷。如果沒有其它中斷的到來,那么軟中斷將調用netif_receive_skb函數。代碼和分析如下所述:
int netif_receive_skb(struct sk_buff *skb) { //當網絡設備收到網絡數據包時,最終會在軟件中斷環境里調用此函數 //檢查該數據包是否有packet socket來接收該包,如果有則往該socket //拷貝一份,由deliver_skb來完成。 list_for_each_entry_rcu(ptype, &ptype_all, list) { if (!ptype->dev || ptype->dev == skb->dev) { if (pt_prev) ret = deliver_skb(skb, pt_prev, orig_dev); pt_prev = ptype; } } // 先試着將該數據包讓網橋函數來處理,如果該數據包的入口接口確實是網橋接口, // 則按網橋方式來處理,並且handle_bridge返回NULL,表示網橋已處理了。 // 如果不是網橋接口的數據包,則不應該讓網橋來處理,handle_bridge返回skb, // 后面代碼會讓協議棧來處理上層協議。 skb = handle_bridge(skb, &pt_prev, &ret, orig_dev); if (!skb) goto out; skb = handle_macvlan(skb, &pt_prev, &ret, orig_dev); if (!skb) goto out; //對該數據包轉達到它L3協議的處理函數 type = skb->protocol; list_for_each_entry_rcu(ptype, &ptype_base[ntohs(type)&15], list) { if (ptype->type == type && (!ptype->dev || ptype->dev == skb->dev)) { if (pt_prev) ret = deliver_skb(skb, pt_prev, orig_dev); pt_prev = ptype; } } }
6.2 網橋處理邏輯
static inline struct sk_buff *handle_bridge(struct sk_buff *skb, struct packet_type **pt_prev, int *ret, struct net_device *orig_dev) { struct net_bridge_port *port; //如果該數據包產生於本機,而目標同時為本機。 if (skb->pkt_type == PACKET_LOOPBACK || //如果該數據包的輸入接口不是網橋接口 (port = rcu_dereference(skb->dev->br_port)) == NULL) // 以上兩種情況都需要讓上層協議進行處理 return skb; if (*pt_prev) { *ret = deliver_skb(skb, *pt_prev, orig_dev); *pt_prev = NULL; } //數據包的入口接口是網橋接口。下面將按網橋邏輯進行處理。 //如假包換,數據包轉達到真正的網橋處理函數 //br_handle_frame_hook在網橋模塊的init函數被初始化為 //br_handle_frame return br_handle_frame_hook(port, skb); }
這里回調了br_handle_frame_hook()函數,這個是一個鈎子函數。Br_handle_frame_hook()函數在Linux2.6.34\net\bridge\Br_input.c中,br_handle_frame_hook=br_handle_frame,所以實際函數為br_handle_frame.
6.3 br_handle_frame
好,現在我們看看bridge端口的處理函數 br_handle_frame如何處理skb和指示后續操作。該函數位於br_input.c中。
if (!is_valid_ether_addr(eth_hdr(skb)->h_source)) goto drop;
網橋端口不打算處理回環數據;源地址必須為合法Ethernet地址:源MAC地址不能是全0,不能是MAC廣播和多播,是的話就丟棄。
skb = skb_share_check(skb, GFP_ATOMIC); if (!skb) return RX_HANDLER_CONSUMED;
如果skb是共享的,考慮的網橋端口會修改skb,將它clone一份。
接下來數據被分為兩類:目的地址是Link Local MAC層多播的數據包括了STP的BPDU,和普通數據。
STP幀(BPDU)和其他保留多播幀
首先是Link Local MAC多播的處理。802.1D有組保留的 Link Local 多播MAC地址,他們用於控制協議,如STP。如果接收到了STP但網橋沒有開STP協議,就視為普通數據處理;換句話說,就是本網橋當作自己是不認識STP的網橋,例如Hub或不支持STP的Switch。這時需要Flood STP報文到其他端
口,而保證那些支持STP網橋則看不到不支持STP設備的存在。對於其他Kernel不支持的管理幀處理方式類似。
最后能夠在此函數直接遞交到Local Host的只能STP功能打開情況下收到的STP幀。遞交的時候經過Netfilter的NF_BR_LOCAL_IN的 HOOK點,然后是br_handle_local_finish。br_handle_local_finish的處理實際上不如說是“不處理”,它只是在端口處於Learning的情況下學習個skb的源MAC,並且
總是返回0指示包 RX_HANDLER_PASS,由netif_receive_skb繼續根據ptype_base處理(STP報文)。
所有這段代碼對於STP的處理也只是學了個源MAC,然后繼續有netif_receive_sbk處理。並沒有處理STP幀(BPDU).
if (unlikely(is_link_local_ether_addr(dest))) { // MAC Link Local地址通常是管理幀 ... ... switch (dest[5]) { case 0x00: /* Bridge Group Address */// 看看STP要怎么弄法,如果真要處理的話不是在這,而是稍后的protocol dipatching(ptype_base)的地方 /* If STP is turned off, then must forward to keep loop detection */ if (p->br->stp_enabled == BR_NO_STP) goto forward;// 沒開STP,那STP幀就和普通數據幀一樣處理 break; case 0x01: /* IEEE MAC (Pause) */ goto drop; // MAC Control幀不能通過網橋 default:// 其他的保留MAC多播和普通數據幀一樣處理 /* Allow selective forwarding for most other protocols */ if (p->br->group_fwd_mask & (1u << dest[5])) goto forward; } //如果能到達這,只有一種情況:STP功能打開的情況下,收到了STP幀 /* Deliver packet to local host only */ if (NF_HOOK(NFPROTO_BRIDGE, NF_BR_LOCAL_IN, skb, skb->dev, NULL, br_handle_local_finish)) { // br_hanle_local_finishq其實只是在Learning狀態下學習MAC並返回0 return RX_HANDLER_CONSUMED; /* consumed by filter */ } else { // 通常,NF_HOOK(br_handle_local_finish)返回0,於是STB BPDU到此處“pass”,最后由netif_receive_skb根據ptype_base分發到STP協議層。 *pskb = skb; return RX_HANDLER_PASS; /* continue processing */ } }
記住,這個函數不會進行STP BPDU的處理!
普通數據幀
走到這里的幀要么是普通數據幀,要么是被視為普通數據的控制幀。它們的處理都是一樣的,就是當作普通數據處理。 普通數據幀(非STP幀BPDU), 沒有打開STP功能情況下的STP幀,那么就和普通幀一樣處理 要么就是其他的保留多播(非MAC Control),那么就和普通幀一樣處理
如果目的MAC和網橋設備(而不是網橋端口)的MAC相同,標記為
PACKET_HOST skb->pkt_type = PACKET_HOST;
br_handle_frame分析完了,我們留下兩個問題, STP為什么在該函數中沒有被處理,並且還去向了ptype_base的流程。 br_handle_frame_finish是做什么的 第一個問題其實好理解,STP作為一種特殊類型的Ethernet packet type,注冊了自己的packet_type{}。在br_handler_frame的STP處理只是
分流一下不該處理的情況(netif_receive_skb的流程做不到這種分流)。正經的STP處理的方法是在稍后查詢ptype_base,找到相應的處理函數。
// net/llc/llc_core.c static struct packet_type llc_packet_type __read_mostly = { .type = cpu_to_be16(ETH_P_802_2), .func = llc_rcv, };
反觀普通數據流量,普通NIC收到這些數據時應遞交到協議棧,即查詢ptype_base然后遞交。但設備一旦作為網橋端口,就不能這么處理了,可能需要轉發的其他端口什么的,所以才要走br_handler_frame及后續函數。我們看看第二個問題,br_handle_frame_finish接下來是怎么處理普通數據流量
(或當作普通數據處理的保留多播流量)的 。
6.4 數據幀處理:br_handle_frame_finish
接着br_handle_frame討論數據幀的處理,這里的數據幀代表非(STP等)控制幀,當然也包括“視為數據幀”的控制幀(例如STP功能關閉的情況下,BPDU就視為普通數據幀處理)。后面就不再羅嗦了,統一稱為“數據幀”或“數據流量”。
如果數據來自 br_handle_frame,那么 br_handle_frame_finish被調用的時候端口只能處於兩種狀態:Learning和Frowarding。STP端口如果處於Learning和Forwarding狀態,就需要學習新的源MAC(更新FDB)
br_fdb_update(br, p, eth_hdr(skb)->h_source);
如果端口是Learning就說明不是Forwarding,學個MAC就行了,不能繼續接收數據。
if (p->state == BR_STATE_LEARNING) goto drop;
接下來是鏈路層廣播、多播和單播的處理,這段代碼出現兩個skb指針:skb2和原來的skb。理解這段代碼,只需要時刻明白,skb2代表遞交本地host, skb代表需要轉發。抓住這個關鍵即可。
// skb2指針表明,有數據要發往本機的網絡接口,即p->br->dev接口。 58 skb2 = NULL; 59 60 // 如果應用程序要dump本機接口的數據,那么該數據包應往主機發一份, 61 // 一個明顯的例子就是在用戶在運行tcpdump –I br0或類似的程序。 62 if (br->dev->flags & IFF_PROMISC) 63 skb2 = skb; 64 65 dst = NULL; 66 67 if (is_multicast_ether_addr(dest)) { 68 //如果該報文是一個L2多播報文(如arp請求),那么它應該轉發到 69 //該網橋的所有接口。 70 //這同樣是網橋的一個特點,廣播和組播報文要轉發到它的所有接口。 71 br->dev->stats.multicast++; 72 skb2 = skb; 73 } else if ((dst = __br_fdb_get(br, dest)) && dst->is_local) { 74 //__br_fdb_get函數先查MAC-端口映射表,這一步是網橋的關鍵。 75 // 這個報文應從哪個接口轉發出去就看它了。 76 //如果這個報文應發往本機,那么skb置空。不需要再轉發了, 77 skb2 = skb; 78 /* Do not forward the packet since it's local. */ 79 skb = NULL; 80 }
決定完是不是要轉發,是不是要遞交到Host,就可以正在的干活了。如果需要轉發(skb不為NULL),又在FBI中找到了目的端口,就轉發到改端口。否則就flooding。如果需要遞交,就調用br_pass_frame_up。
if (skb) { if (dst) { dst->used = jiffies; br_forward(dst->dst, skb, skb2);// 數據轉發到FDB查詢到的端口 } else br_flood_forward(br, skb, skb2, unicast);// 數據Flood到所有端口 } if (skb2) return br_pass_frame_up(skb2);// 數據遞交到本地Host ... ...
順便提一下,目前為止skb->dev還么有改變,因為不能確定要交換的skb->dev是哪個,如果是本地遞交,就會被替換成網橋設備,如果是轉發或者flooding則需要換成對應端口設備,而且skb可能還需要再clone。
6.5 本地遞交:br_pass_frame_up
進入br_pass_frame_up的skb是打算經由Bridge設備,輸入到本地Host的。數據包從網橋端口設備進入,經過網橋設備,然后再進入協議棧,其實是“兩次經過net_device”,一次是端口設備,另一次是網橋設備。現在數據包離開網橋端口進入網橋設備,需要修改skb->dev字段。
indev = skb->dev; skb->dev = brdev;
skb->dev 起初是網橋端口設備,現在離開網橋端口進入網橋的時候,被替換為網橋設備的net_device。如果設備是TX,或者從一個端口轉發的另一個skb->dev也會相應改變。不論數據的流向如何,skb->dev總是指向目前所在的net_device{}。
遞交的最后一步是經過NF_BR_LOCAL_IN鈎子點,然后是我們熟悉的netif_receive_skb,只不過這次進入該函數的時候skb->dev已經被換成了Bridge設備。這可以理解為進入了Bridge設備的處理。
return NF_HOOK(NFPROTO_BRIDGE, NF_BR_LOCAL_IN, skb, indev, NULL, netif_receive_skb);
Bridge Local In的數據被修改skb->dev后再次進入netif_receive_skb,原來那個netif_receive_skb因為rx_handler返回CONSUMED而結束。
6.6 數據轉發到端口:br_forward
我們再看看 br_handle_frame_finish的另一個支流,轉發支流,首先是轉發到單個端口的情況,出現這種精確的轉發,意味着FDB里面有目的MAC對應的條目,找到了目的端口。直接轉發的某個端口通過函數br_forward。
轉發前需要做幾個檢查,必須同時滿足以下條件:
a.不能轉發給自己 (ingress/egress端口 不能相同)除非目的端口設置了HAIRPIN模式。
b.如果出口端口的狀態不是Forwarding,則不能轉發出去。如果一個網橋沒有啟用STP功能,並且網絡接口的狀態為UP,那么它網橋端口的狀態為Forwarding。如果啟用STP,每個端口都有一個嚴格的狀態,規定那些端口在什么情況下才能成為Forwarding狀態,否則容易造成環路,產生網絡風暴。
以上檢查由should_deliver()完成。
接着調用函數_br_forward(),_br_forward函數也沒干什么,就是調用了br_forward_finish()函數。Br_forward_finish()函數調用了br_dev_queue_push_xmit()函數。
34 int br_dev_queue_push_xmit(struct sk_buff *skb) 35 { 36 /* drop mtu oversized packets except gso */ 37 if (packet_length(skb) > skb->dev->mtu && !skb_is_gso(skb)) 38 kfree_skb(skb); 39 else { 40 /* ip_refrag calls ip_fragment, doesn't copy the MAC header. */ 41 if (nf_bridge_maybe_copy_header(skb)) 42 kfree_skb(skb); 43 else { 44 skb_push(skb, ETH_HLEN); 45 46 dev_queue_xmit(skb); 47 } 48 } 49 50 return 0; 51 }
該函數主要完成如下工作:
1.做些必要的檢查工作。例如,報文的長度比出口端口的MTU還大,則丟掉該報文。
2. 網橋在處理數據包里,只需拆包來獲得目標MAC地址,而不需要 更改數據包的任何內容。但在入口網卡的驅動中已將以太網頭部 剝掉,現在需要將它套上。Skb_push函數實現這一功能。
3. 放到網卡輸出隊列里,該網卡驅動將它送出去。
6.7 Flooding到各個端口:br_flood_forwards
br_flood_forwards只是函數br_flood的包裹函數。br_flood()遍歷每個網橋端口,如果可以的話(滿足剛剛說過的should_deliver的要求),就用__packet_hook( __br_forward())轉發之。不過函數實現的時候用了一個小技巧,判斷為能不能轉發后先不急着轉發,而是看看下一個端口,如果
下一個端口也需要轉發,才把數據轉發到上次那個要轉發到端口。這么做的原因也是減少一次clone。如果沒有后續可以轉發的端口,就不需要clone了。
111 struct net_bridge_port *p; 112 struct net_bridge_port *prev; 113 114 prev = NULL; 115 116 list_for_each_entry_rcu(p, &br->port_list, list) { 117 if (should_deliver(p, skb)) { 118 if (prev != NULL) { 119 struct sk_buff *skb2; 120 121 if ((skb2 = skb_clone(skb, GFP_ATOMIC)) == NULL) { 122 br->dev->stats.tx_dropped++; 123 kfree_skb(skb); 124 return; 125 } 126 127 __packet_hook(prev, skb2); 128 } 129 130 prev = p; 131 } 132 } 133 134 if (prev != NULL) { 135 __packet_hook(prev, skb); 136 return; 137 }
此外br_flood也會使用__br_forward最終轉發數據幀,和br_forward一樣。
6.8 網橋數據流小節
兩次經過net_device{}小節
再談談skb經過兩次net_device{}這事。 輸入路徑經過兩次net_device{}分別是網橋端口的和網橋設備的,也就是兩次調用netif_receive_skb。 和輸入路徑一樣,輸出的幀同樣會經過兩次net_device,即先網橋設備后網橋端口,對輸出而言的函數是兩次調用dev_queue_xmit; 如果將這個概
念擴展,其實對於轉發(forward)的數據幀也是兩次經過net_device,兩次都是網橋端口的net_device{},函數的話,一次是netif_receive_skb,一次是dev_queue_xmit)。
7. 轉發數據庫
轉發數據庫用於記錄MAC地址端口映射。網橋通過地址學習,將學習到的MAC地址和相應端口加入該數據庫;網橋端口本身的MAC會被永久的加入到FDB中(br_add_if());用戶還可以配置靜態的映射。FDB和是否打開STP無關,只不過打開STP后,只有Learning/Forwardnig才會學習。
記錄下的MAC地址(數據庫條目)會被更新,並且有老化時間(默認是300秒,也就是5min),如果使用舊STP算法,拓撲變化的時候該老化時間被設置成15秒,如果使用RSTP,FDB中,某端口相關所有條目會被清除。雖然之前已經介紹過net_device_fdb_entry{},我們還是羅列一下.
struct net_bridge_fdb_entry {
struct hlist_node hlist; // FDB的各個Entry使用哈希表組織,這個是bucket沖突元素鏈表節點 ;
struct net_bridge_port *dst; // 條目對應的網橋端口(沒有直接使用端口ID);
struct rcu_head rcu; unsigned long updated; // 最后一次更新的時間,會與Bridge的老化定時器比較。 unsigned long used; mac_addr addr; // 條目對應的MAC地址 unsigned char is_local; // 是否是本地端口的MAC unsigned char is_static; // 是否是靜態配置的MAC __u16
這里重申一下FDB是網橋的屬性,因此保存在net_bridge{}中,保存的方式是一個Hash表。
struct net_bridge { ... ... struct hlist_head hash[BR_HASH_SIZE]; ... ... };
FDB條目的添加、刪除,查詢,更新操作本身想必不會太復雜,無非是哈希表鏈表操作。關鍵是搞弄清楚FDB訪問和修改的場景。
7.1 FDB初始化,查找
FDB的初始化非常簡單,為net_bridge_fdb_entry{}結構初始化一個cache以便快速分配條目。另外還以隨機值生成一個salt,這個salt在hash的時候使用,引入隨機值可以分散各個Hash鍵,並且防止DoS攻擊。
33 int __init br_fdb_init(void) 34 { 35 br_fdb_cache = kmem_cache_create("bridge_fdb_cache", 36 sizeof(struct net_bridge_fdb_entry), 37 0, 38 SLAB_HWCACHE_ALIGN, NULL); 39 if (!br_fdb_cache) 40 return -ENOMEM; 41 42 get_random_bytes(&fdb_salt, sizeof(fdb_salt)); 43 return 0; 44 }
7.2 地址老化
我們知道網橋學到地址都有一個老化的過程。網橋維護了幾個超期時間值,包括老化時間br->ageing_time,默認300秒;和轉發延遲br->foward_delay,默認15秒。FDB中的每個地址如果自上次跟新(記錄於net_bridge_fdb_entry->updated)以來,流逝的時間超過了“保持時間”(由
hold_time(),返回可能是老化時間或者短老化時間),地址就需要被刪除。hold_time()在正常情況下返回老化時間br->ageing_time,但是如果檢測到了拓撲變化,這將老化時間縮短為br->forward_delay,后者也稱為“短老化定時器(short aging timer)”。
7.2.1 注冊、打開垃圾收集定時器
網橋在什么時候檢查FDB中的各個地址是否老化、並將老化的地址從FDB中移除呢?Kernel將這個工作交由“垃圾收集定時器”來完成。gc_timer保存在net_bridge{}中。
struct net_bridge { ... ... struct timer_list gc_timer; ... ... };
網橋設備被創建並初始化的時候,具體說來是br_dev_setup的時候,通過br_stp_timer_init初始化STP相關的幾個定時器,其中包括了垃圾收集定時器。
br_add_bridge + |- alloc_netdev + |- br_dev_setup + |- br_stp_timer_init + |- ... HELLO Timer ... |- ... TCN Timer ... |- ... Topology Change Timer ... - setup_timer(&br->gc_timer, br_fdb_cleanup, (unsigned long) br);
setup_timer函數將timer->function和timer->data設置為:br_fdb_cleanup和net_bridge{}。要注意的是,不論STP協議是否運行,地址老化(垃圾收集)都是必要的。這里只是設置各個timer的回調函數和私有數據。並沒有啟動Timer。
在網橋設備打開的時候,br_stp_enable_bridge會把各個timer打開,包括gc_timer,
br_dev_open + |- br_stp_enable_bridge + |- ... |- mod_timer(&br->gc_timer, jiffies + HZ/10); // gc_timer第一次啟動的地方 |- ...
第一次打開的時候,在1/10秒后br_fdb_cleanup被調用;此后回調函數br_fdb_cleanup將timer自己設置為每br->aging_time或者“最近的一個條目到期時間”調用。這個timer的實現是值得學習的,因為它不是完全周期性的timer,而是根據條目中需要檢查的時間結合一個最大默認周期來進行。
7.2.2 地址老化處理
我們看看br_fdb_cleanup()是怎么實現的,順便也提一下hold_time()。
void br_fdb_cleanup(unsigned long _data) { struct net_bridge *br = (struct net_bridge *)_data; unsigned long delay = hold_time(br);// 地址老化時間,MIN {ageing_time, forward_delay} unsigned long next_timer = jiffies + br->ageing_time;// 預設下次收集時間為 ageing_time秒后,稍后
可能調整 int i;
spin_lock(&br->hash_lock); for (i = 0; i < BR_HASH_SIZE; i++) {// 遍歷所有FDB Hash Bucket struct net_bridge_fdb_entry *f; struct hlist_node *n; hlist_for_each_entry_safe(f, n, &br->hash[i], hlist) { // 遍歷所有FDB Hash沖突鏈表 unsigned long this_timer; if (f->is_static)// 靜態條目,包括端口地址和用戶設置的條目,不會老化、刪除。 continue; this_timer = f->updated + delay;// 條目老化到期的時間 if (time_before_eq(this_timer, jiffies))// 已經到期(到期時間在當前時間之前),就把它刪除 fdb_delete(br, f); // 這就是清除到期FDB Entry的地方 else if (time_before(this_timer, next_timer)) next_timer = this_timer;// 如果FDB中的某個條目中默認的下次檢查時間之前,就將下次收集時間提前 } } spin_unlock(&br->hash_lock); mod_timer(&br->gc_timer, round_jiffies_up(next_timer));// 設置下次垃圾收集的時間
7.3 “本地”FDB條目
網橋設備、網橋端口設備的MAC地址作為“Local”條目添加到FDB表,其is_local和is_static都需要置1,不會老化。這類FDB Entry通過fdb_insert添加,並且在地址改變的時候,需要做相應的更新。
從下圖我們發現,並沒有添加“網橋設備”MAC FDB的地方,這是因為網橋的MAC因默認情況下是其端口之一的地址,因此無需加入FDB。但是如果網橋端口地址改變時則需要更新。
對於網橋的地址加入,或者不加入FDB對於入口流量的影響,我們應該了解到, 只要幀的目的MAC是網橋或者各個網橋端口的MAC之一,幀就是要被遞交到本地Host的。
了解了何時“插入”本地且靜態的網橋端口、網橋的地址后,我們看看fdb_insert的實現,
static int fdb_insert(struct net_bridge *br, struct net_bridge_port *source, const unsigned char *addr, u16 vid) { struct hlist_head *head = &br->hash[br_mac_hash(addr, vid)];// FDB是Per-VLAN的,addr和vid都作為Hash鍵 struct net_bridge_fdb_entry *fdb;
if (!is_valid_ether_addr(addr))// 要插入的地址必須是合法的Ethernet地址 return -EINVAL; fdb = fdb_find(head, addr, vid);// 在某個VLAN中,地址是否已經存在 if (fdb) { // 地址已經存在? /* it is okay to have multiple ports with same * address, just use the first one. */ if (fdb->is_local) // 並且是Local的 return 0; // 允許多個端口用於同一個地址 br_warn(br, "adding interface %s with same address " "as a received packet\n", source ? source->dev->name : br->dev->name); fdb_delete(br, fdb);// 但如果地址和分本地地址沖突,就需要將非本地地址的條目刪除 } fdb = fdb_create(head, source, addr, vid);// 創建新的net_bridge_fdb_entry{},並插入FDB(br->hash)中 if (!fdb) return -ENOMEM; fdb->is_local = fdb->is_static = 1;// “插入”的地址一定是本地且靜態的 fdb_notify(br, fdb, RTM_NEWNEIGH); return 0;
7.4 地址學習
除了網橋端口和網橋的MAC地址,用戶還能手動添加靜態(通過netlink套接字),已經網橋字段學習地址的過程,
fdb_create的實現也不難理解,
static struct net_bridge_fdb_entry *fdb_create(struct hlist_head *head, struct net_bridge_port *source, const unsigned char *addr,
__u16 vid)
{ struct net_bridge_fdb_entry *fdb;
fdb = kmem_cache_alloc(br_fdb_cache, GFP_ATOMIC); if (fdb) { memcpy(fdb->addr.addr, addr, ETH_ALEN); fdb->dst = source; fdb->vlan_id = vid; fdb->is_local = 0; fdb->is_static = 0; fdb->updated = fdb->used = jiffies; hlist_add_head_rcu(&fdb->hlist, head); } return fdb;
參考資料:
https://github.com/beacer/notes/blob/master/kernel/bridging.md
https://www.ibm.com/developerworks/cn/linux/kernel/l-netbr/
http://tjlxy.lofter.com/post/335f69_10a48df