轉自:http://blog.csdn.net/droidphone/article/details/8112948
版權宣告:本文為博主原創文章,未經博主允許不得轉載。
在前面章節的討論中,我們一直基於一個假設: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中定義:
- /*
- * Per cpu nohz control structure
- */
- static DEFINE_PER_CPU(struct tick_sched, tick_cpu_sched);
- struct tick_sched {
- struct hrtimer sched_timer;
- unsigned long check_clocks;
- enum tick_nohz_mode nohz_mode;
- ktime_t idle_tick;
- int inidle;
- int tick_stopped;
- unsigned long idle_jiffies;
- unsigned long idle_calls;
- unsigned long idle_sleeps;
- int idle_active;
- ktime_t idle_entrytime;
- ktime_t idle_waketime;
- ktime_t idle_exittime;
- ktime_t idle_sleeptime;
- ktime_t iowait_sleeptime;
- ktime_t sleep_length;
- unsigned long last_jiffies;
- unsigned long next_jiffies;
- ktime_t idle_expires;
- int do_timer_last;
- };
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. 低解析度下的動態時鐘
2.1 切換至動態時鐘模式
動態時鐘模式的切換過程的前半部分和切換至高精度定時器模式所經過的路徑是一樣的,請參考:Linux時間子系統之六:高精度定時器(HRTIMER)的原理和實現。這裡再簡單描述一下過程:系統工作於週期時鐘模式,定期地發出tick事件中斷,tick事件中斷觸發定時器軟中斷:TIMER_SOFTIRQ,執行軟中斷處理函式run_timer_softirq,run_timer_softirq呼叫hrtimer_run_pending函式:
- void hrtimer_run_pending(void)
- {
- if (hrtimer_hres_active())
- return;
- ......
- if (tick_check_oneshot_change(!hrtimer_is_hres_enabled()))
- hrtimer_switch_to_hres();
- }
首先,該函式通過tick_switch_to_oneshot函式把tick_device的工作模式設定為單觸發模式,並把它的中斷事件回撥函式置換為tick_nohz_handler,接著把tick_sched結構中的模式欄位設定為NOHZ_MODE_LOWRES:
- static void tick_nohz_switch_to_nohz(void)
- {
- struct tick_sched *ts = &__get_cpu_var(tick_cpu_sched);
- ktime_t next;
- if (!tick_nohz_enabled)
- return;
- local_irq_disable();
- if (tick_switch_to_oneshot(tick_nohz_handler)) {
- local_irq_enable();
- return;
- }
- ts->nohz_mode = NOHZ_MODE_LOWRES;
- hrtimer_init(&ts->sched_timer, CLOCK_MONOTONIC, HRTIMER_MODE_ABS);
- /* Get the next period */
- next = tick_init_jiffy_update();
- for (;;) {
- hrtimer_set_expires(&ts->sched_timer, next);
- if (!tick_program_event(next, 0))
- break;
- next = ktime_add(next, tick_period);
- }
- local_irq_enable();
- }
2.2 低解析度動態時鐘下的事件中斷處理函式
上一節提到,當切換至低解析度動態時鐘模式後,tick_device的事件中斷處理函式會被設定為tick_nohz_handler,總體來說,它和週期時鐘模式的事件處理函式tick_handle_periodic所完成的工作大致類似:更新時間、更新jiffies計數值、呼叫update_process_time更新程式資訊和觸發定時器軟中斷等等,最後重新程式設計tick_device,使得它在下一個正確的tick時刻再次觸發本函式:
- static void tick_nohz_handler(struct clock_event_device *dev)
- {
- ......
- dev->next_event.tv64 = KTIME_MAX;
- if (unlikely(tick_do_timer_cpu == TICK_DO_TIMER_NONE))
- tick_do_timer_cpu = cpu;
- /* Check, if the jiffies need an update */
- if (tick_do_timer_cpu == cpu)
- tick_do_update_jiffies64(now);
- ......
- if (ts->tick_stopped) {
- touch_softlockup_watchdog();
- ts->idle_jiffies++;
- }
- update_process_times(user_mode(regs));
- profile_tick(CPU_PROFILING);
- while (tick_nohz_reprogram(ts, now)) {
- now = ktime_get();
- tick_do_update_jiffies64(now);
- }
- }
- static int tick_nohz_reprogram(struct tick_sched *ts, ktime_t now)
- {
- hrtimer_forward(&ts->sched_timer, now, tick_period);
- return tick_program_event(hrtimer_get_expires(&ts->sched_timer), 0);
- }
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 :
- void irq_exit(void)
- {
- ......
- if (!in_interrupt() && local_softirq_pending())
- invoke_softirq();
- #ifdef CONFIG_NO_HZ
- /* Make sure that timer wheel updates are propagated */
- if (idle_cpu(smp_processor_id()) && !in_interrupt() && !need_resched())
- tick_nohz_irq_exit();
- #endif
- ......
- }
- void tick_nohz_irq_exit(void)
- {
- struct tick_sched *ts = &__get_cpu_var(tick_cpu_sched);
- if (!ts->inidle)
- return;
- tick_nohz_stop_sched_tick(ts);
- }
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迴圈:
- static void tick_nohz_restart(struct tick_sched *ts, ktime_t now)
- {
- hrtimer_cancel(&ts->sched_timer);
- hrtimer_set_expires(&ts->sched_timer, ts->idle_tick);
- while (1) {
- /* Forward the time to expire in the future */
- hrtimer_forward(&ts->sched_timer, now, tick_period);
- if (ts->nohz_mode == NOHZ_MODE_HIGHRES) {
- hrtimer_start_expires(&ts->sched_timer,
- HRTIMER_MODE_ABS_PINNED);
- /* Check, if the timer was already in the past */
- if (hrtimer_active(&ts->sched_timer))
- break;
- } else {
- if (!tick_program_event(
- hrtimer_get_expires(&ts->sched_timer), 0))
- break;
- }
- /* Reread time and update jiffies */
- now = ktime_get();
- tick_do_update_jiffies64(now);
- }
- }
3. 高精度模式下的動態時鐘
- NOHZ_MODE_HIGHRES
- NOHZ_MODE_LOWRES
4. 動態時鐘對中斷的影響
- void tick_check_idle(int cpu)
- {
- tick_check_oneshot_broadcast(cpu);
- tick_check_nohz(cpu);
- }
- static inline void tick_check_nohz(int cpu)
- {
- struct tick_sched *ts = &per_cpu(tick_cpu_sched, cpu);
- ktime_t now;
- if (!ts->idle_active && !ts->tick_stopped)
- return;
- now = ktime_get();
- if (ts->idle_active)
- tick_nohz_stop_idle(cpu, now);
- if (ts->tick_stopped) {
- tick_nohz_update_jiffies(now);
- tick_nohz_kick_tick(cpu, now);
- }
- }