在WALT裏面,一個task的util大小,涉及到下面幾個參數:
- WALT窗口大小
- cpu當前頻率和cpu最高頻率
- task在一個窗口實際運行時間
- task demand獲取機制(比如最近窗口的數值,前五個窗口的最大數值等等)
在系統開機階段已知的條件或者常量數值,舉例如下:
- 小core的max_cap數值爲488,A55,即cpu_scale數值。小core最高頻率爲1820MHZ
- 大core的max_cap數值爲1024,A75,即cpu_scale數值,大core最高頻率爲2028MHZ
- 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的。根據窗口策略來獲取的:
最近窗口。
- 6個窗口的最大數值。
- 6個窗口的平均值
- 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兩個
- cpu_util_freq使用了之前窗口的數值,使用在schedutil governor裏面計算cpu util使用的
- 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的時機。對上面的圖形解釋如下:
- ms1 task被wakeup,有一個初始數值demand,獲取得到task的初始util,如果符合頻率變化需求則會主動去觸發頻率變化
- wc1是此task運行一個tick之後,需要更新此task的最新的util,即會更加上面的公式,scale本次運行一個tick時間的真實時間並累加到task的demand sum變量中,即p->ravg.sum +=scale_time;
- wc2與2類似,繼續累加p->ravg.sum
- wc3與2也類似,不同之處在與,scale的時間爲WS2-ms2,累加到p->ravg.sum
- 在wc3處update history value。這時候會將此時p->ravg.sum放置在最近的窗口裏面佔據一個窗口。
- 從wc4開始與上面類似,如此的循環往復,直到此task的運行完畢或者util增大到系統cpu能夠容納的capacity的上限爲止。
OK,上面理解了運行時間如果scale爲計算util的真實時間。看到cpu util有兩種計算方式
- cpu_util,使用的cumulative_runnable_avg,這就是本次窗口的數值,也就是所有在此窗口內處於runnable狀態 此rq task的demand的累加和。
- 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