7.3. 延后執(zhí)行

2018-02-24 15:49 更新

7.3.?延后執(zhí)行

設(shè)備驅(qū)動常常需要延后一段時間執(zhí)行一個特定片段的代碼, 常常允許硬件完成某個任務(wù). 在這一節(jié)我們涉及許多不同的技術(shù)來獲得延后. 每種情況的環(huán)境決定了使用哪種技術(shù)最好; 我們?nèi)甲屑?xì)檢查它們, 并且指出每一個的長處和缺點.

一件要考慮的重要的事情是你需要的延時如何與時鐘嘀噠比較, 考慮到 HZ 的跨各種平臺的范圍. 那種可靠地比時鐘嘀噠長并且不會受損于它的粗粒度的延時, 可以利用系統(tǒng)時鐘. 每個短延時典型地必須使用軟件循環(huán)來實現(xiàn). 在這 2 種情況中存在一個灰色地帶. 在本章, 我們使用短語" long " 延時來指一個多 jiffy 延時, 在一些平臺上它可以如同幾個毫秒一樣少, 但是在 CPU 和內(nèi)核看來仍然是長的.

下面的幾節(jié)討論不同的延時, 通過采用一些長路徑, 從各種直覺上不適合的方法到正確的方法. 我們選擇這個途徑因為它允許對內(nèi)核相關(guān)定時方面的更深入的討論. 如果你急于找出正確的代碼, 只要快速瀏覽本節(jié).

7.3.1.?長延時

偶爾地, 一個驅(qū)動需要延后執(zhí)行相對長時間 -- 多于一個時鐘嘀噠. 有幾個方法實現(xiàn)這類延時; 我們從最簡單的技術(shù)開始, 接著進入到高級些的技術(shù).

7.3.1.1.?忙等待

如果你想延時執(zhí)行多個時鐘嘀噠, 允許在值中某些疏忽, 最容易的( 盡管不推薦 ) 的實現(xiàn)是一個監(jiān)視 jiffy 計數(shù)器的循環(huán). 這種忙等待實現(xiàn)常??磥硐笙旅娴拇a, 這里 j1 是 jiffies 的在延時超時的值:


while (time_before(jiffies, j1))
    cpu_relax();

對 cpu_relex 的調(diào)用使用了一個特定于體系的方式來說, 你此時沒有在用處理器做事情. 在許多系統(tǒng)中它根本不做任何事; 在對稱多線程(" 超線程" ) 系統(tǒng)中, 可能讓出核心給其他線程. 在如何情況下, 無論何時有可能, 這個方法應(yīng)當(dāng)明確地避免. 我們展示它是因為偶爾你可能想運行這個代碼來更好理解其他代碼的內(nèi)幕.

我們來看一下這個代碼如何工作. 這個循環(huán)被保證能工作因為 jiffies 被內(nèi)核頭文件聲明做易失性的, 并且因此, 在任何時候 C 代碼尋址它時都從內(nèi)存中獲取. 盡管技術(shù)上正確( 它如同設(shè)計的一樣工作 ), 這種忙等待嚴(yán)重地降低了系統(tǒng)性能. 如果你不配置你的內(nèi)核為搶占操作, 這個循環(huán)在延時期間完全鎖住了處理器; 調(diào)度器永遠(yuǎn)不會搶占一個在內(nèi)核中運行的進程, 并且計算機看起來完全死掉直到時間 j1 到時. 這個問題如果你運行一個可搶占的內(nèi)核時會改善一點, 因為, 除非這個代碼正持有一個鎖, 處理器的一些時間可以被其他用途獲得. 但是, 忙等待在可搶占系統(tǒng)中仍然是昂貴的.

更壞的是, 當(dāng)你進入循環(huán)時如果中斷碰巧被禁止, jiffies 將不會被更新, 并且 while 條件永遠(yuǎn)保持真. 運行一個搶占的內(nèi)核也不會有幫助, 并且你將被迫去擊打大紅按鈕.

這個延時代碼的實現(xiàn)可拿到, 如同下列的, 在 jit 模塊中. 模塊創(chuàng)建的這些 /proc/jit* 文件每次你讀取一行文本就延時一整秒, 并且這些行保證是每個 20 字節(jié). 如果你想測試忙等待代碼, 你可以讀取 /proc/jitbusy, 每當(dāng)它返回一行它忙-循環(huán)一秒.

為確保讀, 最多, 一行( 或者幾行 ) 一次從 /proc/jitbusy. 簡化的注冊 /proc 文件的內(nèi)核機制反復(fù)調(diào)用 read 方法來填充用戶請求的數(shù)據(jù)緩存. 因此, 一個命令, 例如 cat /proc/jitbusy, 如果它一次讀取 4KB, 會凍住計算機 205 秒.

推薦的讀 /proc/jitbusy 的命令是 dd bs=200 < /proc/jitbusy, 可選地同時指定塊數(shù)目. 文件返回的每 20-字節(jié) 的行表示 jiffy 計數(shù)器已有的值, 在延時之前和延時之后. 這是一個例子運行在一個其他方面無負(fù)擔(dān)的計算機上:


phon% dd bs=20 count=5 < /proc/jitbusy
 1686518 1687518
 1687519 1688519
 1688520 1689520
 1689520 1690520
 1690521 1691521 

看來都挺好: 延時精確地是 1 秒 ( 1000 jiffies ), 并且下一個 read 系統(tǒng)調(diào)用在上一個結(jié)束后立刻開始. 但是讓我們看看在一個有大量 CPU-密集型進程在運行(并且是非搶占內(nèi)核)的系統(tǒng)上會發(fā)生什么:


phon% dd bs=20 count=5 < /proc/jitbusy
 1911226 1912226
 1913323 1914323
 1919529 1920529
 1925632 1926632
 1931835 1932835 

這里, 每個 read 系統(tǒng)調(diào)用精確地延時 1 秒, 但是內(nèi)核耗費多過 5 秒在調(diào)度 dd 進程以便它可以發(fā)出下一個系統(tǒng)調(diào)用之前. 在一個多任務(wù)系統(tǒng)就期望是這樣; CPU 時間在所有運行的進程間共享, 并且一個 CPU-密集型 進程有它的動態(tài)減少的優(yōu)先級. ( 調(diào)度策略的討論在本書范圍之外).

上面所示的在負(fù)載下的測試已經(jīng)在運行 load50 例子程序中進行了. 這個程序派生出許多什么都不做的進程, 但是以一種 CPU-密集的方式來做. 這個程序是伴隨本書的例子文件的一部分, 并且缺省是派生 50 個進程, 盡管這個數(shù)字可以在命令行指定. 在本章, 以及在本書其他部分, 使用一個有負(fù)載的系統(tǒng)的測試已經(jīng)用 load50 在一個其他方面空閑的計算機上運行來進行了.

如果你在運行一個可搶占內(nèi)核時重復(fù)這個命令, 你會發(fā)現(xiàn)沒有顯著差別在一個其他方面空閑的 CPU 上以及下面的在負(fù)載下的行為:


phon% dd bs=20 count=5 < /proc/jitbusy
 14940680 14942777
 14942778 14945430
 14945431 14948491
 14948492 14951960
 14951961 14955840 

這里, 沒有顯著的延時在一個系統(tǒng)調(diào)用的末尾和下一個的開始之間, 但是單獨的延時遠(yuǎn)遠(yuǎn)比 1 秒長: 直到 3.8 秒在展示的例子中并且隨時間上升. 這些值顯示了進程在它的延時當(dāng)中被中斷, 調(diào)度其他的進程. 系統(tǒng)調(diào)用之間的間隙不是唯一的這個進程的調(diào)度選項, 因此沒有特別的延時在那里可以看到.

7.3.1.2.?讓出處理器

如我們已見到的, 忙等待強加了一個重負(fù)載給系統(tǒng)總體; 我們樂意找出一個更好的技術(shù). 想到的第一個改變是明確地釋放 CPU 當(dāng)我們對其不感興趣時. 這是通過調(diào)用調(diào)度函數(shù)而實現(xiàn)地, 在 <linux/sched.h> 中聲明:


while (time_before(jiffies, j1)) {
    schedule();
}

這個循環(huán)可以通過讀取 /proc/jitsched 如同我們上面讀 /proc/jitbusy 一樣來測試. 但是, 還是不夠優(yōu)化. 當(dāng)前進程除了釋放 CPU 不作任何事情, 但是它保留在運行隊列中. 如果它是唯一的可運行進程, 實際上它運行( 它調(diào)用調(diào)度器來選擇同一個進程, 進程又調(diào)用調(diào)度器, 這樣下去). 換句話說, 機器的負(fù)載( 在運行的進程的平均數(shù) ) 最少是 1, 并且空閑任務(wù) ( 進程號 0, 也稱為對換進程, 由于歷史原因) 從不運行. 盡管這個問題可能看來無關(guān), 在計算機是空閑時運行空閑任務(wù)減輕了處理器工作負(fù)載, 降低它的溫度以及提高它的生命期, 同時電池的使用時間如果這個計算機是你的膝上機. 更多的, 因為進程實際上在延時中執(zhí)行, 它所耗費的時間都可以統(tǒng)計.

/proc/jitsched 的行為實際上類似于運行 /proc/jitbusy 在一個搶占的內(nèi)核下. 這是一個例子運行, 在一個無負(fù)載的系統(tǒng):


phon% dd bs=20 count=5 < /proc/jitsched
 1760205 1761207
 1761209 1762211
 1762212 1763212
 1763213 1764213
 1764214 1765217 

有趣的是要注意每次 read 有時結(jié)束于等待比要求的多幾個時鐘嘀噠. 這個問題隨著系統(tǒng)變忙會變得越來越壞, 并且驅(qū)動可能結(jié)束于等待長于期望的時間. 一旦一個進程使用調(diào)度來釋放處理器, 無法保證進程將拿回處理器在任何時間之后. 因此, 以這種方式調(diào)用調(diào)度器對于驅(qū)動的需求不是一個安全的解決方法, 另外對計算機系統(tǒng)整體是不好的. 如果你在運行 load50 時測試 jitsched, 你可以見到關(guān)聯(lián)到每一行的延時被擴充了幾秒, 因為當(dāng)定時超時的時候其他進程在使用 CPU .

7.3.1.3.?超時

到目前為止所展示的次優(yōu)化的延時循環(huán)通過查看 jiffy 計數(shù)器而不告訴任何人來工作. 但是最好的實現(xiàn)一個延時的方法, 如你可能猜想的, 常常是請求內(nèi)核為你做. 有 2 種方法來建立一個基于 jiffy 的超時, 依賴于是否你的驅(qū)動在等待其他的事件.

如果你的驅(qū)動使用一個等待隊列來等待某些其他事件, 但是你也想確保它在一個確定時間段內(nèi)運行, 可以使用 wait_event_timeout 或者 wait_event_interruptible_timeout:


#include <linux/wait.h>
long wait_event_timeout(wait_queue_head_t q, condition, long timeout);
long wait_event_interruptible_timeout(wait_queue_head_t q, condition, long timeout);

這些函數(shù)在給定隊列上睡眠, 但是它們在超時(以 jiffies 表示)到后返回. 因此, 它們實現(xiàn)一個限定的睡眠不會一直睡下去. 注意超時值表示要等待的 jiffies 數(shù), 不是一個絕對時間值. 這個值由一個有符號的數(shù)表示, 因為它有時是一個相減運算的結(jié)果, 盡管這些函數(shù)如果提供的超時值是負(fù)值通過一個 printk 語句抱怨. 如果超時到, 這些函數(shù)返回 0; 如果這個進程被其他事件喚醒, 它返回以 jiffies 表示的剩余超時值. 返回值從不會是負(fù)值, 甚至如果延時由于系統(tǒng)負(fù)載而比期望的值大.

/proc/jitqueue 文件展示了一個基于 wait_event_interruptible_timeout 的延時, 結(jié)果這個模塊沒有事件來等待, 并且使用 0 作為一個條件:


wait_queue_head_t wait; 
init_waitqueue_head (&wait); 
wait_event_interruptible_timeout(wait, 0, delay); 

當(dāng)讀取 /proc/jitqueue 時, 觀察到的行為近乎優(yōu)化的, 即便在負(fù)載下:


phon% dd bs=20 count=5 < /proc/jitqueue
 2027024  2028024 
 2028025  2029025 
 2029026  2030026 
 2030027  2031027 
 2031028  2032028  

因為讀進程當(dāng)?shù)却瑫r( 上面是 dd )不在運行隊列中, 你看不到表現(xiàn)方面的差別, 無論代碼是否運行在一個搶占內(nèi)核中.

wait_event_timeout 和 wait_event_interruptible_timeout 被設(shè)計為有硬件驅(qū)動存在, 這里可以用任何一種方法來恢復(fù)執(zhí)行: 或者有人調(diào)用 wake_up 在等待隊列上, 或者超時到. 這不適用于 jitqueue, 因為沒人在等待隊列上調(diào)用 wake_up ( 畢竟, 沒有其他代碼知道它 ), 因此這個進程當(dāng)超時到時一直喚醒. 為適應(yīng)這個特別的情況, 這里你想延后執(zhí)行不等待特定事件, 內(nèi)核提供了 schedule_timeout 函數(shù), 因此你可以避免聲明和使用一個多余的等待隊列頭:


#include <linux/sched.h>
signed long schedule_timeout(signed long timeout);

這里, timeout 是要延時的 jiffies 數(shù). 返回值是 0 除非這個函數(shù)在給定的 timeout 流失前返回(響應(yīng)一個信號). schedule_timeout 請求調(diào)用者首先設(shè)置當(dāng)前的進程狀態(tài), 因此一個典型調(diào)用看來如此:


set_current_state(TASK_INTERRUPTIBLE);
schedule_timeout (delay);

前面的行( 來自 /proc/jitschedto ) 導(dǎo)致進程睡眠直到經(jīng)過給定的時間. 因為 wait_event_interruptible_timeout 在內(nèi)部依賴 schedule_timeout, 我們不會費勁顯示 jitschedto 返回的數(shù), 因為它們和 jitqueue 的相同. 再一次, 不值得有一個額外的時間間隔在超時到和你的進程實際被調(diào)度來執(zhí)行之間.

在剛剛展示的例子中, 第一行調(diào)用 set_current_state 來設(shè)定一些東西以便調(diào)度器不會再次運行當(dāng)前進程, 直到超時將它置回 TASK_RUNNING 狀態(tài). 為獲得一個不可中斷的延時, 使用 TASK_UNINTERRUPTIBLE 代替. 如果你忘記改變當(dāng)前進程的狀態(tài), 調(diào)用 schedule_time 如同調(diào)用 shcedule( 即, jitsched 的行為), 建立一個不用的定時器.

如果你想使用這 4 個 jit 文件在不同的系統(tǒng)情況下或者不同的內(nèi)核, 或者嘗試其他的方式來延后執(zhí)行, 你可能想配置延時量當(dāng)加載模塊時通過設(shè)定延時模塊參數(shù).

7.3.2.?短延時

當(dāng)一個設(shè)備驅(qū)動需要處理它的硬件的反應(yīng)時間, 涉及到的延時常常是最多幾個毫秒. 在這個情況下, 依靠時鐘嘀噠顯然不對路.

The kernel functions ndelay, udelay, and mdelay serve well for short delays, delaying execution for the specified number of nanoseconds, microseconds, or milliseconds respectively. Their prototypes are: The u in udelay represents the Greek letter mu and stands for micro.
內(nèi)核函數(shù) ndelay, udelay, 以及 mdelay 對于短延時好用, 分別延后執(zhí)行指定的納秒數(shù), 微秒數(shù)或者毫秒數(shù). [27]它們的原型是:


#include <linux/delay.h>
void ndelay(unsigned long nsecs);
void udelay(unsigned long usecs);
void mdelay(unsigned long msecs);

這些函數(shù)的實際實現(xiàn)在 <asm/delay.h>, 是體系特定的, 并且有時建立在一個外部函數(shù)上. 每個體系都實現(xiàn) udelay, 但是其他的函數(shù)可能或者不可能定義; 如果它們沒有定義, <linux/delay.h> 提供一個缺省的基于 udelay 的版本. 在所有的情況中, 獲得的延時至少是要求的值, 但可能更多; 實際上, 當(dāng)前沒有平臺獲得了納秒的精度, 盡管有幾個提供了次微秒的精度. 延時多于要求的值通常不是問題, 因為驅(qū)動中的短延時常常需要等待硬件, 并且這個要求是等待至少一個給定的時間流失.

udelay 的實現(xiàn)( 可能 ndelay 也是) 使用一個軟件循環(huán)基于在啟動時計算的處理器速度, 使用整數(shù)變量 loos_per_jiffy. 如果你想看看實際的代碼, 但是, 小心 x86 實現(xiàn)是相當(dāng)復(fù)雜的一個因為它使用的不同的時間源, 基于什么 CPU 類型在運行代碼.

為避免在循環(huán)計算中整數(shù)溢出, udelay 和 ndelay 強加一個上限給傳遞給它們的值. 如果你的模塊無法加載和顯示一個未解決的符號, __bad_udelay, 這意味著你使用太大的參數(shù)調(diào)用 udleay. 注意, 但是, 編譯時檢查只對常量進行并且不是所有的平臺實現(xiàn)它. 作為一個通用的規(guī)則, 如果你試圖延時幾千納秒, 你應(yīng)當(dāng)使用 udelay 而不是 ndelay; 類似地, 毫秒規(guī)模的延時應(yīng)當(dāng)使用 mdelay 完成而不是一個更細(xì)粒度的函數(shù).

重要的是記住這 3 個延時函數(shù)是忙等待; 其他任務(wù)在時間流失時不能運行. 因此, 它們重復(fù), 盡管在一個不同的規(guī)模上, jitbusy 的做法. 因此, 這些函數(shù)應(yīng)當(dāng)只用在沒有實用的替代時.

有另一個方法獲得毫秒(和更長)延時而不用涉及到忙等待. 文件 <linux/delay.h> 聲明這些函數(shù):


void msleep(unsigned int millisecs);
unsigned long msleep_interruptible(unsigned int millisecs);
void ssleep(unsigned int seconds)

前 2 個函數(shù)使調(diào)用進程進入睡眠給定的毫秒數(shù). 一個對 msleep 的調(diào)用是不可中斷的; 你能確保進程睡眠至少給定的毫秒數(shù). 如果你的驅(qū)動位于一個等待隊列并且你想喚醒來打斷睡眠, 使用 msleep_interruptible. 從 msleep_interruptible 的返回值正常地是 0; 如果, 但是, 這個進程被提早喚醒, 返回值是在初始請求睡眠周期中剩余的毫秒數(shù). 對 ssleep 的調(diào)用使進程進入一個不可中斷的睡眠給定的秒數(shù).

通常, 如果你能夠容忍比請求的更長的延時, 你應(yīng)當(dāng)使用 schedule_timeout, msleep, 或者 ssleep.

[27] udelay 中的 u 表示希臘字母 mu 并且代表 micro.

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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號