网卡驱动程序

一、网卡驱动程序框架

相关驱动特点

  • 字符设备驱动

    1. 设置主设备号
    2. 填充file_operations结构体
    3. 用register_chrdev(主设备号,name,file_operations结构体)注册驱动
    4. 入口函数
    5. 出口函数
  • 块符设备驱动

    1. 分配geadisk结构体(利用alloc_disk函数)
    2. 设置
      • 构造队列queue=blk_init_queue(处理队列的函数)
      • 其他属性(主设备号、容量)
    3. 注册geadisk结构体(利用add_disk函数)

    上述字符设备驱动和块符设备驱动的共同特点:

    • 都有设备节点
    • 字符设备需要先open、块设备需要先格式化然后挂接(mount)
  • 网卡驱动

    不需要打开某设备,直接使用socket即可编程

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

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

Linux网络设备驱动的4层模型

OSI 7层模型

上图就是经典的OSI 7层模型,Linux的网卡驱动程序处于OSI模型中的数据链路层,他的职责就是将上上层的协议栈传过来的信息通过网卡发送出去,Linux的网络驱动模型采用4层结构:

1)网络协议接口层:

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

2)网络设备接口层:

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

3)设备驱动功能层:

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

4)网络设备与媒介层:

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

网卡驱动初始化

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

net_device结构体的重要成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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结构体,其中重要成员如下所示:

1
2
3
4
5
6
7
8
9
10
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; /*发送的错误数据包数*/
... ...
}

初始化网卡步骤

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

网卡驱动发包过程

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

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

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

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

sk_buff结构体及其重要成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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结构体空间

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

data数据包格式

即:

sk_buff

hard_start_xmit()发包的处理步骤

  • 1)把数据包发出去之前,需要使用**netif_stop_queue()**来停止上层传下来的数据包,

  • 2)设置寄存器,通过网络设备硬件,来发送数据

  • 3)当数据包发出去后, 再调用**dev_kfree_skb()**函数来释放sk_buff,该函数原型如下:

    1
    void dev_kfree_skb(struct sk_buff *skb);
  • 4)当数据包发出成功,就会进入TX中断函数,然后更新统计信息,调用**netif_wake_queue()**来唤醒,启动上层继续发包下来.

  • 5)若数据包发出去超时,一直进不到TX中断函数,就会调用net_device结构体的(*tx_timeout)超时成员函数,在该函数中更新统计信息, 调用**netif_wake_queue()**来唤醒

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

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

网卡驱动收包过程

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

如下所示,参考的内核中自带的网卡驱动:/drivers/net/cs89x0.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static irqreturn_t net_interrupt(int irq, void *dev_id)
{
struct net_device *dev = dev_id;
struct net_local *lp;
int ioaddr, status;
int handled = 0;

ioaddr = dev->base_addr;
lp = netdev_priv(dev);

/* we MUST read all the events out of the ISQ, otherwise we'll never
get interrupted again. As a consequence, we can't have any limit
on the number of times we loop in the interrupt handler. The
hardware guarantees that eventually we'll run out of events. Of
course, if you're on a slow machine, and packets are arriving
faster than you can read them off, you're screwed. Hasta la
vista, baby! */
while ((status = readword(dev->base_addr, ISQ_PORT))) {
if (net_debug > 4)printk("%s: event=%04x\n", dev->name, status);
handled = 1;
switch(status & ISQ_EVENT_MASK) {
case ISQ_RECEIVER_EVENT: //判断是否为接受中断
/* Got a packet(s). */
net_rx(dev); //通过net_rx函数将接受到的数据上交给上层
break;
case ISQ_TRANSMITTER_EVENT: //判断是否为发送中断
......

该函数就是通过获取的status标志来判断是什么中断,如果是接收中断,就进入net_rx()

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()**函数原型如下所示:

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

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

sk_buff缓冲区变化

二、编写虚拟网卡驱动

这里写一个简单的虚拟网卡驱动,也就是说不需要硬件相关操作,所以就没有中断函数,我们通过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()**来唤醒被阻塞的上层,

在收包函数中:

该部分可以参考LDD3(linux device drivers,third edition)

首先修改发送的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成员指向的地址处:

    1
    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传递给上层协议中

驱动具体代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
#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;

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

/* 设置下面两项才能ping通 */
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:NU-LL");

三、测试运行

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

网卡信息

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

网卡测试

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

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

测试验证