7.1. 測量時間流失

2018-02-24 15:49 更新

7.1.?測量時間流失

內(nèi)核通過定時器中斷來跟蹤時間的流動. 中斷在第 10 章詳細(xì)描述.

定時器中斷由系統(tǒng)定時硬件以規(guī)律地間隔產(chǎn)生; 這個間隔在啟動時由內(nèi)核根據(jù) HZ 值來編程, HZ 是一個體系依賴的值, 在 中定義或者它所包含的一個子平臺文件中. 在發(fā)布的內(nèi)核源碼中的缺省值在真實硬件上從 50 到 1200 嘀噠每秒, 在軟件模擬器中往下到 24. 大部分平臺運行在 100 或者 1000 中斷每秒; 流行的 x86 PC 缺省是 1000, 盡管它在以前版本上(向上直到并且包括 2.4)常常是 100. 作為一個通用的規(guī)則, 即便如果你知道 HZ 的值, 在編程時你應(yīng)當(dāng)從不依賴這個特定值.

可能改變 HZ 的值, 對于那些要系統(tǒng)有一個不同的時鐘中斷頻率的人. 如果你在頭文件中改變 HZ 的值, 你需要使用新的值重編譯內(nèi)核和所有的模塊. 如果你愿意付出額外的時間中斷的代價來獲得你的目標(biāo), 你可能想提升 HZ 來得到你的異步任務(wù)的更細(xì)粒度的精度. 實際上, 提升 HZ 到 1000 在使用 2.4 或 2.2 內(nèi)核版本的 x86 工業(yè)系統(tǒng)中是相當(dāng)普遍的. 但是, 對于當(dāng)前版本, 最好的方法是保持 HZ 的缺省值, 由于我們完全信任內(nèi)核開發(fā)者, 他們肯定已經(jīng)選擇了最好的值. 另外, 一些內(nèi)部計算當(dāng)前實現(xiàn)為只為從 12 到 1535 范圍的 HZ (見 和 RFC-1589).

每次發(fā)生一個時鐘中斷, 一個內(nèi)核計數(shù)器的值遞增. 這個計數(shù)器在系統(tǒng)啟動時初始化為 0, 因此它代表從最后一次啟動以來的時鐘嘀噠的數(shù)目. 這個計數(shù)器是一個 64-位 變量( 即便在 32-位的體系上)并且稱為 jiffies_64. 但是, 驅(qū)動編寫者正常地存取 jiffies 變量, 一個 unsigned long, 或者和 jiffies_64 是同一個或者它的低有效位. 使用 jiffies 常常是首選, 因為它更快, 并且再所有的體系上存取 64-位的 jiffies_64 值不必要是原子的.

除了低精度的內(nèi)核管理的 jiffy 機制, 一些 CPU 平臺特有一個高精度的軟件可讀的計數(shù)器. 盡管它的實際使用有些在各個平臺不同, 它有時是一個非常有力的工具.

7.1.1.?使用 jiffies 計數(shù)器

這個計數(shù)器和來讀取它的實用函數(shù)位于 , 盡管你會常常只是包含 , 它會自動地將 jiffies.h 拉進來. 不用說, jiffies 和 jiffies_64 必須當(dāng)作只讀的.

無論何時你的代碼需要記住當(dāng)前的 jiffies 值, 可以簡單地存取這個 unsigned long 變量, 它被聲明做 volatile 來告知編譯器不要優(yōu)化內(nèi)存讀. 你需要讀取當(dāng)前的計數(shù)器, 無論何時你的代碼需要計算一個將來的時間戳, 如下面例子所示:

#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-位 平臺上當(dāng) HZ 是 1000 時, 計數(shù)器只是每 50 天回繞一次, 你的代碼應(yīng)當(dāng)準(zhǔn)備面對這個事件. 為比較你的被緩存的值( 象上面的 stamp_1 ) 和當(dāng)前值, 你應(yīng)當(dāng)使用下面一個宏定義:

#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);

第一個當(dāng) a, 作為一個 jiffies 的快照, 代表 b 之后的一個時間時, 取值為真, 第二個當(dāng) 時間 a 在時間 b 之前時取值為真, 以及最后 2 個比較"之后或相同"和"之前或相同". 這個代碼工作通過轉(zhuǎn)換這個值為 signed long, 減它們, 并且比較結(jié)果. 如果你需要以一種安全的方式知道 2 個 jiffies 實例之間的差, 你可以使用同樣的技巧: diff = (long)t2 - (long)t1;.

你可以轉(zhuǎn)換一個 jiffies 差為毫秒, 一般地通過:

msec = diff * 1000 / HZ; 

有時, 但是, 你需要與用戶空間程序交換時間表示, 它們打算使用 struct timeval 和 struct timespec 來表示時間. 這 2 個結(jié)構(gòu)代表一個精確的時間量, 使用 2 個成員: seconds 和 microseconds 在舊的流行的 struct timeval 中使用, seconds 和 nanoseconds 在新的 struct timespec 中使用. 內(nèi)核輸出 4 個幫助函數(shù)來轉(zhuǎn)換以 jiffies 表達(dá)的時間值, 到和從這些結(jié)構(gòu):

#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 計數(shù)值不象存取 jiffies 那樣直接. 而在 64-位 計算機體系上, 這 2 個變量實際上是一個, 存取這個值對于 32-位 處理器不是原子的. 這意味著你可能讀到錯誤的值如果這個變量的兩半在你正在讀取它們時被更新. 極不可能你會需要讀取這個 64-位 計數(shù)器, 但是萬一你需要, 你會高興地得知內(nèi)核輸出了一個特別地幫助函數(shù), 為你完成正確地加鎖:

#include <linux/jiffies.h> 
u64 get_jiffies_64(void);

在上面的原型中, 使用了 u64 類型. 這是一個定義在 中的類型, 在 11 章中討論, 并且表示一個 unsigned 64-位 類型.

如果你在奇怪 32-位 平臺如何同時更新 32-位 和 64-位 計數(shù)器, 讀你的平臺的連接腳本( 查找一個文件, 它的名子匹配 valinux.lds). 在那里, jiffies 符號被定義來存取這個 64-位 值的低有效字, 根據(jù)平臺是小端或者大端. 實際上, 同樣的技巧也用在 64-位 平臺上, 因此這個 unsigned long 和 u64 變量在同一個地址被存取.

最后, 注意實際的時鐘頻率幾乎完全對用戶空間隱藏. 宏 HZ 一直擴展為 100 當(dāng)用戶空間程序包含 param.h, 并且每個報告給用戶空間的計數(shù)器都對應(yīng)地被轉(zhuǎn)換. 這應(yīng)用于 clock(3), times(2), 以及任何相關(guān)的函數(shù). 對 HZ 值的用戶可用的唯一證據(jù)是時鐘中斷多快發(fā)生, 如在 /proc/interrupts 所顯示的. 例如, 你可以獲得 HZ, 通過用在 /proc/uptime 中報告的系統(tǒng) uptime 除這個計數(shù)值.

7.1.2.?處理器特定的寄存器

如果你需要測量非常短時間間隔, 或者你需要非常高精度, 你可以借助平臺依賴的資源, 一個要精度不要移植性的選擇.

在現(xiàn)代處理器中, 對于經(jīng)驗性能數(shù)字的迫切需求被大部分 CPU 設(shè)計中內(nèi)在的指令定時不確定性所阻礙, 這是由于緩存內(nèi)存, 指令調(diào)度, 以及分支預(yù)測引起. 作為回應(yīng), CPU 制造商引入一個方法來計數(shù)時鐘周期, 作為一個容易并且可靠的方法來測量時間流失. 因此, 大部分現(xiàn)代處理器包含一個計數(shù)器寄存器, 它在每個時鐘周期固定地遞增一次. 現(xiàn)在, 資格時鐘計數(shù)器是唯一可靠的方法來進行高精度的時間管理任務(wù).

細(xì)節(jié)每個平臺不同: 這個寄存器可以或者不可以從用戶空間可讀, 它可以或者不可以寫, 并且它可能是 64 或者 32 位寬. 在后一種情況, 你必須準(zhǔn)備處理溢出, 就象我們處理 jiffy 計數(shù)器一樣. 這個寄存器甚至可能對你的平臺來說不存在, 或者它可能被硬件設(shè)計者在一個外部設(shè)備實現(xiàn), 如果 CPU 缺少這個特性并且你在使用一個特殊用途的計算機.

無論是否寄存器可以被清零, 我們強烈不鼓勵復(fù)位它, 即便當(dāng)硬件允許時. 畢竟, 在任何給定時間你可能不是這個計數(shù)器的唯一用戶; 在一些支持 SMP 的平臺上, 例如, 內(nèi)核依賴這樣一個計數(shù)器來在處理器之間同步. 因為你可以一直測量各個值的差, 只要差沒有超過溢出時間, 你可以通過修改它的當(dāng)前值來做這個事情不用聲明獨自擁有這個寄存器.

最有名的計數(shù)器寄存器是 TSC ( timestamp counter), 在 x86 處理器中隨 Pentium 引入的并且在所有從那之后的 CPU 中出現(xiàn) -- 包括 x86_64 平臺. 它是一個 64-位 寄存器計數(shù) CPU 的時鐘周期; 它可從內(nèi)核和用戶空間讀取.

在包含了 (一個 x86-特定的頭文件, 它的名子代表"machine-specific registers"), 你可使用一個這些宏:

rdtsc(low32,high32);
rdtscl(low32);
rdtscll(var64);

第一個宏自動讀取 64-位 值到 2 個 32-位 變量; 下一個("read low half") 讀取寄存器的低半部到一個 32-位 變量, 丟棄高半部; 最后一個讀 64-位 值到一個 long long 變量, 由此得名. 所有這些宏存儲數(shù)值到它們的參數(shù)中.

對大部分的 TSC 應(yīng)用, 讀取這個計數(shù)器的的低半部足夠了. 一個 1-GHz 的 CPU 只在每 4.2 秒溢出一次, 因此你不會需要處理多寄存器變量, 如果你在使用的時間流失確定地使用更少時間. 但是, 隨著 CPU 頻率不斷上升以及定時需求的提高, 將來你會幾乎可能需要常常讀取 64-位 計數(shù)器.

作為一個只使用寄存器低半部的例子, 下面的代碼行測量了指令自身的執(zhí)行:

unsigned long ini, end;
rdtscl(ini); rdtscl(end);
printk("time lapse: %li\n", end - ini);

一些其他的平臺提供相似的功能, 并且內(nèi)核頭文件提供一個體系獨立的功能, 你可用來代替 rdtsc. 它稱為 get_cycles, 定義在 ( 由 包含). 它的原型是:

 #include <linux/timex.h>
 cycles_t get_cycles(void); 

這個函數(shù)為每個平臺定義, 并且它一直返回 0 在沒有周期-計數(shù)器寄存器的平臺上. cycles_t 類型是一個合適的 unsigned 類型來持有讀到的值.

不論一個體系獨立的函數(shù)是否可用, 我們最好利用機會來展示一個內(nèi)聯(lián)匯編代碼的例子. 為此, 我們實現(xiàn)一個 rdtscl 函數(shù)給 MIPS 處理器, 它與在 x86 上同樣的方式工作.

拖尾的 nop 指令被要求來阻止編譯器在 mfc0 之后馬上存取指令中的目標(biāo)寄存器. 這種內(nèi)部鎖在 RISC 處理器中是典型的, 并且編譯器仍然可以在延遲時隙中調(diào)度有用的指令. 在這個情況中, 我們使用 nop 因為內(nèi)聯(lián)匯編對編譯器是一個黑盒并且不會進行優(yōu)化.[26]

#define rdtscl(dest) \
 __asm__ __volatile__("mfc0 %0,$9; nop" : "=r" (dest))

有這個宏在, MIPS 處理器可以執(zhí)行同樣的代碼, 如同前面為 x86 展示的一樣的代碼.

使用 gcc 內(nèi)聯(lián)匯編, 通用寄存器的分配留給編譯器. 剛剛展示的這個宏使用 %0 作為"參數(shù) 0"的一個占位符, 之后它被指定為"任何用作輸出( = )的寄存器( r )". 這個宏還聲明輸出寄存器必須對應(yīng) C 表達(dá)式 dest. 內(nèi)聯(lián)函數(shù)的語法是非常強大但是有些復(fù)雜, 特別對于那些有限制每個寄存器可以做什么的體系上(就是說, x86 家族). 這個用法在 gcc 文檔中描述, 常常在 info 文檔目錄樹中有.

本節(jié)已展示的這個簡短的 C-代碼片段已在一個 K7-級 x86 處理器 和一個 MIPS VR4181 ( 使用剛剛描述過的宏 )上運行. 前者報告了一個 11 個時鐘嘀噠的時間流失而后者只是 2 個時鐘嘀噠. 小的數(shù)字是期望的, 因為 RISC 處理器常常每個時鐘周期執(zhí)行一條指令.

有另一個關(guān)于時戳計數(shù)器的事情值得知道: 它們在一個 SMP 系統(tǒng)中不必要跨處理器同步. 為保證得到一個一致的值, 你應(yīng)當(dāng)為查詢這個計數(shù)器的代碼禁止搶占.


[26]?我們在 MIPS 上建立這例子, 因為大部分的 MIPS 處理器特有一個 32-位 計數(shù)器作為它們內(nèi)部"協(xié)處理器 0"的寄存器 9. 為存取這個寄存器, 僅僅從內(nèi)核空間可讀, 你可以定義下列的宏來執(zhí)行一條"從協(xié)處理器 0 轉(zhuǎn)移"的匯編指令:

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號