Linux中定时器分两种,一种是timeout类型,另外一种是timer类型。timeout类型的定时器一般用于检测各类错误条件,例如用于检测网卡发收数据包是否会超时,IO设备的读写是否会超时的定时器等。使用timeout类型的定时器每每不关心超时处理,所以超时精确与否,并不重要。这类定时器是基于time wheel机制实现的。timer类型的定时器与timeout类型的定时器正好相反,使用timer类型的定时器每每要求在精确的时钟条件下完成特定的事件。timer类型的定时器是基于红黑树实现的。数组
Linux须要进行时钟管理,离不开底层的硬件支持。在早期的Linux内核中,经过8253芯片提供的PIT来提供时钟,可是PIT的频率很低,只能提供最多1ms的时钟精度,因为PIT触发的中断速度太慢,会致使很大的时延,对于像音视频这类对时间精度要求很高的应用并不足够,会极大的影响用户体验。随着硬件平台的不断发展,陆续出现了TSC,HPET,ACPI PM Timer,CPU Local APIC Timer等精度更高的时钟,内核为了可使用更高精度的定时器,开发出了基于rbtree的hrtimer子系统。app
在Linux 2.6.16以前,内核一导致用一种被称为time wheel的机制来管理定时器。这就是内核一直采用的基于HZ的定时器机制。spa
为了不竞争,内核为每一个cpu定义了一个tvec_base结构指针,用来保存定时器。指针
struct tvec_base {视频
spinlock_t lock;索引
struct timer_list *running_timer;事件
wait_queue_head_t wait_for_running_timer;内存
unsigned long timer_jiffies;开发
unsigned long next_timer;it
struct tvec_root tv1;
struct tvec tv2;
struct tvec tv3;
struct tvec tv4;
struct tvec tv5;
} ____cacheline_aligned;
running_timer,该字段指向当前cpu正在处理的定时器所对应的timer_list结构。
timer_jiffies,该字段表示当前CPU定时器所经历过的jiffies数。大多数状况下,该数和jiffies计数值相等,若是cpu的idle状态连续持续了多个jiffies时,当退出idle状态时,jiffies计数值就会大于该字段,在接下来的tick中断后,定时器系统会让该字段等于jiffies值。
next_timer,该字段指向该CPU下一个即将到期的定时器。
tv1 -- tv5,这5个字段用于对定时器进行分组。实际上,tv1-tv5都是一个链表数组,其中tv1的数组大小为TVR_SIZE,tv2-tv5的大小为TVN_SIZE,根据CONFIG_BASE_SMALL配置项不一样,他们有不一样的大小。默认状况下,CONFIG_BASE_SMALL未使能,TVR_SIZE=256,TVN_SIZE=64。若系统内存不足,则可使能CONFIG_BASE_SMALL,此时TVR_SIZE=64,TVN_SIZE=16。
time wheel机制的工做原理相似于水表的工做原理。假定没有使能CONFIG_BASE_SMALL,此时tv1-tv5这5个链表数组的大小分别是256,64,64,64,64。因为tv1中的定时器会被最早处理而tv5中的定时器会被最后处理,咱们能够认为tv1-tv5分别占据一个32位数的不一样比特位,其中tv1占据最低的8位,tv2占据紧接着的6为,tv5占据最后的6位。
当注册一个定时器时,咱们能够获取定时器到期时间和所属cpu的tvec_base结构中timer_jiffies字段的差值,记为idx。以后比较idx与1<<8-1,1<<14-1, 1<<20-1, 1<<26-1, 1<<32-1的值,肯定定时器应该存放的链表数组。假设idx=4,则存放到tv1数组中。假定idx=500,则存放到tv2数组中。
当肯定了链表数组后,接着要肯定把该定时器放入数组的哪个链表中。若是idx的值小于256,则要被放入tv1中,因此能够简单的使用定时器到期时间timer_list.expires的低8位做为数组下标索引,放入tv1相应的链表中便可。若是idx的值在256-16383之间,则须要把定时器放入tv2链表数组中,因此可使用定时器到期时间timer_list.expires的8-14位做为数组的下标索引便可。tv3-tv5同理,即放入(timer_list.expires << (TVN_SIZE + n*TVR_SIZE)) & (n?TVR_MASK:TVN_MASK)做为下标索引的相应链表便可。
static void internal_add_timer(struct tvec_base *base, struct timer_list *timer)
{
unsigned long expires = timer->expires;
unsigned long idx = expires - base->timer_jiffies;
struct list_head *vec;
if (idx < TVR_SIZE) {
int i = expires & TVR_MASK;
vec = base->tv1.vec + i;
} else if (idx < 1 << (TVR_BITS + TVN_BITS)) {
int i = (expires >> TVR_BITS) & TVN_MASK;
vec = base->tv2.vec + i;
} else if (idx < 1 << (TVR_BITS + 2 * TVN_BITS)) {
int i = (expires >> (TVR_BITS + TVN_BITS)) & TVN_MASK;
vec = base->tv3.vec + i;
} else if (idx < 1 << (TVR_BITS + 3 * TVN_BITS)) {
int i = (expires >> (TVR_BITS + 2 * TVN_BITS)) & TVN_MASK;
vec = base->tv4.vec + i;
} else if ((signed long) idx < 0) {
/*
* Can happen if you add a timer with expires == jiffies,
* or you set a timer to go off in the past
*/
vec = base->tv1.vec + (base->timer_jiffies & TVR_MASK);
} else {
int i;
/* If the timeout is larger than 0xffffffff on 64-bit
* architectures then we use the maximum timeout:
*/
if (idx > 0xffffffffUL) {
idx = 0xffffffffUL;
expires = idx + base->timer_jiffies;
}
i = (expires >> (TVR_BITS + 3 * TVN_BITS)) & TVN_MASK;
vec = base->tv5.vec + i;
}
/*
* Timers are FIFO:
*/
list_add_tail(&timer->entry, vec);
}
定时器的添加,就是首先计算定时器与所属cpu的tvec_base->timer_jiffies的差值,再根据idx的值和定时器的到期时间将定时器放入tv1-tv5链表数组的某一链表中。