📄 (ldd) ch05-字符设备驱动程序的扩展操作.txt
字号:
置100,43”或“降低默认速度”之类的字串。驱动程序仅将/dev中的设备节点当作为应
用程序设立的命令通道。对该设备直接控制的优点是,你可以使用cat来移动摄像头,而
无需写并编译发出ioctl调用的特殊代码。
当编写“面向命令的”驱动程序时,没有理由要实现ioctl方法。为解释器多实现一条命
令对于实现和使用来说,都更容易。
好奇的读者可以看看由O’Reilly FTP站点提供的源码stepper目录中的stepper驱动程序
;由于我认为代码没有太大的意义(而且质量也不是太高),这里没有包含它。
阻塞型I/O
阻塞型I/O
read的一个问题是,当尚未有数据可读,而又没有到文件尾时如何处理。
默认的回答是,“我们必须睡眠等待数据。”本节将介绍进程如何睡眠,如何唤醒,以
及一个应用程序如何在不阻塞read调用的情况下,查看是否有数据。对于写来说也可以
适用同样的方法。
通常,在我向你介绍真实的代码前,我将解释若干概念。
睡眠和唤醒
当进程等待事件(可以是输入数据,子进程的终止或是其他什么)时,它需要进入睡眠
状态以便其他进程可以使用计算资源。你可以调用如下函数之一让进程进入睡眠状态:
(代码)
然后用如下函数之一唤醒进程:
(代码)
在前面的函数中,wait_queue指针的指针用来代表事件;我们将在“等待队列”一节中
详细讨论这个数据结构。从现在开始,唤醒进程需要使用进程睡眠时使用的同一个队列
。因此,你需要为每一个可能阻塞进程的事件对应一个等待队列。如果你管理4个设备,
你需要为阻塞读预备4个等待队列,为阻塞写再预备4个。存放这些队列的最佳位置是与
你需要为阻塞读预备4个等待队列,为阻塞写再预备4个。存放这些队列的最佳位置是与
每个设备相关的硬件数据结构(在我们的例子中就是Scull_Dev)。
但“可中断”调用和普通调用有什么区别呢?
sleep_on不能信号取消,但interruptible_sleep_on可以。其实,仅在内核的临界区才
调用sleep_on;例如,当等待从磁盘上读取交换页面时。没有这些页面进程就无法继续
运行,用信号打断这个操作是没有任何意义的。然而,在所谓“长系统调用”,如read
,中要使用interruptible_sleep_on。当进程正等待键盘输入时,用一个信号将进程杀
死是很有意义的。
类似地,wake_up唤醒睡在队列上的任何一个进程,而wake_up_interruptible仅唤醒可
中断进程。
做为一个驱动程序编写人员,由于进程仅在read或write期间才睡眠在驱动程序代码上,
你应该调用interruptible_sleep_on和wake_up_interruptible。不过,事实上由于没有
“不可中断”的进程在你的队列上睡眠,你也可以调用wake_up。但是,出于源代码一致
性的考虑,最好不这样做。(此外,wake_up比它的搭档来说要稍微慢一点。)
编写可重入的代码
当进程睡眠后,驱动程序仍然活着,而且可以由另一个进程调用。让我们一控制台驱动
程序为例。当一个应用在tty1上等待键盘输入,用户切换到tty2上并派生了一个新的外
壳。现在,两个外壳都在控制台驱动程序中等待键盘输入,但它们睡在不同的队列上:
壳。现在,两个外壳都在控制台驱动程序中等待键盘输入,但它们睡在不同的队列上:
一个睡在与tty1相关的队列上,一个睡在与tty2相关的队列上。每个进程都阻塞在inter
ruptible_sleep_on函数中,但驱动程序让可以继续接收和响应其他tty的请求。
可以通过编写“可重入代码”轻松地处理这种情况。可重入代码是不在全局变量中保留
状态信息的代码,因此能够管理交织在一起的调用,而不会将它们混淆起来。如果所有
的状态信息都与进程有关,就不会发生相互干扰。
如果需要状态信息,既可以在驱动程序函数的局部变量中保存(每个进程都有不同的堆
栈来保存局部变量),也可以保存在访问文件用的filp中的private_data中。由于同一
个filp可能在两个进程间共享(通常是父子进程),最好使用局部变量*。
如果你需要保存大规模的状态信息,你可以将指针保存在局部变量中,并用kmalloc获取
实际存储空间。此时,你千万别忘了kfree这些数据,因为当你在内核空间工作时,没有
“在进程终止时释放所有资源”的说法。
你需要将所有调用了sleep_on(或是schedule)的函数写成可重入的,并且包括所有在
这个函数调用轨迹中的所有函数。如果sample_read调用了sample_getdata,后者可能会
阻塞,由于调用它们的进程睡眠后无法阻止另一个进程调用这些函数,sample_read和sa
mple_gendata都必须是可重入的。此外,任何在用户空间和内核空间复制数据的函数也
必须是可重入的,这是因为访问用户空间可能会产生页面失效,当内核处理失效页面时
,进程可以会进入睡眠状态。
等待队列
我听见你在问的下一个问题是,“我到底如何使用等待队列呢?”
等待队列很容易使用,尽管它的设计很是微妙,但你不需要直到它的内部细节。处理等
待队列的最佳方式就是依照如下操作:
l 声明一个struct wait_queue *变量。你需要为每一个可以让进程睡眠的事件
预备这样一个变量。这就是我建议你放在描述硬件特性数据结构中的数据项。
l 将该变量的指针做为参数传递给不同的sleep_on和wake_up函数。
这相当容易。例如,让我们想象一下,当进程读你的设备时,你要让这个进程睡眠,然
后在某人向设备写数据后唤醒这个进程。下面的代码就可以完成这些工作:
(代码)
该设备的这段代码就是例子程序中的sleepy,象往常一样,可以用cat或输入/输出重定
向等方法测试它。
上面列出的两个操作是你唯一操作在等待队列上的两个操作。不过,我知道某些读者对
它的内部结构感兴趣,但通过源码掌握它的内部结构很困难。如果你不对更多的细节感
兴趣,你可以跳过下一节,你不会损失什么的。注意,我谈论的是“当前”实现(版本2
兴趣,你可以跳过下一节,你不会损失什么的。注意,我谈论的是“当前”实现(版本2
..0.x),但没有什么规定限制内核开发人员必须依照那样的实现。如果出现了更好的实
现,内核很容易就会使用新的,由于驱动程序编写人员只能通过那两个合法操作使用等
待队列,对他们来说没有什么坏的影响。
当前struct wait_queue额实现使用了两个字段:一个指向struct task_struct结构(等
待进程)的指针,和一个指向struct wait_queue(链表中的下一个结构)的指针。等待
队列是循环链表,最后一个结构指向第一个结构。
该设计的引入注目的特点是,驱动程序编写人员从来不声明或使用这个结构;他们仅仅
传递它的指针或指针的指针。实际的结构是存在的,但只在一个地方:在__sleep_on函
数的局部变量中,上面介绍的两个sleep_on函数最终会调用这个函数。
这看上去有点奇怪,不过这是一个非常明智的选择,因为无需处理这种结构的分配和释
放。进程每次睡在某个队列上,描述其睡眠的数据结构驻留在进程对应的不对换的堆栈
页中。
当进程加入或从队列中删除时,实际的操作如图5-1所示。
(图5-1 等待队列的工作示意)
阻塞型和非阻塞型操作
在分析功能完整的read和write方法前,我们还需要看看另外一个问题,这就是filp->f_
在分析功能完整的read和write方法前,我们还需要看看另外一个问题,这就是filp->f_
flags中的O_NONBLOCK标志。这个标志定义在<linux/fcntl.h>中,在最近的内核中,这
个头文件由<linux/fs.h>自动包含了。如果你在内核1.2中编译你的模块,你需要手动包
含fcntl.h。
这个标志的名字取自“打开-非阻塞”,因为这个标志可以在打开时指定(而且,最初只
能在打开时指定)。由于进程在等待数据时的正常行为就是睡眠,这个标志默认情况下
是复位的。在阻塞型操作的情况下,应该实现下列操作:
l 如果进程调用read,但(尚)没有数据,进程必须阻塞。当数据到达时,进程
被唤醒,并将数据返回给调用者,即便少于方法的count参数中所请求的数据量,也是如
此。
l 如果进程调用了write,缓冲区又没有空间,进程也必须阻塞,而且它必须使
用与用来实现读的等待队列不同的等待队列。当数据写进设备后,输出缓冲区中空出部
分空间,唤醒进程,write调用成功完成,如果缓冲区中没有请求中count个字节,则进
程可能只是完成了部分写。
前面的列表的两个语句都假设,有一个输入和输出缓冲区,而且每个设备驱动程序都有
一个。输入缓冲区需要用来在数据达到而又没有人读时避免丢失数据,输出缓冲区用来
尽可能增强计算机的性能,尽管这样做不是严格必须的。由于如果系统调用不接收数据
的话,数据仍然保存在用户空间的缓冲区中,write中可以丢失数据。
在驱动程序中实现输出缓冲区可以获得一定的性能收益,这主要是通过较少了用户级/内
核级转换和上下文切换的数目达到的。如果没有输出缓冲区(假设是一个慢设备),每
次系统调用只接收一个或很少几个字节,并且当进程在write中睡眠时,另一进程就会运
行(有一次上下文切换)。当第一个进程被唤醒后,它恢复运行(又一次上下文切换)
,write返回(内核/用户转换),进程还要继续调用系统调用写更多的数据(内核/用户
转换);然后调用再次被阻塞,再次进行整个循环。如果输出缓冲区足够大,write首次
操作时就成功了;数据在中断时被推送给设备,而不必将控制返回用户空间。适合于设
备的输出缓冲区的尺寸显然是和设备相关的。
我们没有在scull中使用输入缓冲区,这是因为当调用read时,数据已经就绪了。类似地
,也没有使用输出缓冲区,数据简单地复制到设备对应的内存区中。我们将在第9章“中
断处理”的“中断驱动的I/O”一节中介绍缓冲区的使用。
如果设置了O_NONBLOCK标志,read和write的行为是不同的。此时,如果进程在没有数据
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -