《linux設備驅動程序》中對時間流的一段分析 我們來看看內核代碼是如何對時間問題進行處理的。按復雜程度遞增排列,該問題包括: 理解內核時間機制 如何獲得當前時間 如何將操作延遲指定的一段時間 如何調度異步函數到指定的時間後執行 內核中的時間間隔 我們
《linux設備驅動程序》中對時間流的一段分析
我們來看看內核代碼是如何對時間問題進行處理的。按復雜程度遞增排列,該問題包括: 理解內核時間機制 如何獲得當前時間 如何將操作延遲指定的一段時間 如何調度異步函數到指定的時間後執行 內核中的時間間隔 我們首先要涉及的是時鐘中斷,操作系統通過時鐘中斷來確定時間間隔。中斷是異步事件,通常由外部硬件觸發。中斷發生時,CPU停止正在進行的任務,轉而執行另一段特殊的代碼(即中斷服務例程,又稱ISR)來響應這個中斷。中斷和 ISR 的實現將在第9章討論。 時鐘中斷由系統計時硬件以周期性的間隔產生,這個間隔由內核根據 HZ 的值設定,HZ 是一個與體系結構有關的常數,在文件 中定義。當前的Linux版本為大多數平台定義的 HZ 的值是100,某些平台上是 1024,IA-64 仿真器上是 20。驅動程序開發者不應使用任何特定的 HZ 值來計數,不管你的平台使用的是哪一個值。 當時鐘中斷發生時,變量jiffies的值就增加。jiffies在系統啟動時初始化為0,因此,jiffies 值就是自操作系統啟動以來的時鐘滴答的數目,jiffies在頭文件 中被定義為數據類型為 unsigned long volatile型變量,這個變量在經過長時間的連續運行後有可能溢出(不過現在還沒有哪種平台會在運行不到 16 個月就使jiffies溢出)。為了保證 jiffies 溢出時內核仍能正常工作,人們已做了很多努力。驅動程序開發人員通常不用考慮 jiffies 的溢出問題,知道有這種可能性就行了。 如果想改變系統時鐘中斷發生的頻率,可以修改HZ值。有人使用Linux處理硬實時任務,他們增加了HZ值以獲得更快的響應時間,為此情願忍受額外的時鐘中斷產生的系統開銷。總而言之,時鐘中斷的最好方法是保留 HZ 的缺省值,因為我們可以完全相信內核的開發者們,他們一定已經為我們挑選了最佳值。 處理器特有的寄存器 如果需要度量非常短的時間,或是需要極高的時間精度,可以使用與特定平台相關的資源,這是將時間精度的重要性凌駕於代碼的可移植性之上的做法。 大多數較新的CPU都包含一個高精度的計數器,它每個時鐘周期遞增一次。這個計數器可用於精確地度量時間。由於大多數系統中的指令執行時間具有不可預測性(由於指令調度、分支預測、緩存等等),在運行具有很小時間粒度的任務時,使用這個時鐘計數器是唯一可靠的計時方法。為適應現代處理器的高速度,滿足衡量性能指標的緊迫需求,同時由於CPU設計中的多層緩存引起的指令時間的不可預測性,CPU 的制造商們引入了記錄時鐘周期這一測量時間的簡單可靠的方法。所以絕大多數現代處理器都包含一個隨時鐘周期不斷遞增的計數寄存器。 基於不同的平台,在用戶空間,這個寄存器可能是可讀的,也可能不可讀;可能是可寫的,也可能不可寫;可能是64位的也可能是32位的。如果是 32 位的,還得注意處理溢出的問題。無論該寄存器是否可以置0,我們都強烈建議不要重置它,即使硬件允許這麼做。因為總可以通過多次讀取該寄存器並比較讀出數值的差異來完成要做的事,我們無須要求獨占該寄存器並修改它的當前值。 最有名的計數器寄存器就是TSC(timestamp counter,時間戳計數器),從x86的Pentium處理器開始提供該寄存器,並包括在以後的所有 CPU 中。它是一個64位寄存器,記錄 CPU時鐘周期數,內核空間和用戶空間都可以讀取它。 包含了頭文件 (意指“machine-specific registers,機器特有的寄存器”)之後,就可以使用如下的宏: rdtsc(low,high); rdtscl(low); 前一個宏原子性地把 64 位的數值讀到兩個 32 位變量中;後一個只把寄存器的低半部分讀入一個 32 位變量,在大多數情況,這已經夠用了。舉例來說,一個 500MHz 的系統使一個 32 位計數器溢出需 8.5 秒,如果要處理的時間肯定比這短的話,那就沒有必要讀出整個寄存器。 下面這段代碼可以測量該指令自身的運行時間: unsigned long ini, end; rdtscl(ini); rdtscl(end); printk("time lapse: %li\en", end - ini); 其他一些平台也提供了類似的功能,在內核頭文件中還有一個與體系結構無關的函數可以代替rdtsc,它就是get_cycles,是在2.1版的開發過程中引入的。其原型是: #include cycles_t get_cycles(void); 在各種平台上都可以使用這個函數,在沒有時鐘周期記數寄存器的平台上它總是返回0。cycles_t 類型是能裝入對應 CPU 單個寄存器的合適的無符號類型。選擇能裝入單個寄存器的類型意味著,舉例來說,get_cycles 用於 Pentium 的時鐘周期計數器時只返回低32位。這種選擇是明智的,它避免了多寄存器操作的問題,與此同時並未阻礙對該計數器的正常用法,即用來度量很短的時間間隔。 除了這個與體系結構無關的函數外,我們還將示例使用一段內嵌的匯編代碼。為此,我們來給 MIPS 處理器實現一個rdtscl函數,功能就象x86的一樣。 這個例子之所以基於 MIPS,是因為大多數MIPS處理器都有一個32位的計數器,在它們的內部“coprocessor 0”中命名為register 9寄存器。為了從內核空間讀取該寄存器,可以定義下面的宏,它執行“從coprocessor 0讀取”的匯編指令: =======footnote begins============= nop 指令是必需的,防止了編譯器在指令mfc0之後立刻訪問目標寄存器。這種互鎖(interlock)在 RISC處理器中是很典型的,在延遲期間編譯器仍然可以調度其它指令執行。我們在這裡使用nop,是因為內嵌匯編指令對編譯器來說是個黑盒,不能進行優化。 =======footnote ends============= #define rdtscl(dest) \e _ _asm_ _ _ _volatile_ _("mfc0 %0,; nop" : "=r" (dest)) 通過使用這個宏,MIPS處理器就可以執行和前面所示用於x86的相同的代碼了。 gclearcase/" target="_blank" >cc內嵌匯編的有趣之處在於通用寄存器的分配使用是由編譯器完成的。這個宏中使用的 %0只是“參數0”的占位符,參數0由隨後的“作為輸出(=)使用的任意寄存器(r)”定義。該宏還說明了輸出寄存器要對應於 C 表達式dest 。內嵌匯編的語法功能強大但也比較復雜,特別是在對各寄存器使用有限制的平台上更是如此,如x86系列。完整的語法描述在gcc文檔中提供,一般在info中就可找到。 這節展示的短小的C代碼段已經在一個K7類的x86處理器和一個MIPS VR4181處理器(使用了剛才的宏)上運行過了。前者給出的時間消耗為11時鐘周期,後者僅為2時鐘周期。這是可以理解的,因為RISC處理器通常每時鐘周期運行一條指令。 獲取當前時間 內核一般通過jiffies值來獲取當前時間。該數值表示的是自最近一次系統啟動到當前的時間間隔,它和設備驅動程序不怎麼相關,因為它的生命期只限於系統的運行期(uptime)。但驅動程序可以利用jiffies的當前值來計算不同事件間的時間間隔(比如在輸入設備驅動程序中就用它來分辨鼠標的單雙擊)。簡而言之,利用jiffies值來測量時間間隔在大多數情況下已經足夠了,如果還需要測量更短的時間,就只能使用處理器特有的寄存器了。 驅動程序一般不需要知道牆鐘時間(指日常生活使用的時間),通常只有象 cron 和 at 這樣用戶程序才需要牆鐘時間。需要牆鐘時間的情形是使用設備驅動程序的特殊情況,此時可以通過用戶程序來將牆鐘時間轉換成系統時鐘。直接處理牆鐘時間常常意味著正在實現某種策略,應該仔細審視一下是否該這樣做。 如果驅動程序真的需要獲取當前時間,可以使用do_gettimeofday函數。該函數並不返回今天是本周的星期幾或類似的信息;它是用秒或微秒值來填充一個指向struct timeval的指針變量,gettimeofday系統調用中用的也是同一變量。do_gettimeofday的原型如下: #include void do_gettimeofday(struct timeval *tv); 源碼中描述do_gettimeofday在許多體系結構上有“接近微秒級的分辨率”,然而實際精度是隨不同的平台而變化的,在舊版本的內核中還會低些。當前時間也可以通過 xtime 變量(類型為struct timeval)獲得(但精度差些),但是,並不鼓勵直接使用該變量,因為除非關閉中斷,否則無法原子性地訪問 timeval 變量的兩個成員 tv_sec 和 tv_usec。在2.2版的內核中,一個快捷安全的獲得時間的辦法(可能精度會差些)是使用 get_fast_time: void get_fast_time(struct timeval *tv); 獲取當前時間的代碼可見於jit(“Just In Time”)模塊,源文件可以從 O'Reilly公司的 FTP站點獲得。jit模塊將創建 /proc/currentime 文件,讀取該文件將以 ASCII 碼的形式返回三項: 由 do_gettimeofday 返回的當前時間 從 xtime 鐘獲得的當前時間 jiffies 的當前值 我們選擇用動態的 /proc 文件,是因為這樣模塊代碼量會小些――不值得為返回三行文本而寫一個完整的設備驅動程序。 If you use \*[PGN]cat\*[/PGN] to read the file multiple times in less than a timer tick, you'll see the difference between xtime and [ I ]do_gettimeofday[ R ], reflecting the fact that xtime is updated less frequently: 如果用 cat 命令在一個時鐘滴答內多次讀該文件,就會發現 xtime 和 do_gettimeofday 兩者的差異了,xtime 更新的次數不那麼頻繁: morgana% cd /proc; cat currentime currentime currentime gettime: 846157215.937221 xtime: 846157215.931188 jiffies: 1308094 gettime: 846157215.939950 xtime: 846157215.931188 jiffies: 1308094 gettime: 846157215.942465 xtime: 846157215.941188 jiffies: 1308095 延遲執行 設備驅動程序經常需要將某些特定代碼延遲一段時間後執行――通常是為了讓硬件能完成某些任務。這一節將介紹許多實現延遲的不同技術,哪種技術最好取決於實際環境中的具體情況。我們將介紹所有的這些技術並指出各自的優缺點。 一件需要考慮的很重要的事情是所需的延遲長度是否多於一個時鐘滴答。較長的延遲可以利用系統時鐘;較短的延遲通常必須通過軟件循環來獲得。 長延遲 如果想把執行延遲若干個時鐘滴答,或者對延遲的精度要求不高(比如,想延遲整數數目的秒數),最簡單的也是最笨的實現如下,也就是所謂的“忙等待”: unsigned long j = jiffies + jit_delay * HZ; while (jiffies < j) /* nothing */; 這種實現當然要避免。我們在這裡提到它,只是因為讀者可能某時需要運行這段代碼,以便更好地理解其他的延遲技術。 還是先看看這段代碼是如何工作的。因為內核的頭文件中jiffies被聲明為volatile型變量,每次C代碼訪問它時都會重新讀取它,因此該循環可以起到延遲的作用。盡管也是“正確”的實現,但這個忙等待循環在延遲期間會鎖住處理器,因為調度器不會中斷運行在內核空間的進程。更糟糕的是,如果在進入循環之前正好關閉了中斷,jiffies值就不會得到更新,那麼while循環的條件就永遠為真,這時,你不得不按下那只大的紅按鈕(指電源按鈕)。 這種延遲和下面的幾種延遲方法都在jit模塊中實現了。由該模塊創建的所有 /proc/jit* 文件每次被讀取時都延遲整整 1 秒。如果你想測試忙等待代碼,可以讀 /proc/jitbusy 文件,當該文件的 read 方法被調用時它將進入忙等待循環,延遲 1 秒;而象dd if=/proc/jitbusy bs=1這樣的命令每次讀一個字符就要延遲 1 秒。 可以想見,讀 /proc/jitbusy 文件會大大影響系統性能,因為此時計算機要到1秒後才能運行其他進程。 更好的延遲方法如下,它允許其他進程在延遲的時間間隔內運行,盡管這種方法不能用於硬實時任務或者其他對時間要求很嚴格的場合: while (jiffies < j) schedule(); 這個例子和下面各例中的變量j應是延遲到達時的jiffies值,計算方法和忙等待一樣。 這種循環(可以通過讀 /proc/jitsched 文件來測試它)延遲方法還不是最優的。系統可以調度其他任務;當前任務除了釋放CPU之外不做任何工作,但是它仍在任務隊列中。如果它是系統中唯一的可運行的進程,它還會被運行(系統調用調度器,調度器選擇同一個進程運行,此進程又再調用調度器,然後...)。換句話說,機器的負載(系統中運行的進程平均數)至少為1,而idle進程(進程號為0,由於歷史原因被稱為“swapper”)絕不會被運行。盡管這個問題看來無所謂,當系統空閒時運行idle進程可以減輕處理器負載,降低處理器溫度,延長處理器壽命,如果是手提電腦,還能延長電池的壽命。而且,延遲期間實際上進程是在執行的,因此延遲消耗的所有時間都是記在它的運行時間上的。運行命令 time cat /proc/jitsched 就可以發現這一點。 另一種情況下,如果系統很忙,驅動程序等待的時間可能會比預計多得多。一旦一個進程在調度時讓出了處理器,無法保證以後的某個時間就能重新分配給它。如果可接受的延遲時間有上限的話,用這種方式調用schedule,對驅動程序來說並不是一個安全的解決方案。 盡管有些毛病,這種循環延遲還是提供了一種有點“髒”但比較快的監視驅動程序工作的途徑。如果模塊中的某個 bug 會鎖死整個系統,則可在每個用於調試的 printk 語句後添加一小段延遲,這樣可以保證在處理器碰到令人厭惡的 bug 而被鎖死之前,所有的打印消息都能進入系統日志。如果沒有這樣的延遲,這些消息只能進入內存緩沖區,但在 klogd 得到運行前系統可能已經被鎖住了。 獲得延遲的最好方法,是請求內核為我們實現延遲。根據驅動程序是否在等待其他事件,有兩種設置短期延遲的辦法。 如果驅動程序使用等待隊列等待某個事件,而你又想確保在一段時間後一定運行該驅動程序,可以使用 sleep 函數的超時版本,這在第5章“睡眠和喚醒”一節中已介紹過了: sleep_on_timeout(wait_queue_head_t *q, unsigned long timeout); interruptible_sleep_on_timeout(wait_queue_head_t *q, unsigned long timeout); 兩種實現都能讓進程在指定的等待隊列上睡眠,而在超時期限(用jiffies表示)未到時的任何事件都會將其喚醒。由此它們就實現了一種有上限的不會永遠持續下去的睡眠。注意超時值表示要等待的 jiffies 數量,而不是絕對的時間值。這種方式的延遲可以在 /proc/jitqueue 的實現中看到: wait_queue_head_t wait; init_waitqueue_head (&wait); interruptible_sleep_on_timeout(&wait, jit_delay*HZ); 在通常的驅動程序中,可以以下列兩種方式重新獲得執行:在等待隊列上調用一個 wake_up,或者 timout 超時。在這個特定實現中,沒人會調用 wake_up(畢竟其它代碼根本就不知道這件事),所以進程總是因 timeout 超時而被喚醒。這是一個完美有效的實現,不過,如果驅動程序無須等待其它事件,可以用一種更直接的方式獲取延遲,即使用schedule_timeout: set_current_state(TASK_INTERRUPTIBLE); schedule_timeout (jit_delay*HZ); 上述代碼行(在 /proc/jitself 中實現)使進程進入睡眠直到指定時間。schedule_timeout也是處理一個時間增量而不是一個 jiffies 的絕對值。和前面一樣,在從超時到進程實際被調度執行之間,可能會消耗一些毫無價值的額外時間。 短延遲 有時驅動程序需要非常短的延遲來和硬件同步。此時,使用jiffies值無法達到目的。 這時就要用內核函數udelay 和 mdelay*。 ============footnote begins=========== u表示希臘字母“mu”(μ),它代表“微”。 ============footnote ends=========== 它們的原型如下: #include void udelay(unsigned long usecs); void mdelay(unsigned long msecs); 該函數在絕大多數體系結構上是作為內聯函數編譯的。前者使用軟件循環延遲指定數目的微秒數,後者使用 udelay 做循環,用於方便程序開發。udelay 函數裡要用到 BogoMips 值:它的循環基於整數值 loops_per_second,這個值是在引導階段計算 BogoMips 時得到的結果。 udelay函數只能用於獲取較短的時間延遲,因為loops_per_second值的精度只有8位,所以,當計算更長的延遲時會積累出相當大的誤差。盡管最大能允許的延遲將近1秒(因為更長的延遲就要溢出),推薦的 udelay 函數的參數的最大值是取1000微秒(1毫秒)。延遲大於 1 毫秒時可以使用函數 mdelay。 要特別注意的是 udelay 是個忙等待函數(所以 mdelay 也是),在延遲的時間段內無法運行其他的任務,因此要十分小心,尤其是 mdelay,除非別無他法,要盡量避免使用。 目前在支持大於幾個微秒和小於1個時鐘滴答的延遲時還是很低效的,但這通常不是個問題,因為延遲需要足夠長,以便能夠讓人或者硬件注意到。對人來說,百分之一秒的時間間隔是比較適合的精度,而 1 毫秒對硬件動作來說也足夠長了。 mdelay 在 Linux 2.0 中並不存在,頭文件 sysdep.h 彌補了這一缺陷。 任務隊列 許多驅動程序需要將任務延遲到以後處理,但又不想借助中斷。Linux 為此提供了三種方法:任務隊列、tasklet(從內核 2.3.43 開始)和內核定時器。任務隊列和 tasklet 的使用很靈活,可以或長或短地延遲任務到以後處理,在編寫中斷處理程序時非常有用,我們還將在第9章“Tasklet和底半部處理”一節中繼續討論。內核定時器則用來調度任務在未來某個指定時間執行,將在本章的“內核定時器”一節中討論。 使用任務隊列或tasklet的一個典型情形是,硬件不產生中斷,但仍希望提供阻塞型的讀取。此時需要對設備進行輪詢,同時要小心地不使 CPU 負擔過多無謂的操作。將讀進程以固定的時間間隔喚醒(例如,使用 current->timeout 變量)並不是個很好的方法,因為每次輪詢需要兩次上下文切換(一次是切換到讀進程中運行輪詢代碼,另一次是返回執行實際工作的某個進程),而且通常來講,恰當的輪詢機制應該在進程上下文之外實現。 類似的情形還有象不時地給簡單的硬件設備提供輸入。例如,有一個直接連接到並口的步進馬達,要求該馬達能一步步地移動,但馬達每次只能移動一步。在這種情況下,由控制進程通知設備驅動程序進行移動,但實際上,移動是在 write 返回後,才在周期性的時間間隔內一步一步進行的。 快速完成這類不定操作的恰當方法是注冊任務在未來執行。內核提供了對“任務隊列”的支持,任務可以累積,而在運行隊列時被“消耗”。我們可以聲明自己的任務隊列,並且在任意時刻觸發它,或者也可以將任務注冊到預定義的任務隊列中去,由內核來運行(觸發)它。 這一節將首先概述任務隊列,然後介紹預定義的任務隊列,這使讀者可以開始一些有趣的測試(如果出錯也可能掛起系統),最後介紹如何運行自己的任務隊列。接著,我們來看看新的 tasklet 接口,在 2.4 內核中它在很多情況下取代了任務隊列。 任務隊列的本質 任務隊列其實一個任務鏈表,每個任務用一個函數指針和一個參數表示。任務運行時,它接受一個void * 類型的參數,返回值類型為 void,而指針參數可用來將一個數據結構傳入函數,或者可以被忽略。隊列本身是一個結構(即任務)鏈表,並由聲明和操縱它們的內核模塊所擁有。模塊要全權負責這些數據結構的分配和釋放,為此一般使用靜態的數據結構。 隊列元素由下面這個結構來描述,這段代碼是直接從頭文件 拷貝下來的: struct tq_struct { struct tq_struct *next; /* linked list of active bh's */ int sync; /* must be initialized to zero */ void (*routine)(void *); /* function to call */ void *data; /* argument to function */ }; 第一個注釋中的 bh 指的是底半部(bottom-half)。底半部是“中斷處理程序的一半部”,我們將在第9章的“tasklet和底半部”一節中介紹中斷時詳細討論。現在,我們只要知道底半部是驅動程序實現的一種機制就可以了,它用於處理異步任務,這些任務通常比較大,不適於在處理硬件中斷時完成。本章並不要求你理解底半部處理,但必要時也會偶爾提及。 上面的數據結構中最重要的成員是routine和data。為了將隨後執行的任務排隊,必須先設置好結構的這些成員,並把 next 和 sync 兩個字段清零。結構中的 sync 標志位由內核使用,以避免同一任務被插入多次,因為這會破壞 next 指針。一旦任務被排隊,該數據結構就被認為由內核“擁有”了,不能再被修改,直到任務開始運行。 與任務隊列有關的其他數據結構還有 task_queue,目前它實現為指向 tq_struct 結構的指針,如果將來需要擴充task_queue,只要用typedef將該指針定義為其他符號就可以了。在使用之前,必須將 task_queue 指針初始化為 NULL。 下面匯總了所有可以在任務隊列和 tq_struct 結構上執行的操作。 DECLARE_TASK_QUEUE(name); 這個宏用給定的名稱 name 聲明了一個任務隊列,並把它初始化為空。 int queue_task(struct tq_struct *task, task_queue *list); 正如該函數的名字,它用於將任務排進隊列中。如果隊列中已有該任務,返回0,否則返回非0。 void run_task_queue(task_queue *list); run_task_queue函數用於運行累積在隊列上的任務。除非你要聲明和維護自己的任務隊列,否則不必調用本函數。 在討論使用任務隊列的細節之前,我們先看一下它們在內核中是怎樣工作的。 任務隊列的運行 如前所述,一個任務隊列,實際上是一個函數鏈表。當調用 run_task_queue 運行某個隊列時,列表中的每一項都會被執行。在編寫和任務隊列有關的函數時,一定要記住,當內核調用 run_task_queue 時,實際的上下文將限制能夠進行的操作。也不應對隊列中任務的運行順序做任何假定,它們每個都是獨立完成自己的任務的。 那麼任務隊列在什麼時候運行呢?如果使用的是下面一節介紹的預定義的任務隊列,則答案是“在內核輪到它那裡時”。不同的隊列在不同的時間運行,只要內核沒有其他更緊要的任務,它們總是會運行的。 更重要的是,當對任務進行排隊的進程運行時,任務隊列幾乎肯定是不會運行的,相反,它們是異步執行的。到現在為止,示例驅動例程中所有的事情都是在這個執行系統調用的進程上下文中完成的。但當任務隊列運行時,這個進程可能正在睡眠,或正在另一個處理器上運行,甚至可能已經完全退出了。 這種異步執行類似於硬件中斷發生時的情景(我們會在第9章詳細討論)。實際上,任務隊列常常是作為“軟件中斷”的結果而運行的。在中斷模式(或中斷期間)下,代碼的運行會受到許多限制。我們現在介紹這些限制,這些限制還會在本書後面多次出現。我們也會多次重復,中斷模式下的這些規則必須遵守,否則系統會有大麻煩。 許多動作需要在進程上下文中才能執行。如果處於進程上下文之外(比如在中斷模式下),則必須遵守如下規則: 不允許訪問用戶空間。因為沒有進程上下文,無法將進程與用戶空間關聯起來。 current指針在中斷模式下是無效的,不能使用。 不能執行睡眠或調度。中斷模式代碼不可以調用schedule或者sleep_on;也不能調用任何可能引起睡眠的函數。例如,調用kmalloc(...,GFP_KERNEL)就不符合本規則。信號量也不能用,因為可能引起睡眠。 內核代碼可以通過調用函數in_interrupt( ) 來判斷自己是否正運行於中斷模式,該函數無需參數,如果處理器在中斷期間運行就返回非0值。 當前的任務隊列實現還有一個特性,隊列中的一個任務可以將自己重新插回到它原先所在的隊列。舉個例子,定時器隊列中的任務可以在運行時將自己插回到定時器隊列中去,從而在下一個定時器滴答又再次被運行。這是通過調用 queue_task 把自己放回隊列來實現的。由於在處理任務隊列之前,是先用NULL指針替換隊列的頭指針,因此才可能進行不斷的重新調度。結果是,一旦舊的隊列開始執行,就有一個新的隊列被建立。 盡管一遍遍地重新調度同一個任務看起來似乎沒什麼意義,但有時這也有些用處。例如,步進馬達每次移動一步直到目的地,它的驅動程序就可以通過讓任務在定時器隊列上不斷地重新調度自己來實現。其他的例子還有 jiq 模塊,該模塊中的打印函數通過重新調度自己來產生輸出――結果是利用定時器隊列產生多次迭代。 預定義的任務隊列 延遲任務執行的最簡單方法是使用由內核維護的任務隊列。這種隊列有好幾種,但驅動程序只能使用下面列出的其中三種。任務隊列的定義在頭文件 中,驅動程序代碼需要包含該頭文件。 調度器隊列 調度器隊列在預定義任務隊列中比較獨特,它運行在進程上下文中,這意味著該隊列中的任務可以更多的事情。在Linux 2.4,該隊列由一個專門的內核線程 keventd 管理,通過函數 schedule_task 訪問。在較老的內核版本,沒有用keventd,所以該隊列(tq_scheduler)是直接操作的。 tq_timer 該隊列由定時器處理程序(定時器嘀哒)運行。因為該處理程序(見函數do_timer)是在中斷期間運行的,因此該隊列中的所有任務也是在中斷期間運行的。 tq_immediate 立即隊列是在系統調用返回時或調度器運行時得到處理,以便盡可能快地運行該隊列。該隊列在中斷期間得到處理。 還有其它的預定義隊列,但驅動程序開發中通常不會涉及到它們。 使用任務隊列的一個設備驅動程序的執行流程可見圖6-1。該圖演示了設備驅動程序是如何在中斷處理程序中將一個函數插入tq_immediate隊列中的。 Postscript Figure ./figs/ldr2_0601.eps here Figure Caption:task_queue的使用流程 示例程序是如何工作的 延遲計算的示例程序包含在jiq(Just In Queue)模塊中,本節中抽取了它的部分源碼。該模塊創建 /proc 文件,可以用 dd 或者其他工具來讀,這點上與 jit 模塊很相似。 讀 jiq 文件的進程被轉入睡眠狀態直到緩沖區滿*。 ===========footnote begins============ /proc文件的緩沖區是內存中的一頁:4KB,或對應於使用平台的尺寸。 ===========footnote ends============ 睡眠是由一個簡單的等待隊列處理的,聲明為 DECLARE_WAIT_QUEUE_HEAD (jiq_wait); 緩沖區由不斷運行的任務隊列來填充。任務隊列的每次運行都會在要填充的緩沖區中添加一個字符串,該字符串記錄了當前時間(jiffies值),當前進程以及 in_interrupt的返回值。 填充緩沖區的代碼都在jiq_print_tq函數中,任務隊列的每遍運行都要調用它。打印函數沒什麼意思,不在這裡列出,我們還是來看看插入隊列的任務的初始化代碼: struct tq_struct jiq_task; /* global: initialized to zero */ /* these lines are in jiq_init() */ jiq_task.routine = jiq_print_tq; jiq_task.data = (void *)&jiq_data; 這裡沒必要對 jiq_task結構的 sync成員和next成員清零,因為靜態變量已由編譯器初始化為零了。 調度器隊列 最容易使用的任務隊列是調度器(scheduler)隊列,因為該隊列中的任務不會在中斷模式運行,因此可以做更多事,特別是它們還能睡眠。內核中有多處使用該隊列完成各種任務。 在內核2.4.0-test11,實際實現調度器隊列的任務隊列被內核的其余部分隱藏了。使用這個隊列的代碼必須調用schedule_task把任務放入隊列,而不能直接使用queue_task: int schedule_task(struct tq_struct *task); 其中的 task 當然就是要調度的任務。返回值直接來自queue_task:如果任務不在隊列中就返回非零。 再提一次,從版本 2.4.0-test11 開始內核使用了一個特殊進程 keventd,它唯一的任務就是運行 scheduler 隊列中的任務。keventd 為它運行的任務提供了可預期的進程上下文,而不象以前的實現,任務是在完全隨機的進程上下文中運行的。 對於 keventd 的執行有幾點是值得牢記的。首先,這個隊列中的任務可以睡眠,一些內核代碼就使用了這一優點。但是,好的代碼應該只睡眠很短的時間,因為在 keventd 睡眠的時候,調度器隊列中的其他任務就不會再運行了。還有一點需要牢記,你的任務是和其它任務共享調度器隊列,這些任務也可以睡眠。正常情況下,調度器隊列中的任務會很快運行(也許甚至在schedule_task返回之前)。但如果其它某個任務睡眠了,輪到你的任務執行時,中間流逝的時間會顯得很久。所以那些有嚴格的執行時限的任務應該使用其它隊列。 /proc/jiqsched 文件是使用調度器隊列的示例文件,該文件對應的 read 函數以如下的方式將任務放進隊列中: int jiq_read_sched(char *buf, char **start, off_t offset, int len, int *eof, void *data) { jiq_data.len = 0; /* nothing printed, yet */ jiq_data.buf = buf; /* print in this place */ jiq_data.jiffies = jiffies; /* initial time */ /* jiq_print will queue_task() again in jiq_data.queue */ jiq_data.queue = SCHEDULER_QUEUE; schedule_task(&jiq_task); /* ready to run */ interruptible_sleep_on(&jiq_wait); /* sleep till completion */ *eof = 1; return jiq_data.len; } 讀取 /proc/jiqsched 文件產生如下輸出: time delta interrupt pid cpu command 601687 0 0 2 1 keventd 601687 0 0 2 1 keventd 601687 0 0 2 1 keventd 601687 0 0 2 1 keventd 601687 0 0 2 1 keventd 601687 0 0 2 1 keventd 601687 0 0 2 1 keventd 601687 0 0 2 1 keventd 601687 0 0 2 1 keventd 上面的輸出中,time域是任務運行時的jiffies值,delta是自任務最近一次運行以來jiffies的增量,interrupt是in_interrupt函數的輸出,pid是運行進程的ID,cpu是正被使用的CPU的編號(在單處理器系統中始終為0),command是當前進程正在運行的命令。 在這個例子中,我們看到,任務總是在 keventd 進程中運行,而且運行得非常快,一個不斷把自己重復提交給調度器隊列的任務可以在一次定時器滴答中運行數百甚至數千次。即使是在一個負載很重的系統,調度器隊列的延遲也是非常小的。 定時器隊列 定時器隊列的使用方法和調度器隊列不同,它(tq_timer)是可以直接操作的。還有,定時器隊列是在中斷模式下執行的。另外,該隊列一定會在下一個時鐘滴答被運行,這消除了可能因系統負載造成的延遲。 示例代碼使用定時器隊列實現了/proc/jiqtimer。使用這個隊列要用到 queue_task 函數。 int jiq_read_timer(char *buf, char **start, off_t offset, int len, int *eof, void *data) { jiq_data.len = 0; /* nothing printed, yet */ jiq_data.buf = buf; /* print in this place */ jiq_data.jiffies = jiffies; /* initial time */ jiq_data.queue = &tq_timer; /* reregister yourself here */ queue_task(&jiq_task, &tq_timer); /* ready to run */ interruptible_sleep_on(&jiq_wait); /* sleep till completion */ *eof = 1; return jiq_data.len; } 下面是在我的系統在編譯一個新內核時運行命令head /proc/jiqtimer輸出的結果: time delta interrupt pid cpu command 45084845 1 1 8783 0 cc1 45084846 1 1 8783 0 cc1 45084847 1 1 8783 0 cc1 45084848 1 1 8783 0 cc1 45084849 1 1 8784 0 as 45084850 1 1 8758 1 cc1 45084851 1 1 8789 0 cpp 45084852 1 1 8758 1 cc1 45084853 1 1 8758 1 cc1 45084854 1 1 8758 1 cc1 45084855 1 1 8758 1 cc1 注意,這次在任務的每次執行之間正好都經過了一個定時器滴答,而且正在運行的可能是任意一個進程。 立即隊列 最後一個可由模塊代碼使用的預定義隊列是立即隊列。這個隊列通過底半處理機制運行,所以要用它還需額外的步驟。底半處理程序只有在通知內核需要它運行時才會運行,這是通過“標記”底半部完成的。對於tq_immediate,必須調用mark_bh(IMMEDIATE_BH)。注意必須在任務插入隊列後才能調用mark_bh,否則可能在任務還沒加入隊列時內核就開始運行隊列了。 立即隊列是系統處理得最快的隊列――它反應最快並且在中斷期間運行。立即隊列既可以由調度器執行,也可以在一個進程從系統調用返回時被盡快地執行。典型的輸出大致如下: time delta interrupt pid cpu command 45129449 0 1 8883 0 head 45129453 4 1 0 0 swapper 45129453 0 1 601 0 X 45129453 0 1 601 0 X 45129453 0 1 601 0 X 45129453 0 1 601 0 X 45129454 1 1 0 0 swapper 45129454 0 1 601 0 X 45129454 0 1 601 0 X 45129454 0 1 601 0 X 45129454 0 1 601 0 X 45129454 0 1 601 0 X 45129454 0 1 601 0 X 45129454 0 1 601 0 X 顯然該隊列不能用於延遲任務的執行――它是個“立即”隊列。相反,它的目的是使任務盡快地得以執行,但是要在“安全的時間”內。這對中斷處理非常有用,因為它提供了在實際的中斷處理程序之外執行處理程序代碼的一個入口點,例如接收網絡包的機制就類似這樣。 注意不要把任務重新注冊到立即隊列中(盡管/proc/jiqimmed為了演示而這麼做),這種做法沒什麼好處,而且在某些版本/平台的搭配上運行時會鎖死計算機。因為在有些實現中會不斷重運行立即隊列直到它空為止。這種情況出現過,例如在PC上運行2.0版本的時候。 運行自己的工作隊列 聲明新的任務隊列並不困難。驅動程序可以隨意地聲明一個甚至多個新任務隊列。這些隊列的使用和我們前面討論過的預定義隊列差不多。 與預定義隊列不同的是,內核不會自動處理定制的任務隊列。定制的任務隊列要由程序員自己維護,並安排運行方法。 下面的宏聲明一個定制隊列並擴展為變量聲明。最好把它放在文件開頭的地方,所有函數的外面: DECLARE_TASK_QUEUE(tq_custom); 聲明完隊列,就可以調用下面的函數對任務進行排隊。上面的宏和下面的調用相匹配: queue_task(&custom_task, &tq_custom); 當要運行累積的任務隊列時,執行下面一行,運行tq_custom隊列: run_task_queue(&tq_custom); 如果現在想測試定制的任務隊列,則需要在某個預定義的隊列中注冊一個函數來觸發這個隊列。盡管看起來象繞了彎路,但其實並非如此。當需要累積任務以便同時得到執行時,定制的任務隊列是非常有用的,盡管需要用另一個隊列來決定這個“同時”。 Tasklets 就在 2.4 內核發布之前,開發者們增加了一種用於內核任務延遲的新機制。這種新機制稱為tasklet,現在是實現底半任務的推薦方法。實際上,現在的底半處理程序本身就是用 tasklet 實現的。 tasklets在很多方面類似任務隊列。它們都是把任務延遲到安全時間執行的一種方式,都在中斷期間運行。象任務隊列一樣,即使被調度多次,tasklet 也只運行一次,不過tasklet 可以在 SMP 系統上和其它(不同的) tasklet 並行地運行。在SMP系統上,tasklet 還被確保在第一個調度它的 CPU 上運行,因為這樣可以提供更好的高速緩存行為,從而提高性能。 每個 tasklet 都與一個函數相聯系,當 tasklet 要運行的時候該函數被調用。該函數只有一個 unsigned long 類型的參數,這多少使一些內核開發者的生活變得輕松;但對那些寧願傳遞一個指針的開發人員來說肯定是增加了苦惱。把long類型的參數轉換為一個指針類型在所有已支持的平台上都是安全的操作,在內存管理中(第13章討論)更是普遍使用。這個tasklet的函數的類型是void,無返回值。 tasklet 的實現部分在 中,它自己必須用下列中的一種來聲明: DECLARE_TASKLET(name, function, data); 用指定的名字 name 聲明一個 tasklet,在該 tasklet 執行時(後面要講到),指定的函數 function 被調用,傳遞的參數值為 (unsigned long) data 。 DECLARE_TASKLET_DISABLED(name, function, data); 和上面一樣聲明一個 tasklet,不過初始狀態是“禁止的”,意味著可以被調度但不會執行,直到被“使能”以後才能執行。 用2.4的頭文件編譯 jiq 示例驅動程序,可以實現 /proc/jiqtasklet,它和其他的 jiq 入口工作類似,只不過使用了tasklet。我們並沒有在 sysdep.h 中為舊版本模擬實現 tasklet。該模塊如下定義它的 tasklet: void jiq_print_tasklet (unsigned long); DECLARE_TASKLET (jiq_tasklet, jiq_print_tasklet, (unsigned long) &jiq_data); 當驅動程序要調度一個 tasklet 運行的時候,它調用 tasklet_schedule: tasklet_schedule(&jiq_tasklet); 一旦一個 tasklet 被調度,它就肯定會在一個安全時間運行一次(如果已經被使能)。tasklet 可以重新調度自己,其方式和任務隊列一樣。在多處理器系統上,一個 tasklet 無須擔心自己會在多個處理器上同時運行,因為內核采取了措施確保任何 tasklet 都只能在一個地方運行。但是,如果驅動程序中實現了多個 tasklet,那麼就可能會有多個 tasklet 在同時運行。在這種情況下,需要使用自旋鎖來保護臨界區代碼(信號量是可以睡眠的,因為 tasklet 是在中斷期間運行,所以不能用於tasklet)。 /proc/jiqtasklet的輸出如下: time delta interrupt pid cpu command 45472377 0 1 8904 0 head 45472378 1 1 0 0 swapper 45472379 1 1 0 0 swapper 45472380 1 1 0 0 swapper 45472383 3 1 0 0 swapper 45472383 0 1 601 0 X 45472383 0 1 601 0 X 45472383 0 1 601 0 X 45472383 0 1 601 0 X 45472389 6 1 0 0 swapper 注意這個tasklet總是在同一個CPU上運行,即使輸出來自雙CPU系統。 tasklet 子系統提供了一些其它的函數,用於高級的tasklet操作: void tasklet_disable(struct tasklet_struct *t); 這個函數禁止指定的tasklet。該tasklet仍然可以用 tasklet_schedule 調度,但執行被推遲,直到重新被使能。 void tasklet_enable(struct tasklet_struct *t); 使能一個先前被禁止的tasklet。如果該tastlet已經被調度,它很快就會運行(但一從 tasklet_enable 返回就直接運行)。 void tasklet_kill(struct tasklet_struct *t); 該函數用於對付那些無休止地重新調度自己的 tasklet。tasklet_kill 把指定的 tasklet 從它所在的所有隊列刪除。為避免與正重新調度自己的tasklet產生競態,該函數會等到tasklet執行,然後再把它移出隊列。這樣就可以確保 tasklet 不會在中途被打斷。然而,如果目標 tasklet 當前既沒有運行也沒有重調度自己,tasklet_kill會掛起。tasklet_kill不能在中斷期間被調用。 內核定時器 內核中最終的計時資源還是定時器。定時器用於調度函數(定時器處理程序)在未來某個特定時間執行。與任務隊列和 tasklet 不同,我們可以指定某個函數在未來何時被調用,但不能確定隊列中的會在何時執行。另外,內核定時器與任務隊列相似的是,注冊的處理函數只執行一次――定時器不是循環執行的。 有時候要執行的操作不在任何進程上下文內,比如關閉軟驅馬達和中止某個耗時的關閉操作,在這些情況下,延遲從 close 調用的返回對於應用程序不合適,而且這時也沒有必要使用任務隊列,因為已排隊的任務在必要的時間過去之後還要不斷重新注冊自己。 這時,使用定時器就方便得多。注冊處理函數一次,當定時器超時後內核就調用它一次。這種處理一般較適合由內核完成,但有時驅動程序也需要,就象軟驅馬達的例子。 內核定時器被組織成雙向鏈表。這意味著我們可以加入任意多的定時器。定時器包括它的超時值(單位是jiffies)和超時時要調用的函數。定時器處理程序需要接收一個參數,該參數和處理程序函數指針本身一起存放在一個數據結構中。 定時器的數據結構如下,取自頭文件 : struct timer_list { struct timer_list *next; /* never touch this */ struct timer_list *prev; /* never touch this */ unsigned long expires; /* the timeout, in jiffies */ unsigned long data; /* argument to the handler */ void (*function)(unsigned long); /* handler of the timeout */ volatile int running; /* added in 2.4; don't touch */ }; 定時器的超時值是個 jiffies值,當jiffies值大於等於timer->expires時,timer->function函數就要運行。timeout值是個絕對數值,它通常是用 jiffies 的當前值加上需要的延遲量計算出來的。 一旦完成對 timer_list 結構的初始化,add_timer 函數就將它插入一張有序鏈表中,該鏈表每秒鐘會被查詢 100 次左右。即使某些系統(如Alpha)使用更高的時鐘中斷頻率,也不會更頻繁地檢查定時器列表。因為如果增加定時器分辨率,遍歷鏈表的代價也會相應增加。 用於操作定時器的有如下函數: void init_timer(struct timer_list *timer); 該內聯函數用來初始化定時器結構。目前,它只將prev和next指針清零(在SMP系統上還有運行標志)。強烈建議程序員使用該函數來初始化定時器而不要顯式地修改結構內的指針,以保證向前兼容。 void add_timer(struct timer_list *timer); 該函數將定時器插入活動定時器的全局隊列。 int mod_timer(struct timer_list *timer, unsigned long expires); 如果要更改定時器的超時時間則調用它,調用後定時器使用新的 expires 值。 int del_timer(struct timer_list *timer); 如果需要在定時器超時前將它從列表中刪除,則應調用 del_timer 函數。但當定時器超時時,系統會自動地將它從鏈表中刪除。 int del_timer_sync(struct timer_list *timer); 該函數的工作類似 del_time,不過它還確保了當它返回時,定時器函數不在任何 CPU 上運行。當一個定時器函數在無法預料的時間運行時,使用del_timer_sync可避免產生競態,大多數情況下都應該使用這個函數。調用 del_timer_sync 時還必須保證定時器函數不會使用 add_timer 把它自己重新加入隊列。 使用定時器的一個例子是 jiq 示例模塊。/proc/jitimer 文件使用一個定時器來產生兩行數據,所使用的打印函數和前面任務隊列中用到的是同一個。第一行數據是由 read 調用產生的(由查看/proc/jitimer的用戶進程調用),而第二行是 1 秒後後定時器函數打印出的。 用於 /proc/jitimer文件的代碼如下所示: struct timer_list jiq_timer; void jiq_timedout(unsigned long ptr) { jiq_print((void *)ptr); /* print a line */ wake_up_interruptible(&jiq_wait); /* awaken the process */ } int jiq_read_run_timer(char *buf, char **start, off_t offset, int len, int *eof, void *data) { jiq_data.len = 0; /* prepare the argument for jiq_print() */ jiq_data.buf = buf; jiq_data.jiffies = jiffies; jiq_data.queue = NULL; /* don't requeue */ init_timer(&jiq_timer); /* init the timer structure */ jiq_timer.function = jiq_timedout; jiq_timer.data = (unsigned long)&jiq_data; jiq_timer.expires = jiffies + HZ; /* one second */ jiq_print(&jiq_data); /* print and go to sleep */ add_timer(&jiq_timer); interruptible_sleep_on(&jiq_wait); del_timer_sync(&jiq_timer); /* in case a signal woke us up */ *eof = 1; return jiq_data.len; } 運行命令 head /proc/jitimer得到如下輸出結果: time delta interrupt pid cpu command 45584582 0 0 8920 0 head 45584682 100 1 0 1 swapper 從輸出中可以發現,打印出最後一行的定時器函數是在中斷模式運行的。 可能看起來有點奇怪的是,定時器總是可以正確地超時,即使處理器正在執行系統調用。我在前面曾提到,運行在內核態的進程不會被調出,但時鐘中斷是個例外,它與當前進程無關,獨立完成了自己的任務。讀者可以試試同時在後台讀 /proc/jitbusy 文件和在前台讀 /proc/jitimer 文件會發生什麼。這時盡管看起來系統似乎被忙等待的系統調用給鎖死住了,但定時器隊列和內核定時器還是能不斷得到處理。 因此,定時器是另一個競態資源,即使是在單處理器系統中。定時器函數訪問的任何數據結構都要進行保護以防止並發訪問,保護方法可以用原子類型(第10章講述)或者用自旋鎖。 刪除定時器時也要小心避免競態。考慮這樣一種情況:某一模塊的定時器函數正在一個處理器上運行,這時在另一個處理器上發生了相關事件(文件被關閉或模塊被刪除)。結果是,定時器函數等待一種已不再出現的狀態,從而導致系統崩潰。為避免這種競態,模塊中應該用 del_timer_sync 代替 del_timer。如果定時器函數還能夠重新啟動自己的定時器(這是一種普遍使用的模式),則應該增加一個“停止定時器”標志,並在調用del_timer_sync之前設置。這樣定時器函數執行時就可以檢查該標志,如果已經設置,就不會用 add_timer 重新調度自己了。 還有一種會引起競態的情況是修改定時器:先用 del_timer 刪除定時器,再用 add_timer 加入一個新的以達到修改目的。其實在這種情況下簡單地使用 mod_timer 是更好的方法。 向後兼容性 任務隊列和時間機制的實現多年來基本保持著相對的穩定。不過,還是有一些值得注意的改進。 sleep_on_timeout、interruptible_sleep_on_timeout和schedule_timeout這幾個函數是在2.2版本內核才加入的。在使用2.0的時期,超時值是通過 task 結構中的一個變量(timeout)處理的。作一個比較,現在的代碼是這樣進行調用的: interruptible_sleep_on_timeout(my_queue, timeout); 而以前則是如下這樣編寫: current->timeout = jiffies + timeout; interruptible_sleep_on(my_queue); 頭文件sysdep.h為2.4以前的內核重建了schedule_timeout,所以可以在2.0和2.2版本使用新語法並正常運行: extern inline void schedule_timeout(int timeout) { current->timeout = jiffies + timeout; current->state = TASK_INTERRUPTIBLE; schedule(); current->timeout = 0; } 2.0 版本還有另外兩個函數可把函數放入任務隊列。中斷被禁止時可以用queue_task_irq代替queue_task,這會損失一點性能。queue_task_irq_off更快些,但在任務已經插入隊列或正在運行時會出錯,所以只有在確保這類情況不會發生時才能使用。這兩個函數在提升性能方面都沒什麼好處,從內核 2.1.30 開始把它們去掉了。任何情況下,使用 queue_task 都能在所有內核版本下工作。(要注意一點,在 2.2 及其以前內核中,queue_task 返回值的類型是void) 2.4內核之前不存在schedule_task函數及 keventd 進程,使用的是另一個預定義任務隊列 tq_scheduler。tq_scheduler隊列中的任務在 schedule 函數中執行,所以總是運行在進程上下文中。然而,“提供”上下文的進程總是不同的,它有可能是當時正被CPU調度運行的任何一個進程。tq_scheduler通常有比較大的延遲,特別是對那些會重復提交自己的任務更是如此。sysdep.h 在 2.0 和 2.2 系統上對 schedule_task 的實現如下: extern inline int schedule_task(struct tq_struct *task) { queue_task(task, &tq_scheduler); return 1; } 前面已經提到,2.3內核系列中增加了tasklet機制。在此之前,只有任務隊列可以用於“立即延遲”的執行。底半處理部分也改動了,不過大多數改動對驅動程序開發人員是透明的。sysdep.h中不再模擬tasklet在舊內核上的實現,它們對驅動程序操作來說並非嚴格必要。如果想要保持向後兼容,要麼編寫自己的模擬代碼,要麼用任務隊列代替。 Linux 2.0中沒有 in_interrupt函數,代替它的是一個全局變量intr_count,記錄著正運行的中斷處理程序的個數。查詢 intr_count 的語法和調用 in_interrupt 差不多,所以在sysdep.h 中保持兼容性是很容易實現的。 函數 del_timer_sync 在內核 2.4.0-test2 之前還沒有引入。sysdep.h 中進行了一些替換,以便使用舊的內核頭文件也可以編譯。2.0版本內核也沒有mod_timer。這個問題也在兼容性頭文件中得以解決。 快速參考 本章引入如下符號: #include "HZ" HZ符號指出每秒鐘產生的時鐘滴答數。 #include "volatile unsigned long jiffies" jiffies變量每個時鐘滴答後加1,因此它每秒增加 HZ 次。 #include "rdtsc(low,high);" "rdtscl(low);" 讀取時間戳計數器或其低半部分。頭文件和宏是 PC 類處理器特有的,其它平台可能需要用匯編語句實現類似功能。 extern struct timeval xtime; 當前時間,由最近一次定時器滴答計算出。 #include "void do_gettimeofday(struct timeval *tv);" "void get_fast_time(struct timeval *tv);" 這兩個函數返回當前時間。前者具有很高的分辨率,後者更快些,但分辨率較差。 #include "void udelay(unsigned long usecs);" "void mdelay(unsigned long msecs);" 這兩個函數引入整數數目的微秒或毫秒的延遲。前一個應用於不超過1毫秒的延遲;後一個使用時要格外慎重,因為它們使用的都是忙等待循環。 int in_interrupt(); 如果處理器正在中斷模式運行,就返回非0值。 #include "DECLARE_TASK_QUEUE(variablename);" 該宏聲明一個新的變量並作初始化。 void queue_task(struct tq_struct *task, task_queue *list); 該函數注冊一個稍後執行的任務。 void run_task_queue(task_queue *list); 該函數運行任務隊列。 task_queue tq_immediate, tq_timer; 這些預定義的任務隊列在內核調度新的進程前(tq_immediate)盡快地,或者在每個時鐘滴答後(tq_timer)得到執行。 int schedule_task(struct tq_struct *task); 調度一個任務在調度器隊列運行。 #include "DECLARE_TASKLET(name, function, data)" "DECLARE_TASKLET_DISABLED(name, function, data)" 聲明一個 tasklet 結構,運行時它將調用指定的函數function(並將指定參數unsigned long data傳遞給函數)。第二種形式把 tasklet 初始化為禁止狀態,直到明確地使能後tasklet才能運行。 void tasklet_schedule(struct tasklet_struct *tasklet); 調度指定的 tasklet 運行。如果該 tasklet 沒有被禁止,它將在調用了 tasklet_schedule 的CPU上很快得到執行。 tasklet_enable(struct tasklet_struct *tasklet); "tasklet_disable(struct tasklet_struct *tasklet);" 這兩個函數分別使能和禁止指定的tasklet。被禁止的 tasklet 可以被調度,但只有使能後才能運行。 void tasklet_kill(struct tasklet_struct *tasklet); 使一個正“無休止重新調度”的tasklet停止執行。該函數可以阻塞,而且不能在中斷期間調用。 #include "void init_timer(struct timer_list * timer);" 該函數初始化新分配的定時器。 void add_timer(struct timer_list * timer); 該函數將定時器插入待處理定時器的全局隊列。 int mod_timer(struct timer_list *timer, unsigned long expires); 該函數用於更改一個已調度的定時器結構中的超時時間。 int del_timer(struct timer_list * timer); del_timer函數將定時器從待處理定時器隊列中刪除。如果隊列中存在該定時器,del_timer返回1,否則返回0。 int del_timer_sync(struct timer_list *timer); 該函數類似del_timer,但是確保定時器函數當前不在其它 CPU 上運行。 FROM:http://www.linuxforum.net/forum/showflat.php?Cat=&Board=linuxK&Number=294854&page=6&view=collapsed&sb=5&o=all