linux中的时间流
来源:互联网
作者:west263.com
时间:2008-04-16
西部数码-全国
虚拟主机10强!40余项虚拟主机管理功能,全国领先!双线多线
虚拟主机南北访问畅通无阻!免费赠送
企业邮局,.CN域名,
自助建站480元起,免费试用7天,满意再付款! P4
主机租用799元/月.月付免压金!

中,他自己必须用下列中的一种来声明:
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不能在中断期间被调用。
内核定时器
内核中最终的计时资源还是定时器。定时器用于调度函数(定时器处理程式)在未来某个特定时间执行。和任务队列和 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 是更好的方法。
向后兼容性
任务队列和时间机制的实现多年来基本保持着相对的稳定。但是,还是有一些值得注意的改进。
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。这个问题也在兼容性头文档中得以解决。
快速参考
本章引入如下符号:
#include
文章整理:西部数码--专业提供域名注册、虚拟主机服务
http://www.west263.com
以上信息与文章正文是不可分割的一部分,如果您要转载本文章,请保留以上信息,谢谢!