重学计算机(十八、CFS完全公平调度器<上:虚拟运行时间和红黑树>)

没想到这篇文章成为跨年之作,而且还是写的第二次的,确实对这个CFS完全公平调度器不是很了解,经过上一篇的准备,重新写已经有把握一点吧。

18.1 虚拟运行时间

18.1.1 unix系统时间片

我们先来认识一下传统Unix系统的调度过程。现代调度器都有两个通用的概念:进程优先级和时间片。

时间片指进程运行多少时间后,就会切换到另一个进程,进程一旦启动就会默认分配一个时间片。

具有更高优先级的进程将运行得更频繁,而且也会赋予更多的时间。

听起来比较简单,下面我们就来看看有什么缺点。

缺点如下:

  1. 优先级失效。比如有两个进程,进程A的nice是0,分配的时间片为100ms,进程B的nice是+20,分配的时间片为5ms。如果我们的时间片为5ms,在进程A运行完5ms之后,就会切换到进程B运行,进程B也会运行5ms,所以在10ms之内,进程A和进程B运行得时间是相等的,以此类推,100ms内,进程A和进程B运行得时间也是相等的,这个明显跟我们分配的时间片不一致。(应该是这样理解的,看了好几遍没看懂)
  2. 相对nice值。比如有两个进程,第一个假设nice值为0,第二个假设nice值为1。这两个进程会分别分配到100ms和95ms的时间片。但是如果两个进程的nice值分别为18和19,那他们分别分配的时间为10ms和5ms。这样对整个cpu的效率都比较低。(CFS的做法就是相对nice值,18和19也是分配100ms和95ms)
  3. 绝对时间片需要在定时器节拍上。到了时间片,需要切换进程,这时候,需要定时器一个中断来触发,这样子的话,两个进程的时间片就分不了那么细,因为要满足定时器节拍。(可以在使用另一个值来计算,作为跟定时器节拍分离)
  4. 基于优先级调整问题。为了进程能更快的投入运行,而去对新唤醒的进程提升优先级。

上面的问题说实话我也没遇到过,哈哈哈。都是从书上抄过来,因为都说CFS完全公平调度器牛逼,但是没有以前的缺点怎么能体现这个调度算法的牛逼了,绿叶衬红花,所以就把这个给写出来,下面的介绍,主要是介绍CFS完全公平调度器。

18.1.2 linux虚拟运行时间

linux引入虚拟运行时间来解决这个问题。我们继续假设运行队列上有两个进程需要调度,nice值分别为0与5,两者权重比是3:1(不知道权重,可以回到优先级那篇查看重学计算机(十六、linux系统优先级))。

调度周期为12ms,这个值也可以回到这一篇查看重学计算机(十六、linux系统优先级),那么按照公式,第一个进程运行时间为9ms,第二个进程运行3ms。

但是我们完全公平调度器的原则是希望两个进程的虚拟运行时间是一样的,这才符合公平的原则。

那怎么搞成一样呢?其实是有一个公式。
加 权 运 行 时 间 = 真 实 运 行 时 间 ∗ N I C E _ 0 _ L O A D 进 程 权 重 加权运行时间 = 真实运行时间 * \frac {NICE\_0\_LOAD}{进程权重} =NICE_0_LOAD
然后我们之前计算的真实运行时间:
分 配 给 进 程 的 时 间 ( 真 实 运 行 时 间 ) = 调 度 周 期 ∗ 进 程 权 重 就 绪 进 程 权 重 之 和 分配给进程的时间(真实运行时间) = \frac {调度周期 * 进程权重}{就绪进程权重之和} ()=
把这个公式写入上面的式子:
加 权 运 行 时 间 ( 虚 拟 运 行 时 间 ) = 调 度 周 期 ∗ 进 程 权 重 就 绪 进 程 权 重 之 和 ∗ N I C E _ 0 _ L O A D 进 程 权 重 = 调 度 周 期 ∗ N I C E _ 0 _ L O A D 就 绪 进 程 权 重 之 和 加权运行时间(虚拟运行时间) = \frac {调度周期 * 进程权重}{就绪进程权重之和} * \frac {NICE\_0\_LOAD}{进程权重} = \frac {调度周期 * NICE\_0\_LOAD}{就绪进程权重之和} ()=NICE_0_LOAD=NICE_0_LOAD
计算出的结果就跟优先级无关了,不同优先级得到的虚拟运行时间也是一样的。这才是公平调度的意义。

继续按上面的例子计算一下:

第一个进程运行时间为9ms,第二个进程运行3ms。

第一个进程的虚拟运行时间为:9ms*(1024/1024) = 9ms

第二个进程的虚拟运行时间为:3ms * (1024/355) = 9ms。

18.1.3 计算进程实际运行时间片函数sched_slice()

我们先来看一下代码是怎么实现的:

/*
 * 我们通过权重成比例的部分来计算时间片。
 *
 * s = p*P[w/rw]
 */
static u64 sched_slice(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
    // 这个函数内部实现,我们会很熟悉,计算进程的调度周期
	u64 slice = __sched_period(cfs_rq->nr_running + !se->on_rq);

    // linux支持组调度,所以此处有一个循环,如果不考虑组调度,这个只是一个进程
	for_each_sched_entity(se) {
		struct load_weight *load;
		struct load_weight lw;

		cfs_rq = cfs_rq_of(se);
		load = &cfs_rq->load;   // cfs总的负载

		if (unlikely(!se->on_rq)) {   // on_rq大于0是在对列中,如果在对列中就更新,unlikely是制定cpu流水线的unlikely代码逻辑假的概率大,也就是在对列中概率比较大
			lw = cfs_rq->load;
		// 更新cfs_rq上所有调度实体的负载总和
			update_load_add(&lw, se->load.weight);   
			load = &lw;		 // 总负载
		}
        // 根据调度实体所占的权重,分配时间片的大小
        // slice:进程调度周期
        // se->load.weight:进程的负载
        // load:总负载
		slice = __calc_delta(slice, se->load.weight, load);
	}
	return slice;
}

18.1.4 计算进程调度周期__sched_period()

我们先来看看这个函数__sched_period:

unsigned int sysctl_sched_latency = 6000000ULL;
unsigned int normalized_sysctl_sched_latency = 6000000ULL;

unsigned int sysctl_sched_min_granularity = 750000ULL;
unsigned int normalized_sysctl_sched_min_granularity = 750000ULL;

/*
 * is kept at sysctl_sched_latency / sysctl_sched_min_granularity
 */
static unsigned int sched_nr_latency = 8;

/*
	 其思想是设置每个任务运行一次的周期。
	 当有太多任务(sched_nr_latency)时,我们必须延长这段时间,否则片就会变得太小
	 p = (nr <= nl) ? l : l*nr/nl
*/
static u64 __sched_period(unsigned long nr_running)
{
	if (unlikely(nr_running > sched_nr_latency))   // 如果运行队列中的进程进程大于8
		return nr_running * sysctl_sched_min_granularity;   // 进程个数*最小调度事件
	else
		return sysctl_sched_latency;   // 直接返回调度周期
}

代码跟之前分析的一样,如果小于8就直接返回调度周期,如果大于8就是返回进程个数*最小周期。但是这个值跟我在/porc看到的不一样,有点奇怪,先保留观点。(有可能是版本不一样)

18.1.5 具体计算时间片函数__calc_delta()

接下来就看看怎么计算时间片的:

/*
* 这个公式就是这个函数主要的内容:分配给进程的运行时间=调度周期 * 进程权重 / 所有进程权重之和
 * delta_exec * weight / lw.weight
 *   OR
 * 后面这条公式是内容为了快速计算,因为除运算比慢,转化成乘运算会快很多。
 * (delta_exec * (weight * lw->inv_weight)) >> WMULT_SHIFT
 *
 */
//__calc_delta(slice, se->load.weight, load);
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);
}

看不懂,竟然不是用浮点运行,不过也是,在内核中的代码效率比较高的,用移位来替换浮点运行,确实很靠谱。反正我看不懂,哈哈哈哈。

18.1.6 计算虚拟运行时间片calc_delta_fair()

通过上面的计算是不是很奇怪,为啥计算的都是进程实际运行的时间片,我们内核不是都是使用虚拟运行时间的么,下面我们来看这个函数就明白了:

/*
 * delta /= w
 * delta:真实运行时间
 * se:调度实体(一个进程)
 */
static inline u64 calc_delta_fair(u64 delta, struct sched_entity *se)
{
	if (unlikely(se->load.weight != NICE_0_LOAD))   // 如果这个进程的负载不是NICE_0_LOAD
		delta = __calc_delta(delta, NICE_0_LOAD, &se->load);  // 就重新计算一下

	return delta;
}

是不是都很怀疑这个传参怎么是NICE_0_LOAD, &se->load,那这样传参计算的是不是我们的虚拟运行时间呢?
加 权 运 行 时 间 ( 虚 拟 运 行 时 间 ) = 真 实 运 行 时 间 ∗ N I C E _ 0 _ L O A D 进 程 权 重 加权运行时间(虚拟运行时间) = 真实运行时间 * \frac {NICE\_0\_LOAD}{进程权重} ()=NICE_0_LOAD
我们这个delta参数其实是真实运行时间,等会我们看看调用的地方就知道了,然后我们再看公式,是不是就符合__calc_delta这个函数的意思,忘记的可以往上回顾一下,这个函数是怎么求的。

18.1.7 计算运行虚拟运行时间片sched_vslice()

除了上面那个函数是计算虚拟运行时间片,这个函数也是计算虚拟运行时间片的。

/*
 * We calculate the vruntime slice of a to-be-inserted task.
 我们计算一个要插入的任务的vruntime段。
 *
 * vs = s/w
 */
static u64 sched_vslice(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
	return calc_delta_fair(sched_slice(cfs_rq, se), se);   // 这个把计算出的真实运行时间传入
}

这个函数就比较狠,直接把计算出真实运行时间作为参数直接传入,太狠了。反正也是这个意思。

18.1.8 更新进程虚拟运行时间update_curr()

写的第一次把这个函数介绍放在后面了,这一次的话,结果还是放在里比较好,这一次,打算把全部的基础函数都介绍完,然后再介绍整体吧。来一次倒叙:

/*
 * Update the current task's runtime statistics.
 */
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);  // 也是统计cfs队列上运行时间 exec_clock

	/* 这个就是我们刚刚看的计算虚拟运行时间:calc_delta_fair计算出加权后运行时间 */
	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);
}

18.2 红黑树

linux内核为了加快查找虚拟运行时间,所以把虚拟运行时间用红黑树来存储,我们简单来看一下内核的红黑树的实现:

18.2.1 红黑树根结点

上一篇介绍了cfs_rq :是完全公平调度器的就绪队列,这个队列里就有红黑树的根结点,维护着完全公平调度器就绪的调度实体。

/* CFS-related fields in a runqueue */
struct cfs_rq {
	struct load_weight load;   // cfs队列总负载
    // nr_running : cfs_rq上所有的调度实体不含调度组中的调度实体
    // h_nr_running : cfs_rq上所有的调度实体包含调度组中所有调度实体
	unsigned int nr_running, h_nr_running;

	u64 exec_clock;		// 这个也是统计数据
	u64 min_vruntime;	// 最小虚拟运行时间
#ifndef CONFIG_64BIT
	u64 min_vruntime_copy;
#endif

	struct rb_root tasks_timeline;	// 这个就是红黑树的根结点
	struct rb_node *rb_leftmost;   // 缓存最左边的值?也就是最小值

	/*
	 * 'curr' points to currently running entity on this cfs_rq.
	 * It is set to NULL otherwise (i.e when none are currently running).
	 */
    // curr : 当前调度实体
    // next : 下一个调度实体
    // last : 上次执行过的调度实体
	struct sched_entity *curr, *next, *last, *skip;
};

上一篇其实也介绍了这个结构体,这一篇再重温一下。

18.2.2 红黑树根结点初始化

红黑树根结点的初始化是在cfs队列初始化中完成的

// RB_ROOT的定义
#define RB_ROOT	(struct rb_root) { NULL, }

void init_cfs_rq(struct cfs_rq *cfs_rq)
{
	cfs_rq->tasks_timeline = RB_ROOT;   // 就是这样简单
	cfs_rq->min_vruntime = (u64)(-(1LL << 20));   // 最开始的最小虚拟运行时间
#ifndef CONFIG_64BIT
	cfs_rq->min_vruntime_copy = cfs_rq->min_vruntime;
#endif
#ifdef CONFIG_SMP
	atomic_long_set(&cfs_rq->removed_load_avg, 0);
	atomic_long_set(&cfs_rq->removed_util_avg, 0);
#endif
}

18.2.3 红黑树结点

红黑树的结点是需要重点介绍的,其实这个红黑树的结点就是我们上一节说的调度实体:

// 这一次再拷贝一次之后,对调度实体又有更深一层的理解了,不容易
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			nr_migrations;

#ifdef CONFIG_SCHEDSTATS
	struct sched_statistics statistics;		//统计信息包含进程的睡眠统计、等待延迟统计、CPU迁移统计、唤醒统计等。
#endif

#ifdef CONFIG_SMP
	/* Per entity load average tracking */
	struct sched_avg	avg;
#endif
};

其实这一个上一篇也介绍了,不过这里再次拷贝一下,表示这个结构体是真的很重要。

18.2.4 红黑树插入节点函数__enqueue_entity()

红黑树插入节点函数:

/*
 * Enqueue an entity into the rb-tree:
 * 红黑树插入
 */
static void __enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
	struct rb_node **link = &cfs_rq->tasks_timeline.rb_node;  // 根结点
	struct rb_node *parent = NULL;		// 这个缓存父节点的指针
	struct sched_entity *entry;
	int leftmost = 1;

	/*
	 * Find the right place in the rbtree:
	 */
	while (*link) {
		parent = *link;
		entry = rb_entry(parent, struct sched_entity, run_node);   // 返回结点
		/*
		 * We dont care about collisions. Nodes with
		 * the same key stay together.
		 我们不关心碰撞。具有相同键的节点保持在一起。
		 */
		if (entity_before(se, entry)) {   // 比较
			link = &parent->rb_left;		// 小于往左边走
		} else {
			link = &parent->rb_right;
			leftmost = 0;
		}
	}

	/*
	 * Maintain a cache of leftmost tree entries (it is frequently
	 * used):
	 维护最左边树条目的缓存(经常使用):
	 */
	if (leftmost)    // 如果新插入结点是最小的,就赋值给rb_leftmost
		cfs_rq->rb_leftmost = &se->run_node;

	rb_link_node(&se->run_node, parent, link);    // 这个是在初始化 se->run_node
	rb_insert_color(&se->run_node, &cfs_rq->tasks_timeline);	// 这个就是插入了,细节就不看了,以后有机会可以分析一下内核的红黑树实现
}

18.2.5 红黑树删除节点函数__dequeue_entity()

static void __dequeue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
	if (cfs_rq->rb_leftmost == &se->run_node) {		// 如果删除的结点是最小节点,需要更新
		struct rb_node *next_node;

		next_node = rb_next(&se->run_node);			// 查找下一个结点
		cfs_rq->rb_leftmost = next_node;			// 更新最小的结点
	}

	rb_erase(&se->run_node, &cfs_rq->tasks_timeline);	// 删除红黑树的结点
}

18.2.6 获取第一个节点__pick_first_entity()

struct sched_entity *__pick_first_entity(struct cfs_rq *cfs_rq)
{
	struct rb_node *left = cfs_rq->rb_leftmost;

	if (!left)
		return NULL;

	return rb_entry(left, struct sched_entity, run_node);
}

这个就更狠了,直接获取rb_leftmost的值就行了,残暴。

18.2.7 获取下一个节点__pick_next_entity()

static struct sched_entity *__pick_next_entity(struct sched_entity *se)
{
	struct rb_node *next = rb_next(&se->run_node);

	if (!next)
		return NULL;

	return rb_entry(next, struct sched_entity, run_node);
}

也不知道获取下一个节点有什么用,就先这样吧。

18.2.8 更新最小虚拟时间update_min_vruntime()

这个就有点作用了,其实在上一节的update_curr函数中就有调用了,不过那时候还没分析红黑树,就觉得先放一放,现在的时机就不错了,直接上。

static void update_min_vruntime(struct cfs_rq *cfs_rq)
{
	u64 vruntime = cfs_rq->min_vruntime;

	if (cfs_rq->curr)
		vruntime = cfs_rq->curr->vruntime;   // 如果有当前进程,获取当前进程的虚拟运行时间
	
	if (cfs_rq->rb_leftmost) {   // 如果有最小的
		struct sched_entity *se = rb_entry(cfs_rq->rb_leftmost,
						   struct sched_entity,
						   run_node);

		if (!cfs_rq->curr)
			vruntime = se->vruntime;   // 如果没有当前运行调度实体,就直接赋值最小的
		else
			vruntime = min_vruntime(vruntime, se->vruntime);   // 如果有当前实体,就比较一个最小的 
	}

	/* ensure we never gain time by being placed backwards.
    确保我们永远不会因为被放置在后面而而获取不到时间
    这个就有点奇怪了,最后赋值的是一个最大值*/
	cfs_rq->min_vruntime = max_vruntime(cfs_rq->min_vruntime, vruntime);
#ifndef CONFIG_64BIT
	smp_wmb();
	cfs_rq->min_vruntime_copy = cfs_rq->min_vruntime;
#endif
}

红黑树就介绍到这里。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值