定時器中斷由系統定時硬件以規律地間隔產生; 這個間隔在啟動時由內核根據 HZ 值來編
程, HZ 是一個體系依賴的值, 在 <linux/param.h>中定義或者它所包含的一個子平臺文
件中. 在發布的內核源碼中的缺省值在真實硬件上從 50 到 1200 嘀噠每秒, 在軟件模擬
器中往下到 24. 大部分平臺運行在 100 或者 1000 中斷每秒; 流行的 x86 PC 缺省是
1000, 盡管它在以前版本上(向上直到并且包括 2.4)常常是 100. 作為一個通用的規則,
即便如果你知道 HZ 的值, 在編程時你應當從不依賴這個特定值.
可能改變 HZ 的值, 對于那些要系統有一個不同的時鐘中斷頻率的人. 如果你在頭文件中
改變 HZ 的值, 你需要使用新的值重編譯內核和所有的模塊. 如果你愿意付出額外的時間
中斷的代價來獲得你的目標, 你可能想提升 HZ 來得到你的異步任務的更細粒度的精度.
實際上, 提升 HZ 到 1000 在使用 2.4 或 2.2 內核版本的 x86 工業系統中是相當普遍
的. 但是, 對于當前版本, 最好的方法是保持 HZ 的缺省值, 由于我們完全信任內核開發
者, 他們肯定已經選擇了最好的值. 另外, 一些內部計算當前實現為只為從 12 到 1535
范圍的 HZ (見 <linux/timex.h> 和 RFC-1589).
每次發生一個時鐘中斷, 一個內核計數器的值遞增. 這個計數器在系統啟動時初始化為 0,
因此它代表從最后一次啟動以來的時鐘嘀噠的數目. 這個計數器是一個 64-位 變量( 即
便在 32-位的體系上)并且稱為 jiffies_64. 但是, 驅動編寫者正常地存取 jiffies 變
量, 一個 unsigned long, 或者和 jiffies_64 是同一個或者它的低有效位. 使用
jiffies 常常是首選, 因為它更快, 并且再所有的體系上存取 64-位的 jiffies_64 值不
必要是原子的.
除了低精度的內核管理的 jiffy 機制, 一些 CPU 平臺特有一個高精度的軟件可讀的計數
器. 盡管它的實際使用有些在各個平臺不同, 它有時是一個非常有力的工具.
使用 jiffies 計數器
這個計數器和來讀取它的實用函數位于 <linux/jiffies.h>, 盡管你會常常只是包含
<linux/sched.h>, 它會自動地將 jiffies.h 拉進來. 不用說, jiffies 和 jiffies_64
必須當作只讀的.
無論何時你的代碼需要記住當前的 jiffies 值, 可以簡單地存取這個 unsigned long 變
量, 它被聲明做 volatile 來告知編譯器不要優化內存讀. 你需要讀取當前的計數器, 無
論何時你的代碼需要計算一個將來的時間戳, 如下面例子所示:
#include <linux/jiffies.h>
unsigned long j, stamp_1, stamp_half, stamp_n;
j = jiffies; /* read the current value */
stamp_1 = j + HZ; /* 1 second in the future */
stamp_half = j + HZ/2; /* half a second */
stamp_n = j + n * HZ / 1000; /* n milliseconds */
這個代碼對于 jiffies 回繞沒有問題, 只要不同的值以正確的方式進行比較. 盡管在
32-位 平臺上當 HZ 是 1000 時, 計數器只是每 50 天回繞一次, 你的代碼應當準備面對
這個事件. 為比較你的被緩存的值( 象上面的 stamp_1 ) 和當前值, 你應當使用下面一
個宏定義:
#include <linux/jiffies.h>
int time_after(unsigned long a, unsigned long b);
int time_before(unsigned long a, unsigned long b);
int time_after_eq(unsigned long a, unsigned long b);
int time_before_eq(unsigned long a, unsigned long b);
第一個當 a, 作為一個 jiffies 的快照, 代表 b 之后的一個時間時, 取值為真, 第二個
當 時間 a 在時間 b 之前時取值為真, 以及最后 2 個比較"之后或相同"和"之前或相同".
這個代碼工作通過轉換這個值為 signed long, 減它們, 并且比較結果. 如果你需要以一
種安全的方式知道 2 個 jiffies 實例之間的差, 你可以使用同樣的技巧: diff =
(long)t2 - (long)t1;.
你可以轉換一個 jiffies 差為毫秒, 一般地通過:
msec = diff * 1000 / HZ;
有時, 但是, 你需要與用戶空間程序交換時間表示, 它們打算使用 struct timeval 和
struct timespec 來表示時間. 這 2 個結構代表一個精確的時間量, 使用 2 個成員:
seconds 和 microseconds 在舊的流行的 struct timeval 中使用, seconds 和
nanoseconds 在新的 struct timespec 中使用. 內核輸出 4 個幫助函數來轉換以
jiffies 表達的時間值, 到和從這些結構:
#include <linux/time.h>
unsigned long timespec_to_jiffies(struct timespec *value);
void jiffies_to_timespec(unsigned long jiffies, struct timespec *value);
unsigned long timeval_to_jiffies(struct timeval *value);
void jiffies_to_timeval(unsigned long jiffies, struct timeval *value);
存取這個 64-位 jiffy 計數值不象存取 jiffies 那樣直接. 而在 64-位 計算機體系上,
這 2 個變量實際上是一個, 存取這個值對于 32-位 處理器不是原子的. 這意味著你可能
讀到錯誤的值如果這個變量的兩半在你正在讀取它們時被更新. 極不可能你會需要讀取這
個 64-位 計數器, 但是萬一你需要, 你會高興地得知內核輸出了一個特別地幫助函數,
為你完成正確地加鎖:
#include <linux/jiffies.h>
u64 get_jiffies_64(void);
在上面的原型中, 使用了 u64 類型. 這是一個定義在 <linux/types.h> 中的類型, 在
11 章中討論, 并且表示一個 unsigned 64-位 類型.
如果你在奇怪 32-位 平臺如何同時更新 32-位 和 64-位 計數器, 讀你的平臺的連接腳
本( 查找一個文件, 它的名子匹配 valinux*.lds*). 在那里, jiffies 符號被定義來存
取這個 64-位 值的低有效字, 根據平臺是小端或者大端. 實際上, 同樣的技巧也用在
64-位 平臺上, 因此這個 unsigned long 和 u64 變量在同一個地址被存取.
最后, 注意實際的時鐘頻率幾乎完全對用戶空間隱藏. 宏 HZ 一直擴展為 100 當用戶空
間程序包含 param.h, 并且每個報告給用戶空間的計數器都對應地被轉換. 這應用于
clock(3), times(2), 以及任何相關的函數. 對 HZ 值的用戶可用的唯一證據是時鐘中斷
多快發生, 如在 /proc/interrupts 所顯示的. 例如, 你可以獲得 HZ, 通過用在
/proc/uptime 中報告的系統 uptime 除這個計數值.
處理器特定的寄存器
如果你需要測量非常短時間間隔, 或者你需要非常高精度, 你可以借助平臺依賴的資源,
一個要精度不要移植性的選擇.
在現代處理器中, 對于經驗性能數字的迫切需求被大部分 CPU 設計中內在的指令定時不
確定性所阻礙, 這是由于緩存內存, 指令調度, 以及分支預測引起. 作為回應, CPU 制造
商引入一個方法來計數時鐘周期, 作為一個容易并且可靠的方法來測量時間流失. 因此,
大部分現代處理器包含一個計數器寄存器, 它在每個時鐘周期固定地遞增一次. 現在, 資
格時鐘計數器是唯一可靠的方法來進行高精度的時間管理任務.
細節每個平臺不同: 這個寄存器可以或者不可以從用戶空間可讀, 它可以或者不可以寫,
并且它可能是 64 或者 32 位寬. 在后一種情況, 你必須準備處理溢出, 就象我們處理
jiffy 計數器一樣. 這個寄存器甚至可能對你的平臺來說不存在, 或者它可能被硬件設計
者在一個外部設備實現, 如果 CPU 缺少這個特性并且你在使用一個特殊用途的計算機.
無論是否寄存器可以被清零, 我們強烈不鼓勵復位它, 即便當硬件允許時. 畢竟, 在任何
給定時間你可能不是這個計數器的唯一用戶; 在一些支持 SMP 的平臺上, 例如, 內核依
賴這樣一個計數器來在處理器之間同步. 因為你可以一直測量各個值的差, 只要差沒有超
過溢出時間, 你可以通過修改它的當前值來做這個事情不用聲明獨自擁有這個寄存器.
最有名的計數器寄存器是 TSC ( timestamp counter), 在 x86 處理器中隨 Pentium 引
入的并且在所有從那之后的 CPU 中出現 -- 包括 x86_64 平臺. 它是一個 64-位 寄存器
計數 CPU 的時鐘周期; 它可從內核和用戶空間讀取.
在包含了 <asm/msr.h> (一個 x86-特定的頭文件, 它的名子代表"machine-specific
registers"), 你可使用一個這些宏:
rdtsc(low32,high32);
rdtscl(low32);
rdtscll(var64);
第一個宏自動讀取 64-位 值到 2 個 32-位 變量; 下一個("read low half") 讀取寄存
器的低半部到一個 32-位 變量, 丟棄高半部; 最后一個讀 64-位 值到一個 long long
變量, 由此得名. 所有這些宏存儲數值到它們的參數中.
對大部分的 TSC 應用, 讀取這個計數器的的低半部足夠了. 一個 1-GHz 的 CPU 只在每
4.2 秒溢出一次, 因此你不會需要處理多寄存器變量, 如果你在使用的時間流失確定地使
用更少時間. 但是, 隨著 CPU 頻率不斷上升以及定時需求的提高, 將來你會幾乎可能需
要常常讀取 64-位 計數器.
作為一個只使用寄存器低半部的例子, 下面的代碼行測量了指令自身的執行:
unsigned long ini, end;
rdtscl(ini); rdtscl(end);
printk("time lapse: %li\n", end - ini);
一些其他的平臺提供相似的功能, 并且內核頭文件提供一個體系獨立的功能, 你可用來代
替 rdtsc. 它稱為 get_cycles, 定義在 <asm/timex.h>( 由 <linux/timex.h> 包含).
它的原型是:
#include <linux/timex.h>
cycles_t get_cycles(void);
這個函數為每個平臺定義, 并且它一直返回 0 在沒有周期-計數器寄存器的平臺上.
cycles_t 類型是一個合適的 unsigned 類型來持有讀到的值.
不論一個體系獨立的函數是否可用, 我們最好利用機會來展示一個內聯匯編代碼的例子.
為此, 我們實現一個 rdtscl 函數給 MIPS 處理器, 它與在 x86 上同樣的方式工作.
拖尾的 nop 指令被要求來阻止編譯器在 mfc0 之后馬上存取指令中的目標寄存器. 這種
內部鎖在 RISC 處理器中是典型的, 并且編譯器仍然可以在延遲時隙中調度有用的指令.
在這個情況中, 我們使用 nop 因為內聯匯編對編譯器是一個黑盒并且不會進行優化.
[26]26
#define rdtscl(dest) \
__asm__ __volatile__("mfc0 %0,$9; nop" : "=r" (dest))
有這個宏在, MIPS 處理器可以執行同樣的代碼, 如同前面為 x86 展示的一樣的代碼.
使用 gcc 內聯匯編, 通用寄存器的分配留給編譯器. 剛剛展示的這個宏使用 %0 作為"參
數 0"的一個占位符, 之后它被指定為"任何用作輸出( = )的寄存器( r )". 這個宏還聲
明輸出寄存器必須對應 C 表達式 dest. 內聯函數的語法是非常強大但是有些復雜, 特別
對于那些有限制每個寄存器可以做什么的體系上(就是說, x86 家族). 這個用法在 gcc
文檔中描述, 常常在 info 文檔目錄樹中有.
本節已展示的這個簡短的 C-代碼片段已在一個 K7-級 x86 處理器 和一個 MIPS VR4181
( 使用剛剛描述過的宏 )上運行. 前者報告了一個 11 個時鐘嘀噠的時間流失而后者只是
2 個時鐘嘀噠. 小的數字是期望的, 因為 RISC 處理器常常每個時鐘周期執行一條指令.
有另一個關于時戳計數器的事情值得知道: 它們在一個 SMP 系統中不必要跨處理器同步.
為保證得到一個一致的值, 你應當為查詢這個計數器的代碼禁止搶占.