LDD3第六章的学习-高级字符驱动程序操作

 

作者:Aningsk ,本作品采用知识共享署名-非商业性使用-相同方式共享 3.0 未本地化版本许可协议进行许可。

 

这一章主要是说了ioctl,阻塞IO,poll和select,以及异步通知等等。内容还是蛮多的嘛~一时间不知道从哪里开始写啊。

 

ioctl

 

在用户空间,ioctl系统调用的原型是ioctl(int fd, unsigned long, ...);原型中的一串点是一个可选参数,并不是数目不定的一串参数。在这里使用一串点,主要是为了在编译时防止编译器进行类型检查。我认为之所以不让编译器对最后一个参数进行检查,是因为最后一个参数是作为命令(第二个参数unsigned long)的参数,它的形式是不确定的,不同的命令需要的参数可能是char、int、long、指针等等。为了实现这个参数能够由用户向内核传递不同的类型,就不能编译时确定唯一的数据类型。

在驱动程序中ioctl的原型是ioctl(struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg); inode和filp对应于应用程序的传递的文件描述符fd,最后的参数arg对应于应用程序中那一串点的参数,也就是说不管应用程序给命令的参数是什么类型,内核给驱动的都是unsigned long类型。在使用时按照自己的所需进行强制转换。(这里我就不清楚了为什么在用户空间不用unsigned long)。

 

关于ioctl的命令

简单来说,命令就是一个数字,用来标示不同命令之间是不同的;但命令的的数字不能随便用的,我们能够保证在一个驱动中命令的序号是不同的,可无法保证不与其他驱动冲突。Linux为了解决命令冲突的问题(发给一个驱动的命令,却有其他驱动识别了这个命令),采用了一个统一的约定:ioctl命令由type(幻数)、number(序数)、direction(数据传输方向)、size(数据大小)。在注意防止冲突时,我们要关注的是幻数和序数。驱动已经使用的幻数和序数在ioctl-number.txt中已经记录。在这个文件记录的幻数和序数也是有冲突的,正如comment项中标示了“conflict!”。我的理解是那些冲突的不能出现在同一个内核里。如果在一个内核里没有那个驱动,那它所占用的幻数和序数是可以被我们使用;如果驱动程序不发布的话,我们找个能用的,默默使用就好;如果要发布,用到的幻数和序数就要根据这个文件中的记录好好斟酌,并且把自己最终要用的幻数与序数发个邮件给Linus,打个招呼。

除了不能和其他的驱动的ioctl有冲突,我们也不能和内核预定义的冲突,这个在书中142页有描述。

说了这么多,其实使用并不是很复杂,对于定义一个ioctl命令Linux已经提供了一系列的宏(包括定义命令和解析命令),像定义用的_IO,_IOW,_IOR,_IOWR,解析用的_IOC_DIR(nr),_IOC_TYPE(nr)等等。对于书中,解析命令的宏的参数被命名为“nr”,我觉得就是“cmd”,名字之所以没有改,可能是因为某些历史原因吧~(Linux内核里的历史原因不算少呢)。

这些命令常常是统一放在一个头文件中的,并且这个头文件要提供给上层使用。

 

关于ioctl命令参数的使用

书中在实际实现ioctl的时候,在switch之前会首先检查幻数是不是当前驱动的幻数,检查命令的序号是否大于我们定义的最大值(在头文件中自己添加的)。然后调用access_ok检查传递下来的指针是否能够正确访问,当然如果那个参数不是指针而是数据,那就直接使用了。在具体使用时,使用了__put_user()和__get_user(),个人感觉如果使用put_user()和get_user()就不必使用acces_ok()了,但这两个函数占用的时钟周期多,在对性能要求高的位置,使用__put_user()和__get_user()比较好。具体驱动和用户程序的实现例子在书中146页,分别实现了通过指针设置/获取、数值设置、返回值获取。通过书中的例子,可以看出,尽管_IO定义的命令是不管读或写的,即不带有参数;但ioctl系统调用在使用_IO的cmd时,仍可以加上arg这个ioctl中的参数,用来实现数据的读和写。我觉得即使这样使可以的,最好还是用_IOR,_IOW,_IOWR,同时使用统一的数据交换形式,这样会清晰好多。

 

阻塞I/O

 

阻塞:就是在进程不能获得它想要的资源的时候进入休眠,让出CPU供其他进程使用。安全地让进程进入休眠,有两条规则必须遵循:第一,永远不要在原子上下文中进入休眠,在驱动要进入休眠时,驱动不应该拥有自旋锁、seqlock等会导致忙等的锁机制。因为一旦一个进程在持有锁时休眠,让出CPU其他进程如果需要获得锁则要在CPU上忙等,休眠的进程不能被唤醒或很久才能唤醒,将大大降低系统效率甚至造成系统死锁。第二,当进程被唤醒时,我们永远无法知道休眠了多长时间,或者在休眠期间发生了什么事情,也不会知道是否有其他进程因为等待相同的资源而休眠;因此我们被唤醒时,不能确定所等待的资源是否已经被其他进程拿走,所以我们被唤醒后必须检查我们等待的条件确切为真。

 

如何管理休眠

一个进程在需要休眠功能时,需要将自己加入到一个队列中,这个队列记录着因为相同原因进入休眠的进程;对应的唤醒进程完成某项任务找到这个队列,就可以唤醒等待此任务的所有的进程。这个队列的意义就在于将因相同原因休眠的进程纳入统一的管理。这个队列不妨叫做“进程等待队列”,为了区分“等待队列”“等待队列头”的概念,下面还会继续描述。

 

简单休眠

所谓的简单休眠,是指休眠不在原子上下文中,即要休眠的进程没有持有任何锁。这样的休眠实现步骤很简单,Linux提供了wait_event(queue, condition)宏和它的几个变种。这里queue被叫为“等待队列头”,虽然被称为“头”,实际上它代表着整个进程等待队列(也就是说上文所说的“进程等待队列”就是“等待队列头”)。

驱动在进程不能获得资源而需要休眠时,调用wait_event(或其他变种,建议使用wait_event_interruptible)进程就会进入休眠。当对应的资源可以获得时,驱动需要调用wake_up(queue)(或其他变种)来唤醒在等待队列头queue的所有进程。被唤醒的进程将从之前开始睡眠的位置(wait_event)继续运行。

另外说一下wait_event_interruptible(queue, condition),这是比较推荐使用的。该函数返回非零,意味着休眠被信号打断。我们要向内核上层返回ERESTARTSYS。返回零,意味着condition满足。仅仅是此刻满足了,所以要进一步检查,以防其他竞争的进程已经抢先。

书中154页提供了例子(如下图):

从126行看起,如果没有数据可读,立即释放信号量,测试是否阻塞:非阻塞立即返回。如果是阻塞,则要进入休眠。当在运行时(即被唤醒),继续由131行运行,我们无法确定是否是因为有数据而醒了(即rp != wp);所以,我们判断wait的返回值。如果非0,则是休眠被中断打断,我们向内核返回ERESTARTSYS。如果是0,则进程的确是被wake_up唤醒,但我们无法确定进程醒来之前发生了什么事情(比如数据已经被其他进程读走),我们要取得信号量,并再次检查是否有数据(在while处的rp == wp)。如果有数据了,不再运行while循环;否则while继续循环(因为数据已经被读走了)。

 

高级休眠

在简单休眠中,我们调用wait_event使进程休眠;在高级休眠中,休眠的完成是由我们驱动的代码来完成的。这时候就需要我们自己定义一个“等待队列”(wait_queue_t)了,“等待队列”只是“等待队列头”指向的“进程等待队列”中的一个成员节点,而不是完整的一个链表。以前我总是被“等待队列”和“等待队列头”这两个名称搞糊涂。

休眠的过程由驱动代码分步完成,我们就可以在进入休眠时做更多的事情,比如设置“独占等待”。

休眠的实现有三个步骤:一、分配并初始化“等待队列”(wait_event_t),然后加入到对应的“进程等待队列”(由“等待队列头”指向的);二、设置进程状态,将其标记为休眠(如TASK_INTERRUPTIBLE);三、调用schedule()放弃CPU。具体实现的例子在书中159页有详细代码。

 

poll和select

 

前面说的wait_event和wake_up那一套是为了实现阻塞的。而poll和select操作多个在非阻塞读写文件时使用的。详细来说挺复杂的呢,分别从用户空间和驱动中的使用方法来说吧。

 

用户空间poll和select

这两个系统调用本质是一样的:都允许进程决定是否可以对一个或多个打开的文件做非阻塞的读取或写入。这些调用本身是会阻塞进程的,直到给定的文件描述符集合中的任何一个可以读取或写入。它们常常用于多个输入或输出流而又不会阻塞与其中任意一个流的应用程序里。好像网络编程中服务器端的程序经常使用这两个系统调用。

用户在应用程序中告诉select,他关注的那几个文件描述符合关心的文件状态(如读、写等)。等select调用返回时,会告诉用户哪些文件可以无阻塞读、哪些文件可以无阻塞写。用户根据select的返回信息,去调用IO函数(read、write),这些函数的调用便不会阻塞。

 

内核驱动poll

上层应用使用select,可能涉及到多个文件描述符,它们背后的驱动也可能不同。用户不需要在意那些细节。同样,对于一个驱动,它的poll要做的就是提供自己对应文件的“等待队列”给内核的poll_table,并设置一下返回值。至于如何告诉用户这些信息(文件可以无阻塞读/写了),驱动程序也不用去在意。

无论是上层应用还是底层驱动,都不管太多的事情,那些都是内核干的,相信它就是了。

对于一个设备的驱动来说,它不在意用户使用的poll或是select调用。它只要管好自己对应的文件就好:管好自己的文件很简单,把自己的文件描述符和它的等待队列,再加上一个poll_table,给poll_wait函数就好;然后是列出不同状态(可读/可写等),设置相应的标志位,并返回。在书的165页有使用的例子。

 

异步通知

 

异步通知,就是让应用程序可以在数据可用是收到一个信号,而不需要不停的使用轮询来关注数据。

 

对于用户程序来说,有两步要做:一是设置进程为文件的属主,二是设置文件的FASYNC标志。在169页有示例。

对于驱动程序来说,需要写fasync来支持上层设置FASYNC标志,而实现这个函数也很简单,参见书中171页。同时记得在驱动合适的地方添加发送信号的函数kill_fasync。在文件关闭时,也要调用一次fasync(scull_p_fasync(-1, filp, 0))以便从活动的异步读取进程列表中删除该文件。

 

 

好~先到这里啦。定位设备什么的就不详细写了。

 

 

Aningsk

2015-01-31

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值