Linux定时器和时间管理
这部分讲Linux内核定时器。
基本概念
- 系统定时器:一种可编程硬件芯片,能以固定频率产生中断。
- 定时器中断:系统定时器固定时间周期产生的中断,其中断处理程序负责更新系统时间,执行周期性任务。
- 动态定时器:一种用来推迟执行程序的工具。内核可以动态创建、销毁动态定时器。
- 节拍率(tick rate):系统定时器频率,系统定时器以某种频率自行触发(又称为击中(hitting)或射中(popping))时钟中断,该频率可以通过编程设定。
- 节拍(tick):预编的节拍率对内核来说是可知的,因此内核知道连续2次时钟中断间隔时间。这个时间称为节拍。节拍 = 1/ 节拍率。常用来计算墙上时间和系统运行时间。
- 墙上时间(walk clock time):实际时间,对用户空间的应用程序来说很重要。代表从进程开始运行到结束,系统时钟走过的时间(时钟数),包含了进程阻塞的时间。每秒滴答数(节拍率)可通过sysconf(_SC_CLK_TCK)获取。
- 系统运行时间:自系统启动开始所经过的时间,对用户空间和内核都很有用。墙上时间 = 阻塞时间 + 就绪时间 + 运行时间,运行时间 = 用户CPU实际 + 系统CPU时间。
系统定时器中断周期性执行的任务:
- 更新系统时间。
- 更新实际时间。
- 在smp系统上,均衡调度程序中各处理器上的运行队列。如果运行队列负载不均衡的话,尽量使它们均衡。
- 检查当前进程是否用尽了自己的时间片。如果用尽,就重新调度。
- 运行超时的动态定时器。
- 更新资源消耗和处理器时间的统计值。
[======]
节拍率:HZ
系统定时器(节拍率)通过静态预处理定义,系统启动时按HZ值对硬件进行设置。HZ值取决于体系结构。如i386体系结构,HZ值为1000(Hz),代表每秒钟产生1000次节拍
#include <asm/param.h>
#define HZ 1000 /* 内核时钟频率 */
其他体系结构节拍率:
系统定时器使用高频率优缺点
优点:
- 内核定时器能以更高频度和准确度运行。
- 依赖定时值执行的系统调用,如select,poll,能以更高精度运行。
- 对诸如资源消耗和系统运行时间等的测量会有精细的解析度。
- 提高进程抢占的准确度。
缺点:
- 节拍率越高,系统时钟中断频率越高,意味着系统负担越重,即中断处理处理程序占用的处理器时间越多,减少了处理其他工作的时间。
[======]
jiffies
全局变量jiffies用来记录自系统启动以来产生的节拍总数。启动时,初值0;之后,每次时钟中断处理程序都会让jiffies+1。
jiffies定义:
#include <linux/jiffies.h>
extern unsigned long volatile jiffies;
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回绕
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)
用户空间和HZ
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)的中断处理程序而注册到内核,以便产生时钟中断时能运行。
处理程序主要执行以下工作:
- 获得xtime_lock锁,对访问jiffies_64和墙上时间进行保护。
- 需要时应答或重新设置系统时钟。
- 周期性地使用墙上时间更新实时时钟。
- 调用体系结构无关的时钟例程:do_timer()。
中断服务程序主要通过调用与体系结构无关的do_timer()执行工作:
- 给jiffies_64 + 1。
- 更新资源消耗的统计值,如当前进程所消耗的系统时间和用户时间。
- 执行已经到期的动态定时器。
- 更新墙上时间,该时间存放在xtime变量中。
- 计算平均负载值。
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关系
内核也实现了time(), ftime()系统调用,但都被gettimeofday()所取代。为保持向后兼容,Linux还保留着。
- time() 返回从1970年1月1日午夜开始所走过的秒数。
- ftime() 返回一个类型为timeb的数据结构,该结构包含从1970年1月1日午夜开始所走过的秒数;在最后1秒内所走过的毫秒数;时区以及夏令时当前的状态。
- gettimeofday() 返回的值存放在2个数据结构timeval和timezone,其中包含的信息与ftime相同。
[======]
定时器
定时器被称为动态定时器或内核定时器,是管理内核时间的基础。
定时器使用
思路:先进行一些初始化工作,设置一个超时时间,指定超时后执行的函数,然后激活定时器。指定的函数将在定时器到期时自动执行。
定时器不会周期运行,超时后自行销毁。这是这种定时器被称为动态定时器的一个原因。因此,动态定时器是在不断的创建和销毁,而且运行次数不受限制。在内核中应用非常普遍。
定时器由结构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)不要在持有锁或者禁止中断时,使用忙等待,因为类似于忙等待,会让系统响应速度和性能大打折扣。
schedule_timeout() 睡眠到指定延迟时间
该方法会让需要延迟执行的任务睡眠到指定的延迟时间耗尽后,再重新运行。不能保证睡眠时间刚好等于指定的延迟时间,只能是尽量接近。当指定时间到期后,内核唤醒被延迟的任务并将其重新放回运行队列。
典型用法:
/* 将任务设置为可中断睡眠状态 */
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.