Download presentation
Presentation is loading. Please wait.
1
Chapter 6 時序
2
介紹 由簡至繁 瞭解核心的時序(timing) 認識目前時間(current time) 將作業的時間點延遲到一定時間之後才開始
將非同步函式(asynchronous function)安排到一定時間之後才開始作用
3
6.1 核心的計時間隔 中斷—CPU暫停目前工作,然後執行ISR (Interrupt Service Routine)來處理中斷.(CH9) 計時器中斷,固定間隔觸發的中斷事件,核心依據HZ(定義在<linux/param.h>)的值來設定間隔長度,硬體平台不同,值也不同. 每次發生計時器中斷,jiffies變數的值就會被遞增一次,宣告在<linux/sched.h>,型別為unsigned long volatile,核心會確保溢位之後還能正確運作,驅動程式不必擔心jiffies溢位.
4
6.1.1 處理器特有的暫存器 大多數系統上,由於指令時序的不可預測性(因為有指令排程、分支預測、快取記憶體等因素),時脈計數器成為唯一可靠的精密計時工具. 時脈計數器的設計隨平台而異,不一定可寫,長度也不一定是32bits或64bits,無論是否可歸零,建議不要如此,因為可以使用無號變數來計算差值. 最知名的時脈計數暫存器是x86 Pentium系列的TSC (TimeStamp Counter),為計算CPU時脈週期數的64bits的暫存器,kernel & user -space都可以讀取. 引入<asm/msr.h> (Machine-Specific Registers)之後,可使用下列巨集 rdtsc(low,high); rdtscl(low);
5
6.1.1 處理器特有的暫存器 (Cont.) 量測指令本身執行時間 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); //無時脈計數,則回傳TSC暫存器低半段的值,因為可避免多暫存 器操作的問題且時脈計數器主要用途為測量極短的時間 如何內插組語指令(iniline assembly) 將x86的rdtscl()移植到MIPS系統 組語指令 Move from coprocessor 0 #define rdtscl (dest) \ __asm__ __volatile__(“mfc0 %0,$9; nop” : “=r” (dest)) 內插組語的語法相當有威力,但是有點複雜,特別是在那些會限定暫存器用途的平台上(x86系列).完整語法請參考gcc的說明文件.
6
6.2 取得目前時間 在Kernel-space的程式可以從jiffies的值取得目前時間
驅動程式可利用jiffies的現值來估算兩事件之間的間隔時間,如mouse driver 驅動程式不需要知道牆鐘時刻(wall-clock time),若真的需要靠自己處理當時的時刻,do_gettimeofday()或許可派上用場. 此函式並非直接告知今天是星期幾,而是將一般的秒與微秒填入一個struct timeval,原型如下 #include <linux/time.h> void do_gettimeofday(struct timeval *tv);
7
cat /proc/currentime /proc/currentime /proc/currentime
6.2 取得目前時間 (Cont.) 從xtime變數同樣也可取得目前時刻,但這是不被鼓勵的行為,因為無法連動(atomically)取得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
8
6.3 延遲執行 驅動程式通常需要拖延某段程式碼的開始執行時間—通常是為了讓硬體有足夠充裕的時間完成某些工作
需要考慮的重點之一,是延遲時間是否超過一個時脈單位 較長的延遲,可以利用系統時鐘來計時,較短的延遲,則通常以軟體迴圈來應付
9
6.3.1 長期延遲 (1/4) 最簡單也是最蠢的做法,稱為忙著等待(busy waiting):
unsigned long j = jiffies + jit_delay * HZ; while (jiffies < j) /* 發呆… */ ; 因為jiffies是volatile變數,使得C編譯器會落實每次的讀取動作(不使用快取技術). 在延遲期間,處理器是被鎖死的,因為這是核心裡的迴圈,排程器不會岔斷進入核心的行程. 若中斷失效時進入迴圈,jiffies就不會被更新,迴圈也將無法終止,此時只能使用reset按鈕.
10
6.3.1 長期延遲 (2/4) 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
11
6.3.1 長期延遲 (3/4) 排程迴圈提供一個觀測驅動程式工作程序的速成工具.(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絕對值.
12
6.3.1 長期延遲 (4/4) 範例 /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
13
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()
14
6.4 工作佇列 (Task Queue) 驅動程式都需要的能力為不倚賴中斷機制的前提下,將某些工作安排到一段時間之後才開始執行.
Linux有三種介面,分別是工作佇列、tasklet(2.3.43) 以及核心計時器 工作佇列和tasklet是安排工作執行時機的工具,最常被應用在interrupt handler.而核心計時器是用來將工作安排到未來的特定時間才執行. 本節先說明工作佇列,然後介紹核心提供的現成工作佇列,以及如何觸發驅動程式自己產生的工作佇列,最後看看新玩意—tasklet介面.
15
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)
16
6.4.1 工作佇列的本質 (Cont.) 核心利用sync旗標來避免將同一工作重複排在多個佇列裡,因為會破壞到next指標.
另一種資料結構是task_queue,它目前只是一個指向struct tq_struct的指標,task_queue指標必須先初始化成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); 用來消化指定佇列中的累積工作,你不必直接呼叫,除非你宣告並維護自己的工作佇列.
17
6.4.2 工作佇列的運作原理 工作佇列何時開工? 當核心空閒時。 不同種類的佇列,各有不同的開工時機。
工作佇列時常被當成軟體中斷的處理機制,在中斷模式執行程式碼,能力受到一些限制,必須嚴格遵守下列規矩: 不容許存取user-space.因為沒有行程環境,所以沒有辦法接觸到任何特定行程的user-space. 在中斷模式下,current指標是無效的,而且也不能使用. 不能休眠,也不能要求排程,也不能使用其他可能被催眠的函數,如呼叫kmalloc將違反本規矩或使用權狀(semaphore)等,因為在等待別人讓出權狀的過程中,你會被催眠. 核心程式如何得知自己是否處於中斷模式下? 使用in_interrupt(), 若傳回非零值,表示處理器正處於中斷模式. 2.4版核心的工作佇列,還有一向值得注意的特性,那就是工作可以將自己排入自己原本所在的佇列裡,這種行為稱為重新排隊.
18
6.4.3 核心內建的工作佇列 排程器佇列(scheduler queue)
要延遲特定工作的開始執行時間,最簡便的辦法是利用核心所維護的佇列.其中有三個可供驅動程式運用(宣告在<linux/tqueue.h>),分別是: 排程器佇列(scheduler queue) 在行程環境內運作,所以工作限制較寬鬆.在2.4版中,此佇列是以專用的kernel thread來執行的,稱為keventd,並且必須透過schedule_task()來存取. 計時器佇列(tq_timer) 由計時器時脈訊號(timer tick)觸發的佇列。由於是發生在中斷期,所以此佇列的任何工作發生在中斷期,所以必須遵守中斷模式規則。 即期佇列(tq_immediate) 被排入此佇列的工作會被盡快進行,立即佇列會在中斷期被消化完。
20
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
21
6.4.3.1 排程器佇列 time cat /proc/jiqsched
某些用途上,排程器佇列是最容易使用的.因為它並不會在中斷時期執行,所以能力限制較寬鬆,作的事也比較多.最特別的是它可以休眠. 在2.4版核心,實作排程器佇列的實際工作佇列被隱藏,不讓核心其他部分接觸到。且不能直接使用queue_task(),必須呼叫schedule_task(); int schedule_task(struct tq_struct *task); //傳回非零值代表task先前並未排入佇列 /proc/jiqsched 是使用排程器佇列的範例檔 良好的程式只能休眠非常短的週期,因為在keventd休眠期間,排程器裡的其它工作都將無法進行. time cat /proc/jiqsched
22
6.4.3.2 計時器佇列 head /proc/jiqtimer
計時器和排程器佇列最大的差異,再於你可以直接使用計時器佇列(tq_timer),由於它是在中斷時期執行,所能做的動作限制較多. 計時器佇列最明顯的特性,就是它保證下次計時器中斷時,佇列裡的工作一定會被執行一次,這樣可以消除系統負載所帶來的延遲效應. 讀取jiq模組的/proc/jiqtimer檔案,可讓該模組將jiq_task排到計時器佇列裡跑。 讀取jiq模組的/proc/jiqtimer檔案,可讓模組將jiq_task排到計時器佇列裡跑。不同於排程器佇列之處,為這次則必須使用queue_task()來將工作排入tq_timer. head /proc/jiqtimer
23
即期佇列 即期佇列是透過bottom-half機制來執行的,表示需要額外步驟才能使用,但核心不會隨便執行你寫好的bh,除非你將它”標示”出來. 對於td_immediate而言,若將工作排入該佇列(使用queue_task())之後,必須立刻呼叫mark_bh(),否則核心有可能在你的工作排入佇列之前,就開始消化工作佇列. 即期佇列是Linux系統上最快速的佇列,只要一遇到中斷,就會被立刻執行. 消化其佇列的時機有二,一是由排程器觸發,二是在行程從系統呼叫返回之後那一瞬間.
24
6.4.3.3 即期佇列 (Cont.) 讀取/proc/jiqimmed
很顯然地,即期佇列不能用來延遲工作的執行,畢竟它是”即期”佇列,這項特性使它成為interrupt handler的重要資源,因為它可讓interrupt handler用來安排來不及在中斷時期內執行完畢的工作,如:接收網路封包. 注意,排在即期佇列裡的工作,不應該有重新排隊的行為.這樣做並沒有好處,反而還有可能鎖死系統,因為某些平台核心的即期佇列會一直跑到清空為止,而重新排隊將使得佇列沒有清空的機會. head /proc/jiqimmed
25
6.4.4 自製的工作佇列 驅動程式有權宣告自己專用的新工作佇列(一個或多個),不同於核心所提供的工作佇列,核心不會自動執行驅動程式產生的佇列,所以程式設計師必須另外安排觸發動作. 以下巨集用來宣告自製佇列.此巨集展開後會成為一般的變數宣告,所以應該宣告在程式開頭 DECLARE_TASK_QUEUE(tq_custom); 之後就可以使用正常函式來操作工作佇列,通常第一步是: queue_task(&custom_task, &tq_custom); 到了應該消化累積在佇列中的工作時,使用下列函式: run_task_queue(&tq_custom); 還需要另外再核心提供的現成工作佇列註冊一個函式,由該函式觸發你的自製佇列.
26
6.4.5 Tasklet (1/3) 在2.4版正式發行之前,開發人員增加了一種新機制來執行延期的核心工作.此機制稱為tasklets,並且成為bottom-half工作的最佳選擇.事實上,bottom-half本身現在就是做成tasklet的形式. Tasklet如同工作佇列,不管被排程幾次,tasklet也是只跑一次而已. 在SMP系統上,tasklet可以與其他不同的tasklet並行運作,哪個CPU安排的tasklet,就由哪一個CPU負責執行,這不僅簡化了系統的快取機制,同時也獲得更高的效能. 每一個tasklet都有一個專屬的函式,當tasklet到了應該被執行的時間,該函式就會被呼叫.此函式只能有一個unsigned long引數,不過將long引數鑄型(cast)成指標型別,在任何支援linux的平台上都是可行而安全的,甚至是記憶管理常用的技巧(Ch13).
27
6.4.5 Tasklet (2/3) 對於tasklet的軟體支援,都收納在<linux/interrupt.h>,而tasklet本身必須以下列方式之一宣告: DECLARE_TASKLET(name, function, data); 以指定的name宣告一個tasklet.在tasklet要被執行時,指定的function會被呼叫,並且會收到一個unsigned long data值. DECLARE_TASKLET_DISABLED(name, function, data); 同上式,但是其初始狀態是失效(disabled).這表示它會參與排程,但是不會被執行,除非你在未來的某時間點讓它生效. jiq模組以下列方式宣告其tasklet: void jiq_print_tasklet (unsigned long); DECLARE_TASKLET (jiq_tasklet, jiq_print_tasklet, (unsigned long) &jiq_data); 當驅動程式想要讓tasklet開始接受排程時,必須呼叫tasklet_schedule(&jiq_tasklet);
28
6.4.5 Tasklet (3/3) head /proc/jiqtasklet /proc/jiqtasklet指令
注意到tasklet總是由同一個CPU負責執行,即使另一個CPU空閒時。 核心的子系統還提供了一些輔助工具,幫助你進行更進階的應用: void tasklet_disable(struct tasklet_struct *t); 將tasklet暫時失效,但已加入排程則會繼續參與,但不會被選中,除非你在未來的某時間點讓它生效. void tasklet_enable(struct tasklet_struct *t); 使失效的tasklet重新生效,對於已納入排程系統的tasklet,將盡快恢復執行。 void tasklet_kill(struct tasklet_struct *t); tasklet_kill()會將tasklet從任何他所在的佇列中抽離,為了那些解決會主動重新排隊的tasklet,此函數會等待要開始執行tasklet之前,才將他抽離佇列。不過,要是指定的tasklet不是可執行狀態而且不會重新排隊,則此tasklet_kill()可能會當掉。且不可在中斷時期呼叫它. head /proc/jiqtasklet
29
6.5 核心計時器 (1/3) 它是核心內維持時序的終極資源,可用來安排一個函式(timeout handler)在未來的某特定時間才開始執行,而且只會執行一次(這個特性跟工作佇列一樣). 計時器用法相當容易,你的工作函式只要註冊一次,核心就會在計時器到期時呼叫該函式. 核心的計時器群是組織成一個雙向鏈結串列,可隨意增加. 注意,期限值是一個絕對值,取jiffies的現值在加上你想要延遲的間隔. 初始化timer_list後,就可用add_timer()將該結構安插到一個有序串列,大約每10ms會被輪詢一次.
30
6.5 核心計時器 (2/3) 以下是來操作計時器的工具函式:
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); 類似del_timer(),但它保證在它返回後,timeout handler不會在任何CPU上執行. 當timeout hendler在預期外的時間執行時,可利用它避免相競狀況,除了那些自己會使用add_timer()來重新排隊的timeout handler之外,應該儘量使用它來替代del_timer().
31
/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); /* 喚醒行程 */ } 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; }
32
6.5 核心計時器 (3/3) head /proc/jitimer //jit,jiq都需載入
timeout handler是在中斷時期執行的,更令人好奇的是,不管當時系統是否忙碌,每次讀取/proc/jitimer所等待的時間間隔必定剛好一秒. 因為時脈中斷的工作與當時的行程環境無關,所以,即使CPU被鎖死在忙碌迴圈裡,計時器佇列與核心計時器仍然能順利運行. 因此計時器成為另一個相競狀況的來源,即使在單處理器系統上.因此必須以連動型別(atomic type)或空轉鎖(spinlocks)來加以保護,避免被同時存取. 刪除計時器是另一項可能引發相競狀況的動作,解決辦法是必須設立一個停止計時器的旗標,然後呼叫del_timer_sync(),timeout handler必須檢查此旗標是否設定,來決定要不要重新排隊. 修改計時器也可能造成相競狀況(del add),當你先使用del_timer()刪除舊的,然後使用add_timer()排入一個新的計時器的話,解決這問題方法為用mod_timer(),一次搞定. head /proc/jitimer //jit,jiq都需載入
33
P.200 第9行 P.207 第3行 time cat /proc/jitsched 改為time cat /proc/jitqueue
DECLARE_WAIT_QUEUE_HEAD(jit_wait)改為 DECLARE_WAIT_QUEUE_HEAD(jiq_wait)
Similar presentations