1. 程式人生 > >linux網橋淺析

linux網橋淺析

之間 功能 停止 下一個 多少 不一定 mac地址 開始 使用

轉:http://www.cnblogs.com/morphling/p/3458546.html

什麽是橋接?

簡單來說,橋接就是把一臺機器上的若幹個網絡接口“連接”起來。其結果是,其中一個網口收到的報文會被復制給其他網口並發送出去。以使得網口之間的報文能夠互相轉發。
交換機就是這樣一個設備,它有若幹個網口,並且這些網口是橋接起來的。於是,與交換機相連的若幹主機就能夠通過交換機的報文轉發而互相通信。

如下圖:主機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收到。

linux的橋接實現



相關模型
linux 內核支持網口的橋接(目前只支持以太網接口)。但是與單純的交換機不同,交換機只是一個二層設備,對於接收到的報文,要麽轉發、要麽丟棄。小型的交換機裏 面只需要一塊交換芯片即可,並不需要CPU。而運行著linux內核的機器本身就是一臺主機,有可能就是網絡報文的目的地。其收到的報文除了轉發和丟棄, 還可能被送到網絡協議棧的上層(網絡層),從而被自己消化。
linux內核是通過一個虛擬的網橋設備來實現橋接的。這個虛擬設備可以綁定若幹個以太網接口設備,從而將它們橋接起來。如下圖(摘自ULNI):


網 橋設備br0綁定了eth0和eth1。對於網絡協議棧的上層來說,只看得到br0,因為橋接是在數據鏈路層實現的,上層不需要關心橋接的細節。於是協議 棧上層需要發送的報文被送到br0,網橋設備的處理代碼再來判斷報文該被轉發到eth0或是eth1,或者兩者皆是;反過來,從eth0或從eth1接收 到的報文被提交給網橋的處理代碼,在這裏會判斷報文該轉發、丟棄、或提交到協議棧上層。
而有時候eth0、eth1也可能會作為報文的源地址或目的地址,直接參與報文的發送與接收(從而繞過網橋)。

相關數據結構

要 使用橋接功能,我們需要在編譯內核時指定相關的選項,並讓內核加載橋接模塊。然後通過“brctl addbr {br_name}”命令新增一個網橋設備,最後通過“brctl addif {eth_if_name}”命令綁定若幹網絡接口。完成這些操作後,內核中的數據結構關系如下圖所示(摘自ULNI):


其中最左邊的net_device是一個代表網橋的虛擬設備結構,它關聯了一個net_bridge結構,這是網橋設備所特有的數據結構。
在 net_bridge結構中,port_list成員下掛一個鏈表,鏈表中的每一個節點(net_bridge_port結構)關聯到一個真實的網口設備 的net_device。網口設備也通過其br_port指針做反向的關聯(那麽顯然,一個網口最多只能同時被綁定到一個網橋)。
net_bridge 結構中還維護了一個hash表,是用來處理地址學習的。當網橋準備轉發一個報文時,以報文的目的Mac地址為key,如果可以在hash表中索引到一個 net_bridge_fdb_entry結構,通過這個結構能找到一個網口設備的net_device,於是報文就應該從這個網口轉發出去;否則,報文 將從所有網口轉發。

接收過程

在《linux網絡報文接收發送淺析》一文中我們看到,網口設備接收到的報文最終通過net_receive_skb函數被網絡協議棧所接收。

net_receive_skb(skb);
這個函數主要做三件事情:
1、如果有抓包程序需要skb,將skb復制給它們;
2、處理橋接;
3、將skb提交給網絡層;

這 裏我們只關心第2步。那麽,如何判斷一個skb是否需要做橋接相關的處理呢?skb->dev指向了接收這個skb的設備,如果這個 net_device的br_port不為空(它指向一個net_bridge_port結構),則表示這個net_device正在被橋接,並且通過 net_bridge_port結構中的br指針可以找到網橋設備的net_device結構。於是調用到br_handle_frame函數,讓橋接的 代碼來處理這個報文;

br_handle_frame(net_bridge_port, skb);
如果skb的目的Mac地址與接收該skb的網口的Mac地址相同,則結束橋接處理過程(返回到net_receive_skb函數後,這個skb會最終被提交給網絡層);
否則,調用到br_handle_frame_finish函數將報文轉發,然後釋放skb(返回到net_receive_skb函數後,這個skb就不會往網絡層提交了);

br_handle_frame_finish(skb);
首先通過br_fdb_update函數更新網橋設備的地址學習hash表中對應於skb的源Mac地址的記錄(更新時間戳及其所指向的net_bridge_port結構);
如 果skb的目的地址與本機的其他網口的Mac地址相同(但是與接收該skb的網口的Mac地址不同,否則在上一個函數就返回了),就調用 br_pass_frame_up函數,該函數會將skb->dev替換成網橋設備的dev,然後再調用netif_receive_skb來處理 這個報文。這下子netif_receive_skb函數被遞歸調用了,但是這一次卻不會再觸發網橋的相關處理函數,因為skb->dev已經被替 換,skb->dev->br_port已經是空了。所以這一次netif_receive_skb函數最終會將skb提交給網絡層;
否 則,通過__br_fdb_get函數在網橋設備的地址學習hash表中查找skb的目的Mac地址所對應的dev,如果找到(且通過其時間戳認定該記錄 未過期),則調用br_forward將報文轉發給這個dev;而如果找不到則調用br_flood_forward進行轉發,該函數會遍歷網橋設備中的 port_list,找到每一個綁定的dev(除了與skb->dev相同的那個),然後調用br_forward將其轉發;

br_forward(net_bridge_port, skb);
將skb->dev替換成將要進行轉發的dev,然後調用br_forward_finish,而後者又會調用br_dev_queue_push_xmit。
最終,br_dev_queue_push_xmit會調用dev_queue_xmit將報文發送出去(見《linux網絡報文接收發送淺析》)。註意,此時skb->dev已經被替換成進行轉發的dev了,報文會從這個網口被轉發出去;

發送過程
在《linux網絡報文接收發送淺析》 一文中我們看到,協議棧上層需要發送報文時,調用dev_queue_xmit(skb)函數。如果這個報文需要通過網橋設備來發送,則 skb->dev指向一個網橋設備。網橋設備沒有使用發送隊列(dev->qdisc為空),所以dev_queue_xmit將直接調用 dev->hard_start_xmit函數,而網橋設備的hard_start_xmit等於函數br_dev_xmit;

br_dev_xmit(skb, dev);
通 過__br_fdb_get函數在網橋設備的地址學習hash表中查找skb的目的Mac地址所對應的dev,如果找到,則調用br_deliver將報 文發送給這個dev;而如果找不到則調用br_flood_deliver進行發送,該函數會遍歷網橋設備中的port_list,找到每一個綁定的 dev,然後調用br_deliver將其發送(此處邏輯與之前的轉發很像);

br_deliver(net_bridge_port, skb);
這 個函數的邏輯與之前轉發時調用的br_forward很像。先將skb->dev替換成將要進行轉發的dev,然後調用 br_forward_finish。如前面所述,br_forward_finish又會調用到br_dev_queue_push_xmit,後者最 終調用dev_queue_xmit將報文發送出去。

以上過程忽略了對於廣播或多播Mac地址的處理,如果Mac地址是廣播或多播地址,就向所有綁定的dev轉發報文就行了。

另外,關於地址學習的過期記錄,專門有一個定時器周期性地調用br_fdb_cleanup函數來將它們清除。

生成樹協議

對於網橋來說,報文的轉發、地址學習其實都是很簡單的事情。在簡單的網絡環境中,這就已經足夠了。
而對於復雜的網絡環境,往往需要對數據通路做一定的冗余,以便當網絡中某個交換機出現故障、或交換機的某個網口出現故障時,整個網絡還能夠正常使用。
那麽,我們假設在上面的網絡拓撲中增加一條冗余的連接,看看會發生什麽事情吧。


假 設交換機S1和S2都是剛剛啟動(沒有學習到任何地址),此時主機C向B發送一個報文。交換機S2的eth2口收到報文,並將其轉發到eth0、 eth1、eth3,並且記錄下“主機C由eth2接入”。交換機S1在其eth2和eth3口都會收到報文,eth2口收到的報文又會從eth3口(及 其他口)轉發出去、eth3口收到的報文也會從eth2口(及其他口)轉發出去。於是交換機S2的eth0、eth1口又將再次收到這個報文,報文的源地 址還是主機C。於是S2相繼更新學習到的地址,記錄下“主機C由eth0接入”,然後又更新為“主機C由eth1接入”。然後報文又繼續被轉發給交換機 S1,S1又會轉發回S2。形成一個回路,周而復始,並且每一次輪回還會導致報文被復制給其他網口,最終形成網絡風暴。整個網絡可能就癱瘓了。
可見,我們之前討論的交換機是不能在這樣的帶有環路的拓撲中使用的。但是如果要想給網絡添加一定的冗余連接,則又必定會存在環路,這該怎麽辦呢?
IEEE 規範定義了生成樹協議(STP),如果網絡拓撲中的交換機支持這種協議,則它們會通過BPUD報文(網橋協議數據單元)進行通信,相互協調,暫時阻塞掉某 些交換機的某些網口,使得網絡拓撲不存在環路,成為一個樹型結構。而當網絡中某些交換機出現故障,這些被暫時阻塞掉的網口又會重新啟用,以保持整個網絡的 連通性。

由一個帶有 環路的圖生成一棵樹的算法是很簡單的,但是,正所謂“不識廬山真面目,只緣身在此山中”,網絡中的每一臺交換機都不知道確切的網絡拓撲,並且網絡拓撲還可 能動態地改變。要通過交換機間的信息傳遞(傳遞BPUD報文)來生成這麽一棵樹,那就不是一件簡單的事情了。來看看生成樹協議是怎麽做到的吧。

確定樹根
要生成一棵樹,第一步是確定樹根。協議規定,只有作為樹根節點的交換機才能發送BPUD報文,以協調其他交換機。當一臺交換機啟動時,它不知道誰是樹根,則他會把自己就當作樹根,從它的各個網口發出BPUD報文。
BPUD 報文可以說是表明發送者身份的報文,裏面含有一個“root_id”,也就是發送者的ID(發送者都認為自己就是樹根)。這個ID由兩部份組成,優先 級+Mac地址。ID越小則該交換機越重要,越應該被任命為樹根。ID中的優先級是由網絡管理員來指定的,當然性能越好的交換機應該被指定為越高的優先級 (即越小的值)。兩個交換機的ID比較,首先比較的就是優先級。而如果優先級相同,則比較其Mac地址。就好比兩個人地位相當,只好按姓氏筆劃排列了。而 交換機的Mac地址是全世界唯一的,所以交換機ID不會相同。

一 開始,各個交換機都自以為是地認為自己是樹根,都發出了BPUD報文,並在其中表明了自己的身份。而各個交換機自然也會收到來自於其他交換機的BPUD報 文,如果發現別人的ID更小(優先級更高),這時,交換機才意識到“天外有天、人外有人”,於是停止自己愚昧的“自稱樹根”的舉動。並且將收到的帶有更高 優先級的BPUD報文轉發,讓其他人也知道有這麽個高優先級的交換機存在。
最終,所有交換機會達成共識,知道網絡中有一個ID為XXXX的家夥,他才是樹根。

確定上行口
確定了樹根,也就確定了網絡拓撲的最頂層。而其他交換機則需要確定自己的某個網口,作為其向上(樹根方向)轉發報文的網口(上行口)。想一想,如果一個交換機有多個上行口,則網絡拓撲必然會存在回路。所以一個交換機的上行口有且只有一個。
那麽這個唯一的上行口怎麽確定呢?取各個網口中,到樹根的開銷最小的那一個。

上 面說到,樹根發出的BPUD報文會被其他交換機所轉發,最終每個交換機的某些網口會收到這個BPUD。BPUD中還有這麽三個字段,“到樹根的開銷”、 “交換機ID”、“網口ID”。交換機在轉發BPUD時,會更新這三個字段,把“交換機ID”更新為自己的ID,把“網口ID”更新為轉發該BPUD的那 個網口的編號,而“到樹根的開銷”則被增加一定的值(根據實際的轉發開銷,由交換機自己決定。可能是個大概值)。樹根最初發出的BPUD,“到樹根的開 銷”為0。每轉發一次,該字段就被增加相應的開銷值。
假 設樹根發出了一個BPUD,由於轉發,一個交換機的同一個網口可能會多次收到這個BPUD報文的復本。這些復本可能經過了不同的轉發路徑才來到這個網口, 因此有著不同的“到樹根的開銷”、“交換機ID”、“網口ID”。這三個字段的值越小,表示按照該BPUD轉發的路徑,到達樹根的開銷越小,就認為該 BPUD的優先級越高(其實後兩個字段也只是啟到“按姓氏筆劃排列”的作用)。交換機會記錄下在其每一個網口上收到的優先級最高的BPUD,並且只有當一 個網口當前收到的這個BPUD比它所記錄的BPUD(也就是曾經收到的優先級最高的BPUD)的優先級還高時,這個交換機才會將該BPUD報文由其他網口 轉發出去。最後,比較各個網口所記錄的BPUD的優先級,最高者被作為交換機的上行口。

確定需要被阻塞的下行口
交換機除了其上行口之外的其他網口都是下行口。交換機的上行路徑不會存在環路,因為交換機都只有唯一的上行口。
而 不同交換機的多個下行口有可能是相互連通的,會形成環路。(這些下行口也不一定是直接相連,可能是由物理層的轉發設備將多個交換機的多個下行口連在一 起。)生成樹協議的最後一道工序就是在這一組相互連通的下行口中,選擇一個讓其轉發報文,其他網口都被阻塞。由此消除存在的環路。而那些沒有與其他下行口 相連的下行口則不在考慮之列,它們不會引起環路,都照常轉發。
不過,既然下行口兩兩相連會產生回路,是不是把這些相連的下行口都阻塞就好了呢?前面提到過可能存在物理層設備將多個網口同時連在一起的情況(如集線器Hub,盡管現在已經很少用了),如圖:


假設交換機S2的eth2口和交換機S3的eth1口是互相連通的兩個下行口,如果武斷地將這兩網口都阻塞,則主機E就被斷網了。所以,這兩個網口還必須留下一個來提供報文轉發服務。

那麽對於一組相互連通的下行口,該選擇誰來作為這個唯一能轉發報文的網口呢?
上 面說到,每個交換機在收到優先級最高的BPUD時,都會將其轉發。轉發的時候,“到樹根的開銷”、“交換機ID”、“網口ID”都會被更新。於是對於一組 相互連通的下行口,從誰那裏轉發出來的BPUD優先級最高,就說明從它到達樹根的開銷最小。於是這個網口就可以繼續轉發報文,而其他網口都被阻塞。
從實現上來說,每個網口需記錄下自己轉發出去的BPUD的優先級是多少。如果其沒有收到比該優先級更高的BPUD(沒有與其他下行口相連,收不到BPUD;或者與其他下行口相連,但是收到的BPUD優先級較低),則網口可以轉發;否則網口被阻塞。

經 過交換機之間的這一系列BPUD報文交換,生成樹完成。然而網絡拓撲也可能因為一些人為因素(如網絡調整)或非人為因素(如交換機故障)而發生改變。於是 生成樹協議中還定義了很多機制來檢測這種改變,而後觸發新一輪的BPUD報文交換,形成新的生成樹。這個過程就不再贅述了。

linux網橋淺析