1 通用时间子系统
1.1 用于时间管理的对象
时钟源设备(clock-source device)
系统中可以提供一定精度的计时设备都可以作为时钟源设备。如x86构架里的TSC、HPET、ACPI PM-Timer、PIT等。但是不同的时钟源提供的时钟精度是不一样的。像TSC、HPET等时钟源既支持高精度模式(high-resolution mode)也支持低精度模式(low-resolution mode),而PIT只能支持低精度模式。此外,时钟源的计时都是单调递增的(monotonically),如果时钟源的计时出现翻转(即返回到0值),很容易造成计时错误,内核的一个patch(commit id: ff69f2)就是处理这类问题的一个很好示例。时钟源作为系统时钟的提供者,在可靠并且可用的前提下精度越高越好。在 Linux 中不同的时钟源有不同的rating,有更高 rating的时钟源会优先被系统使用,如图 2 所示。
时钟事件设备(clock-event device)
系统中可以触发one-shot(单次)或者周期性中断的设备都可以作为时钟事件设备。如 HPET、CPU Local APIC Timer等。HPET比较特别,它既可以做时钟源设备也可以做时钟事件设备。时钟事件设备的类型分为全局和per-CPU两种类型。全局的时钟事件设备虽然附属于某一个特定的CPU上,但是完成的是系统相关的工作,例如完成系统的tick更新;per-CPU 的时钟事件设备主要完成 Local CPU 上的一些功能,例如对在当前 CPU 上运行进程的时间统计,profile,设置 Local CPU 上的下一次事件中断等。和时钟源设备的实现类似,时钟事件设备也通过 rating 来区分优先级关系。
tick device
Tick device提供时钟事件的连续流,各个事件定期触发。Tick device其实是时钟事件设备的一个 wrapper,因此tick device也有one-shot和周期性这两种中断触发模式。每注册一个时钟事件设备,这个设备会自动被注册为一个tick device。全局的tick device用来更新诸如jiffies这样的全局信息,per-CPU的tick device则用来更新每个CPU相关的特定信息。 Broadcast
Broadcast的出现是为了应对这样一种情况:假定CPU使用Local APIC Timer作为 per-CPU的tick device,但是某些特定的CPU如 Intel的Westmere之前的CPU)在进入C3+ 的状态时Local APIC Timer也会同时停止工作,进入睡眠状态。在这种情形下broadcast可以替代Local APIC Timer继续完成统计进程的执行时间等有关操作。本质上broadcast是发送一个IPI(Inter-processor interrupt)中断给其他所有的CPU,当目标CPU收到这个IPI中断后就会调用原先Local APIC Timer正常工作时的中断处理函数,从而实现了同样的功能。目前主要在 x86 以及 MIPS 下会用到 broadcast 功能(补充:在ARM Cortex-A9上也可以使用)。
1.2 Timekeeping & GTOD (Generic Time-of-Day)
Timekeeping(可以理解为时间测量或者计时)是内核时间管理的一个核心组成部分。没有 Timekeeping,就无法更新系统时间,维持系统“心跳”。 GTOD 是一个通用的框架,用来实现诸如设置系统时间do_gettimeofday或者修改系统时间do_settimeofday等工作。这些功能的实现都依赖于系统的clocksource设备。
为了实现以上功能,Linux 实现了多种与时间相关但用于不同目的的数据结构。
Ø struct timespec
struct timespec {
__kernel_time_t tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};
timespec 精度是纳秒。它用来保存从00:00:00 GMT, 1 January 1970开始经过的时间。内核使用全局变量xtime来记录这一信息,这就是通常所说的“Wall Time”或者“Real Time”。与此对应的是“System Time”。System Time是一个单调递增的时间,每次系统启动时从0开始计时。
Ø struct timeval
struct timeval {
__kernel_time_t tv_sec; /* seconds */
__kernel_suseconds_t tv_usec; /* microseconds */
};
timeval精度是微秒。timeval主要用来指定一段时间间隔。
Ø ktime
typedef union {
s64 tv64;
#if BITS_PER_LONG != 64 && !defined(CONFIG_KTIME_SCALAR)
struct {
# ifdef __BIG_ENDIAN
s32 sec, nsec;
# else
s32 nsec, sec;
# endif
} tv;
#endif
}ktime_t;
ktime_t是hrtimer主要使用的时间结构。无论使用哪种体系结构,ktime_t始终保持64bit的精度,并且考虑了大小端的影响。
Ø cycle_t;
typedef u64 cycle_t;
cycle_t 是从时钟源设备中读取的时钟类型。
为了管理这些不同的时间结构,Linux 实现了一系列辅助函数来完成相互间的转换。
ktime_to_timespec,ktime_to_timeval,ktime_to_ns/ktime_to_us,反过来有诸如 ns_to_ktime 等类似的函数。
timeval_to_ns,timespec_to_ns,反过来有诸如 ns_to_timeval 等类似的函数。
timeval_to_jiffies,timespec_to_jiffies,msecs_to_jiffies, usecs_to_jiffies, clock_t_to_jiffies 反过来有诸如 ns_to_timeval 等类似的函数。
clocksource_cyc2ns / cyclecounter_cyc2ns
时钟源设备和时钟事件设备的引入,将原本放在各个体系结构中重复实现的冗余代码封装到各自的抽象层中,这样做不但消除了原来timer wheel与内核其他模块的紧耦合性,更重要的是系统可以在运行状态动态更换时钟源设备和时钟事件设备而不影响系统正常使用,譬如当 CPU 由于睡眠要关闭当前使用的时钟源设备或者时钟事件设备时系统可以平滑的切换到其他仍处于工作状态的设备上。Timekeeping/GTOD在使用时钟源设备的基础上也采用类似的封装实现了体系结构的无关性和通用性。hrtimer则可以通过timekeeping提供的接口完成定时器的更新,通过时钟事件设备提供的事件机制,完成对timer的管理。在图2 中还有一个重要的模块就是tick device的抽象,尤其是dynamic tick。Dynamic tick的出现是为了能在系统空闲时通过停止tick的运行以达到降低CPU功耗的目的。使用dynamic tick的系统,只有在有实际工作时才会产生tick,否则tick是处于停止状态。