幾個重要的名詞
- HZ:系統定時器頻率HZ用來定義系統定時器每隔1秒產生多少個時鐘中斷
- Tick:HZ的倒數,系統定時器兩次時鐘中斷的時間間隔
- Xtime:記錄Wall time值,也就是UTC時間,是一個struct timeval結構,在使用者空間通過gettimeofday讀取
- Jiffies:記錄系統開機以來經過了多少次Tick,定義為unsigned long volatile __jiffy_data jiffies;
- RTC:系統時鐘,是一個硬體時鐘,用來持久存放系統時間
HZ
HZ是靜態編譯到核心中的,其定義如下:
// /usr/include/asm-generic/param.h檔案中
#ifdef __KERNEL__
# define HZ CONFIG_HZ /* Internal kernel timer frequency */
# define USER_HZ 100 /* some user interfaces are */
# define CLOCKS_PER_SEC (USER_HZ) /* in "ticks" like times() */
#endif
#ifndef HZ
#define HZ 100
#endif
在2.6以前的核心中,如果改變核心中的HZ值會給使用者空間中某些程式造成異常結果。因為核心是以節拍數/秒的形式給使用者空間匯出這個值的,應用程式便依賴這個特定的HZ值。如果在核心中改變了HZ的定義值,就打破了使用者空間的常量關係---使用者空間並不知道新的HZ值。
核心更改所有匯出的jiffies值。核心定義了USER_HZ來代表使用者空間看到的HZ值。在x86體繫結構上,由於HZ值原來一直是100,所以USER_HZ值就定義為100。核心可以使用宏jiffies_to_clock_t()將一個有HZ表示的節拍計數轉換為一個由USER_HZ表示的節拍計數:
start=jiffies;
//doing some jobs
total_time=jiffies-start;
ticks=jiffies_to_clock_t(total_time);
jiffies_to_clock_t()函數的定義如下:
/*
* Convert jiffies/jiffies_64 to clock_t and back.
*/
clock_t jiffies_to_clock_t(unsigned long x)
{
#if (TICK_NSEC % (NSEC_PER_SEC / USER_HZ)) == 0
# if HZ < USER_HZ
return x * (USER_HZ / HZ);
# else
return x / (HZ / USER_HZ);
# endif
#else
return div_u64((u64)x * TICK_NSEC, NSEC_PER_SEC / USER_HZ);
#endif
}
Jiffies
Jiffies記錄了系統啟動以來所經過的ticks數,在開機時該值是0,隨後每次時鐘中斷髮生時加1.
根據jiffies可以計算系統的開機時間:jiffies/HZ 秒
Jiffies的定義為unsigned long volatile __jiffy_data jiffies;
由於jiffies存在溢出的可能,核心定義了一組輔助函數來處理jiffies的比較操作,操作jiffies時最好使用這些輔助函數:
/*
* These inlines deal with timer wrapping correctly. You are
* strongly encouraged to use them
* 1. Because people otherwise forget
* 2. Because if the timer wrap changes in future you won't have to
* alter your driver code.
*
* time_after(a,b) returns true if the time a is after time b.
*
* Do this with "<0" and ">=0" to only test the sign of the result. A
* good compiler would generate better code (and a really good compiler
* wouldn't care). Gcc is currently neither.
*/
#define time_after(a,b) \
(typecheck(unsigned long, a) && \
typecheck(unsigned long, b) && \
((long)(b) - (long)(a) < 0))
#define time_before(a,b) time_after(b,a)
#define time_after_eq(a,b) \
(typecheck(unsigned long, a) && \
typecheck(unsigned long, b) && \
((long)(a) - (long)(b) >= 0))
#define time_before_eq(a,b) time_after_eq(b,a)
系統時鐘RTC
系統時鐘是一個硬體時鐘,用來持久存放系統時間,系統關閉後靠主板上的微型電池保持計時;
系統啟動時,核心通過讀取RTC來初始化Wall Time, 並存放在xtime變數中,這是RTC最主要的作用;
當使用者修改了時間後,可以用hwclock –w將其儲存到RTC中。
系統定時器
每個PC機中都有一個PIT,以通過IRQ0產生周期性的時鐘中斷訊號,作為系統定時器 system timer。當發生時鐘中斷時,就會自動調用時鐘中斷處理常式。
時鐘中斷處理常式分為兩個部分:體繫結構相關部分和體繫結構無關部分。相關的部分作為系統定時器的中斷處理常式而註冊到核心中,以便在產生時鐘中斷時,它能夠相應地運行。執行的工作如下:
1.獲得xtime_lock鎖,以便對訪問jiffies_64和牆上時間xtime進行保護。
2.需要時應答或重新設定系統時鐘。
3.周期性地使用牆上時間更新系統時鐘。
4.調用體繫結構無關的時間常式:do_timer().
中斷服務程式主要通過調用與體繫結構無關的常式do_timer()執行下面的工作:
1.給jiffies_64變數加1.
2.更新資源消耗的統計值,比如當前進程所消耗的系統時間和使用者時間。
3.執行已經到期的動態定時器.
4.執行scheduler_tick()函數.
5.更新牆上時間,該時間存放在xtime變數中.
6.計算平均負載值.
Xtime
記錄Wall time值,也就是UTC時間,是一個struct timeval結構。
在核心空間讀寫這個xtime變數需要xtime_lock鎖,該鎖是一個順序鎖(seqlock)。
而在使用者空間通過gettimeofday讀取xtime,它在核心中對應系統調用為sys_gettimeofday()。
動態定時器
動態定時器並不周期執行,它在逾時後就自行銷毀。定義器由定義在linux/timer.h中的time_list表示,如下:
struct timer_list {
/*
* All fields that change during normal runtime grouped to the
* same cacheline
*/
struct list_head entry;
unsigned long expires;
struct tvec_base *base;
void (*function)(unsigned long);
unsigned long data;
int slack;
}
核心提供了一組函數用來簡化管理定時器的操作。所有這些介面都聲明在檔案linux/timer.h中,大多數介面在檔案kernel/timer.c中獲得實現。有了這些介面,我們要做的事情就很簡單了:
1.建立定時器:struct timer_list my_timer;
2.初始化定時器:init_timer(&my_timer);
3.根據需要,設定定時器了:
my_timer.expires = jiffies + delay;
my_timer.data = 0;
my_timer.function = my_function;
4.啟用定時器:add_timer(&my_timer);
經過上面的幾步,定時器就可以開始工作了。然而,一般來說,定時器都在逾時後馬上就會執行,但是也有可能被延遲到下一時鐘節拍時才能運行,所以不能使用它來實現硬即時。
如果修改定時器,使用mod_timer(&my_timer,jiffies+new_delay)來修改已經啟用的定時器時間。它也可以操作那些已經初始化,但還沒有被啟用的定時器,如果定時器未被啟用,mod_timer會啟用它。如果調用時定時器時未被啟用,該函數返回0;否則返回1。但不論哪種情況,一旦從mod_timer函數返回,定時器都將被啟用而且設定了新的定時值。
當然你也可以在逾時前刪除定時器用:del_timer(&my_timer); 另外需要注意的是在多處理器上定時器中斷可能已經在其它機器上運行了,這是就需要等待可能在其它處理器上啟動並執行定時器處理常式都退出後再刪除該定時器。這是就要使用del_timer_sync()函數執行刪除工作。這個函數參數和上面一個一樣,只是不能在中斷上下文中使用而已。定時器是獨立與當前代碼的,這意味著可能存在競爭條件,這個就要特別小心,從這個意義上講後者刪除比前者更加安全。
順延強制
核心代碼(尤其是驅動程式)除了使用定時器或下半部機制以外還提供了許多延遲的方法來處理各種延遲請求。
- 忙等待(也叫忙迴圈):通常是最不理想的方法,因為處理器被白白佔用而無法做其他的事情。該方法僅僅在想要延遲的時間是節拍的整數倍或者精確率要求不高時才可以使用。實現起來還是挺簡單的,就是在迴圈中不斷旋轉直到希望的時鐘節拍數耗盡。比如:
unsigned long delay = jiffies+10; //10個節拍
while(time_before(jiffies,delay)) ;
缺點很明顯,更好的方法是在代碼等待時,允許核心重新調度執行其他任務,如下:
unsigned long delay = jiffies+10; //10個節拍
while(time_before(jiffies,delay))
cond_resched();
cond_resched()函數將調度一個新程式投入運行,但它只有在設定完need_resched標誌後才會生效。換句話說,就是系統中存在更重要的任務需要運行。再由於該方法需要調用發送器,所以它不能在中斷上下文中使用----只能在進程上下文中使用。事實上,所有延遲方法在進程上下文中使用,因為中斷處理常式都應該儘可能快的執行。另外,順延強制不管在哪種情況下都不應該在持有鎖時或者禁止中斷時發生。
- udelay mlelay
至於說那些需要很短暫的延遲(比時鐘節拍還短)而且還要求延遲的時間很精確,這種情況多發生在和硬體同步時,也就是說需要短暫等待某個動作的完成----等待時間往往小於1ms,所以不可能使用像前面例子中那種基於jiffies的延遲方法。這時,就可以使用在linux/delay.h中定義的兩個函數,它們不使用,這兩個函數可以處理微秒和毫秒層級的延遲的時間,如下所示:
void udelay(unsigned long usecs);
void mdelay(unsigned long msecs);
前者是依靠執行次數迴圈來達到延遲效果的,而mdelay()函數又是通過udelay()函數實現的。因為核心知道處理器在一秒內能執行多少次迴圈,所以udelay()函數僅僅需要根據指定的延遲時間在1秒中占的比例,就能決定需要進行多少次迴圈就能達到需要的延遲時間。udelay()函數僅能在要求的延遲時間很短的情況下執行,而在高速機器中時間很長的延遲會造成溢出,經驗表明,不要試圖在延遲超過1ms的情況下使用這個函數。這兩個函數其實和忙等待一樣,如果不是非常必要,還是不要用了算了。
- schedule_timeout
更理想的順延強制方法是使用schedule_timeout()函數,該方法會讓需要順延強制的任務睡眠到指定的延遲時間耗盡後再重新運行。但該方法也不能保證睡眠時間正好等於指定的延遲時間----只能盡量是睡眠時間接近指定的延遲時間。當指定的時間到期後,核心喚醒被延遲的任務並將其重新放回運行隊列,如下:
set_current_state(TASK_INTERRUPTIBLE);
schedule_timeout(s*HZ);
唯一的參數是延遲的相對時間,單位是jiffies,上例中將相應的任務推入可中斷睡眠隊列,睡眠s秒。在調用函數schedule_timeout之前,要將任務設定成可中斷或不和中斷的一種,否則任務不會休眠。這個函數需要調用發送器,所以調用它的代碼必須保證能夠睡眠,簡而言之,調用代碼必須處於進程上下文中,並且不能持有鎖。
最後,等待隊列上的某個任務可能既在等待一個特定事件到來,又在等待一個特定時間到期——就看誰來得更快。這種情況下,代碼可以簡單的使用scedule_timeout()函數代替schedule()函數,這樣一來,當希望指定時間到期後,任務都會被喚醒,當然,代碼需要檢查被喚醒的原因----有可能是被事件喚醒,也有可能是因為延遲的時間到期,還可能是因為接收到了訊號——然後執行相應的操作。
參考資料:
http://blog.csdn.net/zhandoushi1982/article/details/5536210
http://blog.csdn.net/qinzhonghello/article/details/3588224
《Linux Kernel Development》