WALT 调度与负载计算

WALT负载

top task load

本质上是根据busytime路径计算出来的任务负载得到的一个值,计算的方法也类似demand路径中用到的桶算法。【WALT】top task 相关代码详解 

任务负载

在WALT算法中有两个路径计算任务负载,分别是 update_task_demand 和 update_cpu_busy_time。我们简单称之为 demand 路径与 busytime 路径。

Demand 路径

update_task_demand()会统计任务在当前WALT窗口中的runtime,通过scale_exec_time()对runtime进行归一化处理,称归一化后的runtime为负载,然后统计出任务在每个窗口的负载,在窗口翻滚(rollover)时通过update_hisroty()保存进数组sum_history中。sum_history[RAVG_HIST_SIZE]保存了历史RAVG_HIST_SIZE个窗口的负载。

  • p->ravg.demand:在update_hisroty()中根据数据 sum_history 中保存的历史窗口负载对下一个窗口的负载进行预测,预测结果为demand。demand主要用于选核;

  • p->ravg.pred_demand:在predict_and_update_buckets()中根据桶算法和数组sum_history中保存的历史窗口负载对下一个窗口的负载进行预测,预测结果为pred_demand。同时,在update_task_pred_demand()中,如果任务在当前窗口中预测出来的demand值小于curr_window(该值的描述在下方busytime路径中),则再次使用桶算法计算pred_demand。pred_demand主要用于调频。

Busytime 路径

update_cpu_busy_time()会统计任务在当前WALT窗口中的runtime,通过scale_exec_time()对runtime进行归一化处理,统计出任务在每个窗口内的负载,更新到当前窗口的curr_window中以及前一窗口的prev_window中。更新指的是它并不会在窗口翻滚时才保存负载值,而是实时更新,在窗口翻滚时才会将curr_window的值赋给prev_window,然后将curr_window的值置0。

  • p->ravg.curr_window:表示任务在当前(尚未完成)窗口中的CPU需求(represents cpu demand of task in its most recently tracked window)

  • p->ravg.prev_window:表示curr_window跟踪的窗口之前的窗口中任务的CPU需求(represents cpu demand of task in the window prior to the one being tracked by curr_window)

值得注意的是,update_task_demand() 和 update_cpu_busy_time()两个路径的进入条件不同,统计出来的runtime也不同。

CPU 负载

CPU负载也有两种路径,可以简单视作对两种路径的任务负载的累加。

Busytime 路径

update_cpu_busy_time()会统计任务在当前WALT窗口中的runtime,并直接累加进runnable_sum中,窗口翻滚时才会将curr_runnable_sum赋给prev_runnable_sum,并将curr_runnable_sum置0。根据是否是当前窗口以及任务是否是新任务,将runnable_sum分为以下四种:

  • rq->curr_runnable_sum:从当前(尚未完成)窗口执行的所有任务的总需求(aggregate demand from all tasks which executed during the current (not yet completed) window)

  • rq->prev_runnable_sum:最近完成的窗口期间执行的所有任务的聚合需求(aggregate demand from all tasks which executed during the most recent completed window)

  • rq->nt_curr_runnable_sum:当前(尚未完成)窗口期间执行的所有“新”任务的总需求(aggregate demand from all ‘new’ tasks which executed during the current (not yet completed) window)

  • rq->nt_prev_runnable_sum:最近完成的窗口期间执行的所有“新”任务的总需求(aggregate demand from all ‘new’ tasks which executed during the most recent completed window)

其中,新任务指的是:自fork以来活动窗口数量小于 SCHED_NEW_TASK_WINDOWS 的任务。活动窗口定义为观察到任务可以运行的窗口。(A ‘new’ task is defined as a task whose number of active windows since fork is less than SCHED_NEW_TASK_WINDOWS. An active window is defined as a window where a task was observed to be runnable.)

如果任务属于相关线程组(related_thread_group),那么以上runnable_sum将会统计进rq的grp_time中(由rq->xxx变为rq->grp_time.xxx)。

busytime 路径下的负载计算可以点击【WALT】update_cpu_busy_time() 代码详解 查看

demand路径

demand路径下的CPU负载较为复杂,以下篇幅将详细阐述三种负载的统计方式:

  • rq->cum_window_demand

  • rq->walt_stats.cumulative_runnable_avg

  • rq->walt_stats.pred_demands_sum

demand 路径下CPU负载的统计方式

基础信息

此部分的任务主要指进程。

TASK_RUNNING(任务状态之一)

	|			|<----- runnable ------>|<---------- running ---------->|<-- runnable ->|
	|-----------|-----------------------|-------------------------------|---------------|
 wakeup		 enqueue               start_exec						end_exec		 dequeue

任务从休眠状态被唤醒时,任务的状态会被设置为TASK_RUNNING,随后任务入队。

TASK_RUNNING包含两种状态:

  • running:任务正在占用CPU;

  • runnable(ready):任务刚进入队列还未上CPU状态,或是任务在running时间片耗尽或者主动让出CPU、或者被更高优先级进程抢占后,在队列中等待的状态。

处于TASK_RUNNING状态的进程要么正在CPU上运行,要么随时都可以投入运行,只不过CPU资源有限,调度器暂时没有选中他们。

处于TASK_RUNNING状态的进程是调度器的调度对象。在Linux中,每个CPU都有自己的运行队列。如果是实时进程(rt),则根据优先级的情况落在相应的优先级的队列上;如果是普通进程(cfs),则根据虚拟运行时间,落在红黑树相应位置上。

Linux通过__schedule()函数进行任务的调度,该函数调用pick_next_task()选出下一个要上CPU的任务,通过context_switch()进行上下文切换,让选出的任务上CPU执行。

调度相关

调度策略

进程在sched.h中的数据结构如下:

struct task_struct {
	……
	int on_rq;
	const struct sched_class *sched_class;
	struct sched_entity se;
	struct sched_rt_entity rt;
#ifdef CONFIG_SCHED_WALT
	struct ravg ravg;
#endif
	struct sched_dl_entity dl;
	unsigned int policy;
};

policy:进程的调度策略,一共分为6种调度策略:

/*
 * Scheduling policies
 */
#define SCHED_NORMAL		0
#define SCHED_FIFO			1
#define SCHED_RR			2
#define SCHED_BATCH			3
/* SCHED_ISO: reserved but not implemented yet */
#define SCHED_IDLE			5
#define SCHED_DEADLINE		6
  1. SCHED_DEADLINE:基于EDF(earliest deadline first),针对突发型计算,并且对延迟和完成时间敏感的进程使用,调度优先级最高的用户进程,选择 deadline 调度器;

  2. SCHED_RR:时间片轮转,进程用完时间片后加入优先级对应运行队列的尾部,把 CPU 让给同优先级的其他进程,选择 realtime 调度器;

  3. SCHED_FIFO:先进先出调度,没有时间片,在没有更高优先级的情况下, 只能等待主动让出 CPU,选择 realtime 调度器;

  4. SCHED_NORMAL:普通的分时进程,被用于绝大多数用户进程,选择 CFS 调度器;

  5. SCHED_BATCH:是SCHED_NORMAL的分化版本。适用于没有用户交互行为的后台进程,用户对该类进程的响应时间要求不高,但对吞吐量要求较高,因此调度器会在完成所有 SCHED_NORMAL 的进程之后让该类进程不受打扰地跑上一段时间,这样能够最大限度地利用缓存,适合成批处理的工作,选择 CFS 调度器;

  6. SCHED_IDLE:优先级最低,在系统空闲时运行,给 0 号进程(swapper)使用,选择 idle 调度器。

调度类

sched_class:对调度器进行抽象,一共分为5类调度类,按照优先级由高到低排序依次为:

  1. stop_sched_class:优先级最高的调度类,可以抢占其他所有进程,不能被其他进程抢占;

  2. dl_sched_class:使用红黑树,把进程按照绝对截止期限进行排序,选择最小进程进行调度运行;

  3. rt_sched_class:为每个优先级维护一个队列;

  4. fair_sched_class:采用完全公平调度算法,引入虚拟运行时间概念;

  5. idle_sched_class:每个CPU都会有一个idle线程,当没有其他进程可以调度时,调度运行idle线程。

不同的调度类绑定了不同的方法:如下所示:

rt任务:
const struct sched_class rt_sched_class = {
	……
	.enqueue_task		= enqueue_task_rt,
	.dequeue_task		= dequeue_task_rt,
#ifdef CONFIG_SCHED_WALT
	.fixup_walt_sched_stats	= fixup_walt_sched_stats_common,
#endif
}

dl任务:
const struct sched_class dl_sched_class = {
	……
	.enqueue_task		= enqueue_task_dl,
	.dequeue_task		= dequeue_task_dl,
#ifdef CONFIG_SCHED_WALT
	.fixup_walt_sched_stats	= fixup_walt_sched_stats_common,
#endif
}

stop任务:
const struct sched_class stop_sched_class = {
	……
	.enqueue_task		= enqueue_task_stop,
	.dequeue_task		= dequeue_task_stop,
#ifdef CONFIG_SCHED_WALT
	.fixup_walt_sched_stats	= fixup_walt_sched_stats_common,
#endif
}

cfs任务:
const struct sched_class fair_sched_class = {
	……
	.enqueue_task		= enqueue_task_fair,
	.dequeue_task		= dequeue_task_fair,
#ifdef CONFIG_SCHED_WALT
	.fixup_walt_sched_stats	= walt_fixup_sched_stats_fair,
#endif

调度实体

struct sched_entity se:采用CFS算法调度的普通非实时进程的调度实体。

cfs调度实体在sched.h中的数据结构如下:

struct sched_entity {
	……
	struct rb_node		run_node;
	unsigned int		on_rq;
	u64					vruntime;
#ifdef CONFIG_FAIR_GROUP_SCHED
	struct sched_entity	*parent;
	/* rq on which this entity is (to be) queued: */
	struct cfs_rq		*cfs_rq;
	/* rq "owned" by this entity/group: */
	struct cfs_rq		*my_q;
#endif
};

每个就绪态的调度实体包含插入红黑树中使用的节点run_node,同时vruntime成员记录已经运行的虚拟时间。

同时,on_rq表示是否处于CFS运行队列中,这部分将在上下文详细描述。


struct sched_rt_entity rt:采用Roound-Robin或者FIFO算法调度的实时调度实体。

struct sched_rt_entity {
	……
	unsigned short on_rq;
#ifdef CONFIG_RT_GROUP_SCHED
	struct sched_rt_entity	*parent;
	/* rq on which this entity is (to be) queued: */
	struct rt_rq		*rt_rq;
#endif
};

struct sched_dl_entity dl:采用EDF算法调度的实时调度实体。

值得注意的是:每一个进程的task_struct中都嵌入了sched_entity对象,所以进程是可调度的实体,但是可调度的实体不一定是进程,也可以是进程组。

运行队列

调度器的核心工作就是把可运行的任务放到CPU上运行,Linux内核使用一个运行队列(runqueue)来存放可运行的任务,是本CPU上所有可运行进程的队列集合。运行队列在sched.h中的数据结构如下:

struct rq {
    ……
    struct cfs_rq cfs;
    struct rt_rq rt;
    struct dl_rq dl;
};

每个CPU都有一个运行队列,每个运行队列中有三个调度队列,task作为调度实体加入到各自的调度队列中。

on_rq 标志位

任务入队时,它的 on_rq 为1,任务对应的 cfs 调度实体的 on_rq 同样也1。

p->on_rq:在sched.h中设置了两种on_rq的状态:

/* task_struct::on_rq states: */
#define TASK_ON_RQ_QUEUED		1
#define TASK_ON_RQ_MIGRATING	2
  • on_rq == 0 时意味着任务不在运行队列中;

  • on_rq == 1 时意味着任务在运行队列中;

  • on_rq == 2 时意味着任务在运行队列中,但正在迁移到另一个运行队列。

cfs调度实体中也设置了on_rq,但它只等于 0 或 1。它表明了 cfs 调度实体是否在 cfs 运行队列之中。

cfs_rq:跟踪就绪队列信息以及管理就绪态调度实体,并维护一棵按照虚拟时间排序的红黑树。

struct cfs_rq {
  ...
  struct rb_root_cached tasks_timeline
};

tasks_timeline->rb_root是红黑树的根,tasks_timeline->rb_leftmost指向红黑树中最左边的调度实体,即虚拟时间最小的调度实体。上文说到,每个就绪态的调度实体包含插入红黑树中使用的节点run_node,同时vruntime成员记录已经运行的虚拟时间。

值得注意的是:

  1. cfs运行队列里面包含有一个红黑树,但这个红黑树并不是cfs运行队列的全部,因为红黑树仅仅是用于选择出下一个调度程序的算法。当一个进程由就绪态切换到运行态时,该进程会被从红黑树中移除,但是还是处于cfs运行队列中,因此其on_rq为1.只有准备退出、即将休眠等待和转为实时进程的进程,其cfs运行队列的on_rq为0。

  2. 尽管cfs运行队列使用红黑树对所有调度实体进行管理,但这并不是cfs运行队列的全部,还包括相应数据结构以及标志位的更新,进程加入就绪队列并不仅仅时将调度实体的run_node节点添加到红黑树中,还涉及到一系列的操作,包括更新vruntime、更新负载信息、更新on_rq标志位等。

组调度

进程是可调度的实体,但是可调度的实体不一定是进程,也可能是进程组。sched_entity结构体中判断一个调度实体是否是进程组的字段是my_q。如果该字段为NULL,则表示该调度实体一个任务,否则就是一个进程组。

my_q同样是结构体cfs_rq,也就说说同一个CPU上隶属于同一个进程组的进程又通过另一个cfs_rq---my_q组织起来,放在另一棵红黑树中。

进程组在sched.h中的数据结构如下:

/* task group related information */
struct task_group {
	……
#ifdef CONFIG_FAIR_GROUP_SCHED
	/* schedulable entities of this group on each cpu */
	struct sched_entity **se;
	/* runqueue "owned" by this group on each cpu */
	struct cfs_rq **cfs_rq;
	unsigned long shares;
	……
#endif

#ifdef CONFIG_RT_GROUP_SCHED
	struct sched_rt_entity **rt_se;
	struct rt_rq **rt_rq;

	struct rt_bandwidth rt_bandwidth;
#endif
	……
	struct cfs_bandwidth cfs_bandwidth;
};

隶属于同一个调度组的调度实体不一定都在同一个cfs_rq中,可以分布在多个CPU的cfs_rq中,如下图底部的五个调度实体。

假设CPU1的调度组(group se)中的一个调度实体(例如右下角的调度实体)在执行,在上层cfs_rq看来,调度组就是正在执行的调度实体,但实体上真正执行的是调度组内部的cfs_rq上的一个调度实体。这个时候,调度实体的on_rq为1,调度组的on_rq也为1。

假设CPU1中调度组A中有一个调度实体B已经在队列中(下方的cfs_rq),那么调度组A同样也在队列中(上方的cfs_rq)。此时B->on_rq 和 A->on_rq都为1。如果调度组A中有另一个调度实体C要入队,直接将其添加到A->cfs_rq(下方的cfs_rq)就行。

但是如果调度实体B不在队列中,那么调度组A也不在队列中。如果调度实体C要入队,这时候就需要通过递归(enqueue_task_fair())将C和A都进行入队操作。

cfs 宽带控制

带宽控制是通过设置两个变量quota 和 period来限制一个进程组的最大使用CPU带宽。

period是指一段周期时间;quota是指在period周期时间内,一个进程组可以使用的CPU时间限额。当进程组进程的运行时间超过quota后,找哥进程组就会被限制运行,这个动作被称作throttle。知道下一个period周期开始,这个进程组会被重新调度,这个过程称作unthrottle。

quota 和 period的值存储在cfs_bandwidth结构体中,该结构体嵌在task_group中,cfs_bandwidth结构体还包含runtime成员记录剩余限额时间。宽带控制在sched.h中的数据机构如下:

struct cfs_bandwidth {
#ifdef CONFIG_CFS_BANDWIDTH
	……
	ktime_t period;
	u64 quota, runtime;
	s64 hierarchical_quota;
	u64 runtime_expires;

	int idle, period_active;
	struct hrtimer period_timer, slack_timer;
	struct list_head throttled_cfs_rq;

	/* statistics */
	int nr_periods, nr_throttled;
	u64 throttled_time;

	bool distribute_running;
#endif
};

每当进程组中的进程运行一段时间时,对应runtime时间也在减少。系统会启动一个高精度定时器,周期时间时period,在定时器时间到达后重置剩余限额时间runtime为quota,开始下一个轮时间跟踪。所有的进程组进程运行的时间累加在一起,保证总的运行时间小于quota。

限制一个进程组中的进程可以简单的将进程组管理的调度实体从对应的就绪队列上删除,然后标记调度实体对应的group cfs_rq的标志位。每个用户组会管理CPU个数的就绪队列group cfs_rq。每个group cfs_rq中也有限额时间,改限额时间是从全局用户组quota中申请。例如,周期period值100ms,限额quota值50ms,2个CPU系统。CPU0上group cfs_rq首先从全局限额时间中申请5ms时间(此实runtime值为45),然后运行进程。当5ms时间消耗完时,继续从全局时间限额quota中申请5ms(此实runtime值为40)。CPU1上的情况也同样如此,先以就绪队列cfs_rq的身份从quota中申请一个时间片,然后供进程运行消耗。当全局quota剩余时间不足以满足CPU0或者CPU1申请时,就需要throttle对应的cfs_rq。在定时器时间到达后,unthrottle所有已经throttle的cfs_rq。

总结一下就是,cfs_bandwidth就像是一个全局时间池(时间池管理时间,类似内存池管理内存)。每个group cfs_rq如果想让其管理的红黑树上的调度实体调度,必须首先向全局时间池中申请固定的时间片,然后供其他进程消耗。当时间片消耗完,继续从全局时间池中申请时间片。终有一刻,时间池中已经没有时间可供申请。此时就是throttle cfs_rq的大好时机。

DL 调度器

deadline任务有三个重要的属性:运行时间runtime,周期period,截止期限deadline。

									curr_time
	|				|					↓	 		    |					|
	|---------------|-------------------·---|-----------|-------------------|
 wakeup  	   start_exec					|    absolute deadline
 					|<------ runtime ------>|
 	|<----------------- deadline ------------------>|
 	|<---------------------------- period ----------------------------->|

runtime是完成任务工作需要的CPU执行时间,即在一个周期中,需要CPU参与运算的时间值。runtime在设定的时候必须考虑最坏情况下的执行时间,且调度器需要确保任务在每个period时间窗口内得到runtime这么多的CPU时间,并且必须在deadline这个时间点之前得到。

deadline定义了处理完成的结构必须被交付的最后期限。当产生一个调度点的时候,DL调度器总是选择其deadline距离当前时间点最近的那个任务并调度它执行。

主要函数(walt_fixup_cum_window_demand()fixup_cumulative_runnable_avg()

这两个函数是demand路径下CPU负载统计时必要的函数,分别是

walt_fixup_cum_window_demand()fixup_cumulative_runnable_avg()

static inline void walt_fixup_cum_window_demand(struct rq *rq, s64 delta)
{
	rq->cum_window_demand += delta;
	if (unlikely((s64)rq->cum_window_demand < 0))
		rq->cum_window_demand = 0;
}

将delta累加进任务所在的CPU的运行队列的cum_window_demand中。

static inline void
fixup_cumulative_runnable_avg(struct walt_sched_stats *stats,
			      s64 task_load_delta, s64 pred_demand_delta)
{
	// 是否禁用 CPU 的窗口统计
	if (sched_disable_window_stats)
		return;

	stats->cumulative_runnable_avg += task_load_delta;
	BUG_ON((s64)stats->cumulative_runnable_avg < 0);

	stats->pred_demands_sum += pred_demand_delta;
	BUG_ON((s64)stats->pred_demands_sum < 0);
}

将task_load_delta累加进任务所在的CPU的运行队列的cumulative_runnable_avg中,将pred_demand_delta累加进任务所在的CPU的运行队列的pred_demands_sum中。

初始化

在walt.c中的walt_sched_init()中,会将cum_window_demand、cumulative_runnable_avg 和 pred_demands_sum 初始化为0。

void walt_sched_init(struct rq *rq) {
	……
	rq->walt_stats.cumulative_runnable_avg = 0;
	rq->walt_stats.pred_demands_sum = 0;
	rq->cum_window_demand = 0;
}

任务入队与出队

入队相关代码

任务入队时,会调用enqueue_task()函数:

static inline void enqueue_task(struct rq *rq, struct task_struct *p, int flags) {
	……
	p->sched_class->enqueue_task(rq, p, flags);
}

通过判断任务所在的调度类,就能直接调用对应的方法:

rt任务:
static void
enqueue_task_rt(struct rq *rq, struct task_struct *p, int flags) {
	……
	walt_inc_cumulative_runnable_avg(rq, p);
}
--------------------------------------------------------------------------------
dl任务:
static void enqueue_task_dl(struct rq *rq, struct task_struct *p, int flags) {
	……
	enqueue_dl_entity(&p->dl, pi_se, flags);
}

static void enqueue_dl_entity(struct sched_dl_entity *dl_se,
		  struct sched_dl_entity *pi_se, int flags) {
	……
	__enqueue_dl_entity(dl_se);
}

static void __enqueue_dl_entity(struct sched_dl_entity *dl_se) {
	……
	inc_dl_tasks(dl_se, dl_rq);
}

static inline
void inc_dl_tasks(struct sched_dl_entity *dl_se, struct dl_rq *dl_rq) {
	……
	walt_inc_cumulative_runnable_avg(rq_of_dl_rq(dl_rq), dl_task_of(dl_se));
}
--------------------------------------------------------------------------------
stop任务:
static void
enqueue_task_stop(struct rq *rq, struct task_struct *p, int flags) {
	……
	walt_inc_cumulative_runnable_avg(rq, p);
}
--------------------------------------------------------------------------------
cfs任务:' 细节可以查看【二、0. b. 5) 组调度】相关介绍 '
static void
enqueue_task_fair(struct rq *rq, struct task_struct *p, int flags)
{
	struct cfs_rq *cfs_rq;
	struct sched_entity *se = &p->se;
	……
	/* #define for_each_sched_entity(se) \
	/*	 for (; se; se = se->parent)	*/
	' 为了递归向上将没有添加到就绪队列的 '
	' se 以及 se->parent 添加到就绪队列 '
	for_each_sched_entity(se) {
		if (se->on_rq)
			break;
		cfs_rq = cfs_rq_of(se);
		enqueue_entity(cfs_rq, se, flags);
		if (cfs_rq_throttled(cfs_rq))
			break;
		cfs_rq->h_nr_running++;
		
		walt_inc_cfs_rq_stats(cfs_rq, p);
		
		flags = ENQUEUE_WAKEUP;
	}

	' 更新已经存在于就绪队列中的 '
	' parent 节点中的相关信息 '
	for_each_sched_entity(se) {
		cfs_rq = cfs_rq_of(se);
		cfs_rq->h_nr_running++;
		
		walt_inc_cfs_rq_stats(cfs_rq, p);

		if (cfs_rq_throttled(cfs_rq))
			break;
		update_load_avg(se, UPDATE_TG);
		update_cfs_shares(se);
	}

	if (!se) {
		add_nr_running(rq, 1);
		inc_rq_walt_stats(rq, p);
	}
	……
}

static void walt_inc_cfs_rq_stats(struct cfs_rq *cfs_rq, struct task_struct *p) {
	……
	fixup_cumulative_runnable_avg(&cfs_rq->walt_stats, p->ravg.demand,
				      p->ravg.pred_demand);
}

void inc_rq_walt_stats(struct rq *rq, struct task_struct *p) {
	……
	walt_inc_cumulative_runnable_avg(rq, p);
}

在fair调度类的这一段代码中,进入walt_inc_cfs_rq_stats()执行fixup_cumulative_runnable_avg()时是让cfs_rq的cumulative_runnable_avg 和 pred_demands_sum加上任务的demand 和 pred_demand;

而进入inc_rq_walt_stats()执行walt_inc_cumulative_runnable_avg()时是让rq的cumulative_runnable_avg 和 pred_demands_sum加上任务的demand 和 pred_demand。


几乎所有的任务入队的最后都会进入walt_inc_cumulative_runnable_avg():

static inline void
walt_inc_cumulative_runnable_avg(struct rq *rq, struct task_struct *p) {
	// 是否禁用 CPU 的窗口统计
	if (sched_disable_window_stats)
		return;

	fixup_cumulative_runnable_avg(&rq->walt_stats, p->ravg.demand,
				      p->ravg.pred_demand);

	/*
	 * Add a task's contribution to the cumulative window demand when
	 *
	 * (1) task is enqueued with on_rq = 1 i.e migration, prio/cgroup/class change.
	 * (2) task is waking for the first time in this window.
	 */
	if (p->on_rq || (p->last_sleep_ts < rq->window_start))
		walt_fixup_cum_window_demand(rq, p->ravg.demand);
}

出队相关代码

任务入队时,会调用dequeue_task()函数:

static inline void dequeue_task(struct rq *rq, struct task_struct *p, int flags) {
	……
	p->sched_class->dequeue_task(rq, p, flags);
}

通过判断任务所在的调度类,就能直接调用对应的方法:

rt任务:
static void dequeue_task_rt(struct rq *rq, struct task_struct *p, int flags) {
	……
	walt_dec_cumulative_runnable_avg(rq, p);
}
--------------------------------------------------------------------------------
dl任务:
static void dequeue_task_dl(struct rq *rq, struct task_struct *p, int flags) {
	……
	__dequeue_task_dl(rq, p, flags);
}

static void __dequeue_task_dl(struct rq *rq, struct task_struct *p, int flags) {
	dequeue_dl_entity(&p->dl);
	……
}

static void dequeue_dl_entity(struct sched_dl_entity *dl_se) {
	__dequeue_dl_entity(dl_se);
}

static void __dequeue_dl_entity(struct sched_dl_entity *dl_se) {
	……
	dec_dl_tasks(dl_se, dl_rq);
}

static inline
void dec_dl_tasks(struct sched_dl_entity *dl_se, struct dl_rq *dl_rq) {
	……
	walt_dec_cumulative_runnable_avg(rq_of_dl_rq(dl_rq), dl_task_of(dl_se));
}
--------------------------------------------------------------------------------
stop任务:
static void
dequeue_task_stop(struct rq *rq, struct task_struct *p, int flags) {
	……
	walt_dec_cumulative_runnable_avg(rq, p);
}
--------------------------------------------------------------------------------
cfs任务:' cfs任务出队和入队的逻辑相近 '
static void dequeue_task_fair(struct rq *rq, struct task_struct *p, int flags)
{
	struct cfs_rq *cfs_rq;
	struct sched_entity *se = &p->se;
	int task_sleep = flags & DEQUEUE_SLEEP;

	' 如果当前 se 存在 parent,且该 parent 下'
	' 不存在其它子 se 在就绪队列上,parent 同时出队; '
	' 如果 parent 下还存在其它 se 在就绪队列上, '
	' parent 不需要出队 '
	for_each_sched_entity(se) {
		cfs_rq = cfs_rq_of(se);
		dequeue_entity(cfs_rq, se, flags);
		if (cfs_rq_throttled(cfs_rq))
			break;
		cfs_rq->h_nr_running--;
		
		walt_dec_cfs_rq_stats(cfs_rq, p);

		/* Don't dequeue parent if it has other entities besides us */
		if (cfs_rq->load.weight) {
			se = parent_entity(se);
			if (task_sleep && se && !throttled_hierarchy(cfs_rq))
				set_next_buddy(se);
			break;
		}
		flags |= DEQUEUE_SLEEP;
	}
	
	' 更新已经存在于就绪队列中的 '
	' parent 节点中的相关信息 '
	for_each_sched_entity(se) {
		cfs_rq = cfs_rq_of(se);
		cfs_rq->h_nr_running--;
		
		walt_dec_cfs_rq_stats(cfs_rq, p);

		if (cfs_rq_throttled(cfs_rq))
			break;
		update_load_avg(se, UPDATE_TG);
		update_cfs_shares(se);
	}

	if (!se) {
		sub_nr_running(rq, 1);
		dec_rq_walt_stats(rq, p);
	}
	……
}

static void walt_dec_cfs_rq_stats(struct cfs_rq *cfs_rq, struct task_struct *p) {
	……
	fixup_cumulative_runnable_avg(&cfs_rq->walt_stats, -(s64)p->ravg.demand,
				      -(s64)p->ravg.pred_demand);
}

void dec_rq_walt_stats(struct rq *rq, struct task_struct *p) {
	……
	walt_dec_cumulative_runnable_avg(rq, p);
}

在fair调度类的这一段代码中,进入walt_dec_cfs_rq_stats() 执行 fixup_cumulative_runnable_avg()时是让cfs_rq的cumulative_runnable_avg 和 pred_demands_sum减去任务的demand 和 pred_demand;

而进入dec_rq_walt_stats()执行walt_dec_cumulative_runnable_avg()时是让rq的cumulative_runnable_avg 和 pred_demands_sum减去任务的demand 和 pred_demand。


几乎所有的任务出队的最后都会进入walt_dec_cumulative_runnable_avg():

static inline void
walt_dec_cumulative_runnable_avg(struct rq *rq, struct task_struct *p) {
	if (sched_disable_window_stats)
		return;

	fixup_cumulative_runnable_avg(&rq->walt_stats, -(s64)p->ravg.demand,
				      -(s64)p->ravg.pred_demand);

	/*
	 * on_rq will be 1 for sleeping tasks. So check if the task is migrating or dequeuing in RUNNING state to change the prio/cgroup/class.
	 */
	if (task_on_rq_migrating(p) || p->state == TASK_RUNNING)
		walt_fixup_cum_window_demand(rq, -(s64)p->ravg.demand);
}

任务入队与出队总结

入队

任务入队的时候,无论哪一种调度类,最终都要执行fixup_cumulative_runnable_avg(),进行rq->walt_stats.cumulative_runnable_avg += demand 和 rq->walt_stats.pred_demands_sum += pred_demand。

同时,任务对应调度实体及其父节点入队时也都会进行一次cfs_rq->walt_stats.cumulative_runnable_avg += demand 和 cfs_rq->walt_stats.pred_demands_sum += pred_demand。

除此之外,会根据以下条件之一是否满足执行walt_fixup_cum_window_demand():

  • 任务正在运行队列中时(p->on_rq == 1 == TASK_ON_RQ_QUEUED);

  • 任务在此窗口中首次唤醒(p->last_sleep_ts < rq->window_start)。

这两个条件其一得到满足的时候,cum_window_demand += demand。

出队

任务出队的时候,无论哪一种调度类,最终都会执行fixup_cumulative_runnable_avg(),进程rq->walt_stats.cumulative_runnable_avg -= demand 和 rq->walt_stats.pred_demands_sum -= pred_demand。

同时,任务对应调度实体及其父节点出队时也都会进行一次cfs_rq-》walt_stats.cumulative_runnable_avg -= demand 和 cfs_rq->walt_stats.pred_demands_sum -= pred_demand。

除此之外,会根据以下条件之一是否满足执行walt_fixup_cum_window_demand():

(代码中的注释说:对于休眠任务,on_rq 将为 1。是这样的吗?) 
  • 任务处于迁移过程中,可能不在就绪队列中(p->on_rq == 2TASK_ON_RQ_MIGRATING);

  • 任务处于TASK_RUNNING状态下迁移或出队(p->state == TASK_RUNNING)。

这两个条件其中之一满足的时候,cum_window_demand -= demand。

总而言之,在任务入队的时候会直接将其之前得到的demand 和 pred_demand 放入CPU负载之中,在出队时再去掉这部分。

WALT窗口翻滚

【WALT】update_history()代码详解中我们介绍了WALT在窗口翻滚的时候是如何计算demand的,以及是如何通过【WALT】predict_and_upate_buckets()代码详解计算pred_demand的。

窗口翻滚相关代码

我们在update_history()中的这部分代码中可以知道,在计算完任务的demand 和 pred_demand之后,WALT将他们更新到CPU负载中去了:

static void update_history(struct rq *rq, struct task_struct *p,
			 u32 runtime, int samples, int event)
{
	……
	// ⑸ 将 demand 与 pred_demand 更新到 rq 中
	if (!task_has_dl_policy(p) || !p->dl.dl_throttled) {
		if (task_on_rq_queued(p))
			p->sched_class->fixup_walt_sched_stats(rq, p, demand,
							       pred_demand);
		else if (rq->curr == p)
			walt_fixup_cum_window_demand(rq, demand);
	}
	……
}

首先先判断两个条件:

  • !task_has_dl_policy(struct task_struct *p):这句话等同于policy != SCHED_DEADLINE,也就是该任务的调度策略并未dl策略;

  • !p->dl.dl_throttled:即任务没有耗尽调度器设置的运行时间,解释如下:

    • 内核中这么描述 dl_throttled:@dl_throttled 告诉我们是否耗尽了运行时间。如果是这样,任务必须等待下次触发 dl_timer 时执行补充。(@dl_throttled tells if we exhausted the runtime. If so, the task has to wait for a replenishment to be performed at the next firing of dl_timer.)

    • 在之前描述了 runtime、deadline 和 period 之间的关系。耗尽了运行时间是指任务在当前 period 之中的运行时间大于等于调度器设置的任务能够运行的最长时间 runtime。

如果任务的调度策略不是deadline,或DL任务并没有耗时运行时间,还需要进行两次条件判断:

  • 如果task_on_rq_queued(p)为真,即p->on_rq == 1 == TASK_ON_RQ_QUEUED时(猜测:任务此时处于runnable状态。否则条件一囊括了条件二):

    • 执行不同调度类的fixup_walt_sched_stats(rq,p,demand,pred_demand)

      p->sched_class->fixup_walt_sched_stats(rq, p, p->ravg.demand, p->ravg.pred_demand);

      • 除了cfs任务对应的方法是walt_fixup_sched_stats_fair(),其他的任务对应的方法都是fixup_walt_sched_stats_common()。

        #ifdef CONFIG_CFS_BANDWIDTH
        static void walt_fixup_sched_stats_fair(struct rq *rq, struct task_struct *p,
        				       u32 new_task_load, u32 new_pred_demand)
        {
        	struct cfs_rq *cfs_rq;
        	struct sched_entity *se = &p->se;
        	// new_demand - p->ravg.demand
        	s64 task_load_delta = (s64)new_task_load - task_load(p);
        	// new_pred_demand - p->ravg.pred_demand
        	s64 pred_demand_delta = PRED_DEMAND_DELTA;
        
        	for_each_sched_entity(se) {
        		cfs_rq = cfs_rq_of(se);
        
        		fixup_cumulative_runnable_avg(&cfs_rq->walt_stats,
        					      task_load_delta,
        					      pred_demand_delta);
        		if (cfs_rq_throttled(cfs_rq))
        			break;
        	}
        
        	/* Fix up rq->walt_stats only if we didn't find any throttled cfs_rq */
        	if (!se) {
        		fixup_cumulative_runnable_avg(&rq->walt_stats,
        					      task_load_delta,
        					      pred_demand_delta);
        		walt_fixup_cum_window_demand(rq, task_load_delta);
        	}
        }
        #else /* CONFIG_CFS_BANDWIDTH */
        static void walt_fixup_sched_stats_fair(struct rq *rq, struct task_struct *p,
        				       u32 new_task_load, u32 new_pred_demand)
        {
        	fixup_walt_sched_stats_common(rq, p, new_task_load, new_pred_demand);
        }
        #endif /* CONFIG_CFS_BANDWIDTH */
        • 在CONFIX_CFS_BANDWIDTH打开的时候:

          • task_load_delta 是用新计算出来的 demand 减去上一次的 demand 得到的,pred_demand_delta 是用新计算出来的 pred_demand 减去上一次的 pred_demand 得到的;

          • 遍历 se 及其 parent,通过 fixup_cumulative_runnable_avg() 将任务的 task_load_delta 和 pred_demand_delta 加进 cfs_rq 的 cumulative_runnable_avg 和 pred_demands_sum 中;

          • 最后通过 fixup_cumulative_runnable_avg() 和 walt_fixup_cum_window_demand() 将任务的 task_load_delta 加进 rq 的 cumulative_runnable_avg 和 cum_window_demand,将任务的 pred_demand_delta 加进 rq 的 pred_demands_sum 中。

        • 在CONFIG_CFS_BANDWIDTH没有打开的时候其实也执行的是fixup_walt_sched_stats_common()。

          void fixup_walt_sched_stats_common(struct rq *rq, struct task_struct *p,
          				   u32 new_task_load, u32 new_pred_demand)
          {
          	s64 task_load_delta = (s64)new_task_load - task_load(p);
          	s64 pred_demand_delta = PRED_DEMAND_DELTA;
          
          	fixup_cumulative_runnable_avg(&rq->walt_stats, task_load_delta,
          				      pred_demand_delta);
          
          	walt_fixup_cum_window_demand(rq, task_load_delta);
          }

          也就是说,窗口翻滚的时候,如果当前任务处于runnable状态,直接通过fixup_cumulative_runnable_avg()将任务的task_load_delta加进rq的pred_demands_sum中;也会通过walt_fixup_cum_window_demand()将任务的task_load_delta加进rq的cum_window_demand。

        • 如果任务就是运行队列中的当前任务,即rq->curr == p时:

          • 直接执行walt_fixup_cum_window_demand(rq, demand),将任务的demand加进rq的cum_window_demand。

          • 也就是说,窗口翻滚的时候,如果当前任务处于running状态,就会将本次计算的demand累加进任务所在CPU的运行队列的cum_window_demand中,但是不会将差值累加。

        评论
        添加红包

        请填写红包祝福语或标题

        红包个数最小为10个

        红包金额最低5元

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

        抵扣说明:

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

        余额充值