第11章 定时器和时间管理
相对于事件驱动而言,内核中有大量的函数基于时间驱动的。有些函数周期执行,对调度程序中的运行队列进行平衡调整或对屏幕进行刷新。有些函数需要等待一个相对函数后才运行。
周期性产生的事件都是由系统定时器驱动的,系统定时器是一个硬件,以固定频率产生中断。
动态定时器,一种用来推迟执行程序的工具。
内核中的时间概念
内核必须在硬件的帮助下才能计算和管理时间。
硬件为内核提供了一个系统定时器,可看成电子时钟资源,比如数字时钟或处理器频率。
系统定时器以某种频率自行触发(击中(hitting)或者射中(poping))时钟中断。该频率可以通过编程预定,称作节拍率(tick rate)。
节拍率对内核可知,两次时钟中断的间隔时间是可知的,称为节拍(tick),等于节拍率分之一秒。
大量内核的函数都离不开时间的控制,如更新系统时间、更新实际时间、均衡运动队列,有些工作在每次时钟中断都要去处理,有些工作是n次时钟中断才执行一次。
节拍率:HZ
静态宏预定义的<asm/param.h>,在系统启动的时候按照HZ对硬件进行设置。
HZ不是一个固定不变的值,大多数体系结构都是可调的。
理想的HZ值
高HZ意味着时钟中断产生的更加频繁,可以提高时钟驱动事件的解析度(resolution),提高时间驱动事件的准确度(accuracy)。
高HZ的优势
- 内核定时器更高的频度和更高的准确度。
- 依赖定时器的系统调用,如poll()和select(),能够以更高的精度运行。
- 对资源消耗和运行时间等的测量有更精细的解析度。
- 提高进程抢占的准确度。
高HZ的劣势
中断频率越高,意味着系统负但越重。
内核支持无节拍的操作,CONFIG_HZ配置选项,系统会动态调度时钟中断, 如果一个时钟频率被设置为3ms,就没3ms触发一次中断,如果接下来的50ms内无事可做,就50ms重新调度时钟中断,减少了系统的能耗,省电。
jiffies
全局变量jiffies用来记录自系统起来以后产生的节拍的总数。
内核将该变量初始化为0,每次时钟中断都会增加该变量的值。实际情况稍微复杂些:内核给jiffies赋一个特殊的值,引起这个变量的不断溢出,由此捕捉bug。当找到实际的jiffies值后就把这个偏差减去。
/* <linux/jiffies.h> */
extern unsigned long volatile jiffies;
jiffies的内部表示
jiffies变量总是无符号长整数(unsigned long),因此在32位上是32位,在64位上是64位。
32位的jiffies变量,在时钟频率为100HZ的情况下,497天后会溢出。64位的jiffies变量溢出时间是32位的232倍!
由于性能和历史的原因,考虑到与现有代码的兼容性,内核开发者依然希望jiffes为unsigned long。
第二个变量也定义在<linux/jiffies.h>中:
extern u64 jiffies_64;
ld(1)脚本用于链接主内核镜像(在x86上位与arch/x86/kernel/vmlinux.lds.S),然后用jiffies_64变量的初值覆盖jiffies变量:
jiffies = jiffies_64;
代码可以完全像以前一样继续访问jiffies,大多数代码只关心时间的流失,32位已经足够。时间管理代码仍然使用整个64位。
jiffies的回绕
jiffies在达到最大值后溢出,回绕(wrap around)到0。
内核提供了宏来帮助比较节拍计数。
/* <linux/jiffies.h> */
#define time_after(unknown,known) ((long)(known)-(long)(unknown)<0))
#define time_before(unknown,known) ((long)(unknown)-(long)(unknown)<0)
#define time_after_eq(unknown,known) ((long)(unknown)-(long)(unknown)>=0)
#define time_before_eq(unknown,known) ((long)(known)-(long)(unknown)>=0))
用户空间和HZ
USER_HZ来代表内核空间看到的HZ值。
内核可以使用函数jiffies_to_clock_t()(定义在kernel/time.c中),将一个由HZ表示的节拍数转换成一个由USER_HZ表示的节拍计数。
硬时钟和定时器
实时时钟
实时时钟(RTC)是持久用来存放系统时间的设备。
当系统启动后,内核读取RTC初始化时间,初始化xtime变量。
系统定时器
根本思想,提供一种周期性触发中断机制。
时钟中断处理程序
可以划分两个部分,体系结构相关部分和体系结构无关部分。
最低限度的工作:
- 获得xtime_lock锁,访问jiffies_64和保护xtime。
- 需要时应答和设置系统时钟。
- 周期性使用墙上时间更新实时时钟。
- 调用和体系无关的时钟例程,tick_periodic()。
和体系结构无关工作tick_periodic():
- jiffies_64 + 1
- 更新资源消耗的统计值,如当前进程所消耗的系统时间和用户时间。
- 执行sheduler_tick()函数。
- 更新墙上时间,存放在xtime中。
- 计算平均负载值。
/* 和体系无关的执行函数 */
static void tick_periodic(int cpu)
{
if(tick_do_timer_cpu == cpu)
{
write_seqlock(&xtime_lock);
/* 记录下一个节拍事件 */
tick_next_period = ktime_add(tick_next_period,tick_period);
do_timer(1);
write_sequnlock(&xtime_lock);
}
update_process_times(user_mode(get_irq_regs()));
prifile_tick(CPU_PROFILING);
}
/* 增加jiffies_64 */
void do_timer(unsigned long ticks)
{
jiffies_64 += ticks;
update_wall_time(); /* 根据流逝的时间更新墙上时钟 */
calc_global_load(); /* 更新系统的平均负载统计值 */
}
/* 更新所耗费的各种节拍数,通过user_tick区别花费在用户空间还是在内核空间 */
void update_process_times(int user_tick)
{
struct task_struct *p=current;
int cpu = smp_processor_id();
account_process_tick(p,user_tick);
run_local_timers();
rcu_check_callbacks(cpu,user_tick);
printk_tick();
scheduler_tick();
run_posix_cpu_timers(p);
}
/* 对进程的时间进行实质性更新
计数时根据中断发生时所处的模式进行分类统计的,把上一个节拍全部算给了进程,不是很准确
但也是没有办法的事情,所以要采用更高频率
*/
void account_process_tick(struct task_struct *p, int user_tick)
{
cputime_t one_jiffy_scaled = cputime_to_scaled(cputime_one_jiffy);
struct rq *rq = this_rq();
if(user_tick)
account_user_time(p,cputime_one_jiffy,one_jiffy_scaled);
else if((p!= rq->idle)||(irq_count()!=HAEDIRQ_OFFSET))
account_system_time(p,HARDIRQ_OFFSET,cputime_one_jiffy,one_jiffy_scaled);
else
account_idle_time(cputime_one_jiffy);
}
run_lock_timers()标记一个软中断去处理所有到期的定时器。
scheduler_tick()负责减少当前进程的时间片计数值,并在需要时设置need_resched标志,对于SMP,还要平衡运行队列。
实际时间
当前实际时间(墙上时间),定义在kernel/time/timekeeping.c中:
struct timespec xtime;
/*
xtime.tv_sec存放自1970年1月1日(UTC,纪元)以来的时间。
xtime.tvnsec记录自上一秒开始的ns数。
*/
读写xtime变量需要使用xtime_lock锁,该锁是一个seqlock。
更新要申请锁:
write_seqlock(&xtime_lock);
/* update xtime... */
write_sequnlock(&xtime_lock);
读取时也要使用read_seqbegin()和read_seqretry()函数:
/* 确保读取过程中没有写数据介入 */
unsigned long seq;
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));
定时器
定时器使用步骤,初始化,设置一个超时时间,指定超时发生后执行的函数,然后激活定时器就可以了。定时器并不周期运行,在超时后就自行撤销。
使用定时器
内核定时器由结构timer_list表示,定义在<linux/timer.h>中。
内核提供的接口位于<linux/timer.h>中,大多数接口在文件kernel/timer.c中实现。
- 定义
struct timer_list my_timer;
- 初始化
init.timer(&my_timer);
my_timer.expires = jiffies+delay; /*超时时间,节拍为单位*/
my_timer.data=0; /* */
my_timer.function=my_function; /* void my_timer_function(unsigned long data) */
- 激活
add_timer(&my_timer);
内核保证不会在超时时间前执行处理函数,但是有可能延误定时器的执行,不能用定时器来实现任何硬实时任务。
4. 修改
mod_timer(&my_timer,jiffies+new_delay); /* 新的定时值,也会激活未激活的定时器 */
- 超时之前删除(已经超时的会自动删除)
del_timer(&my_timer);
存在竞争条件,定义器中断可能已经触发,要等待其他处理器上的 定时器处理程序退出。
del_timer_sync(&my_timer); /* 不能在中断上下文中使用 */
定时器竞争条件
定时器于当前执行的代码是异步的,可能存在竞争条件。应该重点保护定时器中断处理程序中的共享数据。
实现定时器
内核在时钟中断发生后执行定时器,定时器作为软中断在下半部上下文中执行。
内核采用分组定时器加快定时器的搜索。
延迟执行
除了定时器和下半部机制以外,还有其他方式来推迟任务。通常是等待硬件完成某些工作时,比如重新设置网卡的以太模式需要花费2ms。
忙等待
最简单的是忙等待(通常最不理想的办法),延时节拍的整数倍或者精确率要求不高时使用。
更好的方法应该是应该在代码等待时,允许内核重新调度执行其他任务(cond_resched())。因为需要调度程序,不能在中断上下文中使用,只能在进程上下文中使用。
短延迟
延迟短暂,时间精确。
<linux/delay.h>
<asm/delay.h>
void udelay(unsigned long usecs);
void ndelay(unsigned long nsecs);
void mdelay(unsigned long msecs);
使用忙循环实现,延时精确,主要用在延时小的地方。
schedule_timeout()
更理想的延迟执行方法是让需要延迟执行的任务睡眠到指定的延迟时间耗尽后再重新运行。当指定的时间到期后,内核唤醒被延迟的任务并将其重新放回任务队列。
/* 将任务设置为可中断睡眠状态 */
set_current_state(TASK_INTERRUPTIBLE);
/* 小睡一会,“s”秒后唤醒 */
schedule_timeout(s*HZ);
因为任务处于可中断状态,任务收到信号将被唤醒。如果睡眠任务不想被唤醒,可以设置为TASK_UNINTERRUPTIBLE。调用schedule_timeout之前必须设置为这两个状态,否则不会睡眠。
schedule_timeout需要重新调度程序,所以调用他的代码必须保证能够睡眠。简而言之,调用代码必须处于进程上下文中,且不能持有锁。文章来源:https://www.toymoban.com/news/detail-508734.html
- schedule_timeout的实现
用法相当简单,是内核定时器的一个简单应用。
signed long schedule_timeout(signed long timeout)
{
timer_list tiemr;
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));
current->state = TASK_RUNING;
goto_out
}
}
expire = timeout + jiffies;
init_timer(&timer);
timer.expires=expire;
timer.data=(unsigned long)current; /* 这里的data用来唤醒 */
tiemr.function=process_timeout;
add_timer(&timer);
schedule();
del_timer_sync(&timer);
timeout=expire - jiffies;
out:
return timeout<0?0:timeout;
当定时器超时时,process_timeout()函数会被调用,该函数将任务设置为TASK_RUNING状态,然后将其放入运行队列:文章来源地址https://www.toymoban.com/news/detail-508734.html
void process_timeout(unsigned long data)
{
wake_up_process((task_t*)data);
}
- 设置超时时间,在等待队列上睡眠。
到了这里,关于第11章 定时器和时间管理的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!