1. 程式人生 > >Linux 網路裝置驅動開發(三) —— 網路裝置驅動基本原理和框架

Linux 網路裝置驅動開發(三) —— 網路裝置驅動基本原理和框架

一、協議棧層次對比

二、Linux網路子系統

    Linux網路子系統的頂部是系統呼叫介面層。它為使用者空間提供的應用程式提供了一種訪問核心網路子系統的方法(socket)。位於其下面是一個協議無關層,它提供一種通用的方法來使用傳輸層協議。然後是具體協議的實現,在Linux中包括核心的協議TCP,UDP,當然還有IP。然後是裝置無關層,它提供了協議與裝置驅動通訊的通用介面,最下面是裝置的驅動程式。



    裝置無關介面將協議與各種網路驅動連線在一起,這一層提供一組通用函式供底層網路裝置驅動使用,讓它們可以對高層協議棧進行操作。需要從協議層向裝置發生資料,需要呼叫dev_queue_xmit函式,這個函式對資料進行列隊,然後交由底層驅動程式的hard_start_xmit方法最終完成傳輸。接收通常是使用netif_rx執行的。當底層裝置程式接收到一個報文(發生中斷)時,就會呼叫netif_rx將資料上傳至裝置無關層。


三、裝置無關層到驅動層的體系結構

下圖為裝置無關層到驅動層的體系結構


1)、網路協議介面層向網路層協議提供提供統一的資料包收發介面,不論上層協議為ARP還是IP,都通過dev_queue_xmit()函式傳送資料,並通過netif_rx()函式接受資料。這一層的存在使得上層協議獨立於具體的裝置。
2)、網路裝置介面層向協議介面層提供統一的用於描述具體網路裝置屬性和操作的結構體net_device,該結構體是裝置驅動功能層中各函式的容器。實際上,網路裝置介面層從巨集觀上規劃了具體操作硬體的裝置驅動功能層的結構。
3)、裝置驅動功能層各函式是網路裝置介面層net_device資料結構的具體成員,是驅使網路裝置硬體完成相應動作的程式,他通過hard_start_xmit()函式啟動傳送操作,並通過網路裝置上的中斷觸發接受操作。

4)、網路裝置與媒介層是完成資料包傳送和接受的物理實體,包括網路介面卡和具體的傳輸媒介,網路介面卡被驅動功能層中的函式物理上驅動。對於Linux系統而言,網路裝置和媒介都可以是虛擬的。

1、網路協議介面層:

這裡主要進行資料包的收發,使用函式原型為:

dev_queue_xmit(struct sk_buff *skb);int netif_rx(struct sk_buff *skb);  
這裡使用了一個skb_buff結構體,定義於include/linux/skbuff.h中,它的含義為“套接字緩衝區”,用於在Linux網路子系統各層間傳輸資料。他是一個雙向連結串列,在老的核心中會有一個list域指向sk_buff_head也就是連結串列頭,但是在我研究的linux2.6.30.4核心中已經不存在了,如下圖:

sk_buff中重要的資料成員

struct device *dev;正在處理該包的裝置

__u32 sadd;r//IP元地址

__u32 daddr;//IP目的地址

__u32 raddr;//IP路由器地址

unsigned char *head;//分配空間的開始

unsigned char *data;//有效資料的開始

unsigned char *tail;//有效資料的結束

unsigned char *end;//分配空間的結束

unsigned long len;//有效資料的長度


sk_buff操作
a -- 分配:分配一個sk_buff結構,供協議棧程式碼使用

struct sk_buff *alloc_skb(unsigned int len, int priority);
struct sk_buff *dev_alloc_skb(unsigned int len);  

  分配一個緩衝區。alloc_skb函式分配一個緩衝區並初始化skb->data和skb->tail為skb->head。引數len為資料緩衝區的空間大小,通常以L1_CACHE_BYTES位元組(對ARM為32)對齊,引數priority為記憶體分配的優先順序。dev_alloc_skb()函式以GFP_ATOMIC優先順序進行skb的分配。

b -- 釋放:

void kfree_skb(struct sk_buff *skb);
void dev_kfree_skb(struct sk_buff *skb); 

Linux核心內部使用kfree_skb()函式,而網路裝置驅動程式中則最好使用dev_kfree_skb()。

sk_buff中比較重要的成員是指向資料包中資料的指標,如下圖所示:

用於定址資料包中資料的指標,head指向已分配空間開頭,data指向有效的octet開頭,tail指向有效的octet結尾,而end指向tail可以到達的最大地址。如果不這樣做而分配一個大小固定的緩衝區,如果buffer不夠用,則要申請一個更大的buffer,拷貝進去再增加,這樣降低了效能。

3)變更

unsigned char *skb_put(struct sk_buff *skb, int len);將taill指標向後移動len長度,並返回tail移動之前的值。用於向skb有效資料區域末尾新增資料。
unsigned char *skb_push(struct sk_buff *skb, int len);將data指標向前移動len長度。並返回移動之後的值。用於向skb有效資料區域前端新增資料(包頭)。
unsigned char *skb_pull(struct sk_buff *skb, int len);
void skb_reserve(struct sk_buff ×skb, int len); 
下圖分別對應了這四個函式,看了這張圖應該對這4個函式的作用瞭然於胸。

2、網路裝置介面層:

網路裝置介面層的主要功能是為千變萬化的網路裝置定義了統一,抽象的資料結構net_device結構體,以不變應萬變,實現多種硬體在軟體層次上的統一。

    每一個網路裝置都由struct net_device來描述,該結構可使用如下核心函式進行動態分配

struct net_device *alloc_netdev(int sizeof_priv, const char *mask, void(*setup)(struct net_device *))

sizeof_priv是私有資料區大小;mask是裝置名,setup是初始化函式,在註冊該裝置時,該函式被呼叫。也就是net_deivce的init成員。

struct net_device *alloc_etherdev(intsizeof_priv)
這個函式和上面的函式不同之處在於核心知道會將該裝置做一個乙太網裝置看待並做一些相關的初始化。

net_device結構可分為全域性成員、硬體相關成員、介面相關成員、裝置方法成員和公用成員等五個部分

a -- 主要全域性成員

char name[INFAMSIZ]    裝置名,如:eh%d
unsigned long state  裝置狀態
unsigned long base_addr  I/O基地址
unsigned int irq   中斷號

b -- 主要裝置方法

//首先看開啟和關閉網路裝置的函式:

int (*open)(struct net_device *dev);
//開啟介面。ifconfig啟用時,介面將被開啟

int (*stop)(struct net_device *dev);  
//停止介面,ifconfig eth% down時呼叫
//要注意的是ifconfig是interface config的縮寫,通常我們在使用者空間輸入:
//ifconfig eth0 up  會呼叫這裡的open函式。
//在使用者空間輸入:
//ifconfig eth0 down  會呼叫這裡的stop函式。
//在使用ifconfig向介面賦予地址時,要執行兩個任務。首先,它通過ioctl(SIOCSIFADDR)(Socket I/O Control Set Interface Address)賦予地址,然後通過ioctl(SIOCSIFFLAGS)(Socket I/O Control Set Interface Flags)設定dev->flag中的IFF_UP標誌以開啟介面。這個呼叫會使得裝置的open方法得到呼叫。類似的,在介面關閉時,ifconfig使用ioctl(SIOCSIFFLAGS)來清理IFF_UP標誌,然後呼叫stop函式。

int  (*init)(struct  net_device *dev)
//初始化函式,該函式在register_netdev時被呼叫來完成對net_device結構的初始化

int (*hard_start_xmit)(struct sk_buf*skb,struct net_device *dev)
//資料傳送函式

int (*hard_header)(struct sk_buff *skb, struct net_device *dev, unsigned short type, void *daddr, void *saddr, unsigned len); 
//該方法根據先前檢索到的源和目的硬體地址建立硬體頭

int (*rebuild_header)(struct sk_buff *skb);
//乙太網的mac地址是固定的,為了高效,第一個包去詢問mac地址,得到對應的mac地址後就會作為cache把mac地址儲存起來。以後每次發包不用詢問了,直接把包的地址拷貝出來。

void (*tx_timeout)(struct net_device *dev);  
//如果資料包傳送在超時時間內失敗,這時該方法被呼叫,這個方法應該解決失敗的問題,並重新開始傳送資料。

struct net_device_stats *(*get_stats)(struct net_device *dev);  
//當應用程式需要獲得介面的統計資訊時,這個方法被呼叫。

int (*set_config)(struct net_device *dev, struct ifmap *map);  
//改變介面的配置,比如改變I/O埠和中斷號等,現在的驅動程式通常無需該方法。

int (*do_ioctl)(struct net_device *dev, struct ifmap *map);  
//用來實現自定義的ioctl命令,如果不需要可以為NULL。

void (*set_multicast_list)(struct net_device *dev);  
//當裝置的組播列表改變或裝置標誌改變時,該方法被呼叫。

int (*set_mac_address)(struct net_device *dev, void *addr);  
//如果介面支援mac地址改變,則可以實現該函式。</span>

3、裝置驅動介面層:

net_device結構體的成員(屬性和函式指標)需要被裝置驅動功能層的具體數值和函式賦予。對具體的設定xxx,工程師應該編寫裝置驅動功能層的函式,這些函式型如xxx_open(),xxx_stop(),xxx_tx(),xxx_hard_header(),xxx_get_stats(),xxx_tx_timeout()等。

4、網路裝置與媒介層:

網路裝置與媒介層直接對應於實際的硬體裝置。

網路裝置的註冊

網路設備註冊方式與字元驅動不同之處在於它沒有主次裝置號,並使用下面的函式註冊

int register_netdev(struct net_deivce*dev)
網路裝置的登出
void unregister_netdev(struct net_device*dev)

四、驅動的實現

1)初始化(init)

裝置探測工作在init方法中進行,一般呼叫一個稱之為probe方法的函式

初始化的主要工作時檢測裝置,配置和初始化硬體,最後向系統申請這些資源。此外填充該裝置的dev結構,我們呼叫核心提供的ether_setup方法來設定一些乙太網預設的設定。

2)開啟(open)

open這個方法在網路裝置驅動程式裡是網路裝置被啟用時被呼叫(即裝置狀態由down變成up)

實際上很多在初始化的工作可以放到這裡來做。比如說資源的申請,硬體的啟用。如果dev->open返回非0,則硬體狀態還是down,
註冊中斷、DMA等;設定暫存器,啟動裝置;啟動傳送佇列

 一般註冊中斷都在init中做,但在網絡卡驅動程式中,註冊中斷大部分都是放在open中註冊,因為要經常關閉和重啟網絡卡

3)關閉(stop)

stop方法做和open相反的工作

可以釋放某些資源以減少系統負擔

stop是在裝置狀態由up轉為down時被呼叫

4)傳送(hard_start_xmit)

在系統呼叫的驅動程式的hard_start_xmit時,傳送的資料放在一個sk_buff結構中。一般的驅動程式傳給硬體發出去。也有一些特殊的裝置比如說loopback把資料組成一個接收資料在傳送給系統或者dummy裝置直接丟棄資料。

如果傳送成功,hard_start_xmit方法釋放sk_buff。如果裝置暫時無法處理,比如硬體忙,則返回1。

5)接收

驅動程式並存在一個接受方法。當有資料收到時驅動程式呼叫netif_rx函式將skb交交給裝置無關層。

一般裝置收到資料後都會產生一箇中斷,在中斷處理程式中驅動程式申請一塊sk_buff(skb)從硬體中讀取資料位置到申請號的緩衝區裡。

接下來填充sk_buff中的一些資訊。

中斷有可能是收到資料產生也可能是傳送完成產生,中斷處理程式要對中斷型別進行判斷,如果是收到資料中斷則開始接收資料,如果是傳送完成中斷,則處理髮送完成後的一些操作,比如說重啟發送佇列。
接收流程:
1、分配skb=dev_alloc_skb(pkt->datalen+2)
2、從硬體中讀取資料到skb
3、呼叫netif_rx將資料交給協議棧

中斷處理

網路介面通常支援3種類型的中斷:新報文到達中斷、報文傳送完成中斷和出錯中斷。中斷處理程式可通過檢視網絡卡的中斷狀態暫存器,來分辨出中斷型別。