嵌入式Linux——網卡驅動(1):網卡驅動框架介紹【轉】


轉自:https://blog.csdn.net/W1107101310/article/details/79616286

聲明:文本是看完韋東山老師的視頻和看了一些文章后,所寫的總結。我會盡力將自己所了解的知識寫出來,但由於自己感覺並沒有學的很好,所以文中可能有錯的地方敬請指出,謝謝。

        在介紹本文之前,我想先對前面的知識做一下總結,我們知道Linux系統的設備分為字符設備(char device),塊設備(block device),以及網絡設備(network device)。字符設備是指存取時沒有緩存的設備。塊設備的讀寫都有緩存來支持,並且塊設備必須能夠隨機存取(random access),字符設備則沒有這個要求。典型的字符設備包括鼠標,鍵盤,串行口等。塊設備主要包括硬盤軟盤設備,CD-ROM等。一個文件系統要安裝進入操作系統必須在塊設備上。 
        網絡設備在Linux里做專門的處理。Linux的網絡系統主要是基於BSD unix的socket機制。在系統和驅動程序之間定義有專門的數據結構(sk_buff)進行數據的傳遞。系統里支持對發送數據和接收數據的緩存,提供流量控制機制,提供對多協議的支持。

        而本文主要對網卡驅動進行講解,同時會分為兩部分,第一部分介紹網卡驅動程序的框架,而另一部分我將以一個老師課上用的例子來完成一個虛擬的網卡驅動程序的編寫。

下面開始介紹網卡驅動程序的框架:

而要說到網卡驅動我們就要說到網絡協議的分層了,下面是一個網絡協議的分層圖:

 

在上面這幅圖中我們可以看到有兩種分層方式,一種是OSI七層網絡模型,而另一種是LinuxTCP/IP四層概念模型。而我們要寫的就是LinuxTCP/IP四層概念模型中的網絡接口層。而對應到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                    /* 再次查看設備的統計信息 */

 

而下面是兩篇介紹網卡信息的文章,我在寫文章時,對他們有所借鑒:

26.Linux-網卡驅動介紹以及制作虛擬網卡驅動(詳解)

Linux網卡驅動程序編寫
————————————————
版權聲明:本文為CSDN博主「moxue10」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/W1107101310/article/details/79616286


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM