Linux cpuidle framework(4)_menu governor

1. 前言

本文以menu governor爲例,進一步理解cpuidle framework中governor的概念,並學習governor的實現方法。

在當前的kernel中,有2個governor,分別爲ladder和menu(蝸蝸試圖理解和查找,爲什麼會叫這兩個名字,暫時還沒有答案)。ladder在periodic timer tick system中使用,menu在tickless system中使用。

現在主流的系統,出於電源管理的考量,大多都是tickless system。另外,menu governor會利用pm qos framework(蝸蝸會在後續的文章中分析),在選擇策略中加入延遲容忍度(Latency tolerance)的考量。因此本文選取menu governor作爲分析對象,至於ladder,就不再分析了。

注:有關periodic timer tick和tickless的知識,可參考本站時間子系統的系列文章。

2. 背後的思考

本節的內容,主要來源於drivers/cpuidle/governors/menu.c中的註釋。

governor的主要職責,是根據系統的運行情況,選擇一個合適idle state(在kernel的標準術語中,也稱作C state)。具體的算法,需要基於下面兩點考慮:

1)切換的代價

進入C state的目的,是節省功耗,但CPU在C state和normal state之間切換,是要付出功耗上面的代價的。這最終會體現在idle state的target_residency字段上。

idle driver在註冊idle state時,要非常明確state切換的代價,基於該代價,CPU必須在idle state中停留超過一定的時間(target_residency)纔是划算的。

因此governor在選擇C state時,需要預測出CPU將要在C state中的停留時間,並和備選idle state的target_residency字段比較,選取滿足“停留時間 > target_residency”的state。

2)系統的延遲容忍程度

備選的的C state中,功耗和退出延遲是一對不可調和的矛盾,電源管理的目標,是在保證延遲在系統可接受的範圍內的情況下,儘可能的節省功耗。

idle driver在註冊idle state時,會提供兩個信息:CPU在某個state下的功耗(power_usage)和退出該state的延遲(exit_latency)。那麼如果知道系統當前所能容忍的延遲(簡稱latency_req),就可以在所有exit_latency小於latency_req的state中,選取功耗最小的那個。

因此,governor算法就轉換爲獲取系統當前的latency_req,而這正是pm qos的特長。

基於上面的考量,menu governor的主要任務就轉化爲兩個:1. 根據系統的運行情況,預測CPU將在C state中停留的時間(簡稱predicted_us);2. 藉助pm qos framework,獲取系統當前的延遲容忍度(簡稱latency_req)。

任務1,menu governor從如下幾個方面去達成:

前面講過,menu governor用於tickless system,簡化處理,menu將“距離下一個tick來臨的時間(由next timer event測量,簡稱next_timer_us)”作爲基礎的predicted_us。

當然,這個基礎的predicted_us是不準確的,因爲在這段時間內,隨時都可能產生除next timer event之外的其它wakeup event。爲了使預測更準確,有必要加入一個校正因子(correction factor),該校正因子基於過去的實際predicted_us和next_timer_us之間的比率,例如,如果wakeup event都是在預測的next timer event時間的一半時產生,則factor爲0.5。另外,爲了更精確,menu使用動態平均的factor。

另外,對不同範圍的next_timer_us,correction factor的影響程度是不一樣的。例如期望50ms和500ms的next timer event時,都是在10ms時產生了wakeup event,顯然對500ms的影響比較大。如果計算平均值時將它們混在一起,就會對預測的準確性產生影響,所以計算correction factor的數據時,需要區分不同級別的next_timer_us。同時,系統是否存在io wait,對factor的敏感度也不同。基於這些考慮,menu使用了一組factor(12個),分別用於不同next_timer_us、不同io wait的場景下的的校正。

最後,在有些場合下,next_timer_us的預測是完全不正確的,如存在固定週期的中斷時(音頻等)。這時menu採用另一種不同的預測方式:統計過去8次停留時間的標準差(stand deviation),如果小於一定的門限值,則使用這8個停留時間的平均值,作爲預測值。

任務2,延遲容忍度(latency_req)的估算,menu綜合考慮了兩種因素,如下:

1)由pm qos獲得的,系統期望的,CPU和DMA的延遲需求。這是一個硬性指標。

2)基於這樣一個經驗法則:越忙的系統,對系統延遲的要求越高,結合任務1中預測到的停留時間(predicted_us),以及當前系統的CPU平均負荷和iowaiters的個數(get_iowait_load函數獲得),算出另一個延遲容忍度,計算公式(這是一個經驗公式)爲: 
                predicted_us / (1 + 2 * loadavg +10 * iowaiters) 
這個公式反映的是退出延遲和預期停留時間之間的比例,loadavg和iowaiters越大,對退出延遲的要求就越高奧。

最後,latency_req的值取上面兩個估值的最小值。

3. 代碼分析

理解menu governor背後的思考之後,再去看代碼,就比較簡單了。

3.1 初始化

首先,在init代碼中,調用cpuidle_register_governor,註冊menu_governor,如下:

   1: static struct cpuidle_governor menu_governor = {
   2:         .name =         "menu",
   3:         .rating =       20,
   4:         .enable =       menu_enable_device,
   5:         .select =       menu_select,
   6:         .reflect =      menu_reflect,
   7:         .owner =        THIS_MODULE,
   8: };
   9:  
  10: /**
  11:  * init_menu - initializes the governor
  12:  */
  13: static int __init init_menu(void)
  14: {
  15:         return cpuidle_register_governor(&menu_governor);
  16: }
  17:  
  18: postcore_initcall(init_menu);

由menu_governor變量可知,該governor的名字爲“menu”,rating爲20,共提供了enable、select、reflect三個API。

3.2 enable API

enable API負責governor運行前的準備動作,由menu_enable_device實現:

   1: static int menu_enable_device(struct cpuidle_driver *drv,
   2:                                 struct cpuidle_device *dev)
   3: {
   4:         struct menu_device *data = &per_cpu(menu_devices, dev->cpu);
   5:         int i;
   6:  
   7:         memset(data, 0, sizeof(struct menu_device));
   8:  
   9:         /*
  10:          * if the correction factor is 0 (eg first time init or cpu hotplug
  11:          * etc), we actually want to start out with a unity factor.
  12:          */
  13:         for(i = 0; i < BUCKETS; i++)
  14:                 data->correction_factor[i] = RESOLUTION * DECAY;
  15:  
  16:         return 0;
  17: }

由代碼可知,主要任務是初始化在私有數據結構(struct menu_device)中保存的correction_factor。struct menu_device的定義如下:

   1: struct menu_device {
   2:         int             last_state_idx;
   3:         int             needs_update;
   4:  
   5:         unsigned int    next_timer_us;
   6:         unsigned int    predicted_us;
   7:         unsigned int    bucket;
   8:         unsigned int    correction_factor[BUCKETS];
   9:         unsigned int    intervals[INTERVALS];
  10:         int             interval_ptr;
  11: };

last_state_idx,記錄了上一次進入的C state;

needs_update,每次從C state返回時,kernel(kernel\sched\idle.c)會調用governor的reflect接口,以便有機會讓governor考慮這一次state切換的結果(如更新統計信息)。對menu而言,它的reflect接口會設置needs_update標誌,並在下一次select時,更新狀態,具體行爲可參考後面的描述;

next_timer_us、predicted_us,可參考第2章中的有關說明;

correction_factor,保存校正因子的數組,因子的個數爲BUCKETS(當前代碼爲12);

bucket,指明select state時所使用的因子(當前的校正因子);

intervals、interval_ptr,可參考第2章中的描述,用於計算停留時間的標準差,當前代碼使用了8個停留時間(INTERVALS)。

3.2 select接口

governor的核心API,根據系統的運行情況,選擇一個合適的C state。由menu_select接口實現,邏輯如下:

   1: /**
   2:  * menu_select - selects the next idle state to enter
   3:  * @drv: cpuidle driver containing state data
   4:  * @dev: the CPU
   5:  */
   6: static int menu_select(struct cpuidle_driver *drv, struct cpuidle_device *dev)
   7: {
   8:     struct menu_device *data = this_cpu_ptr(&menu_devices);
   9:     int latency_req = pm_qos_request(PM_QOS_CPU_DMA_LATENCY);
  10:     int i;
  11:     unsigned int interactivity_req;
  12:     unsigned long nr_iowaiters, cpu_load;
  13:  
  14:     if (data->needs_update) {
  15:         menu_update(drv, dev);
  16:         data->needs_update = 0;
  17:     }
  18:  
  19:     data->last_state_idx = CPUIDLE_DRIVER_STATE_START - 1;
  20:  
  21:     /* Special case when user has set very strict latency requirement */
  22:     if (unlikely(latency_req == 0))
  23:         return 0;
  24:  
  25:     /* determine the expected residency time, round up */
  26:     data->next_timer_us = ktime_to_us(tick_nohz_get_sleep_length());
  27:  
  28:     get_iowait_load(&nr_iowaiters, &cpu_load);
  29:     data->bucket = which_bucket(data->next_timer_us, nr_iowaiters);
  30:  
  31:     /*
  32:      * Force the result of multiplication to be 64 bits even if both
  33:      * operands are 32 bits.
  34:      * Make sure to round up for half microseconds.
  35:      */
  36:     data->predicted_us = div_round64((uint64_t)data->next_timer_us *
  37:                      data->correction_factor[data->bucket],
  38:                      RESOLUTION * DECAY);
  39:  
  40:     get_typical_interval(data);
  41:  
  42:     /*
  43:      * Performance multiplier defines a minimum predicted idle
  44:      * duration / latency ratio. Adjust the latency limit if
  45:      * necessary.
  46:      */
  47:     interactivity_req = data->predicted_us / performance_multiplier(nr_iowaiters, cpu_load);
  48:     if (latency_req > interactivity_req)
  49:         latency_req = interactivity_req;
  50:  
  51:     /*
  52:      * We want to default to C1 (hlt), not to busy polling
  53:      * unless the timer is happening really really soon.
  54:      */
  55:     if (data->next_timer_us > 5 &&
  56:         !drv->states[CPUIDLE_DRIVER_STATE_START].disabled &&
  57:         dev->states_usage[CPUIDLE_DRIVER_STATE_START].disable == 0)
  58:         data->last_state_idx = CPUIDLE_DRIVER_STATE_START;
  59:  
  60:     /*
  61:      * Find the idle state with the lowest power while satisfying
  62:      * our constraints.
  63:      */
  64:     for (i = CPUIDLE_DRIVER_STATE_START; i < drv->state_count; i++) {
  65:         struct cpuidle_state *s = &drv->states[i];
  66:         struct cpuidle_state_usage *su = &dev->states_usage[i];
  67:  
  68:         if (s->disabled || su->disable)
  69:             continue;
  70:         if (s->target_residency > data->predicted_us)
  71:             continue;
  72:         if (s->exit_latency > latency_req)
  73:             continue;
  74:  
  75:         data->last_state_idx = i;
  76:     }
  77:  
  78:     return data->last_state_idx;
  79: }

8行,取出per cpu的struct menu_device指針;

9行,調用pm_qos_request接口,獲取系統CPU和DMA所能容忍的延遲。因爲cpuidle狀態下,運行任何的中斷事件喚醒,因此這裏只考慮了CPU和DMA;

14~17行,根據needs_update標誌,調用menu_update,更新統計信息,具體可參考代碼;

19行,last_state_idx會在menu_reflect中設置,並在menu_update中使用,此時已經沒有用處了,初始化爲無效值;

22~23行,如果pm qos要求的latency爲0,則當前系統是一個比較苛刻的狀態,不能進入idle狀態,直接返回零。由此可以看出,software可以通過pm qos,控制系統是否可以進入idle狀態,後續分析pm qos時,會再說明;

26~29行,調用timer子系統的接口,獲取next_timer_us,調用sched提供de接口,獲取iowaiter的個數以及CPU load信息,並利用next_timer_us和iowaiters信息,計算出需要使用哪一類校正因子。計算邏輯比較簡單,詳見代碼;

36~39行,將next_timer_us乘以校正因子,得到predicted_us。計算時考慮了溢出、精度等情況;

40行,調用get_typical_interval接口,檢查是否存在固定週期的情況,檢查的邏輯就是計算8次停留時間的標準差,如果存在,則利用平均值更新predicted_us;

42~48,根據predicted_us和系統負荷情況(cpu load、iowaiters),估算另一個延遲容忍值,並和latency_req,取最小值;

51~78行,根據上面的信息,查找cpuidle device的所有state,選出一個符合條件的state,並返回該state在cpuidle state數組中的index。

3.3 reflect接口

menu的reflect接口比較簡單,更新data->last_state_idx後,置位data->needs_update標誌。可以多思考一下:爲什麼不直接在reflect中更新狀態,而是到下一次select時再更新?這個問題留給讀者吧。

發佈了35 篇原創文章 · 獲贊 46 · 訪問量 16萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章