linux HZ
Linux核心幾個重要跟時間有關的名詞或變數,底下將介紹HZ、tick與jiffies。
HZ
Linux核心每隔固定周期會發出timer interrupt (IRQ 0),HZ是用來定義每一秒有幾次timer interrupts。舉例來說,HZ為1000,代表每秒有1000次timer interrupts。 HZ可在編譯核心時設定,如下所示(以核心版本2.6.20-15為例):
adrian@adrian-desktop:~$ cd /usr/src/linux
adrian@adrian-desktop:/usr/src/linux$ make menuconfig
Processor type and features ---》 Timer frequency (250 HZ) ---》
其 中HZ可設定100、250、300或1000。以小弟的核心版本預設值為250。
小實驗
觀察/proc/interrupt的 timer中斷次數,并于一秒后再次觀察其值。理論上,兩者應該相差250左右。
adrian@adrian-desktop:~$ cat /proc/interrupts | grep timer && sleep 1 && cat /proc/interrupts | grep timer
0: 9309306 IO-APIC-edge timer
0: 9309562 IO-APIC-edge timer
上 面四個欄位分別為中斷號碼、CPU中斷次數、PIC與裝置名稱。
要檢查系統上HZ的值是什么,就執行命令
cat /boot/config-`uname -r` | grep ‘^CONFIG_HZ=’
還可以直接更改文件
os/linux/include/asm-cris/param.h?
Tick
Tick是HZ的倒數,意即timer interrupt每發生一次中斷的時間。如HZ為250時,tick為4毫秒(millisecond)。
Jiffies
Jiffies為Linux核心變數(32位元變數,unsigned long),它被用來紀錄系統自開幾以來,已經過多少的tick。每發生一次timer interrupt,Jiffies變數會被加一。值得注意的是,Jiffies于系統開機時,并非初始化成零,而是被設為-300*HZ (arch/i386/kernel/time.c),即代表系統于開機五分鐘后,jiffies便會溢位。那溢位怎么辦?事實上,Linux核心定義幾 個macro(timer_after、time_after_eq、time_before與time_before_eq),即便是溢位,也能藉由這 幾個macro正確地取得jiffies的內容。
另外,80x86架構定義一個與jiffies相關的變數jiffies_64 ,此變數64位元,要等到此變數溢位可能要好幾百萬年。因此要等到溢位這刻發生應該很難吧。那如何經由jiffies_64取得jiffies資訊呢?事 實上,jiffies被對應至jiffies_64最低的32位元。因此,經由jiffies_64可以完全不理會溢位的問題便能取得jiffies。
HZ的設定:
#make menuconfig
processor type and features---》Timer frequency (250 HZ)---》
HZ的不同值會影響timer (節拍)中斷的頻率
2.2 jiffies及其溢出
全局變量jiffies取值為自操作系統啟動以來的時鐘滴答的數目,在頭文 件中定義,數據類型為unsigned long volatile (32位無符號長整型)。關于 jiffies為什么要采用volatile來限定,可參考《關于volatile和jiffies.txt》。
jiffies轉換為秒可采用 公式:(jiffies/HZ)計算,將秒轉換為jiffies可采用公式:(seconds*HZ)計算。
當時鐘中斷發生時,jiffies 值就加1。因此連續累加一年又四個多月后就會溢出(假定HZ=100,1個jiffies等于1/100秒,jiffies可記錄的最大秒數為 (2^32 -1)/100=42949672.95秒,約合497天或1.38年),即當取值到達最大值時繼續加1,就變為了0。
在 Vxworks操作系統中,定義HZ的值為60,因此連續累加兩年又三個多月后也將溢出(jiffies可記錄的最大秒數為約合2.27年)。如果在 Vxworks操作系統上的應用程序對jiffies的溢出沒有加以充分考慮,那么在連續運行兩年又三個多月后,這些應用程序還能夠穩定運行嗎?
下 面我們來考慮jiffies的溢出,我們將從以下幾個方面來闡述:
。 無符號整型溢出的具體過程
。 jiffies溢出造成程序邏輯 出錯
。 Linux內核如何來防止jiffies溢出
。 time_after等比較時間先/后的宏背后的原理
。 代碼中 使用time_after等比較時間先/后的宏
3. 無符號整型溢出的具體過程
我們首先來看看無符號長整型(unsigned long)溢出的具體過程。實際上,無符號整型的溢出過 程都很類似。為了更具體地描述無符號長整型溢出的過程,我們以8位無符號整型為例來加以說明。
8位無符號整型從0開始持續增長,當增長到最大值 255時,繼續加1將變為0,然后該過程周而復始:
0, 1, 2, 。.., 253, 254, 255,
0, 1, 2, 。.., 253, 254, 255,
。..
4. jiffies溢出造成程序邏輯出錯
下面,通過一個例子來看jiffies的溢出。
例4-1:jiffies溢出造成程 序邏輯出錯
unsigned long timeout = jiffies + HZ/2; /* timeout in 0.5s */
/* do some work 。.. */
do_somework();
/* then see whether we took too long */
if (timeout 》 jiffies) {
/* we did not time out, call no_timeout_handler() 。.. */
no_timeout_handler();
} else {
/* we timed out, call timeout_handler() 。.. */
timeout_handler();
}
本 例的目的是從當前時間起,如果在0.5秒內執行完do_somework(),則調用no_timeout_handler()。如果在0.5秒后執行完 do_somework(),則調用timeout_handler()。
我們來看看本例中一種可能的溢出情況,即在設置timeout并執行 do_somework()后,jiffies值溢出,取值為0。設在設置timeout后,timeout的值臨近無符號長整型的最大值,即小于 2^32-1。設執行do_somework()花費了1秒,那么代碼應當調用timeout_handler()。但是當jiffies值溢出取值為0 后,條件timeout 》 jiffies成立,jiffies值(等于0)小于timeout(臨近但小于2^32-1),盡管從邏輯上講 jiffies值要比timeout大。但最終代碼調用的是no_timeout_handler(),而不是timeout_handler()。
5. Linux內核如何來防止jiffies溢出
Linux內核中提供了以下四個宏,可有效地解決由于jiffies溢出而造成程序邏 輯出錯的情況。下面是從Linux Kernel 2.6.7版本中摘取出來的代碼:
/*
* 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)
在宏time_after中,首先確保兩個輸入參數a和b的數據類型為unsigned long,然后才執行實際的比較。這是以后編碼中應當注意 的地方。
6. time_after等比較時間先后的宏背后的原理
那么,上述time_after等比較時間先/后的宏為什么能夠解決 jiffies溢出造成的錯誤情況呢?
我們仍然以8位無符號整型(unsigned char)為例來加以說明。仿照上面的 time_after宏,我們可以給出簡化的8位無符號整型對應的after宏:
#define uc_after(a, b) ((char)(b) - (char)(a) 《 0)
設a和b的數據類型為unsigned char,b為臨近8位無符號整型最大值附近的一個固定值254,下面給出隨著a(設其初始值為254)變 化而得到的計算值:
a b (char)(b) - (char)(a)
254 254 0
255 - 1
0 - 2
1 - 3
。..
124 -126
125 -127
126 -128
127 127
128 126
。..
252 2
253 1
從上面的計算可以看出,設定b不變,隨著a(設其初始值為254)不斷增長1,a的取值變化為:
254, 255, (一次產生溢出)
0, 1, 。.., 124, 125, 126, 127, 126, 。.., 253, 254, 255, (二 次產生溢出)
0, 1, 。..
。..
而(char)(b) - (char)(a)的變化為:
0, -1,
-2, -3, 。.., -126, -127, -128, 127, 126, 。.., 1, 0, -1,
-2, -3, 。..
。..
從上面的詳細過程可以看出,當a取值為254,255, 接著在(一次產生溢出)之后變為0,然后增長到127之前,uc_after(a,b)的 結果都顯示a是在b之后,這也與我們的預期相符。但在a取值為127之后,uc_after(a,b)的結果卻顯示a是在b之前。
從上面的運算 過程可以得出以下結論:
使用uc_after(a,b)宏來計算兩個8位無符號整型a和b之間的大小(或先/后,before/after), 那么a和b的取值應當滿足以下限定條件:
。 兩個值之間相差從邏輯值來講應小于有符號整型的最大值。
。 對于8位無符號整型,兩個值 之間相差從邏輯值來講應小于128。
從上面可以類推出以下結論:
對于time_after等比較jiffies先/后的宏,兩個值的 取值應當滿足以下限定條件:
兩個值之間相差從邏輯值來講應小于有符號整型的最大值。
對于32位無符號整型,兩個值之間相差從邏輯值來 講應小于2147483647。
對于HZ=100,那么兩個時間值之間相差不應當超過2147483647/100秒 = 0.69 年 = 248.5天。對于HZ=60,那么兩個時間值之間相差不應當超過2147483647/60秒 = 1.135年。在實際代碼應用中,需要比較 先/后的兩個時間值之間一般都相差很小,范圍大致在1秒~1天左右,所以以上time_after等比較時間先/后的宏完全可以放心地用于實際的代碼 中。
7. 代碼中使用time_after等比較時間先/后的宏
下面代碼是針對例4-1修改后的正確代碼:
例7-1:在例4-1基 礎上使用time_before宏后的正確代碼
unsigned long timeout = jiffies + HZ/2; /* timeout in 0.5s */
/* do some work 。.. */
do_somework();
/* then see whether we took too long */
if (time_before(jiffies, timeout)) {
/* we did not time out, call no_timeout_handler() 。.. */
no_timeout_handler();
} else {
/* we timed out, call timeout_handler() 。.. */
timeout_handler();
}
8. 結論
系統中采用jiffies來計算時間,但由于jiffies溢出可能造成時間比較的錯誤,因而強烈建議在編碼中使用 time_after等宏來比較時間先后關系,這些宏可以放心使用。
內核時鐘:
內核使用硬件提供的不同時鐘來提供依賴于時間的服務,如busy-waiting(浪費CPU周期)和sleep-waiting(放棄CPU)
HZ and Jiffies
系統時鐘以可編程的頻率中斷處理器,這個頻率即每秒滴答數,記錄在HZ,選擇合適的HZ值是個重要問題,而且HZ值是架構相關的,可以在配置菜單中修改。 【2.6.21核提供了對tickless kernel(CONFIG_NO_HZ)的支持,它會根據系統負載動態觸發時鐘中斷】。
jiffies記錄了系統啟動后的滴答數,常用的函數:time_before()、 time_after()、time_after_eq()、time_before_eq()。因為jiffies隨時鐘滴答變化,不能 用編譯器優化它,應取volatile值。
32位jiffies變量會在50天后溢出,太小,因此內核提供變量jiffies_64來hold 64位jiffies。該64位的低32位即為jiffies,在32位機上需要兩天指令來賦值64位數據,不是原子的,因此內核提供函數 get_jiffies_64()。
Long Delays
busy-wait:timebefore(),使CPU忙等待;sleep-wait:shedule_timeout(截至時間);無論在內核空間還 是用戶空間,都沒有比HZ更精確的控制了,因為時間片都是根據滴答更新的,而且即使定義了您的進程在超過指定時間后運行,調度器也可能根據優先級選擇其他 進程執行。
sleep-wait():wait_event_timeout()用于在滿足某個條件或超時后重新執行,msleep()睡眠指定的ms后重新進入就 緒隊列,這些長延遲僅適用于進程上下文,在中斷上下文中不能睡眠也不能長時間busy-waiting。
內核提供了timer API來在一定時間后執行某個函數:
#include
struct timer_list my_timer;
init_timer(&my_timer); /* Also see setup_timer() */
my_timer.expire = jiffies + n*HZ; /* n is the timeout in number
of seconds */
my_timer.function = timer_func; /* Function to execute
after n seconds */
my_timer.data = func_parameter; /* Parameter to be passed
to timer_func */
add_timer(&my_timer); /* Start the timer */
如果您想周期性執行上述代碼,那么把它們加入timer_func()函數。您使用mod_timer()來改變my_timer的 超時值,del_timer()來刪掉my_timer, 用timer_pending()查看是否my_timer處于掛起狀態。
用戶空間函數clock_settime()和clock_gettime()用 于獲取內核時鐘服務。用戶應用程序使用setitimer()和getitimer()來控制alarm信號的傳遞當指定超時發生后。
Short Delays
前面討論的sleep-wait不適用,只能用busy-wait方法。內核用于short delay的有mdelay()、udelay()、ndelay()。
Busy-waiting對于short durations的實現是通過衡量處理器執行一條指令和必要數量的反復loop來實現的,根據loops_per_jiffy實現。
Pentium Time Stamp Counter
Time Stamp Counter(TSC)是一個64位寄存器,僅出現在奔騰兼容機上,記錄了自啟動后處理器消耗的clock cycles數目。TSC是以processor cycle速率增長的,使用rdtsc()讀 取,提供微秒級的精確度。TSC ticks可以通過除以CPU時鐘速率來轉換成秒數,這可以從cpu_khz讀 取。
unsigned long low_tsc_ticks0, high_tsc_ticks0;
unsigned long low_tsc_ticks1, high_tsc_ticks1;
unsigned long exec_time;
rdtsc(low_tsc_ticks0, high_tsc_ticks0); /* Timestamp
before */
printk(“Hello World\n”); /* Code to be
profiled */
rdtsc(low_tsc_ticks1, high_tsc_ticks1); /* Timestamp after */
exec_time = low_tsc_ticks1 - low_tsc_ticks0;
從核2.6.21已經提供high-resolution timers(CONFIG_HIGH_RES_TIMERS), 它利用硬件相關高速時鐘來提供高精確度的功能函數如nanosleep()。奔騰機上使用 TSC實現該功能。
Real Time Clock
RTC時鐘track絕對時間,記錄在非易失性存儲器中。RTC電池常超過computer生存期。可以用RTC完成以下功能:(1)、讀或設置絕對時 鐘,并在clock updates時產生中斷;(2)以2HZ到8192HZ來產生周期性中斷;(3)設置alarms。
jiffies僅是相對于系統啟動的相對時間,如果想獲取absolute time或wall time,則需要使用RTC,內核用變量xtime來記錄,當系統啟動時,讀取RTC并記錄 在xtime中,當系統halt時,則將wall time寫回RTC,函數do_gettimeofday()來讀取wall time。
#include
static struct timeval curr_time;
do_gettimeofday(&curr_time);
my_timestamp = cpu_to_le32(curr_time.tv_sec); /* Record timestamp */
用戶空間獲取wall time的函數:time()返回calendar time或從00:00:00 on January 1,1970的秒數;(2)localtime():返回calendar time in broken-down format;(3)mktime():與 localtime()相反;(4)gettimeofday()以microsecond 精確度返回calendar時間(需硬件支持)
另外一個獲取RTC的方法是通過字符設備/dev/rtc,一個時刻僅允許一個處理器訪問它。
時鐘和定時器對Linux內核來說十分重要。首先內核要管理系統的運行時間(uptime)和當前墻上時間(wall time), 即當前實際時間。其次,內核中大量的活動由時間驅動(time driven)。其中一些活動是周期性的,比如調度調度器(scheduler)中的運行隊列(runqueue)或者刷新屏幕這樣的活動,它們以固有的 頻率定時發生;同時,內核要非周期性地調度某些函數在未來某個時間發生,比如推遲執行的磁盤I/O操作等。
---------------------------------------------------------
內核必須借助硬件來實現時間管理。實時時鐘(real time clock)是用來持久存放系統時間的設備,它與CMOS集成在一起,并通過主板電池供電,所以即便在關閉計算機系統之后,實時時鐘仍然能繼續工作。
系統啟動時,內核讀取實時時鐘,將所讀的時間存放在變量xtime中作為墻上時間(wall time),xtime保存著從1970年1月1日0:00到當前時刻所經歷的秒數。雖然在Intel x86機器上,內核會周期性地將當前時間存回實時時鐘中,但應該明確,實時時鐘的主要作用就是在啟動時初始化墻上時間xtime。
系統定時器與動態定時器
---------------------------------------------------------
周期性發生的事件都是由系統定時器(system timer)驅動。在X86體系結構上,系統定時器通常是一種可編程硬件芯片(如8254 CMOS芯片),又稱可編程間隔定時器(PIT, Programmable Interval Timer),其產生的中斷就是時鐘中斷(timer interrupt)。時鐘中斷對應的處理程序負責更新系統時間和執行周期性運行的任務。系統定時器的頻率稱為節拍率(tick rate),在內核中表示為HZ。
以X86為例,在2.4之前的內核中其大小為100; 從內核2.6開始,HZ = 1000, 也就是說每秒時鐘中斷發生1000次。這一變化使得系統定時器的精度(resolution)由10ms提高到1ms,這大大提高了系統對于時間驅動事件 調度的精確性。過于頻繁的時鐘中斷不可避免地增加了系統開銷(overhead),但是總的來說,在現在計算機系統上,HZ = 1000不會導致難以接受的系統開銷。
與系統定時器相對的是動態定時器(dynamic timer),它是調度事件(執行調度程序)在未來某個時刻發生的時機。內核可以動態地創建或銷毀動態定時器。
系統定時器及其中斷處理程序是內核管理機制的中樞,下面是一些利用系統定時器周期執行的工作(中斷處理程序所做的工作):
(1) 更新系統運行時間(uptime)
(2) 更新當前墻上時間(wall time)
(3) 在對稱多處理器系統(SMP)上,均衡調度各處理器上的運行隊列
(4) 檢查當前進程是否用完了時間片(time slice),如果用盡,則進行重新調度
(5) 運行超時的動態定時器
(6) 更新資源耗盡和處理器時間的統計值
內核動態定時器依賴于系統時鐘中斷,因為只有在系統時鐘中斷發生后內核才會去檢查當前是否有超時的動態定時器。
X86體系結構中時鐘資源還包括CPU本地APIC(local Advanced Programmable Interrupt Controller)中的定時器和時間戳計時器TSC(Time Stamp Counter)。高精度定時器將使用CPU本地APIC作為高精度定時中斷源。
高精度定時器的設計與實現
---------------------------------------------------------
X86體系結構中,內核2.6.X的HZ = 1000, 即系統時鐘中斷執行粒度為1ms,這意味著系統中周期事情最快為1ms執行一次,而不可能有更高的精度。動態定時器隨時都可能超時,但由于只有在系統時鐘 中斷到來時內核才會檢查執行超時的動態定時器,所以動態定時器的平均誤差大約為半個系統時鐘周期(即0.5ms)。
對于實時要求較高的電信應用來說,普通Linux在實時性方面與電信平臺的要求之間還存在一定的差距。CGL為了增強Linux的軟實時能力,在以下方面 對內核進行了改進:提供高精度的動態定時器;提供可搶占式內核(preemption kernel)等。下面主要介紹高精度實時器的設計思想及實現,該實現遵循PISIX 1003.1b中時鐘和定時器相關API標準,方便應用程序開發人員的使用。
高精度定時器的基本設計思想為:用 (jiffies+sub_jiffie)表示動態定時器的超時時間,PIT仍然按頻率HZ = 1000產生系統時鐘中斷。如果在一個時鐘中斷tick與下一個時鐘中斷(tick+1)之間,即[jiffies, jiffies+1)之間,有高精度動態定時器等待處理(超時時間表示為(jiffies+sub_jiffie), sub_jiffie 《 1), 那么用最近的動態定時器超時值sub_jiffie對硬件定時器(PIT或local APIC)進行設定,使其在時刻(jiffies+sub_jiffie)產生中斷,通知內核對該高精度定時器進行處理。而不必總是等到系統時鐘中斷到來 后才檢查執行所有超時的定時器,從而達到提高動態定時器精度的目的。
高精度定時器的中斷源
---------------------------------------------------------
高精度定時器在內核中,仍然使用可編程間隔定時器PIC產生每秒HZ次的系統時鐘中斷,對于采用哪種硬件定時器產生高精度定時器中斷則取決于CPU上是否 有本地APIC。若CPU上沒有本地APIC,那么仍可使用PIT產生高精度定時中斷,雖然這種然PIT即產生系統時鐘中斷又產生高精度定時器中斷的做法 效率不高。
獲取高精度定時器的發生時間
---------------------------------------------------------
高精度定時器發生在連續兩個jiffies之間(即時刻(jiffies+sub_jiffie)),要確定其產生時間,就必須確定sub_jiffie 的大小。通過函數get_arch_cycles(ref_jiffies)可獲取sub_jiffie的值,sub_jiffe以CPU時鐘周期為最小 計時單位。函數具體實現思想是,通過訪問計數器TSC,計算從上一個jiffies到當前時刻ref_jiffies之間的TSC差值,最終確定 sub_jiffies的大小。
The high-resolution timer API
---------------------------------------------------------
Last September, this page featured an article on the ktimers patch by Thomas Gleixner. The new timer abstraction was designed to enable the provision of high-resolution timers in the kernel and to address some of the inefficiencies encountered when the current timer code is used in this mode. Since then, there has been a large amount of discussion, and the code has seen significant work. The end product of that work, now called “hrtimers,” was merged for the 2.6.16 release.
At its core, the hrtimer mechanism remains the same. Rather than using the “timer wheel” data structure, hrtimers live on a time-sorted linked list, with the next timer to expire being at the head of the list. A separate red/black tree is also used to enable the insertion and removal of timer events without scanning through the list. But while the core remains the same, just about everything else has changed, at least superficially.
struct ktime_t
-----------------------------
There is a new type, ktime_t, which is used to store a time value in nanoseconds. This type, found in , is meant to be used as an opaque structure. And, interestingly, its definition changes depending on the underlying architecture. On 64-bit systems, a ktime_t is really just a 64-bit integer value in nanoseconds. On 32-bit machines, however, it is a two-field structure: one 32-bit value holds the number of seconds, and the other holds nanoseconds. The order of the two fields depends on whether the host architecture is big-endian or not; they are always arranged so that the two values can, when needed, be treated as a single, 64-bit value. Doing things this way complicates the header files, but it provides for efficient time value manipulation on all architectures.
struct--ktime_t
typedef union {
On 64-bit systems
|----------------------|
| s64 tv64; |
|----------------------|
On 32-bit machines
|----------------------|
| struct { |
| s32 sec, nsec; |
| s32 nsec, sec; |
| } tv; |
|----------------------|
} ktime_t;
初始化ktime_t
-----------------------------
A whole set of functions and macros has been provided for working with ktime_t values, starting with the traditional two ways to declare and initialize them(ktime_t values):
(1) Initialize to zero
DEFINE_KTIME(name);
(2) ktime_t kt;
kt = ktime_set(long secs, long nanosecs);
初始化高精度時鐘hrtimer
-----------------------------
The interface for hrtimers can be found in 。 A timer is represented by struct hrtimer, which must be initialized with:
void hrtimer_init(struct hrtimer *timer, clockid_t which_clock);
System clocks
-----------------------------
Every hrtimer is bound to a specific clock. The system currently supports two clocks, being:
* CLOCK_MONOTONIC: a clock which is guaranteed always to move forward in time, but which does not reflect “wall clock time” in any specific way. In the current implementation, CLOCK_MONOTONIC resembles the jiffies tick count in that it starts at zero when the system boots and increases monotonically from there.
* CLOCK_REALTIME which matches the current real-world time.
The difference between the two clocks can be seen when the system time is adjusted, perhaps as a result of administrator action, tweaking by the network time protocol code, or suspending and resuming the system. In any of these situations, CLOCK_MONOTONIC will tick forward as if nothing had happened, while CLOCK_REALTIME may see discontinuous changes. Which clock should be used will depend mainly on whether the timer needs to be tied to time as the rest of the world sees it or not. The call to hrtimer_init() will tie an hrtimer to a specific clock, but that clock can be changed with:
void hrtimer_rebase(struct hrtimer *timer, clockid_t new_clock);
hrtimer_start()
-----------------------------
Actually setting a timer is accomplished with:
int hrtimer_start(struct hrtimer *timer,
ktime_t time,
enum hrtimer_mode mode);
The mode parameter describes how the time parameter should be interpreted. A mode of HRTIMER_ABS indicates that time is an absolute value, while HRTIMER_REL indicates that time should be interpreted relative to the current time.
評論
查看更多