嵌入式Linux——網絡卡驅動(1):網絡卡驅動框架介紹
宣告:文字是看完韋東山老師的視訊和看了一些文章後,所寫的總結。我會盡力將自己所瞭解的知識寫出來,但由於自己感覺並沒有學的很好,所以文中可能有錯的地方敬請指出,謝謝。
在介紹本文之前,我想先對前面的知識做一下總結,我們知道Linux系統的裝置分為字元裝置(char device),塊裝置(block device),以及網路裝置(network device)。字元裝置是指存取時沒有快取的裝置。塊裝置的讀寫都有快取來支援,並且塊裝置必須能夠隨機存取(random access),字元裝置則沒有這個要求。典型的字元裝置包括滑鼠,鍵盤,序列口等。塊裝置主要包括硬碟軟盤裝置,CD-ROM等。一個檔案系統要安裝進入作業系統必須在塊裝置上。
網路裝置在Linux裡做專門的處理。Linux的網路系統主要是基於BSD unix的socket機制。在系統和驅動程式之間定義有專門的資料結構(sk_buff)進行資料的傳遞。系統裡支援對傳送資料和接收資料的快取,提供流量控制機制,提供對多協議的支援。
而本文主要對網絡卡驅動進行講解,同時會分為兩部分,第一部分介紹網絡卡驅動程式的框架,而另一部分我將以一個老師課上用的例子來完成一個虛擬的網絡卡驅動程式的編寫。
下面開始介紹網絡卡驅動程式的框架:
而要說到網絡卡驅動我們就要說到網路協議的分層了,下面是一個網路協議的分層圖:
在上面這幅圖中我們可以看到有兩種分層方式,一種是OSI七層網路模型
而上圖中每一層的含義為:
1)網路協議介面層:
實現統一的資料包收發的協議,該層主要負責呼叫dev_queue_xmit()函式傳送資料包到下層或者呼叫 netif_rx()函式接收資料包
2)網路裝置介面層:
通過net_device結構體來描述一個具體的網路裝置的資訊,實現不同的硬體的統一
3)裝置驅動功能層:
用來負責驅動網路裝置硬體來完成各個功能, 它通過hard_start_xmit() 函式啟動傳送操作, 並通過網路裝置上的中斷觸發接收操作,
4)網路裝置與媒介層:
用來負責完成資料包傳送和接收的物理實體, 裝置驅動功能層的函式都在這物理上驅動的
通過上面的描述我們知道,net_device結構體描述了網路裝置的資訊,並實現了不同硬體的統一,所以要寫驅動程式要先看net_device中有什麼引數,然後看哪些引數是要我們去完成的,而那些是上面協議層已經幫我們寫好的。下面是net_device:
/*
* The DEVICE structure. 裝置框架
*/
struct net_device
{
char name[IFNAMSIZ]; /* 裝置名 */
/*I/O specific fields IO特有的域*/
unsigned long mem_end; /* 記憶體結束地址 */
unsigned long mem_start; /* 記憶體開始地址*/
unsigned long base_addr; /* 記憶體基地址 */
unsigned int irq; /* 裝置中斷號 */
unsigned char if_port; /* 多埠裝置使用的埠型別 */
unsigned char dma; /* DMA通道 */
unsigned long state; /* 裝置狀態資訊 */
int (*init)(struct net_device *dev); /* 裝置的初始化函式,只調用一次 */
/* 網路裝置特徵 */
unsigned long features; /* 介面特徵 */
/* 獲取流量的統計資訊,通過執行ifconfig便可以呼叫該成員函式,並返回一個net_device_stats結構體獲取資訊 */
struct net_device_stats* (*get_stats)(struct net_device *dev);
struct net_device_stats stats; /* 用來儲存統計資訊的net_device_stats結構體 */
unsigned int flags; /*flags指網路介面標誌,以IFF_(Interface Flags)開頭*/
/*當flags =IFF_UP( 當裝置被啟用並可以開始傳送資料包時, 核心設定該標誌)、
*IFF_AUTOMEDIA(設定裝置可在多種媒介間切換)、IFF_BROADCAST( 允許廣播)、
*IFF_DEBUG( 除錯模式, 可用於控制printk呼叫的詳細程度) 、 IFF_LOOPBACK( 迴環)、
*IFF_MULTICAST( 允許組播) 、 IFF_NOARP( 介面不能執行ARP,點對點介面就不需要執行 ARP)
* 和IFF_POINTOPOINT( 介面連線到點到點鏈路) 等。
*/
unsigned short priv_flags; /* 和flags相似,但是使用者空間不可見 */
unsigned short padded; /* 通過alloc_netdev()填充多少 */
unsigned mtu; /* 最大傳輸單元,也叫最大資料包 */
unsigned short type; /* 介面硬體型別 */
unsigned short hard_header_len; /* 硬體幀頭長度,一般被賦為ETH_HLEN,即14 */
/* 介面地址資訊 */
unsigned char perm_addr[MAX_ADDR_LEN]; /* 不變的實體地址 */
unsigned char addr_len; /* 實體地址長度 */
unsigned short dev_id; /* for shared network cards */
struct dev_mc_list *mc_list; /* Mac地址 */
int mc_count; /* mcasts個數 */
unsigned char dev_addr[MAX_ADDR_LEN]; /* 存放裝置的MAC地址 */
int (*hard_start_xmit) (struct sk_buff *skb, struct net_device *dev); //資料包傳送函式, sk_buff就是用來收發資料包的結構體
void (*tx_timeout) (struct net_device *dev);//發包超時處理函式
介紹完net_device結構體,我想介紹一下他的操作函式,其中包括他的分配函式alloc_netdev()函式或者alloc_etherdev()函式,以及其登出函式free_netdev(vnet_dev)。
/**
* alloc_netdev - 分配網路裝置
* @sizeof_priv: 私有資料空間大小,在本程式中設為0,即不需要私有資料
* @name: 裝置名
* @setup: 初始化裝置的回撥函式,這裡寫回調函式:ether_setup
*/
struct net_device *alloc_netdev(int sizeof_priv, const char *name,void (*setup)(struct net_device *))
而我們再看alloc_etherdev()函式:
/**
* alloc_etherdev - 分配設定一個乙太網裝置
* @sizeof_priv: 私有資料的大小
*/
struct net_device *alloc_etherdev(int sizeof_priv)
{
return alloc_netdev(sizeof_priv, "eth%d", ether_setup);
}
通過觀察上面兩個函式我們發現,其實alloc_etherdev()函式就是呼叫alloc_netdev()函式,只是給他設定了通用的值。
/**
* free_netdev - 釋放裝置
* @dev: 網路裝置
*/
void free_netdev(struct net_device *dev)
我們下面在介紹兩個重要的結構體:net_device_stats結構體和sk_buff結構體。我們知道我們所寫的網路裝置驅動,其主要的功能就是完成收發資料。而net_device_stats結構體就是統計收發的資訊,而sk_buff就是用於收發的資料包。
下面我們下說net_device_stats結構體:
struct net_device_stats
{
unsigned long rx_packets; /* 接收的資料包總數 */
unsigned long tx_packets; /* 傳送的資料包總數 */
unsigned long rx_bytes; /* 接收的總位元組數 */
unsigned long tx_bytes; /* 傳輸的總位元組數 */
unsigned long rx_errors; /* 接收的錯誤包數 */
unsigned long tx_errors; /* 傳輸的錯誤包數 */
unsigned long rx_dropped; /* Linux緩衝區沒有空間 */
unsigned long tx_dropped; /* 在Linux中沒有空間可用 */
};
下面說另一個結構體:sk_buff
/**
* struct sk_buff - socket 緩衝區
*/
struct sk_buff {
/* 這兩個引數一定要放在最前面 */
struct sk_buff *next; /* 列表中的下一個快取區 */
struct sk_buff *prev; /* 列表中的上一個快取區 */
struct sock *sk; /* 我們所屬的socket */
struct net_device *dev; /* 我們要到的或者要離開的裝置 */
unsigned int len, /* 資料包的總長度 */
data_len, /* 資料包中真實資料的長度 */
mac_len; /* Mac包頭長度 */
__u32 priority; /* 包序列優先順序 */
__be16 protocol; /* 存放上層的協議型別,可以通過eth_type_trans()來獲取 */
sk_buff_data_t transport_header; /* 傳輸層頭偏移量 */
sk_buff_data_t network_header; /* 網路層頭偏移量 */
sk_buff_data_t mac_header; /* 鏈路層頭偏移量 */
/* These elements must be at the end, see alloc_skb() for details. */
sk_buff_data_t tail; /* 快取區資料包末尾指標 */
sk_buff_data_t end; /* 快取區末尾指標 */
unsigned char *head, /* 快取區協議頭指標 */
*data; /* 快取區資料包開始位置指標 */
};
我們用下圖對其空間說明:
而sk_buff中的data又可以細分為:MAC頭,IP頭,type和真正的資料。而下圖是其空間排布:
而對sk_buff操作的函式有:
struct sk_buff *alloc_skb(unsigned int len, int priority) /* 分配一個sk_buff結構,供協議棧程式碼使用 */
struct sk_buff *dev_alloc_skb(unsigned int len) /* 分配一個sk_buff結構,供驅動程式碼使用 */
unsigned char *skb_push(struct sk_buff *skb, int len) /* 向後移動skb的tail指標,並返回tail移動之前的值。 */
unsigned char *skb_put(structsk_buff *skb, int len) /* 向前移動skb的head指標,並返回head移動之後的值。 */
kfree_skb(struct sk_buff *skb) /* 釋放一個sk_buff結構,供協議棧程式碼使用。 */
dev_kfree_skb(struct sk_buff *skb) /* 釋放一個sk_buff結構,供驅動程式碼使用 */
而說到sk_buff就要介紹兩個運用他的函式,一個是傳送包函式:hard_start_xmit,以及接收包函式:netif_rx();這是一個網路裝置最基本的功能。一塊網絡卡所做的無非就是收發工作。所以驅動程式裡要告訴系統你的傳送函式在哪裡,系統在有資料要傳送時就會呼叫你的發 送程式。還有驅動程式由於是直接操縱硬體的,所以網路硬體有資料收到最先能得到這個資料的也就是驅動程式,它負責把這些原始資料進行必要的處理然後送給系統。這裡,作業系統必須要提供兩個機制,一個是找到驅動程式的傳送函式,一個是驅動程式把收到的資料送給系統。
我們先講解發包函式hard_start_xmit,對於真實的網絡卡,就是把skb中的資料通過網絡卡傳送出去:
1.停止該網絡卡的佇列(禁止再向網絡卡傳送資料,而其他的資料要等待):netif_stop_queue(dev);
2.把skb的資料寫入到網絡卡中
3. 寫入完成後釋放skb :dev_kfree_skb(skb);
4.更新統計資訊:dev->stats.tx_packets++;
dev->stats.tx_bytes += skb->l; /* 這裡就用到了上面講的net_device_stats中的資料 */
5.資料全部發送完後,喚醒網絡卡的佇列 :netif_wake_queue(dev);
而對於接受資料包函式netif_rx(),我並不是很瞭解,這裡引用一個網友的說法(本文的結尾有該篇文章的連線,我認為這是一篇很好的文章):
而接收資料包主要是通過中斷函式處理,來判斷中斷型別,如果等於ISQ_RECEIVER_EVENT,表示為接收中斷,然後進入接收資料函式,通過netif_rx()將資料上交給上層
例如下圖所示,參考的核心中自帶的網絡卡驅動:/drivers/net/cs89x0.c
如上圖所示,通過獲取的status標誌來判斷是什麼中斷,如果是接收中斷,就進入net_rx()
其中net_rx()收包函式處理步驟如下所示:
- 1)使用dev_alloc_skb()來構造一個新的sk_buff
- 2)使用skb_reserve(rx_skb, 2); 將sk_buff緩衝區裡的資料包先後位移2位元組,來騰出sk_buff緩衝區裡的頭部空間
- 3)讀取網路裝置硬體上接收到的資料
- 4)使用memcpy()將資料複製到新的sk_buff裡的data成員指向的地址處,可以使用skb_put()來動態擴大sk_buff結構體裡中的資料區
- 5)使用eth_type_trans()來獲取上層協議,將返回值賦給sk_buff的protocol成員裡
- 6)然後更新統計資訊,最後使用netif_rx( )來將sk_fuffer傳遞給上層協議中
其中skb_put()函式原型如下所示:
static inline unsigned char *skb_put(struct sk_buff *skb, unsigned int len);
//len:將資料區向下擴大len位元組
使用skb_put()函式後,其中sk_buff緩衝區變化如下圖:
講解完上面這些基礎的部分,那麼下面我們以老師在課上講的寫一個虛擬的網絡卡例子來講解網絡卡驅動程式的編寫步驟:
在該例子中我們會構造一個假的sk_buff上報函式,而在這個函式中我們會將從接收函式hard_start_xmit接收到的資料包,用netif_rx(rx_skb)函式傳送回到上層的網路層中,而不去接觸物理層。這裡我們要在sk_buff->data中做一些修改來完成這個功能。而具體的修改辦法為:
也就是:
1.對調“源/目的”的MAC地址
2.對調“源/目的”的IP地址
3.修改型別,將0x8改為0
4.使用ip_fast_csum重新獲得IP的校驗碼
5.構造一個sk_buff結構體
6. 將修改好的data複製到原來的data中
7.更新統計資訊
8.向上層提交sk_buff
通過下面這幅圖:
我們已經對網絡卡驅動有了大致的瞭解,而且我們知道在核心中,都會以面向物件的思想去設定一個結構體,在這個結構體中有這個模組或者這個層中所用到的引數,方法或者介面資訊,正是這些有統一介面的方法,掩蔽了硬體的具體細節,讓系統對各種網路裝置的訪問都採用統一的形式,做到硬體無關性。而我們編寫驅動程式時所要做的就是去填充這個結構體。而在網絡卡驅動中這個結構體就是net_device結構體,而我們編寫驅動的步驟也就清楚了:
1.分配一個net_device結構體
2.設定net_device結構體
2.1 提供發包函式:hard_start_xmit
2.2 收到資料時(在中斷處理函式中)用netif_rx函式上報資料
2.3 其他的設定
3.註冊net_device結構體:register_netdev()。
那麼我們根據上面的介紹,就可以寫自己的網絡卡驅動程式了,下面是我寫的驅動程式:
#include <linux/errno.h>
#include <linux/netdevice.h>
#include <linux/etherdevice.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/fcntl.h>
#include <linux/interrupt.h>
#include <linux/ioport.h>
#include <linux/in.h>
#include <linux/skbuff.h>
#include <linux/slab.h>
#include <linux/spinlock.h>
#include <linux/string.h>
#include <linux/init.h>
#include <linux/bitops.h>
#include <linux/delay.h>
#include <linux/ip.h>
#include <asm/system.h>
#include <asm/io.h>
#include <asm/irq.h>
#include <asm/dma.h>
static struct net_device *vnet_dev;
static void emulator_rx_packet(struct sk_buff *skb,struct net_device *dev)
{
/* 參考LDD3 */
unsigned char *type;
struct iphdr *ih;
__be32 *saddr,*daddr,tmp;
unsigned char tmp_dev_addr[ETH_ALEN];
struct ethhdr *ethhdr;
struct sk_buff *rx_skb;
//從硬體讀出/儲存資料
/* 對調“源/目的”的MAC地址 */
ethhdr = (struct ethhdr *)skb->data;
memcpy(tmp_dev_addr,ethhdr->h_dest,ETH_ALEN);
memcpy(ethhdr->h_dest,ethhdr->h_source,ETH_ALEN);
memcpy(ethhdr->h_source,tmp_dev_addr,ETH_ALEN);
/* 對調“源/目的”的IP地址 */
ih = (struct iphdr *)(skb->data + sizeof(struct ethhdr));
saddr = &ih->saddr;
daddr = &ih->daddr;
tmp = *saddr;
*saddr = *daddr;
*daddr = tmp;
type = skb->data + sizeof(struct ethhdr) + sizeof(struct iphdr);
//修改型別,原來0x8表示ping
*type = 0; /* 0表示reply */
ih->check = 0; /* and rebuild the checksum (ip need it) */
ih->check = ip_fast_csum((unsigned char *)ih,ih->ihl);
//構造一個sk_buff
rx_skb = dev_alloc_skb(skb->len + 2);
skb_reserve(rx_skb,2); /* align IP on 16B boundary *//*使用skb_reserve()來騰出2位元組頭部空間 */
memcpy(skb_put(rx_skb,skb->len),skb->data,skb->len);/*使用memcpy()將之前修改好的sk_buff->data複製到新的sk_buff裡*/
// skb_put():來動態擴大sk_buff結構體裡中的資料區,避免溢位
/* write metadata,and then pass to the receive level */
rx_skb->dev = dev;
rx_skb->protocol = eth_type_trans(rx_skb,dev);
rx_skb->ip_summed = CHECKSUM_UNNECESSARY; /* don't check it */
/* 更新接收統計資訊,並使用netif_rx( )來 傳遞sk_fuffer收包 */
dev->stats.rx_packets++;
dev->stats.rx_bytes += skb->len;
dev->last_rx= jiffies; //收包時間戳
netif_rx(rx_skb);
}
static int virt_net_sendpacket(struct sk_buff *skb,struct net_device *dev)
{
static int cnt = 0;
printk(" virt_net_sendpacket cnt = %d \n",++cnt);
/* 對於真實的網絡卡,把skb裡的資料通過網絡卡傳送出去 */
netif_stop_queue(dev); /* 停止該網絡卡的佇列 */
/* */ /* 把skb的資料寫入網絡卡 */
/* 構造一個假的sk_buff上報 */
emulator_rx_packet(skb,dev);
dev_kfree_skb(skb); /* 釋放skb */
/* 更新統計資訊 */
dev->stats.tx_packets++;
dev->stats.tx_bytes += skb->l;
netif_wake_queue(dev); /* 資料全部發送出去後,喚醒網絡卡的佇列 */
return 0;
}
static int s3c_vnet_init(void)
{
/* 1. 分配一個net_device結構體 */
vnet_dev = alloc_netdev(0,"vnet%d",ether_setup); /* 也可以使用alloc_etherdev函式來分配 */
/* 2. 設定net_device結構體 */
vnet_dev->hard_start_xmit = virt_net_sendpacket; /* 發包函式 */
/* 2.1 設定MAC地址 */
vnet_dev->dev_addr[0] = 0x08;
vnet_dev->dev_addr[1] = 0x89;
vnet_dev->dev_addr[2] = 0x89;
vnet_dev->dev_addr[3] = 0x89;
vnet_dev->dev_addr[4] = 0x89;
vnet_dev->dev_addr[5] = 0x11;
/* 2.2 設定下面兩項才能ping通 */
/* keep the default flags, just add NOARP */
vnet_dev->flags |= IFF_NOARP;
vnet_dev->features |= NETIF_F_NO_CSUM;
/* 3. 註冊net_device結構體:register_netdev */
register_netdev(vnet_dev);
return 0;
}
static void s3c_vnet_exit(void)
{
unregister_netdev(vnet_dev);
free_netdev(vnet_dev);
}
module_init(s3c_vnet_init);
module_exit(s3c_vnet_exit);
MODULE_LICENSE("GPL");
編寫完程式我們就應該對其進行測試了:
1.insmod virt_net.ko /*在本文中我生成的是名為virt_net.ko 的檔案,你的可能不一樣 */
2.ifconfig /* 檢視系統中已有的網路裝置 */
3. ifconfig vnet0 3.3.3.3 /* 設定虛擬網絡卡為3.3.3.3,注意,這裡的vnet0是使用alloc_netdev函式設定的名字 */
4. ifconfig /* 再次檢視系統中的網路裝置 */
5.ping 3.3.3.3 /* ping 自己看是否可以ping通 */
5.1 ifconfig /* 檢視裝置的統計資訊 */
6.ping 3.3.3.4 /* ping其他的伺服器看是否可以ping通 */
6.1 ifconfig /* 再次檢視裝置的統計資訊 */
而下面是兩篇介紹網絡卡資訊的文章,我在寫文章時,對他們有所借鑑: