Linux中的jiffies介紹

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的計算方法。

發佈了40 篇原創文章 · 獲贊 13 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章