这部分讲Linux内核定时器。
基本概念
系统定时器中断周期性执行的任务:
[======]
系统定时器(节拍率)通过静态预处理定义,系统启动时按HZ值对硬件进行设置。HZ值取决于体系结构。如i386体系结构,HZ值为1000(Hz),代表每秒钟产生1000次节拍
#include <asm/param.h> #define HZ 1000 /* 内核时钟频率 */
其他体系结构节拍率:
系统定时器使用高频率优缺点
优点:
缺点:
[======]
全局变量jiffies用来记录自系统启动以来产生的节拍总数。启动时,初值0;之后,每次时钟中断处理程序都会让jiffies+1。
jiffies定义:
#include <linux/jiffies.h> extern unsigned long volatile jiffies;
32bit体系结构上,jiffies是32bit,如果时钟频率100Hz,497天后会溢出;频率1000Hz,49.7天后溢出。
64bit体系结构上,几乎不可能会看到它溢出。
除了前面定义,jiffies还有第二个变量定义:
#include <linux/jiffies.h> extern u64 jiffies_64;
ld(1) 脚本用于连接主内核映像,然后用jiffies_64初值覆盖jiffies变量:
// x86, arch/i386/kernel/vmlinux.lds.S jiffies = jiffies_64;
也就是说,jiffies只取jiffies_64低32bit。因为大多数代码只使用jiffies存放流失的时间,二时间管理代码使用整个64bit的jiffies_64,以避免溢出。
在32bit体系结构上,jiffies 读取jiffies_64低32bit值;get_jiffies_64()读取jiffies_64整个64bit值。
周64bit体系结构上,jiffies 等价于get_jiffies_64(),和jiffies_64是同一个变量。
jiffies 溢出后,会绕回(wrap around)到0。内核提供4个宏函数,用于比较节拍计数,以避免回绕问题。
#include <linux/jiffies.h> // unknown是jiffies, known是需要对比的值 #define timer_after(unknown, known) ((long)(known) - (long)(unknown) < 0) #define timer_before(unknown, known) ((long)(unknown) - (long)(known) < 0) #define timer_after_eq(unknown, known) ((long)(unknown) - (long)(known) >= 0) #define timer_after(unknown, known) ((long)(known) - (long)(unknown) >= 0)
Linux内核2.6以前,如果改变内核中HZ值,会给用户空间中某些程序造成异常结果,因为应用程序已经依赖这个特定HZ值。
要避免上面错误,内核需要更改所有导出的jiffies值。因此,内核定义USER_HZ代表用户空间看到的HZ值。
例如,x86体系结构上,HZ值原来一直是100,因此USER_HZ值定义为100。
内核使用宏jiffies_to_clock_t() 将一个由HZ表示的节拍计数转换成一个由USER_HZ表示的节拍数。
当USER_HZ是HZ的整数倍时,
#define jiffies_to_clock_t(x) ((x) / (HZ/USER_HZ))
另外,jiffies_64_to_clock_t()将64位jiffies值单位从HZ转换为USER_HZ。
[======]
体系结构提供3种硬时钟用于计时:实时时钟,时间戳计数,可编程中断定时器。
实时时钟 RTC
RTC是用来持久存放系统时间的设备,即使PC关掉电源,RTC还能依靠主板电池继续计时。
主要作用:
1)系统启动时,内核通过读取RTC来初始化墙上时间,该时间存放在xtime变量中。
2)Linux只用RTC来获得当前时间和日期。
时间戳计数 TSC
x86包含一个64位的时间戳计数器(寄存器),对每个时钟信号进行计数。例如,如果时钟节拍400MHz,那么TSC每2.5ns计数+1。而时钟信号频率没有在预编译时指定,必须在Linux初始化时确定。通过calibrate_tsc(),在系统初始化阶段完成时钟信号频率计算。
可编程中断定时器 PIT
x86体系结构中,主要采用可编程中断时钟(PIT)作为系统定时器。
Linux中,若PIT以100Hz频率向IRQ0发出定时中断,即每10ms产生1次定时中断。这个10ms间隔,就是一个节拍(tick),以微妙为单位存放在tick变量。
TSC与PIT相比,拥有更高的精度。PIT针对编写软件而言,更加灵活。
[======]
时钟中断处理程序可划分2个部分:体系结构相关部分,体系结构无关部分。
与体系结构相关的例程作为系统定时器(PIT)的中断处理程序而注册到内核,以便产生时钟中断时能运行。
处理程序主要执行以下工作:
中断服务程序主要通过调用与体系结构无关的do_timer()执行工作:
do_timer()看起来像:
void do_timer(struct pt_regs* regs) { jiffies_64++; update_process_times(user_mode(regs)); // 对用户或系统进行时间更新 update_times(); // 更新墙上时钟 }
user_mode()宏查询处理器寄存器regs的状态。如果时钟中断发生在用户空间,它返回1;如果发生在内核,则返回0。update_process_times()函数根据时钟中断产生的位置(用户态 or 内核态),对用户或对系统进行相应的时间更新。
void update_process_times(int user_tick) { struct task_struct *p = current; int cpu = smp_processor_id(); int system = user_tick ^ 1; // user_tick和system只会有一个变量为1,另一个必为0 update_one_process(p, user_tick, system, cpu); // 更新进程时间 run_local_timers(); // 标记一个软中断处理所有到期的定时器 scheduler_tick(user_tick, system); // 负责减少当前运行进程的时间片计数值,并在需要时设置need_resched标志 }
update_one_process() 通过判断分支,将user_tick和system加到进程相应的计数上:
/* 更新恰当的时间计数器,给其加一个jiffy */ p->utime += user; p->stime += system; update_times()负责更新墙上时钟: void update_times(void) { unsigned long ticks; // 记录最近一次更新后新产生的节拍数 ticks = jiffies - wall_jiffies; if (ticks) { wall_jiffies += ticks; update_wall_time(ticks); // 更新存储墙上时间的xtime } last_time_offset = 0; calc_load(ticks); // 更新载入平均值 }
ticks记录最近一次更新后新产生的节拍数。通常,ticks应为1,但时钟中断可能丢失,导致节拍丢失。中断长时间被禁止时,就会出现这种情况(虽然很可能是bug)。
cal_load(0更新载入平均值,到此,update_times()执行完毕。do_timer()亦执行完毕并返回与体系结构相关的中断处理程序,继续执行后面的工作,释放xtime_lock锁,然后退出。
墙上时间定义在kernel/timer.c中
struct timespec xtime;
timespec结构定义:
#include <linux/time.h> struct timespec { time_t tv_sec; /* 秒 */ long tv_nsec; /* 纳秒 */ };
xtime.tv_sec 存放着自1970年7月1日(UTC)以来经过的时间。1970年7月1日被称为纪元,Unix墙上时间都是基于该纪元的。
xtime.ntv_sec记录着自上一秒开始经过的纳秒数。
读写xtime变量需要用xtime_lock锁,这是一个seqlock锁。
更新xtime:
write_seqlock(&xtime_lock); /* 更新xtime... */ write_sequnlock(&time_lock);
读取xtime:
/* 循环更新xtime, 直到确认循环期间没有时钟中断处理程序更新xtime */ do { unsigned long lost; seq = read_seqbegin(&xtime_lock); usec = timer->get_offset(); lost = jiffies->wall_jiffies; if (lost) usec += lost * (1000000/HZ); sec = xtime.tv_sec; usec += (xtime.tv_nsec/1000); } while(read_seqretry(&xtime_lock, seq));
如果循环期间有时钟中断处理程序更新xtime,read_seqretry()会返回无效序列号,继续循环等待。
从用户空间取得墙上时间的主要接口:gettimeofday(),内核中对应系统调用sys_gettimeofday():
asmlinkage long sys_gettimeofday(struct timeval* tv, struct timezone* tz) { if (likely(tv)) { // <=> if (tv) struct timeval ktv; do_gettimeofday(&ktv); // 循环读取xtime操作 } if (copy_to_user(tv, &ktv, sizeof(ktv))) // 在给用户空间拷贝墙上时间或时区 return -EFAULT; // 拷贝时发生错误 if (unlikely(tz)) { // <=> if (!tz) if (copy_to_user(tz, &sys_tz, sizeof(sys_tz))) return -EFAULT; } return 0; } /* 宏likely和unlikey在内核中定义, 便于编译器优化, 以提升性能 */ #define likely(x) __builtin_expect(!!(x), 1) #define unlikely(x) __builtin_expect(!!(x), 0)
内核也实现了time(), ftime()系统调用,但都被gettimeofday()所取代。为保持向后兼容,Linux还保留着。
[======]
定时器被称为动态定时器或内核定时器,是管理内核时间的基础。
定时器使用
思路:先进行一些初始化工作,设置一个超时时间,指定超时后执行的函数,然后激活定时器。指定的函数将在定时器到期时自动执行。
定时器不会周期运行,超时后自行销毁。这是这种定时器被称为动态定时器的一个原因。因此,动态定时器是在不断的创建和销毁,而且运行次数不受限制。在内核中应用非常普遍。
定时器由结构timer_list表示,定义于<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; /* 定时器内部值, 用户不要使用 */ };
使用定时器不用深入了解timer_list结构。内核提供一组接口简化管理定时器的操作。
1)定义定时器
struct timer_list my_timer;
2)初始化定时器
init_timer(&my_timer);
3)填充定时器结构中需要的值
my_timer.expires = jiffies + delay; /* 定时器超时节拍数 */ my_timer.data = 0; /* 给定时器处理函数传入值0 */ my_timer.function = my_function; /* 定时器超时调用的处理函数 */
超时处理函数必须是这种原型:
void my_timer_function(unsigned long data);
4)激活定时器
add_timer(&my_timer);
定时器工作条件:当前节拍计数jiffies >= my_timer.expires
定时器会在超时后马上执行,但也可能推迟到下一个时钟节拍,因此不能用于硬实时任务。
5)修改定时器
改变超时时间
mod_timer(&my_timer, jiffies + new_delay); /* new expiration */
mod_timer可用于已经初始化但未激活的定时器;如果定时器未被激活,mod_timer会激活之。
如果调用时,定时器未被激活,函数返回0;否则,返回1.
6)删除定时器
在定时器超时前定制定时器
del_timer(&my_timer);
激活或未被激活的定时器都可以用该函数,如果未被激活,函数返回0;否则,返回1。
已超时的定时器不需要调用该函数,因为会自动被删除。
del_timer只能保证定时器将来不会被激活,不保证当前在其他处理器上已运行时会停止。此时,需要用del_timer_sync,等待其他处理器上运行的超时处理函数退出。
del_timer_sync(&my_timer); /* 如果有并发访问可能性, 推荐优先使用 */ del_timer_sync() 不能在中断上下文中使用,因为会阻塞。
定时器与当前执行(设置定时器的)代码是异步的,因此可能存在潜在竞争条件。因此,不能用如下方式替代mod_timer(),来改变定时器的超时时间,因为在多处理器上是不安全的:
/* 用下面代码替换mod_timer, 修改定时器超时时间是错误的 */ del_timer(&my_timer); my_timer->expires = jiffies + new_delay; add_timer(&my_timer);
通常,用过用del_timer_sync() 取代del_timer()删除定时器,避免并发访问的问题,因为无法确定删除定时器的时候,它是否在其他处理器上运行。
定时器作为软中断在下半部上下文中执行。
时钟中断处理程序会执行update_process_timers(),该函数会随即调用run_local_timers()。
void run_local_timers(void) { raise_softirq(TIMER_SOFTIRQ); }
run_timer_softirq()处理软件中断TIMER_SOFTIRQ,从而在当前处理器上运行所有的超时定时器。
内核定时器是以链表形式存放,但并没有遍历链表以寻找超时定时器,也没有在链表中插入和删除定时器。
而是,将定时器按超时时间分为五组。当定时器超时时间接近时,定时器将随组一起下移。采用分组定时器的方法可以在执行软中断的多数情况下,可以确保内核尽可能减少搜索超时定时器所带来的负担。
[======]
内核代码(尤其驱动程序)除了用定时器或下半部机制外,还需要其他方法来推迟执行任务。
常适用于:短时间等待硬件完成某些工作,比如,重新设计网卡的以太网模式(2ms)。
内核提供多种延迟方法处理各种延迟要求:
1)忙等待
2)短延迟
3)schedule_timeout()
4)设置超时时间,在等待队列上睡眠
忙等待(或称忙循环),是最简单的延迟方法,也是最不理想的。
方法仅适用于想要延迟的时间是节拍的整数倍,或者精确度要求不高时使用。
忙循环使用示例:在循环中不断旋转直到希望的时钟节拍数耗尽
unsigned long delay = jiffies + 10; /* 10个节拍 */ while (time_before(jiffies, delay)) /* CPU循环等待 jiffies > delay (自动处理定时器值回绕) */ ;
上面循环不断旋转,等待10个节拍。HZ值为1000的x86体系结构上,每个节拍1ms,10个节拍总共耗时10ms。
unsigned long delay = jiffies + 2 * HZ; /* 2秒 */ while (time_before(jiffies, delay)) ;
上面循环自旋时,并不会放弃CPU。下面cond_resched()将调度一个新程序投入运行,不过只有在设置完need_resched标志后,才能生效。因为cond_resched方法会调用调度程序,因此不能在中断上下文中使用,而只能在进程上下文中使用。
unsigned long delay = jiffies + 5 * HZ; while (time_before(jiffies, delay)) cond_resched(); /* 调度一个新程序投入运行 */
注意:
1)所有延迟方法都只能在进程上下文使用,不能在中断上下文使用。因为中处理程序应尽快执行。
2)延迟执行 不应在持有锁或者禁止中断的时候发生。
有时驱动程序不但需要很短的延迟(比时钟节拍typ.为1ms还短),而且要求延迟的时间很精确。不可能使用精度为1ms的jiffies节拍用于延迟。
此时,可以用内核提供的另外2个函数,用于处理微妙和毫秒级延迟。
头文件:<linux/delay.h>
void udelay(unsigned long usecs); void mdelay(unsigned long msecs);
mdelay是通过udelay实现的。
如,延迟150微秒,延迟200毫秒
udelay(150); /* 延迟150us */ mdelay(200); /* 延迟200ms */
注意:
1)延迟超过1ms时,不要用udelay,应该用mdelay。
2)能不用则不用mdelay,尽量少用。
3)不要在持有锁或者禁止中断时,使用忙等待,因为类似于忙等待,会让系统响应速度和性能大打折扣。
该方法会让需要延迟执行的任务睡眠到指定的延迟时间耗尽后,再重新运行。不能保证睡眠时间刚好等于指定的延迟时间,只能是尽量接近。当指定时间到期后,内核唤醒被延迟的任务并将其重新放回运行队列。
典型用法:
/* 将任务设置为可中断睡眠状态 */ set_current_state(TASK_INTERRUPTIBLE); unsigned long S = 10; /* 小睡一会儿,S秒后唤醒 */ schedule_timeout(s * HZ);
唯一的参数是延迟的相对时间,单位jiffies。
如果睡眠时,想接收信号,可将任务状态设置为TASK_INTERRUPTIBLE;如果不想,可以将任务状态设置为TASK_UNINTERRUPTIBLE。
注意:调用schedule_timeout()前,必须将任务设置为上面两种状态之一,否则任务不会睡眠。
schedule_timeout的简单实现:
signed long schedule_timeout(singed long timeout) { timer_t timer; unsigned long expire; switch(timeout) { /* 处理特殊情况 */ case MAX_SCHEDULE_TIMEOUT: /* 无限期睡眠 */ schedule(); /* 调度进程: 从就绪队列中选一个优先级最高的进程来替代当前进程运行 */ goto out; default: if (timeout < 0) { printk(KERN_ERR"schedule_timeout: wrong timeout value %lx from %p\n", timeout, __builtin_return_address(0)); goto out; } } expire = timeout + jiffies; init_timer(&timer); /* 初始化动态定时器 */ timer.expires = expire; timer.data = (unsigned long)current; timer.funtion = process_timeout; add_timer(&timer); /* 激活定时器 */ schedule(); del_timer_sync(&timer); /* 同步删除定时器 */ timeout = expire - jiffies; out: return timeout < 0 ? 0 : timeout; } /* 定时器超时处理函数 */ void process_timeout(unsigned long data) { wake_up_progress((task_t *)data); /* 唤醒进程, 将任务设置为TASK_RUNNING */ }
因为任务被标识为TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE(在调用schedule_timeout之前),所以调度程序不会再选择该任务投入运行,而会选择其他新任务运行。
进程上下文中为了等待特定事件发生,会将自己放入等待队列,然后调用调度程序执行新任务。一旦事件发生,内核可调用wake_up()唤醒在睡眠队列上的任务,使其重新投入运行。
schedule_timeout用在什么地方?
当等待队列上的某个任务可能既在等待一个特定事件到来,又在等待一个特定时间到期,看谁先来。此时,可以用schedule_timeout替换schedule(),因为schedule()只是简单的阻塞等待唤醒事件,而schedule_timeout除了可以等待IO事件,还会等待超时。
[======]
1)讲述了时间的基本概念,如墙上时间,时钟中断,时钟节拍,HZ,jiffies等。
2)定时器的实现,应用方法等。
3)开发者用于延迟的方法:忙等待、短延迟、schedule_timeout。
[======]
[1]RobertLove, 洛夫, 陈莉君,等. Linux内核设计与实现[M]. 机械工业出版社, 2006.