Linux定时器与时间管理
Linux中的定时器与时间管理
时间概念
周期性产生的事件——比如每10ms一次——都是由系统定时器驱动的。系统定时器是一种可编程硬件芯片,它能以固定频率产生中断。该中断就是所谓的定时器中断,它所对应的中断处理程序负责更新系统时间,也负责执行需要周期性运行的任务。
内核知道连续两次时钟中断的间隔时间,这个间隔时间就称为节拍(tick),它等于节拍率分之一(1/(tick rate))秒。内核就是靠这种已知的时钟中断间隔来计算墙上时间和系统运行时间的。墙上时间(也就是实际时间)对用户空间的应用程序来说是最重要的。内核通过控制时钟中断维护实际时间,另外内核也为用户空间提供了一组系统调用以获取实际日期和实际时间。系统运行时间(自系统启动开始所经的时间)对用户空间和内核都很有用,因为许多程序都必须清楚流逝的时间。通过两次(现在和以后)读取运行时间再计算它们的差,就可以得到相对的流逝的时间了。
节拍率
系统定时器频率(节拍率)是通过静态预处理定义的,也就是HZ(赫兹),在系统启动时按照HZ值对硬件进行设置。体系结构不同,HZ的值也不同,大部分是100HZ,又有1000HZ。编写内核代码时,不要认为HZ值是一个固定不变的值。
提高节拍率意味着时钟中断产生得更加频繁,所以中断处理程序也会更频繁地执行。如此一来会给整个系统带来如下好处:
- 更高的时钟中断解析度(resolution)可提高时间驱动事件的解析度。100HZ的时钟的执行粒度为10ms,周期事件最快为每10ms运行一次,而不可能有更高的精度,但是1000HZ的解析度就是1ms,精细了十倍。
- 提高了时间驱动事件的准确度(accuracy)。假定内核在某个随机时刻触发定时器,而它可能在任何时间超时,但由于只有在时钟中断到来时才可能执行它,所以平均误差大约为半个时钟中断周期。比如说,如果时钟周期为HZ=100,那么事件平均在设定时刻的+/-5ms内发生,所以平均误差为5ms。如果HZ=1000,那么平均误差可降低到0.5ms——准确度提高了10倍。
节拍率越高,意味着时钟中断频率越高,也就意味着系统负担越重。因为处理器必须花时间来执行时钟中断处理程序,所以节拍率越高,中断处理程序占用的处理器的时间越多。这样不但减少了处理器处理其他工作的时间,而且还会更频繁地打乱处理器高速缓存并增加耗电。最后的结论是:至少在现代计算机系统上,时钟频率为1000HZ不会导致难以接受的负担,并且不会对系统性能造成较大的影响,尽管如此,2.6版本的内核还是允许在编译内核时选定不同的HZ值
jiffies
全局变量jiffies用来记录自系统启动以来产生的节拍的总数。启动时,内核将该变量初始化为0,此后,每次时钟中断处理程序就会增加该变量的值。因为一秒内时钟中断的次数等于HZ,所以jiffies一秒内增加的值也就为HZ。系统运行时间以秒为单位计算,就等于jiffies/HZ。
jiffies定义于文件<linux/jiffes.h>
中:
1 | extern unsigned long volatile jiffies; |
关键字volatile指示编译器在每次访问变量时都重新从主内存中获得,而不是通过寄存器中的变量别名来访问
jiffies变量总是无符号长整数(unsigned long),因此,在32位体系结构上是32位,在64位体系结构上是64位。由于性能与历史的原因,主要还考虑到与现有内核代码的兼容性,内核开发者希望jiffies依然为unsigned long。内核用了很巧妙的链接程序,用jiffies_64变量的初值覆盖了jiffies变量,jiffies取整个64位jiffies_64变量的低32位。
和任何C整型一样,当jiffies变量的值超过它的最大存放范围后就会发生溢出。如果节拍计数达到了最大值后还要继续增加的话,它的值会回绕(wrap around)到0。
考虑下面的代码,如果jiffies重新回绕为0,那么timeout永远都比jiffies大,这明显不是我们希望的
1 | unsigned long timeout = jiffies + HZ/2;/*0.5秒后超时*/ |
幸好,内核提供了四个宏来帮助比较节拍计数,它们能正确地处理节拍计数回绕情况。这些宏定义在文件<linux/jiffies.h>
中,这里列出的去是简化版,其中unkown参数通常是jifies,known参数是需要对比的值
1 |
为了便于说明,我们假设jiffies是单字节的无符号数,范围为0~255。假如jiffies开始为250,由于是无符号数据,那么它在机器中实际存储的补码为11111010,记为J1;timeout如果被设为252,实际存储为11111100;而过了一会jiffies发生回绕编变成了1,实际存储变为00000001,记为J2。 那么此时如果按照无符号数比较其大小关系,有:
J1<timeout & J2 <timeout
,这样的结果与实际的时间节拍统计是不符的,但是如果我们按照有符号数来比较会有什么结果呢?J1如果按照有符号数读取,首先从补码转换成原码:10000110,转换成十进制为-6;timeout按照有符号数读取,首先从补码转换成原码:10000100,转换成十进制为-4;J2按照有符号数读取,首先从补码转换成原码:00000001,转换成十进制为1;这样它们的大小关系为:J1<timeout<J2
。 这与实际的节拍计数就吻合了,以上内核定义的几个宏就是通过这种方式巧妙解决jiffies回绕问题的。
硬时钟和定时器
实时时钟(RTC)是用来持久存放系统时间的设备,即便系统关闭后,它也可以靠主板上的微型电池提供的电力保持系统的计时。在PC体系结构中,RTC和CMOS集成在一起,而且RTC的运行和BIOS的保存设置都是通过同一个电池供电的。当系统启动时,内核通过读取RTC来初始化墙上时间,该时间存放在xtime变量中。
系统定时器是内核定时机制中最为重要的角色。尽管不同体系结构中的定时器实现不尽相同,但是系统定时器的根本思想并没有区别——提供一种周期性触发中断机制。有些体系结构是通过对电子晶振进行分频来实现系统定时器,还有些体系结构则提供了一个衰减测量器(decrementer)——衰减测量器设置一个初始值,该值以固定频率递减,当减到零时,触发一个中断。无论哪种情况,其效果都一样。
时钟中断处理程序
- 获得xtime_lock锁,以便对访问jiffies_64和墙上时间xtime进行保护。
- 需要时应答或重新设置系统时钟。
- 周期性地使用墙上时间更新实时时钟。
- 调用体系结构无关的时钟例程:tick periodic()。
- 给jiffies_64变量增加1(这个操作即使是在32位体系结构上也是安全的,因为前面已经获得了xtime_lock锁)。
- 更新资源消耗的统计值,比如当前进程所消耗的系统时间和用户时间。
- 执行已经到期的动态定时器(11.6节将讨论)。
- 执行第4章曾讨论的sheduler_tick)函数。
- 更新墙上时间,该时间存放在xtime变量中。
- 计算平均负载值。
实际时间
当前实际时间(墙上时间)定义在文件kernel/time/timekeeping.c
中:xtime,timespec数据结构定义在文件<linux/time.h>
中,形式如下:
1 | struct timespec xtime; |
xtime.tv_sec以秒为单位,存放着自1970年1月1日(UTC)以来经过的时间,1970年1月1日被称为纪元,多数Unix系统的墙上时间都是基于该纪元而言的。xtime.v_nsec记录自上一秒开始经过的ns数。
读写xtime变量需要使用xtime_lock锁,该锁不是普通自旋锁而是一个seq锁
从用户空间取得墙上时间的主要接口是gettimeofday(),在内核中对应系统调用为sys_gettimeofday(),定义于kernel/time.c
:
定时器
定时器(有时也称为动态定时器或内核定时器)是管理内核流逝的时间的基础。
定时器的使用很简单。你只需要执行一些初始化工作,设置一个超时时间,指定超时发生后执行的函数,然后激活定时器就可以了。指定的函数将在定时器到期时自动执行。注意定时器并不周期运行,它在超时后就自行撤销,这也正是这种定时器被称为动态定时器e的一个原因;动态定时器不断地创建和撤销,而且它的运行次数也不受限制。定时器在内核中应用得非常普遍。
定时器由结构timer_list表示,定义在文件<linux/timer.h>
中。
1 | struct timer_list{ |
Linux的定时器机制(时间轮与红黑树)
前提:Linux根据时钟源设备启动tick中断,用tick计时,基于Hz,精度是1/Hz
低精度:用时间轮(timing wheel)机制维护定时事件,时间轮的触发基于tick,周期触发,内核根据时间轮处理超时事件
高精度:hrtimer(high resolution),基于事件触发,基于红黑树,将高精度时钟硬件的下次中断触发设置为红黑树最早到期的时间,到期后又取得下一个最早到期时间(类似于最小堆)
延迟执行
忙等待,最笨的方法
1 | unsigned long timeout=jiffies+10;/*10个节拍*/ |
更好的方法应该是在代码等待时,允许内核重新调度执行其他任务,cond_resched0函数将调度一个新程序投入运行:
1 | unsigned long delay=jiffies +5*HZ; |
对于短延迟,内核提供了三个可以处理ms、ns和ms级别的延迟函数,它们定义在文件<linux/delay.h>
和<asm/delay.h>
中,可以看到它们并不使用jiffies:
1 | void udelay(unsigned long usecs) // 利用忙循环将任务延迟指定的ms数后运行,后者延迟指定的ms数。 |
udelay())函数依靠执行数次循环达到延迟效果,而mdelay()函数又是通过udelay()函数实现的。因为内核知道处理器在1秒内能执行多少次循环(BogoMIPS值记录处理器在给定时间内忙循环执行的次数),所以udelay()函数仅仅需要根据指定的延迟时间在1秒中占的比例,就能决定需要进行多少次循环即可达到要求的推迟时间。