HZ和jiffies
Linux中的軟定時器(低分辨率的timer_list定時器)利用CPU時鐘中斷來感知時間更新,並通過TIMER_SOFTIRQ軟中斷來運行到期的定時器。時鐘中斷每秒觸發HZ次,HZ的值可在編譯時通過CONFIG_HZ選項來配置。
較高的HZ可使系統具有更好的交互性和相應速度,適合於桌面系統等交互性強的系統,但HZ增高也會導致內核中處理定時中斷以及調用定時器例程更頻繁,使系統開銷也隨之增高。
全局變量jiffies_64就和HZ有關,它是一個64位整型變量,記錄了系統啓動以來時鐘中斷的個數(也就是tick數)。我們知道HZ是每秒鐘產生的時鐘中斷的個數,那麼jiffies_64每秒鐘就增加HZ大小的值,例如,如果HZ=250,那jiffies_64在一秒後會變爲jiffies_64+250,也就是精度爲1000/HZ毫秒。在timer_list定時器中設置到期時間時,我們會用 (jiffies + 5 * HZ) 來表示5秒後到期就是這個道理。
在32位系統上,jiffies_64是一個複合變量,由兩個32位拼接而成,爲了在不同系統上兼容,不要直接讀取這個值而是要藉助get_jiffies_64()訪問。
還有一個32位的unsigned long型變量jiffies,我們用這個變量會更多些。實際上可認爲jiffies_64和jiffies是同一個東西,jiffies直接指向jiffes_64的低4字節,這樣的話,二者總是同時更新的。
jiffies的定義在arch/arm/kernel/vmlinux.lds.S:
#ifndef __ARMEB__
jiffies = jiffies_64;
#else
jiffies = jiffies_64 + 4;
#endif
可見這裏同時定義了jiffies和jiffes_64,並且他們指向相同區域(jiffies取jiffes_64的低4字節,上面區分了一下大小端),因此更新jiffes_64也就同時更新了jiffes。
jiffies_64在內核中的定義如下:
kernel/time.c:
u64 jiffies_64 __cacheline_aligned_in_smp = INITIAL_JIFFIES;
EXPORT_SYMBOL(jiffies_64);
kernel/time/jiffies.c
EXPORT_SYMBOL(jiffies);
注意一點,上面jiffies_64定義的初始值並不是0,而是一個對於32位unsigned long快要回繞的值:
include/linux/jiffies.h
/*
* Have the 32 bit jiffies value wrap 5 minutes after boot
* so jiffies wrap bugs show up earlier.
*/
#define INITIAL_JIFFIES ((unsigned long)(unsigned int) (-300*HZ))
這樣可以使開發者儘早發現是否添加了合法性判斷。爲了讓開發者不受jiffies迴繞的困擾,方便地判斷時間差,內核還提供了四個宏:
time_after(unknown, known) //如果unknow在know之後,則返回true
time_before(unknown, known)
time_after_eq(unknown, known) // >=
time_before_eq(unknown, known)
例如:
unsigned long timeout = jiffies +_ 2 * HZ;
/* ...doing other stuff.. */
if (time_before(jiffies, timeout)) {
/* not timeout */
} else {
/* timeout */
}
使用這個宏,即使jiffies迴繞了,也可以判斷正確。對於jiffies_64上面的宏也有相應的_64版本,但64位的jiffies_64基本不會發生迴繞。
jiffies_64更新是通過do_timer()完成的(kernel/time/timekeeping.c),它在系統範圍內更新jiffies_64的值。
msleep()和udelay()
用戶態的nanosleep和sleep都是可被信號中斷的。在我的內核中,msleep是不可中斷的,msleep_interruptible和do_nanosleep是可中斷的。這些sleep都是通過schedule_timeout()來實現延時的,它的實現就是使用上面的軟定時器,精度也就是1000/HZ毫秒。
由於要經過軟定時器調度以及可能產生的進程切換開銷,msleep()系列函數的精度不高,也就是到不了1000/HZ毫秒的精度,用戶態的nanosleep和sleep更是沒什麼精度了(我usleep 10ms結果可能睡了600ms…),用select睡眠都比usleep()高很多。
內核中還有一個延遲函數udelay(),以及在此基礎上的mdelay()/ndelay()等函數。這個函數不用於睡眠而是用於產生延遲。對於睡眠而言,你在睡的時候沒什麼事兒做,會主動把CPU讓給其他進程;而對於延遲,你只是想等一會再執行後面的代碼,而並沒有想讓出CPU。
udelay是通過忙等來實現的,它一直執行與程序邏輯無關的指令直到到期,讓別人以爲他還在做事情也不肯讓出CPU。它的精度比msleep()要高,因爲額外開銷少一些。
根據CPU主頻可以算出1ms執行多少條指令,通過執行這麼多條指令來達到準確延時的目的。一般一個時鐘週期就是(1/CPU主頻)s,一條指令要經過1到多個時鐘週期,但可能因爲芯片採用超標量或超流水線,理論值就不準確,因此在calibrate_delay()中計算出一個實測值loops_per_jiffy,udelay()的延遲就利用了這個實測值。calibrate_delay()還計算出一個bogomips的值,指的是CPU在1s內實際可以執行的指令數,通常這個值比CPU主頻要大。例如Linux啓動時下面的打印(lpj即loops_per_jiffy):
Calibrating delay loop... 266.24 BogoMIPS (lpj=532480)
亂七八糟
在mips裏面單純地獲得精確時間差可以通過CPU的協處理器(體系相關),例如:
//write_c0_count(0);
clocks_at_start = read_c0_count();
udelay(2 * 1000 * 1000);
clocks_at_end = read_c0_count();
printk("=>=>=>clock count in 2 sec is %x\n", clocks_at_end - clocks_at_start);
上面通過mips的c0寄存器獲得一個不斷遞增的clock counter,精度(通常)爲CPU流水線時鐘速率的一半,即比如CPU是720MHz的,那這個counter就每2/720M秒加1。
假設CPU頻率是720MHz,上面獲得間隔時間就是(clocks_at_end - clocks_at_start) * 1000000 / 360,單位是us。
內核中mips_hpt_frequency全局變量記錄了CPU主頻:
include <asm/time.h>
printk("%d MHz CPU detected\n", mips_hpt_frequency * 2 / 1000000);
如果不知道CPU主頻,可以通過讀取CPU Phase Lock Loop Configuration(CPU_PLL_CONFIG)寄存器來計算CPU主頻,請看CPU datasheet的PLL Frequency的計算方法。