# 802.1D - Spanning Tree Algorithm
[TOC]
## 課程影片
### 第 4G 講 IEEE 802.1D 交換機的擴張樹演算法 (Spanning Tree Algorithm) L01 7
{%youtube qS7mIExtLqs %}
### 第 4H 講 IEEE 802.1D 交換機的擴張樹演算法 (Spanning Tree Algorithm) L01 8
{%youtube AZIkkp6rxyU %}
## 三個任務
1. 任務一:找出 root bridge:也就是找到 BID 最低的 bridge
2. 任務二:找到自己的 root port。
3. 任務三:幫一個 LAN 找到 designated port。
這三個任務未必是依照 1~3 的順序決定的。
## 情報交換的單位 --- BPDU

[`net/bridge/br_private_stp.h`](https://elixir.bootlin.com/linux/latest/source/net/bridge/br_private_stp.h#L28) 中:
```c
struct br_config_bpdu {
unsigned int topology_change:1;
unsigned int topology_change_ack:1;
bridge_id root;
int root_path_cost;
bridge_id bridge_id;
port_id port_id;
int message_age;
int max_age;
int hello_time;
int forward_delay;
};
```
兩者之間欄位可以由 [`br_record_config_information()`](https://elixir.bootlin.com/linux/latest/source/net/bridge/br_stp.c#L245) 得知:
```c
static void br_record_config_information(struct net_bridge_port *p,
const struct br_config_bpdu *bpdu)
{
p->designated_root = bpdu->root;
p->designated_cost = bpdu->root_path_cost;
p->designated_bridge = bpdu->bridge_id;
p->designated_port = bpdu->port_id;
p->designated_age = jiffies - bpdu->message_age;
mod_timer(&p->message_age_timer, jiffies
+ (bpdu->max_age - bpdu->message_age));
}
```
### `bridge_id` --- 自己的 BID
誰發出這個 BPDU。
### `root` --- 發送者認為的 Root BID
也就是發送者目前看過「最小」的 BID。
### `root_path_cost` --- 自己到 Root 的成本
這個 BPDU 中所記錄的的 `bridge_id` 到所記錄的 `root` 所需要的成本。
## Port 的資料結構 --- `struct net_bridge_port`
```c
struct net_bridge_port {
struct net_bridge *br;
...
/* STP */
u8 priority;
u8 state;
u16 port_no;
unsigned char topology_change_ack;
unsigned char config_pending;
port_id port_id;
port_id designated_port;
bridge_id designated_root;
bridge_id designated_bridge;
u32 path_cost;
u32 designated_cost;
unsigned long designated_age;
...
}
```
每個 port 中,都會紀錄「這個 port 認為的 root bridge」是誰,以及到這個 root bridge 的成本。最後,還有這個 port 認為的 designated port 是誰。這些成員分別是:
### `path_cost` --- Port 的成本
這就是計算 `root_path_cost` 時,經過這個端口要加上的那個成本。
### `designated_root` --- 目前看過最小的 BID
這個 port 收到的所以 BPDU 中,出現過的最小的 BID。所以可以想像如果一個 port 的 `designated_root` 被更新,下一步就是拿回去跟 bridge 目前已知的 root bridge 做比較。
### `designated_cost` --- Port 到 Root Bridge 的最小成本
從這個 port 出發到 root bridge 所需要的最小成本。所以所有的 port 中,具有最小 `designated_cost` 的那個 port 就會變 root port。
### `designated_bridge` 與 `designated_port` --- 目前的 Designated Port
`designated_bridge` 與 `designated_port` 這兩個成員指得是「目前找到的 designated port 是哪一個 bridge 的哪個 port」。因為 ==一個 BPDU 會紀錄這個 BPDU 從哪個 Bridge 的哪個 Port 發出,而一個 Port 如果要收到一個 BPDU,就要跟發出 BPDU 的那個 Port 處於同一個 LAN 上。所以藉由比較這個 BPDU 上的 `root_path_cost` ,就可以知道自己是否要從 designated port 「退位」,讓給那個發出 BPDU 的 port==。
因此,對於一個 designated port,看看他的 `designated_bridge` 與 `designated_port` 是否恰好就是「自己所屬的 bridge」與「自己的 port ID」,就可以確認自己是不是 designated port。而這也可以在 [`br_is_designated_port()`](https://elixir.bootlin.com/linux/latest/source/net/bridge/br_private_stp.h#L42) 的實作中看到:
```c
static inline int br_is_designated_port(const struct net_bridge_port *p)
{
return !memcmp(&p->designated_bridge, &p->br->bridge_id, 8) &&
(p->designated_port == p->port_id);
}
```
### 觀察:從眾多 Port 中挑出 Root Port
這時可以觀察到:一個 bridge 中的所有 port 中:
1. 具有最小 `designated_root` 的那個 port,就會是 root port。
2. 如果這樣的 port 有很多個,具有最小 `designated_cost` 的 port 就會是 root port。
3. 如果這樣的 port 還是有很多個,那麼具有最小 `port_id` 的,就會是 root port。
## Bridge 的資料結構 --- `struct net_bridge`
每個 bridge 也會維持自己的資料結構。在 [`net/bridge/br_private.h`](https://elixir.bootlin.com/linux/latest/source/net/bridge/br_private.h#L447) 中。其中,跟 STP 有關的部分:
```c
struct net_bridge {
...
/* STP */
bridge_id designated_root;
bridge_id bridge_id;
unsigned char topology_change;
unsigned char topology_change_detected;
u16 root_port;
unsigned long max_age;
unsigned long hello_time;
unsigned long forward_delay;
unsigned long ageing_time;
unsigned long bridge_max_age;
unsigned long bridge_hello_time;
unsigned long bridge_forward_delay;
unsigned long bridge_ageing_time;
u32 root_path_cost;
u8 group_addr[ETH_ALEN];
enum {
BR_NO_STP, /* no spanning tree */
BR_KERNEL_STP, /* old STP in kernel */
BR_USER_STP, /* new RSTP in userspace */
} stp_enabled;
...
```
### `designated_root` --- Root Bridge 的 BID
### `root_port` --- 自己的哪個 Port 是 Root Port?
## 收到 BPDU 時:由 Port 到 Bridge 往上更新
首先,每個 bridge 都往自己的所有端口廣播「Root BID 為自己 BUD」 BPDU,接著開始接收旁邊廣播來的 BPDU。接下來每收到一個 BPDU,就依照現在大家正在「尋找 Root Bridge」「決定 Root Port」「決定 Designated Port」的哪個階段,來決定收到 BPDU 時該做什麼反應。:
```c
void br_received_config_bpdu(struct net_bridge_port *p,
const struct br_config_bpdu *bpdu)
{
struct net_bridge *br;
int was_root;
p->stp_xstats.rx_bpdu++;
br = p->br;
was_root = br_is_root_bridge(br);
if (br_supersedes_port_info(p, bpdu)) {
br_record_config_information(p, bpdu);
br_configuration_update(br);
br_port_state_selection(br);
if (!br_is_root_bridge(br) && was_root) {
del_timer(&br->hello_timer);
if (br->topology_change_detected) {
del_timer(&br->topology_change_timer);
br_transmit_tcn(br);
mod_timer(&br->tcn_timer,
jiffies + br->bridge_hello_time);
}
}
if (p->port_no == br->root_port) {
br_record_config_timeout_values(br, bpdu);
br_config_bpdu_generation(br);
if (bpdu->topology_change_ack)
br_topology_change_acknowledged(br);
}
} else if (br_is_designated_port(p)) {
br_reply(p);
}
}
```
這是第一次廣播之後開始發生的事情,所以這是最先發生的。如果現在大家正在決定哪一個 bridge 是 root bridge,或者是說大家正在找「哪個 bridge 有最小的 BID」,那麼就會發生以下的事情:
## 第一關:每個 Port 看看自己有沒有收到有用的情報?
首先,拿到 BPDU 時,會先看看上面是不是個更好的情報來源。這個「更好」包含:
1. BPDU 上面記錄的 root bridge(BPDU 的 `root` 成員)是否比自己現在認為的 root(port 自己的 `designated_root` 成員)還要小。
2. BPDU 上面記錄的 root BID 相同,但是上面的 `root_path_cost` 更小。
這個比較發生在 [`br_supersedes_port_info()`](https://elixir.bootlin.com/linux/latest/source/net/bridge/br_stp.c#L325) 中:
```c
static int br_supersedes_port_info(const struct net_bridge_port *p,
const struct br_config_bpdu *bpdu)
{
int t;
t = memcmp(&bpdu->root, &p->designated_root, 8);
if (t < 0)
return 1;
else if (t > 0)
return 0;
if (bpdu->root_path_cost < p->designated_cost)
return 1;
else if (bpdu->root_path_cost > p->designated_cost)
return 0;
t = memcmp(&bpdu->bridge_id, &p->designated_bridge, 8);
if (t < 0)
return 1;
else if (t > 0)
return 0;
if (memcmp(&bpdu->bridge_id, &p->br->bridge_id, 8))
return 1;
if (bpdu->port_id <= p->designated_port)
return 1;
return 0;
}
```
## 第二關:Port 先把最好的情報收起來
如果發現 BPDU 上面記錄著更好的路徑(也就是上面這個函式回傳 `1` 時),那麼就把這個 BPDU 上面的資訊更新到這個 port 上,並且叫 bridge 更新自己的 root port。其中,「把 BPDU 更新到 port 上」是由 [`br_record_config_information()`](https://elixir.bootlin.com/linux/latest/source/net/bridge/br_stp.c#L245) 來做:
```c
static void br_record_config_information(struct net_bridge_port *p,
const struct br_config_bpdu *bpdu)
{
p->designated_root = bpdu->root;
p->designated_cost = bpdu->root_path_cost;
p->designated_bridge = bpdu->bridge_id;
p->designated_port = bpdu->port_id;
p->designated_age = jiffies - bpdu->message_age;
mod_timer(&p->message_age_timer, jiffies
+ (bpdu->max_age - bpdu->message_age));
}
```
其中,一個 port 的 `designated_*` 成員,指得是:如果指定從這個 port 送封包給這個 port 所認為的 root,那麼 `root_path_cost` 是多少?以及這個風包送出去之後,第一個會送給哪個 bridge 的哪個 port?等等。
而更新到 port 上之後,就再看看現在這個 bridge 上的所有 port 中,會不會有更適合的 port 能夠當 root port 或 designted port。這個也就是接下來的 [`br_configuration_update()`](https://elixir.bootlin.com/linux/latest/source/net/bridge/br_stp.c#L405) 要做的事:
```c
void br_configuration_update(struct net_bridge *br)
{
br_root_selection(br);
br_designated_port_selection(br);
}
```
## 第三關:從所以 Port 的情報中找出「最好中的最好」的情報
### 看這個 Port 會不會成為 Root Port --- `br_root_selection()`
其中,`br_root_select()` 做的事情就是把 bridge 上的每個 root port 看一遍,看看有哪個比現在的 root port 更好。如果沒有 root port,就表示現在這個 bridge 是 root bridge,所以就把自己變成 root bridge; 反之,就把 bridge 的 root port 設定成剛剛找到的那個 root port:
```c
static void br_root_selection(struct net_bridge *br)
{
struct net_bridge_port *p;
u16 root_port = 0;
list_for_each_entry(p, &br->port_list, list) {
if (!br_should_become_root_port(p, root_port))
continue;
if (p->flags & BR_ROOT_BLOCK)
br_root_port_block(br, p);
else
root_port = p->port_no;
}
br->root_port = root_port;
if (!root_port) {
br->designated_root = br->bridge_id;
br->root_path_cost = 0;
} else {
p = br_get_port(br, root_port);
br->designated_root = p->designated_root;
br->root_path_cost = p->designated_cost + p->path_cost;
}
}
```
而從眾多 port 中挑出 root port 的標準,就是看每個 port 的 `designated_*` 成員。「以一個 port 為出發點,可以走到的最佳路徑」。如果發現從某一個 port 出發到 root 的路徑比現在 bridge 上的路徑更好,那就表示這個 port 應該要成為新的 root port,所以就回傳 `1`:
```c
static int br_should_become_root_port(const struct net_bridge_port *p,
u16 root_port)
{
struct net_bridge *br;
struct net_bridge_port *rp;
int t;
br = p->br;
if (p->state == BR_STATE_DISABLED ||
br_is_designated_port(p))
return 0;
if (memcmp(&br->bridge_id, &p->designated_root, 8) <= 0)
return 0;
if (!root_port)
return 1;
rp = br_get_port(br, root_port);
t = memcmp(&p->designated_root, &rp->designated_root, 8);
if (t < 0)
return 1;
else if (t > 0)
return 0;
if (p->designated_cost + p->path_cost <
rp->designated_cost + rp->path_cost)
return 1;
else if (p->designated_cost + p->path_cost >
rp->designated_cost + rp->path_cost)
return 0;
t = memcmp(&p->designated_bridge, &rp->designated_bridge, 8);
if (t < 0)
return 1;
else if (t > 0)
return 0;
if (p->designated_port < rp->designated_port)
return 1;
else if (p->designated_port > rp->designated_port)
return 0;
if (p->port_id < rp->port_id)
return 1;
return 0;
}
```
1. 拿到 BPDU 時,看看這個 BPDU 是哪個端口來的。
2. 檢查「BPDU 上的 `root_path_cost`」加上「收到端口的成本」,看看跟目前的 `root_path_cost` 比起來是否更低。如果成本更低,就 root port 更新成現在這個端口。
### 看看這個 Port 會不會變成 Designated Port --- `br_designated_port_selection()`
```c
static void br_designated_port_selection(struct net_bridge *br)
{
struct net_bridge_port *p;
list_for_each_entry(p, &br->port_list, list) {
if (p->state != BR_STATE_DISABLED &&
br_should_become_designated_port(p))
br_become_designated_port(p);
}
}
```
LAN 上的所有 port 會聽到所有其他 port 送出的 BPDU。如果發現這個「目前聽到所有其他 port 的最小 `root_path_cost`」比「經過自己會有的 `root_path_cost`」還要小,那這個 port 就有資格成為 designated port:
```c
static int br_should_become_designated_port(const struct net_bridge_port *p)
{
struct net_bridge *br;
int t;
br = p->br;
if (br_is_designated_port(p))
return 1;
if (memcmp(&p->designated_root, &br->designated_root, 8))
return 1;
if (br->root_path_cost < p->designated_cost)
return 1;
else if (br->root_path_cost > p->designated_cost)
return 0;
t = memcmp(&br->bridge_id, &p->designated_bridge, 8);
if (t < 0)
return 1;
else if (t > 0)
return 0;
if (p->port_id < p->designated_port)
return 1;
return 0;
}
```
而「成為 designated port」的意思就是「把自己到 root bridge 的成本,從『打聽到的最小值』變成『走自己的 bridge 的成本』」:
```c
void br_become_designated_port(struct net_bridge_port *p)
{
struct net_bridge *br;
br = p->br;
p->designated_root = br->designated_root;
p->designated_cost = br->root_path_cost;
p->designated_bridge = br->bridge_id;
p->designated_port = p->port_id;
}
```
## 第四階段:昭告天下
```c
void br_config_bpdu_generation(struct net_bridge *br)
{
struct net_bridge_port *p;
list_for_each_entry(p, &br->port_list, list) {
if (p->state != BR_STATE_DISABLED &&
br_is_designated_port(p))
br_transmit_config(p);
}
}
```
在這個過程中,只有 root port 的 BPDU 能夠暢行無阻地一路被所有 Bridge 「複誦」過去。
```c
void br_become_root_bridge(struct net_bridge *br)
{
br->max_age = br->bridge_max_age;
br->hello_time = br->bridge_hello_time;
br->forward_delay = br->bridge_forward_delay;
br_topology_change_detection(br);
del_timer(&br->tcn_timer);
if (br->dev->flags & IFF_UP) {
br_config_bpdu_generation(br);
mod_timer(&br->hello_timer, jiffies + br->hello_time);
}
}
```
```c
static inline int br_is_root_bridge(const struct net_bridge *br)
{
return !memcmp(&br->bridge_id, &br->designated_root, 8);
}
```
### 任務二:檢查 Root Port 需不需要改變?
如果現在是「所有人正在決定自己的哪個 port 要當 root port」,那麼拿到 BPDU 時,會發生下面的事:
3. 把帶有新的 `root_path_cost` 的 BPDU 廣播出去。
把新的 BPDU 廣播出去時,其它收到的 bridge 就繼續做上面這些事。
```c
static int br_should_become_root_port(const struct net_bridge_port *p,
u16 root_port)
{
struct net_bridge *br;
struct net_bridge_port *rp;
int t;
br = p->br;
if (p->state == BR_STATE_DISABLED ||
br_is_designated_port(p))
return 0;
if (memcmp(&br->bridge_id, &p->designated_root, 8) <= 0)
return 0;
if (!root_port)
return 1;
rp = br_get_port(br, root_port);
t = memcmp(&p->designated_root, &rp->designated_root, 8);
if (t < 0)
return 1;
else if (t > 0)
return 0;
if (p->designated_cost + p->path_cost <
rp->designated_cost + rp->path_cost)
return 1;
else if (p->designated_cost + p->path_cost >
rp->designated_cost + rp->path_cost)
return 0;
t = memcmp(&p->designated_bridge, &rp->designated_bridge, 8);
if (t < 0)
return 1;
else if (t > 0)
return 0;
if (p->designated_port < rp->designated_port)
return 1;
else if (p->designated_port > rp->designated_port)
return 0;
if (p->port_id < rp->port_id)
return 1;
return 0;
}
```
```c
static void br_root_selection(struct net_bridge *br)
{
struct net_bridge_port *p;
u16 root_port = 0;
list_for_each_entry(p, &br->port_list, list) {
if (!br_should_become_root_port(p, root_port))
continue;
if (p->flags & BR_ROOT_BLOCK)
br_root_port_block(br, p);
else
root_port = p->port_no;
}
br->root_port = root_port;
if (!root_port) {
br->designated_root = br->bridge_id;
br->root_path_cost = 0;
} else {
p = br_get_port(br, root_port);
br->designated_root = p->designated_root;
br->root_path_cost = p->designated_cost + p->path_cost;
}
}
```