這部分講Linux內核定時器。
基本概念
- 系統定時器:一種可編程硬件芯片,能以固定頻率產生中斷。
- 定時器中斷:系統定時器固定時間周期產生的中斷,其中斷處理程序負責更新系統時間,執行周期性任務。
- 動態定時器:一種用來推遲執行程序的工具。內核可以動態創建、銷毀動態定時器。
- 節拍率(tick rate):系統定時器頻率,系統定時器以某種頻率自行觸發(又稱為擊中(hitting)或射中(popping))時鍾中斷,該頻率可以通過編程設定。
- 節拍(tick):預編的節拍率對內核來說是可知的,因此內核知道連續2次時鍾中斷間隔時間。這個時間稱為節拍。節拍 = 1/ 節拍率。常用來計算牆上時間和系統運行時間。
- 牆上時間(walk clock time):實際時間,對用戶空間的應用程序來說很重要。代表從進程開始運行到結束,系統時鍾走過的時間(時鍾數),包含了進程阻塞的時間。每秒滴答數(節拍率)可通過sysconf(_SC_CLK_TCK)獲取。
- 系統運行時間:自系統啟動開始所經過的時間,對用戶空間和內核都很有用。牆上時間 = 阻塞時間 + 就緒時間 + 運行時間,運行時間 = 用戶CPU實際 + 系統CPU時間。
系統定時器中斷周期性執行的任務:
- 更新系統時間。
- 更新實際時間。
- 在smp系統上,均衡調度程序中各處理器上的運行隊列。如果運行隊列負載不均衡的話,盡量使它們均衡。
- 檢查當前進程是否用盡了自己的時間片。如果用盡,就重新調度。
- 運行超時的動態定時器。
- 更新資源消耗和處理器時間的統計值。
[======]
節拍率:HZ
系統定時器(節拍率)通過靜態預處理定義,系統啟動時按HZ值對硬件進行設置。HZ值取決於體系結構。如i386體系結構,HZ值為1000(Hz),代表每秒鍾產生1000次節拍
#include <asm/param.h>
#define HZ 1000 /* 內核時鍾頻率 */
其他體系結構節拍率:

系統定時器使用高頻率優缺點
優點:
- 內核定時器能以更高頻度和准確度運行。
- 依賴定時值執行的系統調用,如select,poll,能以更高精度運行。
- 對諸如資源消耗和系統運行時間等的測量會有精細的解析度。
- 提高進程搶占的准確度。
缺點:
- 節拍率越高,系統時鍾中斷頻率越高,意味着系統負擔越重,即中斷處理處理程序占用的處理器時間越多,減少了處理其他工作的時間。
[======]
jiffies
全局變量jiffies用來記錄自系統啟動以來產生的節拍總數。啟動時,初值0;之后,每次時鍾中斷處理程序都會讓jiffies+1。
jiffies定義:
#include <linux/jiffies.h>
extern unsigned long volatile jiffies;
jiffies內部表示
32bit體系結構上,jiffies是32bit,如果時鍾頻率100Hz,497天后會溢出;頻率1000Hz,49.7天后溢出。
64bit體系結構上,幾乎不可能會看到它溢出。
除了前面定義,jiffies還有第二個變量定義:
#include <linux/jiffies.h>
extern u64 jiffies_64;
ld(1) 腳本用於連接主內核映像,然后用jiffies_64初值覆蓋jiffies變量:
// x86, arch/i386/kernel/vmlinux.lds.S
jiffies = jiffies_64;
也就是說,jiffies只取jiffies_64低32bit。因為大多數代碼只使用jiffies存放流失的時間,二時間管理代碼使用整個64bit的jiffies_64,以避免溢出。

在32bit體系結構上,jiffies 讀取jiffies_64低32bit值;get_jiffies_64()讀取jiffies_64整個64bit值。
周64bit體系結構上,jiffies 等價於get_jiffies_64(),和jiffies_64是同一個變量。
jiffies回繞
jiffies 溢出后,會繞回(wrap around)到0。內核提供4個宏函數,用於比較節拍計數,以避免回繞問題。
#include <linux/jiffies.h>
// unknown是jiffies, known是需要對比的值
#define timer_after(unknown, known) ((long)(known) - (long)(unknown) < 0)
#define timer_before(unknown, known) ((long)(unknown) - (long)(known) < 0)
#define timer_after_eq(unknown, known) ((long)(unknown) - (long)(known) >= 0)
#define timer_after(unknown, known) ((long)(known) - (long)(unknown) >= 0)
用戶空間和HZ
Linux內核2.6以前,如果改變內核中HZ值,會給用戶空間中某些程序造成異常結果,因為應用程序已經依賴這個特定HZ值。
要避免上面錯誤,內核需要更改所有導出的jiffies值。因此,內核定義USER_HZ代表用戶空間看到的HZ值。
例如,x86體系結構上,HZ值原來一直是100,因此USER_HZ值定義為100。
內核使用宏jiffies_to_clock_t() 將一個由HZ表示的節拍計數轉換成一個由USER_HZ表示的節拍數。
當USER_HZ是HZ的整數倍時,
#define jiffies_to_clock_t(x) ((x) / (HZ/USER_HZ))
另外,jiffies_64_to_clock_t()將64位jiffies值單位從HZ轉換為USER_HZ。
[======]
硬時鍾和定時器
體系結構提供3種硬時鍾用於計時:實時時鍾,時間戳計數,可編程中斷定時器。
實時時鍾 RTC
RTC是用來持久存放系統時間的設備,即使PC關掉電源,RTC還能依靠主板電池繼續計時。
主要作用:
1)系統啟動時,內核通過讀取RTC來初始化牆上時間,該時間存放在xtime變量中。
2)Linux只用RTC來獲得當前時間和日期。
時間戳計數 TSC
x86包含一個64位的時間戳計數器(寄存器),對每個時鍾信號進行計數。例如,如果時鍾節拍400MHz,那么TSC每2.5ns計數+1。而時鍾信號頻率沒有在預編譯時指定,必須在Linux初始化時確定。通過calibrate_tsc(),在系統初始化階段完成時鍾信號頻率計算。
可編程中斷定時器 PIT
x86體系結構中,主要采用可編程中斷時鍾(PIT)作為系統定時器。
Linux中,若PIT以100Hz頻率向IRQ0發出定時中斷,即每10ms產生1次定時中斷。這個10ms間隔,就是一個節拍(tick),以微妙為單位存放在tick變量。
TSC與PIT相比,擁有更高的精度。PIT針對編寫軟件而言,更加靈活。
[======]
時鍾中斷處理程序
時鍾中斷處理程序可划分2個部分:體系結構相關部分,體系結構無關部分。
與體系結構相關的例程作為系統定時器(PIT)的中斷處理程序而注冊到內核,以便產生時鍾中斷時能運行。
處理程序主要執行以下工作:
- 獲得xtime_lock鎖,對訪問jiffies_64和牆上時間進行保護。
- 需要時應答或重新設置系統時鍾。
- 周期性地使用牆上時間更新實時時鍾。
- 調用體系結構無關的時鍾例程:do_timer()。
中斷服務程序主要通過調用與體系結構無關的do_timer()執行工作:
- 給jiffies_64 + 1。
- 更新資源消耗的統計值,如當前進程所消耗的系統時間和用戶時間。
- 執行已經到期的動態定時器。
- 更新牆上時間,該時間存放在xtime變量中。
- 計算平均負載值。
do_timer()看起來像:
void do_timer(struct pt_regs* regs)
{
jiffies_64++;
update_process_times(user_mode(regs)); // 對用戶或系統進行時間更新
update_times(); // 更新牆上時鍾
}
user_mode()宏查詢處理器寄存器regs的狀態。如果時鍾中斷發生在用戶空間,它返回1;如果發生在內核,則返回0。update_process_times()函數根據時鍾中斷產生的位置(用戶態 or 內核態),對用戶或對系統進行相應的時間更新。
void update_process_times(int user_tick)
{
struct task_struct *p = current;
int cpu = smp_processor_id();
int system = user_tick ^ 1; // user_tick和system只會有一個變量為1,另一個必為0
update_one_process(p, user_tick, system, cpu); // 更新進程時間
run_local_timers(); // 標記一個軟中斷處理所有到期的定時器
scheduler_tick(user_tick, system); // 負責減少當前運行進程的時間片計數值,並在需要時設置need_resched標志
}
update_one_process() 通過判斷分支,將user_tick和system加到進程相應的計數上:
/* 更新恰當的時間計數器,給其加一個jiffy */
p->utime += user;
p->stime += system;
update_times()負責更新牆上時鍾:
void update_times(void)
{
unsigned long ticks; // 記錄最近一次更新后新產生的節拍數
ticks = jiffies - wall_jiffies;
if (ticks) {
wall_jiffies += ticks;
update_wall_time(ticks); // 更新存儲牆上時間的xtime
}
last_time_offset = 0;
calc_load(ticks); // 更新載入平均值
}
ticks記錄最近一次更新后新產生的節拍數。通常,ticks應為1,但時鍾中斷可能丟失,導致節拍丟失。中斷長時間被禁止時,就會出現這種情況(雖然很可能是bug)。
cal_load(0更新載入平均值,到此,update_times()執行完畢。do_timer()亦執行完畢並返回與體系結構相關的中斷處理程序,繼續執行后面的工作,釋放xtime_lock鎖,然后退出。
牆上時間(實際時間)
牆上時間定義在kernel/timer.c中
struct timespec xtime;
timespec結構定義:
#include <linux/time.h>
struct timespec {
time_t tv_sec; /* 秒 */
long tv_nsec; /* 納秒 */
};
xtime.tv_sec 存放着自1970年7月1日(UTC)以來經過的時間。1970年7月1日被稱為紀元,Unix牆上時間都是基於該紀元的。
xtime.ntv_sec記錄着自上一秒開始經過的納秒數。
讀寫xtime變量需要用xtime_lock鎖,這是一個seqlock鎖。
更新xtime:
write_seqlock(&xtime_lock);
/* 更新xtime... */
write_sequnlock(&time_lock);
讀取xtime:
/* 循環更新xtime, 直到確認循環期間沒有時鍾中斷處理程序更新xtime */
do {
unsigned long lost;
seq = read_seqbegin(&xtime_lock);
usec = timer->get_offset();
lost = jiffies->wall_jiffies;
if (lost)
usec += lost * (1000000/HZ);
sec = xtime.tv_sec;
usec += (xtime.tv_nsec/1000);
} while(read_seqretry(&xtime_lock, seq));
如果循環期間有時鍾中斷處理程序更新xtime,read_seqretry()會返回無效序列號,繼續循環等待。
從用戶空間取得牆上時間的主要接口:gettimeofday(),內核中對應系統調用sys_gettimeofday():
asmlinkage long sys_gettimeofday(struct timeval* tv, struct timezone* tz)
{
if (likely(tv)) { // <=> if (tv)
struct timeval ktv;
do_gettimeofday(&ktv); // 循環讀取xtime操作
}
if (copy_to_user(tv, &ktv, sizeof(ktv))) // 在給用戶空間拷貝牆上時間或時區
return -EFAULT; // 拷貝時發生錯誤
if (unlikely(tz)) { // <=> if (!tz)
if (copy_to_user(tz, &sys_tz, sizeof(sys_tz))) return -EFAULT;
}
return 0;
}
/* 宏likely和unlikey在內核中定義, 便於編譯器優化, 以提升性能 */
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)
time, ftime, gettimeofday關系
內核也實現了time(), ftime()系統調用,但都被gettimeofday()所取代。為保持向后兼容,Linux還保留着。
- time() 返回從1970年1月1日午夜開始所走過的秒數。
- ftime() 返回一個類型為timeb的數據結構,該結構包含從1970年1月1日午夜開始所走過的秒數;在最后1秒內所走過的毫秒數;時區以及夏令時當前的狀態。
- gettimeofday() 返回的值存放在2個數據結構timeval和timezone,其中包含的信息與ftime相同。
[======]
定時器
定時器被稱為動態定時器或內核定時器,是管理內核時間的基礎。
定時器使用
思路:先進行一些初始化工作,設置一個超時時間,指定超時后執行的函數,然后激活定時器。指定的函數將在定時器到期時自動執行。
定時器不會周期運行,超時后自行銷毀。這是這種定時器被稱為動態定時器的一個原因。因此,動態定時器是在不斷的創建和銷毀,而且運行次數不受限制。在內核中應用非常普遍。
定時器由結構timer_list表示,定義於<linux/timer.h>
struct timer_list {
struct list_head entry; /* 定時器鏈表入口 */
unsigned long expires; /* 以jiffies為單位的定時值 */
spinlock_t lock; /* 保護定時器的鎖 */
void (*function)(unsigned long); /* 定時器處理函數 */
unsigned long data; /* 傳給處理函數的長整型參數 */
struct tvec_t_base_s *base; /* 定時器內部值, 用戶不要使用 */
};
使用定時器不用深入了解timer_list結構。內核提供一組接口簡化管理定時器的操作。
1)定義定時器
struct timer_list my_timer;
2)初始化定時器
init_timer(&my_timer);
3)填充定時器結構中需要的值
my_timer.expires = jiffies + delay; /* 定時器超時節拍數 */
my_timer.data = 0; /* 給定時器處理函數傳入值0 */
my_timer.function = my_function; /* 定時器超時調用的處理函數 */
超時處理函數必須是這種原型:
void my_timer_function(unsigned long data);
4)激活定時器
add_timer(&my_timer);
定時器工作條件:當前節拍計數jiffies >= my_timer.expires
定時器會在超時后馬上執行,但也可能推遲到下一個時鍾節拍,因此不能用於硬實時任務。
5)修改定時器
改變超時時間
mod_timer(&my_timer, jiffies + new_delay); /* new expiration */
mod_timer可用於已經初始化但未激活的定時器;如果定時器未被激活,mod_timer會激活之。
如果調用時,定時器未被激活,函數返回0;否則,返回1.
6)刪除定時器
在定時器超時前定制定時器
del_timer(&my_timer);
激活或未被激活的定時器都可以用該函數,如果未被激活,函數返回0;否則,返回1。
已超時的定時器不需要調用該函數,因為會自動被刪除。
del_timer只能保證定時器將來不會被激活,不保證當前在其他處理器上已運行時會停止。此時,需要用del_timer_sync,等待其他處理器上運行的超時處理函數退出。
del_timer_sync(&my_timer); /* 如果有並發訪問可能性, 推薦優先使用 */
del_timer_sync() 不能在中斷上下文中使用,因為會阻塞。
定時器競爭條件
定時器與當前執行(設置定時器的)代碼是異步的,因此可能存在潛在競爭條件。因此,不能用如下方式替代mod_timer(),來改變定時器的超時時間,因為在多處理器上是不安全的:
/* 用下面代碼替換mod_timer, 修改定時器超時時間是錯誤的 */
del_timer(&my_timer);
my_timer->expires = jiffies + new_delay;
add_timer(&my_timer);
通常,用過用del_timer_sync() 取代del_timer()刪除定時器,避免並發訪問的問題,因為無法確定刪除定時器的時候,它是否在其他處理器上運行。
實現定時器
定時器作為軟中斷在下半部上下文中執行。
時鍾中斷處理程序會執行update_process_timers(),該函數會隨即調用run_local_timers()。
void run_local_timers(void)
{
raise_softirq(TIMER_SOFTIRQ);
}
run_timer_softirq()處理軟件中斷TIMER_SOFTIRQ,從而在當前處理器上運行所有的超時定時器。
內核定時器是以鏈表形式存放,但並沒有遍歷鏈表以尋找超時定時器,也沒有在鏈表中插入和刪除定時器。
而是,將定時器按超時時間分為五組。當定時器超時時間接近時,定時器將隨組一起下移。采用分組定時器的方法可以在執行軟中斷的多數情況下,可以確保內核盡可能減少搜索超時定時器所帶來的負擔。
[======]
延遲執行
內核代碼(尤其驅動程序)除了用定時器或下半部機制外,還需要其他方法來推遲執行任務。
常適用於:短時間等待硬件完成某些工作,比如,重新設計網卡的以太網模式(2ms)。
內核提供多種延遲方法處理各種延遲要求:
1)忙等待
2)短延遲
3)schedule_timeout()
4)設置超時時間,在等待隊列上睡眠
忙等待
忙等待(或稱忙循環),是最簡單的延遲方法,也是最不理想的。
方法僅適用於想要延遲的時間是節拍的整數倍,或者精確度要求不高時使用。
忙循環使用示例:在循環中不斷旋轉直到希望的時鍾節拍數耗盡
unsigned long delay = jiffies + 10; /* 10個節拍 */
while (time_before(jiffies, delay)) /* CPU循環等待 jiffies > delay (自動處理定時器值回繞) */
;
上面循環不斷旋轉,等待10個節拍。HZ值為1000的x86體系結構上,每個節拍1ms,10個節拍總共耗時10ms。
unsigned long delay = jiffies + 2 * HZ; /* 2秒 */
while (time_before(jiffies, delay))
;
上面循環自旋時,並不會放棄CPU。下面cond_resched()將調度一個新程序投入運行,不過只有在設置完need_resched標志后,才能生效。因為cond_resched方法會調用調度程序,因此不能在中斷上下文中使用,而只能在進程上下文中使用。
unsigned long delay = jiffies + 5 * HZ;
while (time_before(jiffies, delay))
cond_resched(); /* 調度一個新程序投入運行 */
注意:
1)所有延遲方法都只能在進程上下文使用,不能在中斷上下文使用。因為中處理程序應盡快執行。
2)延遲執行 不應在持有鎖或者禁止中斷的時候發生。
短延遲
有時驅動程序不但需要很短的延遲(比時鍾節拍typ.為1ms還短),而且要求延遲的時間很精確。不可能使用精度為1ms的jiffies節拍用於延遲。
此時,可以用內核提供的另外2個函數,用於處理微妙和毫秒級延遲。
頭文件:<linux/delay.h>
void udelay(unsigned long usecs);
void mdelay(unsigned long msecs);
mdelay是通過udelay實現的。
如,延遲150微秒,延遲200毫秒
udelay(150); /* 延遲150us */
mdelay(200); /* 延遲200ms */
注意:
1)延遲超過1ms時,不要用udelay,應該用mdelay。
2)能不用則不用mdelay,盡量少用。
3)不要在持有鎖或者禁止中斷時,使用忙等待,因為類似於忙等待,會讓系統響應速度和性能大打折扣。
schedule_timeout() 睡眠到指定延遲時間
該方法會讓需要延遲執行的任務睡眠到指定的延遲時間耗盡后,再重新運行。不能保證睡眠時間剛好等於指定的延遲時間,只能是盡量接近。當指定時間到期后,內核喚醒被延遲的任務並將其重新放回運行隊列。
典型用法:
/* 將任務設置為可中斷睡眠狀態 */
set_current_state(TASK_INTERRUPTIBLE);
unsigned long S = 10;
/* 小睡一會兒,S秒后喚醒 */
schedule_timeout(s * HZ);
唯一的參數是延遲的相對時間,單位jiffies。
如果睡眠時,想接收信號,可將任務狀態設置為TASK_INTERRUPTIBLE;如果不想,可以將任務狀態設置為TASK_UNINTERRUPTIBLE。
注意:調用schedule_timeout()前,必須將任務設置為上面兩種狀態之一,否則任務不會睡眠。
schedule_timeout的簡單實現:
signed long schedule_timeout(singed long timeout)
{
timer_t timer;
unsigned long expire;
switch(timeout)
{ /* 處理特殊情況 */
case MAX_SCHEDULE_TIMEOUT: /* 無限期睡眠 */
schedule(); /* 調度進程: 從就緒隊列中選一個優先級最高的進程來替代當前進程運行 */
goto out;
default:
if (timeout < 0) {
printk(KERN_ERR"schedule_timeout: wrong timeout value %lx from %p\n", timeout, __builtin_return_address(0));
goto out;
}
}
expire = timeout + jiffies;
init_timer(&timer); /* 初始化動態定時器 */
timer.expires = expire;
timer.data = (unsigned long)current;
timer.funtion = process_timeout;
add_timer(&timer); /* 激活定時器 */
schedule();
del_timer_sync(&timer); /* 同步刪除定時器 */
timeout = expire - jiffies;
out:
return timeout < 0 ? 0 : timeout;
}
/* 定時器超時處理函數 */
void process_timeout(unsigned long data)
{
wake_up_progress((task_t *)data); /* 喚醒進程, 將任務設置為TASK_RUNNING */
}
因為任務被標識為TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE(在調用schedule_timeout之前),所以調度程序不會再選擇該任務投入運行,而會選擇其他新任務運行。
設置超時時間,在等待隊列上睡眠
進程上下文中為了等待特定事件發生,會將自己放入等待隊列,然后調用調度程序執行新任務。一旦事件發生,內核可調用wake_up()喚醒在睡眠隊列上的任務,使其重新投入運行。
schedule_timeout用在什么地方?
當等待隊列上的某個任務可能既在等待一個特定事件到來,又在等待一個特定時間到期,看誰先來。此時,可以用schedule_timeout替換schedule(),因為schedule()只是簡單的阻塞等待喚醒事件,而schedule_timeout除了可以等待IO事件,還會等待超時。
[======]
小結
1)講述了時間的基本概念,如牆上時間,時鍾中斷,時鍾節拍,HZ,jiffies等。
2)定時器的實現,應用方法等。
3)開發者用於延遲的方法:忙等待、短延遲、schedule_timeout。
[======]
參考
[1]RobertLove, 洛夫, 陳莉君,等. Linux內核設計與實現[M]. 機械工業出版社, 2006.
