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就緒隊列中進程數確定一個最長時間間隔,然后看在該時間間隔內當前進程按照權重比例執行