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鏈表數組的某一鏈表中。