1 / 33

Understanding Timing in System Operation

Learn about core timing concepts in system operation, delay operations to a specific time, and schedule asynchronous functions. Explore processor-specific registers for precise timing and getting current time in the kernel space. Delay execution and handle long-term delays efficiently.

Download Presentation

Understanding Timing in System Operation

An Image/Link below is provided (as is) to download presentation Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author. Content is provided to you AS IS for your information and personal use only. Download presentation by click this link. While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server. During download, if you can't get a presentation, the file might be deleted by the publisher.

E N D

Presentation Transcript


  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從開機到至今的時間,與驅動程式生命期無關,也不可能跨越開關機時間. • 驅動程式可利用jiffies的現值來估算兩事件之間的間隔時間,如mouse driver • 驅動程式不需要知道牆鐘時刻(wall-clock time),若真的需要靠自己處理當時的時刻,do_gettimeofday()或許可派上用場. • 此函式並非直接告知今天是星期幾,而是將一般的秒與微秒填入一個struct timeval,原型如下 #include <linux/time.h> void do_gettimeofday(struct timeval *tv);

  7. 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) • 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 核心內建的工作佇列 • 要延遲特定工作的開始執行時間,最簡便的辦法是利用核心所維護的佇列.其中有三個可供驅動程式運用(宣告在<linux/tqueue.h>),分別是: 排程器佇列(scheduler queue) 在行程環境內運作,所以工作限制較寬鬆.在2.4版中,此佇列是以專用的kernel thread來執行的,稱為keventd,並且必須透過schedule_task()來存取. 計時器佇列(tq_timer) 由計時器時脈訊號(timer tick)觸發的佇列。由於是發生在中斷期,所以此佇列的任何工作發生在中斷期,所以必須遵守中斷模式規則。 即期佇列(tq_immediate) 被排入此佇列的工作會被盡快進行,立即佇列會在中斷期被消化完。

  19. 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

  20. 6.4.3.1 排程器佇列 • 某些用途上,排程器佇列是最容易使用的.因為它並不會在中斷時期執行,所以能力限制較寬鬆,作的事也比較多.最特別的是它可以休眠. • 在2.4版核心,實作排程器佇列的實際工作佇列被隱藏,不讓核心其他部分接觸到。且不能直接使用queue_task(),必須呼叫schedule_task(); int schedule_task(struct tq_struct *task); //傳回非零值代表task先前並未排入佇列 • /proc/jiqsched 是使用排程器佇列的範例檔 • 良好的程式只能休眠非常短的週期,因為在keventd休眠期間,排程器裡的其它工作都將無法進行. time cat /proc/jiqsched

  21. 6.4.3.2 計時器佇列 • 計時器和排程器佇列最大的差異,再於你可以直接使用計時器佇列(tq_timer),由於它是在中斷時期執行,所能做的動作限制較多. • 計時器佇列最明顯的特性,就是它保證下次計時器中斷時,佇列裡的工作一定會被執行一次,這樣可以消除系統負載所帶來的延遲效應. • 讀取jiq模組的/proc/jiqtimer檔案,可讓該模組將jiq_task排到計時器佇列裡跑。 • 讀取jiq模組的/proc/jiqtimer檔案,可讓模組將jiq_task排到計時器佇列裡跑。不同於排程器佇列之處,為這次則必須使用queue_task()來將工作排入tq_timer. head /proc/jiqtimer

  22. 6.4.3.3 即期佇列 • 即期佇列是透過bottom-half機制來執行的,表示需要額外步驟才能使用,但核心不會隨便執行你寫好的bh,除非你將它”標示”出來. • 對於td_immediate而言,若將工作排入該佇列(使用queue_task())之後,必須立刻呼叫mark_bh(),否則核心有可能在你的工作排入佇列之前,就開始消化工作佇列. • 即期佇列是Linux系統上最快速的佇列,只要一遇到中斷,就會被立刻執行. • 消化其佇列的時機有二,一是由排程器觸發,二是在行程從系統呼叫返回之後那一瞬間.

  23. 6.4.3.3 即期佇列 (Cont.) • 讀取/proc/jiqimmed • 很顯然地,即期佇列不能用來延遲工作的執行,畢竟它是”即期”佇列,這項特性使它成為interrupt handler的重要資源,因為它可讓interrupt handler用來安排來不及在中斷時期內執行完畢的工作,如:接收網路封包. • 注意,排在即期佇列裡的工作,不應該有重新排隊的行為.這樣做並沒有好處,反而還有可能鎖死系統,因為某些平台核心的即期佇列會一直跑到清空為止,而重新排隊將使得佇列沒有清空的機會. head /proc/jiqimmed

  24. 6.4.4 自製的工作佇列 • 驅動程式有權宣告自己專用的新工作佇列(一個或多個),不同於核心所提供的工作佇列,核心不會自動執行驅動程式產生的佇列,所以程式設計師必須另外安排觸發動作. • 以下巨集用來宣告自製佇列.此巨集展開後會成為一般的變數宣告,所以應該宣告在程式開頭 DECLARE_TASK_QUEUE(tq_custom); • 之後就可以使用正常函式來操作工作佇列,通常第一步是: queue_task(&custom_task, &tq_custom); • 到了應該消化累積在佇列中的工作時,使用下列函式: run_task_queue(&tq_custom); • 還需要另外再核心提供的現成工作佇列註冊一個函式,由該函式觸發你的自製佇列.

  25. 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).

  26. 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);

  27. 6.4.5 Tasklet (3/3) • /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

  28. 6.5 核心計時器 (1/3) • 它是核心內維持時序的終極資源,可用來安排一個函式(timeout handler)在未來的某特定時間才開始執行,而且只會執行一次(這個特性跟工作佇列一樣). • 計時器用法相當容易,你的工作函式只要註冊一次,核心就會在計時器到期時呼叫該函式. • 核心的計時器群是組織成一個雙向鏈結串列,可隨意增加. • 注意,期限值是一個絕對值,取jiffies的現值在加上你想要延遲的間隔. • 初始化timer_list後,就可用add_timer()將該結構安插到一個有序串列,大約每10ms會被輪詢一次.

  29. 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().

  30. /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; }

  31. 6.5 核心計時器 (3/3) • /proc/jitimer檔案使用一個計時器產生兩列資料,其訊息輸出函數是借用自先前的工作佇列範例 • 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都需載入

  32. P.200 第9行 • time cat /proc/jitsched 改為time cat /proc/jitqueue • P.207 第3行 • DECLARE_WAIT_QUEUE_HEAD(jit_wait)改為 DECLARE_WAIT_QUEUE_HEAD(jiq_wait)

More Related