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;
在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来减少内存的使用。
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
一个弊端就是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