【原創】(二)Linux進程調度器-CPU負載


背景

  • Read the fucking source code! --By 魯迅
  • A picture is worth a thousand words. --By 高爾基

說明:

  1. Kernel版本:4.14
  2. ARM64處理器,Contex-A53,雙核
  3. 使用工具:Source Insight 3.5, Visio

1. 概述

CPU負載(cpu load)指的是某個時間點進程對系統產生的壓力。
來張圖來類比下(參考Understanding Linux CPU Load

  • CPU的運行能力,就如大橋的通行能力,分別有滿負荷,非滿負荷,超負荷等狀態,這幾種狀態對應不同的cpu load值;
  • 單CPU滿負荷運行時cpu_load為1,當多個CPU或多核時,相當於大橋有多個車道,滿負荷運行時cpu_load值為CPU數或多核數;
  • CPU負載的計算(以單CPU為例),假設一分鍾內執行10個任務代表滿負荷,當一分鍾給出30個任務時,CPU只能處理10個,剩余20個不能處理,cpu_load=3;

在實際系統中查看:

  • cat /proc/cpuinfo:查看CPU信息;
  • cat /proc/loadavg:查看cpu最近1/5/15分鍾的平均負載:

計算CPU負載,可以讓調度器更好的進行負載均衡處理,以便提高系統的運行效率。此外,內核中的其他子系統也可以參考這些CPU負載值來進行相應的調整,比如DVFS等。

目前內核中,有以下幾種方式來跟蹤CPU負載:

  1. 全局CPU平均負載;
  2. 運行隊列CPU負載;
  3. PELT(per entity load tracking);

這也是本文需要探討的內容,開始吧。

2. 全局CPU平均負載

2.1 基礎概念

先來明確兩個與CPU負載計算相關的概念:

  1. active task(活動任務):只有知道活動任務數量,才能計算CPU負載,而活動任務包括了TASK_RUNNINGTASK_UNINTERRUPTIBLE兩類任務。包含TASK_UNINTERRUPTIBLE任務的原因是,這類任務經常是在等待I/O請求,將其包含在內也合理;

  2. NO_HZ:我們都知道Linux內核每隔固定時間發出timer interrupt,而HZ是用來定義1秒中的timer interrupts次數,HZ的倒數是tick,是系統的節拍器,每個tick會處理包括調度器、時間管理、定時器等事務。周期性的時鍾中斷帶來的問題是,不管CPU空閑或繁忙都會觸發,會帶來額外的系統損耗,因此引入了NO_HZ模式,可以在CPU空閑時將周期性時鍾關掉。在NO_HZ期間,活動任務數量的改變也需要考慮,而它的計算不如周期性時鍾模式下直觀。

2.2 流程

Linux內核中定義了三個全局變量值avenrun[3],用於存放最近1/5/15分鍾的平均CPU負載。

看一下計算流程:

  • 計算活動任務數,這個包括兩部分:1)周期性調度中新增加的活動任務;2)在NO_HZ期間增加的活動任務數;
  • 根據活動任務數值,再結合全局變量值avenrun[]中的old value,來計算新的CPU負載值,並最終替換掉avenrun[]中的值;
  • 系統默認每隔5秒鍾會計算一次負載,如果由於NO_HZ空閑而錯過了下一個CPU負載的計算周期,則需要再次進行更新。比如NO_HZ空閑20秒而無法更新CPU負載,前5秒負載已經更新,需要計算剩余的3個計算周期的負載來繼續更新;

2.3 計算方法

Linux內核中,采用11位精度的定點化計算,CPU負載1.0由整數2048表示,宏定義如下:

#define FSHIFT          11		             /* nr of bits of precision */
#define FIXED_1         (1<<FSHIFT)	    /* 1.0 as fixed-point */
#define LOAD_FREQ   (5*HZ+1)	    /* 5 sec intervals */
#define EXP_1           1884		        /* 1/exp(5sec/1min) as fixed-point */
#define EXP_5           2014		        /* 1/exp(5sec/5min) */
#define EXP_15         2037		      /* 1/exp(5sec/15min) */

計算公式如下:

  • load值為舊的CPU負載值avenrun[],整個計算完成后得到新的負載值,再更新avenrun[]
  • EXP_1/EXP_5/EXP_15,分別代表最近1/5/15分鍾的定點化值的指數因子;
  • active值,根據讀取calc_load_tasks的值來判斷,大於0則乘以FIXED_1(2048)傳入;
  • 根據activeload值的大小關系來決定是否需要加1,類似於四舍五入的機制;

關鍵代碼如下:

	active = atomic_long_read(&calc_load_tasks);
	active = active > 0 ? active * FIXED_1 : 0;

	avenrun[0] = calc_load(avenrun[0], EXP_1, active);
	avenrun[1] = calc_load(avenrun[1], EXP_5, active);
	avenrun[2] = calc_load(avenrun[2], EXP_15, active);
  • NO_HZ模式下活動任務數量更改的計算
    由於NO_HZ空閑效應而更改的CPU活動任務數量,存放在全局變量calc_load_nohz[2]中,並且每5秒計算周期交替更換一次存儲位置(calc_load_read_idx/calc_load_write_idx),其他程序可以去讀取最近5秒內的活動任務變化的增量值。

計算示例
假設在某個CPU上,開始計算時load=0.5,根據calc_load_tasks值獲取不同的active,中間進入NO_HZ模式空閑了20秒,整個計算的值如下圖:

3. 運行隊列CPU負載

  • Linux系統會計算每個tick的平均CPU負載,並將其存儲在運行隊列中rq->cpu_load[5],用於負載均衡;

下圖顯示了計算運行隊列的CPU負載的處理流程:

最終通過cpu_load_update來計算,邏輯如下:

  • 其中傳入的this_load值,為運行隊列現有的平均負載值。

上圖中的衰減因子,是在NO_HZ模式下去進行計算的。在沒有使用tick時,從預先計算的表中計算負載值。Linux內核中定義了兩個全局變量:

#define DEGRADE_SHIFT		7

static const u8 degrade_zero_ticks[CPU_LOAD_IDX_MAX] = {0, 8, 32, 64, 128};
static const u8 degrade_factor[CPU_LOAD_IDX_MAX][DEGRADE_SHIFT + 1] = {
	{   0,   0,  0,  0,  0,  0, 0, 0 },
	{  64,  32,  8,  0,  0,  0, 0, 0 },
	{  96,  72, 40, 12,  1,  0, 0, 0 },
	{ 112,  98, 75, 43, 15,  1, 0, 0 },
	{ 120, 112, 98, 76, 45, 16, 2, 0 }
};

衰減因子的計算主要是在delay_load_missed()函數中完成,該函數會返回load * 衰減因子的值,作為上圖中的old_load
計算方式如下:

4. PELT

PELT, Per-entity load tracking。在Linux引入PELT之前,CFS調度器在計算CPU負載時,通過跟蹤每個運行隊列上的負載來計算;在引入PELT之后,通過跟蹤每個調度實體的負載貢獻來計算。(其中,調度實體:指task或task_group

4.1 PELT計算方法

總體的計算思路:
將調度實體的可運行狀態時間(正在運行+等待CPU調度運行),按1024us划分成不同的周期,計算每個周期內該調度實體對系統負載的貢獻,最后完成累加。其中,每個計算周期,隨着時間的推移,需要乘以衰減因子y進行一次衰減操作。

先來看一下每個調度實體的負載貢獻計算公式:

  • 當前時間點的負載貢獻 = 當前時間點負載 + 上個周期負載貢獻 * 衰減因子;
  • 假設一個調度實體被調度運行,運行時間段可以分成三個段d1/d2/d3,這三個段是被1024us的計算周期分割而成,period_contrib是調度實體last_update_time時在計算周期間的貢獻值,;
  • 總體的貢獻值,也是根據d1/d2/d3來分段計算,最終相加即可;
  • y為衰減因子,每隔1024us就乘以y來衰減一次;

計算的調用流程如下圖:

  • 函數主要是計算時間差,再分成d1/d2/d3來分段計算處理,最終更新相應的字段;
  • decay_load函數要計算val * y^n,內核提供了一張表來避免浮點運算,值存儲在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,
};

Linux中使用struct sched_avg來記錄調度實體和CFS運行隊列的負載信息,因此struct sched_entitystruct cfs_rq結構體中,都包含了struct sched_avg,字段介紹如下:

struct sched_avg {
	u64				last_update_time;       //上一次負載更新的時間,主要用於計算時間差;
	u64				load_sum;                   //可運行時間帶來的負載貢獻總和,包括等待調度時間和正在運行時間;
	u32				util_sum;                     //正在運行時間帶來的負載貢獻總和;
	u32				period_contrib;           //上一次負載更新時,對1024求余的值;
	unsigned long			load_avg;           //可運行時間的平均負載貢獻;
	unsigned long			util_avg;           //正在運行時間的平均負載貢獻;
};

4.2 PELT計算調用

PELT計算的發生時機如下圖所示:

  • 調度實體的相關操作,包括入列出列操作,都會進行負載貢獻的計算;

PELT的算法還在持續的改進中,各個內核版本也存在差異,大體的思路已經在上文中介紹到了,細節就不再深入分析了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM