1. 程式人生 > >Linux CFS排程器之負荷權重load_weight--Linux程序的管理與排程(二十五)

Linux CFS排程器之負荷權重load_weight--Linux程序的管理與排程(二十五)

1. 負荷權重

1.1 負荷權重結構struct load_weight

負荷權重用struct load_weight資料結構來表示, 儲存著程序權重值weight。其定義在/include/linux/sched.h, v=4.6, L1195, 如下所示

struct load_weight {
    unsigned long weight;       /*  儲存了權重的資訊  */
    u32 inv_weight;                 /*   儲存了權重值用於重除的結果 weight * inv_weight = 2^32  */
};

1.2 排程實體的負荷權重load

既然struct load_weight儲存著程序的權重資訊, 那麼作為程序排程的實體, 必須將這個權重值與特定的程序task_struct, 更一般的與通用的排程實體sched_entity相關聯

struct sched_entity作為程序排程的實體資訊, 其內建了load_weight結構用於儲存當前排程實體的權重, 參照http://lxr.free-electrons.com/source/include/linux/sched.h?v=4.6#L1195

struct task_struct
{
    /*  ......  */
    struct sched_entity se;
    /*  ......  */
}

因此我們就可以通過task_statuct->se.load獲取負荷權重的資訊, 而set_load_weight負責根據程序型別及其靜態優先順序計算符合權重.

2 優先順序和權重的轉換

2.1 優先順序->權重轉換表

一般這個概念是這樣的, 程序每降低一個nice值(優先順序提升), 則多獲得10%的CPU時間, 沒升高一個nice值(優先順序降低), 則放棄10%的CPU時間.

為執行該策略, 核心需要將優先順序轉換為權重值, 並提供了一張優先順序->權重轉換表sched_prio_to_weight, 核心不僅維護了負荷權重自身, 還儲存另外一個數值, 用於負荷重除的結果, 即sched_prio_to_wmult陣列, 這兩個陣列中的資料是一一對應的.

其中相關的資料結構定義在kernel/sched/sched.h?v=4.6, L1132

//   http://lxr.free-electrons.com/source/kernel/sched/sched.h?v=4.6#L1132
/*
 * To aid in avoiding the subversion of "niceness" due to uneven distribution
 * of tasks with abnormal "nice" values across CPUs the contribution that
 * each task makes to its run queue's load is weighted according to its
 * scheduling class and "nice" value. For SCHED_NORMAL tasks this is just a
 * scaled version of the new time slice allocation that they receive on time
 * slice expiry etc.
 */


#define WEIGHT_IDLEPRIO                3              /*  SCHED_IDLE程序的負荷權重  */
#define WMULT_IDLEPRIO         1431655765   /*  SCHED_IDLE程序負荷權重的重除值  */


extern const int sched_prio_to_weight[40];
extern const u32 sched_prio_to_wmult[40];


// http://lxr.free-electrons.com/source/kernel/sched/core.c?v=4.6#L8484
/*
* Nice levels are multiplicative, with a gentle 10% change for every
* nice level changed. I.e. when a CPU-bound task goes from nice 0 to
* nice 1, it will get ~10% less CPU time than another CPU-bound task
* that remained on nice 0.
*
* The "10% effect" is relative and cumulative: from _any_ nice level,
* if you go up 1 level, it's -10% CPU usage, if you go down 1 level
* it's +10% CPU usage. (to achieve that we use a multiplier of 1.25.
* If a task goes up by ~10% and another task goes down by ~10% then
* the relative distance between them is ~25%.)
*/
const int sched_prio_to_weight[40] = {
/* -20 */     88761,     71755,     56483,     46273,     36291,
/* -15 */     29154,     23254,     18705,     14949,     11916,
/* -10 */      9548,      7620,      6100,      4904,      3906,
/*  -5 */      3121,      2501,      1991,      1586,      1277,
/*   0 */      1024,       820,       655,       526,       423,
/*   5 */       335,       272,       215,       172,       137,
/*  10 */       110,        87,        70,        56,        45,
/*  15 */        36,        29,        23,        18,        15,
};


/*
* Inverse (2^32/x) values of the sched_prio_to_weight[] array, precalculated.
*
* In cases where the weight does not change often, we can use the
* precalculated inverse to speed up arithmetics by turning divisions
* into multiplications:
*/
const u32 sched_prio_to_wmult[40] = {
/* -20 */     48388,     59856,     76040,     92818,    118348,
/* -15 */    147320,    184698,    229616,    287308,    360437,
/* -10 */    449829,    563644,    704093,    875809,   1099582,
/*  -5 */   1376151,   1717300,   2157191,   2708050,   3363326,
/*   0 */   4194304,   5237765,   6557202,   8165337,  10153587,
/*   5 */  12820798,  15790321,  19976592,  24970740,  31350126,
/*  10 */  39045157,  49367440,  61356676,  76695844,  95443717,
/*  15 */ 119304647, 148102320, 186737708, 238609294, 286331153,
};

對核心使用的範圍[-20, 19]中的每個nice級別, sched_prio_to_weight陣列都有一個對應項

nice [-20, 19] -=> 下標 [0, 39]

而由於權重weight 用unsigned long 表示, 因此核心無法直接儲存1/weight, 而必須藉助於乘法和位移來執行除法的技術. sched_prio_to_wmult陣列就儲存了這些值, 即sched_prio_to_wmult每個元素的值是2^32/prio_to_weight$每個元素的值.

可以驗證

同時我們可以看到其定義了兩個巨集WEIGHT_IDLEPRIO和WMULT_IDLEPRIO這兩個巨集對應的就是SCHED_IDLE排程的程序的負荷權重資訊, 因為要保證SCHED_IDLE程序的最低優先順序和最低的負荷權重. 這點資訊我們可以在後面分析set_load_weight函式的時候可以看到

可以驗證

3.2 linux-4.4之前的shced_prio_to_weight和sched_prio_to_wmult

關於優先順序->權重轉換表sched_prio_to_weight

在linux-4.4之前的核心中, 優先順序權重轉換表用prio_to_weight表示, 定義在kernel/sched/sched.h, line 1116, 與它一同定義的還有prio_to_wmult, 在kernel/sched/sched.h, line 1139均被定義為static const

但是其實這種能夠方式不太符合規範的編碼風格, 因此常規來說, 我們的標頭檔案中不應該儲存結構的定義, 即為了是程式的模組結構更加清晰, 標頭檔案中儘量只包含巨集或者宣告, 而將具體的定義, 需要分配儲存空間的程式碼放在原始檔中.

否則如果在標頭檔案中定義全域性變數,並且將此全域性變數賦初值,那麼在多個引用此標頭檔案的C檔案中同樣存在相同變數名的拷貝,關鍵是此變數被賦了初值,所以編譯器就會將此變數放入DATA段,最終在連線階段,會在DATA段中存在多個相同的變數,它無法將這些變數統一成一個變數,也就是僅為此變數分配一個空間,而不是多份空間,假定這個變數在標頭檔案沒有賦初值,編譯器就會將之放入BSS段,聯結器會對BSS段的多個同名變數僅分配一個儲存空間

因此在新的核心中, 核心黑客們將這兩個變數存放在了kernel/sched/core.c, 並加上了sched_字首, 以表明這些變數是在程序排程的過程中使用的, 而在kernel/sched/sched.h, line 1144中則只包含了他們的宣告.

下面我們列出優先順序權重轉換表定義更新後對比項

核心版本 實現 地址
<= linux-4.4 static const int prio_to_weight[40] kernel/sched/sched.h, line 1116
>=linux-4.5 const int sched_prio_to_weight[40] 宣告在kernel/sched/sched.h, line 1144, 定義在kernel/sched/core.c

其定義並沒有發生變化, 依然是一個一對一NICE to WEIGHT的轉換表

3.3 1.25的乘積因子

各陣列之間的乘積因子是1.25. 要知道為何使用該因子, 可考慮下面的例子

兩個程序A和B在nice級別0, 即靜態優先順序120執行, 因此兩個程序的CPU份額相同, 都是50%, nice級別為0的程序, 查其權重表可知是1024. 每個程序的份額是1024/(1024+1024)=0.5, 即50%

如果程序B的優先順序+1(優先順序降低), 成為nice=1, 那麼其CPU份額應該減少10%, 換句話說程序A得到的總的CPU應該是55%, 而程序B應該是45%. 優先順序增加1導致權重減少, 即1024/1.25=820, 而程序A仍舊是1024, 則程序A現在將得到的CPU份額是1024/(1024+820=0.55, 而程序B的CPU份額則是820/(1024+820)=0.45. 這樣就正好產生了10%的差值.

4 程序負荷權重的計算

set_load_weight負責根據非實時程序型別極其靜態優先順序計算符合權重

而實時程序不需要CFS排程, 因此無需計算其負荷權重值

早期的程式碼中實時程序也是計算其負荷權重的, 但是隻是採用一些方法保持其權重值較大

在早期有些版本中, set_load_weight中實時程序的權重是普通程序的兩倍, 後來又設定成0, 直到後來linux-2.6.37開始不再設定實時程序的優先順序, 因此這本身就是一個無用的工作
而另一方面, SCHED_IDLE程序的權值總是非常小, 普通非實時程序則根據其靜態優先順序設定對應的負荷權重

4.1 set_load_weight依據靜態優先順序設定程序的負荷權重

static void set_load_weight(struct task_struct *p)
{
    /*  由於陣列中的下標是0~39, 普通程序的優先順序是[100~139]
         因此通過static_prio - MAX_RT_PRIO將靜態優先順序轉換成為陣列下標
    */
    int prio = p->static_prio - MAX_RT_PRIO;
    /*  取得指向程序task負荷權重的指標load,
         下面修改load就是修改程序的負荷權重  */
    struct load_weight *load = &p->se.load;

    /*
     * SCHED_IDLE tasks get minimal weight:
     * 必須保證SCHED_IDLE程序的負荷權重最小
     * 其權重weight就是WEIGHT_IDLEPRIO
     * 而權重的重除結果就是WMULT_IDLEPRIO
     */
    if (p->policy == SCHED_IDLE) {
        load->weight = scale_load(WEIGHT_IDLEPRIO);
        load->inv_weight = WMULT_IDLEPRIO;
        return;
    }

    /*  設定程序的負荷權重weight和權重的重除值inv_weight  */
    load->weight = scale_load(prio_to_weight[prio]);
    load->inv_weight = prio_to_wmult[prio];
}

4.2 scale_load取得負荷權重的值

其中scale_load是一個巨集, 定義在include/linux/sched.h, line 785

#if 0 /* BITS_PER_LONG > 32 -- currently broken: it increases power usage under light load  */
# define SCHED_LOAD_RESOLUTION  10
# define scale_load(w)          ((w) << SCHED_LOAD_RESOLUTION)
# define scale_load_down(w)     ((w) >> SCHED_LOAD_RESOLUTION)
#else
# define SCHED_LOAD_RESOLUTION  0
# define scale_load(w)          (w)
# define scale_load_down(w)     (w)
#endif

我們可以看到目前版本的scale_load其實什麼也沒做就是簡單取了個值, 但是我們注意到負荷權重仍然保留了SCHED_LOAD_RESOLUTION不為0的情形, 只不過目前因為效率原因和功耗問題沒有啟用而已

4.3 set_load_weight的演變

linux核心的排程器經過了不同階段的發展, 但是即使是同一個排程器其演算法也不是一成不變的, 也在不停的改進和優化

核心版本 實現 地址
2.6.18~2.6.22 實時程序的權重用RTPRIO_TO_LOAD_WEIGHT(p->rt_priority);轉換 kernel/sched.c#L746
2.6.23~2.6.34 實時程序的權重為非實時權重的二倍 kernel/sched.c#L1836
2.6.35~2.6.36 實時程序的權重設定為0, 重除值設定為WMULT_CONST kernel/sched.c, L1859
2.6.37~至今4.6 實時程序不再設定權重 其中<= linux-3.2時, 程式碼在sched.c中
3.3~4.4之後, 增加了sched/core.c檔案排程的核心程式碼在此存放
4.5~至今, 修改prio_to_weight為sched_prio_to_weight, 並將宣告存放標頭檔案中

5 就緒佇列的負荷權重

不僅程序, 就緒佇列也關聯到一個負荷權重. 這個我們在前面講Linux程序排程器的設計–Linux程序的管理與排程(十七)的時候提到過了在cpu的就緒佇列rq和cfs排程器的就緒佇列cfs_rq中都儲存了其load_weight.

這樣不僅確保就緒佇列能夠跟蹤記錄有多少程序在執行, 而且還能將程序的權重新增到就緒佇列中.

5.1 cfs就緒佇列的負荷權重

//  http://lxr.free-electrons.com/source/kernel/sched/sched.h?v=4.6#L596
struct rq
{
    /*  ......  */
    /* capture load from *all* tasks on this cpu: */
    struct load_weight load;
    /*  ......  */
};

//  http://lxr.free-electrons.com/source/kernel/sched/sched.h?v=4.6#L361
/* CFS-related fields in a runqueue */
struct cfs_rq
{
    struct load_weight load;
    unsigned int nr_running, h_nr_running;
    /*  ......  */
};

//  http://lxr.free-electrons.com/source/kernel/sched/sched.h?v=4.6#L596
struct rt_rq中不需要負荷權重

//  http://lxr.free-electrons.com/source/kernel/sched/sched.h?v=4.6#L490
struct dl_rq中不需要負荷權重

由於負荷權重僅用於排程普通程序(非實時程序), 因此只在cpu的就緒佇列佇列rq和cfs排程器的就緒佇列cfs_rq上需要儲存其就緒佇列的資訊, 而實時程序的就緒佇列rt_rq和dl_rq 是不需要儲存負荷權重的.

5.2 就緒佇列的負荷權重計算

就緒佇列的負荷權重儲存的其實就是佇列上所有程序的負荷權重的總和, 因此每次程序被加到就緒佇列的時候, 就需要在就緒佇列的負荷權重中加上程序的負荷權重, 同時由於就緒佇列的不是一個單獨被排程的實體, 也就不需要優先順序到負荷權重的轉換, 因而其不需要負荷權重的重除欄位, 即inv_weight = 0;

因此程序從就緒佇列上入隊或者出隊的時候, 就緒佇列的負荷權重就加上或者減去程序的負荷權重, 但是

//struct load_weight {
    /*  就緒佇列的負荷權重 +/-   入隊/出隊程序的負荷權重  */
    unsigned long weight +/- task_struct->se->load->weight;
    /*  就緒佇列負荷權重的重除欄位無用途,所以始終置0  */
    u32 inv_weight = 0;
//};

因此核心為我們提供了增加/減少/重置就緒佇列負荷權重的的函式, 分別是update_load_add, update_load_sub, update_load_set

/* 使得lw指向的負荷權重的值增加inc, 用於程序進入就緒佇列時呼叫
 *  程序入隊    account_entity_enqueue    kernel/sched/fair.c#L2422
 */

static inline void update_load_add(struct load_weight *lw, unsigned long inc)
{
    lw->weight += inc;
    lw->inv_weight = 0;
}

/*  使得lw指向的負荷權重的值減少inc, 用於程序調出就緒佇列時呼叫
 *  程序出隊    account_entity_dequeue    kernel/sched/fair.c#L2422*/
static inline void update_load_sub(struct load_weight *lw, unsigned long dec)
{
    lw->weight -= dec;
    lw->inv_weight = 0;
}

static inline void update_load_set(struct load_weight *lw, unsigned long w)
{
    lw->weight = w;
    lw->inv_weight = 0;
}
函式 描述 呼叫時機 定義位置 呼叫位置
update_load_add 使得lw指向的負荷權重的值增加inc 用於程序進入就緒佇列時呼叫 kernel/sched/fair.c, L117 account_entity_enqueue兩處, sched_slice
update_load_sub 使得lw指向的負荷權重的值減少inc 用於程序調出就緒佇列時呼叫 update_load_sub, L123 account_entity_dequeue兩處
update_load_set

其中sched_slice函式計算當前程序在排程延遲內期望的執行時間, 它根據cfs就緒佇列中程序數確定一個最長時間間隔,然後看在該時間間隔內當前程序按照權重比例執行