snifer

[原创]谈谈linux系统中的任务队列

0
阅读(18119)

许多驱动程序需要将任务延迟到以后处理,但又不想占用中断。Linux为此提供了两种方法:任务队列和内核定时器。任务队列的使用很灵活,可以或长或短地延迟任务到以后处理,在写中断处理程序时任务队列非常有用。内核定时器则用来调度任务在未来某个相对精确的时间执行,今天我就谈谈这个问题。

 

要使用到任务队列的一个典型情形是,硬件不产生中断,但仍希望提供阻塞的读。此时需要对设备进行轮询,但要小心地不使CPU负担过多无谓的操作。将读进程到指定的时间后(例如,使用current->timeout变量)唤醒并不是个很好的方法,因为每次轮询需要两次上下文切换,而且通常轮询机制在进程上下文之外才可能较好地实现。

 

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

 

快速完成这类不固定的任务的恰当方法是注册任务在未来执行。内核提供了对任务“队列”的支持,任务可以累积到队列上一块“运行”。你可以声明你自己的任务队列并且随意地操纵它,或者也可以将你的任务注册到预定义的任务队列中去,由内核来运行它。

 

下面一节将先概述任务队列,然后介绍预定义的任务队列,这让你可以开始进行一些有趣的测试(如果出错也可能挂起系统),最后介绍如何运行你自己的任务队列。

任务队列的特性

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

 

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

 

struct tq_struct {

        struct tq_struct *next;         /* 激活的bh的链接表 */

        unsigned long sync;             /* 必须初始化为零 */

        void (*routine)(void *);        /* 调用的函数 */

        void *data;                     /* 传递给函数的参数 */

};

 

       第一行注释中的bh指的是下半部处理程序(bottom-half)。下半部处理程序是“中断处理处理程序的下半部”;我们将在第9章的“下半部处理程序”一节介绍中断时详细讨论。

 

       任务队列是处理异步事件的重要资源,而且绝大多数的中断处理程序将它们的任务延迟到任务队列被处理时执行。另外,有些任务队列下半部处理程序,通过调用do_bottom_half函数来处理。本章并不要求你理解下半部处理,但必要时我也会涉及到。

 

       上面的数据结构中最重要的字段是routinedata。将要延迟的任务插入队列,必须先设置好结构的这些字段,并把nextsync两个字段清零。结构中的sync标志位用于避免同一任务被插入多次,这会破坏next指针。一旦任务被排入队列,该数据结构就被认为为内核“拥有”了,不能再被修改。

 

       与任务队列有关的其他数据结构还有task_queue,目前它实现为指向tq_struct结构的指针;如果将来需要扩充task_queue,只要用typedef将该指针定义为其他符号就可以了。

 

       下面的列表汇总了所有可以对tq_struct结构进行的操作;所有的函数都是内联的。

 

void queue_task(struct tq_struct *task, task_queue *list);

正如该函数的名字,本函数用于将任务排进队列中。它关闭了中断,避免了竞争,因此可以被模块中任一函数调用。

 

void queue_task_irq(struct tq_struct *task, task_queue *list);

与前者类似,但本函数只能由不可重入的函数调用(象中断处理程序,所以本函数的名字带上了irq)。它比queue_task函数要快一些,因为它在排队前不关闭中断。如果你在一个可重入的函数内调用本函数,由于没有屏蔽资源竞争,是很危险的。但是,本函数排除了“运行时排队”的情形(也即将任务插入正在运行的那个任务的位置上)

 

void queue_task_irq_off(struct tq_struct *task, task_queue *list);

本函数只能在中断已关闭的情况下调用。它比前两个函数要快,但没有防止象“并发排队”和“运行时排队”这样的资源竞争。

 

void run_task_queue(struct tq_struct *task, task_queue *list);

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

 

       2.1.30版的内核已经不提供queue_task_irqqueue_task_irq_off这两个函数了,被认为得不偿失。

       在研讨任务队列的细节之前,最好还是先介绍一下内部的一些实现细节。任务队列与相应的系统调用是异步执行的;这种异步执行特别需要注意,必须先介绍一下。

 

       任务队列要在安全的时间内运行。这里安全的意思是在执行时没有什么特别严格的要求。因为在处理任务队列时允许硬件中断,任务代码也不要求执行的非常快。但队列中的函数执行得也不能太慢,毕竟在整个处理任务队列的期间,只有硬件中断才能被系统处理。

 

       另一个与任务队列有关的概念是中断时间。在Linux中,中断时间是个软件上的概念,取决于内核的全局变量intr_count。任一时候该变量都记录了正在执行的中断处理程序被嵌套的层数*

 

       一般的计算流程中,当处理器允许某个进程时,intr_count值为0。当intr_count不为零时,,执行的代码就与系统的其他部分是异步的了。这些异步代码可以是硬件中断的处理或者是“软件中断”-与任何进程都无关的一个任务,我们称它在“中断时间内”运行。这种异步代码是不允许做某些操作的;特别的,它不能使当前进程进入睡眠,因为current指针的值与正在运行的软件中断代码无关。

 

       典型的例子是退出系统调用时要执行的代码。如果因为某个原因此时还有任务需要得到执行,内核可以一退出系统调用就处理它。这是个“软件中断”,intr_count值在处理这个待执行的任务之前会先加1。由于主线指令流被中断了,该函数算是在“中断时间”内被处理的。

 

       intr_count非零时,不能激活调度器。这也就意味着不允许调用kmalloc(GFP_KERNEL)。在中断时间内,只能进行原子性的分配(见第7章“掌握内存”的“优先权参数”一节),而原子性的分配较“普通的”分配更容易失败。

       如果运行在中断时间的代码调用了调度器,类似“Aiee: scheduling in interrupt”这样的错误信息和以16进制显示的调用点处的地址会打印到控制台上。2.1.37之后的版本,oops消息也会打印出来,通过分析寄存器的值可以进行调试。在中断时间内如果试图非原子性地按优先权分配内存,也会显示包括着调用者的调用点处地址的错误信息。

预定义的任务队列

延迟任务执行的简单方法是使用内核维护的任务队列。这种队列有下面描述的四种,但驱动程序只能用前三种。任务队列的定义在头文件<linux/queue.h>中,你的驱动程序代码要将它包含(include)进来。

 

tq_scheduler队列

当调度器被运行时该队列就会被处理。因为此时调度器在被调度出的进程的上下文中运行,所以该队列中的任务几乎可以做任何事;它们不会在中断时运行。

 

tq_timer队列

该队列由定时器队列处理程序(timer tick)运行。因为该处理程序(见函数do_timer)是在中断时间运行的,该队列中的所有任务就也是在中断时间内运行的了。

 

tq_immediate队列

立即队列在系统调用返回时或调度器运行时尽快得到处理的(不管两种情况谁先发生了)。该队列是在中断时间内得到处理的。

 

tq_disk队列

1.2版的内核不再提供这种任务队列了,内存管理例程内部使用,模块不能使用。

 

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

 

就写这么多吧,任务队列非常重要,如果你用得好能事半功倍哦!