CSDN博客

img collide

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

发表于2004/9/27 20:46:00  3448人阅读

分类: Linux 内核研究

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 内核中它在很多情况下取代了任务队列。

6.4.1  任务队列的本质
任务队列其实是一个任务链表,每个任务用一个函数指针和一个参数表示。任务运行时,它接受一个void * 类型的参数,返回值类型为 void,而指针参数可用来将一个数据结构传入函数,或者可以被忽略。队列本身是一个结构(即任务)链表,并由声明和操纵它们的内核模块所拥有。模块要全权负责这些数据结构的分配和释放,为此一般使用静态的数据结构。

队列元素由下面这个结构来描述,这段代码是直接从头文件 <linux/tqueue.h> 拷贝下来的:



struct tq_struct {
   struct tq_struct *next;         /* linked list of active bh's */
   int sync;                       /* must be initialized to zero */
   void (*routine)(void *);        /* function to call */
   void *data;                     /* argument to function */
};



第一个注释中的 bh 指的是底半部(bottom-half)。底半部是“中断处理程序的一半部”,我们将在第9章的“tasklet和底半部”一节中介绍中断时详细讨论。现在,我们只要知道底半部是驱动程序实现的一种机制就可以了,它用于处理异步任务,这些任务通常比较大,不适于在处理硬件中断时完成。本章并不要求你理解底半部处理,但必要时也会偶尔提及。



译注:在2.4版本的内核中,tq_struct的第一个成员变量已经有所不同,改为
struct list_head list; /* linked list of active bh's */
这是因为通用的双向链表 list_head在内核中大量采用,在很多情况下替代了数据结构中自行维护的链表。相应的task_queue的定义也改为
typedef struct list_head task_queue;



上面的数据结构中最重要的成员是routine和data。为了将随后执行的任务排队,必须先设置好结构的这些成员,并把 next 和 sync 两个字段清零。结构中的 sync 标志位由内核使用,以避免同一任务被插入多次,因为这会破坏 next 指针。一旦任务被排队,该数据结构就被认为由内核“拥有”了,不能再被修改,直到任务开始运行。

与任务队列有关的其他数据结构还有 task_queue,目前它实现为指向 tq_struct 结构的指针,之所以将这个指针(struct tq_struct* )定义成另一个数据结构(struct task_queue)是为了扩展的需要, 在需要的时候,task_queue结构中可以增加别的内容。
在使用之前,必须将 task_queue 指针初始化为 NULL。

下面汇总了所有可以在任务队列和 tq_struct 结构上执行的操作。

DECLARE_TASK_QUEUE(name);
这个宏用给定的名称 name 声明了一个任务队列,并把它初始化为空。

int queue_task(struct tq_struct *task, task_queue *list);
正如该函数的名字,它用于将任务排进队列中。如果队列中已有该任务,返回 0,否则返回非 0。

void run_task_queue(task_queue *list);
run_task_queue函数用于运行累积在队列上的任务。除非你要声明和维护自己的任务队列,否则不必调用本函数。

在讨论使用任务队列的细节之前,我们先看一下它们在内核中是怎样工作的。

6.4.2  任务队列的运行
如前所述,一个任务队列,实际上是一个函数链表。当调用 run_task_queue 运行某个队列时,列表中的每一项都会被执行。在编写和任务队列有关的函数时,必须牢记内核是在什么时候调用run_task_queue的,而且当内核调用 run_task_queue 时,实际的上下文将限制能够进行的操作。也不应对队列中任务的运行顺序做任何假定,它们每个都是独立完成自己的任务的。

那么任务队列在什么时候运行呢?如果使用的是下面一节介绍的预定义的任务队列,则答案是“在内核轮到它那里时”。不同的队列在不同的时间运行,只要内核没有其他更紧要的任务,它们总是会运行的。

更重要的是,当对任务进行排队的进程运行时,任务队列几乎肯定是不会运行的,相反,它们是异步执行的。到现在为止,示例驱动例程中所有的事情都是在这个执行系统调用的进程上下文中完成的。但当任务队列运行时,这个进程可能正在睡眠,或正在另一个处理器上运行,甚至可能已经完全退出了。

这种异步执行类似于硬件中断发生时的情景(我们会在第9章详细讨论)。实际上,任务队列常常是作为“软件中断”的结果而运行的。在中断模式(或中断期间)下,代码的运行会受到许多限制。我们现在介绍这些限制,这些限制还会在本书后面多次出现。我们也会多次重复,中断模式下的这些规则必须遵守,否则系统会有大麻烦。

许多动作需要在进程上下文中才能执行。如果处于进程上下文之外(比如在中断模式下),则必须遵守如下规则:


  • 不允许访问用户空间。因为没有进程上下文,也就没有办法访问与任何一个特定进程相关联的用户空间。
  • current指针在中断模式下是无效的,不能使用。
  • 不能执行睡眠或调度。中断模式代码不可以调用schedule或者sleep_on;也不能调用任何可能引起睡眠的函数。例如,调用kmalloc(...,GFP_KERNEL)就不符合本规则。信号量也不能用,因为可能引起睡眠。


内核代码可以通过调用函数in_interrupt( ) 来判断自己是否正运行于中断模式,该函数无需参数,如果处理器在中断期间运行就返回非0值。

当前的任务队列实现还有一个特性,队列中的一个任务可以将自己重新插回到它原先所在的队列。举个例子,定时器队列中的任务可以在运行时将自己插回到定时器队列中去,从而在下一个定时器滴答又再次被运行。这是通过调用 queue_task 把自己放回队列来实现的。由于在处理任务队列之前,是先用NULL指针替换队列的头指针,也就是将任务队列初始化了,另外,在执行队列中的任务之前,首先将任务从队列中移出来,这样在任务将本身插入任务队列的时候,它其实是将指针指向新的任务队列。结果就是,随着旧队列的执行,新的队列逐渐生成。

尽管一遍遍地重新调度同一个任务看起来似乎没什么意义,但有时这也有些用处。例如,步进马达每次移动一步直到目的地,它的驱动程序就可以通过让任务在定时器队列上不断地重新调度自己来实现。其他的例子还有 jiq 模块,该模块中的打印函数通过重新调度自己来产生输出――结果是利用定时器队列产生多次迭代。

6.4.3  预定义的任务队列
延迟任务执行的最简单方法是使用由内核维护的任务队列。这种队列有好几种,但驱动程序只能使用下面列出的其中三种。任务队列的定义在头文件 <linux/queue.h> 中,驱动程序代码需要包含该头文件。

调度器队列
调度器队列在预定义任务队列中比较独特,它运行在进程上下文中,这意味着该队列中的任务可以更多的事情。在Linux 2.4,该队列由一个专门的内核线程 keventd 管理,通过函数 schedule_task 访问。在较老的内核版本,没有用keventd,所以该队列(tq_scheduler)是直接操作的。

tq_timer
该队列由定时器处理程序(定时器嘀哒)运行。因为该处理程序(见函数do_timer)是在中断期间运行的,因此该队列中的所有任务也是在中断期间运行的。

tq_immediate
立即队列是在系统调用返回时或调度器运行时得到处理,以便尽可能快地运行该队列。该队列在中断期间得到处理。

还有其它的预定义队列,但驱动程序开发中通常不会涉及到它们。

使用任务队列的一个设备驱动程序的执行流程可见图6-1。该图演示了设备驱动程序是如何在中断处理程序中将一个函数插入tq_immediate队列中的。


图 6-1:task_queue的使用流程


示例程序是如何工作的
延迟计算的示例程序包含在jiq(Just In Queue)模块中,本节中抽取了它的部分源码。该模块创建 /proc 文件,可以用 dd 或者其他工具来读,这点上与 jit 模块很相似。读 jiq 文件的进程被转入睡眠状态直到缓冲区满*。



/proc文件的缓冲区是内存中的一页:4KB,或对应于使用平台的尺寸。



睡眠是由一个简单的等待队列处理的,声明为



DECLARE_WAIT_QUEUE_HEAD (jiq_wait);



缓冲区由不断运行的任务队列来填充。任务队列的每次运行都会在要填充的缓冲区中添加一个字符串,该字符串记录了当前时间(jiffies值),当前进程以及 in_interrupt的返回值。

填充缓冲区的代码都在jiq_print_tq函数中,任务队列的每遍运行都要调用它。打印函数没什么意思,不在这里列出,我们还是来看看插入队列的任务的初始化代码:



struct tq_struct jiq_task; /* global: initialized to zero */

   /* these lines are in jiq_init() */
   jiq_task.routine = jiq_print_tq;
   jiq_task.data = (void *)&jiq_data;



这里没必要对 jiq_task结构的 sync成员和next成员清零,因为静态变量已由编译器初始化为零了。

调度器队列
最容易使用的任务队列是调度器(scheduler)队列,因为该队列中的任务不会在中断模式运行,因此可以做更多事,特别是它们还能睡眠。内核中有多处使用该队列完成各种任务。

在内核2.4.0-test11,实际实现调度器队列的任务队列被内核的其余部分隐藏了。使用这个队列的代码必须调用schedule_task把任务放入队列,而不能直接使用queue_task:



int schedule_task(struct tq_struct *task);



其中的 task 当然就是要调度的任务。返回值直接来自queue_task:如果任务不在队列中就返回非零。

再提一次,从版本 2.4.0-test11 开始内核使用了一个特殊进程 keventd,它唯一的任务就是运行 scheduler 队列中的任务。keventd 为它运行的任务提供了可预期的进程上下文,而不象以前的实现,任务是在完全随机的进程上下文中运行的。

对于 keventd 的执行有几点是值得牢记的。首先,这个队列中的任务可以睡眠,一些内核代码就使用了这一优点。但是,好的代码应该只睡眠很短的时间,因为在 keventd 睡眠的时候,调度器队列中的其他任务就不会再运行了。还有一点需要牢记,你的任务是和其它任务共享调度器队列,这些任务也可以睡眠。正常情况下,调度器队列中的任务会很快运行(也许甚至在schedule_task返回之前)。但如果其它某个任务睡眠了,轮到你的任务执行时,中间流逝的时间会显得很久。所以那些有严格的执行时限的任务应该使用其它队列。

/proc/jiqsched 文件是使用调度器队列的示例文件,该文件对应的 read 函数以如下的方式将任务放进队列中:



int jiq_read_sched(char *buf, char **start, off_t offset,
                  int len, int *eof, void *data)
{

   jiq_data.len = 0;               /* nothing printed, yet */
   jiq_data.buf = buf;             /* print in this place */
   jiq_data.jiffies = jiffies;     /* initial time */

   /* jiq_print will queue_task() again in jiq_data.queue */
   jiq_data.queue = SCHEDULER_QUEUE;

   schedule_task(&jiq_task);             /* ready to run */
   interruptible_sleep_on(&jiq_wait);    /* sleep till completion */

   *eof = 1;
   return jiq_data.len;
}



读取 /proc/jiqsched 文件产生如下输出:



time  delta interrupt  pid cpu command
601687   0        0       2   1 keventd
601687   0        0       2   1 keventd
601687   0        0       2   1 keventd
601687   0        0       2   1 keventd
601687   0        0       2   1 keventd
601687   0        0       2   1 keventd
601687   0        0       2   1 keventd
601687   0        0       2   1 keventd
601687   0        0       2   1 keventd



上面的输出中,time域是任务运行时的jiffies值,delta是自任务最近一次运行以来jiffies的增量,interrupt是in_interrupt函数的输出,pid是运行进程的ID,cpu是正被使用的CPU的编号(在单处理器系统中始终为 0),command是当前进程正在运行的命令。

在这个例子中,我们看到,任务总是在 keventd 进程中运行,而且运行得非常快,一个不断把自己重复提交给调度器队列的任务可以在一次定时器滴答中运行数百甚至数千次。即使是在一个负载很重的系统,调度器队列的延迟也是非常小的。

定时器队列
定时器队列的使用方法和调度器队列不同,它(tq_timer)是可以直接操作的。还有,定时器队列是在中断模式下执行的。另外,该队列一定会在下一个时钟滴答被运行,这消除了可能因系统负载造成的延迟。

示例代码使用定时器队列实现了/proc/jiqtimer。使用这个队列要用到 queue_task 函数。



int jiq_read_timer(char *buf, char **start, off_t offset,
                  int len, int *eof, void *data)
{

   jiq_data.len = 0;            /* nothing printed, yet */
   jiq_data.buf = buf;          /* print in this place */
   jiq_data.jiffies = jiffies;  /* initial time */
   jiq_data.queue = &tq_timer;  /* reregister yourself here */

   queue_task(&jiq_task, &tq_timer);     /* ready to run */
   interruptible_sleep_on(&jiq_wait);    /* sleep till completion */

   *eof = 1;
   return jiq_data.len;
}



下面是在我的系统在编译一个新内核时运行命令head /proc/jiqtimer输出的结果:



   time  delta interrupt  pid cpu command
45084845   1        1    8783   0 cc1
45084846   1        1    8783   0 cc1
45084847   1        1    8783   0 cc1
45084848   1        1    8783   0 cc1
45084849   1        1    8784   0 as
45084850   1        1    8758   1 cc1
45084851   1        1    8789   0 cpp
45084852   1        1    8758   1 cc1
45084853   1        1    8758   1 cc1
45084854   1        1    8758   1 cc1
45084855   1        1    8758   1 cc1



注意,这次在任务的每次执行之间正好都经过了一个定时器滴答,而且正在运行的可能是任意一个进程。

立即队列
最后一个可由模块代码使用的预定义队列是立即队列。这个队列通过底半处理机制运行,所以要用它还需额外的步骤。底半处理程序只有在通知内核需要它运行时才会运行,这是通过“标记”底半部完成的。对于tq_immediate,必须调用mark_bh(IMMEDIATE_BH)。注意必须在任务插入队列后才能调用mark_bh,否则可能在任务还没加入队列时内核就开始运行队列了。

立即队列是系统处理得最快的队列――它反应最快并且在中断期间运行。立即队列既可以由调度器执行,也可以在一个进程从系统调用返回时被尽快地执行,这取决于哪个事件先发生。典型的输出大致如下:



   time  delta interrupt  pid cpu command
45129449   0        1    8883   0 head
45129453   4        1       0   0 swapper
45129453   0        1     601   0 X
45129453   0        1     601   0 X
45129453   0        1     601   0 X
45129453   0        1     601   0 X
45129454   1        1       0   0 swapper
45129454   0        1     601   0 X
45129454   0        1     601   0 X
45129454   0        1     601   0 X
45129454   0        1     601   0 X
45129454   0        1     601   0 X
45129454   0        1     601   0 X
45129454   0        1     601   0 X



显然该队列不能用于延迟任务的执行――它是个“立即”队列。相反,它的目的是使任务尽快地得以执行,但是要在“安全的时间”内。这对中断处理非常有用,因为它提供了在实际的中断处理程序之外执行处理程序代码的一个入口点,例如接收网络包的机制就类似这样。

注意不要把任务重新注册到立即队列中(尽管/proc/jiqimmed为了演示而这么做),这种做法没什么好处,而且在某些版本/平台的搭配上运行时会锁死计算机。因为在有些实现中会不断重运行立即队列直到它空为止。这种情况出现过,例如在PC上运行2.0版本的时候。

6.4.4  运行自己的工作队列
声明新的任务队列并不困难。驱动程序可以随意地声明一个甚至多个新任务队列。这些队列的使用和我们前面讨论过的预定义队列差不多。

与预定义队列不同的是,内核不会自动处理定制的任务队列。定制的任务队列要由程序员自己维护,并安排运行方法。

下面的宏声明一个定制队列并扩展为变量声明。最好把它放在文件开头的地方,所有函数的外面:



DECLARE_TASK_QUEUE(tq_custom);



声明完队列,就可以调用下面的函数对任务进行排队。上面的宏和下面的调用相匹配:



queue_task(&custom_task, &tq_custom);



当要运行累积的任务队列时,执行下面一行,运行tq_custom队列:



run_task_queue(&tq_custom);



如果现在想测试定制的任务队列,则需要在某个预定义的队列中注册一个函数来触发这个队列。尽管看起来象绕了弯路,但其实并非如此。当需要累积任务以便同时得到执行时,定制的任务队列是非常有用的,尽管需要用另一个队列来决定这个“同时”。

6.4.5  Tasklet
就在 2.4 内核发布之前,开发者们增加了一种用于内核任务延迟的新机制。这种新机制称为tasklet,现在是实现底半任务的推荐方法。实际上,现在的底半处理程序本身就是用 tasklet 实现的。

tasklet 在很多方面类似任务队列。它们都是把任务延迟到安全时间执行的一种方式,都在中断期间运行。象任务队列一样,即使被调度多次,tasklet 也只运行一次,不过tasklet 可以在 SMP 系统上和其它(不同的) tasklet 并行地运行。在SMP系统上,tasklet 还被确保在第一个调度它的 CPU 上运行,因为这样可以提供更好的高速缓存行为,从而提高性能。

每个 tasklet 都与一个函数相联系,当 tasklet 要运行的时候该函数被调用。该函数只有一个 unsigned long 类型的参数,这多少使一些内核开发者的生活变得轻松;但对那些宁愿传递一个指针的开发人员来说肯定是增加了苦恼。把long类型的参数转换为一个指针类型在所有已支持的平台上都是安全的操作,在内存管理中(第13章讨论)更是普遍使用。这个tasklet的函数的类型是void,无返回值。

tasklet 的实现部分在 <linux/interrupt.h> 中,它自己必须用下列中的一种来声明:

DECLARE_TASKLET(name, function, data);
用指定的名字 name 声明一个 tasklet,在该 tasklet 执行时(后面要讲到),指定的函数 function 被调用,传递的参数值为  (unsigned long) data 。

DECLARE_TASKLET_DISABLED(name, function, data);
和上面一样声明一个 tasklet,不过初始状态是“禁止的”,意味着可以被调度但不会执行,直到被“使能”以后才能执行。

用2.4的头文件编译 jiq 示例驱动程序,可以实现 /proc/jiqtasklet,它和其他的 jiq 入口工作类似,只不过使用了tasklet。我们并没有在 sysdep.h 中为旧版本模拟实现 tasklet。该模块如下定义它的 tasklet:



void jiq_print_tasklet (unsigned long);
DECLARE_TASKLET (jiq_tasklet, jiq_print_tasklet, (unsigned long)
  &jiq_data);



当驱动程序要调度一个 tasklet 运行的时候,它调用 tasklet_schedule:



tasklet_schedule(&jiq_tasklet);



一旦一个 tasklet 被调度,它就肯定会在一个安全时间运行一次(如果已经被使能)。tasklet 可以重新调度自己,其方式和任务队列一样。在多处理器系统上,一个 tasklet 无须担心自己会在多个处理器上同时运行,因为内核采取了措施确保任何 tasklet 都只能在一个地方运行。但是,如果驱动程序中实现了多个 tasklet,那么就可能会有多个 tasklet 在同时运行。在这种情况下,需要使用自旋锁来保护临界区代码(信号量是可以睡眠的,因为 tasklet 是在中断期间运行,所以不能用于tasklet)。

/proc/jiqtasklet的输出如下:



   time  delta interrupt  pid cpu command
45472377   0        1    8904   0 head
45472378   1        1       0   0 swapper
45472379   1        1       0   0 swapper
45472380   1        1       0   0 swapper
45472383   3        1       0   0 swapper
45472383   0        1     601   0 X
45472383   0        1     601   0 X
45472383   0        1     601   0 X
45472383   0        1     601   0 X
45472389   6        1       0   0 swapper



注意这个tasklet总是在同一个CPU上运行,即使输出来自双CPU系统。

tasklet 子系统提供了一些其它的函数,用于高级的tasklet操作:

void tasklet_disable(struct tasklet_struct *t);
这个函数禁止指定的tasklet。该tasklet仍然可以用 tasklet_schedule 调度,但执行被推迟,直到重新被使能。

void tasklet_enable(struct tasklet_struct *t);
使能一个先前被禁止的tasklet。如果该tastlet已经被调度,它很快就会运行(但一从 tasklet_enable 返回就直接运行)。

void tasklet_kill(struct tasklet_struct *t);
该函数用于对付那些无休止地重新调度自己的 tasklet。tasklet_kill 把指定的 tasklet 从它所在的所有队列删除。为避免与正重新调度自己的tasklet产生竞态,该函数会等到tasklet执行,然后再把它移出队列。这样就可以确保 tasklet 不会在中途被打断。然而,如果目标 tasklet 当前既没有运行也没有重调度自己,tasklet_kill会挂起。tasklet_kill不能在中断期间被调用。

6.5  内核定时器
内核中最终的计时资源还是定时器。定时器用于调度函数(定时器处理程序)在未来某个特定时间执行。与任务队列和 tasklet 不同,我们可以指定某个函数在未来何时被调用,但不能确定队列中的任务会在何时执行。另外,内核定时器与任务队列相似的是,注册的处理函数只执行一次――定时器不是循环执行的。

有时候要执行的操作不在任何进程上下文内,比如关闭软驱马达和中止某个耗时的关闭操作,在这些情况下,延迟从 close 调用的返回对于应用程序不合适,而且这时使用任务队列是非常浪费的,因为直到完成该操作所必须的时间过去之前,队列中的任务需要不断重新注册自己。

这时,使用定时器就方便得多。注册处理函数一次,当定时器超时后内核就调用它一次。这种处理一般较适合由内核完成,但有时驱动程序也需要,就象软驱马达的例子。

内核定时器被组织成双向链表。这意味着我们可以加入任意多的定时器。定时器包括它的超时值(单位是jiffies)和超时时要调用的函数。定时器处理程序需要接收一个参数,该参数和处理程序函数指针本身一起存放在一个数据结构中。

定时器的数据结构如下,取自头文件 <linux/timer.h>:



struct timer_list {
    struct timer_list *next;          /* never touch this */
    struct timer_list *prev;          /* never touch this */
    unsigned long expires;            /* the timeout, in jiffies */
    unsigned long data;               /* argument to the handler */
    void (*function)(unsigned long);  /* handler of the timeout */
    volatile int running;             /* added in 2.4; don't touch */
};




定时器的超时值是个 jiffies值,当jiffies值大于等于timer->expires时,timer->function函数就要运行。timeout值是个绝对数值,它通常是用 jiffies 的当前值加上需要的延迟量计算出来的。

一旦完成对 timer_list 结构的初始化,add_timer 函数就将它插入一张有序链表中,该链表每秒钟会被查询 100 次左右。即使某些系统(如Alpha)使用更高的时钟中断频率,也不会更频繁地检查定时器列表。因为如果增加定时器分辨率,遍历链表的代价也会相应增加。

用于操作定时器的有如下函数:

void init_timer(struct timer_list *timer);
该内联函数用来初始化定时器结构。目前,它只将prev和next指针清零(在SMP系统上还有运行标志)。强烈建议程序员使用该函数来初始化定时器而不要显式地修改结构内的指针,以保证向前兼容。

void add_timer(struct timer_list *timer);
该函数将定时器插入活动定时器的全局队列。

int mod_timer(struct timer_list *timer, unsigned long expires);
如果要更改定时器的超时时间则调用它,调用后定时器使用新的 expires 值。

int del_timer(struct timer_list *timer);
如果需要在定时器超时前将它从列表中删除,则应调用 del_timer 函数。但当定时器超时时,系统会自动地将它从链表中删除。

int del_timer_sync(struct timer_list *timer);
该函数的工作类似 del_time,不过它还确保了当它返回时,定时器函数不在任何 CPU 上运行。当一个定时器函数在无法预料的时间运行时,使用del_timer_sync可避免产生竞态,大多数情况下都应该使用这个函数。调用 del_timer_sync 时还必须保证定时器函数不会使用 add_timer 把它自己重新加入队列。

使用定时器的一个例子是 jiq 示例模块。/proc/jitimer 文件使用一个定时器来产生两行数据,所使用的打印函数和前面任务队列中用到的是同一个。第一行数据是由 read 调用产生的(由查看/proc/jitimer的用户进程调用),而第二行是 1 秒后后定时器函数打印出的。

用于 /proc/jitimer文件的代码如下所示:



struct timer_list jiq_timer;

void jiq_timedout(unsigned long ptr)
{
   jiq_print((void *)ptr);            /* print a line */
   wake_up_interruptible(&jiq_wait);  /* awaken the process */
}


int jiq_read_run_timer(char *buf, char **start, off_t offset,
                  int len, int *eof, void *data)
{

   jiq_data.len = 0;      /* prepare the argument for jiq_print() */
   jiq_data.buf = buf;
   jiq_data.jiffies = jiffies;
   jiq_data.queue = NULL;      /* don't requeue */

   init_timer(&jiq_timer);              /* init the timer structure */
   jiq_timer.function = jiq_timedout;
   jiq_timer.data = (unsigned long)&jiq_data;
   jiq_timer.expires = jiffies + HZ; /* one second */

   jiq_print(&jiq_data);   /* print and go to sleep */
   add_timer(&jiq_timer);
   interruptible_sleep_on(&jiq_wait);
   del_timer_sync(&jiq_timer);  /* in case a signal woke us up */
   
   *eof = 1;
   return jiq_data.len;
}



运行命令 head /proc/jitimer得到如下输出结果:



   time  delta interrupt  pid cpu command
45584582   0        0    8920   0 head
45584682 100        1       0   1 swapper



从输出中可以发现,打印出最后一行的定时器函数是在中断模式运行的。

可能看起来有点奇怪的是,定时器总是可以正确地超时,即使处理器正在执行系统调用。我在前面曾提到,运行在内核态的进程不会被调出,但时钟中断是个例外,它与当前进程无关,独立完成了自己的任务。读者可以试试同时在后台读 /proc/jitbusy 文件和在前台读 /proc/jitimer 文件会发生什么。这时尽管看起来系统似乎被忙等待的系统调用给锁死住了,但定时器队列和内核定时器还是能不断得到处理。

因此,定时器是另一个竞态资源,即使是在单处理器系统中。定时器函数访问的任何数据结构都要进行保护以防止并发访问,保护方法可以用原子类型(第10章讲述)或者用自旋锁。

删除定时器时也要小心避免竞态。考虑这样一种情况:某一模块的定时器函数正在一个处理器上运行,这时在另一个处理器上发生了相关事件(文件被关闭或模块被删除)。结果是,定时器函数等待一种已不再出现的状态,从而导致系统崩溃。为避免这种竞态,模块中应该用 del_timer_sync 代替 del_timer。如果定时器函数还能够重新启动自己的定时器(这是一种普遍使用的模式),则应该增加一个“停止定时器”标志,并在调用del_timer_sync之前设置。这样定时器函数执行时就可以检查该标志,如果已经设置,就不会用 add_timer 重新调度自己了。

还有一种会引起竞态的情况是修改定时器:先用 del_timer 删除定时器,再用 add_timer 加入一个新的以达到修改目的。其实在这种情况下简单地使用 mod_timer 是更好的方法。

6.6  向后兼容性
任务队列和时间机制的实现多年来基本保持着相对的稳定。不过,还是有一些值得注意的改进。

sleep_on_timeout、interruptible_sleep_on_timeout和schedule_timeout这几个函数是在2.2版本内核才加入的。在使用2.0的时期,超时值是通过 task 结构中的一个变量(timeout)处理的。作一个比较,现在的代码是这样进行调用的:



interruptible_sleep_on_timeout(my_queue, timeout);



而以前则是如下这样编写:



current->timeout = jiffies + timeout;
interruptible_sleep_on(my_queue);



头文件sysdep.h为2.4以前的内核重建了schedule_timeout,所以可以在2.0和2.2版本使用新语法并正常运行:



extern inline void schedule_timeout(int timeout)
{
   current->timeout = jiffies + timeout;
   current->state = TASK_INTERRUPTIBLE;
   schedule();
   current->timeout = 0;
}



2.0 版本还有另外两个函数可把函数放入任务队列。中断被禁止时可以用queue_task_irq代替queue_task,这会提高一小点性能。queue_task_irq_off更快些,但在任务已经插入队列或正在运行时会出错,所以只有在确保这类情况不会发生时才能使用。这两个函数在提升性能方面都没什么好处,从内核 2.1.30 开始把它们去掉了。任何情况下,使用 queue_task 都能在所有内核版本下工作。(要注意一点,在 2.2 及其以前内核中,queue_task 返回值的类型是void)

2.4内核之前不存在schedule_task函数及 keventd 进程,使用的是另一个预定义任务队列 tq_scheduler。tq_scheduler队列中的任务在 schedule 函数中执行,所以总是运行在进程上下文中。然而,“提供”上下文的进程总是不同的,它有可能是当时正被CPU调度运行的任何一个进程。tq_scheduler通常有比较大的延迟,特别是对那些会重复提交自己的任务更是如此。sysdep.h 在 2.0 和 2.2 系统上对 schedule_task 的实现如下:



extern inline int schedule_task(struct tq_struct *task)
{
       queue_task(task, &tq_scheduler);
       return 1;
}



前面已经提到,2.3内核系列中增加了tasklet机制。在此之前,只有任务队列可以用于“立即延迟”的执行。底半处理部分也改动了,不过大多数改动对驱动程序开发人员是透明的。sysdep.h中不再模拟tasklet在旧内核上的实现,它们对驱动程序操作来说并非严格必要。如果想要保持向后兼容,要么编写自己的模拟代码,要么用任务队列代替。

Linux 2.0中没有 in_interrupt函数,代替它的是一个全局变量intr_count,记录着正运行的中断处理程序的个数。查询 intr_count 的语法和调用 in_interrupt 差不多,所以在sysdep.h 中保持兼容性是很容易实现的。

函数 del_timer_sync 在内核 2.4.0-test2 之前还没有引入。sysdep.h 中进行了一些替换,以便使用旧的内核头文件也可以编译。2.0版本内核也没有mod_timer。这个问题也在兼容性头文件中得以解决。

6.7  快速参考
本章引入如下符号:

#include <linux/param.h>
HZ
HZ符号指出每秒钟产生的时钟滴答数。

#include <linux/sched.h>
volatile unsigned long jiffies
jiffies变量每个时钟滴答后加1,因此它每秒增加 HZ 次。

#include <asm/msr.h>
rdtsc(low,high);
rdtscl(low);
读取时间戳计数器或其低半部分。头文件和宏是 PC 类处理器特有的,其它平台可能需要用汇编语句实现类似功能。

extern struct timeval xtime;
当前时间,由最近一次定时器滴答计算出。

#include <linux/time.h>
void do_gettimeofday(struct timeval *tv);
void get_fast_time(struct timeval *tv);
这两个函数返回当前时间。前者具有很高的分辨率,后者更快些,但分辨率较差。

#include <linux/delay.h>
void udelay(unsigned long usecs);
void mdelay(unsigned long msecs);
这两个函数引入整数数目的微秒或毫秒的延迟。前一个应用于不超过1毫秒的延迟;后一个使用时要格外慎重,因为它们使用的都是忙等待循环。

int in_interrupt();
如果处理器正在中断模式运行,就返回非0值。

#include <linux/tqueue.h>
DECLARE_TASK_QUEUE(variablename);
该宏声明一个新的变量并作初始化。

void queue_task(struct tq_struct *task, task_queue *list);
该函数注册一个稍后执行的任务。

void run_task_queue(task_queue *list);
该函数运行任务队列。

task_queue tq_immediate, tq_timer;
这些预定义的任务队列在内核调度新的进程前(tq_immediate)尽快地,或者在每个时钟滴答后(tq_timer)得到执行。

int schedule_task(struct tq_struct *task);
调度一个任务在调度器队列运行。

#include <linux/interrupt.h>
DECLARE_TASKLET(name, function, data)
DECLARE_TASKLET_DISABLED(name, function, data)
声明一个 tasklet 结构,运行时它将调用指定的函数function(并将指定参数unsigned long data传递给函数)。第二种形式把 tasklet 初始化为禁止状态,直到明确地使能后tasklet才能运行。

void tasklet_schedule(struct tasklet_struct *tasklet);
调度指定的 tasklet 运行。如果该 tasklet 没有被禁止,它将在调用了 tasklet_schedule 的CPU上很快得到执行。

tasklet_enable(struct tasklet_struct *tasklet);
tasklet_disable(struct tasklet_struct *tasklet);
这两个函数分别使能和禁止指定的tasklet。被禁止的 tasklet 可以被调度,但只有使能后才能运行。

void tasklet_kill(struct tasklet_struct *tasklet);
使一个正“无休止重新调度”的tasklet停止执行。该函数可以阻塞,而且不能在中断期间调用。

#include <linux/timer.h>
void init_timer(struct timer_list * timer);
该函数初始化新分配的定时器。

void add_timer(struct timer_list * timer);
该函数将定时器插入待处理定时器的全局队列。

int mod_timer(struct timer_list *timer, unsigned long expires);
该函数用于更改一个已调度的定时器结构中的超时时间。

int del_timer(struct timer_list * timer);
del_timer函数将定时器从待处理定时器队列中删除。如果队列中存在该定时器,del_timer返回1,否则返回0。

int del_timer_sync(struct timer_list *timer);
该函数类似del_timer,但是确保定时器函数当前不在其它 CPU 上运行。


版权所有,不得擅自转载
阅读全文
0 0

相关文章推荐

img
取 消
img