在linux操作系統中,定時器扮演著關鍵的角色,它們被用來執行各種延遲任務,像是廣泛使用的系統調用sleep()。該調用的背后就是基于定時器的機制。
Linux內部主要分為兩個類別的定時器:高精度定時器和低精度定時器。低精度定時器的工作原理是依托于硬件時鐘中斷,它的定時精度由HZ值決定,其表示每秒鐘時鐘中斷的次數。譬如,當內核的HZ設置為1000時,意味著每1毫秒會有一次時鐘中斷,這樣低精度定時器就能以1毫秒為最小的時間間隔來設定計時。相反,高精度定時器的精度更高,可以達到納秒級別,它的具體精度還和CPU的主頻緊密相關。
那么,為什么不全面采用高精度定時器呢?原因在于,盡管高精度定時器的時間精度更高,但其運行成本同樣更高。因此,對于那些對時間精度要求不是特別高的場合,采用低精度定時器是更加經濟且實用的選擇。
文章將著重講解Linux內核中低精度定時器的工作原理和具體實現。
時間輪
低精度定時器是基于時鐘中斷實現的,而時鐘中斷的觸發頻率是可以配置的,Linux 內核一般設置為每秒觸發 1000 次,也就是說每隔 1 毫秒就會觸發一次時鐘中斷。
一般來說,內核中可能會存在成千上萬個定時器,那么內核如何能夠快速找到將要到期的定時器呢?
在學習數據結構課程時,我們知道用于快速查找有序數據的數據結構有如何幾種:
由于這些數據結構的時間復雜度都是 log(n),對性能要求非常高的內核來說是不能接受的,所以內核使用了一種性能更高的數據結構:時間輪。
時間輪能夠保證在時間復雜度為 log(1) 的情況下找到將要到期的定時器,下面我們將會介紹時間輪的原理。
時間輪的基本思想是通過數組來保存定時器,而數組的索引就是定時器的過期時間。如下圖所示:
如上圖所示的數組中,索引為 1 的槽位存放的是 1 毫秒后超時的定時器列表,索引為 2 的槽位存放的是 2 毫秒后超時的定時器列表,如此類推…
此時,我們可以使用一個指針來指向超時的定時器列表,如下圖所示:
每當時鐘中斷被觸發一次,指針向下移動一位,這樣就能在時間復雜度為 log(1) 的情況下獲取到期的定時器。
這樣雖然能夠在時間復雜度為 log(1) 的情況下找到到期的定時器,但如果超時時間非常大的話,那么用于存儲定時器的數組也會非常巨大,如:定時器的超時時間為 4294967295 毫秒,那么將需要一個大小為 4294967296 的數組。
1. 存儲定時器
為了解決這個問題,內核使用 層級 的概念來減少數組占用的內存空間。其原理如下圖所示:
由于超時時間是一個整數(32 位整型),所以可以將其劃分為 5 個等級,每個級別使用一個數組來表示。例如第一級數組占用 8 個位,所以其大小為 256。而其他級別的數組都占用 6 個位,所以大小都為 64。
一個定時器被存放到哪個數組,是由其超時時間決定的,算法也非常簡單:如果第五級的值不為零,那么將會被存放到第五級數組中,而存放的位置以第五級的值作為索引。
例如,一個定時器的超時時間其第五級的值為 32,那么此定時器將會被存放到第五級數組的第 32 個槽位上。如下圖所示:
如果第五級的值為零,而第四級的值不為零,那么定時器將會被存放在第四級數組中,存放的位置以第四級的值作為索引,如此類推。
從上面的分析可以看出,定時器的超時時間越小,其存放的數組層級就越小。所以:
- 第一級數組存放的是超時時間范圍為 [0, 256) 毫秒的定時器。
- 第二級數組存放的是超時時間范圍為 [256, 16384) 毫秒的定時器(16384 = 256 * 64)。
- 第三級數組存放的是超時時間范圍為 [16384, 1048576) 毫秒的定時器(1048576 = 256 * 64 * 64)。
- 第四級數組存放的是超時時間范圍為 [1048576, 67108864) 毫秒的定時器(67108864 = 256 * 64 * 64 * 64)。
- 第五級數組存放的是超時時間范圍為 [67108864, 4294967296) 毫秒的定時器(4294967296 = 256 * 64 * 64 * 64 * 64)。
2. 執行定時器
接下來,我們將要分析內核是如何選擇到期的定時器來執行的。
如果所有定時器只存儲在一級數組中,那么選擇到期的定時器就非常簡單:由于數組每個槽位的索引對應著定時器的超時時間,所以只需要在時鐘中斷發生時,執行到期指針指向的定時器列表。執行完畢后,將到期指針移動到下一個位置即可。如下圖所示:
但對于定時器存儲在多級數組的情況,算法就變得復雜很多。
從上面的分析可知,第一級數組存放的是 0 ~ 255 毫秒后到期的定時器列表,而數組的索引對應的就是定時器的超時時間。如下圖所示:
而其他等級的數組,每個槽位存放的定時器其超時時間并不是一個固定的值,而是一個范圍,范圍與數組的等級和槽位的索引值有關,其計算方式為:
256?*?64^n?*?槽位索引?
在上面的公式中,n 的值等于 數組的等級 減去 2。所以對于第二級數組來說,其公式如下:
256?*?槽位索引?
第三級數組公式如下:
256?*?64?*?槽位索引?
第四和第五級數組如此類推。
由于內核不會使用索引為 0 的槽位,所以第二、第三級數組的定時器如下圖所示:
內核只會執行第一級數組中的定時器,每當時鐘中斷觸發時,會執行第一級數組 到期指針 指向的定時器列表,執行完畢后會將 到期指針 向下移動一位。如下圖所示:
當到期指針執行完最后一個槽位的定時器列表后,會重新移動到第一個槽位。
那么其他級別數組的定時器在什么時候才會被執行呢?其實對于其他級別的數組也有一個 到期指針,每當前一級別的數組執行完一輪后,當前級別數組的 到期指針 將會移動到下一個槽位,如:當第一級數組執行一輪后,第二級數組的 到期指針 將會移動到下一個槽位。
其他級別的數組(非第一級數組)移動 到期指針 時,會將指針指向的定時器列表從數組中刪除,并且重新添加到內核中。如下圖所示:
如上圖所示,第一級數組執行一輪后,內核將會把第二級數組的到期指針指向的定時器列表刪除,并且重新添加到內核中。然后,將會把到期指針移動到下一個槽位。
第三級數組也會在第二級數組執行一輪后,將其到期指針指向的定時器列表刪除,并且重新添加到內核中。接著將到期指針移動到下一個槽位,其他級別的數組如此類推。
源碼實現
接下來,我們將會分析 Linux 內核是如何實現低精度定時器的。由于高版本的內核其實現與上面介紹的原理有些區別,但基本原理是一致的,這里我們將使用 2.4.37 版本作為分析的對象。
1. 五個等級數組
如上面分析一致,在 Linux 內核中定義了 5 個數組來存放系統中的定時器,如下代碼所示:
struct?timer_vec?{ ?int?index;?????//?到期指針 ?struct?list_head?vec[64]; }; struct?timer_vec_root?{ ?int?index;?????//?到期指針 ?struct?list_head?vec[256]; }; static?struct?timer_vec?tv5; static?struct?timer_vec?tv4; static?struct?timer_vec?tv3; static?struct?timer_vec?tv2; static?struct?timer_vec_root?tv1;
上面代碼中,tv1、tv2、tv3、tv4、tv5 分別對應第一級、二級、三級、四級和五級數組。
通過代碼可知,數組元素的類型為鏈表,用于存放不同到期時間的定時器。另外,除了第一級數組的元素個數是 256 個外,其他級別的數組的元素個數都是 64 個。每個級別的數組都有一個到期指針,用于指向當前正在執行的定時器列表。
我們接著來看看內核怎么初始化這些數組的,內核調用 init_timervecs() 函數來初始化各級數組。代碼如下:
void?init_timervecs(void) { ????int?i; ????for?(i?=?0;?i?for?(i?=?0;?i?
init_timervecs() 主要通過 INIT_LIST_HEAD 宏來初始化各級數組的元素。
2. 定時器對象
在內核中,定時器使用 timer_list 對象來表示,其定義如下:
struct?timer_list?{ ????struct?list_head?list; ????unsigned?long?expires; ????unsigned?long?data; ????void?(*function)(unsigned?long); };
下面介紹一下 timer_list 對象各個字段的作用:
我們要向內核添加一個定時器時,需要先創建一個 timer_list 對象,并且設置其到期時間和回調函數。
3. 添加定時器
在內核中,可以使用 add_timer() 函數來添加一個定時器。其代碼如下所示:
void?add_timer(struct?timer_list?*timer) { ????unsigned?long?flags; ????//?上鎖 ????spin_lock_irqsave(&timerlist_lock,?flags); ????... ????//?向內核添加定時器 ????internal_add_timer(timer); ????//?解鎖 ????spin_unlock_irqrestore(&timerlist_lock,?flags); ????return; }
從上面代碼可以看出,add_timer() 函數主要通過調用 internal_add_timer() 函數來添加定時器。我們繼續來分析 internal_add_timer() 函數的實現,代碼如下:
static?inline?void?internal_add_timer(struct?timer_list?*timer) { ????unsigned?long?expires?=?timer->expires; ????unsigned?long?idx?=?expires?-?timer_jiffies;?//?多少毫秒數后到期 ????struct?list_head?*?vec; ????if?(idx?else?if?(idx?>?TVR_BITS)?&?TVN_MASK; ????????vec?=?tv2.vec?+?i; ????}?else?if?(idx?>?(TVR_BITS?+?TVN_BITS))?&?TVN_MASK; ????????vec?=??tv3.vec?+?i; ????}?else?if?(idx?>?(TVR_BITS?+?2?*?TVN_BITS))?&?TVN_MASK; ????????vec?=?tv4.vec?+?i; ????}?else?if?((signed?long)?idx?else?if?(idx?>?(TVR_BITS?+?3?*?TVN_BITS))?&?TVN_MASK; ????????vec?=?tv5.vec?+?i; ????}?else?{ ????????INIT_LIST_HEAD(&timer->list); ????????return; ????} ????list_add(&timer->list,?vec->prev); }
internal_add_timer() 函數首先會計算定時器還有多少毫秒到期,然后按照到期的毫秒數來選擇對應的級別數組:
- 如果到期時間小于256毫秒,那么將會添加到第一級數組中。
- 如果到期時間大于等于256毫秒,并且小于16384毫米,那么將會添加到第二級數組中。
- 其他等級如此類推。
選擇到合適的數組后,內核會調用 list_add() 函數將定時器添加到對應槽位的鏈表中。
4. 執行到期的定時器
內核會在時鐘中斷中通過調用 run_timer_list() 函數來執行到期的定時器,其實現如下:
static?inline?void?run_timer_list(void) { ????... ????while?((long)(jiffies?-?timer_jiffies)?>=?0)?{ ????????struct?list_head?*head,?*curr; ????????//?1.?如果第一級數組已經執行完一輪(到期指針變為0) ????????if?(!tv1.index)?{ ????????????int?n?=?1; ????????????do?{ ????????????????cascade_timers(tvecs[n]); ????????????}?while?(tvecs[n]->index?==?1?&&?++n?next; ????????if?(curr?!=?head)?{ ????????????struct?timer_list?*timer; ????????????void?(*fn)(unsigned?long); ????????????unsigned?long?data; ????????????timer?=?list_entry(curr,?struct?timer_list,?list); ????????????fn?=?timer->function; ????????????data=?timer->data; ????????????//?4.?把定時器從鏈表中刪除 ????????????detach_timer(timer); ????????????timer->list.next?=?timer->list.prev?=?NULL; ????????????timer_enter(timer); ????????????spin_unlock_irq(&timerlist_lock); ????????????//?5.?執行定時器的回調函數 ????????????fn(data); ????????????spin_lock_irq(&timerlist_lock); ????????????timer_exit(); ????????????goto?repeat; ????????} ????????++timer_jiffies; ????????//?6.?將到期指針移動一個位置 ????????tv1.index?=?(tv1.index?+?1)?&?TVR_MASK; ????} ????... }
run_timer_list() 函數主要按照以下步驟來執行到期的定時器:
- 如果第一級數組已經執行完一輪(也就是說,到期指針變為0時),通過調用 cascade_timers() 函數來計算其他等級當前到期指針指向的定時器列表(重新添加到內核中)。
- 遍歷第一級數組的到期指針指向的定時器列表。
- 把定時器從鏈表中刪除。
- 執行定時器的回調函數。
- 將到期指針移動一個位置。
從時間輪的原理可知,每當某一級數組執行完一輪后,就會移動下一級數組的到期指針,并且將指針指向的定時器列表重新添加到內核中,這個過程由 cascade_timers() 函數完成。代碼如下所示:
static?inline?void?cascade_timers(struct?timer_vec?*tv) { ????struct?list_head?*head,?*curr,?*next; ????head?=?tv->vec?+?tv->index; ????curr?=?head->next; ????//?1.?遍歷定時器列表 ????while?(curr?!=?head)?{ ????????struct?timer_list?*tmp; ????????tmp?=?list_entry(curr,?struct?timer_list,?list); ????????next?=?curr->next; ????????list_del(curr); ????????//?2.?將定時器重新添加到內核中 ????????internal_add_timer(tmp); ????????curr?=?next; ????} ????INIT_LIST_HEAD(head); ????//?3.?將到期指針移動到下一個位置 ????tv->index?=?(tv->index?+?1)?&?TVN_MASK; }
總結
本文主要介紹低精度定時器的實現,低精度定時器是一種比較廉價(占用資源較低)的定時器,如果對定時器的到期時間精度不太高的情況下,可以優先使用低精度定時。