學習目的:
- 熟悉Linux網卡驅動基本框架以及驅動程序編寫步驟
- 實現一個虛擬網卡驅動程序
1、概述
網卡工作在OSI的最后兩層,物理層和數據鏈路層,主要是負責收發網絡的數據包,它將網絡通信上層協議傳遞下來的數據包以特定的媒介訪問控制方式進行發送,並將接收到的數據包傳遞給上層協議。在知道了網卡的工作內容后,我們也就清楚了網卡驅動程序要實現的功能,即通過控制硬件實現數據的傳輸,一方面讓硬件將上層傳遞的數據包發送出去,另一方面接收外部數據並傳遞給上層。
為了能更加清楚理解內核中網卡驅動的程序,我們按照功能對它進行層次划分,划分后的Linux內核的網卡驅動程序的框架如下圖所示:
圖1 Linux內核網卡驅動框圖
從上圖可以看出內核中的網卡驅動程序被划分為4層:
- 網絡協議接口層:實現統一的數據包收發協議,該層主要負責調用dev_queue_xmit()函數發送數據包到下層或者調用 netif_rx()函數接收數據包,都使用sk_buff作為數據的載體;
- 網絡設備接口層:通過net_device結構體來描述網絡設備信息,是設備驅動功能層各個函數的容器,向上實現不同硬件類型接口的統一;
- 設備驅動功能層:用來負責驅動網絡設備硬件來完成各個功能,各個函數是網絡設備接口層net_device數據結構的具體成員,比如最核心的功能實現數據包的發送和數據包的接收;
- 網絡設備和媒介層:物理介質,驅動程序作用的對象。對於Linux系統而言,網絡設備和媒介也可以是虛擬的,如后面編寫的虛擬網卡驅動程序它就沒有網絡物理設備媒介;
其中net_device結構體是協議層和硬件交互的橋梁,它屏蔽了硬件之間的差異,使得協議層不需要關心硬件的操作,在發送數據時只需要調用net_device結構體中操作函數完成數據的收發。net_device結構體中的操作函數是由設備驅動功能層實現的函數注冊的,對應不同的硬件設備,驅動功能層實現上會有所差異。總的來說,我們編寫網卡驅動程序也就是圍繞網絡設備接口層和設備驅動功能層進行的,根據硬件功能實現設備驅動功能層的數據收發函數,填充並向上注冊net_device結構體。
2、核心數據結構和函數
2.1 核心數據結構
- sk_buff:網絡驅動框架中信息的載體,是網絡分層模型中對數據進行層層打包以及層層解包的載體
- net_dev_ops:網絡設備的操作函數的集合
- net_device:用於描述了一個網絡設備,net_device結構體中包含net_dev_ops指針,該指針指向操作硬件的方法
2.2 核心函數
- dev_queue_xmit():網絡協議接口層向下發送數據的接口,內核已經實現,不需要網絡設備驅動實現
- netif_rx():網絡設備接口層向上發送數據的接口,不需要網絡驅動實現
- 中斷處理函數:網絡設備媒介層收到數據后向上發送數據的入口,需要網絡驅動實現,最后要調用netif_rx()
- ndo_start_xmit():網絡設備接口層向下發送數據的接口, 位於net_device->net_device_ops, 會被dev_queue_xmit()回調,需要網絡驅動實現
- alloc_netdev():宏定義,最終調用到alloc_netdev_mqs(sizeof_priv, name, setup, 1, 1)函數,在驅動程序中調用,分配和初始化一個net_device結構體
- register_netdev():填充好net_device結構體,向內核注冊一個網絡設備(net_device結構體),需要在驅動程序中注冊
3、驅動程序編寫
編寫一個虛擬網卡的驅動程序,實現數據包的發送和構造應答數據包的向上提交
3.1 入口函數
static int vir_net_init(void) { /* 分配一個net_device結構體 */ vnet_dev = alloc_netdev(0, "vnet%d", ether_setup);---------------------->① /* 設置 */ vnet_dev->netdev_ops = &vnet_ops;--------------------------------------->② 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] = 0x89; /* 設置下面兩項才能ping的通 */ vnet_dev->flags |= IFF_NOARP;-------------------------------------->④ //vnet_dev->features |= NETIF_F_NO_CSUM; /* 注冊 */ register_netdev(vnet_dev);---------------------------------------------->⑤ return 0; }
① 分配一個net_device結構體,第一個參數sizeof_priv,代表額外分配的內存,用於存儲私有數據,設置成0代碼不分配額外私有內存。ether_setup是一個回調函數,使用設置以太網設備通用值,來設置分配net_device結構體一些屬性
② 設置虛擬網卡設備的操作函數集,如上層發送數據會最終調用到該指針指向結構體中的ndo_start_xmit函數
③ 設置虛擬網卡設備的MAC,即媒體訪問控制,代表網卡的地址。這里是任意設置的,如果是真正硬件,需要去獲取網卡硬件的MAC地址
④ 設置虛擬網卡通信的標志flags,由於是虛擬網卡,並沒有真正的和實際的網絡設備進行通信,上報的數據只是我們人為構造的,所有不需要在通信前使用ARP(地址解析協議)獲取通信設備的MAC地址。如果使能了使用ARP協議去獲取相應IP的設備的MAC地址將會導致錯誤
⑤ 向內核注冊網絡設備
3.2 net_dev_ops結構體vnet_ops
static netdev_tx_t vnet_send_packet(struct sk_buff *skb, struct net_device *dev) { static int cnt = 0; printk("vnet_send_packet: cnt = %d\n", ++cnt); /* 對於真實的網卡,把skb里的數據通過網卡發出去 */ netif_stop_queue(dev);------------------------------------------------->① /* 構造一個假的sk_buff上報 */ emulator_rx_packet(skb, dev);------------------------------------------>② /* 釋放skb */ dev_kfree_skb(skb);---------------------------------------------------->③ /* 數據全部發送出去后,喚醒網卡的隊列 */ netif_wake_queue(dev);------------------------------------------------->④ /* 更新統計信息 */ dev->stats.tx_packets++;----------------------------------------------->⑤ dev->stats.tx_bytes += skb->len; return 0; } static const struct net_device_ops vnet_ops = { .ndo_start_xmit = vnet_send_packet, };
由於編寫的是虛擬網卡,沒有實現硬件相關的功能,這里net_dev_ops網絡設備操作集合中只實現了數據的發送函數。在發送函數中打印了調用發送數據的次數,並且構造了一個skb信息上報給上層協議。
① 發送數據時,先調用netif_stop_queue函數讓上層停止將新的數據傳進來
② 構造一個skb_buff返回上層協議,這樣當上層有數據發送時,又構造到了一個相同類型的應答信息返回給上層,上層協議就能認為,當前網絡設備能和給定ip的設備間能夠正常的通信。
③ 使用完畢,釋放上層傳入的skb_buf
④ 數據全部發送成功,喚醒①中休眠隊列,讓上層協議繼續調用設備數據操作函數傳遞數據
⑤ 更新設備的統計信息,記錄總共發送包的個數和總共發送的字節數
3.3 構造接收數據包上報函數
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; //((u8 *)saddr)[2] ^= 1; /* change the third octet (class C) */ //((u8 *)daddr)[2] ^= 1; type = skb->data + sizeof(struct ethhdr) + sizeof(struct iphdr);--------->③ //printk("tx package type = %02x\n", *type); // 修改類型, 原來0x8表示ping *type = 0; /* 0表示reply */ ih->check = 0; /* and rebuild the checksum (ip needs 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 */ memcpy(skb_put(rx_skb, skb->len), skb->data, skb->len); /* 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 */ dev->stats.rx_packets++; dev->stats.rx_bytes += skb->len; // 提交sk_buff netif_rx(rx_skb);-------------------------------------------------------->⑦ }
由於是構造應答數據包,需要將請求數據的源MAC、目標MAC,源IP、目標IP內容調換,並設置數據包類型,使用調換后的信息構造應答的skb_buff
skb_buff中結構體包存放數據格式如下所示:
圖2 skb_buff中ICMP協議數據格式描述
① 將發送的skb_buff緩沖區中的源MAC和目標MAC內容調換
② 將發送的skb_buff緩沖區中的源IP和目標IP內容調換
③ 修改數據類型,設置為應答。應答ICMP請求,將數據類型改成0
④ 根據新的信息,重新計算IP頭部中check sum
⑤ 分配、設置用於應答的新的skb_buff
skb_reserve(rx_skb, 2)-->將skb_buff緩沖區里的數據包先后位移2字節,來騰出skb_buff緩沖區里的頭部空間
使用memcpy()將調整后的skb_buff復制到新的skb_buff里的data成員指向的地址處,可以使用skb_put()來動態擴大skb_buff結構體里中的數據區
⑥ 設置新的skb_buff中的net_device,以及傳輸協議類型,更新網絡設備接收數據的統計信息
⑦ 調用netif_rx提交新構造的應答skb_buff,完成數據的應答
4、驅動程序測試
1)加載驅動程序
insmod vir_net_drv.ko
2)查看網卡驅動程序否注冊成功
在Linux里一個網絡設備也可以叫做一個網絡接口,如eth0,應用程序是通過socket而不是設備節點來訪問網絡設備,在系統里根本就不存在網絡設備節點,但我們可以在/sys/class/net目錄下看網絡設備是否注冊成功
ls /sys/class/net/
3)設置虛擬網卡的IP地址
ifconfig vnet0 3.3.3.3
4)查看是否設置成功
ifconfig
5)ping任意的ip看是否能ping成功
完整驅動程序

#include <linux/module.h> #include <linux/printk.h> #include <linux/errno.h> #include <linux/netdevice.h> #include <linux/etherdevice.h> #include <linux/platform_device.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/spinlock.h> #include <linux/string.h> #include <linux/init.h> #include <linux/bitops.h> #include <linux/delay.h> #include <linux/gfp.h> #include <linux/ip.h> #include <asm/io.h> #include <asm/irq.h> #include <linux/atomic.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; //((u8 *)saddr)[2] ^= 1; /* change the third octet (class C) */ //((u8 *)daddr)[2] ^= 1; type = skb->data + sizeof(struct ethhdr) + sizeof(struct iphdr); //printk("tx package type = %02x\n", *type); // 修改類型, 原來0x8表示ping *type = 0; /* 0表示reply */ ih->check = 0; /* and rebuild the checksum (ip needs 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 */ memcpy(skb_put(rx_skb, skb->len), skb->data, skb->len); /* 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 */ dev->stats.rx_packets++; dev->stats.rx_bytes += skb->len; // 提交sk_buff netif_rx(rx_skb); } static netdev_tx_t vnet_send_packet(struct sk_buff *skb, struct net_device *dev) { static int cnt = 0; printk("vnet_send_packet: cnt = %d\n", ++cnt); /* 對於真實的網卡,把skb里的數據通過網卡發出去 */ netif_stop_queue(dev); /* 構造一個假的sk_buff上報 */ emulator_rx_packet(skb, dev); /* 釋放skb */ dev_kfree_skb(skb); /* 數據全部發送出去后,喚醒網卡的隊列 */ netif_wake_queue(dev); /* 更新統計信息 */ dev->stats.tx_packets++; dev->stats.tx_bytes += skb->len; return 0; } static const struct net_device_ops vnet_ops = { .ndo_start_xmit = vnet_send_packet, }; static int vir_net_init(void) { /* 分配一個net_device結構體 */ vnet_dev = alloc_netdev(0, "vnet%d", ether_setup); /* 設置 */ vnet_dev->netdev_ops = &vnet_ops; 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] = 0x89; /* 設置下面兩項才能ping的通 */ vnet_dev->flags |= IFF_NOARP; //vnet_dev->features |= NETIF_F_NO_CSUM; /* 注冊 */ register_netdev(vnet_dev); return 0; } static void vir_net_exit(void) { unregister_netdev(vnet_dev); free_netdev(vnet_dev); } module_init(vir_net_init); module_exit(vir_net_exit); MODULE_LICENSE("GPL");