十一、定时器和时间管理

11.1 内核中时间的概念

硬件为内核提供了一个系统定时器用以计算流逝的时间,该时钟在内核中可以看成是一个电子时间资源。系统定时器以某种频率自行触发时钟中断,该频率可以通过编程指定,称作节拍率。因为预编的节拍率对内核来说是可知的,所以内核知道连续两次时钟中断的间隔时间。这个间隔时间就称为节拍,等于节拍率分之一秒。

利用时钟中断处理的工作:更新系统运行时间、更新实际时间、均衡调度各处理器上的运行队列、检查当前进程是否用尽了时间片、运行超时的动态定时器、更新资源消耗和处理器时间的统计值。

11.2 节拍率:HZ

系统定时器频率(节拍率)是通过静态预处理定义的,也就是HZ,在系统启动时按照HZ的值对硬件进行设置。体系结构不同,HZ的值也不同,对某些体系结构来说,甚至是机器不同,它的值都会不同。

x86体系结构中,系统定时器默认频率100.因此,x86上时钟中断的频率就为100HZ,在i386处理上的每秒时钟中断100次。编写内核代码时,不要认为HZ值是固定不变的,大多数体系结构的节拍率是可调的。

11.2.1 理想的HZ值

11.2.2 高HZ的优势

1、内核定时器能够以更高的频度和更高的准确度运行

2、依赖定时值执行的系统调用,比如poll()和select(),能够以更高的精度运行。

3、对诸如资源消耗和系统运行时间等的测量会有更精细的解析度。

4、提高进程抢占的准确度。

11.2.3 高HZ的劣势

节拍率越高,也就意味着系统的负担更重。因为处理器必须花时间来执行中断处理程序。这就减少了处理器处理其他任务的时间,而且会更频繁的打乱处理器高速缓存并增加耗电。

无节拍:linux支持无节拍操作。当编译内核时设置了CONFIG_HZ配置选项,系统就根据这个选项动态调度时钟中断。并不是每隔固定的时间间隔触发时钟中断,而是按需动态调度和重新设置。如果下一个时钟频率设置为3ms,就没3ms触发一次时钟中断。之后如果50s内无事,内核以50ms重新调度时钟中断。

11.3 jiffies

全局变量jiffies用来记录自系统启动后产生的节拍总数。启动时,内核将该变量初始化为0,此后,每次时钟中断处理程序就会增加该变量的值。

(seconds*HZ)将以秒为单位的时间转换为jiffies

(jiffies/HZ) 将jiffies转换为以秒为单位的时间

11.3.1 jiffies的内部表示

jiffies变量总是无符号长整型,因此,在32位体系结构上是32位,在64位体系结构上是64位。32位jiffies,在时钟频率为100HZ,的情况下497天会溢出。如果频率为1000HZ则在49.7天会溢出。64位则有生之年不会溢出。

访问jiffies的代码会读jiffies_64的低32位。get_jiffies_64()函数,既可以读取整个64位的值。在64位体系结构上,jiffies和jiffies_64()指向同一个变量,jiffies和get_jiffies_64()作用相同。

11.3.2 jiffies的回绕

和任何c整型一样,当jiffies变量的值超过他的最大存放范围后就会溢出。内核提供了四个宏来帮助比较节拍计数,他们能正确处理节拍计数回绕的情况。

#define time_after(unkonw,konw);

#define time_before(unkonw,know);

#define time_after_eq(unkonw,know);

#define time_befor_eq(unkonw,know);

11.3.3 用户空间和HZ

在2.6以前的内核中,如果改变内核中的HZ的值,会给用户空间中某些程序造成异常结果。这是因为内核是以节拍数/秒的形式给用户空间导出这个值,在这个接口稳定了很长一段时间后,应用程序编逐渐依赖于这个特定的HZ值了。所以如果在内核中更改了HZ的值,就打破了用户空间的常量关系——用户空间并不知道新的HZ值。

内核定义了USER_HZ来代表用户空间看到的HZ的值。内核可以使用jiffies_to_clock_t()将一个由HZ表示的节拍计数转换成一个由USER_HZ表示的节拍计数。内核使用jiffies_64_to_clock_t()将64位的jiffies值从HZ转换为USER_HZ。

11.4 硬时钟和定时器

11.4.1 实时时钟

实时时钟是用来持久存放系统时间的设备,即便系统关闭后,它也可以靠主板上的微型电池提供的电力保持系统的计时。当系统启动时,内核通过读取RTC来初始化墙上时间,该时间存放在xtime变量中。

11.4.2 系统定时器

系统定时器的提供一种周期性触发中断机制。有些体系结构式通过对电子晶振进行分频来实现系统定时器,还有些则提供了一个衰减测量器——衰减测量器设置一个初始值,该值以固定的频率递减,当减到0时,触发一个中断。

11.5 时钟中断处理程序

绝大多数处理程序要做如下工作:

1、获得xtime_lock锁,以便对访问jiffies_64和墙上时间xtime进行保护

2、需要时应答或重新设置时钟

3、周期性的使用墙上时间更新实时时钟

4、调用与体系结构无关的时钟例程:tick_periodic()。

tick_periodic()执行下面工作:

1、给jiffies_64变量加1

2、更新资源消耗的统计值,比如当前进程所消耗的系统时间和用户时间

3、执行已经到期的动态定时器

4、执行sheduler_tick()函数

5、更新墙上时间,该时间存放在xtime中

6、计算平均负载值。

static void tick_periodic(int cpu){

  if(tick_do_time_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()));

  profile_tick(CPU_PROFILING);

}

void do_timer(unsigned long ticks){

  jiffies_64 += ticks;//增加jiffies值

  update_wall_time();//更新墙上时间

  calc_glbal_load();//更新系统的平均负载值

}

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();//负责减少当前运行进程的时间片计数值并且在需要时设置need_resched标志

  run_posix_cpu_timers(p);

}

tick_periodic()函数执行完毕后返回与体系结构相关的中断处理程序,继续执行后面的工作,释放xtime_lock锁,然后退出。

11.6 实际时间

当前实际时间 struct timespec xtime;

timespec 数据结构

struct timespec{

  _kernel_time_t tv_sec; //s 存放字1970.1.1以来经过的时间

  long tv_nsec;   //ns 记录上一秒开始进过的ns数。

}

读写xtime变量需要使用xtime_lock锁,该锁不是普通的自旋锁而是seqlock锁。

write_seqlock(&xtime_lock);

/**更新**/

write_sequnlock(&xtime_lock);

unsigned long seq;

do{

  unsigned long lost;

  seq = reqd_seqbegin(&xtime_lock);

  usec = timer->get_offect();

  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));

从用户空间获取墙上时间的主要接口时gettimeofday(),在内核中对应系统调用为sys_gettimeofday()。

系统调用setimeofday()来设置当前时间,它需要具有CAP_SYS_TIME权限。

11.7 定时器

内核定时器能够使得工作在指定的时间点上执行。定时器并不周期运行,它在超时后就会自行撤销。

11.7.1 使用定时器

定时器用结构timer_list表示

struct timer_list{

  struct list_head entry; //定时器链表的入口

  unsigned long expries; //以jiffies为单位的定时值

  void (*function)(unsigned long);//定时器处理函数

  unsigned long data ;//出给处理函数的参数

  struct tvec_t_base_s *base // 定时器内部值,用户不要使用

}

定义定时器:struct timer_list my_timer;

初始化:init_timer(&my_timer);

赋值:my_timer.expires = jiffies + delay;

  my_timer.data = 0;

  my_timer.function = my_function;

激活定时器:add_timer(&my_timer);

一般来说定时器都在超时后马上执行,但也有可能推迟到下一次时钟节拍时才能运行,所以不能用定时器来实现任何实时硬任务。

有时可能需要更改已经激活的定时器超时时间,所以内核通过函数mod_timer()来实现该功能,该函数可以改变指定的定时器超时时间:

mod_timer(&my_timer,jiffies+new_delay);

mod_timer()函数也可以操作那些已经初始化但是还没有激活的定时器,如果定时器未激活则mod_timer()会激活它。

如果需要在定时器超时前停止定时器,可以使用del_timer()函数:

del_timer(&my_timer);

删除定时器要等待其他处理器上运行的定时器处理程序都退出,这是就要使用del_timer_sync()函数执行操作。

11.7.2 定时器竞争条件

一般情况下使用del_timer_sync()取代del_time();

11.7.3 实现定时器

内核在时钟中断发生后执行定时器,定时器作为软中断在下半部上下文中执行。具体来说,时钟中断会执行update_process_times()函数,该函数随后调用run_local_timers()函数:

void run_local_timers(){

  hrtimer_run_queues();

  raise_softirq(TIMER_SOFTIRQ);

  softlockup_tick();

}

run_timer_softirq()函数处理软中断TIMER_SOFTIRQ,从而在当前处理器上运行所有的超市定时器。

为了提高效率,内核将定时器按他们的超时时间划分为5组。当定时器超时时间接近时,定事情将随组一起下移。采用分组定时器的方法可以在执行软中断的多数情况下,确保内核尽可能减少搜索超时定时器所带来的负担。因此定时器管理代码是非常高效的。

11.8 延迟执行

11.8.1 忙等待

该方法仅仅在想要延迟的时间是节拍的整数倍,或者精确率要求不高时可用:

while(timer_before(jiffies,timeout);

11.8.2 短延迟

内核提供了三个可以处理ms,ns和ms级别的延迟函数。

void udelay(unsigned long usecs);

void ndelay(unsigned long nsecs);

void mdelay(unsigned long msecs):

udelay()函数易耗执行数次循环达到延迟效果,而mdelay()函数又是通过udelay()函数实现的。因为内核知道处理器在1秒内能执行多少次循环,所以udelay()函数仅仅需要根据指定的延迟时间再1秒钟占的比例,就能决定需要进行多少次循环即可达到要求的推迟时间。

udelay()函数应当旨在小延迟中调用,因为在快速机器上的大延迟可能导致溢出。通常超过1ms的范围不要使用udelay()进行延迟。

11.8.3 schedule_timeout()

让需要延迟执行的任务睡眠到执行的延迟时间耗尽后再重新运行。用法:

set_current_state(TASK_INTERRUPTIBLE);

schedule_timeout(s*HZ);

唯一的参数是延迟的相对时间,单位为jiffies。在调用schedule_timeout()时,必须把任务设置为可中断或不可中断的状态,否则任务不会随眠。调用此代码必须处于进程上下文中,且不能持有锁。

该函数用创建了一个定时器timer;然后设置他的超时时间timeout;设置超时执行函数process_timeout();接着激活定时器而且调用schedule()。当定时器超时时process_timeout()调用:

void process_timeout(unsigned long data){

wake_up_process((task_t *)data);

}

此时唤醒指定的睡眠进程,进程开始从睡眠出继续执行:删除定时器。

 

posted @ 2013-04-15 15:57  shuying1234  阅读(600)  评论(0编辑  收藏  举报