linux 设备驱动程序 时间流 之 延迟执行 --转载

在Linux设备驱动程序中,由于中断上下文的限制,不允许访问用户空间或使用current指针。同时,中断模式代码不能执行睡眠、调度操作,包括调用可能导致睡眠的函数如kmalloc(),以及使用信号量。这些限制确保了驱动程序的实时性和系统的稳定性。
摘要由CSDN通过智能技术生成
3  延迟执行

设备驱动程序经常需要将某些特定代码延迟一段时间后执行――通常是为了让硬件能完成某些任务。这一节将介绍许多实现延迟的不同技术,哪种技术最好取决于实际环境中的具体情况。我们将介绍所有的这些技术并指出各自的优缺点。

一件需要考虑的很重要的事情是所需的延迟长度是否多于一个时钟滴答。较长的延迟可以利用系统时钟;较短的延迟通常必须通过软件循环来获得。

6.3.1  长延迟

如果想把执行延迟若干个时钟滴答,或者对延迟的精度要求不高(比如,想延迟整数数目的秒数),最简单的也是最笨的实现如下,也就是所谓的“忙等待”:



unsigned long j = jiffies + jit_delay * HZ;

while (jiffies < j)
    /* nothing */;



这种实现当然要避免。我们在这里提到它,只是因为读者可能某时需要运行这段代码,以便更好地理解其他的延迟技术。

还是先看看这段代码是如何工作的。因为内核的头文件中 jiffies 被声明为 volatile 型变量,每次 C 代码访问它时都会重新读取它,因此该循环可以起到延迟的作用。尽管也是“正确”的实现,但这个忙等待循环在延迟期间会锁住处理器,因为调度器不会中断运行在内核空间的进程。更糟糕的是,如果在进入循环之前正好关闭了中断,jiffies 值就不会得到更新,那么 while 循环的条件就永远为真,这时,你不得不按下那只大的红按钮(指电源按钮)。

这种延迟和下面的几种延迟方法都在 jit 模块中实现了。由该模块创建的所有 /proc/jit* 文件每次被读取时都延迟整整 1 秒。如果你想测试忙等待代码,可以读 /proc/jitbusy 文件,当该文件的 read 方法被调用时它将进入忙等待循环,延迟 1 秒;而象 dd if=/proc/jitbusy bs=1 这样的命令每次读一个字符就要延迟 1 秒。

可以想见,读 /proc/jitbusy 文件会大大影响系统性能,因为此时计算机要到1秒后才能运行其他进程。

更好的延迟方法如下,它允许其他进程在延迟的时间间隔内运行,尽管这种方法不能用于硬实时任务或者其他对时间要求很严格的场合:



while (jiffies < j)
    schedule();



这个例子和下面各例中的变量 j 应是延迟到达时的 jiffies 值,计算方法和忙等待一样。

这种循环(可以通过读 /proc/jitsched 文件来测试它)延迟方法还不是最优的。系统可以调度其他任务;当前任务除了释放 CPU 之外不做任何工作,但是它仍在任务队列中。如果它是系统中唯一的可运行的进程,它还会被运行(系统调用调度器,调度器选择同一个进程运行,此进程又再调用调度器,然后……)。换句话说,机器的负载(系统中运行的进程平均数)至少为 1,而 idle 进程(进程号为 0,由于历史原因被称为“swapper”)绝不会被运行。尽管这个问题看来无所谓,当系统空闲时运行 idle 进程可以减轻处理器负载,降低处理器温度,延长处理器寿命,如果是手提电脑,还能延长电池的寿命。而且,延迟期间实际上进程是在执行的,因此进程在延迟中消耗的所有时间都会被纪录。运行命令 time cat /proc/jitsched 就可以发现这一点。

另一种情况下,如果系统很忙,驱动程序等待的时间可能会比预计多得多。一旦一个进程在调度时让出了处理器,无法保证以后的某个时间就能重新分配给它。如果可接受的延迟时间有上限的话,用这种方式调用 schedule,对驱动程序来说并不是一个安全的解决方案。

尽管有些毛病,这种循环延迟还是提供了一种有点“脏”但比较快的监视驱动程序工作的途径。如果模块中的某个 bug 会锁死整个系统,则可在每个用于调试的 printk 语句后添加一小段延迟,这样可以保证在处理器碰到令人厌恶的 bug 而被锁死之前,所有的打印消息都能进入系统日志。如果没有这样的延迟,这些消息只能进入内存缓冲区,但在 klogd 得到运行前系统可能已经被锁住了。

获得延迟的最好方法,是请求内核为我们实现延迟。根据驱动程序是否在等待其他事件,有两种设置短期延迟的办法。

如果驱动程序使用等待队列等待某个事件,而你又想确保在一段时间后一定运行该驱动程序,可以使用 sleep 函数的超时版本,这在第5章“睡眠和唤醒”一节中已介绍过了:



sleep_on_timeout(wait_queue_head_t *q, unsigned long timeout);
interruptible_sleep_on_timeout(wait_queue_head_t *q,
                               unsigned long timeout);



两种实现都能让进程在指定的等待队列上睡眠,并在超时期限(用jiffies表示)到达时返回。由此它们就实现了一种有上限的不会永远持续下去的睡眠。注意超时值表示要等待的 jiffies 数量,而不是绝对的时间值。这种方式的延迟可以在 /proc/jitqueue 的实现中看到:



wait_queue_head_t wait;

init_waitqueue_head (&wait);
interruptible_sleep_on_timeout(&wait, jit_delay*HZ);



在通常的驱动程序中,可以以下列两种方式重新获得执行:在等待队列上调用一个 wake_up,或者 timout 超时。在这个特定实现中,没人会调用 wake_up(毕竟其它代码根本就不知道这件事),所以进程总是因 timeout 超时而被唤醒。这是一个完美有效的实现,不过,如果驱动程序无须等待其它事件,可以用一种更直接的方式获取延迟,即使用schedule_timeout:



set_current_state(TASK_INTERRUPTIBLE);
schedule_timeout (jit_delay*HZ);



上述代码行(在 /proc/jitself 中实现)使进程进入睡眠直到指定时间。schedule_timeout也是处理一个时间增量而不是一个 jiffies 的绝对值。和前面一样,在从超时到达到进程实际被调度执行之间,可能会消耗少量的额外时间-- 实际上这并不重要。


6.3.2  短延迟
有时驱动程序需要非常短的延迟来和硬件同步。此时,使用jiffies值无法达到目的。

这时就要用内核函数udelay 和 mdelay*。



u表示希腊字母“mu”(μ),它代表“微”。



它们的原型如下:



#include <linux/delay.h>
void udelay(unsigned long usecs);
void mdelay(unsigned long msecs);



该函数在绝大多数体系结构上是作为内联函数编译的。前者使用软件循环延迟指定数目的微秒数,后者使用 udelay 做循环,用于方便程序开发。udelay 函数里要用到 BogoMips 值:它的循环基于整数值 loops_per_second,这个值是在引导阶段计算 BogoMips 时得到的结果。

udelay函数只能用于获取较短的时间延迟,因为loops_per_second值的精度只有8位,所以,当计算更长的延迟时会积累出相当大的误差。尽管最大能允许的延迟将近1秒(因为更长的延迟就要溢出),推荐的 udelay 函数的参数的最大值是取1000微秒(1毫秒)。延迟大于 11 毫秒时可以使用函数 mdelay。

要特别注意的是 udelay 是个忙等待函数(所以 mdelay 也是),在延迟的时间段内无法运行其他的任务,因此要十分小心,尤其是 mdelay,除非别无他法,要尽量避免使用。

目前在支持大于几个微秒和小于1个时钟滴答的延迟时还是很低效的,但这通常不是个问题,因为延迟需要足够长,以便能够让人或者硬件注意到。对人来说,百分之一秒的时间间隔是比较适合的精度,而 1 毫秒对硬件动作来说也足够长了。

mdelay 在 Linux 2.0 中并不存在,头文件 sysdep.h 弥补了这一缺陷。

6.4  任务队列
许多驱动程序需要将任务延迟到以后处理,但又不想借助中断。Linux 为此提供了三种方法:任务队列、tasklet(从内核 2.3.43 开始)和内核定时器。任务队列和 tasklet 的使用很灵活,可以或长或短地延迟任务到以后处理,在编写中断处理程序时非常有用,我们还将在第9章“Tasklet和底半部处理”一节中继续讨论。内核定时器则用来调度任务在未来某个指定时间执行,将在本章的“内核定时器”一节中讨论。

使用任务队列或tasklet的一个典型情形是,硬件不产生中断,但仍希望提供阻塞型的读取。此时需要对设备进行轮询,同时要小心地不使 CPU 负担过多无谓的操作。将读进程以固定的时间间隔唤醒(例如,使用 current->timeout 变量)并不是个很好的方法,因为每次轮询需要两次上下文切换(一次是切换到读进程中运行轮询代码,另一次是返回执行实际工作的某个进程),而且通常来讲,恰当的轮询机制应该在进程上下文之外实现。

类似的情形还有象不时地给简单的硬件设备提供输入。例如,有一个直接连接到并口的步进马达,要求该马达能一步步地移动,但马达每次只能移动一步。在这种情况下,由控制进程通知设备驱动程序进行移动,但实际上,移动是在 write 返回后,才在周期性的时间间隔内一步一步进行的。

快速完成这类不定操作的恰当方法是注册任务在未来执行。内核提供了对“任务队列”的支持,任务可以累积,而在运行队列时被“消耗”。我们可以声明自己的任务队列,并且在任意时刻触发它,或者也可以将任务注册到预定义的任务队列中去,由内核来运行(触发)它。

这一节将首先概述任务队列,然后介绍预定义的任务队列,这使读者可以开始一些有趣的测试(如果出错也可能挂起系统),最后介绍如何运行自己的任务队列。接着,我们来看看新的 tasklet 接口,在 2.4 内核中它在很多情况下取代
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值