By: 潘云登
Date: 2009-6-5
Email: intrepyd@gmail.com
Homepage: http://blog.csdn.net/intrepyd
Copyright: 该文章版权由潘云登所有。可在非商业目的下任意传播和复制。
对于商业目的下对本文的任何行为需经作者同意。
1. 本文内容对应《linux设备驱动程序》第七章。
2. 参考《linux内核设计与实现》。
3. 希望本文对您有所帮助,也欢迎您给我提意见和建议。
4. 本文包含以下内容:
² 内核中的时间概念
² 延迟执行
² 内核定时器
² tasklet
² 工作队列
² 共享队列
内核通过定时器中断来跟踪时间流。时钟中断由系统定时硬件以周期性的间隔产生,这个间隔由内核根据HZ的值设定。HZ是一个与体系结构有关的常数,定义在<asm/param.h>中。提高HZ,能够提高时间驱动事件的准确度和解析度,如使内核定时器以更高的准确度运行,并提高进程抢占的准确度。带来的负作用则是内核执行时钟中断处理程序的负担加重。
每当时钟中断发生时,内核内部计数器的值就增加一。这个计数器是一个64位的变量,称为jiffies_64。它的值在系统引导时被初始化为0,即它是自上次操作系统引导以来的时钟滴答数。驱动程序通常访问的是jiffies变量,它是unsigned long类型,要么和jiffies_64相同,要么仅仅是jiffies_64的低32位。两个变量定义在<linux/jiffies.h>中,均应该被看成只读变量。
/*<linux/jiffies.h>*/ extern u64 __jiffy_data jiffies_64; extern unsigned long volatile __jiffy_data jiffies; /*volatile避免编译器的内存读优化*/
/*直接使用jiffies*/ unsigned long j, stamp_1; j = jiffies; /* read the current value */ stamp_1 = j + HZ; /* 1 second in the future */
/*读取64位jiffies_64,应该使用下面这个函数*/ u64 get_jiffies_64(void); |
32位的jiffies变量,在HZ为1000的情况下,49.7天后就会溢出。内核提供了四个宏来比较jiffies,它们能正确处理jiffies计数回绕的情况。
#include <linux/jiffies.h> /*如果a所代表的时间比b靠后,则第一个宏返回真*/ /*如果a比b靠前,则第二个宏返回真*/ /*后面两个宏用来比较“靠后或者相等”及“靠前或者相等”*/ int time_after(unsigned long a, unsigned long b); int time_before(unsigned long a, unsigned long b); int time_after_eq(unsigned long a, unsigned long b); int time_before_eq(unsigned long a, unsigned long b); |
用户空间使用timeval、timespec结构或者用年月日表达的墙上时间来表述时间。内核提供了以下函数完成它们与jiffies之间的转换。
#include <linux/time.h> struct timespec { time_t tv_sec; /* seconds */ long tv_nsec; /* nanoseconds */ };
struct timeval { time_t tv_sec; /* seconds */ suseconds_t tv_usec; /* microseconds */ };
unsigned long timespec_to_jiffies(struct timespec *value); void jiffies_to_timespec(unsigned long jiffies, struct timespec *value); unsigned long timeval_to_jiffies(struct timeval *value); void jiffies_to_timeval(unsigned long jiffies, struct timeval *value); unsigned long mktime (unsigned int year, unsigned int mon, unsigned int day, unsigned int hour, unsigned int min, unsigned int sec); |
当前实际时间记录在xtime (timespec结构)中。xtime.tv_sec以秒为单位,存放着自1970年7月1 日(纪元)以来经过的时间。内核提供current_kernel_time函数读取xtime,也可以使用do_gettimeofday函数获取timeval版本。
#include <linux/time.h> struct timespec current_kernel_time(void); void do_gettimeofday(struct timeval *tv); |
通常设备驱动程序为了让硬件能完成某些任务,需要将某些特定代码延迟一段时间后执行。我们把涉及多个时钟滴答的延迟称为长延迟,而涉及到几十个毫秒的延迟称为短延迟。实现长延时主要有三种途径:
Ø 忙等待
如果想把执行延迟若干个时钟滴答,或者对延迟的精度要求不高,最简单的实现方法就是一个监视jiffies计数器的循环。但是,这个忙等待循环会严重降低系统性能。
while (time_before(jiffies, j1)) cpu_relax( ); |
在非抢占内核上,这种方法会恰好延迟指定的时钟滴答。但是,在抢占式内核上,进程可能在其延迟过程中被中断,使得延迟长于指定的时钟滴答。在我的电脑上,读取/proc/jitbusy文件,结果如下:
pydeng@pydeng-laptop:~/ldd3_examples/misc-progs$ dd bs=20 count=5 < /proc/jitbusy 33547 33797 33799 34049 34051 34301 34303 34553 34556 34806 |
Ø 让出处理器
忙等待为系统整体增加了沉重的负担,因此,在不需要CPU时主动释放是一种改进办法。
while (time_before(jiffies, j1)) { schedule( ); } |
然而,当前进程虽然释放了CPU而不做任何事情,但它仍然在运行队列中。如果系统中只有它这么一个可运行的进程,空闲(idle)任务从来不会运行,仍然无法减轻处理器负荷。另外,实际的延迟可能要比所请求的长几个时钟滴答。因为,进程使用shedule释放处理器后,无法保证它可以在随后很快就得到处理器。
Ø 超时
最理想的延迟方式是,让进程在延迟时休眠,延迟到期后再次唤醒进程。
#include <linux/wait.h> long wait_event_timeout(wait_queue_head_t q, condition, long timeout); long wait_event_interruptible_timeout(wait_queue_head_t q, condition, long timeout); |
上述函数会在给定的等待队列上休眠,但是会在超时到期时返回。timeout值表示要等待的jiffies值。如果超时到期,这两个函数会返回0,而如果进程由其它事件唤醒,则会返回剩余的延迟时间,并用jiffies表达。
当没有需要等待的事件时,传入的condition为0,此时不需要其它进程执行wake_up。为了适应这种情况,内核提供了schedule_timeout函数,避免声明和使用多余的等待队列。该函数要求调用者首先设置当前进程的状态。正常的返回值是0,除非在给定超时值到期前函数返回,如响应某个信号。
#include <linux/sched.h> signed long schedule_timeout(signed long timeout);
/*典型调用代码*/ set_current_state(TASK_INTERRUPTIBLE); schedule_timeout (delay); |
完成短延时任务,可以使用ndelay、udelay和mdelay,它们分别延迟指定数量的纳秒、微秒和毫秒。三个函数均是忙等待函数,在延迟过程中无法运行其它任务。真正实现的延迟至少会达到所请求的时间值,但可能更长。
#include <linux/delay.h> void ndelay(unsigned long nsecs); void udelay(unsigned long usecs); void mdelay(unsigned long msecs); |
实现毫秒级(或者更长)延迟还有另一种方法,这种方法不涉及忙等待。
#include <linux/delay.h> void msleep(unsigned int millisecs); unsigned long msleep_interruptible(unsigned int millisecs); void ssleep(unsigned int seconds) |
msleep函数是不可中断的,进程将至少休眠指定的毫秒数。如果驱动程序正在某个等待队列上等待,而又希望有唤醒能够打断这个等待的话,则可使用msleep_interruptible。这个函数的返回值通常是0,如果进程被提前唤醒,那么返回值就是原先请求休眠时间的剩余毫秒数。对ssleep的调用将使进程进入不可中断的休眠,但休眠时间以秒计。
如果需要在将来的某个时间点调度执行某个动作,同时在该时间点到达之前不会阻塞当前进程,则可以使用内核定时器。一个内核定时器是一个数据结构,它告诉内核在用户定义的时间点使用用户定义的参数来执行一个用户定义的函数。
#include <linux/timer.h> /*定时器结构*/ struct timer_list { struct list_head entry; unsigned long expires; /*定时器到期jiffies*/ spinlock_t lock; void (*function)(unsigned long); /*定时器处理函数*/ unsigned long data; /*处理函数参数*/ struct tvec_t_base_s *base; }; /*运行时初始化定时器结构*/ void init_timer(struct timer_list *timer); /*编译时声明定时器结构*/ struct timer_list TIMER_INITIALIZER(_function, _expires, _data); /*激活定时器*/ void add_timer(struct timer_list * timer); /*停止定时器,如果定时器未激活返回0,否则返回1*/ int del_timer(struct timer_list * timer); /*更新定时器的到期时间*/ int mod_timer(struct timer_list *timer, unsigned long expires); /*停止定时器,确保返回时没有任何CPU在运行定时器处理函数*/ int del_timer_sync(struct timer_list *timer); /*返回定时器是否正在被调度运行*/ int timer_pending(const struct timer_list * timer); |
需要牢记定时器的几条重要特性:
Ø 定时器处理函数必须以原子的方式运行。
Ø 定时器处理函数没有进程上下文,无法与用户空间关联,不允许访问用户空间。
Ø 定时器处理函数和被中断的进程没有任何联系,current指针没有任何意义,也不可用。
Ø 在定时器处理函数中,不能执行休眠或调度。
Ø 即使在单处理器系统上,定时器也会是竞态的潜在来源。
Ø 定时器处理函数可以将自己注册以在稍后的时间重新运行。
中断处理流程中,除完成对硬件的即时响应外,允许稍后执行的部分称为下半部。tasklet和后面的工作队列以及共享队列,均是下半部的实现机制。tasklet与定时器类似:它始终在中断上下文运行,始终会在调度它们的同一CPU上运行,而且都接收一个unsigned long参数。不同之处在于,tasklet不要求在某个给定的时间执行。调度一个tasklet,只是希望内核选择某个其后的时间来执行给定的函数。tasklet以数据结构的形式存在,在使用前必须初始化。
#include <linux/interrupt.h>
struct tasklet_struct { struct tasklet_struct *next; unsigned long state; atomic_t count; void (*func)(unsigned long); unsigned long data; }; /*运行时初始化*/ void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data); /*静态声明,第二个宏禁用给定tasklet*/ DECLARE_TASKLET(name, func, data); DECLARE_TASKLET_DISABLED(name, func, data); /*禁用tasklet,如果tasklet正在运行,该函数会忙等待直到tasklet退出*/ void tasklet_disable(struct tasklet_struct *t); /*禁用tasklet,但不会等待正在运行的tasklet退出*/ void tasklet_disable_nosync(struct tasklet_struct *t); /*启用一个先前被禁用的tasklet,需要与每个禁用匹配*/ void tasklet_enable(struct tasklet_struct *t); /*调度执行指定的tasklet,如果tasklet在运行时被调度,就会在完成后再次运行*/ void tasklet_schedule(struct tasklet_struct *t); /*调度指定的tasklet以高优先级执行*/ void tasklet_hi_schedule(struct tasklet_struct *t); /*确保指定的tasklet不会被再次调度运行*/ void tasklet_kill(struct tasklet_struct *t); |
tasklet的特性如下:
Ø 一个tasklet可以在稍后被禁止或者重新启用,只有启用和禁止次数相同时,tasklet才会被执行。
Ø 与定时器相同,tasklet可以注册自己本身。
Ø tasklet可被调度以通常的优先级或者高优先级执行。
Ø 如果系统负荷不重,则tasklet会立即得到执行,但始终不会晚于下一个定时器滴答。
Ø 一个tasklet可以和其它tasklet并发,但对自身来讲是严格串行处理的,它会始终在调度自己的同一CPU上运行。
工作队列与tasklet的区别在于:
Ø tasklet在软件中断上下文运行,而工作队列在一个特殊内核进程的上下文运行。工作队列可以休眠,但不能访问用户空间,因为内核线程没有对应的用户空间可以访问。
Ø tasklet始终运行在被初始化提交的同一处理器上,但这只是工作队列的默认方式。
Ø 内核代码可以请求工作队列函数的执行延迟给定的时间间隔。
工作队列的使用步骤如下:
#include <linux/workqueue.h> /*首先,显示创建一个工作队列*/ struct workqueue_struct *create_workqueue(const char *name); struct workqueue_struct *create_singlethread_workqueue(const char *name); /*填充一个work_struct结构*/ DECLARE_WORK(name, void (*function)(void *), void *data); INIT_WORK(struct work_struct *work, void (*function)(void *), void *data); PREPARE_WORK(struct work_struct *work, void (*function)(void *), void *data); /*将工作提交到工作队列*/ int queue_work(struct workqueue_struct *queue, struct work_struct *work); int queue_delayed_work(struct workqueue_struct *queue, struct work_struct *work, unsigned long delay); /*取消某个工作队列入口项*/ int cancel_delayed_work(struct work_struct *work); void flush_workqueue(struct workqueue_struct *queue); /*释放工作队列*/ void destroy_workqueue(struct workqueue_struct *queue); |
如果只是偶尔需要向队列中提交任务,更有效的办法是使用内核提供的共享的默认工作队列。由于是共享队列,所以进程不应该长期独占该队列,即不能长时间休眠,而且可能需要更长的时间才能获得处理器时间。共享队列的用法与工作队列类似,不同之处在于:将工作提交到共享队列由函数schedule_work和schedule_delayed_work完成;而刷新共享队列则使用flush_scheduled_work,而不是flush_workqueue。
int schedule_work(struct work_struct *work); int schedule_delayed_work(struct work_struct *work, unsigned long delay); void flush_scheduled_work(void); |