热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

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

1.基本概念网络设备是完成用户数据包在网络媒介上发送和接收的设备,它将上层协议传递下来的数据包以特定的媒介访问控制方式进行发送,并将接收到的数据包传递给

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,即14unsigned 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
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include 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_buffrx_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_buffnetif_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 cleanrm -rf modules.orderobj-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查看,统计信息变化了:
   


推荐阅读
  • Java中包装类的设计原因以及操作方法
    本文主要介绍了Java中设计包装类的原因以及操作方法。在Java中,除了对象类型,还有八大基本类型,为了将基本类型转换成对象,Java引入了包装类。文章通过介绍包装类的定义和实现,解答了为什么需要包装类的问题,并提供了简单易用的操作方法。通过本文的学习,读者可以更好地理解和应用Java中的包装类。 ... [详细]
  • 李逍遥寻找仙药的迷阵之旅
    本文讲述了少年李逍遥为了救治婶婶的病情,前往仙灵岛寻找仙药的故事。他需要穿越一个由M×N个方格组成的迷阵,有些方格内有怪物,有些方格是安全的。李逍遥需要避开有怪物的方格,并经过最少的方格,找到仙药。在寻找的过程中,他还会遇到神秘人物。本文提供了一个迷阵样例及李逍遥找到仙药的路线。 ... [详细]
  • 本文介绍了在处理不规则数据时如何使用Python自动提取文本中的时间日期,包括使用dateutil.parser模块统一日期字符串格式和使用datefinder模块提取日期。同时,还介绍了一段使用正则表达式的代码,可以支持中文日期和一些特殊的时间识别,例如'2012年12月12日'、'3小时前'、'在2012/12/13哈哈'等。 ... [详细]
  • 重入锁(ReentrantLock)学习及实现原理
    本文介绍了重入锁(ReentrantLock)的学习及实现原理。在学习synchronized的基础上,重入锁提供了更多的灵活性和功能。文章详细介绍了重入锁的特性、使用方法和实现原理,并提供了类图和测试代码供读者参考。重入锁支持重入和公平与非公平两种实现方式,通过对比和分析,读者可以更好地理解和应用重入锁。 ... [详细]
  • 开发笔记:加密&json&StringIO模块&BytesIO模块
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了加密&json&StringIO模块&BytesIO模块相关的知识,希望对你有一定的参考价值。一、加密加密 ... [详细]
  • 本文介绍了C#中生成随机数的三种方法,并分析了其中存在的问题。首先介绍了使用Random类生成随机数的默认方法,但在高并发情况下可能会出现重复的情况。接着通过循环生成了一系列随机数,进一步突显了这个问题。文章指出,随机数生成在任何编程语言中都是必备的功能,但Random类生成的随机数并不可靠。最后,提出了需要寻找其他可靠的随机数生成方法的建议。 ... [详细]
  • 本文讨论了使用差分约束系统求解House Man跳跃问题的思路与方法。给定一组不同高度,要求从最低点跳跃到最高点,每次跳跃的距离不超过D,并且不能改变给定的顺序。通过建立差分约束系统,将问题转化为图的建立和查询距离的问题。文章详细介绍了建立约束条件的方法,并使用SPFA算法判环并输出结果。同时还讨论了建边方向和跳跃顺序的关系。 ... [详细]
  • 自动轮播,反转播放的ViewPagerAdapter的使用方法和效果展示
    本文介绍了如何使用自动轮播、反转播放的ViewPagerAdapter,并展示了其效果。该ViewPagerAdapter支持无限循环、触摸暂停、切换缩放等功能。同时提供了使用GIF.gif的示例和github地址。通过LoopFragmentPagerAdapter类的getActualCount、getActualItem和getActualPagerTitle方法可以实现自定义的循环效果和标题展示。 ... [详细]
  • CF:3D City Model(小思维)问题解析和代码实现
    本文通过解析CF:3D City Model问题,介绍了问题的背景和要求,并给出了相应的代码实现。该问题涉及到在一个矩形的网格上建造城市的情景,每个网格单元可以作为建筑的基础,建筑由多个立方体叠加而成。文章详细讲解了问题的解决思路,并给出了相应的代码实现供读者参考。 ... [详细]
  • WebSocket与Socket.io的理解
    WebSocketprotocol是HTML5一种新的协议。它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送 ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • 本文介绍了Swing组件的用法,重点讲解了图标接口的定义和创建方法。图标接口用来将图标与各种组件相关联,可以是简单的绘画或使用磁盘上的GIF格式图像。文章详细介绍了图标接口的属性和绘制方法,并给出了一个菱形图标的实现示例。该示例可以配置图标的尺寸、颜色和填充状态。 ... [详细]
  • 纠正网上的错误:自定义一个类叫java.lang.System/String的方法
    本文纠正了网上关于自定义一个类叫java.lang.System/String的错误答案,并详细解释了为什么这种方法是错误的。作者指出,虽然双亲委托机制确实可以阻止自定义的System类被加载,但通过自定义一个特殊的类加载器,可以绕过双亲委托机制,达到自定义System类的目的。作者呼吁读者对网上的内容持怀疑态度,并带着问题来阅读文章。 ... [详细]
  • 在Oracle11g以前版本中的的DataGuard物理备用数据库,可以以只读的方式打开数据库,但此时MediaRecovery利用日志进行数据同步的过 ... [详细]
  • 本文讨论了在VMWARE5.1的虚拟服务器Windows Server 2008R2上安装oracle 10g客户端时出现的问题,并提供了解决方法。错误日志显示了异常访问违例,通过分析日志中的问题帧,找到了解决问题的线索。文章详细介绍了解决方法,帮助读者顺利安装oracle 10g客户端。 ... [详细]
author-avatar
无心小象
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有