- 论坛徽章:
- 0
|
第 6 章 时间流
至此,我们基本知道怎样编写一个功能完整的字符模块了。现实中的设备驱动程序,除了实现必需的操作外还要做更多工作,如计时、内存管理,硬件访问等等。幸好,内核中提供的许多机制可以简化驱动程序开发者的工作,我们将在后面几章陆续讨论驱动程序可以访问的一些内核资源。本章,我们先来看看内核代码是如何对时间问题进行处理的。按复杂程度递增排列,该问题包括:
- 理解内核时间机制
- 如何获得当前时间
- 如何将操作延迟指定的一段时间
- 如何调度异步函数到指定的时间后执行
6.1 内核中的时间间隔
我们首先要涉及的是时钟中断,操作系统通过时钟中断来确定时间间隔。中断是异步事件,通常由外部硬件触发。中断发生时,CPU 停止正在进行的任务,转而执行另一段特殊的代码(即中断服务例程,又称 ISR)来响应这个中断。中断和 ISR 的实现将在第 9 章讨论。
时钟中断由系统计时硬件以周期性的间隔产生,这个间隔由内核根据 HZ 的值设定,HZ 是一个与体系结构有关的常数,在文件 中定义。当前的Linux版本为大多数平台定义的 HZ 的值是 100,某些平台上是 1024,IA-64 仿真器上是 20。驱动程序开发者不应使用任何特定的 HZ 值来计数,不管你的平台使用的是哪一个值。
当时钟中断发生时,变量 jiffies 的值就增加。jiffies 在系统启动时初始化为 0,因此,jiffies 值就是自操作系统启动以来的时钟滴答的数目,jiffies 在头文件 中被定义为数据类型为 unsigned long volatile 型变量,这个变量在经过长时间的连续运行后有可能溢出(不过现在还没有哪种平台会在运行不到 16 个月就使 jiffies 溢出)。为了保证 jiffies 溢出时内核仍能正常工作,人们已做了很多努力。驱动程序开发人员通常不用考虑 jiffies 的溢出问题,知道有这种可能性就行了。
如果想改变系统时钟中断发生的频率,可以修改 HZ 值。有人使用 Linux 处理硬实时任务,他们增加了 HZ 值以获得更快的响应时间,为此情愿忍受额外的时钟中断产生的系统开销。总而言之,时钟中断的最好方法是保留 HZ 的缺省值,因为我们可以完全相信内核的开发者们,他们一定已经为我们挑选了最佳值。
6.1.1 处理器特有的寄存器
如果需要度量非常短的时间,或是需要极高的时间精度,可以使用与特定平台相关的资源,这是将时间精度的重要性凌驾于代码的可移植性之上的做法。
大多数较新的 CPU 都包含一个高精度的计数器,它每个时钟周期递增一次。这个计数器可用于精确地度量时间。由于大多数系统中的指令执行时间具有不可预测性(由于指令调度、分支预测、缓存等等),在运行具有很小时间粒度的任务时,使用这个时钟计数器是唯一可靠的计时方法。为适应现代处理器的高速度,满足衡量性能指标的紧迫需求,同时由于 CPU 设计中的多层缓存引起的指令时间的不可预测性,CPU 的制造商们引入了记录时钟周期这一测量时间的简单可靠的方法。所以绝大多数现代处理器都包含一个随时钟周期不断递增的计数寄存器。
基于不同的平台,在用户空间,这个寄存器可能是可读的,也可能不可读;可能是可写的,也可能不可写;可能是 64 位的也可能是 32 位的。如果是 32 位的,还得注意处理溢出的问题。无论该寄存器是否可以置 0,我们都强烈建议不要重置它,即使硬件允许这么做。因为总可以通过多次读取该寄存器并比较读出数值的差异来完成要做的事,我们无须要求独占该寄存器并修改它的当前值。
最有名的计数器寄存器就是 TSC(timestamp counter,时间戳计数器),从 x86 的 Pentium 处理器开始提供该寄存器,并包括在以后的所有 CPU 中。它是一个 64 位寄存器,记录 CPU 时钟周期数,内核空间和用户空间都可以读取它。
包含了头文件 (意指“machine-specific registers,机器特有的寄存器”)之后,就可以使用如下的宏:
rdtsc(low,high);
rdtscl(low);
前一个宏原子性地把 64 位的数值读到两个 32 位变量中;后一个只把寄存器的低半部分读入一个 32 位变量,在大多数情况,这已经够用了。举例来说,一个 500MHz 的系统使一个 32 位计数器溢出需 8.5 秒,如果要处理的时间肯定比这短的话,那就没有必要读出整个寄存器。
下面这段代码可以测量该指令自身的运行时间:
unsigned long ini, end;
rdtscl(ini); rdtscl(end);
printk("time lapse: %li\n", end - ini);
其他一些平台也提供了类似的功能,在内核头文件中还有一个与体系结构无关的函数可以代替 rdtsc,它就是 get_cycles,是在2.1版的开发过程中引入的。其原型是:
#include
cycles_t get_cycles(void);
在各种平台上都可以使用这个函数,在没有时钟周期记数寄存器的平台上它总是返回 0。cycles_t 类型是能装入对应 CPU 单个寄存器的合适的无符号类型。选择能装入单个寄存器的类型意味着,举例来说,get_cycles 用于 Pentium 的时钟周期计数器时只返回低 32 位。这种选择是明智的,它避免了多寄存器操作的问题,与此同时并未阻碍对该计数器的正常用法,即用来度量很短的时间间隔。
除了这个与体系结构无关的函数外,我们还将示例使用一段内嵌的汇编代码。为此,我们来给 MIPS 处理器实现一个 rdtscl 函数,功能就象 x86 的一样。
这个例子之所以基于 MIPS,是因为大多数 MIPS 处理器都有一个 32 位的计数器,在它们的内部“coprocessor 0”中命名为 register 9 寄存器。为了从内核空间读取该寄存器,可以定义下面的宏,它执行“从 coprocessor 0 读取”的汇编指令:*
#define rdtscl(dest) \
_ _asm_ _ _ _volatile_ _("mfc0 %0,$9; nop" : "=r" (dest))
注:nop 指令是必需的,防止了编译器在指令mfc0之后立刻访问目标寄存器。这种互锁(interlock)在 RISC处理器中是很典型的,在延迟期间编译器仍然可以调度其它指令执行。我们在这里使用nop,是因为内嵌汇编指令对编译器来说是个黑盒,不能进行优化。
通过使用这个宏,MIPS 处理器就可以执行和前面所示用于 x86 的相同的代码了。
gcc 内嵌汇编的有趣之处在于通用寄存器的分配使用是由编译器完成的。这个宏中使用的 %0 只是“参数 0”的占位符,参数 0 由随后的“作为输出(=)使用的任意寄存器(r)”定义。该宏还说明了输出寄存器要对应于 C 表达式 dest。内嵌汇编的语法功能强大但也比较复杂,特别是在对各寄存器使用有限制的平台上更是如此,如 x86 系列。完整的语法描述在 gcc 文档中提供,一般在 info 中就可找到。
这节展示的短小的C代码段已经在一个 K7 类的 x86 处理器和一个 MIPS VR4181 处理器(使用了刚才的宏)上运行过了。前者给出的时间消耗为 11 时钟周期,后者仅为 2 时钟周期。这是可以理解的,因为 RISC 处理器通常每时钟周期运行一条指令。
6.2 获取当前时间
内核一般通过 jiffies 值来获取当前时间。该数值表示的是自最近一次系统启动到当前的时间间隔,它和设备驱动程序不怎么相关,因为它的生命期只限于系统的运行期(uptime)。但驱动程序可以利用 jiffies 的当前值来计算不同事件间的时间间隔(比如在输入设备驱动程序中就用它来分辨鼠标的单双击)。简而言之,利用 jiffies 值来测量时间间隔在大多数情况下已经足够了,如果还需要测量更短的时间,就只能使用处理器特有的寄存器了。
驱动程序一般不需要知道墙钟时间(指日常生活使用的时间),通常只有象 cron 和 at 这样用户程序才需要墙钟时间。需要墙钟时间的情形是使用设备驱动程序的特殊情况,此时可以通过用户程序来将墙钟时间转换成系统时钟。直接处理墙钟时间常常意味着正在实现某种策略,应该仔细审视一下是否该这样做。
如果驱动程序真的需要获取当前时间,可以使用 do_gettimeofday 函数。该函数并不返回今天是本周的星期几或类似的信息;它是用秒或微秒值来填充一个指向 struct timeval 的指针变量,gettimeofday 系统调用中用的也是同一变量。do_gettimeofday 的原型如下:
#include
void do_gettimeofday(struct timeval *tv);
源码中描述 do_gettimeofday 在许多体系结构上有“接近微秒级的分辨率”,然而实际精度是随不同的平台而变化的,在旧版本的内核中还会低些。当前时间也可以通过 xtime 变量(类型为struct timeval)获得(但精度差些),但是,并不鼓励直接使用该变量,因为除非关闭中断,否则无法原子性地访问 timeval 变量的两个成员 tv_sec 和 tv_usec。在 2.2 版的内核中,一个快捷安全的获得时间的办法(可能精度会差些)是使用 get_fast_time:
void get_fast_time(struct timeval *tv);
获取当前时间的代码可见于 jit(“Just In Time”)模块,源文件可以从 O'Reilly 公司的 FTP 站点获得。jit 模块将创建 /proc/currentime 文件,读取该文件将以 ASCII 码的形式返回三项:
- 由 do_gettimeofday 返回的当前时间
- 从 xtime 钟获得的当前时间
- jiffies 的当前值
我们选择用动态的 /proc 文件,是因为这样模块代码量会小些――不值得为返回三行文本而写一个完整的设备驱动程序。
如果用 cat 命令在一个时钟滴答内多次读该文件,就会发现 xtime 和 do_gettimeofday 两者的差异了,xtime 更新的次数不那么频繁:
morgana% cd /proc; cat currentime currentime currentime
gettime: 846157215.937221
xtime: 846157215.931188
jiffies: 1308094
gettime: 846157215.939950
xtime: 846157215.931188
jiffies: 1308094
gettime: 846157215.942465
xtime: 846157215.941188
jiffies: 1308095
6.3 延迟执行
设备驱动程序经常需要将某些特定代码延迟一段时间后执行――通常是为了让硬件能完成某些任务。这一节将介绍许多实现延迟的不同技术,哪种技术最好取决于实际环境中的具体情况。我们将介绍所有的这些技术并指出各自的优缺点。
一件需要考虑的很重要的事情是所需的延迟长度是否多于一个时钟滴答。较长的延迟可以利用系统时钟;较短的延迟通常必须通过软件循环来获得。
6.3.1 长延迟
如果想把执行延迟若干个时钟滴答,或者对延迟的精度要求不高(比如,想延迟整数数目的秒数),最简单的也是最笨的实现如下,也就是所谓的“忙等待”:
unsigned long j = jiffies + jit_delay * HZ;
while (jiffies
这种实现当然要避免。我们在这里提到它,只是因为读者可能某时需要运行这段代码,以便更好地理解其他的延迟技术。
还是先看看这段代码是如何工作的。因为内核的头文件中 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 应是延迟到达时的 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
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,而指针参数可用来将一个数据结构传入函数,或者可以被忽略。队列本身是一个结构(即任务)链表,并由声明和操纵它们的内核模块所拥有。模块要全权负责这些数据结构的分配和释放,为此一般使用静态的数据结构。
队列元素由下面这个结构来描述,这段代码是直接从头文件 拷贝下来的:
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 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 的实现部分在 中,它自己必须用下列中的一种来声明:
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)和超时时要调用的函数。定时器处理程序需要接收一个参数,该参数和处理程序函数指针本身一起存放在一个数据结构中。
定时器的数据结构如下,取自头文件 :
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
HZ
HZ符号指出每秒钟产生的时钟滴答数。
#include
volatile unsigned long jiffies
jiffies变量每个时钟滴答后加1,因此它每秒增加 HZ 次。
#include
rdtsc(low,high);
rdtscl(low);
读取时间戳计数器或其低半部分。头文件和宏是 PC 类处理器特有的,其它平台可能需要用汇编语句实现类似功能。
extern struct timeval xtime;
当前时间,由最近一次定时器滴答计算出。
#include
void do_gettimeofday(struct timeval *tv);
void get_fast_time(struct timeval *tv);
这两个函数返回当前时间。前者具有很高的分辨率,后者更快些,但分辨率较差。
#include
void udelay(unsigned long usecs);
void mdelay(unsigned long msecs);
这两个函数引入整数数目的微秒或毫秒的延迟。前一个应用于不超过1毫秒的延迟;后一个使用时要格外慎重,因为它们使用的都是忙等待循环。
int in_interrupt();
如果处理器正在中断模式运行,就返回非0值。
#include
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
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
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 上运行。
本文来自ChinaUnix博客,如果查看原文请点:http://blog.chinaunix.net/u/23470/showart_171396.html |
|