定時器層是基於Tick層之上的,是根據系統jiffies來觸發的,精度相對比較低。利用定時器,我們可以設定在未來的某一時刻,觸發一個特定的事件。經常,也會把這種低精度定時器稱作時間輪(Timer Wheel)。
在內核中,一個定時器是使用timer_list結構體來表示的:
struct timer_list {
struct hlist_node entry;
unsigned long expires;
void (*function)(struct timer_list *);
u32 flags;
......
};
- entry:所有的定時器都會根據到期的時間被分配到一組鏈表中的一箇中,該字段是鏈表的節點成員。
- expires:字段指出了該定時器的到期時刻,也就是期望定時器到期時刻的jiffies計數值。這是一個絕對值,不是距離當前時刻再過多少jiffies。
- function:是一個回調函數指針,定時器到期時,系統將會調用該函數,用於響應該定時器的到期事件。
- flags:看名字應該是標誌位,其定義如下:
#define TIMER_CPUMASK 0x0003FFFF
#define TIMER_MIGRATING 0x00040000
#define TIMER_BASEMASK (TIMER_CPUMASK | TIMER_MIGRATING)
#define TIMER_DEFERRABLE 0x00080000
#define TIMER_PINNED 0x00100000
#define TIMER_IRQSAFE 0x00200000
#define TIMER_ARRAYSHIFT 22
#define TIMER_ARRAYMASK 0xFFC00000
可以看到,其實並不是標誌位那麼簡單。其最高10位記錄了定時器放置到桶的編號,後面會提到一共最多隻有576個桶,所以10位足夠了。而最低的18位指示了該定時器綁定到了哪個CPU上,注意是一個數值,而不是位圖。夾在中間的一些位到真的是一些標誌位。TIMER_MIGRATING表示定時器正在從一個CPU遷移到另外一個CPU。TIMER_DEFERRABLE表示該定時器是可延遲的。TIMER_PINNED表示定時器已經綁死了當前的CPU,無論如何都不會遷移到別的CPU上。TIMER_IRQSAFE表示定時器是中斷安全的,使用的時候只需要加鎖,不需要關中斷。
系統中可能同時存在成千上萬個定時器,如果處理不好效率會非常低下。Linux目前會將定時器按照綁定的CPU和種類(普通定時器還是可延遲定時器兩種)進行區分,由timer_base結構體組織起來:
struct timer_base {
raw_spinlock_t lock;
struct timer_list *running_timer;
......
unsigned long clk;
unsigned long next_expiry;
unsigned int cpu;
bool is_idle;
bool must_forward_clk;
DECLARE_BITMAP(pending_map, WHEEL_SIZE);
struct hlist_head vectors[WHEEL_SIZE];
} ____cacheline_aligned;
- lock:保護該timer_base結構體的自旋鎖,這個自旋鎖還同時保護包含在vectors鏈表數組中的所有定時器。
- running_timer:該字段指向當前CPU正在處理的定時器所對應的timer_list結構。
- clk:當前定時器所經過的 jiffies,用來判斷包含的定時器是否已經到期或超時。
- next_expiry:該字段指向該CPU下一個即將到期的定時器。最早 (距離超時最近的 timer) 的超時時間
- cpu:所屬的CPU號。
- is_idle:指示是否處於空閒模式下,在NO_HZ模式下會用到。
- must_forward_clk:指示是否需要更新當前clk的值,在NO_HZ模式下會用到。
- pending_map:一個比特位圖,時間輪中有幾個桶就有幾個比特位。如果某個桶內有定時器存在,那麼就將相應的比特位置1。
- vectors:時間輪所有桶的數組,每一個元素是一個鏈表。
每個CPU都含有一到兩個timer_base結構體變量:
static DEFINE_PER_CPU(struct timer_base, timer_bases[NR_BASES]);
其中NR_BASES定義如下:
#ifdef CONFIG_NO_HZ_COMMON
# define NR_BASES 2
# define BASE_STD 0
# define BASE_DEF 1
#else
# define NR_BASES 1
# define BASE_STD 0
# define BASE_DEF 0
#endif
所以如果內核編譯選項包含CONFIG_NO_HZ_COMMON,則每個CPU有兩個timer_base結構體,下標分別是BASE_STD(Standard)和BASE_DEF(Deferrable)。如果內核編譯選項沒有包含CONFIG_NO_HZ_COMMON,那麼每個CPU只有一個timer_base結構體,BASE_STD和BASE_DEF是同一個。
爲什麼支持NO_HZ模式要包含兩個timer_base呢?這其實和NO_HZ的工作模式有關。如果NO_HZ模式,那麼當CPU處於空閒狀態時,定時器層是收不到也不需要收到任何Tick的,這樣可以節省電力。這時候底層的Tick層(準確說是Tick Sched)不會按照預定好的HZ頻率,每次到期後都去不停的設置底層的定時事件設備(啓動NO_HZ模式的前提是已經切換到了高精度模式下而高精度模式又要求定時事件設備是單次觸發模式的)。但是,如果定時器到期了不就錯過去了嘛。所以,在停止Tick之前,Tick層會從定時器層獲得最近的下一次定時器到期的時間(通過調用get_next_timer_interrupt函數),然後對下面的定時事件設備進行編程,讓其在這個最近的到期時刻到期,觸發中斷。但是,系統中有很多定時器,它們對到期的要求沒有那麼嚴格,遲一點到期也不是很要緊。對於這類定時器,在停止Tick之前,就沒必要管他們到低什麼時候到期。具體點說,就是Tick層在向定時器層詢問下一次最近到期時間時,定時器層更本就不會查找這些可延遲的定時器。對於前面說的第一種定時器存放在BASE_STD指明的那個timer_base結構體裏面,而第二種定時器存放在BASE_DEF指明的那個timer_base結構體裏面。如果在編譯內核的時候沒有包含CONFIG_NO_HZ_COMMON,也就是內核不支持NO_HZ模式,Tick從來就沒有停止過,當然就不存在前面說的問題,也就沒必要分兩個了。
如果在內核配置文件裏定義Tick週期是100的話,一共有8個級別(編號從0到7);而如果大於100的話,則一共會包含9個級別(編號從0到8)。
#if HZ > 100
# define LVL_DEPTH 9
# else
# define LVL_DEPTH 8
#endif
一個級(Level)裏面共有64(LVL_SIZE)個桶(Bucket),用6個比特表示:
#define LVL_BITS 6
#define LVL_SIZE (1UL << LVL_BITS)
#define LVL_MASK (LVL_SIZE - 1)
#define LVL_OFFS(n) ((n) * LVL_SIZE)
宏LVL_OFFS定義了每一級桶下表的起始編號。
所以,對於每個timer_base一共需要的桶的數目定義爲:
#define WHEEL_SIZE (LVL_SIZE * LVL_DEPTH)
還有一個概念叫做粒度(Granularity),表示系統至少要過多少個Tick纔會檢查某一個級裏面的所有定時器。
每一級的64個桶的檢查粒度是一樣的,而不同級內的桶之間檢查的粒度不同,級數越小,檢查粒度越細。每一級粒度的Tick數由宏定義LVL_CLK_DIV的值決定:
#define LVL_CLK_SHIFT 3
#define LVL_CLK_DIV (1UL << LVL_CLK_SHIFT)
#define LVL_CLK_MASK (LVL_CLK_DIV - 1)
#define LVL_SHIFT(n) ((n) * LVL_CLK_SHIFT)
#define LVL_GRAN(n) (1UL << LVL_SHIFT(n))
具體的計算公式爲:
也就是第0級內64個桶中存放的所有定時器每個Tick都會檢查,第1級內64個桶中存放的所有定時器每8個Tick纔會檢查,第2級內64個桶中存放的所有定時器每64個Tick纔會檢查,以此類推。
對應每一個級,都有一個範圍,其起始的Tick值由LVL_START定義:
#define LVL_START(n) ((LVL_SIZE - 1) << (((n) - 1) * LVL_CLK_SHIFT))
這裏n從1開始,取值範圍1到7或1到8。不過這個定義貌似有問題,應該是:
#define LVL_START(n) ((LVL_SIZE) << (((n) - 1) * LVL_CLK_SHIFT))
下面具體舉個例子,內核配置選項將HZ配置位250,那麼就一共需要9個級別,每個級別裏面有64個桶,所以一共需要576個桶。每個級別的情況如下表:
級(Level) | 編號偏移 | 粒度(Granularity) | 差值範圍(Tick) |
0 | 0 | 1 Tick(4 ms) | 0 ~ 63 |
1 | 64 | 8 Ticks(32 ms) |
64 ~ 511 |
2 | 128 | 64 Ticks(256 ms) | 512 ~ 4095 |
3 | 192 | 512 Ticks(2048 ms) | 4096 ~ 32767 |
4 | 256 | 4096 Ticks(16384 ms) | 32768 ~ 262143 |
5 | 320 | 32768 Ticks(131072 ms) | 262144 ~ 2097151 |
6 | 384 | 262144 Ticks(1048576 ms) | 2097152 ~ 16777215 |
7 | 448 | 2097152 Ticks(8388608 ms) | 16777216 ~ 134217727 |
8 | 512 | 16777216 Ticks(67108864 ms) | 134217728 ~ 1073741822 |
因爲配置的是250Hz,所以每次Tick之間經過4毫秒。可以看出來,定時到期時間距離現在越久,那粒度就越差,誤差也越大。
具體將定時器放到哪一個級下面是由到期時間距離現在時間的差值,也就是距離現在還要過多長時間決定的;而要放到哪個桶裏面,則單純是由到期時間決定的。
所以,綜上所述,定時器層的數據結構如下圖所示:
下面分場景介紹一下定時器層的工作過程。
1)桶編號的計算
calc_wheel_index函數根據到期jiffies和已經過jiffies兩個參數,計算要將定時器放置到哪個桶下:
static int calc_wheel_index(unsigned long expires, unsigned long clk)
{
/* 到期jiffies和已經過jiffies的差 */
unsigned long delta = expires - clk;
unsigned int idx;
/* 按照差所處的範圍來決定把定時器放到哪一級 */
if (delta < LVL_START(1)) {
idx = calc_index(expires, 0);
} else if (delta < LVL_START(2)) {
idx = calc_index(expires, 1);
} else if (delta < LVL_START(3)) {
idx = calc_index(expires, 2);
} else if (delta < LVL_START(4)) {
idx = calc_index(expires, 3);
} else if (delta < LVL_START(5)) {
idx = calc_index(expires, 4);
} else if (delta < LVL_START(6)) {
idx = calc_index(expires, 5);
} else if (delta < LVL_START(7)) {
idx = calc_index(expires, 6);
} else if (LVL_DEPTH > 8 && delta < LVL_START(8)) {
idx = calc_index(expires, 7);
} else if ((long) delta < 0) {
idx = clk & LVL_MASK;
} else {
/* 如果差值過大強制限定過期時間到WHEEL_TIMEOUT_MAX */
if (expires >= WHEEL_TIMEOUT_CUTOFF)
expires = WHEEL_TIMEOUT_MAX;
idx = calc_index(expires, LVL_DEPTH - 1);
}
return idx;
}
可以看到,將定時器放到時間輪的哪一級是由距離現在還要過多長時間(準確的說是過多少jiffies)纔到期決定的。該函數首先計算到期jiffies和當前已經過jiffies的差值。然後,根據差值的範圍,決定放置到哪一個級別的桶內。如果差值爲負代表其實定時器已經過期了,就把它放到最低級別(0)內,反正再過一個Tick就能檢查到了。如果差值過大,強制限定到期時間到WHEEL_TIMEOUT_MAX,並將其放置到最後一級。定好級後,最後,調用calc_index函數,計算具體放置到的桶下標。
static inline unsigned calc_index(unsigned expires, unsigned lvl)
{
expires = (expires + LVL_GRAN(lvl)) >> LVL_SHIFT(lvl);
return LVL_OFFS(lvl) + (expires & LVL_MASK);
}
通過LVL_OFFS宏計算出對應該級的桶起始下標,每一級下面有64個桶,具體放到哪個桶下面是根據級號取到期時間的特定6位決定的。可以看到,最終的結果還要加1,因爲定時器不會在到期時間之前被觸發,所以放到下一個。在某個級之內,每個桶之間的的定時器到期時間相差一個該級的粒度。
2)通過定時器找到對應的timer_base結構體
定時器層一般調用lock_timer_base函數,找到定時器所對應的timer_base結構體,同時獲得timer_base結構體內的自旋鎖並關閉中斷:
static struct timer_base *lock_timer_base(struct timer_list *timer,
unsigned long *flags)
__acquires(timer->base->lock)
{
for (;;) {
struct timer_base *base;
u32 tf;
/* 讀取定時器的標誌位 */
tf = READ_ONCE(timer->flags);
/* 如果定時器沒有正在遷移中 */
if (!(tf & TIMER_MIGRATING)) {
/* 通過標誌位中的CPU號來獲得timer_base結構體 */
base = get_timer_base(tf);
raw_spin_lock_irqsave(&base->lock, *flags);
/* 在這期間定時器的標誌位是否發生了變化 */
if (timer->flags == tf)
return base;
raw_spin_unlock_irqrestore(&base->lock, *flags);
}
cpu_relax();
}
}
該函數會獲得定時器內的標誌字段,判斷其是不是正在遷移的過程中,如果是的話就像自旋鎖一樣循環等待其完成。如果沒有在遷移,則調用get_timer_base函數,通過標誌位中的CPU號來獲得timer_base結構體。在獲得了自旋鎖並關閉中斷之後,還要判斷一下定時器當前的標誌位是否和之前讀取的相同,如果不同則釋放鎖,再走一次循環,否則直接返回找到的timer_base結構體。注意,lock_timer_base函數返回時,是已經持有了timer_base內的自旋鎖,並且本地中斷是關閉的。
我們接着來看看get_timer_base函數:
static inline struct timer_base *get_timer_base(u32 tflags)
{
return get_timer_cpu_base(tflags, tflags & TIMER_CPUMASK);
}
因爲定時器的flags字段包含了CPU號的信息,所以直接取出來,然後調用get_timer_cpu_base函數:
static inline struct timer_base *get_timer_cpu_base(u32 tflags, u32 cpu)
{
/* 獲得BASE_STD編號的timer_base結構體 */
struct timer_base *base = per_cpu_ptr(&timer_bases[BASE_STD], cpu);
/* 如果設置了CONFIG_NO_HZ_COMMON內核編譯選項並且定時器是可延遲的話 */
if (IS_ENABLED(CONFIG_NO_HZ_COMMON) && (tflags & TIMER_DEFERRABLE))
/* 獲得BASE_DEF編號的timer_base結構體 */
base = per_cpu_ptr(&timer_bases[BASE_DEF], cpu);
return base;
}
這個函數就很簡單了,直接通過CPU號找到對應的Per CPU變量timer_bases。前面提到了,如果編譯選項中包含NO_HZ的支持,則timer_bases其實包含了兩個timer_base結構體,一個給標準的定時器,一個給可延遲的定時器。所以,該函數會判斷定時器是否是可延遲的,如果不是或者不支持NO_HZ則返回BASE_STD編號的timer_base結構體;如果定時器是可延遲的,並且內核支持NO_HZ模式,則需要返回BASE_DEF編號的timer_base結構體。
3)定時器的刪除
定時器的刪除是通過調用函數del_timer實現的:
int del_timer(struct timer_list *timer)
{
struct timer_base *base;
unsigned long flags;
int ret = 0;
debug_assert_init(timer);
/* 判斷定時器是否已經被添加進某個鏈表中了 */
if (timer_pending(timer)) {
/* 找到定時器對應的timer_base結構體並對其上鎖 */
base = lock_timer_base(timer, &flags);
/* 從鏈表中解除定時器 */
ret = detach_if_pending(timer, base, true);
raw_spin_unlock_irqrestore(&base->lock, flags);
}
return ret;
}
EXPORT_SYMBOL(del_timer);
先調用timer_pending函數判斷定時器是否還存在於某個鏈表中,如果已經不在任何鏈表中了,證明已經被刪除了,直接返回。
static inline int timer_pending(const struct timer_list * timer)
{
return timer->entry.pprev != NULL;
}
這個函數就是檢查定時器內的鏈表元素的向前指針是否是空指針,也就意味着該定時器沒有被添加進任何鏈表中。
如果還存在於某個鏈表中,則繼續執行刪除的動作。先通過定時器找到對應的timer_base結構體並上鎖,然後調用detach_if_pending函數將定時器從鏈表中解除,最後釋放鎖並返回。
static int detach_if_pending(struct timer_list *timer, struct timer_base *base,
bool clear_pending)
{
/* 獲得存放定時器的桶編號 */
unsigned idx = timer_get_idx(timer);
/* 判斷定時器是否已經被添加進某個鏈表中了 */
if (!timer_pending(timer))
return 0;
/* 如果對應的桶中只有當前這一個定時器則清除pending_map對應位 */
if (hlist_is_singular_node(&timer->entry, base->vectors + idx))
__clear_bit(idx, base->pending_map);
/* 從鏈表中解除定時器 */
detach_timer(timer, clear_pending);
return 1;
}
該函數先調用timer_get_idx函數從定時器的flags字段中抽取出存放定時器的桶編號((timer->flags & TIMER_ARRAYMASK) >> TIMER_ARRAYSHIFT),接着再次判斷定時器是否已經被解除,如果仍然沒有還需要判斷當前需要解除的定時器是否是對應桶內的最後一個定時器,如果是的話要將對應timer_base結構體內的pending_map變量中的對應標誌位清0。最後調用detach_timer函數正式解除定時器:
static inline void detach_timer(struct timer_list *timer, bool clear_pending)
{
struct hlist_node *entry = &timer->entry;
debug_deactivate(timer);
/* 將定時器從鏈表中刪除 */
__hlist_del(entry);
if (clear_pending)
entry->pprev = NULL;
entry->next = LIST_POISON2;
}
detach_timer就完全是鏈表的操作了,想將自己從對應鏈表中刪除,如果設置了clear_pending的話,將entry的前向指針設置位空(前面說的timer_pending函數就是通過這個來判斷定時器是否已經添加進某個鏈表中的),後向指針設置爲LIST_POISON2。
4)定時器的添加和修改
要向系統中添加一個定時器,需要調用add_timer函數:
void add_timer(struct timer_list *timer)
{
BUG_ON(timer_pending(timer));
mod_timer(timer, timer->expires);
}
先調用timer_pending函數,看要添加的定時器是否已經被添加過了,如果已經添加過了,會報內核錯誤。接着調用了mod_timer函數:
int mod_timer(struct timer_list *timer, unsigned long expires)
{
return __mod_timer(timer, expires, 0);
}
EXPORT_SYMBOL(mod_timer);
mod_timer函數只是簡單封裝了一下__mod_timer函數,後者是定時器層的核心函數,後面我們會分析。
如果我們要修改一個已經存在的定時器,比如說減小其到期時間,要使用timer_reduce函數:
int timer_reduce(struct timer_list *timer, unsigned long expires)
{
return __mod_timer(timer, expires, MOD_TIMER_REDUCE);
}
EXPORT_SYMBOL(timer_reduce);
其也最終會調用__mod_timer函數。該函數有三個參數,第一個是要添加或修改的定時器;第二個是到期時間,如果是新添加的定時器,就將其設置成定時器自己的到期時間;第三個參數是模式,目前系統中共有兩個:
#define MOD_TIMER_PENDING_ONLY 0x01
#define MOD_TIMER_REDUCE 0x02
MOD_TIMER_PENDING_ONLY表示本次修改只針對還存在在系統內的定時器,如果定時器已經被刪除了則不會再將其激活。MOD_TIMER_REDUCE則表示本次修改只會將定時器的到期值減小。
下面我們來重點分析一下__mod_timer函數:
static inline int
__mod_timer(struct timer_list *timer, unsigned long expires, unsigned int options)
{
struct timer_base *base, *new_base;
unsigned int idx = UINT_MAX;
unsigned long clk = 0, flags;
int ret = 0;
/* 定時器的回調函數必須不爲空 */
BUG_ON(!timer->function);
/* 定時器是否已經被添加進某個鏈表中 */
if (timer_pending(timer)) {
/* 計算定時器的到期時間和參數到期時間之間的差值 */
long diff = timer->expires - expires;
/* 如果兩個差值爲0即相同則直接返回成功什麼都不用做 */
if (!diff)
return 1;
/* 如果是要減定時器到期時間但是傳入的到期時間比定時器當前的到期時間還大則直接返回成功 */
if (options & MOD_TIMER_REDUCE && diff <= 0)
return 1;
/* 找到定時器對應的timer_base結構體並對其上鎖 */
base = lock_timer_base(timer, &flags);
/* 試着更新timer_base中的clk數 */
forward_timer_base(base);
/* 如果是要減定時器到期時間但是傳入的到期時間比定時器當前的到期時間還大則直接返回成功 */
if (timer_pending(timer) && (options & MOD_TIMER_REDUCE) &&
time_before_eq(timer->expires, expires)) {
ret = 1;
goto out_unlock;
}
clk = base->clk;
/* 計算要放置到的桶下標 */
idx = calc_wheel_index(expires, clk);
/* 如果定時器修改之後還是放在原來的那個桶下 */
if (idx == timer_get_idx(timer)) {
/* 如果選項不是MOD_TIMER_REDUCE則直接修改定時器的到期時間 */
if (!(options & MOD_TIMER_REDUCE))
timer->expires = expires;
/* 如果選項是MOD_TIMER_REDUCE則還要比較新老到期時間再修改 */
else if (time_after(timer->expires, expires))
timer->expires = expires;
ret = 1;
goto out_unlock;
}
} else {
/* 找到定時器對應的timer_base結構體並對其上鎖 */
base = lock_timer_base(timer, &flags);
/* 試着更新timer_base中的clk數 */
forward_timer_base(base);
}
/* 將定時器從當前鏈表中移除 */
ret = detach_if_pending(timer, base, false);
/* 如果定時器不在任何鏈表中且設置了MOD_TIMER_PENDING_ONLY選項則直接返回 */
if (!ret && (options & MOD_TIMER_PENDING_ONLY))
goto out_unlock;
/* 獲得系統指定的最合適的timer_base結構體 */
new_base = get_target_base(base, timer->flags);
/* 如果定時器指定的和系統挑選的timer_base結構體不一直則可能需要遷移 */
if (base != new_base) {
/* 如果定時器不是當前timer_base中正在處理的定時器 */
if (likely(base->running_timer != timer)) {
/* 設置TIMER_MIGRATING標記位 */
timer->flags |= TIMER_MIGRATING;
/* 釋放遷移出的timer_base結構體的自旋鎖 */
raw_spin_unlock(&base->lock);
base = new_base;
/* 獲取遷移進的timer_base結構體的自旋鎖 */
raw_spin_lock(&base->lock);
/* 寫入新的CPU號並清除TIMER_MIGRATING標記位 */
WRITE_ONCE(timer->flags,
(timer->flags & ~TIMER_BASEMASK) | base->cpu);
/* 試着更新timer_base中的clk數 */
forward_timer_base(base);
}
}
debug_timer_activate(timer);
/* 更新定時器的到期時間 */
timer->expires = expires;
/* 如果桶下標已經計算過了且timer_base的clk沒變(也意味着桶下標沒變) */
if (idx != UINT_MAX && clk == base->clk) {
/* 將定時器加入對應桶中 */
enqueue_timer(base, timer, idx);
trigger_dyntick_cpu(base, timer);
} else {
/* 重新計算桶下標並添加進去 */
internal_add_timer(base, timer);
}
out_unlock:
/* 釋放timer_base結構體的自旋鎖並開中斷 */
raw_spin_unlock_irqrestore(&base->lock, flags);
return ret;
}
首先,可以看到該函數在獲得了定時器對應的timer_base結構體後,都需要調用forward_timer_base函數更新timer_base結構體中的clk變量:
static inline void forward_timer_base(struct timer_base *base)
{
#ifdef CONFIG_NO_HZ_COMMON
unsigned long jnow;
/* 必須設置了must_forward_clk */
if (likely(!base->must_forward_clk))
return;
/* 獲得當前的jiffies */
jnow = READ_ONCE(jiffies);
base->must_forward_clk = base->is_idle;
/* 如果當前jiffies和clk變量之間的差值小於2證明當前CPU沒有進入空閒模式 */
if ((long)(jnow - base->clk) < 2)
return;
if (time_after(base->next_expiry, jnow))
base->clk = jnow;
else
base->clk = base->next_expiry;
#endif
}
forward_timer_base函數只有在內核在編譯時打開CONFIG_NO_HZ_COMMON編譯選項的時候纔有實際的作用。這是因爲,如果內核不支持NO_HZ模式的話,那Tick就不會中斷,每次Tick到來時,clk都會得到更新,就不需要調用forward_timer_base函數來補了。相反,在支持NO_HZ模式時,CPU如果處於空閒狀態,是不會收到任何Tick的,在這段時間內對應CPU的timer_base結構體中的clk就肯定不會得到 更新,因此需要調用該函數來補。補的條件有兩個,首先必須設置了must_forward_clk(後面會看到在處理定時期到期時會關閉must_forward_clk),還有就是當前的jiffies和clk中記錄的已經經過的jiffies相差大於等於2(小於2基本說明還沒進空閒模式)。最後,如果下一個到期時間在現在的jiffies之後,則將clk設置爲當前的jiffies;如果當前的jiffies已經超過了下一個到期時間(某些定時器已經過期了),則將clk設置爲下一個到期時間,一般對於可延遲定時器會出現這種情況。
每次都要補的目的其實是爲了儘量提高定時器的精度,前面已經說過了,到期時間距離clk越近,就會將其放到級別越低的桶裏面,檢查的Tick間隔就會越小,當然精度越高。如果長期不補clk的值,那即使到期時間只在1個Tick之後,也有可能被放到級別較大的桶內,哪怕是放到級別爲1的桶中,都要每8個Tick纔會被檢查一次,最差情況會延遲7個Tick。
同時,我們還可以看出,一個定時器一旦加入了一個桶裏之後,除非到期刪除或者主動修改了定時器到期時間,否則就再也不會移動了,不會因爲時間的流逝,距離到期時間越近而移動到更低級別的桶裏面。
最後,再提一下,在調用enqueue_timer函數將定時器放到timer_base的某個桶中後,一般還會接着調用trigger_dyntick_cpu函數:
static void
trigger_dyntick_cpu(struct timer_base *base, struct timer_list *timer)
{
/* 如果沒有切換到NO_HZ模式則直接返回 */
if (!is_timers_nohz_active())
return;
if (timer->flags & TIMER_DEFERRABLE) {
/* 沒有打開CONFIG_NO_HZ_FULL時該函數永遠返回false */
if (tick_nohz_full_cpu(base->cpu))
wake_up_nohz_cpu(base->cpu);
return;
}
/* 如果timer_base對應的CPU不是空閒的則直接返回 */
if (!base->is_idle)
return;
/* 如果定時器的到期時間晚於timer_base中的到期時間則直接返回 */
if (time_after_eq(timer->expires, base->next_expiry))
return;
/* 將timer_base的到期時間設置爲定時器的到期時間 */
base->next_expiry = timer->expires;
/* 喚醒timer_base對應的CPU */
wake_up_nohz_cpu(base->cpu);
}
這個函數主要是解決下面這種情況,如果要將定時器插入一個正處於空閒狀態的CPU下的timer_base的時候,那個CPU的定時事件設備應該是已經被編程到了所有包含的定時器中最近要到期的那個時刻,這時候恰好要插入的定時器的到期時刻比原來最近的到期時刻還要早的話,那這個新被插入的定時器一定會超時,因爲在這之前都不會有Tick到來。對於這種情況,只有調用wake_up_nohz_cpu函數將那個空閒的CPU喚醒,讓它重新再檢查一遍。
5)定時器的遷移
前面分析__mod_timer函數時已經碰到了定時器遷移的情況,定時器切換髮生在定時器指定的CPU上的timer_base和系統調用get_target_base函數挑選的timer_base不一致的情況:
static inline struct timer_base *
get_target_base(struct timer_base *base, unsigned tflags)
{
#if defined(CONFIG_SMP) && defined(CONFIG_NO_HZ_COMMON)
if (static_branch_likely(&timers_migration_enabled) &&
!(tflags & TIMER_PINNED))
return get_timer_cpu_base(tflags, get_nohz_timer_target());
#endif
return get_timer_this_cpu_base(tflags);
}
如果沒有配置CONFIG_SMP,那麼系統中只有一個CPU,也就無處遷移了。而如果內核沒有配置CONFIG_NO_HZ_COMMON,則Tick不會中斷,只需要返回當前CPU中對應的timer_base結構體就行了。timers_migration_enabled值將在切換到NO_HZ模式時變成True,而退出NO_HZ模式時變成False。所以只有在切換到NO_HZ模式下,且定時器沒有綁死到某個CPU的情況下,纔會選擇別的CPU上的timer_base,否則一定是當前CPU上的timer_base。get_nohz_timer_target函數會判斷當前的CPU是否處於空閒狀態,如果不是空閒狀態,那還是返回當前的CPU編號,如果真是空閒的話,會找到最近的一個忙的處理器,並返回其編號。所以,一共應該有兩種情況會出現要遷移定時器的行爲:
- 在當前CPU上嘗試修改一個沒有綁定到當前CPU上的定時器;
- 當前CPU空閒的時候,修改任何綁定到當前CPU上的定時器。
6)Tick到來的處理
當一個Tick到來時,無論是Tick層還是Tick模擬層最終都會調用update_process_times通知定時器層:
void update_process_times(int user_tick)
{
struct task_struct *p = current;
account_process_tick(p, user_tick);
/* 處理當前CPU下的所有定時器 */
run_local_timers();
rcu_sched_clock_irq(user_tick);
#ifdef CONFIG_IRQ_WORK
if (in_irq())
irq_work_tick();
#endif
scheduler_tick();
if (IS_ENABLED(CONFIG_POSIX_TIMERS))
run_posix_cpu_timers();
}
除了一些其它功能外,該函數會調用run_local_timers函數處理當前CPU下的所有定時器:
void run_local_timers(void)
{
/* 獲得當前CPU下BASE_STD下標的timer_base結構體 */
struct timer_base *base = this_cpu_ptr(&timer_bases[BASE_STD]);
/* 通知高精度定時器 */
hrtimer_run_queues();
/* 如果當前jiffies小於timer_base的clk值表明還沒有任何定時器到期 */
if (time_before(jiffies, base->clk)) {
if (!IS_ENABLED(CONFIG_NO_HZ_COMMON))
return;
/* 接着查當前CPU下BASE_DEF下標的timer_base結構體 */
base++;
if (time_before(jiffies, base->clk))
return;
}
/* 發起TIMER_SOFTIRQ軟中斷 */
raise_softirq(TIMER_SOFTIRQ);
}
該函數先取出當前CPU下BASE_STD編號的timer_base結構體。如果當前系統的jiffies小於結構體中的clk變量的值,表示該結構體內包含的所有定時器都還沒有到期。如果內核沒有配置CONFIG_NO_HZ_COMMON編譯選項,則直接退出(沒有配置NO_HZ模式,也就沒有第二個timer_base結構體了)。否則繼續檢查BASE_DEF標號的timer_base結構體,如果全都沒有到期的定時器,就沒必要激活軟中斷繼續處理了,直接退出就可以了。如果有可能有任何定時器到期的話,則激活TIMER_SOFTIRQ軟中斷。這個函數還會調用hrtimer_run_queues函數通知高精度定時器層。所以,在高精度定時器層沒有切換到高精度模式前,其定時觸發其實是靠精度較低的定時器層驅動的。
TIMER_SOFTIRQ軟中斷的處理函數是在init_timers函數裏面初始化的:
void __init init_timers(void)
{
init_timer_cpus();
open_softirq(TIMER_SOFTIRQ, run_timer_softirq);
}
可以看到TIMER_SOFTIRQ軟中斷的處理函數是run_timer_softirq:
static __latent_entropy void run_timer_softirq(struct softirq_action *h)
{
struct timer_base *base = this_cpu_ptr(&timer_bases[BASE_STD]);
__run_timers(base);
if (IS_ENABLED(CONFIG_NO_HZ_COMMON))
__run_timers(this_cpu_ptr(&timer_bases[BASE_DEF]));
}
就是分別調用__run_timers函數處理本CPU下的BASE_STD和BASE_DEF兩個timer_base中包含的所有定時器:
static inline void __run_timers(struct timer_base *base)
{
struct hlist_head heads[LVL_DEPTH];
int levels;
/* 如果當前時間早於timer_base的clk值表明沒有定時器到期 */
if (!time_after_eq(jiffies, base->clk))
return;
timer_base_lock_expiry(base);
/* 獲得timer_base的自旋鎖並關中斷 */
raw_spin_lock_irq(&base->lock);
/* 在__mod_timer函數中不需要再更新timer_base的clk值 */
base->must_forward_clk = false;
/* 如果當前時間晚於或等於timer_base的clk值循環並遞增 */
while (time_after_eq(jiffies, base->clk)) {
/* 收集所有已經到期的定時器 */
levels = collect_expired_timers(base, heads);
base->clk++;
while (levels--)
/* 按級從高到低處理所有到期定時器 */
expire_timers(base, heads + levels);
}
/* 釋放timer_base的自旋鎖並開中斷 */
raw_spin_unlock_irq(&base->lock);
timer_base_unlock_expiry(base);
}
該函數其實很簡單,基本上就是先調用collect_expired_timers函數獲得所有到期定時器,然後調用expire_timers函數處理所有的到期定時器。如果表示當前時間的系統jiffies值等於或晚於timer_base中的clk值,表明確實是經過了一些Tick,這時候就需要一個Tick一個Tick的追查到底有多少個定時器已經到期了,直到追到當前時間爲止。
處理到期定時器的expire_timers函數相對簡單,我們先來看看:
static void expire_timers(struct timer_base *base, struct hlist_head *head)
{
unsigned long baseclk = base->clk - 1;
/* 循環訪問所有超時定時器 */
while (!hlist_empty(head)) {
struct timer_list *timer;
void (*fn)(struct timer_list *);
timer = hlist_entry(head->first, struct timer_list, entry);
/* 更新timer_base的running_timer的值爲當前待處理定時器 */
base->running_timer = timer;
/* 從鏈表中刪除該定時器 */
detach_timer(timer, true);
fn = timer->function;
if (timer->flags & TIMER_IRQSAFE) {
raw_spin_unlock(&base->lock);
/* 調用定時器到期處理函數 */
call_timer_fn(timer, fn, baseclk);
/* 設置timer_base的running_timer的值爲空 */
base->running_timer = NULL;
raw_spin_lock(&base->lock);
} else {
raw_spin_unlock_irq(&base->lock);
/* 調用定時器到期處理函數 */
call_timer_fn(timer, fn, baseclk);
/* 設置timer_base的running_timer的值爲空 */
base->running_timer = NULL;
timer_sync_wait_running(base);
raw_spin_lock_irq(&base->lock);
}
}
}
該函數的第一個參數是對應的timer_base結構體,第二個參數是要處理的到期定時器的列表。如果定時器的標誌位設置了TIMER_IRQSAFE標誌位,除了加鎖和釋放鎖,還需要同時關閉中斷和打開中斷。
收集所有到期定時器是在collect_expired_timers函數中實現的:
static int collect_expired_timers(struct timer_base *base,
struct hlist_head *heads)
{
unsigned long now = READ_ONCE(jiffies);
/* 如果當前jiffies和clk變量之間的差值大於2證明當前CPU已經進入過空閒模式 */
if ((long)(now - base->clk) > 2) {
/* 搜尋timer_base下最早到期定時器的時間 */
unsigned long next = __next_timer_interrupt(base);
/* 如果最近的到期時間晚於當前的時間 */
if (time_after(next, now)) {
/* 更新clk的值爲當前時間後直接返回 */
base->clk = now;
return 0;
}
/* 更新clk的值爲最近的到期時間 */
base->clk = next;
}
/* 收集所有到期的定時器 */
return __collect_expired_timers(base, heads);
}
函數第一個參數是要收集的timer_base結構體,第二個參數是一個輸出參數,是一個鏈表數組,按照級編號。在正式收集之前,會檢查是不是剛從空閒模式中出來。在空閒模式下,不會收到Tick,所以就會導致當前時間jiffies和timer_base的clk值之間差距比較大。如果是這樣的話,還是像處理普通模式一樣一個Tick一個Tick追就太沒有效率了,因爲理論上在Tick中斷期間是沒有要到期的定時器的。所以,可以調用__next_timer_interrupt函數找到最近到期定時器的到期時間,並更新clk的值,再去收集。
static unsigned long __next_timer_interrupt(struct timer_base *base)
{
unsigned long clk, next, adj;
unsigned lvl, offset = 0;
next = base->clk + NEXT_TIMER_MAX_DELTA;
clk = base->clk;
/* 循環每一個級 */
for (lvl = 0; lvl < LVL_DEPTH; lvl++, offset += LVL_SIZE) {
/* 在某一級下獲得下一個到期桶偏移距離 */
int pos = next_pending_bucket(base, offset, clk & LVL_MASK);
if (pos >= 0) {
/* 計算對應桶的到期時間 */
unsigned long tmp = clk + (unsigned long) pos;
tmp <<= LVL_SHIFT(lvl);
/* 找出最小的到期時間 */
if (time_before(tmp, next))
next = tmp;
}
/* 如果當前clk的最低3位不爲0,則切換到下一級的時候要加1。 */
adj = clk & LVL_CLK_MASK ? 1 : 0;
/* 對clk移位切換下一級 */
clk >>= LVL_CLK_SHIFT;
clk += adj;
}
return next;
}
該函數搜尋所有級下面的所有桶中第一個馬上要到期定時器的到期時間。clk會在切換到下一級搜索前向右移3位,並且如果最低3位不爲0的時候,移位後還需要加1。這是因爲這個函數是用來找馬上要到期的定時器,不是現在已經到期的,所以應該要找下一級的下一個。
前面提到過,定時器是不會因爲快要到期了而移動位置的,因此有可能在高級別的桶內的到期時間反而早於在低級別桶內的到期時間,所以需要每個級別都要搜索。
next_pending_bucket函數用來獲得下一個到期桶的編號:
static int next_pending_bucket(struct timer_base *base, unsigned offset,
unsigned clk)
{
unsigned pos, start = offset + clk;
unsigned end = offset + LVL_SIZE;
/* 從start開始到end往後搜 */
pos = find_next_bit(base->pending_map, end, start);
if (pos < end)
return pos - start;
/* 從offset開始到到start回過來搜 */
pos = find_next_bit(base->pending_map, start, offset);
return pos < start ? pos + LVL_SIZE - start : -1;
}
注意,參數clk不是timer_base的clk值,而是對應該級的6位。函數返回的數值是在某一個級下的桶偏移距離,也就是編號的範圍是0到63,同時還要考慮回滾的情況。這個函數是通過搜索timer_base的pending_map字段查找的,前面提過,在向某個桶中插入定時器的時候會設置pending_map的相應位。這個函數先從當前位置向該級最後一個桶的位置查找,如果找到了那就返回找到的位置和當前位置的距離:
如果找不到,還會繼續從該級第一個桶的位置向當前位置查找,但最後計算距離的時候,要考慮回滾,也就是當前位置到該級最後一個桶的位置之間的距離加上該級第一個桶的位置到找到的位置之間的距離:
在更新完timver_base的clk值之後,collect_expired_timers函數最終會調用__collect_expired_timers函數真正去收集到期的定時器:
static int __collect_expired_timers(struct timer_base *base,
struct hlist_head *heads)
{
unsigned long clk = base->clk;
struct hlist_head *vec;
int i, levels = 0;
unsigned int idx;
/* 按級別從低到高循環 */
for (i = 0; i < LVL_DEPTH; i++) {
/* 找到對應clk值在指定級下面的桶下標 */
idx = (clk & LVL_MASK) + i * LVL_SIZE;
/* 看對應的桶下面有沒有定時器 */
if (__test_and_clear_bit(idx, base->pending_map)) {
/* 獲得對應桶鏈表 */
vec = base->vectors + idx;
/* 將桶內所有定時器鏈表切換到heads參數裏 */
hlist_move_list(vec, heads++);
levels++;
}
/* 如果還沒到下一個級的檢查週期則跳出循環 */
if (clk & LVL_CLK_MASK)
break;
/* 對clk移位切換下一級 */
clk >>= LVL_CLK_SHIFT;
}
return levels;
}
該函數其實就是根據timer_base的clk值到每個級下的相應桶內查找看有沒有到期的定時器。如果下一級的檢查粒度還沒達到就退出循環,在該級停止。
所以,總結一下,時間輪不是定時器在滾動,而是到期的位置在不停的移動。定時器的位置在添加的一剎那,根據到期時間距離當前時間的間隔,以及到期時間對應相應級的6位固定好了,而且一旦固定下來就不會移動了。每當Tick到來,都會更新timer_base的clk值,計算所指向桶的位置,然後通過pending_map判斷桶裏面是不是存在定時器,如果有的話那它們一定已經到期甚至是超時了。同時,只有在相應時刻(粒度對應的3位全爲0時)纔會檢查下一級。