Linux SMP时间管理及
linux的时钟系统的两大主要功能是计时和定时。计时功能就是指记录或设置当前的系统时间(包括日期),gettimeofday、settimeofday、time、clock_gettime、clock_settime等系统调用与计时相关。
定时功能与定时器相关。设定一个定时器的(定时)时间,设定定时器的回调函数,启动定时器,在(定时)时间到时,定时器的回调函数会被调用。在 Linux 内核中主要有两种类型的定时器。一类称为timeout 类型,另一类称为timer 类型。timeout类型的定时器通常用于检测各种错误条件,例如用于检测网卡收发数据包是否会超时的定时器,IO设备的读写是否会超时的定时器等等。通常情况下这些错误很少发生,因此,使用 timeout 类型的定时器一般在超时之前就会被移除,从而很少产生真正的函数调用和系统开销。总的来说使用 timeout 类型的定时器产生的系统开销很小,它是下文提及的timer wheel通常使用的环境。此外,在使用timeout类型定时器的地方往往并不关心超时处理,因此超时精确与否,早0.01秒或者晚0.01秒并不十分重要。timer类型的定时器与timeout类型的定时器正相反,使用 timer类型的定时器往往要求在精确的时钟条件下完成特定的事件,通常是周期性的并且依赖超时机制进行处理。例如设备驱动通常会定时读写设备来进行数据交互。
Linux具有动态tick和高精度定时器hrtimer功能,它们需要具体硬件平台的支持,动态tick功能由CONFIG_NO_HZ宏定义控制,高精度定时器由CONFIG_HIGH_RES_TIMERS宏定义控制。动态tick功能是指系统不需要为调度器周期性产生中断(tick),系统只在需要时才产生中断,提高系统效率,降低系统功耗。hrtimer的精度很高,可以用于realtime和多媒体等需要高精度定时的场景,常规定时器timer的精度不高,不能用于需要高精度定时的场景。本文基于CONFIG_NO_HZ和CONFIG_HIGH_RES_TIMERS都使能的情况进行介绍。
任务调度器(scheduler)使用hrtimer作为其调度定时器,在CPU非空闲时,设置调度hrtimer的到时时间为下一个tick,在hrtimer到时时,触发任务调度。在CPU空闲时,根据timer的超时时间设置调度hrtimer的到时时间。
jiffies是linux当前tick计数,在linux中,一些超时函数的时间单位是tick。linux要求具体的硬件平台提供两个硬件时钟设备,一个是时钟源(clocksource)设备,另一个是时钟事件(clockevent)设备。时钟源设备用于计时;时钟事件设备用于触发时钟事件(产生定时中断),hrtimer通过时钟事件设备实现,它的到时回调函数在时钟事件设备中断服务函数中调用。
系统初始化过程中与时钟系统相关的初始化函数在start_kernel函数中调用,它们的调用顺序:tick_init,init_timers,hrtimers_init,timekeeping_init,time_init,sched_clock_init。
1 Linux Wheel时钟
1.1 Wheel时钟的实现(低精度定时器的实现)
通常OS操作系统都支持Wheel方式,例如Linux、Neclues和vxworks都支持100-200Hz的节拍时钟。通过节拍OS进行时钟刷新以及产生任务调度,而每个硬件节拍就称为tick。
1.1.1 核心数据结构
在Linux 2.6.16之前,内核一直使用一种称为timer wheel(定时器轮)的机制来管理时钟。这就是kernel一直采用的基于HZ的timer机制。Timer wheel 的核心数据结构如下所示:
#define TVN_BITS (CONFIG_BASE_SMALL ? 4 : 6)
#define TVR_BITS (CONFIG_BASE_SMALL ? 6 : 8)
#define TVN_SIZE (1 << TVN_BITS)
#define TVR_SIZE (1 << TVR_BITS)
#define TVN_MASK (TVN_SIZE - 1)
#define TVR_MASK (TVR_SIZE - 1)
struct tvec {
struct list_head vec[TVN_SIZE];
};
struct tvec_root {
struct list_head vec[TVR_SIZE];
};
struct tvec_base {
spinlock_t lock;
struct timer_list *running_timer;
unsigned long timer_jiffies;
unsigned long next_timer;
struct tvec_root tv1; //每一项称为一组
struct tvec tv2;
struct tvec tv3;
struct tvec tv4;
struct tvec tv5;
} ____cacheline_aligned;
表1 tvec_base的各个域
域名 |
类型 |
描述 |
Lock |
spinlock_t |
用于同步操作 |
running_timer |
struct timer_list * |
正在处理的软件时钟 |
timer_jiffies |
unsigned long |
当前正在处理的软件时钟到期时间 |
next_timer |
unsigned long |
下一个将要到期的timer的到期时间 |
tv1 |
struct tvec_root |
保存了到期时间从 timer_jiffies到timer_jiffies + 2^8-1之间(包括边缘值)的所有软件时钟 |
tv2 |
struct tvec |
保存了到期时间从 timer_jiffies+2^8到 timer_jiffies+2^14-1之间(包括边缘值)的所有软件时钟 |
tv3 |
struct tvec |
保存了到期时间从 timer_jiffies+2^14到 timer_jiffies+2^20-1之间(包括边缘值)的 所有软件时钟 |
tv4 |
struct tvec |
保存了到期时间从 timer_jiffies+2^20到 timer_jiffies+2^26-1之间(包括边缘值)的 所有软件时钟 |
tv5 |
struct tvec |
保存了到期时间从 timer_jiffies+2^26到 timer_jiffies+2^32-1之间(包括边缘值)的 所有软件时钟 |
在SMP系统中,通过如下方式定义per-cpu变量:
struct tvec_base boot_tvec_bases;
EXPORT_SYMBOL(boot_tvec_bases);
static DEFINE_PER_CPU(struct tvec_base *, tvec_bases) = &boot_tvec_bases;
在内存较小的系统中可以设置CONFIG_BASE_SMALL选项为1来减少内存的使用。以 CONFIG_BASE_SMALL 定义为 0 为例,TVR_SIZE = 256,TVN_SIZE = 64,这样可以得到如图1所示的 timer wheel 的结构。
图1 管理定时器的timer wheel结构
从图1中可以清楚地看出:软件时钟(struct timer_list在图中由timer表示)以双向链表(struct list_head)的形式,按照它们的到期时间保存相应的桶(tv1~tv5)中。tv1中保存了相对于timer_jiffies下256个tick时间内到期的所有软件时钟;tv2中保存了相对于 timer_jiffies下256*64个tick时间内到期的所有软件时钟;tv3中保存了相对于timer_jiffies下256*64*64个tick时间内到期的所有软件时钟;tv4中保存了相对于timer_jiffies下256*64*64*64个tick时间内到期的所有软件时钟;tv5中保存了相对于timer_jiffies下256*64*64*64*64个tick时间内到期的所有软件时钟。从静态的角度看,假设timer_jiffies为0,那么tv1[0]保存着当前到期(到期时间等于timer_jiffies)的软件时钟(需要马上被处理),tv1[1]保存着下一个tick到达时,到期的所有软件时钟,tv1[n](0<= n <=255)保存着下n个 tick 到达时,到期的所有软件时钟。而tv2[0]则保存着下256到511个tick之间到期所有软件时钟,tv2[1]保存着下512到767个 tick之间到期的所有软件时钟,tv2[n](0<= n <=63)保存着下256*(n+1)到256*(n+2)-1个 tick 之间到达的所有软件时钟。tv3~tv5依次类推。
1.1.2 定时器到期处理
对所有定时器的处理都由update_process_times发起,具体的调用流程如下:
update_process_times
run_local_timers
hrtimer_run_queues
raise_softirq(TIMER_SOFTIRQ)
在init_timers初始化时设置TIMER_SOFTIRQ的软中处理函数为run_timer_softirq,那么在run_timer_softirq将完成对到期的定时器实际的处理工作。其调用流程如下:
run_timer_softirq
__run_timers
从上述的调用流程可以看出__run_timers完成了实际的工作,具体代码如下
static inline void __run_timers(struct tvec_base *base)
{
struct timer_list *timer;
spin_lock_irq(&base->lock);
while (time_after_eq(jiffies, base->timer_jiffies)) {
struct list_head work_list;
struct list_head *head = &work_list;
int index = base->timer_jiffies & TVR_MASK;
if (!index &&
(!cascade(base, &base->tv2, INDEX(0))) &&
(!cascade(base, &base->tv3, INDEX(1))) &&
!cascade(base, &base->tv4, INDEX(2)))
cascade(base, &base->tv5, INDEX(3));
++base->timer_jiffies;
list_replace_init(base->tv1.vec + index, &work_list);
while (!list_empty(head)) {
void (*fn)(unsigned long);
unsigned long data;
timer = list_first_entry(head, struct timer_list, entry);
fn = timer->function;
data = timer->data;
. . . .
fn(data);
. . . .
}
base->timer_jiffies用来记录在TV1中最接近超时的tick的位置。index是用来遍历TV1 的索引。每一次循环index会定位一个当前待处理的tick,并处理这个tick下所有超时的timer。base->timer_jiffies会在每次循环后增加一个jiffy,index也会随之向前移动。当index变为 0 时表示TV1完成了一次完整的遍历,此时所有在TV1中的timer都被处理,因此需要通过 cascade将后面TV2,TV3等timer list中的timer向前移动,类似于进位。这种层叠的timer list实现机制可以大大降低每次检查超时timer的时间,每次中断只需要针对TV1进行检查,只有必要时才进行cascade。即便如此,timer wheel的实现机制仍然存在很大弊端。一个弊端就是cascade开销过大。在极端的条件下,同时会有多个TV需要进行cascade处理,会产生很大的时延。这也是为什么说timeout类型的定时器是timer wheel的主要应用环境,或者说timer wheel是为timeout类型的定时器优化的。因为timeout类型的定时器的应用场景多是错误条件的检测,这类错误发生的机率很小,通常不到超时就被删除了,因此不会产生 cascade的开销。另一方面,由于timer wheel是建立在 HZ 的基础上的,因此其计时精度无法进一步提高。毕竟一味的通过提高 HZ 值来提高计时精度并无意义,结果只能是产生大量的定时中断,增加额外的系统开销。因此,有必要将高精度的 timer 与低精度的 timer 分开,这样既可以确保低精度的 timeout 类型的定时器应用,也便于高精度的 timer 类型定时器的应用。还有一个重要的因素是 timer wheel 的实现与 jiffies 的耦合性太强,非常不便于扩展。因此,自从2.6.16开始, hrtimer这个新的timer子系统被加入到内核中。
1.2 Linux Wheel时钟的初始化过程
在kernel启动的时候完成对Linux Wheel的核心数据结构的初始化工作。具体的初始化流程为
start_kernel
init_timers
timer_cpu_notify
init_timers_cpu
init_timer_stats
register_cpu_notifier
open_softirq
其核心函数为init_timers_cpu,具体的代码如下
static int __cpuinit init_timers_cpu(int cpu)
{
int j;
struct tvec_base *base;
static char __cpuinitdata tvec_base_done[NR_CPUS];
/*根据初始化的阶段,使用不同的方式求出tvec_bases*/
if (!tvec_base_done[cpu]) {//tvec_base_done用来表示per-cpu变量tvec_bases是否初始化完
static char boot_done;
//boot_done=0表示系统的boot CPU没有完成启动
if (boot_done) {
/*
* The APs use this path later in boot
*/
base = kmalloc_node(sizeof(*base),
GFP_KERNEL | __GFP_ZERO,
cpu_to_node(cpu));
if (!base)
return -ENOMEM;
/* Make sure that tvec_base is 2 byte aligned */
if (tbase_get_deferrable(base)) {
WARN_ON(1);
kfree(base);
return -ENOMEM;
}
per_cpu(tvec_bases, cpu) = base;
} else {
/*
* This is for the boot CPU - we use compile-time
* static initialisation because per-cpu memory isn't
* ready yet and because the memory allocators are not
* initialised either.
*/
boot_done = 1;
base = &boot_tvec_bases;
}
tvec_base_done[cpu] = 1;
} else {
base = per_cpu(tvec_bases, cpu);
}
spin_lock_init(&base->lock);
//初始化核心的数据结构
for (j = 0; j < TVN_SIZE; j++) {
INIT_LIST_HEAD(base->tv5.vec + j);
INIT_LIST_HEAD(base->tv4.vec + j);
INIT_LIST_HEAD(base->tv3.vec + j);
INIT_LIST_HEAD(base->tv2.vec + j);
}
for (j = 0; j < TVR_SIZE; j++)
INIT_LIST_HEAD(base->tv1.vec + j);
base->timer_jiffies = jiffies;
base->next_timer = base->timer_jiffies;
return 0;
}
2 通用时间子系统
2.1 用于时间管理的对象
2.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 所示。
图 2 时钟源的分类
1~99 |
100~199 |
200~299 |
300~399 |
400~499 |
非常差的时钟源,只能作为最后的选择。如 jiffies |
基本可以使用但并非理想的时钟源。如 PIT |
正确可用的时钟源。如 ACPI PM Timer,HPET |
快速并且精确的时钟源。如 TSC |
理想时钟源。如 kvm_clock,xen_clock |
2.1.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 来区分优先级关系。
内核中使用如下数据结构来描述时钟事件设备:
2.1.3 tick device
Tick device提供时钟事件的连续流,各个事件定期触发。Tick device其实是时钟事件设备的一个 wrapper,因此tick device也有one-shot和周期性这两种中断触发模式。每注册一个时钟事件设备,这个设备会自动被注册为一个tick device。全局的tick device用来更新诸如jiffies这样的全局信息,per-CPU的tick device则用来更新每个CPU相关的特定信息。
2.1.4 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上也可以使用)。
2.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是处于停止状态。
3 hrtimer 的实现机制
hrtimer 首先要实现的功能就是要克服timer wheel的缺点:低精度以及与内核其他模块的高耦合性,其设计框架如图2所示
图2
hrtimer是建立在per-CPU时钟事件设备上的,对于一个SMP系统,如果只有全局的时钟事件设备,hrtimer无法工作。因为如果没有per-CPU时钟事件设备,时钟中断发生时系统必须产生必要的IPI中断来通知其他CPU完成相应的工作,而过多的IPI中断会带来很大的系统开销,这样会令使用hrtimer的代价太大,不如不用。为了支持hrtimer,内核需要配置CONFIG_HIGH_RES_TIMERS=y。
hrtimer有两种工作模式:低精度模式(low-resolution mode)与高精度模式(high-resolution mode)。虽然hrtimer子系统是为高精度的timer准备的,但是系统可能在运行过程中动态切换到不同精度的时钟源设备,因此,hrtimer必须能够在低精度模式与高精度模式下自由切换。由于低精度模式是建立在高精度模式之上的,因此即便系统只支持低精度模式,部分支持高精度模式的代码仍然会编译到内核当中。