1.Linux網(wǎng)絡(luò)棧下兩層實(shí)現(xiàn)
1.1簡(jiǎn)介
VLAN是網(wǎng)絡(luò)棧的一個(gè)附加功能,且位于下兩層。首先來學(xué)習(xí)Linux中網(wǎng)絡(luò)棧下兩層的實(shí)現(xiàn),再去看如何把VLAN這個(gè)功能附加上去。下兩層涉及到具體的硬件設(shè)備,日趨完善的Linux內(nèi)核已經(jīng)做到了很好的代碼隔離,對(duì)網(wǎng)絡(luò)設(shè)備驅(qū)動(dòng)也是如此,如下圖所示:
這里要注意的是,Linux下的網(wǎng)絡(luò)設(shè)備net_dev并不一定都對(duì)應(yīng)實(shí)際的硬件設(shè)備,只要注冊(cè)一個(gè)struct net_device{}結(jié)構(gòu)體(netdevice.h)到內(nèi)核中,那么這個(gè)網(wǎng)絡(luò)設(shè)備就存在了。該結(jié)構(gòu)體很龐大,其中包含設(shè)備的協(xié)議地址(對(duì)于IP即IP地址),這樣它就能被網(wǎng)絡(luò)層識(shí)別,并參與路由系統(tǒng),最有名的當(dāng)數(shù)loopback設(shè)備。不同的設(shè)備(包括硬件和非硬件)的ops操作方法各不相同,由驅(qū)動(dòng)自己實(shí)現(xiàn)。一些通用性的、與設(shè)備無關(guān)的操作流程(如設(shè)備鎖定等)則被Linux提煉出來,我們稱為驅(qū)動(dòng)框架。
1.2代碼框架
就是對(duì)于上圖的擴(kuò)展,從代碼的角度看網(wǎng)絡(luò)棧的實(shí)現(xiàn)。這里主要是學(xué)習(xí)的過程,一方面算是賞析Linux優(yōu)美的代碼結(jié)構(gòu),另一方面只有了解這些,才能更好地寫網(wǎng)絡(luò)設(shè)備的驅(qū)動(dòng),或者做平臺(tái)移植。
與網(wǎng)絡(luò)相關(guān)的代碼主要在~/net,框架性的代碼在~/net/core中,另外很多結(jié)構(gòu)定義、宏、簡(jiǎn)單內(nèi)聯(lián)函數(shù)在~/include/net、~/include/linux中,具體設(shè)備的驅(qū)動(dòng)在~/driver/net中。代碼量很大,這里僅給出一些關(guān)鍵的代碼流程,且有些流程比較復(fù)雜,放到下一節(jié)描述。如下圖所示:
網(wǎng)絡(luò)層的代碼比較清晰,實(shí)際上還有一個(gè)forward流程(即路過主機(jī),傳向他處),這里沒畫出,其中NF_函數(shù)就是Netfilter框架的鉤子函數(shù)。這里以IP協(xié)議為例,發(fā)送流程的代碼大多在ip_input.c文件中,接收流程的代碼大多在ip_output.c文件中。其它網(wǎng)絡(luò)層協(xié)議如IPv6、X25等,流程大致相同。各種協(xié)議在發(fā)送流程的最后,都會(huì)主動(dòng)調(diào)用dev_queue_xmit()函數(shù);而設(shè)備接收到數(shù)據(jù)包后,會(huì)根據(jù)包的類型,傳送給相應(yīng)的協(xié)議函數(shù),如ip_rcv(),當(dāng)然這里的實(shí)現(xiàn)還是比較復(fù)雜的,設(shè)計(jì)到一些全局的數(shù)據(jù)結(jié)構(gòu),不是重點(diǎn),沒看。
驅(qū)動(dòng)框架的代碼基本都在~/net/core/dev.c中。其中的代碼分為3部分:
-
全局性的代碼,如netdev_init()是在系統(tǒng)啟動(dòng)時(shí)初始化網(wǎng)絡(luò)環(huán)境的(注意并不是初始化具體的設(shè)備),register_netdevice()函數(shù)是添加\注冊(cè)網(wǎng)絡(luò)設(shè)備時(shí)調(diào)用的,它們中的一些細(xì)節(jié)直接關(guān)系到設(shè)備的工作過程,下一節(jié)針對(duì)具體模塊時(shí)分別講述;
-
發(fā)送框架相關(guān)的,由上層調(diào)用dev_queue_xmit()函數(shù),經(jīng)過一系列處理(包括鎖定設(shè)備、選擇隊(duì)列、vlan相關(guān)的處理等),最終調(diào)用設(shè)備的hard_start_xmit()函數(shù),由它完成硬件的發(fā)送過程;
-
接收框架相關(guān)的,因?yàn)榻邮帐且粋€(gè)被動(dòng)過程,一般通過中斷來發(fā)起,但為了提高性能,Linux中的中斷處理一般分為兩部分(時(shí)間緊急的和時(shí)間不緊急的),即典型的UH+BH模型;另外近年來,人們發(fā)現(xiàn)大數(shù)據(jù)量時(shí),連續(xù)的中斷有損性能,現(xiàn)在越來越多的驅(qū)動(dòng)都改用NPI接收模型,將BH部分直接在驅(qū)動(dòng)中實(shí)現(xiàn),比較復(fù)雜。不過不管通過什么流程接收到數(shù)據(jù)包后(封裝成skb),都會(huì)把它交給netif_receive_skb()函數(shù),該函數(shù)對(duì)數(shù)據(jù)包進(jìn)行處理(包括vlan相關(guān)的),最終通過deliver_skb(ptype)交付給相應(yīng)的上層。
設(shè)備驅(qū)動(dòng)的代碼(即netdev->ops所指向的函數(shù)),各個(gè)設(shè)備不同,其中最最重要的有5個(gè):dev_open(),hard_start_xmit(),tx_timeout(),interrupt(),poll()。另外其他一些函數(shù)如設(shè)置mtu,更改mac等,則根據(jù)具體設(shè)備的功能選擇實(shí)現(xiàn)。
1.3代碼細(xì)節(jié)
以tealtek的rtl8169驅(qū)動(dòng)為例,首先介紹一般的設(shè)備驅(qū)動(dòng)中實(shí)現(xiàn)那些功能,以及這些功能是如何組合起來的。然后分別從發(fā)送、接收流程出發(fā),分析驅(qū)動(dòng)框架中的代碼是如何支持這些功能的實(shí)現(xiàn)的。
1.3.1設(shè)備驅(qū)動(dòng)的功能組合
前面講到了,設(shè)備驅(qū)動(dòng)中最最重要的5個(gè)函數(shù),這些函數(shù)有機(jī)組合在一起,實(shí)現(xiàn)了可靠的設(shè)備功能,如下圖所示:
打開函數(shù)dev_open()中,首先初始化設(shè)備的私有空間。每個(gè)網(wǎng)絡(luò)設(shè)備有一個(gè)net_device結(jié)構(gòu)體,同時(shí)還有一個(gè)私有結(jié)構(gòu),由net_device.priv指針指向。在module_init()函數(shù)中,一般會(huì)調(diào)用netdev_alloc(sizeof(priv),name,setup_func)函數(shù),該函數(shù)指明設(shè)備的唯一名稱及一個(gè)初始化函數(shù)(對(duì)于以太網(wǎng),一般用ethe_setup()),同時(shí)申請(qǐng)net_device結(jié)構(gòu)和private結(jié)構(gòu)的空間。Private結(jié)構(gòu)由不同的設(shè)備自己決定,在dev_open()中,應(yīng)初始化之。
發(fā)送函數(shù)hard_start_xmit()中,首先利用硬件發(fā)送數(shù)據(jù),注意這里僅是把數(shù)據(jù)寫入設(shè)備的發(fā)送緩存中(或有些設(shè)備直接是利用dma的),然后寫相應(yīng)的寄存器,通知硬件開始發(fā)送,之后該函數(shù)就正確返回了,然后硬件到底有沒有正確發(fā)送數(shù)據(jù)還不知道。
中斷函數(shù)interrupt(),是在dev_open()時(shí)申請(qǐng)的,并根據(jù)實(shí)際硬件的中斷號(hào),與某個(gè)中斷線聯(lián)系并注冊(cè)進(jìn)內(nèi)核。當(dāng)該中斷線上有中斷時(shí),CPU跳轉(zhuǎn)到該函數(shù)執(zhí)行。雖然是一根中斷線,但設(shè)備中斷的類型卻不一樣,這有具體設(shè)備決定,一般可通過讀取硬件的狀態(tài)寄存器獲悉。若是接收中斷,則以某種方式去調(diào)用poll()函數(shù),把數(shù)據(jù)包傳遞給上層。
1.3.2發(fā)送流程細(xì)節(jié)
首先需要知道,Linux為每個(gè)網(wǎng)絡(luò)設(shè)備準(zhǔn)備了發(fā)送/接收隊(duì)列,alloc_netdev(sizeof(priv),name,setup_func)實(shí)際上被定義為alloc_netdev_mqs(x,x,x,1,1)(netdevice.h),即默認(rèn)為每個(gè)設(shè)備分配一個(gè)發(fā)送隊(duì)列和一個(gè)接收隊(duì)列,隊(duì)列結(jié)構(gòu)為struct netdev_queue,每個(gè)隊(duì)列中有個(gè)重要的結(jié)構(gòu)struct Qdisc。該結(jié)構(gòu)的功能主要是提供多進(jìn)程使用同一個(gè)設(shè)備時(shí)的鎖定功能,在SMP架構(gòu)(或多核架構(gòu))的機(jī)器中,這種鎖定功能的實(shí)現(xiàn)變得尤為復(fù)雜,這也是現(xiàn)在內(nèi)核設(shè)計(jì)的關(guān)鍵和難點(diǎn),暫且不管。
現(xiàn)在來看網(wǎng)絡(luò)設(shè)備的發(fā)送流程,如下圖所示:
對(duì)于沒有隊(duì)列的設(shè)備(主要是一些虛擬設(shè)備,如loopback),處理比較簡(jiǎn)單。對(duì)于一般設(shè)備,主要對(duì)它進(jìn)行一些復(fù)雜的鎖定功能,而且函數(shù)調(diào)用出錯(cuò)時(shí)需把該skb重新放回隊(duì)列中。最終都會(huì)調(diào)用dev_hard_start_xmit()函數(shù),該函數(shù)是發(fā)送流程中的關(guān)鍵,它是設(shè)備無關(guān)的,主要會(huì)檢查并處理skb中的各種特性,一些新功能(如vlan)的實(shí)現(xiàn)都在這個(gè)函數(shù)中完成。該函數(shù)最終調(diào)用設(shè)備驅(qū)動(dòng)中的設(shè)備相關(guān)的發(fā)送函數(shù)。
前面講的出錯(cuò),僅是函數(shù)調(diào)用的出錯(cuò)。正如前面講述的,即使函數(shù)調(diào)用正確返回了,也并不代表硬件成功把數(shù)據(jù)包發(fā)送出去了。所以一般網(wǎng)卡設(shè)備都會(huì)在設(shè)備成功發(fā)送數(shù)據(jù)時(shí)產(chǎn)生中斷,并在相應(yīng)的寄存器中顯示這是個(gè)TxOK中斷。
Linux的網(wǎng)絡(luò)設(shè)備驅(qū)動(dòng)框架中,很好地利用了這點(diǎn)。每個(gè)設(shè)備的net_device結(jié)構(gòu)體中都有一個(gè)watchdog_timer,在module_init()中注冊(cè)該模塊時(shí),register_netdev()函數(shù)中會(huì)初始化該定時(shí)器,并注冊(cè)其func為dev_watchdog(),該函數(shù)的內(nèi)容就是運(yùn)行設(shè)備驅(qū)動(dòng)中實(shí)現(xiàn)的tx_timeout()。另外內(nèi)核提供打開、消去該定時(shí)器的函數(shù),供驅(qū)動(dòng)程序在相應(yīng)的位置使用。
1.3.3接收流程細(xì)節(jié)
接收過程是被動(dòng)觸發(fā)的,一般由硬件的中斷引發(fā)。Linux在處理這種IO時(shí)一般采用典型的UH+BH模型,即把一些實(shí)時(shí)性高的操作(如把設(shè)備緩存中的數(shù)據(jù)copy到內(nèi)核中,以便設(shè)備可接收其它數(shù)據(jù))發(fā)在中斷處理函數(shù)中完成,而把實(shí)時(shí)性要求不高的操作(如處理數(shù)據(jù))發(fā)在稍后的時(shí)間里完成(一般是另開一個(gè)線程)。
在經(jīng)典的網(wǎng)絡(luò)設(shè)備驅(qū)動(dòng)中也經(jīng)常使用這種模型,首先介紹兩個(gè)數(shù)據(jù)結(jié)構(gòu),分別是struct napi_struct{},里面主要有擁有該數(shù)據(jù)結(jié)構(gòu)的設(shè)備的索引dev,和一個(gè)函數(shù)指針poll_func;另一個(gè)是struct softnet_data{},該結(jié)構(gòu)中主要維護(hù)一個(gè)napi_struct的隊(duì)列。
內(nèi)核準(zhǔn)備了一個(gè)全局的struct softnet_data sd結(jié)構(gòu)(實(shí)際上是為每個(gè)cpu準(zhǔn)備了一個(gè)),另外準(zhǔn)備了一個(gè)通用的poll_func函數(shù)process_backlog()。好了,現(xiàn)在來看驅(qū)動(dòng)中的BH部分,如下圖所示:
設(shè)備驅(qū)動(dòng)中的interrupt函數(shù),檢查到接收了新數(shù)據(jù)包時(shí),就準(zhǔn)備新的skb,并把數(shù)據(jù)copy到skb中,然后調(diào)用驅(qū)動(dòng)框架中的netif_rx(skb)函數(shù);該函數(shù)主要調(diào)用enqueue_to_backlog();該函數(shù)檢查是否初次進(jìn)入,是則準(zhǔn)備一個(gè)新的napi_struct結(jié)構(gòu),其poll_func定義為通用函數(shù)process_backlog(),并調(diào)用__napi_schedule()函數(shù),把準(zhǔn)備好的結(jié)構(gòu)體放入sd的poll_list中,然后調(diào)用__raise_softirq_irqoff(RxIRQ)打開軟中斷,且以后每次進(jìn)入都把skb壓入backlog的queue中。
這里要檢索另一個(gè)函數(shù)netdev_init()(在系統(tǒng)啟動(dòng)時(shí)調(diào)用的),上述講的sd結(jié)構(gòu)就是在這個(gè)函數(shù)中分配的,另外該函數(shù)還注冊(cè)了軟中斷函數(shù)net_rx_action(),軟中斷的原理沒去看,應(yīng)該就是利用Linux內(nèi)核的tasklet機(jī)制實(shí)現(xiàn)的。__raise_softirq_irqoff(RxIRQ)函數(shù)講軟中斷掩碼mask中的RxIRQ置位,這樣,BH部分就完成了,此時(shí)的sd結(jié)構(gòu)如下圖:
之后就是UH部分了,即系統(tǒng)在之后的某個(gè)時(shí)間,啟動(dòng)軟中斷線程,執(zhí)行net_rx_action()函數(shù),該函數(shù)遍歷softnet_data sd結(jié)構(gòu)中的poll_list,并執(zhí)行每個(gè)napi_struct->poll_func()函數(shù),由前面的敘述可知,這里的poll_func()函數(shù)都是process_backlog(),該函數(shù)采用while循環(huán)取下dev上的skb(因?yàn)樵谲浿袛鄨?zhí)行前可能發(fā)生了多次接收中斷),并調(diào)用__netif_receive_skb(skb)函數(shù),講skb傳遞給上層協(xié)議。當(dāng)接收到一定數(shù)量的包后,就認(rèn)為本次數(shù)據(jù)包接收完畢了,并把該napi_struct結(jié)構(gòu)從sd中刪除,如下圖所示:
這就是傳統(tǒng)的中斷方式,可以參見RTL8012的驅(qū)動(dòng)~/driver/net/ethernet/realtek/apt.c,就是利用的該方法,它的優(yōu)點(diǎn)是,需要驅(qū)動(dòng)程序 做的非常少,僅需準(zhǔn)備好skb,調(diào)用net_rx(skb)即可,其它都有驅(qū)動(dòng)框架完成。缺點(diǎn)是,欠靈活,且數(shù)據(jù)量大時(shí),會(huì)不停的中斷,影響系統(tǒng)性能。
現(xiàn)在很多網(wǎng)絡(luò)設(shè)備驅(qū)動(dòng)已不再使用這種結(jié)構(gòu),而是采用NAPI結(jié)構(gòu),它完全摒棄了內(nèi)核驅(qū)動(dòng)框架中的UH+BH模型,并且不再用中斷方式,而是在驅(qū)動(dòng)內(nèi)部使用輪詢方式。
與中斷方式最大的不同在于,每次發(fā)生接收中斷時(shí),關(guān)閉接收中斷,啟動(dòng)軟中斷,在poll函數(shù)break前,重新打開接收中斷,一遍下一輪的數(shù)據(jù)接收。其次,驅(qū)動(dòng)程序自己定義napi_struct結(jié)構(gòu)和poll_func函數(shù)。最后,poll_func函數(shù)和前面講的在結(jié)構(gòu)上差不多,都是while循環(huán),但它要自己準(zhǔn)備skb(因?yàn)?span style="COLOR: red">它之前沒有中斷程序來準(zhǔn)備skb),并且直接上傳該skb,一般不會(huì)實(shí)現(xiàn)隊(duì)列queue(因?yàn)?span style="COLOR: red">它之后沒有其它線程再去處理queue了)。
這就是所謂的NAPI方式,它避免了多次的硬件中斷,一定程度上提高系統(tǒng)系能。但驅(qū)動(dòng)程序也因此更加復(fù)雜,并且poll_func()函數(shù)中要做的事太多(摒棄了UH+BH模型),在數(shù)據(jù)量很大時(shí),會(huì)出現(xiàn)丟包的現(xiàn)象(這好像是Linux的一個(gè)bug)。Rtl8169就是采用的這種方式,參見~/driver/net/Ethernet/realtek/r8169.c。
2.Linux中VLAN的實(shí)現(xiàn)
2.1Linux網(wǎng)絡(luò)中的namespace
這個(gè)概念我不是了解的很清楚,不過可以簡(jiǎn)單地把它看成是一種分類,目前所了解的網(wǎng)絡(luò)設(shè)備有3類:傳統(tǒng)的網(wǎng)絡(luò)設(shè)備,它們不需要依賴于其它設(shè)備而獨(dú)自存在,如eth0、loopback等;VLAN網(wǎng)絡(luò)設(shè)備,它需要依賴于一個(gè)宿主設(shè)備,若宿主設(shè)備沒了,它是不能工作的;Bridge網(wǎng)絡(luò)設(shè)備,它也是虛擬的,它依賴于從設(shè)備。
與此相關(guān)的結(jié)構(gòu)有struct net{},相關(guān)文件包括namespace.h、namespace.c等。這3類網(wǎng)絡(luò)設(shè)備都是以module的形式被加入內(nèi)核中,它們可以看成是網(wǎng)絡(luò)子系統(tǒng)的頂層module,下面實(shí)現(xiàn)的驅(qū)動(dòng)模塊等都依賴于它們。這3個(gè)頂層模塊加載時(shí)分別執(zhí)行的init函數(shù)為:netdev_init(),@dev.c;vlan_proto_init(),@vlan.c;br_init(),@br_device.c。
這3個(gè)函數(shù)中有自己特有的部分,如netdev_init中分配softnet_data等,它們也有相似的部分,如
這里要重點(diǎn)看的是ioctl_set函數(shù),這涉及到Linux下網(wǎng)絡(luò)設(shè)備的ioctl操作。在Linux中,所有網(wǎng)絡(luò)設(shè)備的ioctl操作都被抽象成對(duì)/proc/net/下的文件的操作,最終調(diào)用內(nèi)核中的sock_ioctl函數(shù),該函數(shù)結(jié)構(gòu)如下:
其中各個(gè)hook函數(shù)就這里init()時(shí)利用ioctl_set_func()設(shè)置的。這種設(shè)計(jì)架構(gòu)大大方便了用戶空間對(duì)各類虛擬設(shè)備(如vlan,br等)的操作,如目前Linux下vlan的操作命令vconfig就是打開/proc/net/vlan/config文件,然后對(duì)它進(jìn)行ioctl操作,詳細(xì)參見vconfig的源碼(非常簡(jiǎn)單)。Br也是差不多,以后學(xué)習(xí)br時(shí)再細(xì)看。
注意:這里關(guān)于namespace的概念可能錯(cuò)了,現(xiàn)在先不看,后面講到協(xié)議族時(shí),再一起看看整個(gè)網(wǎng)絡(luò)棧頂層的實(shí)現(xiàn)框架,這里先關(guān)注底層的設(shè)備。另注:這里的vlan_ioctl的概念可能是錯(cuò)誤的,它實(shí)際上是sock_ioctl的特殊情況,以后再看吧,包括應(yīng)用層如何調(diào)用到它。
2.2VLAN的實(shí)現(xiàn)
Vlan的分析,主要從其ioctl入手,一步步看其源碼就能大致理解了,為了敘述方便,這里首先給出我所理解的vlan實(shí)現(xiàn)框架,再去敘述其實(shí)現(xiàn)細(xì)節(jié)。
2.2.1Vlan的功能框圖
如前面所述,Linux中VLAN是一種特殊的設(shè)備,首先簡(jiǎn)單看一下vconfig命令創(chuàng)建一個(gè)VLAN設(shè)備
vconfig add eth0 10
VLAN設(shè)備必須依賴于一個(gè)實(shí)際的宿主設(shè)備,并制定一個(gè)vlan_id,這樣就創(chuàng)建出一個(gè)eth0.10設(shè)備。創(chuàng)建好后,就可以和實(shí)際網(wǎng)絡(luò)設(shè)備一樣,用ifconfig命令配置它。
它發(fā)送/接收數(shù)據(jù)的流程大致如下圖所示:
通過vlan_dev發(fā)送時(shí),首先會(huì)調(diào)用它自己的驅(qū)動(dòng)中的ndo_start_xmit()函數(shù),就仿佛它是一個(gè)實(shí)際設(shè)備一樣,而它的發(fā)送函數(shù)會(huì)將skb重定向到real_dev,并利用real_dev重啟發(fā)送流程,這是內(nèi)部實(shí)現(xiàn)的,后面會(huì)講到,且對(duì)上層是透明的。
接收是有硬件中斷觸發(fā)的,所以一定是由real_dev的驅(qū)動(dòng)接收到數(shù)據(jù)并打包成skb,若發(fā)現(xiàn)該數(shù)據(jù)是vlan的,則重定向skb->dev=vlan_dev,然后提交給上層。對(duì)上層而言,這也是透明的,就仿佛是vlan_dev收到了數(shù)據(jù)。注意vlan_dev的硬件地址必須和real_dev相同,這樣,發(fā)往vlan_dev的數(shù)據(jù)包才能被實(shí)際的硬件設(shè)備接收到。
相關(guān)數(shù)據(jù)結(jié)構(gòu)框圖如下。Vlan設(shè)備的priv結(jié)構(gòu)中有real_dev指針,同時(shí)實(shí)際設(shè)備中的vlan_info信息指明它所有的vlan設(shè)備。
2.2.2Vlan設(shè)備的創(chuàng)建
前面講了,通過vconfig add命令可以創(chuàng)建一個(gè)vlan設(shè)備,該命令實(shí)際上是對(duì)/proc/net/vlan/config文件的ioctl操作,映射到內(nèi)核中就是vlan.c中的vlan_ioctl_handler()函數(shù),add命令最終調(diào)用register_vlan_device(*real_dev, vid)。
首先申請(qǐng)了一個(gè)新的struct net_device結(jié)構(gòu)作為vlan設(shè)備,并為它分配一個(gè)struct vlan_dev_priv型的私有空間(vlan.h),并指明它的初始化函數(shù)為vlan_setup。該初始化函數(shù)設(shè)置該dev的flag為802.1q;并設(shè)置它的發(fā)送queue為0,這一點(diǎn)對(duì)vlan的發(fā)送流程很重要;設(shè)置其netdev_ops為一個(gè)通用的vlan_netdev_ops,它直接決定了vlan設(shè)備的工作方式,后面會(huì)細(xì)講。
然后修改vlan設(shè)備的私有空間,指明它的宿主設(shè)備及vid;并且相應(yīng)地修改宿主設(shè)備real_dev中的vlan_info信息。
最后把它注冊(cè)進(jìn)內(nèi)核中的netdevice鏈表中,從此它對(duì)上層協(xié)議棧而言,就仿佛是一個(gè)實(shí)際的設(shè)備,和其它所有設(shè)備有平等的地位,可以用ifconfig配置它,也可以把它加入bridge等。
相關(guān)函數(shù)集中在vlan.c中,里面還有其它一些ioctl功能函數(shù);另外vlan_core.c中主要是和vlan相關(guān)的核心操作;vlan_dev.c中主要是vlan設(shè)備相關(guān)的代碼。
2.2.3Vlan設(shè)備的發(fā)送流程
Vlan設(shè)備對(duì)上層協(xié)議棧而言,和實(shí)際設(shè)備時(shí)平等的,所以它也會(huì)參與路由選擇,若vlan設(shè)備被選中為出口設(shè)備,那么上層最終會(huì)調(diào)用dev_queue_xmit(vlan_dev)來發(fā)送數(shù)據(jù),參見1.3.2節(jié)的圖。上一節(jié)講了,vlan設(shè)備的tx_queue被初始化為0,所以發(fā)送流程會(huì)直接調(diào)用hard_dev_start_xmit()函數(shù),該函數(shù)首先對(duì)skb作一系列檢查,包括vlan的檢查,然后調(diào)用skb->dev->ops->ndo_start_xmit()發(fā)送。
首先來看對(duì)vlan的檢查,參見dev.c中的hard_dev_start_xmit(skb)函數(shù),其實(shí)很簡(jiǎn)單,檢查skb中的vlan標(biāo)志,若有,則插入vlan_tag,并修改skb->proto=802.1q,最后去除skb中的vlan標(biāo)志。skb中為什么會(huì)有vlan標(biāo)志,因?yàn)樯蠈舆x擇vlan_dev后,根據(jù)它的priv_flags(見上一節(jié))可知道它是一個(gè)vlan設(shè)備,因此給它打上一個(gè)vlan標(biāo)志。
然后來看vlan設(shè)備的驅(qū)動(dòng)中的發(fā)送函數(shù),有上一節(jié)知道,所有vlan設(shè)備的netdev_ops都被初始化為vlan_netdev_ops,它的發(fā)送函數(shù)為設(shè)置為vlan_dev_hard_start_xmit()(vlan_dev.c)。也很簡(jiǎn)單,如下圖所示
2.2.4Vlan設(shè)備的接收流程
在1.3.3節(jié)講了網(wǎng)絡(luò)設(shè)備的接收流程,不管采用中斷方式,還是NAPI方式,最終都會(huì)準(zhǔn)備好skb,并在一個(gè)內(nèi)核線程中調(diào)用__netif_receive_skb(skb)函數(shù),該函數(shù)檢查skb,包括vlan的檢查,然后把skb提交給上層。
若接收到的skb是802.1q協(xié)議的,即mac地址后面跟了0x8100,注意,網(wǎng)卡接收的僅是bit流,這里只能從bit流中的特定字節(jié)來判斷它是否是vlan包。若是vlan包,則調(diào)用vlan_untag()函數(shù),該函數(shù)讀出數(shù)據(jù)流中的vlan_id,并填寫入skb->vlan_tci中,然后刪除vlan_head,從而實(shí)現(xiàn)對(duì)上層的透明。注意這里的skb->vlan_tci標(biāo)志僅是為了把該skb交給vlan_dev(見下面),而skb中的數(shù)據(jù)是透明的以太網(wǎng)包。
發(fā)現(xiàn)skb->vlan_tci置位,則執(zhí)行vlan_do_receive(skb),該函數(shù)由skb->vlan_tci得到該skb包所要發(fā)往的vlan_dev,并且重定向skb->dev為該vlan_dev,最后消除skb中的vlan_tci標(biāo)志。
這樣之后vlan_do_receive()返回,流程繼續(xù)回到netif_receive_skb()中,不過此時(shí)的skb已經(jīng)是一個(gè)普通的數(shù)據(jù)包了(實(shí)現(xiàn)了對(duì)上層的透明),且它看起來就像是由vlan_dev接收的數(shù)據(jù)包。
2.2.5關(guān)于設(shè)備重定向的總結(jié)
在發(fā)送流程中,數(shù)據(jù)包由上層下發(fā)時(shí)(如IP經(jīng)過路由后下發(fā)),首先是到了虛擬設(shè)備(如這里的VLAN,包括以后講的Bridge),這正是這種虛擬化技術(shù)所期望的對(duì)上層透明,要注意的是,此時(shí)數(shù)據(jù)包skb就已經(jīng)準(zhǔn)備好了,其中報(bào)文的MAC地址、IP地址就是這個(gè)虛擬設(shè)備的地址,并且不再改變,這就實(shí)現(xiàn)了對(duì)外仿佛是實(shí)際存在的。
然后在dev_hard_start_xmit(skb,dev)中,對(duì)skb進(jìn)行檢查,發(fā)現(xiàn)是虛擬設(shè)備的數(shù)據(jù),則做相應(yīng)操作(如vlan_untag(),其它的都安正常發(fā)送流程走),接著就調(diào)用虛擬設(shè)備的ops->ndo_start_xmit()函數(shù),在這個(gè)函數(shù)中,進(jìn)行設(shè)備重定向。最后以real_dev重啟發(fā)送流程(vlan是這樣的,因?yàn)閞eal_dev可能被多個(gè)vlan_dev使用,必須重新進(jìn)行鎖定等,而bridge則直接調(diào)用real_dev->ops->ndo_start_xmit(),因?yàn)樗亩丝趶脑O(shè)備僅為它所用)。
在接收流程中,數(shù)據(jù)包skb首先在real_dev中被接收,并通過_netif_receive_skb()提交給上層,正是在這個(gè)函數(shù)中,檢查數(shù)據(jù)包skb,若發(fā)現(xiàn)是發(fā)往虛擬設(shè)備的,則重定向skb->dev,再提交上層,從而實(shí)現(xiàn)對(duì)上層透明。
3.Linux中VLAN的應(yīng)用場(chǎng)景
3.1一般交換機(jī)中的VLAN
Vlan最初的概念是應(yīng)用與交換機(jī)中,并且由硬件來劃分vlan。最傳統(tǒng)的方法是基于port的vlan,即每個(gè)vlan虛擬網(wǎng)由一個(gè)vlan_id標(biāo)示,并由一個(gè)vlan_mask來標(biāo)示哪些port和它同處于一個(gè)vlan虛擬網(wǎng),如下圖所示:
其中vid和vlan_mask都存放在設(shè)備寄存器中,由硬件自動(dòng)訪問識(shí)別。
更簡(jiǎn)化一點(diǎn),上圖中packet中都可以不需要vid(即不需要vlan_head),硬件根據(jù)包是由哪個(gè)port收到的來索引VID_table,從而知道哪些port和它同處于一個(gè)vlan虛擬網(wǎng)。但對(duì)于有些應(yīng)用,需要一個(gè)port同屬于多個(gè)vlan的,如下圖所示:
承載多個(gè)vlan的port稱為trunk口,它上面收發(fā)的數(shù)據(jù)包必須含有vlan_head,以識(shí)別該包是屬于哪個(gè)vlan的;只承載一個(gè)vlan的port稱為access口,若硬件支持,可以不需要vlan_head就能完成vlan功能。
3.2一個(gè)應(yīng)用場(chǎng)景分析
Linux中,Vlan設(shè)備建立在宿主設(shè)備的基礎(chǔ)上,即該物理端口應(yīng)該是trunk口,如下圖所示:
由前面Linux中vlan的實(shí)現(xiàn)可知,發(fā)往vlan設(shè)備的數(shù)據(jù)包都被打上vlan_head,而vlan設(shè)備接收到的數(shù)據(jù)包都默認(rèn)為有vlan_head,并將其去除。這是符合trunk口的定義的。
總之Linux下的VLAN模型,是一套虛擬化的架構(gòu),它為了虛擬出vlan端口,做得比較臃腫。如果把上圖中下方的switch設(shè)備用Linux來驅(qū)動(dòng),該怎么模型化這個(gè)設(shè)備,還要充分利用硬件的特性,實(shí)現(xiàn)高效的vlan。