linux網橋淺析


   什麼是橋接?

   簡單來說,橋接就是把一臺機器上的若干個網絡接口“連接”起來。其結果是,其中一個網口收到的報文會被複制給其他網口併發送出去。以使得網口之間的報文能夠互相轉發。

交換機就是這樣一個設備,它有若干個網口,並且這些網口是橋接起來的。於是,與交換機相連的若干主機就能夠通過交換機的報文轉發而互相通信。

   如下圖:主機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報文交換,形成新的生成樹。這個過程就不再贅述了。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章