学习目的:

  • 熟悉Linux网卡驱动基本框架以及驱动程序编写步骤
  • 实现一个虚拟网卡驱动程序

1、概述

网卡工作在OSI的最后两层,物理层和数据链路层,主要是负责收发网络的数据包,它将网络通信上层协议传递下来的数据包以特定的媒介访问控制方式进行发送,并将接收到的数据包传递给上层协议。在知道了网卡的工作内容后,我们也就清楚了网卡驱动程序要实现的功能,即通过控制硬件实现数据的传输,一方面让硬件将上层传递的数据包发送出去,另一方面接收外部数据并传递给上层。

为了能更加清楚理解内核中网卡驱动的程序,我们按照功能对它进行层次划分,划分后的Linux内核的网卡驱动程序的框架如下图所示:

bios linux 网卡驱动 linux网卡驱动原理_#include

图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中结构体包存放数据格式如下所示:

bios linux 网卡驱动 linux网卡驱动原理_bios linux 网卡驱动_02

图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/

bios linux 网卡驱动 linux网卡驱动原理_bios linux 网卡驱动_03

3)设置虚拟网卡的IP地址

ifconfig vnet0 3.3.3.3

4)查看是否设置成功

ifconfig

bios linux 网卡驱动 linux网卡驱动原理_bios linux 网卡驱动_04

5)ping任意的ip看是否能ping成功

bios linux 网卡驱动 linux网卡驱动原理_linux_05

完整驱动程序

bios linux 网卡驱动 linux网卡驱动原理_linux_06

bios linux 网卡驱动 linux网卡驱动原理_网络设备_07

#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