Linux 網絡設備驅動開發(一) —— linux內核網絡分層結構
Linux內核對網絡驅動程序使用統一的接口,并且對于網絡設備采用面向對象的思想設計。
Linux內核采用分層結構處理網絡數據包。分層結構與網絡協議的結構匹配,既能簡化數據包處理流程,又便于擴展和維護。
一、內核網絡結構
在Linux內核中,對網絡部分按照網絡協議層、網絡設備層、設備驅動功能層和網絡媒介層的分層體系設計。
網絡驅動功能層主要通過網絡驅動程序實現。
在Linux內核,所有的網絡設備都被抽象為一個接口處理,該接口提供了所有的網絡操作。
net_device結構表示網絡設備在內核中的情況,也就是網絡設備接口。網絡設備接口既包括軟件虛擬的網絡設備接口,如環路設備,也包括了網絡硬件設備,如以太網卡。
Linux內核有一個dev_base的全局指針,指向一個設備鏈表,包括了系統內的所有網絡設備。該設備鏈表每個節點是一個網絡設備。
在net_device結構中提供了許多供系統訪問和協議層調用的設備方法,包括初始化、打開關閉設備、數據包發送和接收等。
二、與網絡有關的數據結構
內核對網絡數據包的處理都是基于sk_buff結構的,該結構是內核網絡部分最重要的數據結構。
網絡協議棧中各層協議都可以通過對該結構的操作實現本層協議數據的添加或者刪除。使用sk_buff結構避免了網絡協議棧各層來回復制數據導致的效率低下。
sk_buff結構可以分為兩個部分,一部分是存儲數據包緩存,在圖中表示為PackertData,另一部分是由一組用于內核管理的指針組成。
sk_buff管理的指針最主要的是下面4個:
-
head指向數據緩沖(PackertData)的內核首地址;
-
data指向當前數據包的首地址;
-
tail指向當前數據包的尾地址;
-
end 指向數據緩沖的內核尾地址。
數據包的大小在內核網絡協議棧的處理過程中會發生改變,因此data和tail指針也會不斷變化,而head和tail指針是不會發生改變的。
對于一個TCP數據包為例,sk_buff還提供了幾個指針直接指向各層協議頭。mac指針指向數據的mac頭;nh指針指向網絡協議頭,一般是IP協議頭;h指向傳輸層協議頭,在本例中是TCP協議頭。
對各層設置指針的是方便了協議棧對數據包的處理。
三、net_device結構
Linux內核中網絡設備最重要的數據結構就是net_device結構了,它是網絡驅動程序最重要的部分。
net_device結構保存在include/linux/netdevices.h頭文件,理解該結構對理解網絡設備驅動有很大幫助。
內核中所有網絡設備的信息和操作都在net_device設備中,無論是注冊網絡設備,還是設置網絡設備參數,都用到該結構。
下面是主要數據成員。
-
設備名稱
-
總線參數
-
協議參數
-
鏈接層變量
-
接口標志
四、數據包接收流程
在Linux內核中,一個網絡數據包從網卡接收到用戶空間需要經過鏈路層、傳輸層和socket的處理,最終到達用戶空間。
以DM9000網卡為例,當網卡收到數據包以后,調用中斷處理函數 dm9000_interrupt(),該函數檢查中斷處理類型,如果是接收數據包中斷,則調用 dm9000_rx()函數接收數據包到內核空間。
dm9000_rx()函數收到數據包完成后,內核會繼續調用 netif_rx()函數,函數的作用是把網卡接收到數據提交給協議棧處理。
協議棧使用 net_rx_action()函數處理接收數據包隊列,該函數處理數據包后如果是 IP數據包則提交給ip_recv()函數處理。ip_recv()函數主要是檢查一個數據包IP頭的合法性,檢查通過后交給 ip_local_deliver()和 ip_local_deliver_finish()函數處理,之所以分開處理是因為內核中有防火墻相關的代碼需要動態加載到此處。
IP頭處理完畢后,以UDP數據包為例將交由 udp_recv()函數處理,與 ip_recv()函數類億,該函數檢查 UDP頭的合法性,然后交給 udp_queue_recv()函數處理,最后提交給 sock_queue_recv()函數處理。
數據包進入 socket部分的第一個函數是 skb_recv_datagram(),該函數從內核的 socket隊列取出數據包,交給 socket部分的 udp_recvmsg()函數,該函數負責處理UDP的數據,sock_recvmsg()處理提交給 sock_read()函數。
sock_read()函數讀取接收到的數據緩沖,把數據返回給 sys_read()系統調用。sys_read()函數調用最終把數據復制到用戶空間,供用戶使得。
五、數據包發送流程
以UDP數據包發送流程為例,在DM9000網卡上如何發送一個數據包。
當用戶空間的應用程序通過 socket函數 sento()發送一個UDP數據后,會調用內核空間的 sock_writev()函數,然后通過 sock_sendmsg()函數處理。
sock_sendmsg()函數調用 inet_sendmsg()函數處理,inet_sendmsg()函數會把要發送的數據交給傳輸層的 udp_sendmsg()函數處理。
udp_sendmsg()函數在數據前加入UDP頭,然后把數據交給 ip_build_xmit()函數處理,該函數根據 socket提供的目的 IP和端口信息構造IP頭,然后調用 output_maybe_reroute()函數處理。out_maybe_reroute()函數檢查數據包是否需要經過路由,最后交給 ip_output()函數寫入到發送隊列,寫入完成后由 ip_finish_output()函數處理后續工作。
鏈路層的 dev_queue_xmit()函數處理發送隊列,調用 DM9000網卡的發送數據包函數 dm9000_xmit()發送數據包,發送完畢后,調用 dm9000_xmit_done函數處理發送結果。