Linux内核设计与实现(4)---Timers and Time Management

3730阅读 2评论2013-05-15 leon_yu
分类:LINUX

1.内核中时间的概念

(1)内核中有大量事件是基于时间驱动的,相对时间和绝对时间这两个概念对内核时间管理来说都至关重要。

(2)系统定时器和时钟中断处理程序是Linux系统内核管理机制中的中枢。内核必须在硬件的帮助下才能计算和管理时间,定时器以某种频率自行触发时钟中断。当中断发生时,内核就通过一种特殊的中断处理程序对其进行处理。

(3)内核可以动态创建和撤销动态定时器--- 一种用来推迟执行程序的工具。

墙上时间:就是实际时间,对用户空间的应用程序很重要,可以用来维护实际日期和时间。

系统运行时间:自系统启动以来经历的时间,对内核和用户空间都有用。

2.HZ

系统定时器频率(节拍率),是通过静态预处理定义的。在定义,对用户空间,内核HZ几乎完全隐藏,确切的 HZ 值只能通过 /proc/interrupts 获得:=/proc/interrupts / /proc/uptime ,自开机以来系统的滴答数除以运行时间.

更高的节拍率意味着时钟产生更加频繁,中断处理程序也会执行更频繁。更高的时钟中断解析度可提高时间事件驱动时间的解析度和准确度(时钟中断执行准确度是+/-1/(2HZ))

高HZ优势:

①内核定时器能够以更高的频率和更高的准确度运行;

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

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

④提高进程抢占的准确度;

高HZ劣势

节拍率越高,时钟中断频率越高,中断程序占用处理器时间就越多。这样不但减少了处理器工作时间,还更频繁的打乱了处理器高速缓存,增加功耗。

无节拍OS

当内核配置CONFIG_HZ时,系统根据这个选项动态调度时钟中断,这样实质性受益是省电

3.jiffies

(1)用来记录自系统启动以来产生的节拍总数,转化为时间(jiffies/HZ)

Jiffies总是全局的unsigned long变量,在32位体系结构上,它就是32位,在HZ=100时,497天溢出。在64位体系上是64位,我们看不到溢出。通过连接,jiffiesjiffies_64覆盖(64位机两者相同,在32位机jiffiesjiffies_6432)。通过函数get_jiffies_64()可以读取完整64jiffies值。

(2)为防止溢出问题,内核提供几个宏来比较节拍计数

点击(此处)折叠或打开

  1. time_after(unknown,known)//当jiffies超过指定known时,返回真
  2. time_before(unknown,known)
  3. time_after_eq(unknown,known)
  4. time_before_eq(unknown,known)
  5. unknown通常是jiffies,known = jiffies+HZ/2

(3)用户空间和HZ

内核定义USER_HZ给应用层,当USER_HZ小于等于HZ,且两者互为整数倍时返回给应用的时间是 return x/(HZ/USER_HZ)

内核使用函数jiffies_64_to_clock_t()64jiffiesHZ转换到USER_HZ

点击(此处)折叠或打开

  1. unsigned long start;
  2. unsigned long total_time;
  3. start = jiffies;
  4. /* do some work ... */
  5. total_time = jiffies - start;
  6. printk(“That took %lu ticksn”, jiffies_to_clock_t(total_time));
  7. printk(“That took %lu secondsn”, total_time / HZ);

4.硬时钟和定时器

(1)实时时钟(RTC)用来持久存放系统时间,系统启动时,内核读取RTC来初始化墙上时间,该时间存放在xtime变量中。

(2)系统定时器,在X86中,采用可编程中断时钟(PIT)作为时钟中断源。

5.时钟中断处理函数

分两部分:体系结构相关和体系结构无关。

1)体系结构相关例程,作为系统定时器的中断处理函数注册到内核,主要完成以下工作:

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

需要时应答或重新设置系统时钟;

周期性地使用墙上时间更新实时时钟;

调用体系无关时钟例程:tick_periodic().

2)中断主要通过体系无关例程tick_periodic()完成更多工作

jiffies_64变量加1;

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

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

执行sheduler_tick()函数;

更新墙上时间,该事件存放在xtime

点击(此处)折叠或打开

  1. /*
  2.  * Periodic tick
  3.  */
  4. static void tick_periodic(int cpu)
  5. {
  6.     if (tick_do_timer_cpu == cpu) {
  7.         write_seqlock(&xtime_lock);

  8.         /* Keep track of the next tick event */
  9.         tick_next_period = ktime_add(tick_next_period, tick_period);

  10.         do_timer(1); //操作jiffies_64,更新墙上时间,更新系统平均负载
  11.         write_sequnlock(&xtime_lock);
  12.     }

  13.     update_process_times(user_mode(get_irq_regs()));//更新所消耗的各种节拍数
  14.     profile_tick(CPU_PROFILING);
  15. }

内核对进程进行时间计数,是根据中断发生时处理器所处模式统计的,它把上个节拍全部算给了进程。这种粒度的进程统计方式是传统unix具有的,现在还没有更加精密的统计算法。

run_lock_timer(),标记一个软中断去处理所有到期定时器。

以上全部工作每1/HZ秒发生一次。

6.实际时间

当前实际时间(墙上时间)定义在文件kernel/time/timekeeping.c

点击(此处)折叠或打开

  1. struct timespec xtime;
  2. timespec 定义在<linux/time.h>
  3. struct timespec {
  4.     time_t    tv_sec;    //秒,存放自1970年1月1日(utc)以来经过的时间
  5.     long    tv_nsec;    //自从上一秒开始结果的ns数
  6. };
  7. 读写xtime变量需要使用xtime_lock锁,一个seqlock锁。
  8. write_seqlock(&xtime_lock);
  9. /* update xtime ... */
  10. write_sequnlock(&xtime_lock);
  11. 读取xtime时也要使用read_seqbegin()和read_seqretry()
  12. unsigned long seq;
  13. do {
  14.     unsigned long lost;
  15.     seq = read_seqbegin(&xtime_lock);
  16.     usec = timer->get_offset();
  17.     lost = jiffies - wall_jiffies;
  18.     if (lost)
  19.         usec += lost * (1000000 / HZ);
  20.     sec = xtime.tv_sec;usec += (xtime.tv_nsec / 1000);
  21. } while (read_seqretry(&xtime_lock, seq));

在用户空间取得墙上时间用gettimeofday(),其在内核中调用sys_systimeofday()实现,可以用settimtofday()来设置当前时间,它需要有CAP_SYS_TIME权能。

7.定时器

指定函数将在定时器到期时自动异步执行,执行后自动撤销。

定义在文件

点击(此处)折叠或打开

  1. struct timer_list {
  2.     struct list_head entry; //定时器链表的入口
  3.     unsigned long expires; //以jiffes为单位的计时值

  4.     void (*function)(unsigned long); //定时器处理函数
  5.     unsigned long data; //传给处理函数的参数
  6.     struct tvec_base *base; //定时器内部值,用户不使用
  7. };

用户一般不操作这些结构体,而是通过内核定义好的一系列宏来处理

点击(此处)折叠或打开

  1. 定义定时器:
  2. struct timer_list my_timer;
  3. 初始化定时器:
  4. init_timer(&my_timer);
  5. 填充定时器结构体:
  6. my_timer.expires = jiffes+HZ;//延时1s
  7. my_timer.data = NULL; //可以利用同一个处理器函数注册多个定时器
  8. my_timer.function = my_function;//定时器处理函数
  9. 内核可以保证不会在超时时间到期前运行处理器函数,但是有可能会延误执行,误差是+/-半个节拍,所以定时器不能用来做硬实时任务。
  10. 激活定时器
  11. add_timer(&my_timer);
  12. mod_timer(&my_timer);//激活或修改定时器,若调用时已激活,返回0,否则返回1

  13. del_timer(&my_timer)//删除定时器,定时器未被激活返回0,否则返回1
  14. 为了防止删除定时器时,定时器在多处理器上可能正在运行,即出现竞争条件,使用
  15. del_timer_sync(&my_timer)//不能在中断上下文使用

在中断处理函数中,要保护好共享数据。

定时器作为软中断在下半部上下文中执行,在run_local_timers()中调用所有到时定时器。

内核采用分组定时器方法来管理定时器,可以减少搜索超时定时器所带来的负担。

8.延迟执行

(1)忙等待

该方法用于延迟时间是节拍的整数倍,或对精确度不高时使用

unsigned long timeout = jiffies+10;//延迟10个节拍

while(time_befor(jiffes,timeout)) ;

这是最简单的延迟方法,但是延迟独占CPU,很少用。改进版是:

unsigned long delay = jiffies + 5*HZ;

while (time_befor(jiffies,delay))

cond_resched();//调度一个新程序运行,该方法有效条件是系统中存在更重要的任务要运行

因为调用了调度程序,所以不能在中断上下文使用,只能在进程上下文使用。所有延迟方法在进程上下文都使用的很好,中断处理都应该尽快的执行,最好不用延迟。

延迟不管在哪种情况下,都不应该在持有锁时或禁止中断时使用。

(2)短延迟,比节拍短,短暂又精确的延迟

点击(此处)折叠或打开

  1. 定义在<linux/delay.h><asm/delay.h>
  2. void udelay(unsigned long usecs);
  3. void ndelay(unsigned long usecs);
  4. void mdelay(unsigned long usecs);

udelay是由忙循环实现的,mdelay调用udelay实现,但是会睡眠,1ms以上的延迟就不要用udelay(),防止溢出。

BogoMIPS值记录处理器在给定时间内忙循环执行的次数,存放在loops_per_jiffy

(3)schedule_timeout()

更理想的方案是schedule_timeout()函数,让需要延迟睡眠执行的任务睡眠到指定延迟时间耗尽再重新执行,同样,只保证延迟时间尽量接近睡眠时间,时间到期后,内核唤醒被延迟任务并将其重新放回运行队列。

set_current_state(TASK_INTERRUPTIBLE);//将任务设置为可中断睡眠状态

schedule_timeout(s*HZ); //睡眠s
schedule_timeout()的实现

点击(此处)折叠或打开

  1. signed long __sched schedule_timeout(signed long timeout)
  2. {
  3.     struct timer_list timer;
  4.     unsigned long expire;

  5.     switch (timeout)
  6.     {
  7.     case MAX_SCHEDULE_TIMEOUT: //用来检查是否无限期睡眠
  8.         /*
  9.          * These two special cases are useful to be comfortable
  10.          * in the caller. Nothing more. We could take
  11.          * MAX_SCHEDULE_TIMEOUT from one of the negative value
  12.          * but I' d like to return a valid offset (>=0) to allow
  13.          * the caller to do everything it want with the retval.
  14.          */
  15.         schedule();
  16.         goto out;
  17.     default:
  18.         /*
  19.          * Another bit of PARANOID. Note that the retval will be
  20.          * 0 since no piece of kernel is supposed to do a check
  21.          * for a negative retval of schedule_timeout() (since it
  22.          * should never happens anyway). You just have the printk()
  23.          * that will tell you if something is gone wrong and where.
  24.          */
  25.         if (timeout < 0) {
  26.             printk(KERN_ERR "schedule_timeout: wrong timeout "
  27.                 "value %lxn", timeout);
  28.             dump_stack();
  29.             current->state = TASK_RUNNING;
  30.             goto out;
  31.         }
  32.     }

  33.     expire = timeout + jiffies; //设置超时时间

  34.     setup_timer_on_stack(&timer, process_timeout, (unsigned long)current);//设置超时函数
  35.     __mod_timer(&timer, expire);
  36.     schedule();//
  37.     del_singleshot_timer_sync(&timer);

  38.     /* Remove the timer from the object tracker */
  39.     destroy_timer_on_stack(&timer);

  40.     timeout = expire - jiffies;

  41.  out:
  42.     return timeout < 0 ? 0 : timeout;
  43. }

若任务提前唤醒(比如收到信号),那么定时器被撤销,返回剩余时间。

②设置超时时间,在等待队列上睡眠

有时,等待队列上的某个任务可能既在等待一个特定事件到来,又在等待一个特定时间到期(看谁先到),这样可以使用schedule_timeout()代替schedule().当然,需要检查被唤醒的原因(可能是信号,也可能是时间到),然后执行相应操作。

上一篇:Linux内核设计与实现(2)---Getting Started with the Kernel
下一篇:Linux补丁(patch)制作与应用

文章评论