在上一篇文章中,介绍了计算机系统的时间设备。这些设备在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_offset
timer_hpet
: HPET是中断源,如果丢失中断,mark_offset
则更新jiffies
timer_pmtmr
: PIT是中断源,APIC是时间源。如果丢失中断,mark_offset
则更新jiffies
timer_tsc
: PIT是中断源,TSC是时间源。如果丢失中断,mark_offset
根据需要更新jiffies
timer_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。