計時器是所有操作系統的一個必要組成部分,您將發現多個計時器機制。我們將首先簡要介紹一些 Linux 計時器模式,然后深入研究它們的運行方式。
(Linux)時間的起源
在 Linux 內核中,時間由一個名為?jiffies?的全局變量衡量,該變量標識系統啟動以來經過的滴答數。在最低的級別上,計算滴答數的方式取決于正在運行的特定硬件平臺;但是,滴答計數通常在一次中斷期間仍然繼續進行。滴答速率(jiffies?的最不重要的位)可以配置,但在最近針對 x86 的 2.6 內核中,一次滴答等于 4ms(250Hz)。jiffies?全局變量在內核中廣泛使用,目的有幾個,其中之一是提供用于計算一個計時器的超時值的當前絕對時間(稍后將展示一個例子)。
回頁首
內核計時器
最近的 2.6 內核中有幾個不同的計時器模式,其中最簡單、最不精確(但適用于大多數實例)的模式就是計時器 API。這個 API 允許構造在?jiffies?域(最低 4ms 超時)中運行的計時器。還有一個高精確度計時器 API,它允許構造在以納秒定義的時間中運行的計時器。根據您的處理器和處理器運行的速度,您的里程(mileage)可能會不同,但這個 API 的確提供了一種方法來在?jiffies?滴答間隔下調度超時。
標準計時器
標準計時器 API 作為 Linux 內核的一部分已經有很長一段時間了(自從 Linux 內核的早期版本開始)。盡管它提供的精確性比高精確度計時器要低,但它對于在處理物理設備時提供錯誤覆蓋的傳統驅動程序超時來說比較理想。在很多情況下,這些超時實際上從不觸發,而是被啟動,然后被刪除。
簡單內核計時器使用計時器輪(timer wheel)?實現。這個主意是由 Finn Arne Gangstad 在 1997 年首次引入的。它不理睬管理大量計時器的問題,而是很好地管理數量合理的計時器 — 典型情況。(原始計時器實現只是按照過期順序將計時器實現雙重鏈接。盡管在概念上比較簡單,但這種方法是不可伸縮的。)時間輪是一個 buckets 集合,其中每個 bucker 表示將來計時器過期的一個時間塊。這些 buckets 使用基于 5 個 bucket 的對數時間定義。使用?jiffies?作為時間粒度,定義了幾個組,它們表示將來的過期時段(其中每個組通過一列計時器表示)。計時器插入使用具有 O(1) 復雜度的列表操作發生,過期發生在 O(N) 時間內。計時器過期以串聯的形式出現,其中計時器被從高粒度 buckets 刪除,然后隨著它們的過期時間的下降被插入到低粒度 buckets 中。現在我們查看一下針對這個計時器實現的 API。
計時器 API
Linux 提供了一個簡單的 API 來構造和管理計時器。它包含一些函數(和助手函數),用于創建、取消和管理計時器。
計時器通過?timer_list?結構定義,該結構包括實現一個計時器所需的所有數據(其中包括列表指針和在編譯時配置的可選計時器統計數據)。從用戶角度看,timer_list?包含一個過期時間,一個回調函數(當/如果計時器過期),以及一個用戶提供的上下文。用戶必須初始化計時器,可以采取幾種方法,最簡單的方法是調用?setup_timer,該函數初始化計時器并設置用戶提供的回調函數和上下文。或者,用戶可以設置計時器中的這些值(函數和數據)并簡單地調用?init_timer。注意,init_timer?由?setup_timer?內部調用。
voidinit_timer( struct timer_list *timer );voidsetup_timer( struct timer_list *timer, void (*function)(unsigned long), unsigned long data );
擁有一個經過初始化的計時器之后,用戶現在需要設置過期時間,這通過調用?mod_timer?來完成。由于用戶通常提供一個未來的過期時間,他們通常在這里添加?jiffies?來從當前時間偏移。用戶也可以通過調用?del_timer?來刪除一個計時器(如果它還沒有過期):
intmod_timer( struct timer_list *timer, unsigned long expires );voiddel_timer( struct timer_list *timer );
最后,用戶可以通過調用?timer_pending(如果正在等待,將返回?1)來發現計時器是否正在等待(還沒有發出):
inttimer_pending( const struct timer_list *timer );
計時器示例
我們來檢查一下這些 API 函數的實際運行情況。清單 1?提供了一個簡單的內核模塊,用于展示簡單計時器 API 的核心特點。在?init_module?中,您使用?setup_timer?初始化了一個計時器,然后調用?mod_timer?來啟動它。當計時器過期時,將調用回調函數?my_timer_callback。最后,當您刪除模塊時,計時器刪除(通過?del_timer)發生。(注意來自?del_timer?的返回檢查,它確定計時器是否還在使用。)
清單 1. 探索簡單計時器 API
#include #include #include MODULE_LICENSE("GPL");staticstruct timer_listmy_timer;void my_timer_callback( unsigned long data ){ printk( "my_timer_callback called (%ld).\n", jiffies );}int init_module( void ){ int ret; printk("Timer module installing\n"); // my_timer.function, my_timer.datasetup_timer( &my_timer, my_timer_callback, 0 ); printk( "Starting timer to fire in 200ms (%ld)\n", jiffies ); ret =mod_timer( &my_timer, jiffies + msecs_to_jiffies(200) ); if (ret) printk("Error in mod_timer\n"); return 0;}void cleanup_module( void ){ int ret; ret =del_timer( &my_timer ); if (ret) printk("The timer is still in use...\n"); printk("Timer module uninstalling\n"); return;}
您可以在 ./include/linux/timer.h 中進一步了解計時器 API。盡管簡單計時器 API 簡單有效,但它并不能提供實時應用程序所需的準確性。為此,我們來看一下 Linux 最近新增的功能,該功能用于支持精確度更高的計時器。
高精確度計時器
高精確度計時器(簡稱?hrtimers)提供一個高精確度的計時器管理框架,這個框架獨立于此前討論過的計時器框架,原因是合并這兩個框架太復雜。盡管計時器在?jiffies?粒度上運行,hrtimers 在納秒粒度上運行。
hrtimer 框架的實現方式與傳統計時器 API 不同。hrtimer 不使用 buckets 和串聯操作,而是維護一個按時間排序的計時器數據結構(按時間順序插入計時器,以最小化激活時的處理)。這個數據結構是一個 “紅-黑” 樹,對于注重性能的應用程序很理想(且恰好作為內核中的一個庫普遍可用)。
hrtimer 框架作為內核中的一個 API 可用,用戶空間應用程序也可以通過?nanosleep、itimers?和 Portable Operating System Interface (POSIX)-timers interface 使用它。hrtimer 框架被主線化(mainlined)到 2.6.21 內核中。
高精確度計時器 API
hrtimer API 與傳統 API 有些相似,但它們之間的一些根本差別是它能夠進行額外的時間控制。應該注意的第一點是:時間不是用?jiffies?表示的,而是以一種名為?ktime?的特殊數據類型表示。這種表示方法隱藏了在這個粒度上有效管理時間的一些細節。hrtimer API 正式確認(formalize)了絕對時間和相對時間之間的區別,要求調用者指定類型。
與傳統的計時器 API 類似,高精確度計時器通過一個結構表示 — 這里是?hrtimer。這個結構從用戶角度定義定時器(回調函數、過期時間等)并包含了管理信息(其中計時器存在于 “紅-黑” 樹、可選統計數據等中)。
定義過程首先通過?hrtimer_init?初始化一個計時器。這個調用包含計時器、時鐘定義和計時器模式(one-shot 或 restart)。使用的時鐘在 ./include/linux/time.h 中定義,表示系統支持的各種時鐘(比如實時時鐘或者單一時鐘,后者只表示從一個起點[比如系統啟動]開始的時間)。計時器被初始化之后,就可以通過?hrtimer_start?啟動。這個調用包含過期時間(在?ktime_t中)和時間值的模式(絕對或相對值)。
voidhrtimer_init( struct hrtimer *time, clockid_t which_clock, enum hrtimer_mode mode );inthrtimer_start(struct hrtimer *timer, ktime_t time, const enum hrtimer_mode mode);
hrtimer 啟動后,可以通過調用?hrtimer_cancel?或?hrtimer_try_to_cancel?來取消。每個函數都包含將被停止的計時器的 hrtimer 引用。這兩個函數的區別在于:hrtimer_cancel?函數試圖取消計時器,但如果計時器已經發出,那么它將等待回調函數結束;hrtimer_try_to_cancel?函數也試圖取消計時器,但如果計時器已經發出,它將返回失敗。
inthrtimer_cancel(struct hrtimer *timer);inthrtimer_try_to_cancel(struct hrtimer *timer);
可以通過調用?hrtimer_callback_running?來檢查 hrtimer 是否已經激活它的回調函數。注意,這個函數由?hrtimer_try_to_cancel?內部調用,以便在計時器的回調函數被調用時返回一個錯誤。
inthrtimer_callback_running(struct hrtimer *timer);
ktime API
本文沒有討論 ktime API,它提供一組豐富的函數來以較高的精確度管理時間。可以在 ./linux/include/ktime.h 中查看 ktime API。
一個 hrtimer 示例
hrtimer API 的使用方法非常簡單,如?清單 2?所示。在?init_module?中,首先定義針對超時的相對時間(本例中為 200ms)。然后,通過調用?hrtimer_init?來初始化您的 hrtimer(使用單一時鐘),并設置回調函數。最后,使用此前創建的?ktime?值啟動計時器。當計時器發出時,將調用?my_hrtimer_callback?函數,該函數返回?HRTIMER_NORESTART,以避免計時器自動重新啟動。在?cleanup_module?函數中,通過調用?hrtimer_cancel?來取消計時器。
清單 2. 探索 hrtimer API
#include #include #include #include MODULE_LICENSE("GPL");#define MS_TO_NS(x)(x * 1E6L)staticstruct hrtimerhr_timer;enum hrtimer_restart my_hrtimer_callback( struct hrtimer *timer ){ printk( "my_hrtimer_callback called (%ld).\n", jiffies ); returnHRTIMER_NORESTART;}int init_module( void ){ktime_t ktime; unsigned long delay_in_ms = 200L; printk("HR Timer module installing\n"); ktime =ktime_set( 0, MS_TO_NS(delay_in_ms) );hrtimer_init( &hr_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL );hr_timer.function= &my_hrtimer_callback; printk( "Starting timer to fire in %ldms (%ld)\n", delay_in_ms, jiffies );hrtimer_start( &hr_timer, ktime, HRTIMER_MODE_REL ); return 0;}void cleanup_module( void ){ int ret; ret =hrtimer_cancel( &hr_timer ); if (ret) printk("The timer was still in use...\n"); printk("HR Timer module uninstalling\n"); return;}
關于 hrtimer API,還有許多內容這里沒有涉及到。一個有趣的方面是它能夠定義回調函數的執行上下文(比如在 softirq 或 hardiirq 上下文中)。您可以在 ./include/linux/hrtimer.h 文件中進一步了解 hrtimer API。
回頁首
內核列表
如本文此前所述,列表是有用的結構,內核提供了一個有效的通用使用實現。另外,您將在我們此前討論過的 APIs 下面發現列表。理解這個雙重鏈接的列表 API 有助于使用這個有效的數據結構進行開發,您會發現,代碼在這個利用列表的內核中是多余的。現在我們來快速了解一下這個內核列表 API。
這個 API 提供一個?list_head?結構,用于表示列表頭(錨點)和結構內(in-structure)列表指針。我們來檢查一個包含列表功能的樣例結構(參見?清單 3)。注意,清單 3 添加了?list_head結構,該結構用于對象鏈接(object linkage)。注意,可以在您的結構中的任意位置添加這個?list_head?結構 — 通過一些 GCC(list_entry?和?container_of,在 ./include/kernel/kernel.h 中定義)— 可以取消從列表指針到超對象的引用。
清單 3. 帶有列表引用的樣例結構
struct my_data_structure {int value;struct list_headlist;};
與其他列表實現一樣,需要一個列表頭來充當列表的錨點。這通常通過?LIST_HEAD?宏來完成,這個宏提供列表的聲明和初始化。這個宏創建一個結構?list_head?對象,可以在該對象上添加其他一些對象。
LIST_HEAD( new_list )
也可以通過使用?LIST_HEAD_INIT?宏手動創建一個列表頭(例如,您的列表頭位于另一個結構中)。
主初始化完成后,可以使用?list_add?和?list_del?等函數來操縱列表。下面,我們將跳到示例代碼,以便更好地解釋這個 API 的使用方法。
列表 API 示例
清單 4?提供一個簡單的內核模塊來探索幾個列表 AIO 函數(./include/linux/list.h 中包含更多函數)。這個示例創建了兩個列表,使用?init_module?函數來填充它們,然后使用?cleanup_module?函數來操縱這兩個列表。
一開始,您創建了您的數據結構(my_data_struct),該結構包含一些數據和兩個列表頭。這個示例展示可以將一個對象同時插入到多個列表中。然后,您創建了兩個列表頭(my_full_list?和?my_odd_list)。
在?init_module?函數中,您創建了 10 個數據對象,并使用?list_add?函數將它們加載到列表中(所有對象加載到?my_full_list?中,所有奇值對象加載到?my_odd_list) 中)。這里要注意一點:list_add?接受兩個參數,一個是將用到的對象中的列表引用,另一個是列表錨點。這個示例展示了將一個數據對象插入多個列表的能力,方法是使用內核的內部機制來識別包含列表引用的超級對象。
cleanup_module?函數提供了這個 API 的其他幾個功能,其中之一是?list_for_each?宏,這個宏簡化了列表迭代。對于這個宏,您提供了一個對當前對象(pos)的引用以及將被迭代的列表引用。對于每次迭代,您接收一個?list_head?引用,list_entry?接收這個引用以識別容器對象(您的數據結構)。指定您的結構和結構之內的列表變量,后者用于在內部取消引用,返回容器。
為發出奇值列表(odd list),要使用另一個名為?list_for_each_entry?的迭代宏。這個宏更簡單,因為它自動提供數據結構,無需再執行一個?list_entry?函數。
最后,使用?list_for_each_safe?來迭代列表,以便釋放已分配的元素。這個宏將迭代列表,但阻止刪除列表條目(刪除列表條目是迭代操作的一部分)。您使用?list_entry?來獲取您的數據對象(以便將它釋放回內核池),然后使用?list_del?來釋放列表中的條目。
清單 4. 探索列表 API
#include #include #include MODULE_LICENSE("GPL");struct my_data_struct { int value; structlist_headfull_list; structlist_headodd_list;};LIST_HEAD( my_full_list );LIST_HEAD( my_odd_list );int init_module( void ){ int count; struct my_data_struct *obj; for (count = 1 ; count < 11 ; count++) { obj = (struct my_data_struct *) kmalloc( sizeof(struct my_data_struct), GFP_KERNEL ); obj->value = count;list_add( &obj->full_list, &my_full_list ); if (obj->value & 0x1) {list_add( &obj->odd_list, &my_odd_list ); } } return 0;}void cleanup_module( void ){ structlist_head*pos, *q; struct my_data_struct *my_obj; printk("Emit full list\n");list_for_each( pos, &my_full_list ) { my_obj =list_entry( pos, struct my_data_struct, full_list ); printk( "%d\n", my_obj->value ); } printk("Emit odd list\n");list_for_each_entry( my_obj, &my_odd_list, odd_list ) { printk( "%d\n", my_obj->value ); } printk("Cleaning up\n");list_for_each_safe( pos, q, &my_full_list ) { struct my_data_struct *tmp; tmp =list_entry( pos, struct my_data_struct, full_list );list_del( pos ); kfree( tmp ); } return;}
還有很多其他函數,它們的用途包括在列表末尾而不是頭部添加數據(list_add_tail)、連接列表(list_splice)、測試列表的內容(list_empty)等。請參見?參考資料?詳細了解內核列表函數。
?
評論