Linux CFS调度器之虚拟运行时间vruntime

CFS简介


Linux中大名鼎鼎的CFS调度器是专门为普通进程设计的调度器,CFS调度器的核心是虚拟运行时间vruntime。虚拟运行时间一方面跟进程运行时间有关,另一方面跟进程优先级有关,总的来说,进程占用CPU时间越长,进程优先级越低,其vruntime就越大。vruntime越小,代表CPU对进程的宠幸越少,那么后续就需要优先宠幸它;反之,vruntime越大,代表CPU对进程的宠幸越多,那么后续就需要对它做出一定的惩罚,减少对它的宠幸。CFS调度器就是要在各个进程间维持一种相对的公平,既不能过多宠幸某个进程,也不多过分亏欠某个进程,所有进程同属于一个大家庭,任何一个进程受到亏欠,都会影响整个大家庭。

所有的普通进程以vruntime为键值加入红黑树,vruntime越小,在红黑树的位置越靠左,vruntime越大,在红黑树的位置越考右。CFS调度器从最左边的进程开始调度,进程被调度后,vruntime增加,进程因而向右移动。

更新虚拟运行时间


update_curr函数更新虚拟运行时间,更新虚拟运行时间的时机有很多,主要有以下几种:

  • 定时器tick中断中
 task_tick_fair->entity_tick(cfs_rq, se, queued);->update_curr(cfs_rq);
  • 进程入CFS调度队列的时候
enqueue_task_fair->enqueue_entity(cfs_rq, se, flags);->update_curr(cfs_rq);
  • 进程出CFS调度队列的时候
dequeue_task_fair->dequeue_entity(cfs_rq, se, flags);->update_curr(cfs_rq);
  • 进程创建的时候
task_fork_fair->update_curr(cfs_rq);

更新虚拟运行时间的函数如下,逻辑并不复杂,我们下面详细分析:

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

	if (unlikely(!curr))
		return;

	delta_exec = now - curr->exec_start;
	if (unlikely((s64)delta_exec <= 0))
		return;

	curr->exec_start = now;

	schedstat_set(curr->statistics.exec_max,
		      max(delta_exec, curr->statistics.exec_max));

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

	curr->vruntime += calc_delta_fair(delta_exec, curr);
	update_min_vruntime(cfs_rq);

	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);
	}

	account_cfs_rq_runtime(cfs_rq, delta_exec);
}
  • 计算当前进程两次执行该函数的时间间隔(jiffies)delta_exec
    delta_exec并不是当前进程占用CPU的时间,也不是当前进程休眠的时间,而是当前进程两次执行update_curr CPU所走过的时间。
  • 更新当前进程的exec_start
  • 更新总的执行时间sum_exec_runtime
    该时间也不是当前进程占用CPU的总的时间,而是CPU运行的总的时间。
  • 根据进程的权重调整delta_exec并加入到vruntime中
    在文章开头提到过,vruntime不仅与进程实际运行时间有关,与进程优先级有关,而进程的优先级跟进程的权重是一一对应的,进程优先级越高,其权重越大,那么其理性获得更多的CPU时间,vruntime的增速应该低于低优先级进程,关于这一点,我们再下一个章节详细讨论。
  • 更新min_vruntime
    从字面上来看,min_vruntime记录CFS运行队列上vruntime最小值,但是实际上min_vruntime只能单调递增,所以,如果当前进程vruntime比min_vruntime小,是不会更新min_vruntime的。那么min_vruntime的作用的是什么呢?试想一下如果一个进程睡眠了很长时间,则它的vruntime非常小,一旦它被唤醒,将持续占用CPU,很容易引发进程饥饿。CFS调度器会根据min_vruntime设置一个合适的vruntime值给被唤醒的进程,既要保证它能优先被调度,又要保证其他进程也能得到合理调度。

进程权重


进程的权重跟进程优先级是一一对应的。我们先来谈一谈为什么需要进程优先级?
如果没有进程优先级,或者说所有进程的优先级都是一样的,那么所有进程的vruntime增加的速度是一样的,进而获得的CPU时间片也是一样的。但是有些进程确实更加重要,任务更加繁重,理应获得更多的CPU时间,所以就引入了优先级的概念,优先级越高,就越应该受到CPU的优待,也即获得更多的CPU时间。所以高优先级进程的vruntime增加的速度应该要更慢一点。
Linux分给普通进程的优先级是100-139,其中100代表最高优先级,139代表最低优先级。优先级与权重的关系由下面的表定义:

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,
};

优先级100对应的权重是88761,优先级139对应的权重是15,由此可以看出,优先级越高,其权重越大。这里我们要提一下nice的概念,nice跟优先级也是一一对应的,优先级100对应的nice值是-20,优先级120对应的nice值是0,优先级139对应的优先级是20。

此外还有一张表,记录权重的倒数(2^32/权重)

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,
};

这主要是因为根据进程权重调整delta_exec的时候,是根据nice为0的进程的权重占当前进程权重的比重来计算的。

设置进程权重的地方

static void set_load_weight(struct task_struct *p)
{
	int prio = p->static_prio - MAX_RT_PRIO;
	struct load_weight *load = &p->se.load;

	/*
	 * SCHED_IDLE tasks get minimal weight:
	 */
	if (idle_policy(p->policy)) {
		load->weight = scale_load(WEIGHT_IDLEPRIO);
		load->inv_weight = WMULT_IDLEPRIO;
		return;
	}

	load->weight = scale_load(sched_prio_to_weight[prio]);
	load->inv_weight = sched_prio_to_wmult[prio];
}

进程创建,或者设置进程优先级的时候会调用该函数设置进程权重。

调整delta_exec

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

	return delta;
}

首先判断当前进程是否是nice为0的进程,如果是的话就直接返回delta。也就说调整delta是以nice为0的进程为基准的,即nice为0的进程的权重占当前进程的权重的比重来调整delta。nice小于0的进程权重小于NICE_0_LOAD,则比重大于1,那么delta会被调大;nice大于0的进程权重大于NICE_0_LOAD,则比重小于1,那么delta会被调小。

static u64 __calc_delta(u64 delta_exec, unsigned long weight, struct load_weight *lw)
{
	u64 fact = scale_load_down(weight);
	int shift = WMULT_SHIFT;

	__update_inv_weight(lw);

	if (unlikely(fact >> 32)) {
		while (fact >> 32) {
			fact >>= 1;
			shift--;
		}
	}

	/* hint to use a 32x32->64 mul */
	fact = (u64)(u32)fact * lw->inv_weight;

	while (fact >> 32) {
		fact >>= 1;
		shift--;
	}

	return mul_u64_u32_shr(delta_exec, fact, shift);
}

这段代码我们不再分析,它其实就是按下面的公式调试delta:

delta_exec * weight / lw.weight

或者

 (delta_exec * (weight * lw->inv_weight)) >> WMULT_SHIFT

你可能感兴趣的:(进程管理学习记录)