编写linux设备驱动程序

时间:2023-01-20 16:16:03

http://blog.csdn.net/dongliqiang2006/article/details/4495804

http://edu.stuccess.com/knowcenter/KernelTech/device_driver/00000002.htm 

一.总观设备驱动程序:系统调用是操作系统内核和应用程序之间的接口,而设备驱动程 
序是操作系统内核和设备硬件之间的接口,设备驱动程序为应用程序屏蔽了硬件的细节 
,这样,在应用程序看来,硬件设备只是一个设备文件(所有设备都在/dev下),应用程 
序可以象操作普通文件一样对硬件设备进行操作,可以使用open,read,等系统调用象操 
作普通文件一样操作设备文件,如open(“/dev/consle”,O_RDONLY) 等。此外,设备驱 
动程序是内核的一部分,它需要完成以下功能: 
1.对设备初始化和释放。 
2.把数据从内核传送到硬件和从硬件读取数据.(内核和设备之间的数据传递)。 
3.读取应用程序传送给设备文件的数据和回送应用程序请求的数据(内核空间和用户空 
间的数据传递)。 
4.检测和处理设备出现的错误。 
二.设备驱动程序的分类:1、字符设备驱动程序(如键盘、打印机驱动程序) 
2、块设备驱动程序(如磁盘,USB驱动程序) 
3、网络设备驱动程序(如网卡,MODOME驱动程序) 
三.驱动程序的组成:(1) 设备配置和初始化子程序,负责检测所要驱动的硬件设备是 
否存在和是否能正常工作。如果该 
设备正常,则对该设备及其他的必需的条件(如中断、DMA通道)进行(申请并)初始化 
。当然这部分仅在初始化的时候被调用一次。 
(2)驱动程序的上半部分(top half)(响应I/O请求程序)。调用这部分是由于系统调用 
的结果:这部分程序在执行的时候,系统仍认为是和进行调用的进程属于同一个进程, 
只是由用户空间变成了内核空间,但仍具有调用此系统调用的用户程序的运行环境,因 
此可以在其中调用sleep()、schedul()等与进程运行环境有关的函数,但是由于这部分 
是运行在系统不安全时间内的(中断处于关闭状态),所以要求程序尽可能的快,更多 
的事情留给下面即将介绍的底半部分(bottom half)来处理。 
(3)驱动程序的底半部分(bottom half)(中断服务子程序)。系统接收硬件中断,再 
由操作系统调用中断服务子程序。中断可以产生在任何一个进程运行的时候,因此在中 
断服务程序被调用的时候,不能依赖于任何进程的状态,也就不能调用任何sleep(),sc 
hedule()等与进程运行环境有关的函数。而且因为设备驱动程序一般支持同一类型的若 
干设备,所以一般在系统调用中断服务子程序的时候,都带有一个或多个参数,以唯一 
标识请求服务的设备(如i_rdev用来区别设备)。 
四.设备驱动程序的编写:内核提供一系列的函数接口(内核接口(kernel interface 
)),在内核接到用户系统调用的时候调用这些函数,所以驱动程序的主要任务就是实 
现这些函数(包括open,read等等)。此外任何驱动程序都得 
包含int init_module(),void cleanup_module()函数,其作用不言而 
喻,在此不多赘述。 
I.字符设备驱动程序:内核提供了操作字符设备的函数接口,由file_operations结构 
封装。起具体内容如下: 
#include <linux/fs.h> 
struct file_operations { 
int (*lseek)(struct inode *inode,struct file *filp, off_t off,int pos); 
int (*read)(struct inode *inode,struct file *filp, char *buf, int count); 
int (*write)(struct inode *inode,struct file *filp, char *buf,int count); 
int (*readdir)(struct inode *inode,struct file *filp, struct dirent *dirent, 
int count); 
int (*select)(struct inode *inode,struct file *filp, int sel_type,select_tab 
le *wait); 
int (*ioctl) (struct inode *inode,struct file *filp, unsigned int cmd,unsign 
ed int arg); 
int (*mmap) (void); 
int (*open) (struct inode *inode, struct file *filp); 
void (*release) (struct inode *inode, struct file *filp); 
int (*fsync) (struct inode *inode, struct file *filp); 
}; 
驱动程序就是要实现这些只有函数名,而无函数体的函数接口,以便响应用户的系统调 
用。比如,在用户程序里,调用 
read系统调用,内核收到这样的调用,就查找对应的文件(read操作的文件对象)相关 
的设备的驱动程序里的read所 
对应的函数来实现用户想要的操作。简单的实现: 
int scull_read(struct inode * inode,struct file*filp,char * buf,int count) 

int len = 0; 
len += sprintf(buf,”You are in read function for Device Driver”); 
return len; 
}/*这里的buf,count都是用户的read系统调用里的参数,内核只是作为中介传递*/ 
int scull_open(strcut inode*inode,struct file * filp) 

MOD_INC_USE_COUNT; 
}/*这里的记数用来在关闭设备时使用,只有当使用记数为0时,才能正直的关闭,如卸 
载模块只有在使用记数 
为0是才能卸载,否则会出现device busy这样的提示,而且不能卸载*/ 
int scull_release(struct inode * inode,struct file * filp) 

MOD_DEC_USE_COUNT; 
}/*执行scull_open(…)与相反的操作*/ 
struct file_operations scull_fops = { 
open: scull_open, 
read: scull_read; 
release: scull_release 
}/*在2.4内核中支持标记化的“赋值”*/ 
设备驱动程序需要象内核注册一个主设备号和设备名字,通过如下实现: 
register_chrdev(int MajorNumber,char *DeviceName,struct file_operations * 
);其中当MajorNumber为0时,为动态注册 
主设备号,这样一般是成功的,而指定主设备号则有可能因为与已注册设备号冲突而失 
败,可以通过cat /proc/device查看主设备号注册情况。 
我们可以这样注册我们的简单程序: 
int init_module() 

register_chrdev(MyMajorNum,”MyDevice”,&scull_fops); 
return 0; 

这样就把设备驱动程序加载到了内核!卸载设备可以如下操作: 
void cleanup_module() 

unregister_chrdev(MyMajorNum,”MyDevice”); 

编号驱动程序之后加载设备驱动程序insmod modulename.o,卸载rmmod modulename(用 
lsmod查看已经加载的模块)。然后挂载设备相关的文件mknod /dev/filename c MyMaj 
orNum minor(次设备号,可以自己指定,如1、2等),如果在注册的时候是使用动态注册 
(MyMajorNum为0),则可以通过cat /proc/devices | awk “ /$2 ==/”modulename/ 
” {print $1 } “ 来获得主设备号!测试程序: 
int main() 

char buf[40]; 
int count; 
int fd = open(“/dev/name”,O_RDONLY); 
count = read(fd,buf,40); 
printf(“read buf: %s /n”,buf); 
close(fd); 
return 0; 

运行这段程序会得到这样的结果:You are in read function for Device Driver 
,这不就是我们在驱动程序里的scull_read函数的实现部分吗,有什么感想呢? 
II.块设备驱动程序:设备驱动的编写比字符设备驱动的编写稍微复杂一点,有几个问 
题需要注意(系统资源的分配): 
I/O端口的分配、中断号的申请、内存的分配(包括内存区域的划分)。 
1.I/O端口的分配:内核给我们提供了两个函数,用来检查和申请I/O区域,分别是che 
ck_region(unsigned long address 
,int size);address为端口起始地址,size为区域大小,返回值为0就是I/O区 
域没有被占用,否则是被占用! 
request_region(unsigned long address,int size,char *devicename);当检查I/O区 
域没有被占用的时候,可以使用它来申请I/O区域。 
2.中断号的申请:中段线是系统宝贵而有限的资源(PC机只有15根)。所以要使用中断 
线,就得进行中断线的申请,就是IRQ(Interrupt Requirement),我们也常把申请一条 
中断线称为申请一个IRQ或者是申请一个中断号。 
中段线是非常宝贵的,所以只有当设备打开(被使用)的时候才申请占用一个IRQ,或者 
是在申请IRQ时采用共享中断的方式,这样可以让更多的设备使用中断。 
中断号的申请主要过程为: 
1.将所有的中断线探测一遍,看看哪些中断还没有被占用。那些没有被占用(或者是共 
享)的中断线作为备选线。 
2.通过中断申请函数(request_irq(…))申请选定的IRQ,还要指定申请的方式是独占 
还是共享。 
3.返回值为0表示成功,否则可以重新申请,或者返回错误。不过一般都有约定,比如 
软盘的为2号中断线,所以 大多数的情况下是可以成功返回的(毕竟我们先是做了检测 
的)。 
3.内存的分配:内存分为高端、常规和DMA内存,DMA内存不用多说,只能用于DMA方式 
,而且它会避开MMU内存管理,不需要CPU的直接参与,此外由于DMA的通道是不能共享的 
,而PC机只有7条通道,所以也存在DMA通道的申请问题。块设备驱动程序必须使用缓冲 
区(和字符设备的最大区别),所以必须申请较大的“逻辑连续”内存块,当不能申请 
到内存时(内核内存),内核要么是返回错误,要么是等待,直到有了足够的内存,再 
予以分配!由于kmalloc(…)只能最多申请到128K的内存,所以在块设备驱动程序中申请 
缓冲区的时候都是使用get_free_pages(…)、get_free_page(…)等一次分配一页,或者 
是多页的函数,同时为了防止出错,总是在申请到了缓冲区的时候就用0填充!同时,内 
存的申请一般在常规内存,当然可以通过设置诸如__GFP_HIGHMEM,__GFP_DMA等标志来请 
求分配其他区段的内存。 
简单的实现:内核提供了操作块设备的函数接口,由block_device_operations结构封装 
,起具体内容如下: 
struct block_device_operations{ 
int (*open)(struct inode * inode, struct file * filp); 
int (*release)(struct inode * inode,struct file * filp); 
int (*check_media_change)(k_devt i_rdev); 
int (*revalidate)(k_devt i_rdev); 
};不知道你注意到了没有,在这个结构体里并没有read、write这样的数据传递函数,这 
是因为块设备不是象字符设备那样不需要缓冲区,它是通过中断请求,然后经由缓冲区 
在用户和设备之间进行数据的传递。块设备的数据传递方式可以分为三种:没有队列( 
no queue ,同步)、单个队列(single queue)、多队列(multi queue)。这里有一个细节 
,并不是内核没完成一次请求就释放掉内存,这样容易造成内存碎片(memery fragmen 
t),而是在空闲内存达到一定程度才一起释放(至于到底什么程度就和CPU有关了,关 
系到CPU的分页机制),不过这个是由内核完成的,驱动程序并不要实现它。一般在ope 
n(…)函数里进行资源的探测和申请及打开设备,在release(…)实现相反的操作,可以参 
看字符设备,这里不在赘述。 
函数check_media_change(…)用来支持可以动设备,检查自上次修改以来设备有没有 
发生改变,没有则返回0,否则为1,这里有个小技巧,就是定时过期(改动),PC机的 
可移动存储器就是采用这种策略。函数revalidate(…)用来重新初始化设备。如下注册 
块设备: 
int init_module() 

…… 
register_blkdev(int MyMajorNum,struct block_device_operations *,char * De 
viceName); 
….. 

其他的步骤和字符设备差不多,这里不再赘述。 
III.网络设备驱动程序:网络设备在接受到数据或者是发送完数据的时候都要中断CPU 
。网络设备接受到的数据都是放在由struct sk_buff结构组成的缓冲区里,然后通知用 
户层把数据拿走。用户想要发送的数据也是先送到这样的缓冲区然后在发送出去。至于 
缓冲区的释放,和块设备驱动一样。其它诸如中断线的申请,内存的分配和块设备都差 
不多。网络设备和块设备驱动程序的难处是在硬件设备,但这不属于本文的范畴。注册 
网络设备就是在由内核维护的网络设备链表中加入一个代表当前网络设备的数据接个( 
struct net_device ),同时内核也提供了一些操纵网络设备的函数,封装在struct n 
et_device 结构里,这在头文件linux/net_device.h里可以看到,现列举如下: 
int (*open)(struct net_device * dev); 
int (*stop)(struct net_device * dev); 
int (*hard_start_xmit)(struct sk_buff * skb,stuct net_device * dev); 
int (*hard_header)(struct sk_buff * skb,struct net_device * dev, unsigned sh 
ort types,void * daddr,void * saddr,unsigned len); 
int (*rebuild_header)(struct sk_buff * skb); 
void (*tx_timeout)(struct net_device * dev); 
struct net_device_states*(*get_states)(struct net_device * dev); 
int (*set_config)(struct net_device* dev,struct ifmap *map); 
int (*do_ioctl)(struct net_device *dev,struct ifreq * ifr,int cmd); 
void (*set_multicast_list)(struct net_device * dev); 
int (*set_mac_address)(struct net)device * dev,void * addr); 
int (*change_mtu)(struct net_device * dev,int new_mtu); 
int (*header_cache)(struct neighbour * neigh,struct hh_cache *hh); 
int (*header_cache_update)(stuct hh_cache *hh,struct net_device * dev,unsign 
ed char * haddr); 
int (*hard_header_parse)(struct sk_buff * skb,unsigned char * haddr); 
在驱动程序里主要就是按照自己的目的来实现这些函数,此外就是硬件的探测和资源的 
申请。 
网络设备的注册为:register_netdev(struct net_device);其它不再赘述! 
五.设备驱动的展望:设备驱动程序除了完成必要的功能外,我们还可以实现其它的“ 
附加”的功能,最好实现的就是数据的截取,如网络数据的截取。在实现网络设备的时 
候,数据都是通过一struct sk_buff结构描述的缓冲区做为中介来实现数据的双向传递 
,如果要截取来自外面发向机器,或者机器发给外面的数据,就可以从数据缓冲区考虑 
,在完成用户任务的同时截取想要的数据发到一个“库”。也可以把不想要的数据拒只 
“门”外,或者是不小心发送的东西给“留下”,从而防止“不恰当”的操作。 
六.参考资料:《linux设备驱动程序》 中国电力出版社