原文連接:http://hi.baidu.com/_kouu/item/25787d38efec56637c034bd0linux
什麼是橋接?
簡單來講,橋接就是把一臺機器上的若干個網絡接口「鏈接」起來。其結果是,其中一個網口收到的報文會被複制給其餘網口併發送出去。以使得網口之間的報文可以互相轉發。 交換機就是這樣一個設備,它有若干個網口,而且這些網口是橋接起來的。因而,與交換機相連的若干主機就可以經過交換機的報文轉發而互相通訊。
以下圖:主機A發送的報文被送到交換機S1的eth0口,因爲eth0與eth一、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); 這個函數主要作三件事情: 一、若是有抓包程序須要skb,將skb複製給它們; 二、處理橋接; 三、將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、eth一、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報文交換,造成新的生成樹。這個過程就再也不贅述了。算法