一、 Linux设备驱动基础(基于Linux2.6内核)(参考《Linux Device Drivers 3rd edition》)
Linux Kernel有一个很好的特性,可以支持在运行是进行扩展。这意味着系统启动运行是,我们仍然可以向kernel添加功能。这种运行时可以被添加到kernel的代码称为Module(模块)。
Linux Kernel支持好几种模块类型,包括设备驱动程序。每个模块由目标代码组成,不是一个完整的可执行程序。系统运行时,我们可以通过insmod将模块连接到正在运行的内核中去。也可以使用lsmod列出已加载模块,rmmod或modprobe –r 移除模块。
Linux系统将设备分为三种基本类型:字符设备,块设备,网络接口。
字符设备是能够像字节流一样被访问的设备,一般只能顺序访问。其操作类似文件操作。
块设备上能够容纳文件系统,可以通过文件系统随机访问。其操作也类似于文件操作。
网络接口是负责数据包的传输和接收的,一般无法影射到文件系统的节点。它与内核的通信跟前面两种设备不同,而是通过socket方式。在系统和驱动程序之间定义有专门的数据结构(sk_buff)进行数据的传递。系统里支持对发送数据和接收数据的缓存,提供流量控制机制,提供对多协议的支持。
在编写模块的时候,应该注意,模块仅仅被连接到内核,所以它只能调用由内核导出的那些函数,而不能调用其他的本模块未定义的函数。
在Linux kernel2.6.X下进行模块开发时,需要预先准备好“kernel tree(内核树)”,即获得与本系统相同的内核的源代码并编译出目标文件。
一个最简单的hello world驱动例子:
///
hello_world.c:
#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("Dual BSD/GPL");
static int hello_init(void)
{
printk(KERN_ALERT "Hello, world/n");
return 0;
}
static void hello_exit(void)
{
printk(KERN_ALERT "Goodbye, cruel world/n");
}
module_init(hello_init);
module_exit(hello_exit);
///
Makefile:
obj-m := hello.o
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
///
其中,源文件中的module_init和module_exit指定了模块被加载时执行的初始化函数和卸载时执行的清理函数。另外可以使用module_param指定加载模块时可以设置的参数。Makefile中的obj-m指定了使用make modules时候构造*.ko目标文件时使用的*.o目标文件。
二、 Linux网卡驱动基础(参考《Linux Device Drivers 3rd edition》)
当一个模块被装载到正在运行的内核中时,它要请求资源并提供一些功能设施,比如探测其设备和硬件位置(I/O端口和IRQ线)、检测到接口时向全局网络设备链表中插入一个net_device数据结构。
1. 分配net_device
每个接口都由一个net_device结构体描述,该结构体的分配及其中的很多内核相关的字段由函数alloc_netdev()进行初始化。内核提供了对该函数针对特定网络接口类型的一些封装:
alloc_etherdev: 初始化以太网对应的接口,其中调用了alloc_netdev()和ether_setup()初始化了许多字段。常用。
alloc_fddidev: FDDI设备。
alloc_trdev: 令牌环设备。
net_device中包括如下几类信息:
全局信息
硬件信息(驱动内部使用的私有数据)
接口信息
设备方法(操作设备的接口函数)
工具成员
2. 进一步初始化net_device
然后,需要根据硬件的特定需求进行进一步初始化。其中包括将net_device中的很多函数指针(设备方法)指向我们自己定义的一些函数。如:
dev->hard_start_xmit = our_netdevice_function_tx; //发包函数
dev->get_stats = our_netdevice_function_stats; //统计函数
……
另外,本设备需要用到的一些私有数据(不是所有的net device都有的数据)可以存放到dev->priv字段指向的一个自定义结构体中。
3. 注册net_device
初始化完net_device后,可以调用register_netdev()注册网络接口(其中会调用dev->init来进一步初始化net_device,所以可以将最后需要初始化的内容放到dev->init中去做),这时,就可以通过调用驱动程序操作设备了。
4. open和close
通过ifconfig打开网络接口时,会调用dev->open。关闭网络接口时,会调用dev->close。
在open中,驱动程序要请求必要的系统资源,然后调用内核函数netif_start_queue()启动传输队列,通知内核可以通过网络设备发送数据包,允许接口开始发送(transmit)数据包。
在close中,做与open相反的操作。其中包括netif_stop_queue()函数,通知内核停止通过设备发送(transmit)数据包。
5. 数据包发送(transmit)
内核要发送一个数据包时,都会调用驱动中的dev->hard_start_transmit函数来将数据放入到外发队列中。内核传给驱动的数据包都位于一个socket buffer(sk_buff,简称skb)中。skb是一个复杂的数据结构,内核提供了很多函数操作它。但驱动中的dev->hard_start_transmit函数传输它时,不需要做任何修改,因为此时它已经有了完整的报头。skb->data指向要传输的数据,skb->len是数据的长度(以字节octet为单位)。
dev->hard_start_transmit需要调用硬件相关的函数来将数据发送出去,如果传输成功,释放skb,否则需要重新传输。
在传输过程中,如果硬件设备的缓冲区即将耗尽,这时说明内核向设备发送数据包过快,这时驱动可以调用内核函数netif_stop_queue(),通知内核暂时停止向设备发送数据包。在未来某个时刻,可以通过netif_wake_queue()重新启动队列。(另外注意函数netif_tx_disable。)
传输超时:通过设置dev->watchdog_timeo的成员,可以设置传输超时时间,超过超时时间时,dev->tx_timeout函数会被调用。可以在这个函数里重置传输状态。
6. 数据包接收(receive)
接收数据比发送数据稍微复杂一些,因为必须在原子上下文(GFP_ATOMIC--kmalloc)中分配一个sk_buff并传递给上层处理。有两种模式接收数据包:中断(interrupt)和轮询(poll)。
在接收函数中,当接收到数据包时,首先通过dev_alloc_skb()函数分配skb,然后用memcpy将获得的数据(数据包一般从硬件中获得)拷贝到skb中(这里有一种优化策略,是可以在数据包到达前分配skb,然后指示接口当数据包到达时直接放入skb,省去拷贝过程),然后更新net_device中的统计计数器,最后,通过netif_rx()函数将skb传递给上层软件处理。
7. 关于接口中断处理程序
接口设备在两种事件下产生中断:新数据包到达,外发数据包传输完成。另外,当产生错误、连接状态改变时也会产生中断。
在中断处理程序中,可以通过某种方法判断出中断类型,如果是新数据包到达或者外发数据包完成,需要做如下处理:
新数据包到达--中断:调用接收函数。(上面可以看到,接收函数并不在net_device的设备方法之中。)
外发数据包完成--中断:调用dev_kfree_skb()函数释放skb。
8. 不使用中断接收数据,使用轮询(NAPI)
在数据流量较大时,如果使用中断方式接收数据包会使CPU负荷比较大。这时可以修改中断处理程序,在收到一个接收数据的中断时,禁用中断,然后启用轮询接口。
启用轮询接口时,应该调用netif_rx_schedule(dev)函数。这个函数会在以后的某个时间点调用dev->poll函数。
dev->poll函数的原型如下:
int (*poll) (struct net_device* dev, int* budget);
在这个函数中,将skb交给上层处理应该调用函数netif_receive_skb()而不是netif_rx()。结束轮询时调用netif_rx_complete()函数。
9. 链路状态
内核需要通过驱动程序了解网络链路是否正常。硬件能感知到线路上的载波信号是否正常,驱动程序检测到硬件上的载波信号状态发生改变时,可以通过下面的内核函数通知内核:
netif_carrier_off(dev): 通知内核载波消失,链路不能正常工作。
netif_carrier_on(dev): 通知内核载波出现。
10. sk_buff详述(参见<linux/skbuff.h>)
11. 模块卸载
先调用unregister_netdev(),然后调用free_netdev()释放net_device。