26.Linux-网卡驱动介绍以及制作虚拟网卡驱动(详解)

1.描述

网卡的驱动其实很简单,它还是与硬件相关,主要是负责收发网络的数据包,它将上层协议传递下来的数据包以特定的媒介访问控制方式进行发送, 并将接收到的数据包传递给上层协议。

网卡设备与字符设备和块设备不同, 网络设备并不对应于/dev目录下的文件,不过会存放在/sys/class/net目录下

如下图所示,我们通过ls /sys/class/net/ 命令,可以看到有两个网卡:

2.Linux系统对网络设备驱动定义了4个层次, 这4个层次有到下分为:

1)网络协议接口层:

实现统一的数据包收发的协议,该层主要负责调用dev_queue_xmit()函数发送数据, netif_rx()函数接收数据

2)网络设备接口层:

通过net_device结构体来描述一个具体的网络设备的信息,实现不同的硬件的统一

3)设备驱动功能层:

用来负责驱动网络设备硬件来完成各个功能, 它通过hard_start_xmit() 函数启动发送操作, 并通过网络设备上的中断触发接收操作,

4)网络设备与媒介层:

用来负责完成数据包发送和接收的物理实体, 设备驱动功能层的函数都在这物理上驱动的

层次结构如下图所示:

3.网卡驱动初始化

而我们的网卡驱动程序,只需要编写网络设备接口层,填充net_device数据结构的内容并将net_device注册入内核,设置硬件相关操作,使能中断处理等

3.1其中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通道
       unsigned long              state;                    //网络设备和网络适配器的状态信息

             

      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   dev_addr[MAX_ADDR_LEN];      //存放设备的MAC地址

       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_stats结构体,其中重要成员如下所示:

struct net_device_stats
{
       unsigned long       rx_packets;            /*收到的数据包数*/
       unsigned long       tx_packets;            /*发送的数据包数    */
       unsigned long       rx_bytes;               /*收到的字节数,可以通过sk_buff结构体的成员len来获取*/
       unsigned long       tx_bytes;               /*发送的字节数,可以通过sk_buff结构体的成员len来获取*/
       unsigned long       rx_errors;              /*收到的错误数据包数*/
       unsigned long       tx_errors;              /*发送的错误数据包数*/
       ... ...
}

3.2 所以init函数,初始化网卡步骤如下所示:

  • 1)使用alloc_netdev()来分配一个net_device结构体 
  • 2)设置网卡硬件相关的寄存器
  • 3)设置net_device结构体的成员
  • 4)使用register_netdev()来注册net_device结构体

4.网卡驱动发包过程

在内核中,当上层要发送一个数据包时, 就会调用网络设备层里net_device数据结构的成员hard_start_xmit()将数据包发送出去。

hard_start_xmit()发包函数需要我们自己构建,该函数原型如下所示:

int    (*hard_start_xmit) (struct sk_buff *skb, struct net_device *dev);

在这个函数中需要涉及到sk_buff结构体,含义为(socket buffer)套接字缓冲区,用来网络各个层次之间传递数据.

4.1 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;                   //指向缓冲区的数据包开始位置
       ... ...
}

其中sk_buff结构体的空间,如下图所示:

其中sk_buff-> data数据包格式如下图所示:

4.2 所以,hard_start_xmit()发包函数处理步骤如下所示:

  • 1)把数据包发出去之前,需要使用netif_stop_queue()来停止上层传下来的数据包,
  • 2)设置寄存器,通过网络设备硬件,来发送数据
  • 2)当数据包发出去后, 再调用dev_kfree_skb()函数来释放sk_buff,该函数原型如下:
void dev_kfree_skb(struct sk_buff *skb);           
  • 3)当数据包发出成功,就会进入TX中断函数,然后更新统计信息,调用netif_wake_queue()来唤醒,启动上层继续发包下来.
  • 4)若数据包发出去超时,一直进不到TX中断函数,就会调用net_device结构体的(*tx_timeout)超时成员函数,在该函数中更新统计信息, 调用netif_wake_queue()来唤醒

其中netif_wake_queue()和netif_stop_queue()函数原型如下所示:

void netif_wake_queue(struct net_device *dev);  //唤醒被阻塞的上层,启动继续向网络设备驱动层发送数据包

void netif_stop_queue(struct net_device *dev); //阻止上层向网络设备驱动层发送数据包

5.网卡驱动收包过程

而接收数据包主要是通过中断函数处理,来判断中断类型,如果等于ISQ_RECEIVER_EVENT,表示为接收中断,然后进入接收数据函数,通过netif_rx()将数据上交给上层

参考的内核中自带的网卡驱动:/drivers/net/cs89x0.c

如上图所示,通过获取的status标志来判断是什么中断,如果是接收中断,就进入net_rx()

4.1 其中net_rx()收包函数处理步骤如下所示:

  • 1)使用dev_alloc_skb()来构造一个新的sk_buff
  • 2)使用skb_reserve(rx_skb, 2); 将sk_buff缓冲区里的数据包先后位移2字节,来腾出sk_buff缓冲区里的头部空间
  • 3)读取网络设备硬件上接收到的数据
  • 4)使用memcpy()将数据复制到新的sk_buff里的data成员指向的地址处,可以使用skb_put()来动态扩大sk_buff结构体里中的数据区
  • 5)使用eth_type_trans()来获取上层协议,将返回值赋给sk_buff的protocol成员里
  • 6)然后更新统计信息,最后使用netif_rx( )来将sk_fuffer传递给上层协议中

其中skb_put()函数原型如下所示:

static inline unsigned char *skb_put(struct sk_buff *skb, unsigned int len);
//len:将数据区向下扩大len字节

使用skb_put()函数后,其中sk_buff缓冲区变化:

6.写虚拟网卡驱动

本节便开始来写一个简单的虚拟网卡驱动,也就是说不需要硬件相关操作,所以就没有中断函数,我们通过linux的ping命令来实现发包,然后在发包函数中伪造一个收的ping包函数,实现能ping通任何ip地址

在init初始函数中:

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

在发包函数中:

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

在收包函数中:

首先修改发送的sk_buff里数据包的数据,使它变为一个接收的sk_buff,其中数据包结构如下图所示:

  • 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成员指向的地址处:
memcpy(skb_put(rx_skb, skb->len), skb->data, skb->len);
// skb_put():来动态扩大sk_buff结构体里中的数据区,避免溢出
  • 8)设置新的sk_buff 其它成员
  • 9)使用eth_type_trans()来获取上层协议,将返回值赋给sk_buff的protocol成员里
  • 10)然后更新接收统计信息,最后使用netif_rx( )来将sk_fuffer传递给上层协议中

7.驱动具体代码如下:

#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    *virt_net;


static void virt_rs_packet(struct sk_buff *skb, struct net_device *dev)
{
       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;

    /*1) 对调ethhdr结构体 "源/目的"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);

     /*2)对调 iphdr结构体"源/目的" IP地址*/
       ih = (struct iphdr *)(skb->data + sizeof(struct ethhdr));
       saddr = &ih->saddr;
       daddr = &ih->daddr;
       tmp = *saddr;
       *saddr = *daddr;
       *daddr = tmp;

   

    /*3)使用ip_fast_csum()来重新获取iphdr结构体的校验码*/
        ih->check = 0;               
        ih->check = ip_fast_csum((unsigned char *)ih,ih->ihl);


    /*4)设置数据类型*/
       type = skb->data + sizeof(struct ethhdr) + sizeof(struct iphdr);
       *type = 0;      //之前是发送ping包0x08,需要改为0x00,表示接收ping包


    /*5)使用dev_alloc_skb()来构造一个新的sk_buff   */
       rx_skb = dev_alloc_skb(skb->len + 2);
   
    /*6)使用skb_reserve()来腾出2字节头部空间  */
       skb_reserve(rx_skb, 2);

   
    /*7)使用memcpy()将之前修改好的sk_buff->data复制到新的sk_buff里*/
    memcpy(skb_put(rx_skb, skb->len), skb->data, skb->len); // skb_put():来动态扩大sk_buff结构体里中的数据区,避免溢出

    /*8)设置新的sk_buff 其它成员*/
    rx_skb->dev = dev;
    rx_skb->ip_summed = CHECKSUM_UNNECESSARY; /* don't check it */


    /*9)使用eth_type_trans()来获取上层协议 */
    rx_skb->protocol = eth_type_trans(rx_skb, dev);

 
    /*10) 更新接收统计信息,并使用netif_rx( )来 传递sk_fuffer收包 */
       dev->stats.rx_packets++;                     
       dev->stats.rx_bytes += skb->len;
       dev->last_rx= jiffies;                       //收包时间戳

       netif_rx(rx_skb);

}

static int virt_send_packet(struct sk_buff *skb, struct net_device *dev)
{
  /*1)使用netif_stop_queue()来阻止上层向网络设备驱动层发送数据包*/
    netif_stop_queue(dev);
 
   //期间设置硬件发送数据包

  /*2)调用收包函数,里面来伪造一个收的ping包函数*/
    virt_rs_packet(skb,dev);
 
 /*3)使用dev_kfree_skb()函数来释放发送的sk_buff缓存区*/
     dev_kfree_skb(skb);

 /*4)更新发送的统计信息*/
    dev->stats.tx_packets++;            //成功发送一个包
    dev->stats.tx_bytes+=skb->len;  //成功发送len长字节
    dev->trans_start = jiffies;            //发送时间戳

 /*5)使用netif_wake_queue()来唤醒被阻塞的上层*/
    netif_wake_queue(dev); 

    return 0;
}

 
static int virt_net_init(void)
{
    /*1)使用alloc_netdev()来分配一个net_device结构体*/
    virt_net= alloc_netdev(sizeof(struct net_device), "virt_eth0", ether_setup);

    /*2)设置net_device结构体的成员 */
    virt_net->hard_start_xmit      = virt_send_packet;

    virt_net->dev_addr[0] = 0x08;     
    virt_net->dev_addr[1] = 0x89;
    virt_net->dev_addr[2] = 0x89;
    virt_net->dev_addr[3] = 0x89;
    virt_net->dev_addr[4] = 0x89;
    virt_net->dev_addr[5] = 0x89;

    virt_net->flags           |= IFF_NOARP;
    virt_net->features        |= NETIF_F_NO_CSUM;


   /*3)使用register_netdev()来注册net_device结构体 */
    register_netdev(virt_net);
    return 0;
}

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

module_init(virt_net_init);
module_exit(virt_net_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("by:zhang");

8.测试运行

挂载驱动,如下图所示,可以看到net类下就有了这个网卡设备

开始试验,首先设置这个网卡设备的ip,然后去ping一下其它的ip,如下图所示:

上图的ping,之所以成功,是因为我们在发包函数中,伪造了一个来收包,通过netif_rx()来将收包上传给上层

使用ifconfig,可以看到这个网卡设备的统计信息共收发了6个包,以及收发的总数据

下节便开始学习网卡芯片DM9000C

如何编写移植DM9000C网卡驱动程序: http://www.cnblogs.com/lifexy/p/7777961.html

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏orientlu

FreeRTOS 消息队列

上面这几中方式中, 除了消息通知, 其他几种实现都是基于消息队列。消息队列作为主要的通信方式, 支持在任务间, 任务和中断间传递消息内容。 这一章介绍 Fre...

822
来自专栏高性能服务器开发

经典面试题(一)之服务器内存碎片

年前去过上海掌门集团(做无线wifi万能钥匙的那一家)和百度面试过一次,前者问了linux下gcc的malloc函数如何分配内存的,后者在二面时通过一个链表的数...

4108
来自专栏王亚昌的专栏

对数据操作封装的一点心得

在对数据进行操作时,可能需要读写name,于是我们写了一个接口,这个接口会实时更新缓存

611
来自专栏架构之路

【网络编程系列】二:socket通信原理及实践

我们深谙信息交流的价值,那网络中进程之间如何通信,如我们每天打开浏览器浏览网页时,浏览器的进程怎么与web服务器通信的?当你用QQ聊天时,QQ进程怎么与服务器或...

4526
来自专栏企鹅号快讯

Python的进程

进程 说明:本文是基于Py2.X环境, Python实现多进程的方式主要有两种:一种方法是使用os模块中的fork方法; 另一种是使用multiprocessi...

18310
来自专栏Python

python select模块详解

要理解select.select模块其实主要就是要理解它的参数, 以及其三个返回值。 select()方法接收并监控3个通信列表, 第一个是所有的输入的data...

3716
来自专栏大内老A

回调与并发: 通过实例剖析WCF基于ConcurrencyMode.Reentrant模式下的并发控制机制

对于正常的服务调用,从客户端发送到服务端的请求消息最终会被WCF服务运行时分发到相应的封装了服务实例的InstanceContext上。而在回调场景中,我们同样...

1787
来自专栏Jimoer

Java设计模式学习记录-状态模式

状态模式是一种行为模式,用于解决系统中复杂的对象状态转换以及各个状态下的封装等问题。状态模式是将一个对象的状态从该对象中分离出来,封装到专门的状态类中,使得对象...

571
来自专栏专注 Java 基础分享

详解 Java NIO

文件的抽象化表示,字节流以及字符流的文件操作等属于传统 IO 的相关内容,我们已经在前面的文章进行了较为深刻的学习了。

580
来自专栏linux驱动个人学习

Linux下进程的创建过程分析(_do_fork do_fork详解)--Linux进程的管理与调度(八)

Unix标准的复制进程的系统调用时fork(即分叉),但是Linux,BSD等操作系统并不止实现这一个,确切的说linux实现了三个,fork,vfork,cl...

592

扫码关注云+社区