1.Linux網絡棧下兩層實現
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部分:
-
全局性的代碼,如netdev_init()是在系統啟動時初始化網絡環境的(注意並不是初始化具體的設備),register_netdevice()函數是添加\注冊網絡設備時調用的,它們中的一些細節直接關系到設備的工作過程,下一節針對具體模塊時分別講述;
-
發送框架相關的,由上層調用dev_queue_xmit()函數,經過一系列處理(包括鎖定設備、選擇隊列、vlan相關的處理等),最終調用設備的hard_start_xmit()函數,由它完成硬件的發送過程;
-
接收框架相關的,因為接收是一個被動過程,一般通過中斷來發起,但為了提高性能,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。