十九、Linux驱动之虚拟网卡驱动

1. 基本概念

    网络设备是完成用户数据包在网络媒介上发送和接收的设备,它将上层协议传递下来的数据包以特定的媒介访问控制方式进行发送,并将接收到的数据包传递给上层协议。与字符设备和块设备不同,网络设备并不对应于/dev目录下的文件,应用程序最终使用套接字完成与网络设备的接口。因而在网络设备身上并不能体现出“一切都是文件”的思想。
    Linux系统对网络设备驱动定义了4个层次, 从上到下依次为网络协议接口层、 网络设备接口层、 提供实际功能的设备驱动功能层以及网络设备与媒介层Linux网络设备驱动程序的体系结构如下图:


   
    这4层的作用如下所示:
    1. 网络协议接口层向网络层协议提供统一的数据包收发接口,不论上层协议是ARP,还是IP,都通过dev_queue_xmit()函数发送数据,并通过netif_rx()函数接收数据。这一层的存在使得上层协议独立于具体的设备。
    2. 网络设备接口层向协议接口层提供统一的用于描述具体网络设备属性和操作的结构体net_device,该结构体是设备驱动功能层中各函数的容器。实际上,网络设备接口层从宏观上规划了具体操作硬件的设备驱动功能层的结构。
    3. 设备驱动功能层的各函数是网络设备接口层net_device数据结构的具体成员,是驱使网络设备硬件完成相应动作的程序,它通过hard_start_xmit()函数启动发送操作,并通过网络设备上的中断触发接收操作。
    4. 网络设备与媒介层是完成数据包发送和接收的物理实体,包括网络适配器和具体的传输媒介,网络适配器被设备驱动功能层中的函数在物理上驱动。对于Linux系统而言,网络设备和媒介都可以是虚拟的。

2. 分析内核

    接下来分析内核(linux-2.6.22.6)具体是如何通过这4个层次使用网络设备的。

2.1 网络协议接口层

    网络协议接口层最主要的功能是给上层协议提供透明的数据包发送和接收接口。当上层ARPIP需要发送数据包时, 它将调用网络协议接口层的dev_queue_xmit()函数发送该数据包,同时需传递给该函数一个指向struct sk_buff数据结构的指针。 dev_queue_xmit()函数的原型为:

int dev_queue_xmit(struct sk_buff *skb);

    同样地,上层对数据包的接收也通过向netif_rx()函数传递一个struct sk_buff数据结构的指针来完成。netif_rx()函数的原型为:

int netif_rx(struct sk_buff *skb);

    sk_buff结构体非常重要,它定义于include/linux/skbuff.h文件中,含义为“套接字缓冲区”,用于在Linux网络子系统中的各层之间传递数据,是Linux网络子系统数据传递的“中枢神经”。当发送数据包时,Linux内核的网络处理模块必须建立一个包含要传输的数据包的sk_buff,然后将sk_buff递交给下层,各层在sk_buff中添加不同的协议头直至交给网络设备发送。同样地,当网络设备从网络媒介上接收到数据包后,它必须将接收到的数据转换为sk_buff数据结构并传递给上层,各层剥去相应的协议头直至交给用户。sk_buff数据结构部分代码如下:

struct sk_buff {
       /* These two members must be first. */
       struct sk_buff        *next;      //指向下一个sk_buff结构体
       struct sk_buff        *prev;      //指向前一个sk_buff结构体
    ...
       unsigned int          len,      //数据包的总长度,包括线性数据和非线性数据
                            data_len,    //非线性的数据长度
                            mac_len;     //mac包头长度

    __u32          priority;       //该sk_buff结构体的优先级   

    __be16        protocol;        //存放上层的协议类型,可以通过eth_type_trans()来获取
       ...

      sk_buff_data_t              transport_header;    //传输层头部的偏移值
      sk_buff_data_t              network_header;     //网络层头部的偏移值
      sk_buff_data_t              mac_header;          //MAC数据链路层头部的偏移值

    sk_buff_data_t              tail;                    //指向缓冲区的数据包末尾
      sk_buff_data_t              end;                     //指向缓冲区的末尾
      unsigned char            *head,                   //指向缓冲区的协议头开始位置
                                  *data;                   //指向缓冲区的数据包开始位置
       ...
}

    headend指向缓冲区的头部和尾部,而datatail指向实际数据的头部和尾部。每一层会在headdata之间填充协议头,或者在tailend之间添加新的协议数据。如下图所示:

2.1.1 分配sk_buff

    Linux内核中用于分配套接字缓冲区的函数原型如下:

struct sk_buff *alloc_skb(unsigned int len, gfp_t priority);
struct sk_buff *dev_alloc_skb(unsigned int len);

    alloc_skb()函数分配一个套接字缓冲区和一个数据缓冲区,参数len为数据缓冲区的空间大小,通常以L1_CACHE_BYTES字节(对于ARM为32)对齐,参数priority为内存分配的优先级。dev_alloc_skb()函数以GFP_ATOMIC优先级进行skb的分配,原因是该函数经常在设备驱动的接收中断里被调用。

2.1.2 释放sk_buff

    Linux内核中用于释放套接字缓冲区的函数原型如下:

void kfree_skb(struct sk_buff *skb);
void dev_kfree_skb(struct sk_buff *skb);
void dev_kfree_skb_irq(struct sk_buff *skb);
void dev_kfree_skb_any(struct sk_buff *skb);

    Linux内核内部使用kree_skb()函数,而在网络设备驱动程序中则最好用dev_kfree_skb()dev_kfree_skb_irq()dev_kfree_skb_any()函数进行套接字缓冲区的释放。其中,dev_kfree_skb()函数用于非中断上下文,dev_kfree_skb_irq()函数用于中断上下文,而dev_kfree_skb_any()函数在中断和非中断上下文中皆可采用,它其实是做一个非常简单的上下文判断,然后再调用__dev_kfree_skb_irq()或者dev_kfree_skb()。

2.1.3 改变sk_buff

    在Linux内核中可以用如下函数在缓冲区尾部增加数据:

unsigned char *skb_put(struct sk_buff *skb, unsigned int len);

    它会导致skb->tail后移len(skb->tail+=len),而skb->len会增加len的大小(skb->len+=len)。通常,在设备驱动的接收数据处理中会调用此函数。
    在Linux内核中可以用如下函数在缓冲区开头增加数据:

unsigned char *skb_push(struct sk_buff *skb, unsigned int len);

    它会导致skb->data前移len(skb->data-=len),而skb->len会增加len的大小(skb->len+=len) 。与该函数的功能完成相反的函数是skb_pull(),它可以在缓冲区开头移除数据,执行的动作是skb->len-=len、skb->data+=len

2.2 网络设备接口层

    内核中使用net_device结构来描述网络设备,这个结构是网络设备接口层中最重要的结构。该结构不仅描述了接口方面的信息,还包括硬件信息,致使该结构很大很复杂。通过这个结构,内核在底层的网络驱动和网络层之间构建了一个网络接口核心层,这个中间层类似于文件子系统的VFS。这样底层的驱动程序就不需要过多地关注上层的网络协议,只需要通过内核提供的网络接口核心层就可以很方便将和网络层进行数据的交互。而网络层在向下发送数据时,只需要通过内核提供的这个中间层进行交互即可,不需要关心底层究竟是什么类型的网卡。
    net_device结构体在内核中指代一个网络设备, 它定义于include/linux/netdevice.h文件中, 网络设备驱动程序只需通过填充net_device的具体成员并注册net_device即可实现硬件操作函数与内核的挂接。

struct net_device
{
       char               name[IFNAMSIZ]; //网卡设备名称
       unsigned long      mem_end;        //该设备的内存结束地址
       unsigned long      mem_start;      //该设备的内存起始地址
       unsigned long      base_addr;      //该设备的内存I/O基地址
       unsigned int       irq;            //该设备的中断号

       unsigned char      if_port;        //多端口设备使用的端口类型
    unsigned char      dma;            //该设备的DMA通道

      struct net_device_stats* (*get_stats)(struct net_device *dev); //获取流量的统计信息
      /*运行ifconfig便会调用该成员函数,并返回一个net_device_stats结构体获取信息*/

      struct net_device_stats  stats;    //用来保存统计信息的net_device_stats结构体
 
       unsigned long              features;    //接口特征,     
       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        mtu;                    //最大传输单元,也叫最大数据包

       unsigned short  type;                  //接口的硬件类型

       unsigned short  hard_header_len;        //硬件帧头长度,一般被赋为ETH_HLEN,即14
 
    unsigned char   perm_addr[MAX_ADDR_LEN];  //存放网关地址

       unsigned long   last_rx;                 //接收数据包的时间戳,调用netif_rx()后赋上jiffies即可

       unsigned long   trans_start;            //发送数据包的时间戳,当要发送的时候赋上jiffies即可

       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数据结构的内容并将net_device注册入内核即可。

2.2.1 分配net_device结构体 

    分配net_device结构体原型如下:

struct net_device *alloc_netdev(int sizeof_priv, const char *name,void (*setup)(struct net_device *));

    sizeof_priv表示私有数据大小,name表示网卡名字,ether_setup()函数会初始化一部分net_device结构体成员。

2.2.2 注册net_device结构体

    向内核注册net_device结构体原型如下:

int register_netdev(struct net_device *dev);
int register_netdevice(struct net_device *dev);

    register_netdev()是对register_netdevice()的包装函数。在调用register_netdev()注册设备时,如果指定的名称中包含%d格式串(只支持%d),内核会选择一个适当的数字来替换格式化串,真正的注册工作由register_netdevice()来完成。

2.3 设备驱动功能层

    net_device结构体的成员(属性和net_device_ops结构体中的函数指针)需要被设备驱动功能层赋予具体的数值和函数。对于具体的设备xxx,工程师应该编写相应的设备驱动功能层的函数,这些函数形如xxx_open()、xxx_stop()、xxx_tx()、 xxx_hard_header()、xxx_get_stats()xxx_tx_timeout()等。
    由于网络数据包的接收可由中断引发,设备驱动功能层中的另一个主体部分将是中断处理函数,它负责读取硬件上接收到的数据包并传送给上层协议,因此可能包含xxx_interrupt() xxx_rx()函数,前者完成中断类型判断等基本工作,后者则需完成数据包的生成及将其递交给上层等复杂工作。
    这一层的功能函数网卡芯片厂商都会有demo,我们只需要修改与硬件相关(如中断、I/O地址等)部分即可。

3. 编写代码

    本节编写一个虚拟网卡驱动程序,由于没有真实的网卡,不会接收到数据,不能实现接收中断,所以将收包函数放在发包函数里,将要发送的skb_buff数据再提交上层。(内核驱动里接收数据包主要是通过中断函数处理,中断类型如果等于ISQ_RECEIVER_EVENT表示为接收中断,然后进入接收数据函数,通过netif_rx()将数据上交给上层)。同样我们不用户编写设备驱动功能层(对于真实网卡需要编写对应的设备驱动功能层的函数)。

3.1 代码框架

3.1.1 初始函数中

    1. 使用alloc_netdev()来分配一个net_device结构体。
    2. 设置net_device结构体的成员。
    3. 使用register_netdev()来注册net_device结构体。

3.1.2 发包函数中

    1. 使用netif_stop_queue()来阻止上层向网络设备驱动层发送数据包。
    2. 调用收包函数,并代入发送的sk_buff缓冲区,里面来伪造一个收的ping包函数提交上层。
    3. 使用dev_kfree_skb()函数来释放发送的sk_buff缓存区。
    4. 更新发送的统计信息。
    5. 使用netif_wake_queue()来唤醒被阻塞的上层。

3.1.3 收包函数中

    1. 需要对调上图的ethhdr结构体 ”源/目的”MAC地址。
    2. 需要对调上图的iphdr结构体”源/目的” IP地址。
    3. 使用ip_fast_csum()来重新获取iphdr结构体的校验码。
    4. 设置上图数据包的数据类型,之前是发送ping包0x08,需要改为0x00,表示接收ping包。
    5. 使用dev_alloc_skb()来构造一个新的sk_buff
    6. 使用skb_reserve(rx_skb, 2);将sk_buff缓冲区里的数据包先后位移2字节,来腾出sk_buff缓冲区里的头部空间。
    7. 使用memcpy()将之前修改好的sk_buff->data复制到新的sk_buff里的data成员指向的地址处。

3.2 编写代码

    驱动程序virt_net.c完整代码如下:

/*
 * 参考 drivers\net\cs89x0.c
 */
#include <linux/module.h>
#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>

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 int virt_net_send_packet(struct sk_buff *skb, struct net_device *dev)
{
	static int cnt = 0;
	printk("virt_net_send_packet cnt = %d\n", ++cnt);

	/* 对于真实的网卡, 把skb里的数据通过网卡发送出去 */
	netif_stop_queue(dev); /* 停止该网卡的队列 */
    /* ...... */           /* 把skb的数据写入网卡 */

	/* 构造一个假的sk_buff,上报 */
	emulator_rx_packet(skb, dev);

	dev_kfree_skb (skb);   /* 释放skb */
	netif_wake_queue(dev); /* 数据全部发送出去后,唤醒网卡的队列 */

	/* 更新统计信息 */
	dev->stats.tx_packets++;
	dev->stats.tx_bytes += skb->len;
	
	return 0;
}

static int virt_net_init(void)
{
	/* 1. 分配一个net_device结构体 */
	vnet_dev = alloc_netdev(0, "vnet%d", ether_setup);;  /* alloc_etherdev */

	/* 2. 设置 */
	vnet_dev->hard_start_xmit = virt_net_send_packet;

	/* 设置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;

        /* 设置下面两项才能ping通 */
	vnet_dev->flags           |= IFF_NOARP;
	vnet_dev->features        |= NETIF_F_NO_CSUM;	

	/* 3. 注册 */
	//register_netdevice(vnet_dev);
	register_netdev(vnet_dev);
	
	return 0;
}

static void virt_net_exit(void)
{
	unregister_netdev(vnet_dev);
	free_netdev(vnet_dev);
}

module_init(virt_net_init);
module_exit(virt_net_exit);
MODULE_AUTHOR("lvzhenhai");
MODULE_LICENSE("GPL");

    Makefile代码如下:

KERN_DIR = /work/system/linux-2.6.22.6	//内核目录

all:
	make -C $(KERN_DIR) M=`pwd` modules 

clean:
	make -C $(KERN_DIR) M=`pwd` modules clean
	rm -rf modules.order

obj-m	+= virt_net.o

4. 测试

内核:linux-2.6.22.6
编译器:arm-linux-gcc-3.4.5
环境:ubuntu9.10

    开发板启动内核并安装编译好的驱动,执行如下命令:
      insmod virt_net.ko
      ifconfig vnet0 3.3.3.3   
(设置虚拟网卡vnet0的ip)
   

    执行如下命令ping自己:
      ping 3.3.3.3    (当ping自己时,使用回环网卡,没有调用到底层硬件发包函数)
   
    执行如下命令ping网络:
      ping 3.3.3.4   (使用我们编写的网卡驱动了,调用底层硬件发包函数)

   
    可以执行ifconfig查看,统计信息变化了:
   

  • 6
    点赞
  • 58
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值