1. 程式人生 > >Linux NO_HZ_FULL NO_HZ 框架實現分析

Linux NO_HZ_FULL NO_HZ 框架實現分析

解決一個問題重要的是瞭解問題的領域知識和相關背景,本文的目的是介紹linux核心的NO_HZ_FULL相關領域知識,以便涉及相關問題時候能更容易上手。
簡單的說linux NO_HZ_FULL的作用是消除Linux核心不必要的週期時鐘,提高系統的效能或者節省能耗。NO_HZ需要核心高精度時鐘框架的支援。NO_HZ有幾種選擇,NO_HZ_IDLE是在CPU IDLE的時候關掉該CPU的週期tick。 NO_HZ_FULL是在普通任務不多於一個,或者只有實時FIFO任務等情況下關掉該CPU的週期tick。

Linux tick的作用
Linux核心每隔固定週期會發出timer interrupt,HZ是用來定義每一秒有幾次timer interrupts。HZ為1000,代表每秒有1000次timer interrupts。HZ的倒數稱為一個節拍,也就是一個Tick。
核心會利用這個時鐘中斷進行管理系統,在每個tick中處理這些管理事務,具體內容包括任務排程、定時器、時間管理等。在原來的Linux設計中這個tick是系統管理的基礎,不可或缺。

NO_HZ提出的背景
NO_HZ_FULL是linux核心的一項功能,它提出的背景驅動如下:
提高HPC效能,CPU上只執行一個任務時候應該最大化的利用CPU。HZ=1000的週期tick會帶來大約1%的開銷。NO_HZ_FULL能在這種情況能提高系統0.5%-1.0%的效能。
實時任務希望減少延遲,一些關鍵任務希望抖動儘可能小。核心引起的抖動源就是週期tick。
隨著cpu和核數的增加,一個cpu上執行一個任務變得越來越普遍,這個特性可以在桌面和移動領域發揮作用。

NO_HZ增加的開銷
NO_HZ帶來的開銷主要在CPU進出NO_HZ模式時,定時器需要重設定,會導致處理時間變得稍微長些。
使用者態和核心態切換的時候,會有一點開銷,主要用於通知類似RCU的核心子系統模式切換了。
POSIX CPU timers會妨礙CPU進入NO_HZ_FULL,當實時任務需要採用其他方案代替使用POSIX CPU timers。
如果pending的perf events 超過硬體的限制,通常採用迴圈的方法來處理,不過NO_HZ_FULL會阻礙這種情況,目前只能通過限制pending perf events的數目來解決。

NO_HZ設計框架
既然NO_HZ是停掉Linux核心的週期時鐘,那麼首先需要解決的問題是,什麼情況下可以停掉,如何停掉。這個問題解決了,當然就是是什麼時候恢復,如何恢復的問題。只要搞清楚了上面2個問題,整個NO_HZ的設計框架就清楚了。下面以這些問題為主線簡單說明一下:

什麼時候可以停掉:
核心已經封裝了can_stop_full_tick函式來解決這個問題,這個函式在tick-sched.c檔案中。
static bool can_stop_full_tick(void)
{
WARN_ON_ONCE(!irqs_disabled());

if (!sched_can_stop_tick()) {
    trace_tick_stop(0, "more than 1 task in runqueue\n");
    return false;
}

if (!posix_cpu_timers_can_stop_tick(current)) {
    trace_tick_stop(0, "posix timers running\n");
    return false;
}

if (!perf_event_can_stop_tick()) {
    trace_tick_stop(0, "perf events running\n");
    return false;
}

/* sched_clock_tick() needs us? */

ifdef CONFIG_HAVE_UNSTABLE_SCHED_CLOCK
/*
* TODO: kick full dynticks CPUs when
* sched_clock_stable is set.
*/
if (!sched_clock_stable()) {
trace_tick_stop(0, “unstable sched clock\n”);
/*
* Don’t allow the user to think they can get
* full NO_HZ with this machine.
*/
WARN_ONCE(tick_nohz_full_running,
“NO_HZ FULL will not work with unstable sched clock”);
return false;
}
endif

return true;

}

簡單說下這個函式,要停掉週期tick,首先需要滿足即核心任務排程器這個時候必須不依賴於tick,什麼時候排程器不依賴於tick呢,執行實時FIFO任務的時候,只有一個RR實時任務的時候,或者可以執行的普通任務不超過一個。
其次需要滿足這時候當前任務沒有posix 定時器,主要看使用者態程式是否使用了。
最後這時候需要沒有perf事件,這個通常沒有做效能分析的時候都滿足。
另外,如果系統時鐘是不穩定的,那麼也不滿足條件。穩定的系統時鐘可以通過啟動引數指定,也可以在系統執行時動態指定。

如何停掉週期tick進入NO_HZ:
核心在每個中斷處理完成退出的時候都會檢查是否應該停掉週期tick進入NO_HZ。注意,這裡不僅是檢查是否進入NO_HZ的地方,也是檢查是否退出NO_HZ的地方!
具體呼叫鏈如下:
do_IRQ
exiting_irq
irq_exit
tick_irq_exit
tick_nohz_irq_exit
tick_nohz_full_update_tick
tick_nohz_stop_sched_tick 或者tick_nohz_restart_sched_tick
在檢查的時候,發現符合進入NO_HZ的條件,就呼叫tick_nohz_stop_sched_tick停掉週期tick進入NO_HZ。
tick_nohz_stop_sched_tick這個函式這裡就不進行詳細分析了,簡單說下一個關鍵的地方,tick_nohz_stop_sched_tick會判斷週期tick停止多久,注意下面的判斷
static ktime_t tick_nohz_stop_sched_tick(struct tick_sched *ts,
ktime_t now, int cpu)
{

……
if (rcu_needs_cpu(basemono, &next_rcu) ||
arch_needs_cpu() || irq_work_needs_cpu()) {
next_tick = basemono + TICK_NSEC;
} else {

}

……
}
如果rcu子系統在當前CPU存在回撥,或者在當前CPU有irq_work連結串列的工作要做(這些會在IRQ中被執行),那麼只停TICK_NSEC這麼一小段時間。因此如果要想NO_HZ效果好,那麼必須對這些需要進入NO_HZ的CPU設定合理的RCU回撥配置(沒有rcu回撥 ,或者cpu配置了CONFIG_RCU_NOCB_CPU_ALL),並且不要在這些CPU上新增irq_work。

如何退出NO_HZ:
什麼時候判斷一個CPU應該退出NO_HZ的好時機?
答案是:當有新任務新增到NO_HZ的CPU上,或者有新的定時器新增到NO_HZ的CPU上。wake_up_nohz_cpu會呼叫ick_nohz_full_kick_cpu讓指定CPU退出NO_HZ。

timer加入的no_hz active的cpu上時候
add_timer_on
internal_add_timer
wake_up_nohz_cpu

在no_hz cpu上啟動一個timer的時候
hrtimer_start_range_ns
wake_up_nohz_cpu

往執行佇列中增加的時候
add_nr_running
tick_nohz_full_kick_cpu

下面是tick_nohz_full_kick_cpu的呼叫鏈:
wake_up_nohz_cpu
tick_nohz_full_kick_cpu
irq_work_queue_on(&per_cpu(nohz_full_kick_work, cpu), cpu); —會向指定cpu 傳送IPI
arch_send_call_function_single_ipi(cpu);
tick_nohz_irq_exit –收到ipi之後
tick_nohz_full_update_tick
tick_nohz_restart_sched_tick —-這時候restart
上面呼叫鏈很清楚了,退出的時候只需要向對應的CPU傳送一個IPI,這個中斷什麼也不做,只是觸發中斷。這個空中斷處理函式退出後,會走irq_exit,然後就通過irq退出時候的判斷邏輯執行退出NO_HZ的程式碼,當然在傳送IPI之前,需要設定好退出NO_HZ的標誌。

小結:
NO_HZ的進入和退出都是在中斷退出的時候進行。進入NO_HZ需要滿足特定條件。在有新程序加入執行佇列或者新定時器的時候,系統檢測是否需要退出NO_HZ,如果需要傳送IPI到指定CPU,然後該CPU在IPI中斷退出的時候退出NO_HZ。