3 定时器
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子系统。
3.1 time wheel
在Linux 2.6.16之前,内核一致使用一种被称为time wheel的机制来管理定时器。这就是内核一直采用的基于HZ的定时器机制。
为了避免竞争,内核为每个cpu定义了一个tvec_base结构指针,用来保存定时器。
3.1.1 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;
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。
3.1.2 time wheel机制
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链表数组的某一链表中。