资源描述
驱动程序需要将任务延迟到以后处理,但又不想借助中断。Linux 为此提供了三种方法:任务队列、tasklet(从内核 2.3.43 开始)和内核定时器。任务队列和 tasklet 的使用很灵活,可以或长或短地延迟任务到以后处理,在编写中断处理程序时非常有用,我们还将在第9章“Tasklet和底半部处理”一节中继续讨论。内核定时器则用来调度任务在未来某个指定时间执行,将在本章的“内核定时器”一节中讨论。
使用任务队列或tasklet的一个典型情形是,硬件不产生中断,但仍希望提供阻塞型的读取。此时需要对设备进行轮询,同时要小心地不使 CPU 负担过多无谓的操作。将读进程以固定的时间间隔唤醒(例如,使用 current->timeout 变量)并不是个很好的方法,因为每次轮询需要两次上下文切换(一次是切换到读进程中运行轮询代码,另一次是返回执行实际工作的某个进程),而且通常来讲,恰当的轮询机制应该在进程上下文之外实现。
类似的情形还有象不时地给简单的硬件设备提供输入。例如,有一个直接连接到并口的步进马达,要求该马达能一步步地移动,但马达每次只能移动一步。在这种情况下,由控制进程通知设备驱动程序进行移动,但实际上,移动是在 write 返回后,才在周期性的时间间隔内一步一步进行的。
快速完成这类不定操作的恰当方法是注册任务在未来执行。内核提供了对“任务队列”的支持,任务可以累积,而在运行队列时被“消耗”。我们可以声明自己的任务队列,并且在任意时刻触发它,或者也可以将任务注册到预定义的任务队列中去,由内核来运行(触发)它。
任务队列的本质
任务队列其实是一个任务链表,每个任务用一个函数指针和一个参数表示。任务运行时,它接受一个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函数用于运行累积在队列上的任务。除非你要声明和维护自己的任务队列,否则不必调用本函数。
如前所述,一个任务队列,实际上是一个函数链表。当调用 run_task_queue 运行某个队列时,列表中的每一项都会被执行。在编写和任务队列有关的函数时,必须牢记内核是在什么时候调用run_task_queue的,而且当内核调用 run_task_queue 时,实际的上下文将限制能够进行的操作。也不应对队列中任务的运行顺序做任何假定,它们每个都是独立完成自己的任务的。
那么任务队列在什么时候运行呢?如果使用的是下面一节介绍的预定义的任务队列,则答案是“在内核轮到它那里时”。不同的队列在不同的时间运行,只要内核没有其他更紧要的任务,它们总是会运行的。
更重要的是,当对任务进行排队的进程运行时,任务队列几乎肯定是不会运行的,相反,它们是异步执行的。到现在为止,示例驱动例程中所有的事情都是在这个执行系统调用的进程上下文中完成的。但当任务队列运行时,这个进程可能正在睡眠,或正在另一个处理器上运行,甚至可能已经完全退出了。
这种异步执行类似于硬件中断发生时的情景(我们会在第9章详细讨论)。实际上,任务队列常常是作为“软件中断”的结果而运行的。在中断模式(或中断期间)下,代码的运行会受到许多限制。我们现在介绍这些限制,这些限制还会在本书后面多次出现。我们也会多次重复,中断模式下的这些规则必须遵守,否则系统会有大麻烦。
许多动作需要在进程上下文中才能执行。如果处于进程上下文之外(比如在中断模式下),则必须遵守如下规则:
· 不允许访问用户空间。因为没有进程上下文,也就没有办法访问与任何一个特定进程相关联的用户空间。
· current指针在中断模式下是无效的,不能使用。
· 不能执行睡眠或调度。中断模式代码不可以调用schedule或者sleep_on;也不能调用任何可能引起睡眠的函数。例如,调用kmalloc(...,GFP_KERNEL)就不符合本规则。信号量也不能用,因为可能引起睡眠。
内核代码可以通过调用函数in_interrupt( ) 来判断自己是否正运行于中断模式,该函数无需参数,如果处理器在中断期间运行就返回非0值。
当前的任务队列实现还有一个特性,队列中的一个任务可以将自己重新插回到它原先所在的队列。举个例子,定时器队列中的任务可以在运行时将自己插回到定时器队列中去,从而在下一个定时器滴答又再次被运行。这是通过调用 queue_task 把自己放回队列来实现的。由于在处理任务队列之前,是先用NULL指针替换队列的头指针,也就是将任务队列初始化了,另外,在执行队列中的任务之前,首先将任务从队列中移出来,这样在任务将本身插入任务队列的时候,它其实是将指针指向新的任务队列。结果就是,随着旧队列的执行,新的队列逐渐生成。
尽管一遍遍地重新调度同一个任务看起来似乎没什么意义,但有时这也有些用处。例如,步进马达每次移动一步直到目的地,它的驱动程序就可以通过让任务在定时器队列上不断地重新调度自己来实现。其他的例子还有 jiq 模块,该模块中的打印函数通过重新调度自己来产生输出――结果是利用定时器队列产生多次迭代。
预定义的任务队列
延迟任务执行的最简单方法是使用由内核维护的任务队列。这种队列有好几种,但驱动程序只能使用下面列出的其中三种。任务队列的定义在头文件 <linux/queue.h> 中,驱动程序代码需要包含该头文件。
调度器队列
调度器队列在预定义任务队列中比较独特,它运行在进程上下文中,这意味着该队列中的任务可以更多的事情。在Linux 2.4,该队列由一个专门的内核线程 keventd 管理,通过函数 schedule_task 访问。在较老的内核版本,没有用keventd,所以该队列(tq_scheduler)是直接操作的。
tq_timer
该队列由定时器处理程序(定时器嘀哒)运行。因为该处理程序(见函数do_timer)是在中断期间运行的,因此该队列中的所有任务也是在中断期间运行的。
tq_immediate
立即队列是在系统调用返回时或调度器运行时得到处理,以便尽可能快地运行该队列。该队列在中断期间得到处理。
还有其它的预定义队列,但驱动程序开发中通常不会涉及到它们。
示例程序jiq
/*
* jiq.c -- the just-in-queue module
*
*/
#ifndef __KERNEL__
# define __KERNEL__
#endif
#ifndef MODULE
# define MODULE
#endif
#include <linux/config.h>
#include <linux/module.h>
#include <linux/sched.h>
#include <linux/kernel.h>
#include <linux/fs.h> /* everything... */
#include <linux/proc_fs.h>
#include <linux/errno.h> /* error codes */
#include <linux/tqueue.h>
#include <linux/interrupt.h> /* intr_count */
#include "sysdep.h"
/*
* This module is a silly one: it only embeds short code fragments
* that show how enqueued tasks `feel' theit environment
*/
#define LIMIT (PAGE_SIZE-128) /* don't print any more after this size */
/*
* Print information about the current environment. This is called from
* within the task queues. If the limit is reched, awake the reading
* process.
*/
DECLARE_WAIT_QUEUE_HEAD (jiq_wait);
struct tq_struct jiq_task; /* global: initialized to zero */
/*
* Keep track of info we need between task queue runs.
*/
struct clientdata {
int len;
char *buf;
unsigned long jiffies;
task_queue *queue;
} jiq_data;
#define SCHEDULER_QUEUE ((task_queue *) 1)
#ifdef HAVE_TASKLETS
void jiq_print_tasklet (unsigned long);
DECLARE_TASKLET (jiq_tasklet, jiq_print_tasklet, (unsigned long) &jiq_data);
#endif /* HAVE_TASKLETS */
/*
* Do the printing; return non-zero if the task should be rescheduled.
*/
int jiq_print(void *ptr)
{
struct clientdata *data = (struct clientdata *)ptr;
int len = data->len;
char *buf = data->buf;
unsigned long j = jiffies;
if (len > LIMIT) {
wake_up_interruptible(&jiq_wait);
return 0;
}
if (len == 0)
len = sprintf(buf," time delta interrupt pid cpu command\n");
else
len =0;
/* intr_count is only exported since 1.3.5, but 1.99.4 is needed anyways */
len += sprintf(buf+len,"%9li %3li %i %5i %3i %s\n",
j, j - data->jiffies,
in_interrupt (), current->pid, smp_processor_id (), current->comm);
data->len += len;
data->buf += len;
data->jiffies = j;
return 1;
}
/*
* Call jiq_print from a task queue
*/
void jiq_print_tq(void *ptr)
{
if (jiq_print (ptr)) {
struct clientdata *data = (struct clientdata *)ptr;
if (data->queue == SCHEDULER_QUEUE)
schedule_task(&jiq_task);
else if (data->queue)
queue_task(&jiq_task, data->queue);
if (data->queue == &tq_immediate)
mark_bh(IMMEDIATE_BH); /* this one needs to be marked */
}
}
/*
* Use the scheduler queue -- /proc/jiqsched
*/
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;
}
#ifdef USE_PROC_REGISTER
static int jiq_old_read_sched(char *buf, char **start, off_t offset, int len,
int unused)
{
int eof;
return jiq_read_sched(buf, start, offset, len, &eof, NULL);
}
struct proc_dir_entry jiq_proc_sched = {
0, /* low_ino: the inode -- dynamic */
8, "jiqsched", /* len of name and name */
S_IFREG | S_IRUGO, /* mode */
1, 0, 0, /* nlinks, owner, group */
0, NULL, /* size - unused; operations -- use default */
&jiq_old_read_sched, /* function used to read data */
/* nothing more */
};
#endif
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; /* re-register 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;
}
#ifdef USE_PROC_REGISTER
static int jiq_old_read_timer(char *buf, char **start, off_t offset, int len,
int unused)
{
int eof;
return jiq_read_timer(buf, start, offset, len, &eof, NULL);
}
struct proc_dir_entry jiq_proc_timer = {
0, /* low_ino: the inode -- dynamic */
8, "jiqtimer", /* len of name and name */
S_IFREG | S_IRUGO, /* mode */
1, 0, 0, /* nlinks, owner, group */
0, NULL, /* size - unused; operations -- use default */
&jiq_old_read_timer, /* function used to read data */
/* nothing more */
};
#endif
int jiq_read_immed(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_immediate; /* re-register yourself here */
queue_task(&jiq_task, &tq_immediate); /* ready to run */
mark_bh(IMMEDIATE_BH);
interruptible_sleep_on(&jiq_wait); /* sleep till completion */
*eof = 1;
return jiq_data.len;
}
#ifdef USE_PROC_REGISTER
static int jiq_old_read_immed(char *buf, char **start, off_t offset, int len,
int unused)
{
int eof;
return jiq_read_immed(buf, start, offset, len, &eof, NULL);
}
struct proc_dir_entry jiq_proc_immed = {
0, /* low_ino: the inode -- dynamic */
8, "jiqimmed", /* len of name and name */
S_IFREG | S_IRUGO, /* mode */
1, 0, 0, /* nlinks, owner, group */
0, NULL, /* size - unused; operations -- use default */
&jiq_old_read_immed, /* function used to read data */
/* nothing more */
};
#endif
#ifdef HAVE_TASKLETS
/*
* Call jiq_print from a tasklet
*/
void jiq_print_tasklet(unsigned long ptr)
{
if (jiq_print ((void *) ptr))
tasklet_schedule (&jiq_tasklet);
}
int jiq_read_tasklet(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 */
tasklet_schedule(&jiq_tasklet);
interruptible_sleep_on(&jiq_wait); /* sleep till completion */
*eof = 1;
return jiq_data.len;
}
/* No PROC_REGISTER junk since tasklets postdate all that */
#endif /* HAVE_TASKLETS */
/*
* This one, instead, tests out the timers.
*/
struct timer_list jiq_timer;
void jiq_timedout(unsigned long ptr)
{
jiq_print((void *)ptr); /* print a line */
wake_up_interruptible(&jiq_wait); /* awake 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;
}
#ifdef USE_PROC_REGISTER
static int jiq_old_read_run_timer(char *buf, char **start, off_t offset, int len,
int unused)
{
int eof;
return jiq_read_run_timer(buf, start, offset, len, &eof, NULL);
}
struct proc_dir_entry jiq_proc_run_timer = {
0, /* low_ino: the inode -- dynamic */
7, "jitimer", /* len of name and name */
S_IFREG | S_IRUGO, /* mode */
1, 0, 0, /* nlinks, owner, group */
0, NULL, /* size - unused; operations -- use default */
&jiq_old_read_run_timer, /* function used to read data */
/* nothing more */
};
#endif
/*
* the init/clean material
*/
int jiq_init(void)
{
/* these lines are in jiq_init() */
jiq_task.routine = jiq_print_tq;
jiq_task.data = (void *)&jiq_data;
#ifdef USE_PROC_REGISTER
proc_register_dynamic(&proc_root, &jiq_proc_sched);
proc_register_dynamic(&proc_root, &jiq_proc_timer);
proc_register_dynamic(&proc_root, &jiq_proc_immed);
proc_register_dynamic(&proc_root, &jiq_proc_run_timer);
#else
create_proc_read_entry("jiqsched", 0, NULL, jiq_read_sched, NULL);
create_proc_read_entry("jiqtimer", 0, NULL, jiq_read_timer, NULL);
create_proc_read_entry("jiqimmed", 0, NULL, jiq_read_immed, NULL);
create_proc_read_entry("jitimer", 0, NULL, jiq_read_run_timer, NULL);
#ifdef HAVE_TASKLETS
create_proc_read_entry("jiqtasklet", 0, NULL, jiq_read_tasklet, NULL);
#endif
#endif
#ifndef JIT_DEBUG
EXPORT_NO_SYMBOLS;
#endif
return 0; /* succeed */
}
void jiq_cleanup(void)
{
#ifdef USE_PROC_REGISTER
proc_unregister(&proc_root, jiq_proc_sched.low_ino);
proc_unregister(&proc_root, jiq_proc_timer.low_ino);
proc_unregister(&proc_root, jiq_proc_immed.low_ino);
proc_unregister(&proc_root, jiq_proc_run_timer.low_ino);
#else
remove_proc_entry("jiqsched", 0);
remove_proc_entry("jiqtimer", 0);
remove_proc_entry("jiqimmed", 0);
remove_proc_entry("jitimer", 0);
#ifdef HAVE_TASKLETS
remove_proc_entry("jiqtasklet", 0);
#endif
#endif
}
module_init(jiq_init);
module_exit(jiq_cleanup);
延迟计算的示例程序包含在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
展开阅读全文