Linux驱动开发学习笔记

来源:互联网 发布:人大网络教育学生登录 编辑:程序博客网 时间:2024/03/29 21:32

一、 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。

 

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/MulinB/archive/2009/05/05/4153056.aspx