1. 程式人生 > >Linux時間子系統之八:動態時鐘框架(CONFIG_NO_HZ、tickless)

Linux時間子系統之八:動態時鐘框架(CONFIG_NO_HZ、tickless)

sleep file rup linux時間 load 曾經 大致 獲取 conf

在前面章節的討論中,我們一直基於一個假設:Linux中的時鐘事件都是由一個周期時鐘提供,不管系統中的clock_event_device是工作於周期觸發模式,還是工作於單觸發模式,也不管定時器系統是工作於低分辨率模式,還是高精度模式,內核都竭盡所能,用不同的方式提供周期時鐘,以產生定期的tick事件,tick事件或者用於全局的時間管理(jiffies和時間的更新),或者用於本地cpu的進程統計、時間輪定時器框架等等。周期性時鐘雖然簡單有效,但是也帶來了一些缺點,尤其在系統的功耗上,因為就算系統目前無事可做,也必須定期地發出時鐘事件,激活系統。為此,內核的開發者提出了動態時鐘這一概念,我們可以通過內核的配置項CONFIG_NO_HZ來激活特性。有時候這一特性也被叫做tickless,不過還是把它稱呼為動態時鐘比較合適,因為並不是真的沒有tick事件了,只是在系統無事所做的idle階段,我們可以通過停止周期時鐘來達到降低系統功耗的目的,只要有進程處於活動狀態,時鐘事件依然會被周期性地發出。

/*****************************************************************************************************/
聲明:本博內容均由http://blog.csdn.NET/droidphone原創,轉載請註明出處,謝謝!
/*****************************************************************************************************/

在動態時鐘正確工作之前,系統需要切換至動態時鐘模式,而要切換至動態時鐘模式,需要一些前提條件,最主要的一條就是cpu的時鐘事件設備必須要支持單觸發模式,當條件滿足時,系統切換至動態時鐘模式,接著,由idle進程決定是否可以停止周期時鐘,退出idle進程時則需要恢復周期時鐘。

1. 數據結構

在上一章的內容裏,我們曾經提到,切換到高精度模式後,高精度定時器系統需要使用一個高精度定時器來模擬傳統的周期時鐘,其中利用了tick_sched結構中的一些字段,事實上,tick_sched結構也是實現動態時鐘的一個重要的數據結構,在smp系統中,內核會為每個cpu都定義一個tick_sched結構,這通過一個percpu全局變量tick_cpu_sched來實現,它在kernel/time/tick-sched.c中定義:

[cpp] view plain copy

  1. /*
  2. * Per cpu nohz control structure
  3. */
  4. static DEFINE_PER_CPU(struct tick_sched, tick_cpu_sched);

tick_sched結構在include/linux/tick.h中定義,我們看看tick_sched結構的詳細定義:

[cpp] view plain copy

  1. struct tick_sched {
  2. struct hrtimer sched_timer;
  3. unsigned long check_clocks;
  4. enum tick_nohz_mode nohz_mode;
  5. ktime_t idle_tick;
  6. int inidle;
  7. int tick_stopped;
  8. unsigned long idle_jiffies;
  9. unsigned long idle_calls;
  10. unsigned long idle_sleeps;
  11. int idle_active;
  12. ktime_t idle_entrytime;
  13. ktime_t idle_waketime;
  14. ktime_t idle_exittime;
  15. ktime_t idle_sleeptime;
  16. ktime_t iowait_sleeptime;
  17. ktime_t sleep_length;
  18. unsigned long last_jiffies;
  19. unsigned long next_jiffies;
  20. ktime_t idle_expires;
  21. int do_timer_last;
  22. };

sched_timer 該字段用於在高精度模式下,模擬周期時鐘的一個hrtimer,請參看Linux時間子系統之六:高精度定時器(HRTIMER)的原理和實現。

check_clocks 該字段用於實現clock_event_device和clocksource的異步通知機制,幫助系統切換至高精度模式或者是動態時鐘模式。

nohz_mode 保存動態時鐘的工作模式,基於低分辨率和高精度模式下,動態時鐘的實現稍有不同,根據模式它可以是以下的值:

  • NOHZ_MODE_INACTIVE 系統動態時鐘尚未激活
  • NOHZ_MODE_LOWRES 系統工作於低分辨率模式下的動態時鐘
  • NOHZ_MODE_HIGHRES 系統工作於高精度模式下的動態時鐘

idle_tick 該字段用於保存停止周期時鐘是的內核時間,當退出idle時要恢復周期時鐘,需要使用該時間,以保持系統中時間線(jiffies)的正確性。

tick_stopped 該字段用於表明idle狀態的周期時鐘已經停止。

idle_jiffies 系統進入idle時的jiffies值,用於信息統計。

idle_calls 系統進入idle的統計次數。

idle_sleeps 系統進入idle且成功停掉周期時鐘的次數。

idle_active 表明目前系統是否處於idle狀態中。

idle_entrytime 系統進入idle的時刻。

idle_waketime idle狀態被打斷的時刻。

idle_exittime 系統退出idle的時刻。

idle_sleeptime 累計各次idle中停止周期時鐘的總時間。

sleep_length 本次idle中停止周期時鐘的時間。

last_jiffies 系統中最後一次周期時鐘的jiffies值。

next_jiffies 預計下一次周期時鐘的jiffies。

idle_expires 進入idle後,下一個最先到期的定時器時刻。

我們知道,根據系統目前的工作模式,系統提供周期時鐘(tick)的方式會有所不同,當處於低分辨率模式時,由cpu的tick_device提供周期時鐘,而當處於高精度模式時,是由一個高精度定時器來提供周期時鐘,下面我們分別討論一下在兩種模式下的動態時鐘實現方式。

2. 低分辨率下的動態時鐘

回看之前一篇文章:Linux時間子系統之四:定時器的引擎:clock_event_device中的關於tick_device一節,不管tick_device的工作模式(周期觸發或者是單次觸發),tick_device所關聯的clock_event_device的事件回調處理函數都是:tick_handle_periodic,不管當前是否處於idle狀態,他都會精確地按HZ數來提供周期性的tick事件,這不符合動態時鐘的要求,所以,要使動態時鐘發揮作用,系統首先要切換至支持動態時鐘的工作模式:NOHZ_MODE_LOWRES 。

2.1 切換至動態時鐘模式

動態時鐘模式的切換過程的前半部分和切換至高精度定時器模式所經過的路徑是一樣的,請參考:Linux時間子系統之六:高精度定時器(HRTIMER)的原理和實現。這裏再簡單描述一下過程:系統工作於周期時鐘模式,定期地發出tick事件中斷,tick事件中斷觸發定時器軟中斷:TIMER_SOFTIRQ,執行軟中斷處理函數run_timer_softirq,run_timer_softirq調用hrtimer_run_pending函數:

[cpp] view plain copy

  1. void hrtimer_run_pending(void)
  2. {
  3. if (hrtimer_hres_active())
  4. return;
  5. ......
  6. if (tick_check_oneshot_change(!hrtimer_is_hres_enabled()))
  7. hrtimer_switch_to_hres();
  8. }

tick_check_oneshot_change函數的參數決定了現在是要切換至低分辨率動態時鐘模式,還是高精度定時器模式,我們現在假設系統不支持高精度定時器模式,hrtimer_is_hres_enabled會直接返回false,對應的tick_check_oneshot_change函數的參數則是true,表明需要切換至動態時鐘模式。tick_check_oneshot_change在檢查過timekeeper和clock_event_device都具備動態時鐘的條件後,通過tick_nohz_switch_to_nohz函數切換至動態時鐘模式:

首先,該函數通過tick_switch_to_oneshot函數把tick_device的工作模式設置為單觸發模式,並把它的中斷事件回調函數置換為tick_nohz_handler,接著把tick_sched結構中的模式字段設置為NOHZ_MODE_LOWRES:

[cpp] view plain copy

  1. static void tick_nohz_switch_to_nohz(void)
  2. {
  3. struct tick_sched *ts = &__get_cpu_var(tick_cpu_sched);
  4. ktime_t next;
  5. if (!tick_nohz_enabled)
  6. return;
  7. local_irq_disable();
  8. if (tick_switch_to_oneshot(tick_nohz_handler)) {
  9. local_irq_enable();
  10. return;
  11. }
  12. ts->nohz_mode = NOHZ_MODE_LOWRES;

然後,初始化tick_sched結構中的sched_timer定時器,通過tick_init_jiffy_update獲取下一次tick事件的時間並初始化全局變量last_jiffies_update,以便後續可以正確地更新jiffies計數值,最後,把下一次tick事件的時間編程到tick_device中,到此,系統完成了到低分辨率動態時鐘的切換過程。

[cpp] view plain copy

  1. hrtimer_init(&ts->sched_timer, CLOCK_MONOTONIC, HRTIMER_MODE_ABS);
  2. /* Get the next period */
  3. next = tick_init_jiffy_update();
  4. for (;;) {
  5. hrtimer_set_expires(&ts->sched_timer, next);
  6. if (!tick_program_event(next, 0))
  7. break;
  8. next = ktime_add(next, tick_period);
  9. }
  10. local_irq_enable();
  11. }

上面的代碼中,明明現在沒有切換至高精度模式,為什麽要初始化tick_sched結構中的高精度定時器?原因並不是要使用它的定時功能,而是想重用hrtimer代碼中的hrtimer_forward函數,利用這個函數來計算下一次tick事件的時間。

2.2 低分辨率動態時鐘下的事件中斷處理函數

上一節提到,當切換至低分辨率動態時鐘模式後,tick_device的事件中斷處理函數會被設置為tick_nohz_handler,總體來說,它和周期時鐘模式的事件處理函數tick_handle_periodic所完成的工作大致類似:更新時間、更新jiffies計數值、調用update_process_time更新進程信息和觸發定時器軟中斷等等,最後重新編程tick_device,使得它在下一個正確的tick時刻再次觸發本函數:

[cpp] view plain copy

  1. static void tick_nohz_handler(struct clock_event_device *dev)
  2. {
  3. ......
  4. dev->next_event.tv64 = KTIME_MAX;
  5. if (unlikely(tick_do_timer_cpu == TICK_DO_TIMER_NONE))
  6. tick_do_timer_cpu = cpu;
  7. /* Check, if the jiffies need an update */
  8. if (tick_do_timer_cpu == cpu)
  9. tick_do_update_jiffies64(now);
  10. ......
  11. if (ts->tick_stopped) {
  12. touch_softlockup_watchdog();
  13. ts->idle_jiffies++;
  14. }
  15. update_process_times(user_mode(regs));
  16. profile_tick(CPU_PROFILING);
  17. while (tick_nohz_reprogram(ts, now)) {
  18. now = ktime_get();
  19. tick_do_update_jiffies64(now);
  20. }
  21. }

因為現在工作於動態時鐘模式,所以,tick時鐘可能在idle進程中被停掉不止一個tick周期,所以當該函數被再次觸發時,離上一次觸發的時間可能已經不止一個tick周期,tick_nohz_reprogram對tick_device進行編程時必須正確地處理這一情況,它利用了前面所說的hrtimer_forward函數來實現這一特性:

[cpp] view plain copy

  1. static int tick_nohz_reprogram(struct tick_sched *ts, ktime_t now)
  2. {
  3. hrtimer_forward(&ts->sched_timer, now, tick_period);
  4. return tick_program_event(hrtimer_get_expires(&ts->sched_timer), 0);
  5. }
2.3 動態時鐘:停止周期tick時鐘事件

開啟動態時鐘模式後,周期時鐘的開啟和關閉由idle進程控制,idle進程內最終是一個循環,循環的一開始通過tick_nohz_idle_enter檢測是否允許關閉周期時鐘若幹時間,然後進入低功耗的idle模式,當有中斷事件使得cpu退出低功耗idle模式後,判斷是否有新的進程被激活從而需要重新調度,如果需要則通過tick_nohz_idle_exit重新啟用周期時鐘,然後重新進行進程調度,等待下一次idle的發生,我們可以用下圖來表示:

技術分享

圖2.3.1 idle進程中的動態時鐘處理

停止周期時鐘的時機在tick_nohz_idle_enter函數中,它把主要的工作交由tick_nohz_stop_sched_tick函數來完成。內核也不是每次進入tick_nohz_stop_sched_tick都會停止周期時鐘,那麽什麽時候才會停止?我們想一想,這時候既然idle進程在運行,說明系統中的其他進程都在等待某種事件,系統處於無事所做的狀態,唯一要處理的就是中斷,除了定時器中斷,其它的中斷我們無法預測它會何時發生,但是我們可以知道最先一個到期的定時器的到期時間,也就是說,在該時間到期前,產生周期時鐘是沒有必要的,我們可以據此推算出周期時鐘可以停止的tick數,然後重新對tick_device進行編程,使得在最早一個定時器到期前都不會產生周期時鐘,實際上,tick_nohz_stop_sched_tick還做了一些限制:當下一個定時器的到期時間與當前jiffies值只相差1時,不會停止周期時鐘,當定時器的到期時間與當前的jiffies值相差的時間大於timekeeper允許的最大idle時間時,則下一個tick時刻被設置timekeeper允許的最大idle時間,這主要是為了防止太長時間不去更新timekeeper中的系統時間,有可能導致clocksource的溢出問題。tick_nohz_stop_sched_tick函數體看起來很長,實現的也就是上述的邏輯,所以這裏就不貼它的代碼了,有興趣的讀者可以自行閱讀內核的代碼:kernel/time/tick-sched.c。

看了動態時鐘的停止過程和tick_nohz_handler的實現方式,其實還有一個情況沒有處理:當系統進入idle進程後,周期時鐘被停止若幹個tick周期,當這若幹個tick周期到期後,tick事件必然會產生,tick_nohz_handler被觸發調用,然後最先到期的定時器被處理。但是在tick_nohz_handler的最後,tick_device一定會被編程為緊跟著的下一個tick周期的時刻被觸發,如果剛才的定時器處理後,並沒有激活新的進程,我們的期望是周期時鐘可以用下一個新的定時器重新計算可以停止的時間,而不是下一個tick時刻,但是tick_nohz_handler卻僅僅簡單地把tick_device的到期時間設為下一個周期的tick時刻,這導致了周期時鐘被恢復,顯然這不是我們想要的。為了處理這種情況,內核使用了一點小伎倆,我們知道定時器是在軟中斷中執行的,所以內核在irq_exit中的軟件中斷處理完後,加入了一小段代碼,kernel/softirq.c :

[cpp] view plain copy

  1. void irq_exit(void)
  2. {
  3. ......
  4. if (!in_interrupt() && local_softirq_pending())
  5. invoke_softirq();
  6. #ifdef CONFIG_NO_HZ
  7. /* Make sure that timer wheel updates are propagated */
  8. if (idle_cpu(smp_processor_id()) && !in_interrupt() && !need_resched())
  9. tick_nohz_irq_exit();
  10. #endif
  11. ......
  12. }

關鍵的調用是tick_nohz_irq_exit:

[cpp] view plain copy

  1. void tick_nohz_irq_exit(void)
  2. {
  3. struct tick_sched *ts = &__get_cpu_var(tick_cpu_sched);
  4. if (!ts->inidle)
  5. return;
  6. tick_nohz_stop_sched_tick(ts);
  7. }

tick_nohz_irq_exit再次調用了tick_nohz_stop_sched_tick函數,使得系統有機會再次停止周期時鐘若幹個tick周期。

2.3 動態時鐘:重新開啟周期tick時鐘事件

回到圖2.3.1,當在idle進程中停止周期時鐘後,在某一時刻,有新的進程被激活,在重新調度前,tick_nohz_idle_exit會被調用,該函數負責恢復被停止的周期時鐘。tick_nohz_idle_exit最終會調用tick_nohz_restart函數,由tick_nohz_restart函數最後完成恢復周期時鐘的工作。函數並不復雜:先是把上一次停止周期時鐘的時刻設置到tick_sched結構的sched_timer定時器中,然後在通過hrtimer_forward函數把該定時器的到期時刻設置為當前時間的下一個tick時刻,對於高精度模式,啟動該定時器即可,對於低分辨率模式,使用該時間對tick_device重新編程,最後通過tick_do_update_jiffies64更新jiffies數值,為了防止此時正在一個tick時刻的邊界,可能當前時刻正好剛剛越過了該到期時間,函數使用了一個while循環:

[cpp] view plain copy

  1. static void tick_nohz_restart(struct tick_sched *ts, ktime_t now)
  2. {
  3. hrtimer_cancel(&ts->sched_timer);
  4. hrtimer_set_expires(&ts->sched_timer, ts->idle_tick);
  5. while (1) {
  6. /* Forward the time to expire in the future */
  7. hrtimer_forward(&ts->sched_timer, now, tick_period);
  8. if (ts->nohz_mode == NOHZ_MODE_HIGHRES) {
  9. hrtimer_start_expires(&ts->sched_timer,
  10. HRTIMER_MODE_ABS_PINNED);
  11. /* Check, if the timer was already in the past */
  12. if (hrtimer_active(&ts->sched_timer))
  13. break;
  14. } else {
  15. if (!tick_program_event(
  16. hrtimer_get_expires(&ts->sched_timer), 0))
  17. break;
  18. }
  19. /* Reread time and update jiffies */
  20. now = ktime_get();
  21. tick_do_update_jiffies64(now);
  22. }
  23. }

3. 高精度模式下的動態時鐘

高精度模式和低分辨率模式的主要區別是在切換過程中,怎樣切換到高精度模式,我已經在上一篇文章中做了說明,切換到高精度模式後,動態時鐘的開啟和關閉和低分辨率模式下沒有太大的區別,也是通過tick_nohz_stop_sched_tick和tick_nohz_restart來控制,在這兩個函數中,分別判斷了當前的兩種模式:

  • NOHZ_MODE_HIGHRES
  • NOHZ_MODE_LOWRES
如果是NOHZ_MODE_HIGHRES則對tick_sched結構的sched_timer定時器進行設置,如果是NOHZ_MODE_LOWRES,則直接對tick_device進行操作。

4. 動態時鐘對中斷的影響

在進入和退出中斷時,因為動態時鐘的關系,中斷系統需要作出一些配合。先說中斷發生於周期時鐘停止期間,如果不做任何處理,中斷服務程序中如果要訪問jiffies計數值,可能得到一個滯後的jiffies值,因為正常狀態下,jiffies值會在恢復周期時鐘時正確地更新,所以,為了防止這種情況發生,在進入中斷的irq_enter期間,tick_check_idle會被調用:

[cpp] view plain copy

  1. void tick_check_idle(int cpu)
  2. {
  3. tick_check_oneshot_broadcast(cpu);
  4. tick_check_nohz(cpu);
  5. }

tick_check_nohz函數的最重要的作用就是更新jiffies計數值:

[cpp] view plain copy

  1. static inline void tick_check_nohz(int cpu)
  2. {
  3. struct tick_sched *ts = &per_cpu(tick_cpu_sched, cpu);
  4. ktime_t now;
  5. if (!ts->idle_active && !ts->tick_stopped)
  6. return;
  7. now = ktime_get();
  8. if (ts->idle_active)
  9. tick_nohz_stop_idle(cpu, now);
  10. if (ts->tick_stopped) {
  11. tick_nohz_update_jiffies(now);
  12. tick_nohz_kick_tick(cpu, now);
  13. }
  14. }

另外一種情況是在退出定時器中斷時,需要重新評估周期時鐘的運行狀況,這一點已經在2.3節中做了說明,這裏就不在贅述了。

Linux時間子系統之八:動態時鐘框架(CONFIG_NO_HZ、tickless)