7.3. 延后執(zhí)行

2018-02-24 15:49 更新

7.3.?延后執(zhí)行

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

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

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

7.3.1.?長延時(shí)

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

7.3.1.1.?忙等待

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


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

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

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

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

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

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

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


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

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


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

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

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

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


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

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

7.3.1.2.?讓出處理器

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


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

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

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


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

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

7.3.1.3.?超時(shí)

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

如果你的驅(qū)動(dòng)使用一個(gè)等待隊(duì)列來等待某些其他事件, 但是你也想確保它在一個(gè)確定時(shí)間段內(nèi)運(yùn)行, 可以使用 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ù)在給定隊(duì)列上睡眠, 但是它們在超時(shí)(以 jiffies 表示)到后返回. 因此, 它們實(shí)現(xiàn)一個(gè)限定的睡眠不會(huì)一直睡下去. 注意超時(shí)值表示要等待的 jiffies 數(shù), 不是一個(gè)絕對時(shí)間值. 這個(gè)值由一個(gè)有符號(hào)的數(shù)表示, 因?yàn)樗袝r(shí)是一個(gè)相減運(yùn)算的結(jié)果, 盡管這些函數(shù)如果提供的超時(shí)值是負(fù)值通過一個(gè) printk 語句抱怨. 如果超時(shí)到, 這些函數(shù)返回 0; 如果這個(gè)進(jìn)程被其他事件喚醒, 它返回以 jiffies 表示的剩余超時(shí)值. 返回值從不會(huì)是負(fù)值, 甚至如果延時(shí)由于系統(tǒng)負(fù)載而比期望的值大.

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


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

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


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

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

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


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

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


set_current_state(TASK_INTERRUPTIBLE);
schedule_timeout (delay);

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

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

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

7.3.2.?短延時(shí)

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

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

udelay 的實(shí)現(xiàn)( 可能 ndelay 也是) 使用一個(gè)軟件循環(huán)基于在啟動(dòng)時(shí)計(jì)算的處理器速度, 使用整數(shù)變量 loos_per_jiffy. 如果你想看看實(shí)際的代碼, 但是, 小心 x86 實(shí)現(xiàn)是相當(dāng)復(fù)雜的一個(gè)因?yàn)樗褂玫牟煌臅r(shí)間源, 基于什么 CPU 類型在運(yùn)行代碼.

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

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

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


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

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

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

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

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

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)