1.Linux網路棧下兩層實現

mirkerson發表於2015-07-14


1.1簡介

    VLAN是網路棧的一個附加功能,且位於下兩層。首先來學習Linux中網路棧下兩層的實現,再去看如何把VLAN這個功能附加上去。下兩層涉及到具體的硬體裝置,日趨完善的Linux核心已經做到了很好的程式碼隔離,對網路裝置驅動也是如此,如下圖所示:

這裡要注意的是,Linux下的網路裝置net_dev並不一定都對應實際的硬體裝置,只要註冊一個struct net_device{}結構體(netdevice.h)到核心中,那麼這個網路裝置就存在了。該結構體很龐大,其中包含裝置的協議地址(對於IP即IP地址),這樣它就能被網路層識別,並參與路由系統,最有名的當數loopback裝置。不同的裝置(包括硬體和非硬體)的ops操作方法各不相同,由驅動自己實現。一些通用性的、與裝置無關的操作流程(如裝置鎖定等)則被Linux提煉出來,我們稱為驅動框架。

1.2程式碼框架

    就是對於上圖的擴充套件,從程式碼的角度看網路棧的實現。這裡主要是學習的過程,一方面算是賞析Linux優美的程式碼結構,另一方面只有瞭解這些,才能更好地寫網路裝置的驅動,或者做平臺移植。

與網路相關的程式碼主要在~/net,框架性的程式碼在~/net/core中,另外很多結構定義、巨集、簡單行內函數在~/include/net、~/include/linux中,具體裝置的驅動在~/driver/net中。程式碼量很大,這裡僅給出一些關鍵的程式碼流程,且有些流程比較複雜,放到下一節描述。如下圖所示:

網路層的程式碼比較清晰,實際上還有一個forward流程(即路過主機,傳向他處),這裡沒畫出,其中NF_函式就是Netfilter框架的鉤子函式。這裡以IP協議為例,傳送流程的程式碼大多在ip_input.c檔案中,接收流程的程式碼大多在ip_output.c檔案中。其它網路層協議如IPv6、X25等,流程大致相同。各種協議在傳送流程的最後,都會主動呼叫dev_queue_xmit()函式;而裝置接收到資料包後,會根據包的型別,傳送給相應的協議函式,如ip_rcv(),當然這裡的實現還是比較複雜的,設計到一些全域性的資料結構,不是重點,沒看。

驅動框架的程式碼基本都在~/net/core/dev.c中。其中的程式碼分為3部分:

  1. 全域性性的程式碼,如netdev_init()是在系統啟動時初始化網路環境的(注意並不是初始化具體的裝置),register_netdevice()函式是新增\註冊網路裝置時呼叫的,它們中的一些細節直接關係到裝置的工作過程,下一節針對具體模組時分別講述;

  2. 傳送框架相關的,由上層呼叫dev_queue_xmit()函式,經過一系列處理(包括鎖定裝置、選擇佇列、vlan相關的處理等),最終呼叫裝置的hard_start_xmit()函式,由它完成硬體的傳送過程;

  3. 接收框架相關的,因為接收是一個被動過程,一般通過中斷來發起,但為了提高效能,Linux中的中斷處理一般分為兩部分(時間緊急的和時間不緊急的),即典型的UH+BH模型;另外近年來,人們發現大資料量時,連續的中斷有損效能,現在越來越多的驅動都改用NPI接收模型,將BH部分直接在驅動中實現,比較複雜。不過不管通過什麼流程接收到資料包後(封裝成skb),都會把它交給netif_receive_skb()函式,該函式對資料包進行處理(包括vlan相關的),最終通過deliver_skb(ptype)交付給相應的上層。

裝置驅動的程式碼(即netdev->ops所指向的函式),各個裝置不同,其中最最重要的有5個:dev_open(),hard_start_xmit(),tx_timeout(),interrupt(),poll()。另外其他一些函式如設定mtu,更改mac等,則根據具體裝置的功能選擇實現。

1.3程式碼細節

    以tealtek的rtl8169驅動為例,首先介紹一般的裝置驅動中實現那些功能,以及這些功能是如何組合起來的。然後分別從傳送、接收流程出發,分析驅動框架中的程式碼是如何支援這些功能的實現的。

1.3.1裝置驅動的功能組合

    前面講到了,裝置驅動中最最重要的5個函式,這些函式有機組合在一起,實現了可靠的裝置功能,如下圖所示:

開啟函式dev_open()中,首先初始化裝置的私有空間。每個網路裝置有一個net_device結構體,同時還有一個私有結構,由net_device.priv指標指向。在module_init()函式中,一般會呼叫netdev_alloc(sizeof(priv),name,setup_func)函式,該函式指明裝置的唯一名稱及一個初始化函式(對於乙太網,一般用ethe_setup()),同時申請net_device結構和private結構的空間。Private結構由不同的裝置自己決定,在dev_open()中,應初始化之。

傳送函式hard_start_xmit()中,首先利用硬體傳送資料,注意這裡僅是把資料寫入裝置的傳送快取中(或有些裝置直接是利用dma的),然後寫相應的暫存器,通知硬體開始傳送,之後該函式就正確返回了,然後硬體到底有沒有正確傳送資料還不知道。

中斷函式interrupt(),是在dev_open()時申請的,並根據實際硬體的中斷號,與某個中斷線聯絡並註冊進核心。當該中斷線上有中斷時,CPU跳轉到該函式執行。雖然是一根中斷線,但裝置中斷的型別卻不一樣,這有具體裝置決定,一般可通過讀取硬體的狀態暫存器獲悉。若是接收中斷,則以某種方式去呼叫poll()函式,把資料包傳遞給上層。

1.3.2傳送流程細節

    首先需要知道,Linux為每個網路裝置準備了傳送/接收佇列,alloc_netdev(sizeof(priv),name,setup_func)實際上被定義為alloc_netdev_mqs(x,x,x,1,1)(netdevice.h),即預設為每個裝置分配一個傳送佇列和一個接收佇列,佇列結構為struct netdev_queue,每個佇列中有個重要的結構struct Qdisc。該結構的功能主要是提供多程式使用同一個裝置時的鎖定功能,在SMP架構(或多核架構)的機器中,這種鎖定功能的實現變得尤為複雜,這也是現在核心設計的關鍵和難點,暫且不管。

    現在來看網路裝置的傳送流程,如下圖所示:

    對於沒有佇列的裝置(主要是一些虛擬裝置,如loopback),處理比較簡單。對於一般裝置,主要對它進行一些複雜的鎖定功能,而且函式呼叫出錯時需把該skb重新放回佇列中。最終都會呼叫dev_hard_start_xmit()函式,該函式是傳送流程中的關鍵,它是裝置無關的,主要會檢查並處理skb中的各種特性,一些新功能(如vlan)的實現都在這個函式中完成。該函式最終呼叫裝置驅動中的裝置相關的傳送函式。

    前面講的出錯,僅是函式呼叫的出錯。正如前面講述的,即使函式呼叫正確返回了,也並不代表硬體成功把資料包傳送出去了。所以一般網路卡裝置都會在裝置成功傳送資料時產生中斷,並在相應的暫存器中顯示這是個TxOK中斷。

    Linux的網路裝置驅動框架中,很好地利用了這點。每個裝置的net_device結構體中都有一個watchdog_timer,在module_init()中註冊該模組時,register_netdev()函式中會初始化該定時器,並註冊其func為dev_watchdog(),該函式的內容就是執行裝置驅動中實現的tx_timeout()。另外核心提供開啟、消去該定時器的函式,供驅動程式在相應的位置使用。

1.3.3接收流程細節

    接收過程是被動觸發的,一般由硬體的中斷引發。Linux在處理這種IO時一般採用典型的UH+BH模型,即把一些實時性高的操作(如把裝置快取中的資料copy到核心中,以便裝置可接收其它資料)發在中斷處理函式中完成,而把實時性要求不高的操作(如處理資料)發在稍後的時間裡完成(一般是另開一個執行緒)。

    在經典的網路裝置驅動中也經常使用這種模型,首先介紹兩個資料結構,分別是struct napi_struct{},裡面主要有擁有該資料結構的裝置的索引dev,和一個函式指標poll_func;另一個是struct softnet_data{},該結構中主要維護一個napi_struct的佇列。

    核心準備了一個全域性的struct softnet_data sd結構(實際上是為每個cpu準備了一個),另外準備了一個通用的poll_func函式process_backlog()。好了,現在來看驅動中的BH部分,如下圖所示:

裝置驅動中的interrupt函式,檢查到接收了新資料包時,就準備新的skb,並把資料copy到skb中,然後呼叫驅動框架中的netif_rx(skb)函式;該函式主要呼叫enqueue_to_backlog();該函式檢查是否初次進入,是則準備一個新的napi_struct結構,其poll_func定義為通用函式process_backlog(),並呼叫__napi_schedule()函式,把準備好的結構體放入sd的poll_list中,然後呼叫__raise_softirq_irqoff(RxIRQ)開啟軟中斷,且以後每次進入都把skb壓入backlog的queue中。

這裡要檢索另一個函式netdev_init()(在系統啟動時呼叫的),上述講的sd結構就是在這個函式中分配的,另外該函式還註冊了軟中斷函式net_rx_action(),軟中斷的原理沒去看,應該就是利用Linux核心的tasklet機制實現的。__raise_softirq_irqoff(RxIRQ)函式講軟中斷掩碼mask中的RxIRQ置位,這樣,BH部分就完成了,此時的sd結構如下圖:

    之後就是UH部分了,即系統在之後的某個時間,啟動軟中斷執行緒,執行net_rx_action()函式,該函式遍歷softnet_data sd結構中的poll_list,並執行每個napi_struct->poll_func()函式,由前面的敘述可知,這裡的poll_func()函式都是process_backlog(),該函式採用while迴圈取下dev上的skb(因為在軟中斷執行前可能發生了多次接收中斷),並呼叫__netif_receive_skb(skb)函式,講skb傳遞給上層協議。當接收到一定數量的包後,就認為本次資料包接收完畢了,並把該napi_struct結構從sd中刪除,如下圖所示:

    這就是傳統的中斷方式,可以參見RTL8012的驅動~/driver/net/ethernet/realtek/apt.c,就是利用的該方法,它的優點是,需要驅動程式 做的非常少,僅需準備好skb,呼叫net_rx(skb)即可,其它都有驅動框架完成。缺點是,欠靈活,且資料量大時,會不停的中斷,影響系統效能。

現在很多網路裝置驅動已不再使用這種結構,而是採用NAPI結構,它完全摒棄了核心驅動框架中的UH+BH模型,並且不再用中斷方式,而是在驅動內部使用輪詢方式。

    與中斷方式最大的不同在於,每次發生接收中斷時,關閉接收中斷,啟動軟中斷,在poll函式break前,重新開啟接收中斷,一遍下一輪的資料接收。其次,驅動程式自己定義napi_struct結構和poll_func函式。最後,poll_func函式和前面講的在結構上差不多,都是while迴圈,但它要自己準備skb(因為它之前沒有中斷程式來準備skb),並且直接上傳該skb,一般不會實現佇列queue(因為它之後沒有其它執行緒再去處理queue了)。

    這就是所謂的NAPI方式,它避免了多次的硬體中斷,一定程度上提高系統系能。但驅動程式也因此更加複雜,並且poll_func()函式中要做的事太多(摒棄了UH+BH模型),在資料量很大時,會出現丟包的現象(這好像是Linux的一個bug)。Rtl8169就是採用的這種方式,參見~/driver/net/Ethernet/realtek/r8169.c。

2.Linux中VLAN的實現

2.1Linux網路中的namespace

    這個概念我不是瞭解的很清楚,不過可以簡單地把它看成是一種分類,目前所瞭解的網路裝置有3類:傳統的網路裝置,它們不需要依賴於其它裝置而獨自存在,如eth0、loopback等;VLAN網路裝置,它需要依賴於一個宿主裝置,若宿主裝置沒了,它是不能工作的;Bridge網路裝置,它也是虛擬的,它依賴於從裝置。

    與此相關的結構有struct net{},相關檔案包括namespace.h、namespace.c等。這3類網路裝置都是以module的形式被加入核心中,它們可以看成是網路子系統的頂層module,下面實現的驅動模組等都依賴於它們。這3個頂層模組載入時分別執行的init函式為:netdev_init(),@dev.c;vlan_proto_init(),@vlan.c;br_init(),@br_device.c。

這3個函式中有自己特有的部分,如netdev_init中分配softnet_data等,它們也有相似的部分,如

這裡要重點看的是ioctl_set函式,這涉及到Linux下網路裝置的ioctl操作。在Linux中,所有網路裝置的ioctl操作都被抽象成對/proc/net/下的檔案的操作,最終呼叫核心中的sock_ioctl函式,該函式結構如下:

其中各個hook函式就這裡init()時利用ioctl_set_func()設定的。這種設計架構大大方便了使用者空間對各類虛擬裝置(如vlan,br等)的操作,如目前Linux下vlan的操作命令vconfig就是開啟/proc/net/vlan/config檔案,然後對它進行ioctl操作,詳細參見vconfig的原始碼(非常簡單)。Br也是差不多,以後學習br時再細看。

注意:這裡關於namespace的概念可能錯了,現在先不看,後面講到協議族時,再一起看看整個網路棧頂層的實現框架,這裡先關注底層的裝置。另注:這裡的vlan_ioctl的概念可能是錯誤的,它實際上是sock_ioctl的特殊情況,以後再看吧,包括應用層如何呼叫到它。

2.2VLAN的實現

    Vlan的分析,主要從其ioctl入手,一步步看其原始碼就能大致理解了,為了敘述方便,這裡首先給出我所理解的vlan實現框架,再去敘述其實現細節。

2.2.1Vlan的功能框圖

    如前面所述,Linux中VLAN是一種特殊的裝置,首先簡單看一下vconfig命令建立一個VLAN裝置

vconfig add eth0 10

VLAN裝置必須依賴於一個實際的宿主裝置,並制定一個vlan_id,這樣就建立出一個eth0.10裝置。建立好後,就可以和實際網路裝置一樣,用ifconfig命令配置它。

    它傳送/接收資料的流程大致如下圖所示:

通過vlan_dev傳送時,首先會呼叫它自己的驅動中的ndo_start_xmit()函式,就彷彿它是一個實際裝置一樣,而它的傳送函式會將skb重定向到real_dev,並利用real_dev重啟傳送流程,這是內部實現的,後面會講到,且對上層是透明的。

接收是有硬體中斷觸發的,所以一定是由real_dev的驅動接收到資料並打包成skb,若發現該資料是vlan的,則重定向skb->dev=vlan_dev,然後提交給上層。對上層而言,這也是透明的,就彷彿是vlan_dev收到了資料。注意vlan_dev的硬體地址必須和real_dev相同,這樣,發往vlan_dev的資料包才能被實際的硬體裝置接收到。

相關資料結構框圖如下。Vlan裝置的priv結構中有real_dev指標,同時實際裝置中的vlan_info資訊指明它所有的vlan裝置。

2.2.2Vlan裝置的建立

    前面講了,通過vconfig add命令可以建立一個vlan裝置,該命令實際上是對/proc/net/vlan/config檔案的ioctl操作,對映到核心中就是vlan.c中的vlan_ioctl_handler()函式,add命令最終呼叫register_vlan_device(*real_dev, vid)。

    首先申請了一個新的struct net_device結構作為vlan裝置,併為它分配一個struct vlan_dev_priv型的私有空間(vlan.h),並指明它的初始化函式為vlan_setup。該初始化函式設定該dev的flag為802.1q;並設定它的傳送queue為0,這一點對vlan的傳送流程很重要;設定其netdev_ops為一個通用的vlan_netdev_ops,它直接決定了vlan裝置的工作方式,後面會細講。

    然後修改vlan裝置的私有空間,指明它的宿主裝置及vid;並且相應地修改宿主裝置real_dev中的vlan_info資訊。

    最後把它註冊進核心中的netdevice連結串列中,從此它對上層協議棧而言,就彷彿是一個實際的裝置,和其它所有裝置有平等的地位,可以用ifconfig配置它,也可以把它加入bridge等。

    相關函式集中在vlan.c中,裡面還有其它一些ioctl功能函式;另外vlan_core.c中主要是和vlan相關的核心操作;vlan_dev.c中主要是vlan裝置相關的程式碼。

2.2.3Vlan裝置的傳送流程

    Vlan裝置對上層協議棧而言,和實際裝置時平等的,所以它也會參與路由選擇,若vlan裝置被選中為出口裝置,那麼上層最終會呼叫dev_queue_xmit(vlan_dev)來傳送資料,參見1.3.2節的圖。上一節講了,vlan裝置的tx_queue被初始化為0,所以傳送流程會直接呼叫hard_dev_start_xmit()函式,該函式首先對skb作一系列檢查,包括vlan的檢查,然後呼叫skb->dev->ops->ndo_start_xmit()傳送。

    首先來看對vlan的檢查,參見dev.c中的hard_dev_start_xmit(skb)函式,其實很簡單,檢查skb中的vlan標誌,若有,則插入vlan_tag,並修改skb->proto=802.1q,最後去除skb中的vlan標誌。skb中為什麼會有vlan標誌,因為上層選擇vlan_dev後,根據它的priv_flags(見上一節)可知道它是一個vlan裝置,因此給它打上一個vlan標誌。

    然後來看vlan裝置的驅動中的傳送函式,有上一節知道,所有vlan裝置的netdev_ops都被初始化為vlan_netdev_ops,它的傳送函式為設定為vlan_dev_hard_start_xmit()(vlan_dev.c)。也很簡單,如下圖所示

2.2.4Vlan裝置的接收流程

    在1.3.3節講了網路裝置的接收流程,不管採用中斷方式,還是NAPI方式,最終都會準備好skb,並在一個核心執行緒中呼叫__netif_receive_skb(skb)函式,該函式檢查skb,包括vlan的檢查,然後把skb提交給上層。

若接收到的skb是802.1q協議的,即mac地址後面跟了0x8100,注意,網路卡接收的僅是bit流,這裡只能從bit流中的特定位元組來判斷它是否是vlan包。若是vlan包,則呼叫vlan_untag()函式,該函式讀出資料流中的vlan_id,並填寫入skb->vlan_tci中,然後刪除vlan_head,從而實現對上層的透明。注意這裡的skb->vlan_tci標誌僅是為了把該skb交給vlan_dev(見下面),而skb中的資料是透明的乙太網包。

    發現skb->vlan_tci置位,則執行vlan_do_receive(skb),該函式由skb->vlan_tci得到該skb包所要發往的vlan_dev,並且重定向skb->dev為該vlan_dev,最後消除skb中的vlan_tci標誌。

    這樣之後vlan_do_receive()返回,流程繼續回到netif_receive_skb()中,不過此時的skb已經是一個普通的資料包了(實現了對上層的透明),且它看起來就像是由vlan_dev接收的資料包。

2.2.5關於裝置重定向的總結

在傳送流程中,資料包由上層下發時(如IP經過路由後下發),首先是到了虛擬裝置(如這裡的VLAN,包括以後講的Bridge),這正是這種虛擬化技術所期望的對上層透明,要注意的是,此時資料包skb就已經準備好了,其中報文的MAC地址、IP地址就是這個虛擬裝置的地址,並且不再改變,這就實現了對外彷彿是實際存在的。

    然後在dev_hard_start_xmit(skb,dev)中,對skb進行檢查,發現是虛擬裝置的資料,則做相應操作(如vlan_untag(),其它的都安正常傳送流程走),接著就呼叫虛擬裝置的ops->ndo_start_xmit()函式,在這個函式中,進行裝置重定向。最後以real_dev重啟傳送流程(vlan是這樣的,因為real_dev可能被多個vlan_dev使用,必須重新進行鎖定等,而bridge則直接呼叫real_dev->ops->ndo_start_xmit(),因為它的埠從裝置僅為它所用)。

在接收流程中,資料包skb首先在real_dev中被接收,並通過_netif_receive_skb()提交給上層,正是在這個函式中,檢查資料包skb,若發現是發往虛擬裝置的,則重定向skb->dev,再提交上層,從而實現對上層透明。

3.Linux中VLAN的應用場景

3.1一般交換機中的VLAN

    Vlan最初的概念是應用與交換機中,並且由硬體來劃分vlan。最傳統的方法是基於port的vlan,即每個vlan虛擬網由一個vlan_id標示,並由一個vlan_mask來標示哪些port和它同處於一個vlan虛擬網,如下圖所示:

其中vid和vlan_mask都存放在裝置暫存器中,由硬體自動訪問識別。

    更簡化一點,上圖中packet中都可以不需要vid(即不需要vlan_head),硬體根據包是由哪個port收到的來索引VID_table,從而知道哪些port和它同處於一個vlan虛擬網。但對於有些應用,需要一個port同屬於多個vlan的,如下圖所示:

承載多個vlan的port稱為trunk口,它上面收發的資料包必須含有vlan_head,以識別該包是屬於哪個vlan的;只承載一個vlan的port稱為access口,若硬體支援,可以不需要vlan_head就能完成vlan功能。

3.2一個應用場景分析

    Linux中,Vlan裝置建立在宿主裝置的基礎上,即該物理埠應該是trunk口,如下圖所示:

由前面Linux中vlan的實現可知,發往vlan裝置的資料包都被打上vlan_head,而vlan裝置接收到的資料包都預設為有vlan_head,並將其去除。這是符合trunk口的定義的。

    總之Linux下的VLAN模型,是一套虛擬化的架構,它為了虛擬出vlan埠,做得比較臃腫。如果把上圖中下方的switch裝置用Linux來驅動,該怎麼模型化這個裝置,還要充分利用硬體的特性,實現高效的vlan。

相關文章