1,两大结构体:
(1)cdev结构体
在linux2.6内核中,使用cdev结构体描述一个字符设备,定义如下:
- structcdev{
- structkobjectkobj;//内嵌的kobject对象
- structmodule*owner;//所属模块
- structfile_operations*ops;//文件操作结构体
- structlist_headlist;
- dev_tdev;//设备号,长度为32位,其中高12为主设备号,低20位为此设备号
- unsignedintcount;
- };
(2)file_operations结构体
结构体file_operations在头文件linux/fs.h中定义,用来存储驱动内核模块提供的对设备进行各种操作的函数的指针。这些函数实际会在应用程序进行linux的open(),write(),read(),close()等系统调用时最终被调用。该结构体的每个域都对应着驱动内核模块用来处理某个被请求的事务的函数地址。源代码(2.6.28.7)如下:
- structfile_operations{
- structmodule*owner;
- loff_t(*llseek)(structfile*,loff_t,int);
- ssize_t(*read)(structfile*,char__user*,size_t,loff_t*);
- ssize_t(*write)(structfile*,constchar__user*,size_t,loff_t*);
- ssize_t(*aio_read)(structkiocb*,conststructiovec*,unsignedlong,loff_t);
- ssize_t(*aio_write)(structkiocb*,conststructiovec*,unsignedlong,loff_t);
- int(*readdir)(structfile*,void*,filldir_t);
- unsignedint(*poll)(structfile*,structpoll_table_struct*);
- int(*ioctl)(structinode*,structfile*,unsignedint,unsignedlong);
- long(*unlocked_ioctl)(structfile*,unsignedint,unsignedlong);
- long(*compat_ioctl)(structfile*,unsignedint,unsignedlong);
- int(*mmap)(structfile*,structvm_area_struct*);
- int(*open)(structinode*,structfile*);
- int(*flush)(structfile*,fl_owner_tid);
- int(*release)(structinode*,structfile*);
- int(*fsync)(structfile*,structdentry*,intdatasync);
- int(*aio_fsync)(structkiocb*,intdatasync);
- int(*fasync)(int,structfile*,int);
- int(*lock)(structfile*,int,structfile_lock*);
- ssize_t(*sendpage)(structfile*,structpage*,int,size_t,loff_t*,int);
- unsignedlong(*get_unmapped_area)(structfile*,unsignedlong,unsignedlong,unsignedl ong,unsignedlong);
- int(*check_flags)(int);
- int(*dir_notify)(structfile*filp,unsignedlongarg);
- int(*flock)(structfile*,int,structfile_lock*);
- ssize_t(*splice_write)(structpipe_inode_info*,structfile*,loff_t*,size_t,unsig nedint);
- ssize_t(*splice_read)(structfile*,loff_t*,structpipe_inode_info*,size_t,unsignedint);
- int(*setlease)(structfile*,long,structfile_lock**);
- };
- structmodule*owner;
- /*第一个file_operations成员根本不是一个操作;它是一个指向拥有这个结构的模块的指针.
- 这个成员用来在它的操作还在被使用时阻止模块被卸载.几乎所有时间中,它被简单初始化为
- THIS_MODULE,一个在中定义的宏.这个宏比较复杂,在进行简单学习操作的时候,一般初始化为THIS_MODULE。*/
- loff_t(*llseek)(structfile*filp,loff_tp,intorig);
- /*(指针参数filp为进行读取信息的目标文件结构体指针;参数p为文件定位的目标偏移量;参数orig为对文件定位
- 的起始地址,这个值可以为文件开头(SEEK_SET,0,当前位置(SEEK_CUR,1),文件末尾(SEEK_END,2))
- llseek方法用作改变文件中的当前读/写位置,并且新位置作为(正的)返回值.
- loff_t参数是一个"longoffset",并且就算在32位平台上也至少64位宽.错误由一个负返回值指示.
- 如果这个函数指针是NULL,seek调用会以潜在地无法预知的方式修改file结构中的位置计数器(在"file结构"一节中描述).*/
- ssize_t(*read)(structfile*filp,char__user*buffer,size_tsize,loff_t*p);
- /*(指针参数filp为进行读取信息的目标文件,指针参数buffer为对应放置信息的缓冲区(即用户空间内存地址),
- 参数size为要读取的信息长度,参数p为读的位置相对于文件开头的偏移,在读取信息后,这个指针一般都会移动,移动的值为要读取信息的长度值)
- 这个函数用来从设备中获取数据.在这个位置的一个空指针导致read系统调用以-EINVAL("Invalidargument")失败.
- 一个非负返回值代表了成功读取的字节数(返回值是一个"signedsize"类型,常常是目标平台本地的整数类型).*/
- ssize_t(*aio_read)(structkiocb*,char__user*buffer,size_tsize,loff_tp);
- /*可以看出,这个函数的第一、三个参数和本结构体中的read()函数的第一、三个参数是不同的,
- 异步读写的第三个参数直接传递值,而同步读写的第三个参数传递的是指针,因为AIO从来不需要改变文件的位置。
- 异步读写的第一个参数为指向kiocb结构体的指针,而同步读写的第一参数为指向file结构体的指针,每一个I/O请求都对应一个kiocb结构体);
- 初始化一个异步读--可能在函数返回前不结束的读操作.如果这个方法是NULL,所有的操作会由read代替进行(同步地).
- (有关linux异步I/O,可以参考有关的资料,《linux设备驱动开发详解》中给出了详细的解答)*/
- ssize_t(*write)(structfile*filp,constchar__user*buffer,size_tcount,loff_t*ppos);
- /*(参数filp为目标文件结构体指针,buffer为要写入文件的信息缓冲区,count为要写入信息的长度,
- ppos为当前的偏移位置,这个值通常是用来判断写文件是否越界)
- 发送数据给设备.如果NULL,-EINVAL返回给调用write系统调用的程序.如果非负,返回值代表成功写的字节数.
- (注:这个操作和上面的对文件进行读的操作均为阻塞操作)*/
- ssize_t(*aio_write)(structkiocb*,constchar__user*buffer,size_tcount,loff_t*ppos);
- /*初始化设备上的一个异步写.参数类型同aio_read()函数;*/
- int(*readdir)(structfile*filp,void*,filldir_t);
- /*对于设备文件这个成员应当为NULL;它用来读取目录,并且仅对文件系统有用.*/
- unsignedint(*poll)(structfile*,structpoll_table_struct*);
- /*(这是一个设备驱动中的轮询函数,第一个参数为file结构指针,第二个为轮询表指针)
- 这个函数返回设备资源的可获取状态,即POLLIN,POLLOUT,POLLPRI,POLLERR,POLLNVAL等宏的位“或”结果。
- 每个宏都表明设备的一种状态,如:POLLIN(定义为0x0001)意味着设备可以无阻塞的读,POLLOUT(定义为0x0004)意味着设备可以无阻塞的写。
- (poll方法是3个系统调用的后端:poll,epoll,和select,都用作查询对一个或多个文件描述符的读或写是否会阻塞.
- poll方法应当返回一个位掩码指示是否非阻塞的读或写是可能的,并且,可能地,提供给内核信息用来使调用进程睡眠直到I/O变为可能.
- 如果一个驱动的poll方法为NULL,设备假定为不阻塞地可读可写.
- (这里通常将设备看作一个文件进行相关的操作,而轮询操作的取值直接关系到设备的响应情况,可以是阻塞操作结果,同时也可以是非阻塞操作结果)*/
- int(*ioctl)(structinode*inode,structfile*filp,unsignedintcmd,unsignedlongarg);
- /*(inode和filp指针是对应应用程序传递的文件描述符fd的值,和传递给open方法的相同参数.
- cmd参数从用户那里不改变地传下来,并且可选的参数arg参数以一个unsignedlong的形式传递,不管它是否由用户给定为一个整数或一个指针.
- 如果调用程序不传递第3个参数,被驱动操作收到的arg值是无定义的.
- 因为类型检查在这个额外参数上被关闭,编译器不能警告你如果一个无效的参数被传递给ioctl,并且任何关联的错误将难以查找.)
- ioctl系统调用提供了发出设备特定命令的方法(例如格式化软盘的一个磁道,这不是读也不是写).另外,几个ioctl命令被内核识别而不必引用fops表.
- 如果设备不提供ioctl方法,对于任何未事先定义的请求(-ENOTTY,"设备无这样的ioctl"),系统调用返回一个错误.*/
- int(*mmap)(structfile*,structvm_area_struct*);
- /*mmap用来请求将设备内存映射到进程的地址空间.如果这个方法是NULL,mmap系统调用返回-ENODEV.
- (如果想对这个函数有个彻底的了解,那么请看有关“进程地址空间”介绍的书籍)*/
- int(*open)(structinode*inode,structfile*filp);
- /*(inode为文件节点,这个节点只有一个,无论用户打开多少个文件,都只是对应着一个inode结构;
- 但是filp就不同,只要打开一个文件,就对应着一个file结构体,file结构体通常用来追踪文件在运行时的状态信息)
- 尽管这常常是对设备文件进行的第一个操作,不要求驱动声明一个对应的方法.如果这个项是NULL,设备打开一直成功,但是你的驱动不会得到通知.
- 与open()函数对应的是release()函数。*/
- int(*flush)(structfile*);
- /*flush操作在进程关闭它的设备文件描述符的拷贝时调用;它应当执行(并且等待)设备的任何未完成的操作.
- 这个必须不要和用户查询请求的fsync操作混淆了.当前,flush在很少驱动中使用;
- SCSI磁带驱动使用它,例如,为确保所有写的数据在设备关闭前写到磁带上.如果flush为NULL,内核简单地忽略用户应用程序的请求.*/
- int(*release)(structinode*,structfile*);
- /*release()函数当最后一个打开设备的用户进程执行close()系统调用的时候,内核将调用驱动程序release()函数:
- voidrelease(structinodeinode,structfile*file),release函数的主要任务是清理未结束的输入输出操作,释放资源,用户自定义排他标志的复位等。
- 在文件结构被释放时引用这个操作.如同open,release可以为NULL.*/
- int(*synch)(structfile*,structdentry*,intdatasync);
- //刷新待处理的数据,允许进程把所有的脏缓冲区刷新到磁盘。
- int(*aio_fsync)(structkiocb*,int);
- /*这是fsync方法的异步版本.所谓的fsync方法是一个系统调用函数。系统调用fsync
- 把文件所指定的文件的所有脏缓冲区写到磁盘中(如果需要,还包括存有索引节点的缓冲区)。
- 相应的服务例程获得文件对象的地址,并随后调用fsync方法。通常这个方法以调用函数__writeback_single_inode()结束,
- 这个函数把与被选中的索引节点相关的脏页和索引节点本身都写回磁盘。*/
- int(*fasync)(int,structfile*,int);
- //这个函数是系统支持异步通知的设备驱动,下面是这个函数的模板:
- staticint***_fasync(intfd,structfile*filp,intmode)
- {
- struct***_dev*dev=filp->private_data;
- returnfasync_helper(fd,filp,mode,&dev->async_queue);//第四个参数为fasync_struct结构体指针的指针。
- //这个函数是用来处理FASYNC标志的函数。(FASYNC:表示兼容BSD的fcntl同步操作)当这个标志改变时,驱动程序中的fasync()函数将得到执行。
- }
- /*此操作用来通知设备它的FASYNC标志的改变.异步通知是一个高级的主题,在第6章中描述.
- 这个成员可以是NULL如果驱动不支持异步通知.*/
- int(*lock)(structfile*,int,structfile_lock*);
- //lock方法用来实现文件加锁;加锁对常规文件是必不可少的特性,但是设备驱动几乎从不实现它.
- ssize_t(*readv)(structfile*,conststructiovec*,unsignedlong,loff_t*);
- ssize_t(*writev)(structfile*,conststructiovec*,unsignedlong,loff_t*);
- /*这些方法实现发散/汇聚读和写操作.应用程序偶尔需要做一个包含多个内存区的单个读或写操作;
- 这些系统调用允许它们这样做而不必对数据进行额外拷贝.如果这些函数指针为NULL,read和write方法被调用(可能多于一次).*/
- ssize_t(*sendfile)(structfile*,loff_t*,size_t,read_actor_t,void*);
- /*这个方法实现sendfile系统调用的读,使用最少的拷贝从一个文件描述符搬移数据到另一个.
- 例如,它被一个需要发送文件内容到一个网络连接的web服务器使用.设备驱动常常使sendfile为NULL.*/
- ssize_t(*sendpage)(structfile*,structpage*,int,size_t,loff_t*,int);
- /*sendpage是sendfile的另一半;它由内核调用来发送数据,一次一页,到对应的文件.设备驱动实际上不实现sendpage.*/
- unsignedlong(*get_unmapped_area)(structfile*,unsignedlong,unsignedlong,unsignedlong,unsignedlong);
- /*这个方法的目的是在进程的地址空间找一个合适的位置来映射在底层设备上的内存段中.
- 这个任务通常由内存管理代码进行;这个方法存在为了使驱动能强制特殊设备可能有的任何的对齐请求.大部分驱动可以置这个方法为NULL.[10]*/
- int(*check_flags)(int)
- //这个方法允许模块检查传递给fnctl(F_SETFL...)调用的标志.
- int(*dir_notify)(structfile*,unsignedlong);
- //这个方法在应用程序使用fcntl来请求目录改变通知时调用.只对文件系统有用;驱动不需要实现dir_notify.
2,字符设备驱动程序设计基础
2-1主设备号和次设备号(二者一起为设备号):
一个字符设备或块设备都有一个主设备号和一个次设备号。主设备号用来标识与设备文件相连的驱动程序,用来反映设备类型。次设备号被驱动程序用来辨别操作的是哪个设备,用来区分同类型的设备。
linux内核中,设备号用dev_t来描述,2.6.28中定义如下:
typedefu_longdev_t;
在32位机中是4个字节,高12位表示主设备号,低12位表示次设备号。
可以使用下列宏从dev_t中获得主次设备号:也可以使用下列宏通过主次设备号生成dev_t:
- MAJOR(dev_tdev);
- MKDEV(intmajor,intminor);
- MINOR(dev_tdev);
2-2分配设备号(两种方法):
(1)静态申请:
- intregister_chrdev_region(dev_tfrom,unsignedcount,constchar*name);
(2)动态分配:
- intalloc_chrdev_region(dev_t*dev,unsignedbaseminor,unsignedcount,constchar*name);
2-3注销设备号:
- voidunregister_chrdev_region(dev_tfrom,unsignedcount);
2-4创建设备文件:
利用cat/proc/devices查看申请到的设备名,设备号。
(1)使用mknod手工创建:mknodfilenametypemajorminor
(2)自动创建;
利用udev(mdev)来实现设备文件的自动创建,首先应保证支持udev(mdev),由busybox配置。在驱动初始化代码里调用class_create为该设备创建一个class,再为每个设备调用device_create创建对应的设备。
3,字符设备驱动程序设计:
3-1设备注册:
字符设备的注册分为三个步骤:
- (1)分配cdev:structcdev*cdev_alloc(void);
- (2)初始化cdev:voidcdev_init(structcdev*cdev,conststructfile_operations*fops);
- (3)添加cdev:intcdev_add(structcdev*p,dev_tdev,unsignedcount)
3-2设备操作的实现:
file_operations函数集的实现。
- structfile_operations***_ops={
- .owner=THIS_MODULE,
- .llseek=***_llseek,
- .read=***_read,
- .write=***_write,
- .ioctl=***_ioctl,
- .open=***_open,
- .release=***_release,
- …
- };
特别注意:驱动程序应用程序的数据交换:
驱动程序和应用程序的数据交换是非常重要的。file_operations中的read()和write()函数,就是用来在驱动程序和应用程序间交换数据的。通过数据交换,驱动程序和应用程序可以彼此了解对方的情况。但是驱动程序和应用程序属于不同的地址空间。驱动程序不能直接访问应用程序的地址空间;同样应用程序也不能直接访问驱动程序的地址空间,否则会破坏彼此空间中的数据,从而造成系统崩溃,或者数据损坏。安全的方法是使用内核提供的专用函数,完成数据在应用程序空间和驱动程序空间的交换。这些函数对用户程序传过来的指针进行了严格的检查和必要的转换,从而保证用户程序与驱动程序交换数据的安全性。这些函数有:
- unsignedlongcopy_to_user(void__user*to,constvoid*from,unsignedlongn);
- unsignedlongcopy_from_user(void*to,constvoid__user*from,unsignedlongn);
- put_user(local,user);
- get_user(local,user);
3-3设备注销:
- voidcdev_del(structcdev*p);
4,ioctl函数说明
ioctl是设备驱动程序中对设备的I/O通道进行管理的函数。所谓对I/O通道进行管理,就是对设备的一些特性进行控制,例如串口的传输波特率、马达的转速等等。它的调用个数如下:
intioctl(intfd,indcmd,…);
其中fd就是用户程序打开设备时使用open函数返回的文件标示符,cmd就是用户程序对设备的控制命令,后面的省略号是一些补充参数,有或没有是和cmd的意义相关的。
ioctl函数是文件结构中的一个属性分量,就是说如果你的驱动程序提供了对ioctl的支持,用户就可以在用户程序中使用ioctl函数控制设备的I/O通道。
命令的组织是有一些讲究的,因为我们一定要做到命令和设备是一一对应的,这样才不会将正确的命令发给错误的设备,或者是把错误的命令发给正确的设备,或者是把错误的命令发给错误的设备。
所以在Linux核心中是这样定义一个命令码的:
|设备类型|序列号|方向|数据尺寸|
|--------------|------------|----------|------------|
|8bit|8bit|2bit|13~14bit|
|--------------|------------|----------|------------|
这样一来,一个命令就变成了一个整数形式的命令码。但是命令码非常的不直观,所以LinuxKernel中提供了一些宏,这些宏可根据便于理解的字符串生成命令码,或者是从命令码得到一些用户可以理解的字符串以标明这个命令对应的设备类型、设备序列号、数据传送方向和数据传输尺寸。
- /*usedtocreatenumbers*/
- #define_IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0)
- #define_IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))
- #define_IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
- #define_IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
- #define_IOR_BAD(type,nr,size) _IOC(_IOC_READ,(type),(nr),sizeof(size))
- #define_IOW_BAD(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),sizeof(size))
- #define_IOWR_BAD(type,nr,size)_IOC(_IOC_READ|_IOC_WRITE,(type),(nr),sizeof(size))
- #define_IOC(dir,type,nr,size)\
- (((dir)<<_IOC_DIRSHIFT)|\
- ((type)<<_IOC_TYPESHIFT)|\
- ((nr)<<_IOC_NRSHIFT)|\
- ((size)<<_IOC_SIZESHIFT))
5,文件私有数据
大多数linux的驱动工程师都将文件私有数据private_data指向设备结构体,read等个函数通过调用private_data来访问设备结构体。这样做的目的是为了区分子设备,如果一个驱动有两个子设备(次设备号分别为0和1),那么使用private_data就很方便。
这里有一个函数要提出来:
container_of(ptr,type,member)//通过结构体成员的指针找到对应结构体的的指针
其定义如下(下面那个定义我没有看明白):
- /**
- *container_of-castamemberofastructureouttothecontainingstructure
- *@ptr: thepointertothemember.
- *@type: thetypeofthecontainerstructthisisembeddedin.
- *@member: thenameofthememberwithinthestruct.
- *
- */
- #definecontainer_of(ptr,type,member)({ \
- consttypeof(((type*)0)->member)*__mptr=(ptr); \
- (type*)((char*)__mptr-offsetof(type,member));})
6,字符设备驱动的结构可以概括如下图:
字符设备是3大类设备(字符设备、块设备、网络设备)中较简单的一类设备,其驱动程序中完成的主要工作是初始化、添加和删除cdev结构体,申请和释放设备号,以及填充file_operation结构体中操作函数,并实现file_operations结构体中的read()、write()、ioctl()等重要函数。如图所示为cdev结构体、file_operations和用户空间调用驱动的关系。
7,自旋锁与信号量
为了避免并发,防止竞争。内核提供了一组同步方法来提供对共享数据的保护。我们的重点不是介绍这些方法的详细用法,而是强调为什么使用这些方法和它们之间的差别。
Linux使用的同步机制可以说从2.0到2.6以来不断发展完善。从最初的原子操作,到后来的信号量,从大内核锁到今天的自旋锁。这些同步机制的发展伴随Linux从单处理器到对称多处理器的过度;伴随着从非抢占内核到抢占内核的过度。锁机制越来越有效,也越来越复杂。目前来说内核中原子操作多用来做计数使用,其它情况最常用的是两种锁以及它们的变种:一个是自旋锁,另一个是信号量。
7-1自旋锁
自旋锁是专为防止多处理器并发而引入的一种锁,它在内核中大量应用于中断处理等部分(对于单处理器来说,防止中断处理中的并发可简单采用关闭中断的方式,不需要自旋锁)。
自旋锁最多只能被一个内核任务持有,如果一个内核任务试图请求一个已被争用(已经被持有)的自旋锁,那么这个任务就会一直进行忙循环——旋转——等待锁重新可用。要是锁未被争用,请求它的内核任务便能立刻得到它并且继续进行。自旋锁可以在任何时刻防止多于一个的内核任务同时进入临界区,因此这种锁可有效地避免多处理器上并发运行的内核任务竞争共享资源。
自旋锁的基本形式如下:
- spin_lock(&mr_lock);
- //临界区
- spin_unlock(&mr_lock);
7-2信号量
Linux中的信号量是一种睡眠锁。如果有一个任务试图获得一个已被持有的信号量时,信号量会将其推入等待队列,然后让其睡眠。这时处理器获得自由去执行其它代码。当持有信号量的进程将信号量释放后,在等待队列中的一个任务将被唤醒,从而便可以获得这个信号量。
信号量的睡眠特性,使得信号量适用于锁会被长时间持有的情况;只能在进程上下文中使用,因为中断上下文中是不能被调度的;另外当代码持有信号量时,不可以再持有自旋锁。
信号量基本使用形式为:
- staticDECLARE_MUTEX(mr_sem);//声明互斥信号量
- if(down_interruptible(&mr_sem))
- //可被中断的睡眠,当信号来到,睡眠的任务被唤醒
- //临界区
- up(&mr_sem);
7-3信号量和自旋锁区别
从严格意义上说,信号量和自旋锁属于不同层次的互斥手段,前者的实现有赖于后者,在信号量本身的实现上,为了保证信号量结构存取的原子性,在多CPU中需要自旋锁来互斥。
信号量是进程级的。用于多个进程之间对资源的互斥,虽然也是在内核中,但是该内核执行路径是以进程的身份,代表进程来争夺进程。鉴于进程上下文切换的开销也很大,因此,只有当进程占用资源时间比较长时,用信号量才是较好的选择。
当所要保护的临界区访问时间比较短时,用自旋锁是非常方便的,因为它节省上下文切换的时间,但是CPU得不到自旋锁会在那里空转直到执行单元锁为止,所以要求锁不能在临界区里长时间停留,否则会降低系统的效率
由此,可以总结出自旋锁和信号量选用的3个原则:
1:当锁不能获取到时,使用信号量的开销就是进程上线文切换的时间Tc,使用自旋锁的开销就是等待自旋锁(由临界区执行的时间决定)Ts,如果Ts比较小时,应使用自旋锁比较好,如果Ts比较大,应使用信号量。
2:信号量所保护的临界区可包含可能引起阻塞的代码,而自旋锁绝对要避免用来保护包含这样的代码的临界区,因为阻塞意味着要进行进程间的切换,如果进程被切换出去后,另一个进程企图获取本自旋锁,死锁就会发生。
3:信号量存在于进程上下文,因此,如果被保护的共享资源需要在中断或软中断情况下使用,则在信号量和自旋锁之间只能选择自旋锁,当然,如果一定要是要那个信号量,则只能通过down_trylock()方式进行,不能获得就立即返回以避免阻塞
自旋锁VS信号量
需求建议的加锁方法
低开销加锁优先使用自旋锁
短期锁定优先使用自旋锁
长期加锁优先使用信号量
中断上下文中加锁使用自旋锁
持有锁是需要睡眠、调度使用信号量
8,阻塞与非阻塞I/O
一个驱动当它无法立刻满足请求应当如何响应?一个对 read 的调用可能当没有数据时到来,而以后会期待更多的数据;或者一个进程可能试图写,但是你的设备没有准备好接受数据,因为你的输出缓冲满了。调用进程往往不关心这种问题,程序员只希望调用 read 或 write 并且使调用返回,在必要的工作已完成后,你的驱动应当(缺省地)阻塞进程,使它进入睡眠直到请求可继续。
阻塞操作是指在执行设备操作时若不能获得资源则挂起进程,直到满足可操作的条件后再进行操作。
一个典型的能同时处理阻塞与非阻塞的globalfifo读函数如下:
- /*globalfifo读函数*/
- static ssize_t globalfifo_read(struct file *filp, char __user *buf, size_t count,
- loff_t *ppos)
- {
- int ret;
- struct globalfifo_dev *dev = filp->private_data;
- DECLARE_WAITQUEUE(wait, current);
- down(&dev->sem); /* 获得信号量 */
- add_wait_queue(&dev->r_wait, &wait); /* 进入读等待队列头 */
- /* 等待FIFO非空 */
- if (dev->current_len == 0) {
- if (filp->f_flags &O_NONBLOCK) {
- ret = - EAGAIN;
- goto out;
- }
- __set_current_state(TASK_INTERRUPTIBLE); /* 改变进程状态为睡眠 */
- up(&dev->sem);
- schedule(); /* 调度其他进程执行 */
- if (signal_pending(current)) {
- /* 如果是因为信号唤醒 */
- ret = - ERESTARTSYS;
- goto out2;
- }
- down(&dev->sem);
- }
- /* 拷贝到用户空间 */
- if (count > dev->current_len)
- count = dev->current_len;
- if (copy_to_user(buf, dev->mem, count)) {
- ret = - EFAULT;
- goto out;
- } else {
- memcpy(dev->mem, dev->mem + count, dev->current_len - count); /* fifo数据前移 */
- dev->current_len -= count; /* 有效数据长度减少 */
- printk(KERN_INFO "read %d bytes(s),current_len:%d\n", count, dev->current_len);
- wake_up_interruptible(&dev->w_wait); /* 唤醒写等待队列 */
- ret = count;
- }
- out:
- up(&dev->sem); /* 释放信号量 */
- out2:
- remove_wait_queue(&dev->w_wait, &wait); /* 从附属的等待队列头移除 */
- set_current_state(TASK_RUNNING);
- return ret;
- }
9,poll方法
使用非阻塞I/O的应用程序通常会使用select()和poll()系统调用查询是否可对设备进行无阻塞的访问。select()和poll()系统调用最终会引发设备驱动中的poll()函数被执行。
这个方法由下列的原型:
- unsigned int (*poll) (struct file *filp, poll_table *wait);
这个驱动方法被调用, 无论何时用户空间程序进行一个 poll, select, 或者 epoll 系统调用, 涉及一个和驱动相关的文件描述符. 这个设备方法负责这 2 步:
1. 对可能引起设备文件状态变化的等待队列,调用poll_wait()函数,将对应的等待队列头添加到poll_table.
2. 返回一个位掩码, 描述可能不必阻塞就立刻进行的操作.
poll_table 结构, 给 poll 方法的第 2 个参数, 在内核中用来实现 poll, select, 和 epoll 调用; 它在 中声明, 这个文件必须被驱动源码包含. 驱动编写者不必要知道所有它内容并且必须作为一个不透明的对象使用它; 它被传递给驱动方法以便驱动可用每个能唤醒进程的等待队列来加载它, 并且可改变 poll 操作状态. 驱动增加一个等待队列到 poll_table 结构通过调用函数 poll_wait:
- void poll_wait (struct file *, wait_queue_head_t *, poll_table *);
poll 方法的第 2 个任务是返回位掩码, 它描述哪个操作可马上被实现; 这也是直接的. 例如, 如果设备有数据可用, 一个读可能不必睡眠而完成; poll 方法应当指示这个时间状态. 几个标志(通过 定义)用来指示可能的操作:
POLLIN :如果设备可被不阻塞地读, 这个位必须设置.
POLLRDNORM :这个位必须设置, 如果"正常"数据可用来读. 一个可读的设备返回( POLLIN|POLLRDNORM ).
POLLOUT :这个位在返回值中设置, 如果设备可被写入而不阻塞.
……
poll的一个典型模板如下:
- static unsigned int globalfifo_poll(struct file *filp, poll_table *wait)
- {
- unsigned int mask = 0;
- struct globalfifo_dev *dev = filp->private_data; /*获得设备结构体指针*/
- down(&dev->sem);
- poll_wait(filp, &dev->r_wait, wait);
- poll_wait(filp, &dev->w_wait, wait);
- /*fifo非空*/
- if (dev->current_len != 0) {
- mask |= POLLIN | POLLRDNORM; /*标示数据可获得*/
- }
- /*fifo非满*/
- if (dev->current_len != GLOBALFIFO_SIZE) {
- mask |= POLLOUT | POLLWRNORM; /*标示数据可写入*/
- }
- up(&dev->sem);
- return mask;
- }
应用程序如何去使用这个poll呢?一般用select()来实现,其原型为:
- int select(int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
其中,readfds, writefds, exceptfds,分别是被select()监视的读、写和异常处理的文件描述符集合。numfds是需要检查的号码最高的文件描述符加1。
以下是一个具体的例子:
- /*======================================================================
- A test program in userspace
- This example is to introduce the ways to use "select"
- and driver poll
-
- The initial developer of the original code is Baohua Song
- <author@linuxdriver.cn>. All Rights Reserved.
- ======================================================================*/
- #include <sys/types.h>
- #include <sys/stat.h>
- #include <stdio.h>
- #include <fcntl.h>
- #include <unistd.h>
- #include <sys/time.h>
- #define FIFO_CLEAR 0x1
- #define BUFFER_LEN 20
- main()
- {
- int fd, num;
- char rd_ch[BUFFER_LEN];
- fd_set rfds,wfds;
-
- /*以非阻塞方式打开/dev/globalmem设备文件*/
- fd = open("/dev/globalfifo", O_RDONLY | O_NONBLOCK);
- if (fd != - 1)
- {
- /*FIFO清0*/
- if (ioctl(fd, FIFO_CLEAR, 0) < 0)
- {
- printf("ioctl command failed\n");
- }
- while (1)
- {
- FD_ZERO(&rfds);// 清除一个文件描述符集rfds
- FD_ZERO(&wfds);
- FD_SET(fd, &rfds);// 将一个文件描述符fd,加入到文件描述符集rfds中
- FD_SET(fd, &wfds);
- select(fd + 1, &rfds, &wfds, NULL, NULL);
- /*数据可获得*/
- if (FD_ISSET(fd, &rfds)) //判断文件描述符fd是否被置位
- {
- printf("Poll monitor:can be read\n");
- }
- /*数据可写入*/
- if (FD_ISSET(fd, &wfds))
- {
- printf("Poll monitor:can be written\n");
- }
- }
- }
- else
- {
- printf("Device open failure\n");
- }
- }
FD_ZERO(fd_set *set); //清除一个文件描述符集set
FD_SET(int fd, fd_set *set); //将一个文件描述符fd,加入到文件描述符集set中
FD_CLEAR(int fd, fd_set *set); //将一个文件描述符fd,从文件描述符集set中清除
FD_ISSET(int fd, fd_set *set); //判断文件描述符fd是否被置位。
参考文献:http://archive.cnblogs.com/a/2272869/
http://www.cnitblog.com/zouzheng/archive/2008/02/26/40164.html