在上一篇文章中,介绍了计算机系统的时间设备。这些设备在Linux整个计时架构中处于最底层。 在硬件设备中获取时间相关的数据后,Linux系统需要做以下几个工作:
- 更新自系统启动以来的时间戳
- 更新当前时间日期
- 计算每个CPU上进程执行的时间
- 更新资源使用率统计
- 更新定时器系统调用
这些操作每一个tick都会完成一次。 那么什么是tick呢? tick是Linux计时架构中一个重要的概念,中文称之为时钟滴答。 在IRQ0上连续两次定时器中断的时间间隔称为tick,时长保存在tick_nsec变量中。 tick_nsec通常情况下会初始化为999,848纳秒,接近1毫秒,因为时间中断的频率一般设置为1000Hz(PIT的时间中断频率)。
Kernel将时间抽象成几个重要的数据结构:
-
The timer object
这个结构用来抽象不同时间源的数据。 其中两个重要变量
mark_offset和get_offset。mark_offset记录上一次tick的时间,定时器中断的时候更新。get_offset返回自上一次tick后经过的时间,单位是微秒。
Linux使用这两个变量做插值,提供比tick更高精度的时间记录。 另外
cur_timer变量保存精度最高的时间源(timer_hpet>timer_pmtmr>timer_tsc>timer_pit>timer_none) -
The jiffies variable
jiffies变量保存系统开启到现在的tick数。 在x86体系下,jiffies是一个32位的变量,只能记录约50天。当要溢出的时候,Linux会使用其他变量去转换,做到持续记录tick。 在系统启动的时候,jiffies初始化为0xfffb6c20,五分钟后即溢出。 这样做的目的是可以及时发现不对溢出做的处理的kernel,不会影响稳定的版本。jiffies不声明为64位的原因是在32位系统不能高效读写64位的变量。 -
The xtime variable
xtime保存的是上墙时间。 其中有两个字段tv_sec和tv_nsec。tv_sec保存从(UTC)1970-1-1号到当前时间的秒数。tv_nsec保存上一秒到现在经历的纳秒数。
xtime每个tick更新一次,一秒钟大约更新1000次。
为了完成时间相关的工作,Linux计时架构有两个主要操作,计时器的初始化和定时器的中断处理。
-
计时器初始化
在kernel初始化期间会调用
time_init()函数。time_init()完成以下操作:- 初始化
xtime变量。从RTC读取至1970-1-1至今的秒数。 - 初始化
wall_to_monotonic变量。这个变量和xtime类似。 - 如果系统支持HPET,则调用
hpet_enable()函数启动HPET,否则使用PIT - 调用
select_timer()选择最好的时钟源。 - 开启IRQ0中断,系统定时器可以接收来自PIT或者HPET的中断。
至此计时器初始化完成。之后每次tick都会调用
timer_interrupt()函数。 - 初始化
-
定时器中断处理(timer_interrupt)
- 开启xtime自旋锁
- 根据
cur_timer的值,处理mark_offsettimer_hpet: HPET是中断源,如果丢失中断,mark_offset则更新jiffiestimer_pmtmr: PIT是中断源,APIC是时间源。如果丢失中断,mark_offset则更新jiffiestimer_tsc: PIT是中断源,TSC是时间源。如果丢失中断,mark_offset根据需要更新jiffiestimer_pit: PIT是中断源,而且没有其他时间源,mark_offset不更新
- 调用
do_timer_interrupt()函数- 增加
jiffies - 调用
update_times()函数更新系统时间日期和计算系统复杂 - 调用
update_process_times()函数执行特定CPU相关的操作,比如切换进程等 - 调用
profile_tick()函数
- 增加
- 释放xtime自旋锁
需要注意的是,多核的定时器架构和单核的定时器架构有不同的处理流程。 在单处理器系统,所有的时间活动都由全局定时器触发。 在多处理器系统,所有一般的活动由全局定时器触发,特定CPU的活动由CPU Local Timer触发。 但是这两种情况的界定比较模糊,而且一些多核系统的CPU并没有CPU Local Timer。