常用的定時器實現演算法有兩種:紅黑樹和時間輪(timing wheel)。
在linux2.6的**中,kernel/timer.c檔案實現了乙個通用定時器機制,使用的是時間輪演算法。
每乙個cpu都有乙個struct tvec_base結構,代表這個cpu使用的時間輪。
struct tvec_base
struct tvec_root與struct tvec都是陣列,陣列中的每一項都指定乙個鍊錶。struct tvec_root定義的陣列大小是256(2的8次方);struct tvec_root定義的陣列大小是64(2的6次方)。所以,tv1~6定義的陣列總大小是2的(8 + 4*6 = 32)次方,正好對應32位處理器中jiffies的定義(unsigned long)。
因為使用的是wheel演算法,tv1~5就代表5個wheel。
tv1是轉速最快的wheel,所有在256個jiffies內到期的定時器都會掛在tv1的某個煉表頭中。
tv2是轉速第二快的wheel,裡面掛的定時器超時jiffies在2^8 ~ 2^(8+6)之間。
tv3是轉速第三快的wheel,超時jiffies在2^(8+6) ~ 2^(8+2*6)之間。
tv4、tv5類似。
增加timer分兩步,先是呼叫init_timer初始化乙個struct timer_list結構,然後呼叫add_timer把timer掛到5個wheel中包含的某乙個定時器佇列中。
init_timer函式初始化timer_list中的一些域。
static void __init_timer(struct timer_list *timer)
add_timer是__mod_timer函式的乙個封裝,__mod_timer最終會呼叫internal_add_timer。
static void internal_add_timer(struct tvec_base *base, struct timer_list *timer)
// 如果idx少於2^(8+6)次方,加入tv2
else if (idx < 1 << (tvr_bits + tvn_bits))
// 類推,加入tv3
else if (idx < 1 << (tvr_bits + 2 * tvn_bits))
// 類推,加入tv4
else if (idx < 1 << (tvr_bits + 3 * tvn_bits))
// 如果idx < 0,說明定時器已經超時,應該馬上被排程,把它加入base->timer_jiffies, // 即base的當前jiffies對應的佇列。這裡要注意的是,這個函式在spin_lock_irq被調 // 用之後呼叫,所以不會與定時器排程函式產生競態。
else if ((signed long) idx < 0)
// 加入tv5佇列。
else
i = (expires >> (tvr_bits + 3 * tvn_bits)) & tvn_mask;
vec = base->tv5.vec + i; }
trace_timer_set(timer);
/** timers are fifo:
*/list_add_tail(&timer->entry, vec); }
timer的排程在軟中斷中完成,在init_timers函式中呼叫
open_softirq(timer_softirq, run_timer_softirq)
註冊定時器對應的軟中斷處理函式。
static void run_timer_softirq(struct softirq_action *h)
static inline void __run_timers(struct tvec_base *base)
spin_lock_irq 加鎖,關中斷,保證與定時器新增函式之間不會產生競態
while (time_after_eq(jiffies, base->timer_jiffies))
// index代表這個迴圈需要處理的定時器在陣列
tv1中的下標
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);
// 處理佇列中的每乙個定時器。注意在呼叫定時器中的timer->function之前,會先 // 呼叫spin_unlock_irq,呼叫完之後再重新用spin_lock_irq加鎖。這是為了提高cpu的效率。另外,在處理某個定時器時, // 會呼叫set_running_timer把當前正在執行的定時器標記到base->running_timer // 中。
……// 函式退出,標記現在base中沒有正在執行的定時器
set_running_timer(base, null);
spin_unlock_irq // 解鎖
在時間輪演算法中,隨著時間推移,定時器到期的時間越來越近,時間輪也隨著時間不停轉動。當最快的輪轉到某乙個觸發點,就將次快輪的某一佇列搬到最快輪。次塊輪轉到某一觸發點,把下一級時間輪的某一佇列再往上搬到次快輪。依此類推。所以排隊中的定時器就是這樣一級一級往上,直到在最快輪中被處理。
在這裡,這些搬移由上節描述的__run_timers函式的下列**體現:
// index代表這個迴圈需要處理的定時器在陣列
tv1中的下標
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));
當jiffies的低8位為0時,tv1到了觸發點,呼叫cascade函式把tv2的某一格定時器搬到tv1。tv2的cascade函式返回0時,代表tv2也到了觸發點,於是又觸發下一級時間輪tv3。依此類推。
這裡index的定義為:
#define index(n) ((base->timer_jiffies >> (tvr_bits + (n) * tvn_bits)) & tvn_mask)
可以看出這個巨集返回的是時間輪中某一格的索引:
index(0)返回tv2的索引。
index(1)返回tv3的索引。
index(2)返回tv4的索引。
index(3)返回tv5的索引。
static int cascade(struct tvec_base *base, struct tvec *tv, int index)
// 將tv陣列中以index為下標的定時器佇列取出,準備搬移
list_replace_init(tv->vec + index, &tv_list);
// 根據超時時間,將佇列中的定時器重新排隊。定時器往上一級時間輪排隊的過程就 // 在這裡發生。
list_for_each_entry_safe(timer, tmp, &tv_list, entry)
Linux的動態定時器 時間輪
定時器 有時也稱為動態定時器或核心定時器 是管理核心時間的基礎。定時器是一種軟體功能,即允許在將來的某個時刻,函式在給定的時間間隔用完時被呼叫。注意的是定時器並不會週期執行,它在超時後就自行銷毀,這也是定時器被稱為動態定時器的乙個原因。動態定時器不斷地建立和銷毀,而且它的執行次數也不受限制。定時器在...
linux定時器時間輪演算法詳解
linux高併發程式設計 紅黑樹實現定時器 時間輪實現定時器 linux多執行緒環境下海量定時任務的定時器設計 linux定時器分為低精度定時器和高精度定時器兩種型別,核心對其均有實現。本文討論的是我們在應用程式開發中比較常見的低精度定時器。作為常用的基礎元件,定時器常用的幾種實現方法包括 基於排序...
基於時間輪的定時器
目錄 是乙個單層時間輪,當指標走到某一格上,就獲取那一格上掛的任務將其執行。當時如果時間跨度大的時候,格仔數明顯不夠,那麼就可以做成多級時間輪。其實就是當低層的時間輪走了一圈,將它高一層的時間輪走一格,並且將掛在高層時間輪上的任務分配下來。檔案 include timewheel.h include...