linux 內核定時器 timer_list詳解

原文出處:http://www.cnblogs.com/leaven/archive/2010/08/19/1803382.html

Linux內核2.4版中去掉了老版本內核中的靜態定時器機制,而只留下動態定時器。相應地在timer_bh()函數中也不再通

run_old_timers()函數來運行老式的靜態定時器。動態定時器與靜態定時器這二個概念是相對於Linux內核定時器機

制的可擴展功能而言的,動態定時器是指內核的定時器隊列是可以動態變化的,然而就定時器本身而言,二者並無本質的

區別。考慮到靜態定時器機制的能力有限,因此Linux內核2.4版中完全去掉了以前的靜態定時器機制。

761 Linux內核對定時器的描述

Linuxinclude/linux/timer.h頭文件中定義了數據結構timer_list來描述一個內核定時器:

struct timer_list { 

struct list_head list; 

unsigned long expires; 

unsigned long data; 

void (*function)(unsigned long); 

};

各數據成員的含義如下:

1)雙向鏈表元素list:用來將多個定時器連接成一條雙向循環隊列。

2expires:指定定時器到期的時間,這個時間被表示成自系統啓動以來的時鐘滴答計數(也即時鐘節拍數)。當一個

定時器的expires值小於或等於jiffies變量時,我們就說這個定時器已經超時或到期了。在初始化一個定時器後,通常

把它的expires域設置成當前expires變量的當前值加上某個時間間隔值(以時鐘滴答次數計)。

3)函數指針function:指向一個可執行函數。當定時器到期時,內核就執行function所指定的函數。而data域則

被內核用作function函數的調用參數。

內核函數init_timer()用來初始化一個定時器。實際上,這個初始化函數僅僅將結構中的list成員初始化爲空。如下所示(include/linux/timer.h):

static inline void init_timer(struct timer_list * timer) 

timer->list.next = timer->list.prev = NULL; 

}

由於定時器通常被連接在一個雙向循環隊列中等待執行(此時我們說定時器處於pending狀態)。因此函數time_pending() 

就可以用list成員是否爲空來判斷一個定時器是否處於pending狀態。如下所示

(include/linux/timer.h): 

static inline int timer_pending (const struct timer_list * timer) 

return timer->list.next != NULL; 

}

時間比較操作

在定時器應用中經常需要比較兩個時間值,以確定timer是否超時,所以Linux內核在timer.h頭文件中定義了4個時間

關係比較操作宏。這裏我們說時刻a在時刻b之後,就意味着時間值a≥bLinux強烈推薦用戶使用它所定義的下列4

時間比較操作宏(include/linux/timer.h):

#define time_after(a,b) ((long)(b) - (long)(a) < 0) 

#define time_before(a,b) time_after(b,a) 

 

#define time_after_eq(a,b) ((long)(a) - (long)(b) >= 0) 

#define time_before_eq(a,b) time_after_eq(b,a)

76動態內核定時器機制的原理

Linux是怎樣爲其內核定時器機制提供動態擴展能力的呢?其關鍵就在於定時器向量的概念。所謂定時器向量” 

就是指這樣一條雙向循環定時器隊列(對列中的每一個元素都是一個timer_list結構):對列中的所有定時器都在同

一個時刻到期,也即對列中的每一個timer_list結構都具有相同的expires值。顯然,可以用一個timer_list結構類型

的指針來表示一個定時器向量。

顯然,定時器expires成員的值與jiffies變量的差值決定了一個定時器將在多長時間後到期。在32位系統中,這個時間

差值的最大值應該是0xffffffff。因此如果是基於定時器向量基本定義,內核將至少要維護0xfffffffftimer_list 

結構類型的指針,這顯然是不現實的。

另一方面,從內核本身這個角度看,它所關心的定時器顯然不是那些已經過期而被執行過的定時器(這些定時器完全可

以被丟棄),也不是那些要經過很長時間纔會到期的定時器,而是那些當前已經到期或者馬上就要到期的定時器

(注意!時間間隔是以滴答次數爲計數單位的)。

基於上述考慮,並假定一個定時器要經過interval個時鐘滴答後纔到期(intervalexpiresjiffies),則Linux採用

了下列思想來實現其動態內核定時器機制:對於那些0≤interval≤255的定時器,Linux嚴格按照定時器向量的基本

語義來組織這些定時器,也即Linux內核最關心那些在接下來的255個時鐘節拍內就要到期的定時器,因此將它們按

照各自不同的expires值組織成256個定時器向量。而對於那些256≤interval≤0xffffffff的定時器,由於他們離到

期還有一段時間,因此內核並不關心他們,而是將它們以一種擴展的定時器向量語義(或稱爲鬆散的定時器向量語義

進行組織。所謂鬆散的定時器向量語義就是指:各定時器的expires值可以互不相同的一個定時器隊列。

具體的組織方案可以分爲兩大部分:

1)對於內核最關心的、interval值在[0255]之間的前256個定時器向量,內核是這樣組織它們的:這256個定

時器向量被組織在一起組成一個定時器向量數組,並作爲數據結構timer_vec_root的一部分,該數據結構定義

kernel/timer.c文件中,如下述代碼段所示:

/* 

* Event timer code 

*/ 

#define TVN_BITS 6 

#define TVR_BITS 8 

#define TVN_SIZE (1 << TVN_BITS) 

#define TVR_SIZE (1 << TVR_BITS) 

#define TVN_MASK (TVN_SIZE - 1) 

#define TVR_MASK (TVR_SIZE - 1) 

 

struct timer_vec { 

int index; 

struct list_head vec[TVN_SIZE]; 

}; 

 

struct timer_vec_root { 

int index; 

struct list_head vec[TVR_SIZE]; 

}; 

 

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; 

 

static struct timer_vec * const tvecs[] = { 

(struct timer_vec *)&tv1, &tv2, &tv3, &tv4, &tv5 

}; 

#define NOOF_TVECS (sizeof(tvecs) / sizeof(tvecs[0]))

基於數據結構timer_vec_rootLinux定義了一個全局變量tv1,以表示內核所關心的前256個定時器向量。這樣

內核在處理是否有到期定時器時,它就只從定時器向量數組tv1.vec256]中的某個定時器向量內進行掃描。

tv1index字段則指定當前正在掃描定時器向量數組tv1.vec256]中的哪一個定時器向量,也即該數組的索引,

其初值爲0,最大值爲255(以256爲模)。每個時鐘節拍時index字段都會加1。顯然,index字段所指定的定時器

向量tv1.vecindex]中包含了當前時鐘節拍內已經到期的所有動態定時器。而定時器向量tv1.vecindexk]則

包含了接下來第k個時鐘節拍時刻將到期的所有動態定時器。當index值又重新變爲0時,就意味着內核已經掃描

tv1變量中的所有256個定時器向量。在這種情況下就必須將那些以鬆散定時器向量語義來組織的定時器向量補充到tv1中來。

2)而對於內核不關心的、interval值在[0xff0xffffffff]之間的定時器,它們的到期緊迫程度也隨其interval

的不同而不同。顯然interval值越小,定時器緊迫程度也越高。因此在將它們以鬆散定時器向量進行組織時也應該區別

對待。通常,定時器的interval值越小,它所處的定時器向量的鬆散度也就越低(也即向量中的各定時器的expires

相差越小);而interval值越大,它所處的定時器向量的鬆散度也就越大(也即向量中的各定時器的expires值相差越大)。

內核規定,對於那些滿足條件:0x100≤interval≤0x3fff的定時器,只要表達式(interval>>8)具有相同值的定時

器都將被組織在同一個鬆散定時器向量中。因此,爲組織所有滿足條件0x100≤interval≤0x3fff的定時器,就需

2664個鬆散定時器向量。同樣地,爲方便起見,這64個鬆散定時器向量也放在一起形成數組,並作爲數據結

timer_vec的一部分。基於數據結構timer_vecLinux定義了全局變量tv2,來表示這64條鬆散定時器向量。如

上述代碼段所示。

對於那些滿足條件0x4000≤interval≤0xfffff的定時器,只要表達式(interval>>86)的值相同的定時器都將被

放在同一個鬆散定時器向量中。同樣,要組織所有滿足條件0x4000≤interval≤0xfffff的定時器,也需要2664

鬆散定時器向量。類似地,這64個鬆散定時器向量也可以用一個timer_vec結構來描述,相應地Linux定義了tv3

局變量來表示這64個鬆散定時器向量。

對於那些滿足條件0x100000≤interval≤0x3ffffff的定時器,只要表達式(interval>>866)的值相同的定時

器都將被放在同一個鬆散定時器向量中。同樣,要組織所有滿足條件0x100000≤interval≤0x3ffffff的定時器,也

需要2664個鬆散定時器向量。類似地,這64個鬆散定時器向量也可以用一個timer_vec結構來描述,相應地Linux 

定義了tv4全局變量來表示這64個鬆散定時器向量。

對於那些滿足條件0x4000000≤interval≤0xffffffff的定時器,只要表達式(interval>>8666)的值相同的

定時器都將被放在同一個鬆散定時器向量中。同樣,要組織所有滿足條件0x4000000≤interval≤0xffffffff的定時器,

也需要2664個鬆散定時器向量。類似地,這64個鬆散定時器向量也可以用一個timer_vec結構來描述,相應地Linux

義了tv5全局變量來表示這64個鬆散定時器向量。

最後,爲了引用方便,Linux定義了一個指針數組tvecs[],來分別指向tv1tv2tv5結構變量。如上述代碼所示。

76內核動態定時器機制的實現

在內核動態定時器機制的實現中,有三個操作時非常重要的:

1)將一個定時器插入到它應該所處的定時器向量中。

2)定時器的遷移,也即將一個定時器從它原來所處的定時器向量遷移到另一個定時器向量中。

3)掃描並執行當前已經到期的定時器。

763動態定時器機制的初始化

函數init_timervecs()實現對動態定時器機制的初始化。該函數僅被sched_init()初始化例程所調用。動態定時

器機制初始化過程的主要任務就是將tv1tv2tv55個結構變量中的定時器向量指針數組vec[]初始化

NULL。如下所示(kernel/timer.c):

void init_timervecs (void) 

int i; 

 

for (i = 0; i < TVN_SIZE; i++) { 

INIT_LIST_HEAD(tv5.vec + i); 

INIT_LIST_HEAD(tv4.vec + i); 

INIT_LIST_HEAD(tv3.vec + i); 

INIT_LIST_HEAD(tv2.vec + i); 

for (i = 0; i < TVR_SIZE; i++) 

INIT_LIST_HEAD(tv1.vec + i); 

}

上述函數中的宏TVN_SIZE是指timer_vec結構類型中的定時器向量指針數組vec[]的大小,值爲64

TVR_SIZE是指timer_vec_root結構類型中的定時器向量數組vec[]的大小,值爲256

763動態定時器的時鐘滴答基準timer_jiffies 

由於動態定時器是在時鐘中斷的Bottom Half中被執行的,而從TIMER_BH向量被激活到其timer_bh()函數真正

執行這段時間內可能會有幾次時鐘中斷髮生。因此內核必須記住上一次運行定時器機制是什麼時候,也即內核必須

保存上一次運行定時器機制時的jiffies值。爲此,Linuxkernel/timer.c文件中定義了全局變量timer_jiffies來表

示上一次運行定時器機制時的jiffies值。該變量的定義如下所示:

static unsigned long timer_jiffies;

763對內核動態定時器鏈表的保護

由於內核動態定時器鏈表是一種系統全局共享資源,爲了實現對它的互斥訪問,Linux定義了專門的自旋鎖timerlist_lock 

來保護。任何想要訪問動態定時器鏈表的代碼段都首先必須先持有該自旋鎖,並且在訪問結束後釋放該自旋鎖。其定義

如下(kernel/timer.c):

/* Initialize both explicitly - let's try to have them in the same cache line */ 

spinlock_t timerlist_lock = SPIN_LOCK_UNLOCKED;

763將一個定時器插入到鏈表中

add_timer()用來將參數timer指針所指向的定時器插入到一個合適的定時器鏈表中。它首先調用timer_pending()

數判斷所指定的定時器是否已經位於在某個定時器向量中等待執行。如果是,則不進行任何操作,只是打印一條內核告警

信息就返回了;如果不是,則調用 internal_add_timer()函數完成實際的插入操作。其源碼如下(kernel/timer.c):

void add_timer(struct timer_list *timer) { unsigned long flags; spin_lock_irqsave(&timerlist_lock, flags); if (timer_pending(timer)) goto bug; internal_add_timer(timer); spin_unlock_irqrestore(&timerlist_lock, flags); return; bug: spin_unlock_irqrestore(&timerlist_lock, flags); printk("bug: kernel timer added twice at %p.\n", __builtin_return_address(0)); }

函數internal_add_timer()用於將一個不處於任何定時器向量中的定時器插入到它應該所處的定時器向量中去(根據定

時器的expires值來決定)。如下所示(kernel/timer.c):

static inline void internal_add_timer(struct timer_list *timer) { /* * must be cli-ed when calling this */ unsigned long expires = timer->expires; unsigned long idx = expires - timer_jiffies; struct list_head * vec; if (idx < TVR_SIZE) { int i = expires & TVR_MASK; vec = tv1.vec + i; } else if (idx < 1 << (TVR_BITS + TVN_BITS)) { int i = (expires >> TVR_BITS) & TVN_MASK; vec = tv2.vec + i; } else if (idx < 1 << (TVR_BITS + 2 * TVN_BITS)) { int i = (expires >> (TVR_BITS + TVN_BITS)) & TVN_MASK; vec = tv3.vec + i; } else if (idx < 1 << (TVR_BITS + 3 * TVN_BITS)) { int i = (expires >> (TVR_BITS + 2 * TVN_BITS)) & TVN_MASK; vec = 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 = tv1.vec + tv1.index; } else if (idx <= 0xffffffffUL) { int i = (expires >> (TVR_BITS + 3 * TVN_BITS)) & TVN_MASK; vec = tv5.vec + i; } else { /* Can only get here on architectures with 64-bit jiffies */ INIT_LIST_HEAD(&timer->list); return; } /* * Timers are FIFO! */ list_add(&timer->list, vec->prev); }

對該函數的註釋如下:

1)首先,計算定時器的expires值與timer_jiffies的插值(注意!這裏應該使用動態定時器自己的時間基準),這個差值

就表示這個定時器相對於上一次運行定時器機制的那個時刻還需要多長時間間隔纔到期。局部變量idx保存這個差值。

2)根據idx的值確定這個定時器應被插入到哪一個定時器向量中。其具體的確定方法我們在7.6.2節已經說過了,這裏不再

詳述。最後,定時器向量的頭部指針vec表示這個定時器應該所處的定時器向量鏈表頭部。

3)最後,調用list_add()函數將定時器插入到vec指針所指向的定時器隊列的尾部。

763修改一個定時器的expires

當一個定時器已經被插入到內核動態定時器鏈表中後,我們還可以修改該定時器的expires值。函數mod_timer()實現這一點。

如下所示(kernel/timer.c):

int mod_timer(struct timer_list *timer, unsigned long expires) { int ret; unsigned long flags; spin_lock_irqsave(&timerlist_lock, flags); timer->expires = expires; ret = detach_timer(timer); internal_add_timer(timer); spin_unlock_irqrestore(&timerlist_lock, flags); return ret; }

該函數首先根據參數expires值更新定時器的expires成員。然後調用detach_timer()函數將該定時器從它原來所屬的鏈表

中刪除。最後調用internal_add_timer()函數將該定時器根據它新的expires值重新插入到相應的鏈表中。

detach_timer()首先調用timer_pending()來判斷指定的定時器是否已經處於某個鏈表中,如果定時器原來就不處於任

何鏈表中,detach_timer()函數什麼也不做,直接返回0值,表示失敗。否則,就調用list_del()函數將定時器從它原來所

處的鏈表中摘除。如下所示(kernel/timer.c):

static inline int detach_timer (struct timer_list *timer) { if (!timer_pending(timer)) return 0; list_del(&timer->list); return 1; }

763刪除一個定時器

函數del_timer()用來將一個定時器從相應的內核定時器隊列中刪除。該函數實際上是對detach_timer()函數的高層封裝。

如下所示(kernel/timer.c):

int del_timer(struct timer_list * timer) 

int ret; 

unsigned long flags; 

 

spin_lock_irqsave(&timerlist_lock, flags); 

ret = detach_timer(timer); 

timer->list.next = timer->list.prev = NULL; 

spin_unlock_irqrestore(&timerlist_lock, flags); 

return ret; 

}

763定時器遷移操作

由於一個定時器的interval值會隨着時間的不斷流逝(即jiffies值的不斷增大)而不斷變小,因此那些原本到期緊迫程度較

低的定時器會隨着jiffies值的不斷增大而成爲既把馬上到期的定時器。比如定時器向量tv2.vec[0]中的定時器在經過256

時鐘滴答後會成爲未來256個時鐘滴答內會到期的定時器。因此,定時器在內核動態定時器鏈表中的位置也應相應地隨着改

變。改變的規則是:當tv1.index重新變爲0時(意味着tv1中的256個定時器向量都已被內核掃描一遍了,從而使tv1中的

256個定時器向量變爲空),則用tv2.vecindex]定時器向量中的定時器去填充tv1,同時使tv2.index1(它以64爲模)。

tv2.index重新變爲0(意味着tv2中的64個定時器向量都已經被全部填充到tv1中去了,從而使得tv2變爲空),則

tv3.vecindex]定時器向量中的定時器去填充tv2。如此一直類推下去,直到tv5

函數cascade_timers()完成這種定時器遷移操作,該函數只有一個timer_vec結構類型指針的參數tv。這個函數把把定時

器向量tv->vectv->index]中的所有定時器重新填充到上一層定時器向量中去。如下所示(kernel/timer.c):

static inline void cascade_timers(struct timer_vec *tv) 

/* cascade all the timers from tv up one level */ 

struct list_head *head, *curr, *next; 

 

head = tv->vec + tv->index; 

curr = head->next; 

/* 

* We are removing _all_ timers from the list, so we don't have to 

* detach them individually, just clear the list afterwards. 

*/ 

while (curr != head) { 

struct timer_list *tmp; 

 

tmp = list_entry(curr, struct timer_list, list); 

next = curr->next; 

list_del(curr); // not needed 

internal_add_timer(tmp); 

curr = next; 

INIT_LIST_HEAD(head); 

tv->index = (tv->index + 1) & TVN_MASK; 

}

對該函數的註釋如下:

1)首先,用指針head指向定時器頭部向量頭部的list_head結構。指針curr指向定時器向量中的第一個定時器。

2)然後,用一個while{}循環來遍歷定時器向量tv->vectv->index]。由於定時器向量是一個雙向循環隊列,因此循環

的終止條件是curr=head。對於每一個被掃描的定時器,循環體都先調用list_del()函數把當前定時器從鏈表中摘除,然後調

internal_add_timer()函數重新確定該定時器應該被放到哪個定時器向量中去。

3)當從while{}循環退出後,定時器向量tv->vectv->index]中所有的定時器都已被遷移到其它地方(到它們該呆的地

方:-),因此它本身就成爲一個空隊列。這裏我們顯示地調用INIT_LIST_HEAD()宏來把定時器向量的表頭結構初始化爲空。

4)最後,把tv->index值加1,當然它是以64爲模。以上文章內容均爲網絡轉載或者本站會員原創,"Linux-cn.com不對

文章內容做任何保證。

764掃描並執行當前已經到期的定時器

函數run_timer_list()完成這個功能。如前所述,該函數是被timer_bh()函數所調用的,因此內核定時器是在時鐘中

斷的Bottom Half中被執行的。記住這一點非常重要。全局變量timer_jiffies表示了內核上一次執行run_timer_list()

數的時間,因此jiffiestimer_jiffies的差值就表示了自從上一次處理定時器以來,期間一共發生了多少次時鐘中斷,顯

run_timer_list()函數必須爲期間所發生的每一次時鐘中斷補上定時器服務。該函數的源碼如下(kernel/timer.c):

static inline void run_timer_list(void) 

spin_lock_irq(&timerlist_lock); 

while ((long)(jiffies - timer_jiffies) >= 0) { 

struct list_head *head, *curr; 

if (!tv1.index) { 

int n = 1; 

do { 

cascade_timers(tvecs[n]); 

} while (tvecs[n]->index == 1 && ++n < NOOF_TVECS); 

repeat: 

head = tv1.vec + tv1.index; 

curr = head->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; 

 

detach_timer(timer); 

timer->list.next = timer->list.prev = NULL; 

timer_enter(timer); 

spin_unlock_irq(&timerlist_lock); 

fn(data); 

spin_lock_irq(&timerlist_lock); 

timer_exit(); 

goto repeat; 

++timer_jiffies; 

tv1.index = (tv1.index + 1) & TVR_MASK; 

spin_unlock_irq(&timerlist_lock); 

}

函數run_timer_list()的執行過程主要就是用一個大while{}循環來爲時鐘中斷執行定時器服務,每一次循環服務一次時鐘

中斷。因此一共要執行(jiffiestimer_jiffies1)次循環。循環體所執行的服務步驟如下:

1)首先,判斷tv1.index是否爲0,如果爲0則需要從tv2中補充定時器到tv1中來。但tv2也可能爲空而需要從tv3中補充

定時器,因此用一個do{}while循環來調用cascade_timer()函數來依次視需要從tv2中補充tv1,從tv3中補充tv2

tv5中補充tv4。顯然如果tvi.index=02≤i≤5),則對於tvi執行cascade_timers()函數後,tvi.index肯定爲1

反過來講,如果對tvi執行過cascade_timers()函數後tvi.index不等於1,那麼可以肯定在未對tvi執行cascade_timers()

數之前,tvi.index值肯定不爲0,因此這時tvi不需要從tv(i+1)中補充定時器,這時就可以終止do{}while循環。

2)接下來,就要執行定時器向量tv1.vectv1.index]中的所有到期定時器。因此這裏用一個goto repeat循環從頭到尾

依次掃描整個定時器對列。由於在執行定時器的關聯函數時並不需要關CPU中斷,所以在用detach_timer()函數把當前定時

器從對列中摘除後,就可以調用spin_unlock_irq()函數進行解鎖和開中斷,然後在執行完當前定時器的關聯函數後重新用

spin_lock_irq()函數加鎖和關中斷。

3)當執行完定時器向量tv1.vec[tv1.index]中的所有到期定時器後,tv1.vectv1.index]應該是個空隊列。至此這一次

定時器服務也就宣告結束。

4)最後,把timer_jiffies值加1,把tv1.index值加1,當然它的模是256。然後,回到while循環開始下一次定時器服務。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章