1. 程式人生 > 實用技巧 >Linux 網絡卡驅動程式

Linux 網絡卡驅動程式

學習目的:

  • 熟悉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函式讓上層停止將新的資料傳進來

② 構造一個sk_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內容調換,並設定資料包型別,使用調換後的資訊構造應答的sk_buff

① 將傳送的skb_buff緩衝區中的源MAC和目標MAC內容調換

② 將傳送的skb_buff緩衝區中的源IP和目標IP內容調換

③ 修改型別,設定為應答

④ 計算新的check sum存放到skb_buff中

⑤ 分配新的skb_buff,將調整後的skb_buff資訊複製到分配的新的skb_buff中

⑥ 設定新的skb_buff中的net_device,以及傳輸協議型別。

⑦ 呼叫netif_rx提交新構造的應答sk_buff

4、驅動程式測試

1)載入驅動程式

insmod vir_net_drv.ko

2)檢視網絡卡驅動程式否註冊成功

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");
vir_net_drv.c