Linux內核源碼解析 - CFS調度算法

進程調度,那麼先從進程描述符的數據結構開始

struct task_struct {
	volatile long state;	/* -1 unrunnable, 0 runnable, >0 stopped */
	void *stack;
	atomic_t usage;
	unsigned int flags;	/* per process flags, defined below */
	unsigned int ptrace;

	int lock_depth;		/* BKL lock depth */

        ...

	int prio, static_prio, normal_prio;
	unsigned int rt_priority;
	const struct sched_class *sched_class;
	struct sched_entity se;
	struct sched_rt_entity rt;
    
        ...
}

在進程描述符中注意到sched_entity類型的se成員變量,這個是進程調度器的實體結構,同時我們也看到prio相關的參數(進程優先級)。我們來看下sched_entity的數據結構

struct sched_entity {
	struct load_weight	load;		/* for load-balancing */
	struct rb_node		run_node;
	struct list_head	group_node;
	unsigned int		on_rq;

	u64			exec_start;
	u64			sum_exec_runtime;
	u64			vruntime;
	u64			prev_sum_exec_runtime;

	u64			last_wakeup;
	u64			avg_overlap;

	u64			nr_migrations;

	u64			start_runtime;
	u64			avg_wakeup;

...
};

我們關注下其中一些重要的參數,後面會重點分析。 vruntime變量字面意思虛擬運行時間,可以理解成進程實際調度運行時間(以ms爲單位)的標準化處理的結果,作爲抽象出來的指標。下面來看其相關的操作。

static void update_curr(struct cfs_rq *cfs_rq)
{
	struct sched_entity *curr = cfs_rq->curr;
	u64 now = rq_of(cfs_rq)->clock;
	unsigned long delta_exec;

	if (unlikely(!curr))
		return;

	/*
	 * Get the amount of time the current task was running
	 * since the last time we changed load (this cannot
	 * overflow on 32 bits):
	 */
	delta_exec = (unsigned long)(now - curr->exec_start);
	if (!delta_exec)
		return;

	__update_curr(cfs_rq, curr, delta_exec);
	curr->exec_start = now;

	if (entity_is_task(curr)) {
		struct task_struct *curtask = task_of(curr);

		trace_sched_stat_runtime(curtask, delta_exec, curr->vruntime);
		cpuacct_charge(curtask, delta_exec);
		account_group_exec_runtime(curtask, delta_exec);
	}
}

首先通過cfs_rq獲得當前的進程的調度實體cur,再記錄下當前系統時間爲now,通過進程實體的統計量exec_start即進程剛被調用時刻,計算出當前進程實際運行時間delta_exec。通過_update_curr()子函數計算出當前delta_exec歸一化的結果,並更新對應的vruntime指標。

static inline void
__update_curr(struct cfs_rq *cfs_rq, struct sched_entity *curr,
	      unsigned long delta_exec)
{
	unsigned long delta_exec_weighted;

	schedstat_set(curr->exec_max, max((u64)delta_exec, curr->exec_max));

	curr->sum_exec_runtime += delta_exec;
	schedstat_add(cfs_rq, exec_clock, delta_exec);
	delta_exec_weighted = calc_delta_fair(delta_exec, curr);

	curr->vruntime += delta_exec_weighted;
	update_min_vruntime(cfs_rq);
}

# define schedstat_add(rq, field, amt)	do { (rq)->field += (amt); } while (0)

這裏統計了sum-exec_runtime、rq的field字段,我們可以看到vruntime的具體在calc_delta_fair()函數中求取。

static inline unsigned long
calc_delta_fair(unsigned long delta, struct sched_entity *se)
{
	if (unlikely(se->load.weight != NICE_0_LOAD))
		delta = calc_delta_mine(delta, NICE_0_LOAD, &se->load);

	return delta;
}

static unsigned long
calc_delta_mine(unsigned long delta_exec, unsigned long weight,
		struct load_weight *lw)
{
	u64 tmp;

	if (!lw->inv_weight) {
		if (BITS_PER_LONG > 32 && unlikely(lw->weight >= WMULT_CONST))
			lw->inv_weight = 1;
		else
			lw->inv_weight = 1 + (WMULT_CONST-lw->weight/2)
				/ (lw->weight+1);
	}

	tmp = (u64)delta_exec * weight;
	/*
	 * Check whether we'd overflow the 64-bit multiplication:
	 */
	if (unlikely(tmp > WMULT_CONST))
		tmp = SRR(SRR(tmp, WMULT_SHIFT/2) * lw->inv_weight,
			WMULT_SHIFT/2);
	else
		tmp = SRR(tmp * lw->inv_weight, WMULT_SHIFT);

	return (unsigned long)min(tmp, (u64)(unsigned long)LONG_MAX);
}

我們看到如果load.weight == NICE_0_LOAD,那麼實際運行實際與歸一化的結果相同,直接返回delta。否則將實際運行時間歸一化處理爲delta_exec_weighted直接加到原先統計的vruntime上。

在這裏需要提的是nice、weight、prio之間的關係,有助於我們理解,我們知道進程調度通過優先級來區分。其中NICE_0_LOAD即nice值爲0時對應的weight的值爲1024。

#define NICE_0_LOAD		SCHED_LOAD_SCALE
#define SCHED_LOAD_SHIFT	10
#define SCHED_LOAD_SCALE	(1L << SCHED_LOAD_SHIFT)

weight即sched_entity的數據結構中的load_weight數據結構。

struct load_weight {
	unsigned long weight, inv_weight;
};

我們通過代碼來具體看他們之間的關係

static const int 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,
};

static const u32 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,
};

這段數組體現了prio到weight和inv_的映射關係,下面這些代碼是我總結出來的他們之間的關係

lw->inv_weight = 2^32 / weight
p->static_prio = NICE_TO_PRIO(nice);

/*
 * Convert user-nice values [ -20 ... 0 ... 19 ]
 * to static priority [ MAX_RT_PRIO..MAX_PRIO-1 ],
 * and back.
 */
#define NICE_TO_PRIO(nice)	(MAX_RT_PRIO + (nice) + 20)
#define PRIO_TO_NICE(prio)	((prio) - MAX_RT_PRIO - 20)
#define TASK_NICE(p)		PRIO_TO_NICE((p)->static_prio)

p->se.load.weight = prio_to_weight[p->static_prio - MAX_RT_PRIO];
p->se.load.inv_weight = prio_to_wmult[p->static_prio - MAX_RT_PRIO];

其中MAX_RT_PRIO爲100,MAX_PRIO爲140,通過這些關係,把nice域線性映射到了prio域。

load_weight中的weight跟inv_weight關係:weight * inv_weight = 2 ^32,不禁要問爲什麼通過兩個數組打表的方式構造這樣的數據結構??我們繼續看下去會發現,這樣設計是爲了通過維護一個inv_wight反向變量方式,通過乘法跟位運算代替了歸一化計算需要用的除法操作。

回到核心的calc_delta_mine()方法中。因爲維護了inv_wight變量即作爲2 ^32/weight,那麼inv_weight爲0的話,即爲weight變量的值大於2^32的話,inv_weight需要維持一個最小變量1,參與後面的乘法操作。如果weight爲大於2^32,那麼通過

lw->inv_weight = 1 + (WMULT_CONST-lw->weight/2) / (lw->weight+1);

方式維護inv_weight的值,其中WMULT_CONST即爲2^32,其實我們化簡公式可以看到

lw->inv_weight ~= 1 + (WMULT_CONST / (lw->weight+1)) - 0.5(略小於0.5) ; 近似地維護了weight * inv_weight = 2 ^32關係

其中有一個很有意思的宏SSR

/*
 * Shift right and round:
 */
#define SRR(x, y) (((x) + (1UL << ((y) - 1))) >> (y))

化成我們看得懂的式子 (x+0.5*2^y)/2^y ,目的應該明確了即(x/2^y)的結果四捨五入處理。

看了這麼多總結一下

delat_vruntime = (delta_exec * nice_0_weight * lw->inv_weight) / (2^32)

到這裏算是vruntime的計算過程清晰了。

其中update_curr()會被系統定時週期性且在多個地方調用,無論是進程處於可運行態,還是被堵塞處於不可運行態。後面調度系統會根據計算出的每個進程控制器的vruntime爲依據進行優先隊列的選擇,然後“公平調度”。這一塊內容被稱爲時間記賬。本質上不過是維護了歸一化處理的調度時長,以此爲依據供進程調度選擇。

關於紅黑樹,只是插入跟刪除操作,取出最左邊的進程節點、然後執行、在這之後會更新它的vruntime,等重新調度的時候,插入紅黑樹進行排序調整,需要理解的是紅黑樹中維護的都是就緒態的進程。我們只需維護最左邊節點的指針,這樣取操作時間複雜度O(1)、插入節點操作O(lgn)

個人感覺進程調度這塊跟任務調度類似、只不過根據不同的需求考慮的點不一樣罷了

提供數據結構:紅黑樹、堆、數組(桶排序、時間輪,這個個人認爲是最有意思的點、用的好時間複雜度O1)

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章