[scheduler] task運行時間,util和frequency三者之前的關係

在WALT裏面,一個task的util大小,涉及到下面幾個參數:

  1. WALT窗口大小
  2. cpu當前頻率和cpu最高頻率
  3. task在一個窗口實際運行時間
  4. task demand獲取機制(比如最近窗口的數值,前五個窗口的最大數值等等)

在系統開機階段已知的條件或者常量數值,舉例如下:

  1. 小core的max_cap數值爲488,A55,即cpu_scale數值。小core最高頻率爲1820MHZ
  2. 大core的max_cap數值爲1024,A75,即cpu_scale數值,大core最高頻率爲2028MHZ
  3. WALT窗口大小數值爲16ms

根據WALT算法,一個task的實際運行時間結果當前頻率和當前cpu的最高的capacity scale之後時間作爲WALT窗口真實運行時間。計算方式如下:

 /*                                                                                                                                                                                                          
   * Translate absolute delta time accounted on a CPU 
   * to a scale where 1024 is the capacity of the most 
   * capable CPU running at FMAX 
   */  
  static u64 scale_exec_time(u64 delta, struct rq *rq)  
  {  
      unsigned long capcurr = capacity_curr_of(cpu_of(rq));   
     
      return (delta * capcurr) >> SCHED_CAPACITY_SHIFT;  
  }  
/*     
 * Returns the current capacity of cpu after applying both 
 * cpu and freq scaling. 
 */     
unsigned long capacity_curr_of(int cpu)                                                                                                                                                                        
{  
    unsigned long max_cap = cpu_rq(cpu)->cpu_capacity_orig;  
    unsigned long scale_freq = arch_scale_freq_capacity(NULL, cpu);  
   
    return cap_scale(max_cap, scale_freq);  
}  
#define cap_scale(v, s) ((v)*(s) >> SCHED_CAPACITY_SHIFT)      

經過上面公式整理最後的計算等式如下,delta是task在當前頻率上面運行的時間

scale_time = delta *capcurr/1024
	      = delta * (max_cap * curr_freq *1024/cpuinfo.max_freq)/1024/1024
                  = delta *max_cap*curr_freq / (cpuinfo.max *1024) = demand

上面scale_time就是在當前WALT窗口scale出來的真實運行時間,即task p的demand數值,作爲task util的依據。task util計算方式如下:

static inline unsigned long task_util(struct task_struct *p)                                                                                                                                                   
{                              
#ifdef CONFIG_SCHED_WALT       
    if (likely(!walt_disabled && sysctl_sched_use_walt_task_util))  
        return (p->ravg.demand /  
            (walt_ravg_window >> SCHED_CAPACITY_SHIFT));  
#endif                         
    return READ_ONCE(p->se.avg.util_avg);  
}     

可以知道task util數值如下:

task_util = demand *1024 / walt_ravg_window
              = delta *max_cap *curr_freq / (cpuinfo.max_freq * 16)

16即爲walt_ravg_window一個窗口的數值爲16ms。本來這個數值應該爲16000000,爲了簡化,delta數值都除以過了1000000。
計算公式如下
在這裏插入圖片描述
所以little core task計算公式如下:
在這裏插入圖片描述
big core task計算公式如下:

在這裏插入圖片描述

從上面公式很容易得出下面的結論:
如果task在兩個不同的頻率點(f1,f2,f1<f2)運行相同的時間,那麼在更高頻率點上面運行scale出來的demand數值會變大,從而導致最終的util變大。數值關係爲u2 = (f2/f1)util,即是f2/f1的倍數關係。這樣也好理解,一個task在小頻率點需要運行這麼長時間T1,那麼在高頻率點上面運行時間T2應該更短,T2 = (f1/f2)T1這麼長的時間。
根據公式可以得到如下表格數據:
在這裏插入圖片描述

可以看到在最低頻率614MHZ情況下,task在小core上面運行16ms的util數值僅僅爲164.63297。如果當前頻率爲768MHZ,則運行16ms之後,util爲205.92527=(768/614)*164.63297。與情況一致。

上面得出來的task的util數值。那麼cpu的util怎麼計算的?其實也是根據cpu rq上面task的情況來得到的,如果只有一個task在運行,那麼cpu的util就是此task的util,否則就需要分情況分析。
我們首先需要了解下WALT是怎麼計算cpu的util的。根據窗口策略來獲取的:
最近窗口。

  1. 6個窗口的最大數值。
  2. 6個窗口的平均值
  3. 6個窗口的平均值與最近窗口數值,取最大數值。

註明:這6個窗口也包含了當前task正在運行的窗口
具體可以看到如下,使用WALT下cpu util怎麼計算的:

static inline unsigned long cpu_util(int cpu)  
{     
    struct cfs_rq *cfs_rq;  
    unsigned int util;  
      
#ifdef CONFIG_SCHED_WALT  
    if (likely(!walt_disabled && sysctl_sched_use_walt_cpu_util)) {  
        u64 walt_cpu_util = cpu_rq(cpu)->cumulative_runnable_avg;  
      
        walt_cpu_util <<= SCHED_CAPACITY_SHIFT;  
        do_div(walt_cpu_util, walt_ravg_window);  
      
        return min_t(unsigned long, walt_cpu_util,  
             ┊   capacity_orig_of(cpu));  
    }  
#endif  
      
    cfs_rq = &cpu_rq(cpu)->cfs;  
    util = READ_ONCE(cfs_rq->avg.util_avg);  
      
    if (sched_feat(UTIL_EST))  
        util = max(util, READ_ONCE(cfs_rq->avg.util_est.enqueued));  
      
    return min_t(unsigned long, util, capacity_orig_of(cpu));  
}     
      
static inline unsigned long cpu_util_freq(int cpu)  
{  
#ifdef CONFIG_SCHED_WALT  
    u64 walt_cpu_util;  
    if (unlikely(walt_disabled || !sysctl_sched_use_walt_cpu_util)) {  
        return min(cpu_util(cpu) + cpu_util_rt(cpu),  
               capacity_orig_of(cpu));  
    }  
    walt_cpu_util = cpu_rq(cpu)->prev_runnable_sum;  
    walt_cpu_util <<= SCHED_CAPACITY_SHIFT;  
    do_div(walt_cpu_util, walt_ravg_window);  
    return min_t(unsigned long, walt_cpu_util, capacity_orig_of(cpu));  
#else  
    return min(cpu_util(cpu) + cpu_util_rt(cpu), capacity_orig_of(cpu));  
#endif  
}  

可以看到,cpu_util_freq和cpu_util兩個

  1. cpu_util_freq使用了之前窗口的數值,使用在schedutil governor裏面計算cpu util使用的
  2. cpu_util使用了當前窗口內所有runnable task的demand之和作爲此時的util數值,使用在load balance處。

在這裏插入圖片描述

task爲p.
WS:當前窗口啓動時間
ms:task標記運行時間
wc:當前需要重新計算task demand的系統當前時間

上面WS1是當前窗口的啓動時間,WS2是下一個窗口的啓動時間,WS2-WS1=16ms
ms1是task第一次運行時候的時間,一般爲fork時間或者wakeup時間
wc時間一般是此時需要統計task demand的時刻。時機在每次fork,wakeup,tick期間。對於task運行長時間,一般都是在每次tick作爲update task和cpu util的時機。對上面的圖形解釋如下:

  1. ms1 task被wakeup,有一個初始數值demand,獲取得到task的初始util,如果符合頻率變化需求則會主動去觸發頻率變化
  2. wc1是此task運行一個tick之後,需要更新此task的最新的util,即會更加上面的公式,scale本次運行一個tick時間的真實時間並累加到task的demand sum變量中,即p->ravg.sum +=scale_time;
  3. wc2與2類似,繼續累加p->ravg.sum
  4. wc3與2也類似,不同之處在與,scale的時間爲WS2-ms2,累加到p->ravg.sum
  5. 在wc3處update history value。這時候會將此時p->ravg.sum放置在最近的窗口裏面佔據一個窗口。
  6. 從wc4開始與上面類似,如此的循環往復,直到此task的運行完畢或者util增大到系統cpu能夠容納的capacity的上限爲止。

OK,上面理解了運行時間如果scale爲計算util的真實時間。看到cpu util有兩種計算方式

  1. cpu_util,使用的cumulative_runnable_avg,這就是本次窗口的數值,也就是所有在此窗口內處於runnable狀態 此rq task的demand的累加和。
  2. cpu_util_freq,使用的prev_runnable_sum,是上一個窗口的累加。這裏面其實存在一個問題的,如果prev_runnable_sum比較小,但是task一直在當前窗口運行,會導致prev_runnable_sum會延遲一個窗口更新,也就會延遲cpu頻率的升高。所以如果一個task持續運行,佔滿多個窗口。這個問題復原如下:
    在這裏插入圖片描述
    存在三種可能性,task跨窗口,到底選擇cumulative_runnable_avg還是prev_runnable_sum作爲頻率調節的依據,沒有誰好誰不好,但是prev_runnable_sum有一個明顯的優勢就是,如果一個task在一個窗口內完整運行過了,那麼prev_runnable_sum數值就很大了。所以必須有一個兩全其美的方案,否則會影響性能。

上面已經知道cpu util怎麼來了。下面看看,schedutil如何根據cpu util來降低或者升高頻率。

static unsigned int get_next_freq(struct sugov_policy *sg_policy,  
                ┊ unsigned long util, unsigned long max)  
{                      
    struct cpufreq_policy *policy = sg_policy->policy;  
    unsigned int freq = arch_scale_freq_invariant() ?  
                policy->cpuinfo.max_freq : policy->cur;  
    int freq_margin = sg_policy->tunables->freq_margin;  
                       
    if (freq_margin > -100 && freq_margin < 100)  
        freq_margin = ((int)freq * freq_margin) / 100;  
    else               
        freq_margin = freq >> 2;  
                       
    freq = div64_u64((u64)((int)freq + freq_margin) * (u64)util, max);  
                       
    if (freq == sg_policy->cached_raw_freq && sg_policy->next_freq != UINT_MAX)  
        return sg_policy->next_freq;  
    sg_policy->cached_raw_freq = freq;  
    return cpufreq_driver_resolve_freq(policy, freq);  

上面的代碼是經過修改過的,原始代碼如下:

static unsigned int get_next_freq(struct sugov_policy *sg_policy,  
                  unsigned long util, unsigned long max)  
{  
    struct cpufreq_policy *policy = sg_policy->policy;  
    unsigned int freq = arch_scale_freq_invariant() ?  
                policy->cpuinfo.max_freq : policy->cur;  
    freq = (freq + (freq >> 2)) * util / max;  
    if (freq == sg_policy->cached_raw_freq && sg_policy->next_freq != UINT_MAX)  
        return sg_policy->next_freq;  
    sg_policy->cached_raw_freq = freq;  
    return cpufreq_driver_resolve_freq(policy, freq);  
}  

差異就是原始版本,每次的頻率base都是增加25%,而修改的版本將這個數值作爲變動的來調節。

通過上面的計算方式能夠得到下面的公式如下:
在這裏插入圖片描述
對於little core,頻率計算方式:
在這裏插入圖片描述
對於little core,頻率計算方式:
在這裏插入圖片描述
根據公式計算出的數值如下,boost=1,freqmargin=1.
在這裏插入圖片描述

可以看到,對於大小core來說,只有足夠大的util才能夠升高頻率
具體的變化可以參考下面的公式演示圖:
https://www.desmos.com/calculator/hgnv25fibc

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章