天天看點

Linux核心設計與實作(4)---Timers and Time Management

1.核心中時間的概念

(1)核心中有大量事件是基于時間驅動的,相對時間和絕對時間這兩個概念對核心時間管理來說都至關重要。

(2)系統定時器和時鐘中斷處理程式是Linux系統核心管理機制中的中樞。核心必須在硬體的幫助下才能計算和管理時間,定時器以某種頻率自行觸發時鐘中斷。當中斷發生時,核心就通過一種特殊的中斷處理程式對其進行處理。

(3)核心可以動态建立和撤銷動态定時器--- 一種用來推遲執行程式的工具。

牆上時間:就是實際時間,對使用者空間的應用程式很重要,可以用來維護實際日期和時間。

系統運作時間:自系統啟動以來經曆的時間,對核心和使用者空間都有用。

2.HZ

系統定時器頻率(節拍率),是通過靜态預處理定義的。在

定義,對使用者空間,核心HZ幾乎完全隐藏,确切的 HZ 值隻能通過 /proc/interrupts 獲得:=(/proc/interrupts) / (/proc/uptime) ,自開機以來系統的滴答數除以運作時間.

更高的節拍率意味着時鐘産生更加頻繁,中斷處理程式也會執行更頻繁。更高的時鐘中斷解析度可提高時間事件驅動時間的解析度和準确度(時鐘中斷執行準确度是+/-1/(2HZ))。

高HZ優勢:

①核心定時器能夠以更高的頻率和更高的準确度運作;

②依賴定時值執行的系統調用,比如poll()和select(),能夠以更高的精度運作;

③對諸如資源消耗和系統運作時間等的測量會有更精細的解析度;

④提高程序搶占的準确度;

高HZ劣勢

節拍率越高,時鐘中斷頻率越高,中斷程式占用處理器時間就越多。這樣不但減少了處理器工作時間,還更頻繁的打亂了處理器高速緩存,增加功耗。

無節拍OS

當核心配置CONFIG_HZ時,系統根據這個選項動态排程時鐘中斷,這樣實質性受益是省電。

3.jiffies

(1)用來記錄自系統啟動以來産生的節拍總數,轉化為時間(jiffies/HZ)

Jiffies總是全局的unsigned long變量,在32位體系結構上,它就是32位,在HZ=100時,497天溢出。在64位體系上是64位,我們看不到溢出。通過連接配接,jiffies被jiffies_64覆寫(在64位機兩者相同,在32位機jiffies取jiffies_64低32位)。通過函數get_jiffies_64()可以讀取完整64位jiffies值。

(2)為防止溢出問題,核心提供幾個宏來比較節拍計數

此處)折疊或打開

  1. time_after(unknown,known);//當jiffies超過指定known時,傳回真
  2. time_before(unknown,known)
  3. time_after_eq(unknown,known)
  4. time_before_eq(unknown,known)
  5. unknown通常是jiffies,known = jiffies+HZ/2

(3)使用者空間和HZ

核心定義USER_HZ給應用層,當USER_HZ小于等于HZ,且兩者互為整數倍時傳回給應用的時間是 return x/(HZ/USER_HZ)

核心使用函數jiffies_64_to_clock_t()将64位jiffies從HZ轉換到USER_HZ

此處)折疊或打開

  1. unsigned long start;
  2. unsigned long total_time;
  3. start = jiffies;
  4. /* do some work ... */
  5. total_time = jiffies - start;
  6. printk(“That took %lu ticksn”, jiffies_to_clock_t(total_time));
  7. printk(“That took %lu secondsn”, total_time / HZ);

4.硬時鐘和定時器

(1)實時時鐘(RTC)用來持久存放系統時間,系統啟動時,核心讀取RTC來初始化牆上時間,該時間存放在xtime變量中。

(2)系統定時器,在X86中,采用可程式設計中斷時鐘(PIT)作為時鐘中斷源。

5.時鐘中斷處理函數

分兩部分:體系結構相關和體系結構無關。

(1)體系結構相關例程,作為系統定時器的中斷處理函數注冊到核心,主要完成以下工作:

擷取xtime_lock鎖,以便對通路jiffies_64和牆上時間xtime進行保護;

需要時應答或重新設定系統時鐘;

周期性地使用牆上時間更新實時時鐘;

調用體系無關時鐘例程:tick_periodic().

(2)中斷主要通過體系無關例程tick_periodic()完成更多工作

給jiffies_64變量加1;

更新資源消耗的統計值,比如目前程序所消耗的系統時間和使用者時間;

執行已經到期的動态定時器;

執行sheduler_tick()函數;

更新牆上時間,該事件存放在xtime中

此處)折疊或打開

  1. /*
  2.  * Periodic tick
  3.  */
  4. static void tick_periodic(int cpu)
  5. {
  6.     if (tick_do_timer_cpu == cpu) {
  7.         write_seqlock(&xtime_lock);
  8.         /* Keep track of the next tick event */
  9.         tick_next_period = ktime_add(tick_next_period, tick_period);
  10.         do_timer(1); //操作jiffies_64,更新牆上時間,更新系統平均負載
  11.         write_sequnlock(&xtime_lock);
  12.     }
  13.     update_process_times(user_mode(get_irq_regs()));//更新所消耗的各種節拍數
  14.     profile_tick(CPU_PROFILING);
  15. }

核心對程序進行時間計數,是根據中斷發生時處理器所處模式統計的,它把上個節拍全部算給了程序。這種粒度的程序統計方式是傳統unix具有的,現在還沒有更加精密的統計算法。

run_lock_timer(),标記一個軟中斷去處理所有到期定時器。

以上全部工作每1/HZ秒發生一次。

6.實際時間

目前實際時間(牆上時間)定義在檔案kernel/time/timekeeping.c中

此處)折疊或打開

  1. struct timespec xtime;
  2. timespec 定義在<linux/time.h>
  3. struct timespec {
  4.     time_t    tv_sec;    //秒,存放自1970年1月1日(utc)以來經過的時間
  5.     long    tv_nsec;    //自從上一秒開始結果的ns數
  6. };
  7. 讀寫xtime變量需要使用xtime_lock鎖,一個seqlock鎖。
  8. write_seqlock(&xtime_lock);
  9. /* update xtime ... */
  10. write_sequnlock(&xtime_lock);
  11. 讀取xtime時也要使用read_seqbegin()和read_seqretry()
  12. unsigned long seq;
  13. do {
  14.     unsigned long lost;
  15.     seq = read_seqbegin(&xtime_lock);
  16.     usec = timer->get_offset();
  17.     lost = jiffies - wall_jiffies;
  18.     if (lost)
  19.         usec += lost * (1000000 / HZ);
  20.     sec = xtime.tv_sec;usec += (xtime.tv_nsec / 1000);
  21. } while (read_seqretry(&xtime_lock, seq));

在使用者空間取得牆上時間用gettimeofday(),其在核心中調用sys_systimeofday()實作,可以用settimtofday()來設定目前時間,它需要有CAP_SYS_TIME權能。

7.定時器

指定函數将在定時器到期時自動異步執行,執行後自動撤銷。

定義在檔案

此處)折疊或打開

  1. struct timer_list {
  2.     struct list_head entry; //定時器連結清單的入口
  3.     unsigned long expires; //以jiffes為機關的計時值
  4.     void (*function)(unsigned long); //定時器處理函數
  5.     unsigned long data; //傳給處理函數的參數
  6.     struct tvec_base *base; //定時器内部值,使用者不使用
  7. };

使用者一般不操作這些結構體,而是通過核心定義好的一系列宏來處理

此處)折疊或打開

  1. 定義定時器:
  2. struct timer_list my_timer;
  3. 初始化定時器:
  4. init_timer(&my_timer);
  5. 填充定時器結構體:
  6. my_timer.expires = jiffes+HZ;//延時1s
  7. my_timer.data = NULL; //可以利用同一個處理器函數注冊多個定時器
  8. my_timer.function = my_function;//定時器處理函數
  9. 核心可以保證不會在逾時時間到期前運作處理器函數,但是有可能會延誤執行,誤差是+/-半個節拍,是以定時器不能用來做硬實時任務。
  10. 激活定時器
  11. add_timer(&my_timer);
  12. mod_timer(&my_timer);//激活或修改定時器,若調用時已激活,傳回0,否則傳回1
  13. del_timer(&my_timer);//删除定時器,定時器未被激活傳回0,否則傳回1
  14. 為了防止删除定時器時,定時器在多處理器上可能正在運作,即出現競争條件,使用
  15. del_timer_sync(&my_timer);//不能在中斷上下文使用

在中斷處理函數中,要保護好共享資料。

定時器作為軟中斷在下半部上下文中執行,在run_local_timers()中調用所有到時定時器。

核心采用分組定時器方法來管理定時器,可以減少搜尋逾時定時器所帶來的負擔。

8.延遲執行

(1)忙等待

該方法用于延遲時間是節拍的整數倍,或對精确度不高時使用

unsigned long timeout = jiffies+10;//延遲10個節拍

while(time_befor(jiffes,timeout)) ;

這是最簡單的延遲方法,但是延遲獨占CPU,很少用。改進版是:

unsigned long delay = jiffies + 5*HZ;

while (time_befor(jiffies,delay))

cond_resched();//排程一個新程式運作,該方法有效條件是系統中存在更重要的任務要運作

因為調用了排程程式,是以不能在中斷上下文使用,隻能在程序上下文使用。所有延遲方法在程序上下文都使用的很好,中斷處理都應該盡快的執行,最好不用延遲。

延遲不管在哪種情況下,都不應該在持有鎖時或禁止中斷時使用。

(2)短延遲,比節拍短,短暫又精确的延遲

此處)折疊或打開

  1. 定義在<linux/delay.h>和<asm/delay.h>
  2. void udelay(unsigned long usecs);
  3. void ndelay(unsigned long usecs);
  4. void mdelay(unsigned long usecs);

udelay是由忙循環實作的,mdelay調用udelay實作,但是會睡眠,1ms以上的延遲就不要用udelay(),防止溢出。

BogoMIPS值記錄處理器在給定時間内忙循環執行的次數,存放在loops_per_jiffy中

(3)schedule_timeout()

更理想的方案是schedule_timeout()函數,讓需要延遲睡眠執行的任務睡眠到指定延遲時間耗盡再重新執行,同樣,隻保證延遲時間盡量接近睡眠時間,時間到期後,核心喚醒被延遲任務并将其重新放回運作隊列。

set_current_state(TASK_INTERRUPTIBLE);//将任務設定為可中斷睡眠狀态

schedule_timeout(s*HZ); //睡眠s秒

①schedule_timeout()的實作

此處)折疊或打開

  1. signed long __sched schedule_timeout(signed long timeout)
  2. {
  3.     struct timer_list timer;
  4.     unsigned long expire;
  5.     switch (timeout)
  6.     {
  7.     case MAX_SCHEDULE_TIMEOUT: //用來檢查是否無限期睡眠
  8.         /*
  9.          * These two special cases are useful to be comfortable
  10.          * in the caller. Nothing more. We could take
  11.          * MAX_SCHEDULE_TIMEOUT from one of the negative value
  12.          * but I' d like to return a valid offset (>=0) to allow
  13.          * the caller to do everything it want with the retval.
  14.          */
  15.         schedule();
  16.         goto out;
  17.     default:
  18.         /*
  19.          * Another bit of PARANOID. Note that the retval will be
  20.          * 0 since no piece of kernel is supposed to do a check
  21.          * for a negative retval of schedule_timeout() (since it
  22.          * should never happens anyway). You just have the printk()
  23.          * that will tell you if something is gone wrong and where.
  24.          */
  25.         if (timeout < 0) {
  26.             printk(KERN_ERR "schedule_timeout: wrong timeout "
  27.                 "value %lxn", timeout);
  28.             dump_stack();
  29.             current->state = TASK_RUNNING;
  30.             goto out;
  31.         }
  32.     }
  33.     expire = timeout + jiffies; //設定逾時時間
  34.     setup_timer_on_stack(&timer, process_timeout, (unsigned long)current);//設定逾時函數
  35.     __mod_timer(&timer, expire);
  36.     schedule();//
  37.     del_singleshot_timer_sync(&timer);
  38.     /* Remove the timer from the object tracker */
  39.     destroy_timer_on_stack(&timer);
  40.     timeout = expire - jiffies;
  41.  out:
  42.     return timeout < 0 ? 0 : timeout;
  43. }

若任務提前喚醒(比如收到信号),那麼定時器被撤銷,傳回剩餘時間。

繼續閱讀