1 / 32

Chapter 6

Chapter 6. 時序. 介紹 由簡至繁. 瞭解核心的時序 (timing) 認識目前時間 (current time) 將作業的時間點延遲到一定時間之後才開始 將非同步函式 (asynchronous function) 安排到一定時間之後才開始作用. 6.1 核心的計時間隔. 中斷 —CPU 暫停目前工作 , 然後執行 ISR 來處理中斷 .(CH9) 計時器中斷 , 固定間隔觸發的中斷事件 , 核心依據 HZ( 定義在 <linux/param.h>) 的值來設定間隔長度 , 硬體平台不同 , 值也不同 .

mona-booth
Download Presentation

Chapter 6

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來處理中斷.(CH9) • 計時器中斷,固定間隔觸發的中斷事件,核心依據HZ(定義在<linux/param.h>)的值來設定間隔長度,硬體平台不同,值也不同. • 每次發生計時器中斷,jiffies變數的值就會被遞增一次,宣告在<linux/sched.h>,型別為unsigned long volatile,核心確保溢位之後還能正確運作,不必擔心,只須注意.

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

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

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

  8. 6.3 延遲執行 • 延遲—通常是為了讓硬體有足夠充裕的時間完成某些工作 • 需要考慮的重點之一,是延遲時間是否超過一個時脈單位 • 較長的延遲,可以利用系統時鐘來計時,較短的延遲,則通常以軟體迴圈來應付

  9. 6.3.1 長期延遲 • 最簡單也是最蠢的做法,稱為忙著等待(busy waiting): unsigned long j = jiffies + jit_delay * HZ; while (jiffies < j) /* 發呆… */ ; • 因為jiffies是volatile變數,使得C編譯器會落實每次的讀取動作(不使用快取技術). • 在延遲期間,處理器是被鎖死的,因為這是核心裡的回圈,排程器不會岔斷進入核心行程. • 若中斷失效時進入迴圈,jiffies不會更新,迴圈無法終止,只能使用reset按鈕.

  10. 6.3.1 長期延遲 • 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 長期延遲 • 排程迴圈提供一個觀測驅動程式工作程序的速成工具.(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 長期延遲 • 範例 /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) • 不倚賴中斷機制的前提下,將某些工作安排到一段時間之後才開始執行. • 有三種介面,分別是工作佇列、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 工作佇列的本質 • 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); 用來消化指定佇列中的累積工作,不必直接呼叫,除非宣告並維護自己的工作佇列

  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) 由計時器時脈訊號觸發的佇列.必須遵守中斷模式規則 即期佇列(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先前並未排入佇列 • 但只能休眠非常短的週期,因為在keventd休眠期間,排程器裡的其它工作都將無法進行. time cat /proc/jiqsched

  21. 6.4.3.2 計時器佇列 • 計時器和排程器佇列最大的差異,再於你可以直接使用計時器佇列(tq_timer),由於它是在中斷時期執行,所能作的動作限制較多. • 最明顯的特性,就是它保證下次計時器中斷時,佇列裡的工作一定會被執行一次. • 這次則必須使用queue_task()來將工作排入tq_timer. head /proc/jiqtimer

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

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

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

  25. 6.4.5 Tasklet • 在2.4版正式發行之前,開發人員增加了一種新機制來執行延期的核心工作.此機制稱為tasklets,並且成為bottom-half工作的最佳選擇.事實上,bottom-half本身現在就是作成tasklet的形式. • 它如同工作佇列,不管被排程幾次,tasklet也是只跑一次而已. • 在SMP系統上,tasklet可以與其他不同的tasklet並行運作,哪個CPU安排的tasklet,就由哪一個CPU負責執行,簡化了系統的快取機制,同也獲得更高的效能. • 每一個tasklet都有一個專屬的函式,當tasklet到了應該被執行的時間,該函式就會被呼叫.此函式只能有一個引數,不過將long引數鑄型成指標型別,在任何支援linux的平台上都是可行而安全的,甚至是記憶管理常用的技巧(ch13).

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

  27. 6.4.5 Tasklet • 注意到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

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

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

  30. 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 核心計時器 • timeout handler是在中斷時期,更令人好奇的是,不管當時系統是否忙碌,每次讀取/proc/jitimer所等待的時間間隔必定剛好一秒. • 因為它的工作與當時的行程環境無關,所以,即使CPU被鎖死在忙碌迴圈裡,計時器佇列與核心計時器人然能順利運行. • 因此它研然成為另一個相競狀況的來源,即使在單處理器上.因此必須以連動型別或空轉鎖(spinlocks)來加以保護,避免被同時存取. • 刪除計時器是另一項可能引發相競的動作,解決辦法是必須設立一個停止計時器的旗標,然後呼叫_sync版本,timeout handler必須檢查此旗標是否設定,來決定要不要重新排隊. • 修改計時器也可能造成相競狀況(del add),用mod_timer()來解決,一次搞定. head /proc/jitimer //jit,jiq都需載入

More Related