CFS排程器(4)-PELT(per entity load tracking)
CFS排程器(4)-PELT(per entity load tracking)
為什麼需要PELT?
為了讓排程器更加的聰明,我們總是希望系統滿足最大吞吐量同時又最大限度的降低功耗。雖然可能有些矛盾,但是現實總是這樣。PELT演算法是Linux 3.8合入的,那麼在此之前,我們存在什麼問題才引入PELT演算法呢?在Linux 3.8之前,CFS以每個執行佇列(runqueue,簡稱rq)為基礎跟蹤負載。但是這種方法,我們無法確定當前負載的來源。同時,即使工作負載相對穩定的情況下,在rq級別跟蹤負載,其值也會產生很大變化。為了解決以上的問題,PELT演算法會跟蹤每個排程實體(per-scheduling entity)的負載情況。
注:程式碼分析基於Linux 4.18.0。
如何進行PELT
具體原理的東西可以參考這篇文章 ofollow,noindex" target="_blank">《per-entity load tracking》 。我就無恥的從這篇文章中摘錄一段話吧。為了做到Per-entity的負載跟蹤,時間(物理時間,不是虛擬時間)被分成了1024us的序列,在每一個1024us的週期中,一個entity對系統負載的貢獻可以根據該實體處於runnable狀態(正在CPU上執行或者等待cpu排程執行)的時間進行計算。如果在該週期內,runnable的時間是x,那麼對系統負載的貢獻就是(x/1024)。當然,一個實體在一個計算週期內的負載可能會超過1024us,這是因為我們會累積在過去週期中的負載,當然,對於過去的負載我們在計算的時候需要乘一個衰減因子。如果我們讓Li表示在週期pi中該排程實體的對系統負載貢獻,那麼一個排程實體對系統負荷的總貢獻可以表示為:
L = L0 + L1 * y + L2 * y2 + L3 * y3 + ... + Ln * yn
- L 0 = L 1 = L 2 = ... = L n = 1024us
- y 32 = 0.5, y = 0.97857206
初次看到以上公式,不知道你是否在想這都是什麼玩意!舉個例子,如何計算一個se的負載貢獻。如果有一個task,從第一次加入rq後開始一直執行4096us後一直睡眠,那麼在1023us、2047us、3071us、4095us、5119us、6143us、7167us和8191us時間的每一個時刻負載貢獻分別是多少呢?
1023us: L0 = 1023 2047us: L1 = 1023 + 1024 * y= 1023 + (L0 + 1) * y = 2025 3071us: L2 = 1023 + 1024 * y + 1024 * y2= 1023 + (L1 + 1) * y = 3005 4095us: L3 = 1023 + 1024 * y + 1024 * y2 + 1024 * y3= 1023 + (L2 + 1) * y = 3963 5119us: L4 = 0+ 1024 * y + 1024 * y2 + 1024 * y3 + 1024 * y4=0 + (L3 + 1) * y = 3877 6143us: L5 = 0+0+ 1024 * y2 + 1024 * y3 + 1024 * y4 + 1024 * y5 =0 + L4 * y = 3792 7167us: L6 = 0+ L5 * y = L4 * y2 = 3709 8191us: L7 = 0+ L6 * y = L5 * y2 = L4 * y3 = 3627
經過以上的舉例,我們不難發現一個規律,計算當前時間的負載只需要上個週期負載貢獻總和乘以衰減係數y,並加上當前時間點的負載即可。
從上面的計算公式我們也可以看出,經常需要計算val*y n 的值,因此核心提供decay_load()函式用於計算第n個週期的衰減值。為了避免浮點數運算,採用移位和乘法運算提高計算速度。decay_load(val, n) = val*y n *2 32 >>32。我們將y n *2 32 的值提前計算出來儲存在陣列runnable_avg_yN_inv中。
runnable_avg_yN_inv[n] = yn*232, n > 0 && n < 32
runnable_avg_yN_inv的計算可以參考/Documentation/scheduler/sched-pelt.c檔案calc_runnable_avg_yN_inv()函式。由於y 32 =0.5,因此我們只需要計算y*2 32 ~y 31 *2 32 的值儲存到陣列中即可。當n大於31的時候,為了計算y n *2 32 我們可以藉助y 32 =0.5公式間接計算。例如y 33 *2 32 =y 32 *y*2 32 =0.5*y*2 32 =0.5*runnable_avg_yN_inv[1]。calc_runnable_avg_yN_inv()函式簡單歸納就是: runnable_avg_yN_inv[i] = ((1UL << 32) - 1) * pow(0.97857206, i),i>=0 && i<32
。pow(x, y)是求x y 的值。計算得到runnable_avg_yN_inv陣列的值如下:
static const u32 runnable_avg_yN_inv[] = { 0xffffffff, 0xfa83b2da, 0xf5257d14, 0xefe4b99a, 0xeac0c6e6, 0xe5b906e6, 0xe0ccdeeb, 0xdbfbb796, 0xd744fcc9, 0xd2a81d91, 0xce248c14, 0xc9b9bd85, 0xc5672a10, 0xc12c4cc9, 0xbd08a39e, 0xb8fbaf46, 0xb504f333, 0xb123f581, 0xad583ee9, 0xa9a15ab4, 0xa5fed6a9, 0xa2704302, 0x9ef5325f, 0x9b8d39b9, 0x9837f050, 0x94f4efa8, 0x91c3d373, 0x8ea4398a, 0x8b95c1e3, 0x88980e80, 0x85aac367, 0x82cd8698, };
根據runnable_avg_yN_inv陣列的值,我們就方便實現decay_load()函式。
/* * Approximate: *val * y^n,where y^32 ~= 0.5 (~1 scheduling period) */ static u64 decay_load(u64 val, u64 n) { unsigned int local_n; if (unlikely(n > LOAD_AVG_PERIOD * 63))/* 1 */ return 0; /* after bounds checking we can collapse to 32-bit */ local_n = n; /* * As y^PERIOD = 1/2, we can combine *y^n = 1/2^(n/PERIOD) * y^(n%PERIOD) * With a look-up table which covers y^n (n<PERIOD) * * To achieve constant time decay_load. */ if (unlikely(local_n >= LOAD_AVG_PERIOD)) {/* 2 */ val >>= local_n / LOAD_AVG_PERIOD; local_n %= LOAD_AVG_PERIOD; } val = mul_u64_u32_shr(val, runnable_avg_yN_inv[local_n], 32);/* 2 */ return val; }
- LOAD_AVG_PERIOD的值為32,我們認為當時間經過2016個週期後,衰減後的值為0。即val*y n =0, n > 2016。
- 當n大於等於32的時候,就需要根據y 32 =0.5條件計算y n 的值。y n *2 32 = 1/2 n/32 * y n%32 *2 32 =1/2 n/32 * runnable_avg_yN_inv[n%32]。
如何計算當前負載貢獻
經過上面舉例,我們可以知道計算當前負載貢獻並不需要記錄所有歷史負載貢獻。我們只需要知道上一刻負載貢獻就可以計算當前負載貢獻,這大大降低了程式碼實現複雜度。我們繼續上面舉例問題的思考,我們依然假設一個task開始從0時刻執行,那麼1022us後的負載貢獻自然就是1022。當task經過10us之後,此時(現在時刻是1032us)的負載貢獻又是多少呢?很簡單,10us中的2us和之前的1022us可以湊成一個週期1024us。這個1024us需要進行一次衰減,即現在的負載貢獻是:(1024 - 1022 + 1022)y + 10 - (1024 - 1022) = 1022y + 2y + 8 = 1010。1022y可以理解成由於經歷了一個週期,因此上一時刻的負載需要衰減一次,因此1022需要乘以衰減係數y,2y可以理解成,2us屬於上一個負載計算時距離一個週期1024us的差值,由於2是上一個週期的時間,因此也需要衰減一次,8是當前週期時間,不需要衰減。又經過了2124us,此時(現在時刻是3156us)負載貢獻又是多少呢?即:(1024 - 8 + 1010)y 2 + 1024y + 2124 - 1024 - (1024 - 8) = 1010y 2 + 1016y 2 + 1024y + 84 = 3024。2124us可以分解成3部分:1016us補齊上一時刻不足1024us部分,湊成一個週期;1024us一個整週期;當前時刻不足一個週期的剩餘84us部分。相當於我們經過了2個週期,因此針對上一次的負載貢獻需要衰減2次,也就是1010y 2 部分,1016us是補齊上一次不足一個週期的部分,因此也需要衰減2次,所以公式中還有1016y 2 部分。1024us部分相當於距離當前時刻是一個週期,所以需要衰減1次,最後84部分是當前剩餘時間,不需要衰減。
針對以上事例,我們可以得到一個更通用情況下的計算公式。假設上一時刻負載貢獻是u,經歷d時間後的負載貢獻如何計算呢?根據上面的例子,我們可以把時間d分成3和部分:d1是離當前時間最遠(不完整的)period 的剩餘部分,d2 是完整period時間,而d3是(不完整的)當前 period 的剩餘部分。假設時間d是經過p個週期(d=d1+d2+d3, p=1+d2/1024)。d1,d2,d3 的示意圖如下:
d1d2d3 ^^^ ||| |<->|<----------------->|<--->| |---x---|------| ... |------|-----x (now) p-1 u' = (u + d1) y^p + 1024 \Sum y^n + d3 y^0 n=1 p-1 = u y^p + d1 y^p + 1024 \Sum y^n + d3 y^0 n=1
上面的例子現在就可以套用上面的公式計算。例如,上一次的負載貢獻u=1010,經過時間d=2124us,可以分解成3部分,d1=1016us,d2=1024,d3=84。經歷的週期p=2。所以當前時刻負載貢獻u'=1010y 2 + 1016y 2 + 1024y + 84,與上面計算結果一致。
如何記錄負載資訊
Linux中使用 struct sched_avg
結構體記錄排程實體se或者就緒佇列cfs rq負載資訊。每個排程實體se以及cfs就緒佇列結構體中都包含一個 struct sched_avg
結構體用於記錄負載資訊。 struct sched_avg
定義如下。
struct sched_avg { u64last_update_time; u64load_sum; u64runnable_load_sum; u32util_sum; u32period_contrib; unsigned longload_avg; unsigned longrunnable_load_avg; unsigned longutil_avg; };
- last_update_time:上一次負載更新時間。用於計算時間間隔。
- load_sum:基於可執行(runnable)時間的負載貢獻總和。runnable時間包含兩部分:一是在rq中等待cpu排程執行的時間,二是正在cpu上執行的時間。
- util_sum:基於正在執行(running)時間的負載貢獻總和。running時間是指排程實體se正在cpu上執行時間。
- load_avg:基於可執行(runnable)時間的平均負載貢獻。
- util_avg:基於正在執行(running)時間的平均負載貢獻。
一個排程實體se可能屬於task,也有可能屬於group(Linux支援組排程,需要配置CONFIG_FAIR_GROUP_SCHED)。排程實體se的初始化針對task se和group se也就有所區別。排程實體使用 struct sched_entity
描述如下。
struct sched_entity { struct load_weightload; unsigned longrunnable_weight; #ifdef CONFIG_SMP struct sched_avgavg; #endif };
排程實體se初始化函式是init_entity_runnable_average(),程式碼如下。
void init_entity_runnable_average(struct sched_entity *se) { struct sched_avg *sa = &se->avg; memset(sa, 0, sizeof(*sa)); /* * Tasks are intialized with full load to be seen as heavy tasks until * they get a chance to stabilize to their real load level. * Group entities are intialized with zero load to reflect the fact that * nothing has been attached to the task group yet. */ if (entity_is_task(se)) sa->runnable_load_avg = sa->load_avg = scale_load_down(se->load.weight); se->runnable_weight = se->load.weight; /* when this task enqueue'ed, it will contribute to its cfs_rq's load_avg */ }
針對task se初始化,runnable_load_avg和load_avg的值是和se的權重(se->load.weight)相等。而且根據註釋其實也可以知道,runnable_load_avg和load_avg在後續的負載計算中累加的最大值其實就是se的權重值。也就意味著,runnable_load_avg和load_avg的值可以間接的表明task的繁重程度。runnable_weight成員主要是針對group se提出的。對於task se來說,runnable_weight就是se的weight,二者的值完全一樣。
針對group se,runnable_load_avg和load_avg的值初始化為0。這也意味著當前task group中沒有任何task需要排程。runnable_weight雖然現在初始化為se的權重值,但是在後續的程式碼中會不斷的更新runnable_weight的值。runnable_weight是實體權重的一部分,表示組runqueue的可執行部分。
負載計算程式碼實現
在瞭解了以上資訊後,可以開始研究上一節中計算負載貢獻的公式的原始碼實現。
p-1 u' = (u + d1) y^p + 1024 \Sum y^n + d3 y^0 n=1 = u y^p +(Step 1) p-1 d1 y^p + 1024 \Sum y^n + d3 y^0(Step 2) n=1
以上公式在程式碼中由兩部實現,accumulate_sum()函式計算step1部分,然後呼叫__accumulate_pelt_segments()函式計算step2部分。
static __always_inline u32 accumulate_sum(u64 delta, int cpu, struct sched_avg *sa, unsigned long load, unsigned long runnable, int running) { unsigned long scale_freq, scale_cpu; u32 contrib = (u32)delta; /* p == 0 -> delta < 1024 */ u64 periods; scale_freq = arch_scale_freq_capacity(cpu); scale_cpu = arch_scale_cpu_capacity(NULL, cpu); delta += sa->period_contrib;/* 1 */ periods = delta / 1024; /* A period is 1024us (~1ms) *//* 2 */ /* * Step 1: decay old *_sum if we crossed period boundaries. */ if (periods) { sa->load_sum = decay_load(sa->load_sum, periods);/* 3 */ sa->runnable_load_sum = decay_load(sa->runnable_load_sum, periods); sa->util_sum = decay_load((u64)(sa->util_sum), periods); /* * Step 2 */ delta %= 1024; contrib = __accumulate_pelt_segments(periods,/* 4 */ 1024 - sa->period_contrib, delta); } sa->period_contrib = delta;/* 5 */ contrib = cap_scale(contrib, scale_freq); if (load) sa->load_sum += load * contrib; if (runnable) sa->runnable_load_sum += runnable * contrib; if (running) sa->util_sum += contrib * scale_cpu; return periods; }
- period_contrib記錄的是上次更新負載不足1024us週期的時間。delta是經過的時間,為了計算經過的週期個數需要加上period_contrib,然後整除1024。
- 計算週期個數。
- 呼叫decay_load()函式計算公式中的step1部分。
- __accumulate_pelt_segments()負責計算公式step2部分。
- 更新period_contrib為本次不足1024us部分。
下面分析__accumulate_pelt_segments()函式。
static u32 __accumulate_pelt_segments(u64 periods, u32 d1, u32 d3) { u32 c1, c2, c3 = d3; /* y^0 == 1 */ /* * c1 = d1 y^p */ c1 = decay_load((u64)d1, periods); /* *p-1 * c2 = 1024 \Sum y^n *n=1 * *infinf *= 1024 ( \Sum y^n - \Sum y^n - y^0 ) *n=0n=p */ c2 = LOAD_AVG_MAX - decay_load(LOAD_AVG_MAX, periods) - 1024; return c1 + c2 + c3; }
__accumulate_pelt_segments()函式主要的關注點應該是這個c2是如何計算的。本來是一個多項式求和,非常巧妙的變成了一個很簡單的計算方法。這個轉換過程如下。
p-1 c2 = 1024 \Sum y^n n=1 In terms of our maximum value: infinfp-1 max = 1024 \Sum y^n = 1024 ( \Sum y^n + \Sum y^n + y^0 ) n=0n=pn=1 Further note that: infinfinf ( \Sum y^n ) y^p = \Sum y^(n+p) = \Sum y^n n=0n=0n=p Combined that gives us: p-1 c2 = 1024 \Sum y^n n=1 infinf = 1024 ( \Sum y^n - \Sum y^n - y^0 ) n=0n=p = max - (max y^p) - 1024
LOAD_AVG_MAX其實就是1024(1 + y + y 2 + ... + y n )的最大值,計算方法很簡單,等比數列求和公式一套,然後n趨向於正無窮即可。最終LOAD_AVG_MAX的值是47742。當然我們使用數學方法計算的數值可能和這個值有點誤差,並不是完全相等。那是因為47742這個值是通過程式碼計算得到的,計算機計算的過程中涉及浮點數運算及取整操作,有誤差也是正常的。LOAD_AVG_MAX的計算程式碼如下。
void calc_converged_max(void) { int n = -1; long max = 1024; long last = 0, y_inv = ((1UL << 32) - 1) * y; for (; ; n++) { if (n > -1) max = ((max * y_inv) >> 32) + 1024; /* * This is the same as: * max = max*y + 1024; */ if (last == max) break; last = max; } printf("#define LOAD_AVG_MAX %ld\n", max); }
排程實體更新負載貢獻
更新排程實體負載的函式是update_load_avg()。該函式會在以下情況呼叫。
- 向就緒佇列中新增一個程序,在CFS中就是enqueue_entity操作。
- 從就緒佇列中刪除一個程序,在CFS中就是dequeue_entity操作。
- scheduler tick,週期性呼叫更新負載資訊。
static inline void update_load_avg(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags) { u64 now = cfs_rq_clock_task(cfs_rq); struct rq *rq = rq_of(cfs_rq); int cpu = cpu_of(rq); int decayed; /* * Track task load average for carrying it to new CPU after migrated, and * track group sched_entity load average for task_h_load calc in migration */ if (se->avg.last_update_time && !(flags & SKIP_AGE_LOAD)) __update_load_avg_se(now, cpu, cfs_rq, se);/* 1 */ decayed= update_cfs_rq_load_avg(now, cfs_rq);/* 2 */ /* ...... */ }
- __update_load_avg_se()負責更新排程實體se的負載資訊。
- 在更新se負載後,順便更新se attach的cfs就緒佇列的負載資訊。runqueue的負載就是該runqueue下所有的se負載總和。
__update_load_avg_se()程式碼如下。
static int __update_load_avg_se(u64 now, int cpu, struct cfs_rq *cfs_rq, struct sched_entity *se) { if (entity_is_task(se)) se->runnable_weight = se->load.weight;/* 1 */ if (___update_load_sum(now, cpu, &se->avg, !!se->on_rq, !!se->on_rq,/* 2 */ cfs_rq->curr == se)) { ___update_load_avg(&se->avg, se_weight(se), se_runnable(se));/* 3 */ cfs_se_util_change(&se->avg); return 1; } return 0; }
- runnable_weight稱作可執行權重,該概念主要針對group se提出。針對task se來說,runnable_weight的值就是和程序權重weight相等。針對group se,runnable_weight的值總是小於等於weight。
- 通過___update_load_sum()函式計算排程實體se的負載總和資訊。
- 更新平均負載資訊,例如se->load_avg成員。
___update_load_sum()函式實現如下。
static __always_inline int ___update_load_sum(u64 now, int cpu, struct sched_avg *sa, unsigned long load, unsigned long runnable, int running) { u64 delta; delta = now - sa->last_update_time; delta >>= 10;/* 1 */ if (!delta) return 0; sa->last_update_time += delta << 10;/* 2 */ if (!load) runnable = running = 0; if (!accumulate_sum(delta, cpu, sa, load, runnable, running))/* 3 */ return 0; return 1; }
- delta是兩次負載更新之間時間差,單位是ns。整除1024是將ns轉換成us單位。PELT演算法最小時間計量單位時us,如果時間差連1us都不到,就沒必要衰減計算,直接返回即可。
- 更新last_update_time,方便下次更新負載資訊,計算時間差。
- 通過accumulate_sum()進行負載計算,由上面呼叫地方可知,這裡的引數load、runnable及running非0即1。因此,在負載計算中可知,se->load_sum和se->runnable_load_sum最大值就是LOAD_AVG_MAX - 1024 + se->period_contrib。並且,se->load_sum的值和se->runnable_load_sum相等。
繼續探究平均負載資訊如何更新。___update_load_avg()函式如下。
static __always_inline void ___update_load_avg(struct sched_avg *sa, unsigned long load, unsigned long runnable) { u32 divider = LOAD_AVG_MAX - 1024 + sa->period_contrib; /* * Step 2: update *_avg. */ sa->load_avg = div_u64(load * sa->load_sum, divider); sa->runnable_load_avg =div_u64(runnable * sa->runnable_load_sum, divider); sa->util_avg = sa->util_sum / divider; }
由上面的程式碼可知,load是排程實體se的權重weight,runnable是排程實體se的runnable_weight。因此平均負債計算公式如下。針對task se來說,se->load_avg和se->runnable_load_avg的值是相等的(因為,se->load_sum和se->runnable_load_sum相等,並且se->load.weight和se->runnable_weight相等),並且其值是小於等於se->load.weight。
se->load_sum se->load_avg = -------------------------------------------- * se->load.weight LOAD_AVG_MAX - 1024 + sa->period_contrib se->runnable_load_sum se->runnable_load_avg = -------------------------------------------- * se->runnable_weight LOAD_AVG_MAX - 1024 + sa->period_contrib
針對頻繁執行的程序,load_avg的值會越來越接近權重weight。例如,權重1024的程序長時間執行,其負載貢獻曲線如下。上面的表格是程序執行的時間,下表是負載貢獻曲線。
從某一時刻程序開始執行,負載貢獻就開始一直增加。現在如果是一個週期執行的程序(每次執行1ms,睡眠9ms),那麼負載貢獻曲線圖如何呢?
負載貢獻的值基本維持在最小值和最大值兩個峰值之間。這也符合我們的預期,我們認為負載貢獻就是反應程序執行的頻繁程度。因此,基於PELT演算法,我們在負載均衡的時候,可以更清楚的計算一個程序遷移到其他CPU的影響。
就緒佇列更新負載資訊
前面已經提到更新就緒佇列負載資訊的函式是update_cfs_rq_load_avg()。
static inline int update_cfs_rq_load_avg(u64 now, struct cfs_rq *cfs_rq) { int decayed = 0; decayed |= __update_load_avg_cfs_rq(now, cpu_of(rq_of(cfs_rq)), cfs_rq); return decayed; }
繼續呼叫__update_load_avg_cfs_rq()更新CFS就緒佇列負載資訊。該函式和以上更新排程實體se負載資訊函式很相似。
static int __update_load_avg_cfs_rq(u64 now, int cpu, struct cfs_rq *cfs_rq) { if (___update_load_sum(now, cpu, &cfs_rq->avg, scale_load_down(cfs_rq->load.weight), scale_load_down(cfs_rq->runnable_weight), cfs_rq->curr != NULL)) { ___update_load_avg(&cfs_rq->avg, 1, 1); return 1; } return 0; }
struct cfs_rq
結構體內嵌 struct sched_avg
結構體,用於跟蹤就緒佇列負載資訊。___update_load_sum()函式上面已經分析過,這裡和更新排程實體se負載的區別是傳遞的引數不一樣。load和runnable分別傳遞的是CFS就緒佇列的權重以及可執行權重。CFS就緒佇列的權重是指CFS就緒佇列上所有就緒態排程實體權重之和。CFS就緒佇列平均負載貢獻是指所有排程實體平均負載之和。在每次更新排程實體負載資訊時也會同步更新se依附的CFS就緒佇列負載資訊。
runnable_load_avg和load_avg區別
在介紹 struct sched_avg
結構體的時候,我們只介紹了load_avg成員而忽略了runnable_load_avg成員。那麼他們究竟有什麼區別呢?我們知道 struct sched_avg
結構體會被內嵌在排程實體 struct sched_entity
和就緒佇列 struct cfs_rq
中,分別用來跟蹤排程實體和就緒佇列的負載資訊。針對task se,runnable_load_avg和load_avg的值是沒有差別的。但是對於就緒佇列負載來說,二者就有不一樣的意義。load_avg代表就緒佇列平均負載,其包含睡眠程序的負載貢獻。runnable_load_avg只包含就緒佇列上所有可執行程序的負載貢獻。如何體現區別呢?我們看一下在程序加入就緒佇列的處理。又是大家熟悉的enqueue_entity()函式。
static void enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags) { /* * When enqueuing a sched_entity, we must: *- Update loads to have both entity and cfs_rq synced with now. *- Add its load to cfs_rq->runnable_avg *- For group_entity, update its weight to reflect the new share of *its group cfs_rq *- Add its new weight to cfs_rq->load.weight */ update_load_avg(cfs_rq, se, UPDATE_TG | DO_ATTACH);/* 1 */ enqueue_runnable_load_avg(cfs_rq, se);/* 2 */ }
- load_avg成員更新資訊,傳遞flag包含DO_ATTACH。當程序建立第一次呼叫update_load_avg()函式時,這個flag會用上。
- 更新runnable_load_avg資訊。
我們熟悉的update_load_avg()函式如下。
static inline void update_load_avg(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags) { u64 now = cfs_rq_clock_task(cfs_rq); struct rq *rq = rq_of(cfs_rq); int cpu = cpu_of(rq); int decayed; if (!se->avg.last_update_time && (flags & DO_ATTACH)) { /* * DO_ATTACH means we're here from enqueue_entity(). * !last_update_time means we've passed through * migrate_task_rq_fair() indicating we migrated. * * IOW we're enqueueing a task on a new CPU. */ attach_entity_load_avg(cfs_rq, se, SCHED_CPUFREQ_MIGRATION);/* 1 */ update_tg_load_avg(cfs_rq, 0); } else if (decayed && (flags & UPDATE_TG)) update_tg_load_avg(cfs_rq, 0); }
- 程序第一次被建立之後,se->avg.last_update_time的值為0。因此,attach_entity_load_avg()函式本次會被呼叫。
attach_entity_load_avg()函式如下。
static void attach_entity_load_avg(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags) { u32 divider = LOAD_AVG_MAX - 1024 + cfs_rq->avg.period_contrib; se->avg.last_update_time = cfs_rq->avg.last_update_time; se->avg.period_contrib = cfs_rq->avg.period_contrib; se->avg.util_sum = se->avg.util_avg * divider; se->avg.load_sum = divider; if (se_weight(se)) { se->avg.load_sum = div_u64(se->avg.load_avg * se->avg.load_sum, se_weight(se)); } se->avg.runnable_load_sum = se->avg.load_sum; enqueue_load_avg(cfs_rq, se); cfs_rq->avg.util_avg += se->avg.util_avg; cfs_rq->avg.util_sum += se->avg.util_sum; add_tg_cfs_propagate(cfs_rq, se->avg.load_sum); cfs_rq_util_change(cfs_rq, flags); }
我們可以看到排程室se關於負載的一大堆的初始化。我們現在關注的點是enqueue_load_avg()函式。
enqueue_load_avg()函式如下,很清晰明瞭直接將排程實體負載資訊累加到就緒佇列的load_avg成員。
static inline void enqueue_load_avg(struct cfs_rq *cfs_rq, struct sched_entity *se) { cfs_rq->avg.load_avg += se->avg.load_avg; cfs_rq->avg.load_sum += se_weight(se) * se->avg.load_sum; }
當程序從就緒佇列刪除的時候,並不會將se的負載從就緒佇列的load_avg中刪除。因此,load_avg包含了所有排程實體的可執行狀態以及阻塞狀態的負載資訊。
runnable_load_avg是隻包含可執行程序的負載資訊。我們看下enqueue_runnable_load_avg()函式。很清晰明瞭,直接將排程實體負載資訊累加runnable_load_avg成員。
static inline void enqueue_runnable_load_avg(struct cfs_rq *cfs_rq, struct sched_entity *se) { cfs_rq->runnable_weight += se->runnable_weight; cfs_rq->avg.runnable_load_avg += se->avg.runnable_load_avg; cfs_rq->avg.runnable_load_sum += se_runnable(se) * se->avg.runnable_load_sum; }
下面繼續看下dequeue_entity操作。
static void dequeue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags) { /* * When dequeuing a sched_entity, we must: *- Update loads to have both entity and cfs_rq synced with now. *- Substract its load from the cfs_rq->runnable_avg. *- Substract its previous weight from cfs_rq->load.weight. *- For group entity, update its weight to reflect the new share *of its group cfs_rq. */ update_load_avg(cfs_rq, se, UPDATE_TG); account_entity_dequeue(cfs_rq, se); }
account_entity_dequeue()函式就是減去即將出隊的排程實體的負載資訊。account_entity_dequeue()函式如下。
static inline void dequeue_runnable_load_avg(struct cfs_rq *cfs_rq, struct sched_entity *se) { cfs_rq->runnable_weight -= se->runnable_weight; sub_positive(&cfs_rq->avg.runnable_load_avg, se->avg.runnable_load_avg); sub_positive(&cfs_rq->avg.runnable_load_sum, se_runnable(se) * se->avg.runnable_load_sum); }
我們並沒有看到load_avg成員減去排程實體的負載資訊,只看到runnable_load_avg成員的變化。因此,排程實體入隊和出隊的操作中會對應增加和減少runnable_load_avg。所以,runnable_load_avg包含的是所有就緒佇列上可執行狀態排程實體的負載資訊之和。load_avg是所有的可執行狀態及阻塞狀態程序的負載之和。
標籤:PELT
