网络设备驱动基础
来源:互联网 发布:fifa17球员数据 编辑:程序博客网 时间:2024/05/24 07:41
http://blog.sina.com.cn/s/blog_6e5b342e0100m87g.html
一、体验网卡驱动
1、下载虚拟网卡驱动源码(单击下载)后,执行make得到snull.ko,加载驱动 sudo insmod snull.ko
2、分别配置2张网卡(sn0和sn1)的ip地址
dennis@dennis-desktop:/work/studydriver/snull$ sudo ifconfig sn0 192.168.140.1
dennis@dennis-desktop:/work/studydriver/snull$ sudo ifconfig sn1 192.168.141.2
3、测试2张网卡之间的通信
dennis@dennis-desktop:/work/studydriver/snull$ ping 192.168.140.2
PING 192.168.140.2 (192.168.140.2) 56(84) bytes of data.
64 bytes from 192.168.140.2: icmp_seq=1 ttl=64 time=0.167 ms
64 bytes from 192.168.140.2: icmp_seq=2 ttl=64 time=0.112 ms
64 bytes from 192.168.140.2: icmp_seq=3 ttl=64 time=0.111 ms
64 bytes from 192.168.140.2: icmp_seq=4 ttl=64 time=0.139 ms
--- 192.168.140.2 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3000ms
rtt min/avg/max/mdev = 0.111/0.132/0.167/0.024 ms
注:也许你感觉2中ip地址的分配和3中的ping的目标有些奇怪,甚至有些难以理解。这是由于我们要测试网卡sn0是否能ping通网卡sn1,而sn0和sn1是本机的2张网卡,如果将它们的ip地址配在同一网段,则测试时数据包根本不会外送。为解决测试的问题,scull驱动对外发数据包作了一些额外处理,将目标ip的第3段加1(即:将目标ip从192.168.140.2改为192.168.141.2),将源ip的第3段加1(即:将源ip从192.168.140.1改为192.168.141.1)。详情请参见《Linux Device Driver》17.1节。
二、网卡驱动的基本知识——2个结构体和5个函数
1、结构体 struct net_device
网络设备的注册方式与字符和块设备不同的。网络设备没有主次设备号,驱动为每个刚刚探测到的网络设备(或称为接口)在一个全局的网络设备链表里插入一个节点,该节点就代表1个网络设备,它是struct net_device类型的结构体。
1)、net_device结构体的内容(字段)很多,主要用于操作系统和驱动程序操控和查询网卡
- 全局信息(例如:接口名称eth0)
- 硬件信息(例如:中断号、I/O base)
- 接口信息(例如:MAC地址、混杂模式标志)
- 设备功能函数指针(例如:数据发送函数)
- 其它字段(例如:priv、自旋锁)
2) 、net_device结构体的生成与初始化
struct net_device *alloc_netdev(int sizeof_priv, const char *name, void (*setup)(struct net_device *))
- sizeof_priv 是驱动的的“私有数据”区的大小
- 对于网络驱动, 这个区是同 net_device 结构一起分配的(逻辑上可认为“私有数据”区紧随net_device 结构之后)
- net_device 结构体中有一个成员priv,它就是指向“私有数据”区的指针。它的角色近似于我们用在字符驱动上的 private_data 指针,当一个驱动需要存取私有数据指针, 应当使用 netdev_priv 函数,例如:struct snull_priv *priv = netdev_priv(dev);
- name是网络设备的名字,例如:eth0、eth1、sn0、sn1
- setup 是一个初始化函数的指针, 被alloc_netdev调用以初始化 net_device 结构的大部分字段。网络子系统针对alloc_netdev为各种接口提供了一些封装函数,如:
- 以太网设备:alloc_etherdev
- struct net_device *alloc_etherdev(int sizeof_priv)这个函数分配一个网络设备结构体,使用 eth%d 作为参数 name。它提供了自己的初始化函数 ( ether_setup )来设置许多 net_device 成员,使用对以太网设备合适的值。没有驱动提供的初始化函数给 alloc_etherdev;
- 通过 ether_setup 函数(由 alloc_etherdev 调用),内核负责了一些以太网范围中的缺省值
- 光纤通道设备:alloc_fcdev
- FDDI 设备: alloc_fddidev
- 令牌环设备:alloc_trdev
- 以太网设备:alloc_etherdev
3)、net_device结构体的注册(插入全局的网络设备链表)
在对net_device结构体完成初始化后,传递这个结构给 int register_netdev(struct net_device *dev),以完成注册
4)、将net_device结构体从内核中注销
void unregister_netdev(struct net_device *dev)
5)、net_device结构体的销毁
void free_netdev(struct net_device *dev) 归还 net_device 结构给内核
2、 网络接口的打开与关闭
- 当用户执行ifconfig 命令时,内核会打开或者关闭一个接口
- 打开接口:ifconfig eth0 192.168.10.1 up
- 通过 ioctl(SIOCSIFADDR)( Socket I/O Control Set Interface Address) 来安排ip地址.-----内核实现
- 调用dev->open方法——驱动实现
- 通过 ioctl(SIOCSIFFLAGS) ( Socket I/O Control Set Interface Flags) 来设置 dev->flag 的 IFF_UP 位,. -----内核实现
- 接口关闭:ifconfig eth0 down
- 通过 ioctl(SIOCSIFFLAGS) 来清除dev->flag 的 IFF_UP位——内核实现
- 调用dev->stop 方法——驱动实现
- 驱动的open方法
- 把硬件 (MAC) 地址从硬件设备拷贝到 dev->dev_addr
- 注册中断
- 启动接口传输队列
- void netif_start_queue(struct net_device *dev);
- snull示例
int snull_open(struct net_device *dev) {
memcpy(dev->dev_addr, "\0SNUL0", ETH_ALEN);
netif_start_queue(dev);
}
- 驱动的stop方法
- 注销中断
- 停止接口传输队列
- void netif_stop_queue(struct net_device *dev);
- snull示例
int snull_release(struct net_device *dev) {
netif_stop_queue(dev);
}
3、数据包发送
- 无论何时内核需要传送一个数据包, 它调用驱动的 hard_start_stransmit 方法将数据放在外发队列上,成功时返回0,不成功返回非0
- 每个内核处理的数据包都包含在一个 socket 缓存结构( 结构 sk_buff )skb里
- skb->data 指向要传送的数据包
- skb->len 指向要传送的数据包的长度
- 控制发送并发
- hard_start_xmit 函数由一个 net_device 结构中的自旋锁(xmit_lock)来保护避免并发调用,这样在函数未返回前不会出现对它的并发调用。
- hard_start_xmit 函数一返回,它有可能被再次调用。当软件完成指示硬件发送数据包后,该函数返回,但此时硬件传送可能还没有完成。因此驱动需要告知协议栈不要再启动发送,直到硬件准备好接收新的数据.
- 该告知是驱动通过调用 netif_stop_queue 来实现的
- 在中断中调用void netif_wake_queue(struct net_device *dev)告知协议栈可再次发送数据
- 如果从其他地方停止数据包的传送,不是 hard_start_xmit 函数,则使用的函数是:void netif_tx_disable(struct net_device *dev);这个函数类似 netif_stop_queue, 但是它还保证, 当它返回时, 你的 hard_start_xmit 方法没有在另一个 CPU 上运行。队列能够用 netif_wake_queue 重启
- 发送超时解决办法
- 设置定时器来处理这个问题。不过,网络驱动不需要自己去检测超时。它只需要设置
- 1个超时值(在 net_device 结构的 watchdog_timeo 成员,以 jiffies 计)
- 最后1个数据包的发送时间(在net_device 结构的 trans_start成员,以 jiffies 计) 。
- 如果发送超时,协议栈的网络层最终会发现,并调用驱动的 tx_timeout 方法。这个方法的工作是:
- 将引起发送超时的故障排除
- 并且保证任何已经开始的发送正确地完成。特别地, 驱动没有丢失追踪任何协议栈委托给它的 socket 缓存(即:需要发送的数据)
4、中断处理
- 硬件芯片(例如:网卡芯片)可能因为3种情况而触发中断:
- 硬件将一个外发数据包发送完成
- 一个新数据包到达硬件
- 网络接口也能够产生中断来指示错误, 例如状态改变
- 驱动的中断处理程序可以通过检查硬件芯片中的状态寄存器,能够得知是3种触发中断中的哪一种情况,然后
- 通知协议栈可重新启动发送队列netif_wake_queue(dev);
- 调用接收函数
- 处理错误
5、 数据包接收
数据包接收有2种模式:中断驱动和轮询。这里只介绍第中断驱动,在这种模式下,数据包接收函数是由中断处理程序来调用的。接收函数完成功能的流程如下:
1)分配一个缓存区来保存数据包.
- 缓存分配函数 (dev_alloc_skb) 需要知道数据长度,函数用这些信息来给缓存区分配空间。该信息来源于网卡的硬件寄存器
- dev_alloc_skb 使用 atomic 优先级调用 kmalloc , 因此它可以在中断时间安全使用
2)一旦有一个有效的 skb 指针, 通过调用 memcpy, 报文数据被拷贝到缓存区
- memcpy(skb_put(skb, pkt->datalen), pkt->data, pkt->datalen);
3)skb的dev、protocol、pkt_type 成员必须在缓存向上传递前赋值,使协议栈知道数据包的一些信息。以太网支持代码输出一个辅助函数 eth_type_trans(skb, dev) 来完成这个工作
- 通过skb->mac.raw的协议类型,发现一个合适的protocol值作为该函数的返回值
- 通过skb->mac.raw的目标mac与dev中的本机mac地址比较,来赋值给skb->pkt_type
- PACKET_HOST,是发给本机的包
- PACKET_OTHERHOST,不是发给本机的包
- PACKET_BROADCAST,广播包
- PACKET_MULTICAST,多播包
- 去掉mac头
4)指出IP校验和要如何进行,即:对skb->ip_summed赋值。其取值有3种:
- CHECKSUM_HW,硬件已经进行校验
- CHECKSUM_NONE,未进行校验,需要协议栈进行校验
- CHECKSUM_UNNECESSARY,不必进行校验(loop接口即是如此)
5)驱动更新它的统计计数(存放于私有数据区)来记录收到一个报文。统计结构由几个成员组成,最重要的是:
- rx_packet, 表示收到的报文数目
- rx_bytes, 表示收到的字节总数
- tx_bytes, 表示发送的字节总数
6)调用int netif_rx(struct sk_buff *skb)执行最后的接受数据包工作, 它递交 socket 缓存给上层。netif_rx 返回一个整数:
- NET_RX_SUCCESS(0) 意思是报文成功接收
- 任何其他值指示错误
- NET_RX_CN_LOW, NET_RX_CN_MOD,和 NET_RX_CN_HIGH 指出网络子系统的递增的拥塞级别
- NET_RX_DROP 意思是报文被丢弃
6、结构体 struct sk_buff及相关内核API
1)结构体 struct sk_buff的组成
struct sk_buff {} *skb;
可用缓存空间是 skb->end - skb->head, 有效数据(即:数据包)的空间是 skb->tail - skb->data
truesize:表示缓存区的整体长度,置为sizeof(struct sk_buff)加上传入alloc_skb()函数的长度(或dev_alloc_skb分配的数据缓存区长度)
2)分配Socket 缓存的API(对应的释放API用kfree替换alloc即可)
- struct sk_buff *alloc_skb(unsigned int len, int priority);
分配一个缓存区. alloc_skb 函数分配一个缓存并且将 skb->data 和 skb->tail 都初始化成 skb->head.
- struct sk_buff *dev_alloc_skb(unsigned int len);
dev_alloc_skb 函数是使用 GFP_ATOMIC 优先级调用 alloc_skb 的快捷方法, 并且在 skb->head 和 skb->data 之间保留了一些空间. 这个数据空间用在网络层之间的优化, 驱动不要动它
3)操纵Socket 缓存的API
- unsigned char *skb_put(struct sk_buff *skb, int len);
- unsigned char *__skb_put(struct sk_buff *skb, int len);
更新 sk_buff 结构中的 tail 和 len 成员; 它们用来增加数据到缓存的结尾, 每个函数的返回值是 skb->tail 的前一个值(换句话说, 它指向刚刚创建的数据空间). 两个函数的区别在于 skb_put 检查以确认数据适合缓存, 而 __skb_put 省略这个检查.
- unsigned char *skb_push(struct sk_buff *skb, int len);
- unsigned char *__skb_push(struct sk_buff *skb, int len);
递减 skb->data 和递增 skb->len 的函数. 它们与 skb_put 相似, 除了数据是添加到报文的开始而不是结尾. 返回值指向刚刚创建的数据空间. 这些函数用来在发送报文之前添加一个硬件头部. 又一次, __skb_push 不同在它不检查空间是否足够
- void skb_reserve(struct sk_buff *skb, int len);
将data 和 tail的值加上len.
三、snull网卡驱动代码分析
1、设备生成与注册
688 int snull_init_module(void)
689 {
694
695
696
697
698
703
704
705
706
707
708
714 }
在模块初始化函数中,695、697行初始化net_device结构体。struct snull_priv结构体用来存放一些私有数据,例如:统计数据。函数snull_init则设置net_device的各个重要字段。704行并将代表网卡的net_device结构体注册进操作系统。至此,网卡就存在于系统中,可以被使用了。
模块的卸载函数完成相反的功能——将net_device结构体从内核中注销,以及销毁net_device结构体
617 void snull_init(struct net_device *dev)
618 {
634
635
636
637
638
639
640
641
642
643
644
663 }
snull_init函数将net_device结构体的重要字段初始化为了驱动中实现的各个功能函数,因此
- 当ifconfig sn0 up打开网卡时,snull_open将被调用
- 当ifconfig sn1 down关闭网卡时,snull_release将被调用
- 当ifconfig查看网卡统计数据时,scull_stats将被调用
- 当传送数据包超过了5个jiffies仍然没有成功的话,snull_tx_timeout将被调用进行善后处理
- 当协议栈需要发送数据时,snull_tx将被调用
下面就来分析这几个功能函数。
2、设备打开、关闭与查询
scull_open完成:填写struct net_device结构体的dev_addr字段(mac地址);启动传输队列netif_start_queue(dev)
scull_release完成:停止传输队列netif_stop_queue(dev)
scull_stats完成:将网卡的统计数据返回给操作系统。之后操作系统将该信息返回给应用程序(例如:ifconfig)
78 struct snull_priv {
79
86
87
88 };
557 struct net_device_stats *snull_stats(struct net_device *dev)
558 {
559
560
561 }
3、数据的发送
需要发送数据时,协议栈会调用dev->hard_start_xmit。传入的第1个参数是socket缓存结构体(通过它可以找到需要发送的数据内容、长度等相关信息)的指针;第2个参数是对应于该网卡的net_device结构体。
- 509行获得实际要发送的数据,510行获得要发送数据的长度
- 517行在net_device结构体中设置发送开始时间,以便让协议栈能检测发送超时
- 协议栈一旦调用dev->hard_start_xmit将skb递交给驱动,就不会在协议栈中再保留该skb。因此520行将skb暂存起来,以便将来数据被硬件发送成功后,能在中断处理程序中释放该skb(不在发送函数中释放skb,是因为此时不能保证硬件能发送数据成功)
- 523行将要发送的数据提交给硬件
- 由于本程序驱动的是虚拟网卡,所以不会出现硬件发送数据不成功的情况。在真实的情况下,这种状况是有可能发生的,因此真实网卡驱动程序还会在523行调用netif_stop_queue通知协议栈暂停发送数据,直到硬件发送成功产生中断,在中断处理程序中调用netif_wake_queue通知协议栈重新启动发送
- 525行返回0,表示发送成功。若不为零则表示失败。
- 注:511--516行是预防协议栈下发的数据包长度未能达到以太网最小数据包的长度(#define ETH_ZLEN
60)
501 int snull_tx(struct sk_buff *skb, struct net_device *dev)
502 {
503
504
505
509
510
511
512
513
514
515
516
517
519
520
522
523
525
526 }
如果协议栈检测到发送超时(如果协议栈被netif_stop_queue(dev)通知暂停发送后,超过dev->watchdog_timeo个jiffies仍然没有被netif_wake_queue(dev)通知恢复传送,则协议栈认为在网卡dev上发生了发送超时),将会调用dev->tx_timeout。
- 533、540行更新网卡统计信息
- 541行恢复协议栈可以继续向网卡驱动提交数据
- 注:如果你要追求完美的话,应该在541行之前添加代码调用snull_tx完成重发操作,并在重发前获得dev->xmit_lock自旋锁,重发后释放自旋锁,以避免与协议栈调用dev->hard_start_xmit产生竞态
531 void snull_tx_timeout (struct net_device *dev)
532 {
533
540
541
543 }
4、中断处理程序
- 中断处理程序的注册一般放在驱动的open功能函数中。由于本程序驱动的是虚拟网卡,没有物理中断,所以就没有注册中断的代码。注:本程序使用定时器来模拟中断。
- 332-336行说明,对于真实硬件而言,应该首先检查是否发生的中断
- 由于数据包接收程序需要网卡对应的net_device结构体中的一些字段(例如:dev、dev_addr,具体参见接收程序),所以337行去获得它。当然337行要达到目的,要求我们在注册中断时要用网卡对应的net_device结构体指针作为request_irq的第5个参数(这一点不难做到,因为open被调用时,net_device结构体指针是传入参数)
- 348行表明对于真实设备,此时应该读取硬件获得中断的原因(硬件接收完成?硬件发送完成?硬件错误?),本程序用349行模拟
- 351-358处理接收中断。注:如果是真实硬件,只需要简单的调用snull_rx(dev)即可
- 359-364处理发送中断。361-362更新统计信息;由于硬件已经成功发送数据包,所以363行可以放心大胆地释放skb的空间(发送函数曾暂存该skb)
327 static void snull_regular_interrupt(int irq, void *dev_id, struct pt_regs *regs)
328 {
329
330
331
332
337
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363