Chapter 6 時序
介紹 由簡至繁 瞭解核心的時序(timing) 認識目前時間(current time) 將作業的時間點延遲到一定時間之後才開始 將非同步函式(asynchronous function)安排到一定時間之後才開始作用
6.1 核心的計時間隔 中斷—CPU暫停目前工作,然後執行ISR來處理中斷.(CH9) 計時器中斷,固定間隔觸發的中斷事件,核心依據HZ(定義在<linux/param.h>)的值來設定間隔長度,硬體平台不同,值也不同. 每次發生計時器中斷,jiffies變數的值就會被遞增一次,宣告在<linux/sched.h>,型別為unsigned long volatile,核心確保溢位之後還能正確運作,不必擔心,只須注意.
6.1.1 處理器特有的暫存器 大多數系統上,由於指令時序的不可預測性(指令排程、分支預測、快取等),時脈計數器成為唯一可靠的精密計時工具. 時脈計數器依平台而異,不一定可寫,長度也不一定是32bits或64bits,無論是否可歸零,鄭重建議不要如此,因為可以使用無號變數來計算差值. 最知名的時脈計數暫存器是x86 Pentium系列的TSC(TimeStamp Counter),計算CPU時脈週期數的64bits的暫存器,kernel&user-space都可以讀取. 引入<asm/msr.h> (Machine-Specific Register)之後,可使用下列巨集 rdtsc(low,high); rdtscl(low);
6.1.1 處理器特有的暫存器 量測指令本身執行時間 unsigned long ini, end; rdtscl(ini); rdtscl(end); printk(“time lapse: %li\n”,end-ini); 與平台無關的函式用來替代rdstc() #include <linux/timex.h> cycles_t get_cycles(void); //無時脈計數,則回傳永0,32bit 如何內插組語指令(iniline assembly) for MIPS #define rdtscl (dest) \ __asm__ __volatile__(“mfc0 %0,$9; nop” : “=r” (dest)) 內插組語的語法相當有威力,但是有點複雜,特別是在那些會限定暫存器用途的平台上(x86系列).完整語法請參考gcc的說明文件.
6.2 取得目前時間 jiffies從開機到至今的時間,與驅動程式生命期無關,也不可能跨越開關機時間. 驅動程式可利用jiffies的現值來估算兩事件之間的間隔時間,如mouse 驅動程式不需要知道牆鐘時刻(wall-clock time),若真的需要靠自己處理當時的時刻,do_gettimeofday()或許可派上用場. 此函式並非直接告知今天是星期幾,而是將一般的秒與微秒填入一個struct timeval,原型如下 #include <linux/time.h> void do_gettimeofday(struct timeval *tv);
cat /proc/currentime /proc/currentime /proc/currentime 6.2 取得目前時間 從xtime變數同樣也可取得目前時刻,但這是不被鼓勵的行為,因為無法連動取得timevalue,結構內的tv_sec與tv_usec欄位值,除非暫停掉中斷. 不太講求精準度,2.2版核心提供一個快又安全的函式來取得目前時刻: void get_fast_time(struct timeval *tv); 範例 jit(Just In Time)模組,他不會產生裝置節點,而是直接將它取得的時刻資訊透過/proc/currentime傳到user-space. cat /proc/currentime /proc/currentime /proc/currentime
6.3 延遲執行 延遲—通常是為了讓硬體有足夠充裕的時間完成某些工作 需要考慮的重點之一,是延遲時間是否超過一個時脈單位 較長的延遲,可以利用系統時鐘來計時,較短的延遲,則通常以軟體迴圈來應付
6.3.1 長期延遲 最簡單也是最蠢的做法,稱為忙著等待(busy waiting): unsigned long j = jiffies + jit_delay * HZ; while (jiffies < j) /* 發呆… */ ; 因為jiffies是volatile變數,使得C編譯器會落實每次的讀取動作(不使用快取技術). 在延遲期間,處理器是被鎖死的,因為這是核心裡的回圈,排程器不會岔斷進入核心行程. 若中斷失效時進入迴圈,jiffies不會更新,迴圈無法終止,只能使用reset按鈕.
6.3.1 長期延遲 time cat /proc/jitsched busy-wait範例,讀取/proc/jitbusy,每當它的read作業方法被呼叫一次,其內部的忙碌迴圈就會延遲一秒.如果使用dd if=/proc/jitbusy bs=1命令,就可以看到每秒讀出一個字元的效果. 這種做法會嚴重拖累系統效能,因為其他行程每隔一秒才有機會執行一次,比較合理的做法是: while (jiffies < j) schedule(); 但還不夠理想,倘若它是整個系統上唯一的可跑行程,它真的會動作(呼叫schedule(),然後立刻被排程器選中,然後再呼叫schedule() … 所以說,機器負載程度將至少等於1,而idle行程將沒機會上線. (省電 降溫 生命年) 若在一個非常忙碌的系統上,呼叫排程器的做法,反而有可能造成驅動程式等待了超過原本預期的時間. time cat /proc/jitsched
6.3.1 長期延遲 排程迴圈提供一個觀測驅動程式工作程序的速成工具.(printk()之後一點點延遲,讓klogd有機會盡忠職守,以免不知如何死當) 最佳的延遲方式,是要求核心代勞,核心提供兩種執行短程延遲的機制,看你的驅動程式是否要等待其他事件而定. sleep_on_timeout (wait_queue_head_t *q, unsigned long timeout); interruptible_sleep_on_timeout (wait_queue_head_t *q, unsigned long timeout); 兩種版本都會讓行程待在指定的待命佇列裡休眠,但一定會在指定期限內返回.timeout值是要等待的jiffies個數,而非jiffies絕對值.
6.3.1 長期延遲 範例 /proc/jitqueue wait_queue_head_t wait; init_waitqueue_head (&wait); interruptible_sleep_on_timeout (&wait, jit_delay*HZ); 但沒有人會對這個待命佇列呼叫wake_up(),所以必定是因為超過timeout期限而甦醒,所以這種延遲計完美又合法. 若不須等待其他事件,還有更直接了當的方法: set_current_state (TASK_INTERRUPTIBLE); schedule_timeout (jit_delay*HZ); 但實際延遲時間,有可能略為超過你原本預期的時間. time cat /proc/jitqueue time cat /proc/jitself
6.3.2 短期延遲 在計算非常短暫的延遲,jiffies無法達成,所以核心提供udelay()和mdelay()函式,原型如下: #include <linux/delay.h> void udelay (unsigned long usecs); //inline void mdelay (unsigned long msecs); udelay()以當地系統的BogoMips(開機所計算出的系統常數)值來決定迴圈的圈數,其值大約是CPU時脈數的兩倍左右. mdelay()是含有udelay()的迴圈所構成 兩者都是busy-waiting函式,因此除非沒有其他辦法,否則應該儘量避免使用mdelay()
6.4 工作佇列 (Task Queue) 不倚賴中斷機制的前提下,將某些工作安排到一段時間之後才開始執行. 有三種介面,分別是工作佇列、tasklet(2.3.43) 以及核心計時器 工作佇列和tasklet是安排工作執行時機的工具,最常被應用在interrupt handler.而核心計時器是用來將工作安排到未來的特定時間才執行. 本節先說明工作佇列,然後介紹核心提供的現成工作佇列,以及如何觸發驅動程式自己產生的工作佇列,最後看看新玩意—tasklet介面.
6.4.1 工作佇列的本質 由task構成的串列,每一個工作都是以一個函式指標與一個引數的組合來表示. 當工作開始跑時,它會收到一個void *引數,並傳回void. 指標引數可用來傳遞資料結構給工作函式,也可以被忽略. 下列結構描述引述自<linux/tqueue.h> struct tq_struct { struct list_head list; /* linked list of active bh's */ unsigned long sync; /* must be initialized to zero */ void (*routine)(void *); /* function to call */ void *data; /* argument to function */ } bh代表bottom half(後半段-interrupt handler),目前只要知道它是為了處理非同步工作所提供的機制. (ch9)
6.4.1 工作佇列的本質 sync 避免將同一工作重複排在多個佇列裡,因為會破壞到next指標. 另一種資料結構是task_queue,她目前只是一個指向struct tq_struct的指標,必須先初始化成NULL才能使用. 以下是用來操作tq_struct和工作佇列的工具: DECLARE_TASK_QUEUE(name); 此巨集宣告一個名為name的工作佇列,並清空它 int queue_task(struct tq_struct *task, task_queue *list); 將工作排入佇列,若工作已存在則傳回0,成功則傳回非零值. void run_task_queue(task_queue *list); 用來消化指定佇列中的累積工作,不必直接呼叫,除非宣告並維護自己的工作佇列
6.4.2 工作佇列的運作原理 不同種類的佇列,各有不同的開工時機,唯一的共同點是,只有再核心沒有其他工作壓力時,才會執行它們. 工作佇列時常被當成軟體中斷的處理機制,能力上會受到一些限制,必須嚴格遵守下列規矩: 不容許存取user-space.因為沒有行程環境,所以沒有辦法接觸到任何特定行程的user-space. 在中斷模式下,current指標是無效的,而且也不能使用. 不能休眠,也不能要求排程.不可kmalloc或semaphore等,因為會被催眠. 核心如何得知自己是否處於中斷模式下? 使用in_interrupt(), 若傳回非零值,表示處理器正處於中斷模式. 2.4版核心的工作佇列,還有一向值得注意的特性,那就是工作可以將自己排入自己原本所在的佇列裡,這種行為稱為重新排隊.
6.4.3 核心內建的工作佇列 要延遲特定工作的開始執行時間,最簡便的辦法是利用核心所維護的佇列.其中有三個可供驅動程式運用(宣告在<linux/tqueue.h>),分別是: 排程器佇列(scheduler queue) 在行程環境內運作,所以限制較寬鬆.2.4是以專用的 kernel thread來執行的,稱為keventd,並且必須透過 schedule_task()來存取. 計時器佇列(tq_timer) 由計時器時脈訊號觸發的佇列.必須遵守中斷模式規則 即期佇列(tq_immediate) 可能再系統呼叫返回之前或是排程器介入時,看何者先 到.會在中斷期被消化掉.
6.4.3 核心內建的工作佇列 嘗試讀取/proc/jiq*檔案的行程,會被推入休眠狀態,直到緩衝區填滿(/proc檔是一個分頁為4KB或是當地平合適值)才會被叫醒,待命佇列宣告如下: DECLARE_WAIT_QUEUE_HEAD (jiq_wait); 填寫緩衝區的動作,是由jiq_print_tq()負責,它會被排入工作佇列,並在其開始消化時,輸出訊息到相關的/proc/jiq*檔,初始化程序如下: struct tq_struct jiq_task; /* 全域變數 初始化歸零 */ jiq_task.routine = jiq_print_tq; jiq_task.data = (void *)&jiq_data; 我們不須清除jiq_task的sync和next,因為編譯器會將靜態變數初始會為0
6.4.3.1 排程器佇列 某些用途上,它是最容易使用的.因為它並不會在中斷時期執行,所以能力限制較寬鬆,作的是也比較多.最特別的是它可以休眠. 2.4版實作被隱藏,不能直接使用queue_task(),必須呼叫schedule_task(); int schedule_task(struct tq_struct *task); //非零值代表task先前並未排入佇列 但只能休眠非常短的週期,因為在keventd休眠期間,排程器裡的其它工作都將無法進行. time cat /proc/jiqsched
6.4.3.2 計時器佇列 計時器和排程器佇列最大的差異,再於你可以直接使用計時器佇列(tq_timer),由於它是在中斷時期執行,所能作的動作限制較多. 最明顯的特性,就是它保證下次計時器中斷時,佇列裡的工作一定會被執行一次. 這次則必須使用queue_task()來將工作排入tq_timer. head /proc/jiqtimer
6.4.3.3 即期佇列 此佇列是透過bottom-half機制來執行的,表示需要額外步驟才能使用,但核心不會隨便執行你寫好的bh,除非你將它標示出來. 若將工作排入該佇列(使用queue_task())之後,必須立刻呼叫mark_bh(),否則核心有可能在你的工作排入佇列之前,就開始消化工作佇列了. 它是Linux系統上最快速的佇列,只要一遇到中斷,就會被立刻執行. 消化其佇列的時機有二,一是由排程器觸發,二是再行程從系統呼叫返回之後那一瞬間.
6.4.3.3 即期佇列 很顯然地,它不能用來延遲工作的執行,畢竟它是”即期”佇列,這項特性使它成為interrupt handler的重要資源,因為它可讓interrupt handler用來安排來不及在中斷時期內執行完畢的工作,如 接收網路封包. 注意,排在即期佇列裡的工作,不應該有重新排隊的行為.這樣做並沒有好處,反而還有可能鎖死系統,因為某些平台核心的即期佇列會一直跑到清空為止,而重新排隊將使得佇列沒有清空的機會. head /proc/jiqimmed
6.4.4 自製的工作佇列 驅動程式有權宣告自己專用的新工作佇列(one or more),但核心不會自動執行驅動程式產生的佇列,所以必須另外安排觸發動作. 以下巨集用來宣告自製佇列.此巨集展開後會成為一般的變數宣告,所以應該宣告在程式開頭 DECLARE_TASK_QUEUE(tq_custom); 之後就可以使用正常函式來操作工作佇列,通常第一步是: queue_task(&custom_task, &tq_custom); 到了應該消化累積在佇列中的工作時,使用下列函式: run_task_queue(&tq_custom); 還需要另外再核心提供的現成工作佇列註冊一個函式,由該函式觸發你的自製佇列.
6.4.5 Tasklet 在2.4版正式發行之前,開發人員增加了一種新機制來執行延期的核心工作.此機制稱為tasklets,並且成為bottom-half工作的最佳選擇.事實上,bottom-half本身現在就是作成tasklet的形式. 它如同工作佇列,不管被排程幾次,tasklet也是只跑一次而已. 在SMP系統上,tasklet可以與其他不同的tasklet並行運作,哪個CPU安排的tasklet,就由哪一個CPU負責執行,簡化了系統的快取機制,同也獲得更高的效能. 每一個tasklet都有一個專屬的函式,當tasklet到了應該被執行的時間,該函式就會被呼叫.此函式只能有一個引數,不過將long引數鑄型成指標型別,在任何支援linux的平台上都是可行而安全的,甚至是記憶管理常用的技巧(ch13).
6.4.5 Tasklet 對於tasklet的軟體支援,都收納在<linux/interrupt.h>,而tasklet本身必須以下列方式之一宣告: DECLARE_TASKLET(name, function, data); 以指定的name宣告一個tasklet.再tasklet要被執行時,指定的function會被呼叫,並且會收到一個unsigned long data值. DECLARE_TASKLET_DISABLED(name, function, data); 同上式,但是其初始狀態是失效(disable).這表示它會參與排程,但是不會被執行,除非你在未來的某時間點讓它生效. jiq模組以下列方式宣告其tasklet: void jiq_print_tasklet (unsigned long); DECLARE_TASKLET (jiq_tasklet, jiq_print_tasklet, (unsigned long) &jiq_data); 當驅動程式想要讓tasklet開始接受排程時,必須呼叫tasklet_schedule(&jiq_tasklet);
6.4.5 Tasklet head /proc/jiqtasklet 注意到tasklet總是由同一個CPU負責執行,即使另一個CPU當時閒閒沒事幹. 核心的子系統還提供了一些輔助工具,幫助你進行更近接的應用: void tasklet_disable(struct tasklet_struct *t); 將tasklet暫時失效,但已加入排程則會繼續參與,但不會被選中,除非使它生效. void tasklet_enable(struct tasklet_struct *t); 使失效的重新生效 void tasklet_kill(struct tasklet_struct *t); 將其所在佇列抽離,解決會主動重新排隊的tasklet.不過,要是它不是可執行狀態也不會重排,則此指令可能會當掉.不可在中斷時期呼叫它. head /proc/jiqtasklet
6.5 核心計時器 它是核心內維持時序的終極資源,可用來安排一個函式(timeout handler)在未來的某特定時間才開始執行,而且只會執行一次(和工作佇列一樣). 計時器用法相當容易,妳的工作函式只要註冊一次,核心就會在計時器到期時呼叫該函式. 核心的計時器群是組織成一個雙向鏈結串列,可隨意增加. 注意,期限值是一個絕對值,取jiffies的現值在加上你想要延遲的間隔. 初始化timer_list後,就可用add_timer()將該結構安插到一個有序串列,大約每10ms會被輪詢一次.
6.5 核心計時器 以下是來操作計時器的工具函式: void init_timer(struct timer_list *timer); 這是inline函式,用於初始化timer_list結構.它將prev和next指標歸零(SMP,running歸零) void add_timer(struct timer_list *timer); 將指定計時器安插到全域性的活動計時器串列 int mod_timer(struct timer_list *timer, unsigned long expires); 修改計時器時限. 在返回瞬間開始生效. int del_timer(struct timer_list *timer); 若到期之前,會將計時期移出串列,若是在到期之後,核心會自動將計時器排出串列. int del_timer_sync(struct timer_list *timer); 類似前式,但它保證在它返回時,timeout handler不會在任何CPU上執行. 可避免相競狀況,除了那些自己會使用add_timer()來重新排隊的timeout handler之外,應該儘量使用它來替代del_timer().
struct timer_list jiq_timer; void jiq_timedout(unsigned long ptr) { jiq_print((void *)ptr); /* print a line */ wake_up_interruptible(&jiq_wait); /* 喚醒行程 */ } 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; /* 不重新排隊 */ 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); /* 因為有可能被喚醒 */ *eof = 1; return jiq_data.len; }
6.5 核心計時器 head /proc/jitimer //jit,jiq都需載入 timeout handler是在中斷時期,更令人好奇的是,不管當時系統是否忙碌,每次讀取/proc/jitimer所等待的時間間隔必定剛好一秒. 因為它的工作與當時的行程環境無關,所以,即使CPU被鎖死在忙碌迴圈裡,計時器佇列與核心計時器人然能順利運行. 因此它研然成為另一個相競狀況的來源,即使在單處理器上.因此必須以連動型別或空轉鎖(spinlocks)來加以保護,避免被同時存取. 刪除計時器是另一項可能引發相競的動作,解決辦法是必須設立一個停止計時器的旗標,然後呼叫_sync版本,timeout handler必須檢查此旗標是否設定,來決定要不要重新排隊. 修改計時器也可能造成相競狀況(del add),用mod_timer()來解決,一次搞定. head /proc/jitimer //jit,jiq都需載入