Linux CFS 调度器 (1):概述

1. 前言

限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。

2. CFS 调度器

2.1 概述

CFS,是 Completely Fair Scheduler 的缩写,翻译过来就是 完全公平调度器,由 Ingo Molnar 实现,在 Linux 2.6.23 中合入的新的 桌面系统 进程调度器。

CFS 调度器 80% 的设计可以用一句话来概括:CFS 基本上是在真实硬件上模拟了一个理想、精确多任务 虚拟 CPU。我们将这个虚拟 CPU 的运行能力定义为 1,假定当前有 nr_running 个任务在虚拟 CPU上运行,则每个任务精准的占用虚拟 CPU 1/nr_running运行能力(或 运行时间)。例如有 2 个任务在运行,假定将 虚拟 CPU 的运行能力设定为 100%,那么每个任务占用 虚拟 CPU 50%运行能力(或 运行时间)。为了描述方便,在这里先将任务占用的 虚拟 CPU 的运行时间,称作 虚拟运行时间(virtual runtime),这和后文的 物理 CPU实际运行时间 相对应。

CFS 调度器中,并非真的将 物理 CPU物理运行能力(或 实际时间)平均分配给所有可运行任务CFS 仍然要处理任务优先级:即优先级更高的任务,仍然会分配更多 物理 CPU实际运行时间 给它们。这看起来似乎和 CFS 调度器的 完全公平调度 宗旨相矛盾,但事实是,CFS 调度器只是尽量保证每个任务占用的 虚拟运行时间(virtual runtime) 一样,而任务占用的 实际运行时间,仍然由任务优先级来体现。正如世界不可能完全公平一样,在 CFS 调度器中,分配给每个任务的 虚拟运行时间(virtual runtime)CFS 也只是尽量保证它们一致不可能达到理想状态的完全一致

2.1.1 CFS 核心设计思想

CFS 的核心设计思想小结如下:

  • 想象一个处理器 P,它被理想化了,因为它可以同时执行多个任务。例如,任务 T1T2 可以同时在 P 上执行,每个任务都获得 P 处理能力的 50%。这种理想化描述了完美的多任务处理,CFS 努力在实际而不是理想化的处理器上实现。CFS 旨在实现完美的多任务处理。

  • CFS 有一个目标延迟,即每个可运行任务在处理器上至少运行一次所需的最短时间(理想化为无限小的一段时间)。如果这样的持续时间可以无限小,那么每个可运行的任务在任何给定的时间跨度内都能够在处理器上运行,无论时间跨度有多小(例如,10 毫秒、5 秒等)。当然,在现实世界中必须近似一个理想化的无限小持续时间,默认近似值为 20ms。然后,每个可运行的任务都会获得目标延迟的 1/N 的时间片,其中 N 是任务数。例如,如果目标延迟为 20 毫秒,并且有四个竞争任务,则每个任务的时间片为 5 毫秒。顺便说一句,如果一次调度事件期间只有一个任务,则此任务将获得整个目标延迟作为其时间片。CFS 中的公平性争夺处理器的每个任务分配 1/N 的时间片

  • 目标延迟1/N 切片是一个时间片,但它不是固定的时间片,该时间片随当前争夺处理器的任务数 N 变化而变化,这不同于以往的调度器设计。系统会随着时间而变化:一些进程终止并产生新的进程;可运行的进程会阻塞,阻塞的进程将变为可运行的进程,所以N 的值是动态的。因此,为争用处理器的每个可运行任务计算的 1/N 时间片也是如此。传统的 nice 值用于对 1/N 时间片切片进行加权低优先级任务nice 值意味着 1/N 切片中只有一部分时间被赋予任务,而高优先级任务nice 值意味着赋予任务大于 1/N 切片的时间。总之,nice不决定 1/N 切片的大小,这由系统中任务数 N 决定nice 值的作用是确定一个权重值,将 1/N 切片按该权重缩放得到任务的运行时间1/N 切片用来表示竞争任务之间的公平性

  • 每当发生上下文切换时,操作系统都会产生开销。当一个进程被另一个进程抢占时,为了防止上下文切换时间变得过大,任何调度器必须保证任务被调度运行的最小时间(典型设置为 1 毫秒到 4 毫秒),然后才能被抢占。这个最小时间度被称为最小粒度最小粒度必须要比上下文切换时间要大,不然大把时间都花在了上下文切换上,一个任务调度进来又马上被调度出去,而进程本身没得到机会。另外,系统有越多任务争夺处理器,每个任务的调度延迟也会增大,公平性就会消失。因为 CFS 调度器要保证任务运行的最小时间(即最小粒度),同时 目标延迟 = N * 最小粒度也会随任务数 N 变大而变大,所以每个任务的调度延迟也会增大:毕竟 CFS 只保证目标延迟内任务被调度到,可以是最先调度,也可以是最后调度。当目标延迟延迟超过一定限度(取决于应用场景的要求),则表示系统在当前应用场景已经过载。

  • 抢占何时发生?CFS 试图最小化上下文切换,因为它们的开销:在上下文切换上花费的时间是其他任务无法使用的时间。因此,一旦任务获得处理器,它就会运行整个加权的 1/N 切片,然后被抢占以支持其他任务。假设任务 T1 已为其加权 1/N 切片运行,而可运行的任务 T2 当前在争用处理器的任务中具有最低的虚拟运行时 (vruntime)。vruntime 以纳秒为单位记录任务在处理器上运行的时间。在这种情况下,T1 将被优先于 T2 被调度。

  • CFS 跟踪所有任务的 vruntime包括可运行任务和阻塞任务。任务的 vruntime 越低,该任务就越值得在处理器上的时间。因此,CFS 将低 vruntime 任务移至调度时间线的前面。调度时间线的细节即将在下面说明,调度时间线是以树实现的,而不是列表。

  • CFS 应多久调度一次一种简单的方法给定调度周期。假设目标延迟 (TL) 为 20ms,最小粒度 (MG) 为 4ms:
    TL / MG = (20 / 4) = 5 ## 5 个或更少任务工作良好
    在这种情况下,5个或更少的任务将允许每个任务在目标延迟期间都能在处理器上得到执行。例如,如果任务数为 5,则每个可运行任务的 1/N 切片为 4ms,恰好等于最小粒度;如果任务数为 3,则每个任务将获得近 7 毫秒的 1/N 切片。无论哪种情况,调度程序都将在 20 毫秒(目标延迟时间)内重新调度。
    如果任务数(例如 10)超过 TL / MG,则会出现问题,因为现在每个任务必须获得 4ms 的最短时间(即最小粒度),而不是计算出的 1/N 切片,即 20ms / 10 = 2ms 。在这种情况下,调度程序将在 40 毫秒内重新调度:
    (number of tasks) * MG = (10 * 4) = 40ms ## period = 40ms
    早于 CFS 的 Linux 调度器使用启发式方法来促进在调度方面公平对待交互式任务。CFS 采取了一种完全不同的方法,它让 vruntime 的事实来说话,这恰好保证了对睡眠进程的公平。就其本质而言,交互式任务往往会睡眠很多,因为它等待用户输入而成为 I/O 密集型;因此,这样的任务往往具有相对较低的 vruntime,这往往会导致将任务移到调度时间线的更前面。

2.2 CFS 的一些实现细节

CFS 调度器中,虚拟运行时间(virtual runtime) 通过每个任务的 p->se.vruntime(以纳秒单位)来表示和跟踪,这样,就可以准确地标记时间戳并测量任务应该获得的预期 CPU 时间。其中,p 指向一个 struct task_struct 结构体。

/* include/linux/sched.h */

struct sched_entity {
	...
	/*
	 * 进程的虚拟运行时间 = 进程的实际运行时间 / 相对权重
	 *
	 * 进程的 实际运行时间 是一段一段的,所以进程的 虚拟运行时间 也是一段一段增长的。
	 *
	 * 进程的 虚拟运行时间 还会在 进程入队时 与 运行队列中的最小虚拟时间 相比较,如
	 * 果更小的话会直接进行增加,并不对应 实际的运行时间。为什么要这么做呢?因为有的
	 * 进程可能会因长时间睡眠,导致其 虚拟运行时间 小于运行队列中所有进程中的 最小虚
	 * 拟运行时间,这样的进程一旦运行起来,会因为其 虚拟运行时间 小,占据 CPU 的时间
	 * 就会长,对其它进程就不公平了。
	 */
	u64				vruntime;
	...
};

struct task_struct {
	...
	const struct sched_class	*sched_class; /* 如果任务使用 CFS 调度,则为 &fair_sched_class */
	struct sched_entity		se; /* CFS 类进程调度参数 */
	...
};

在理想的 虚拟 CPU 上,在任何时刻,所有任务 虚拟运行时间值 p->se.vruntime 都保持相同的值。

CFS 调度器,其任务选择逻辑基于 p->se.vruntime 值:CFS 始终尝试运行具有最小 p->se.vruntime 值的任务(即到目前为止虚拟执行时间(virtual runtime)最小的任务)。CFS 始终尝试在可运行任务之间平均分配 虚拟 CPU时间 ,尽可能接近理想的多任务 虚拟 CPU

CFS 设计的其余部分大部分都脱离了这个非常简单的概念,有一些其它附加修饰,如任务的 nice 值,各种识别睡眠任务的算法等等。

2.3 CFS 运行队列:红黑树

CFS 需要高效的数据结构来跟踪任务信息,需要高性能的代码来产生调度。让我们从调度中的一个中心术语开始,即运行队列(runqueue)运行队列(runqueue)是一个数据结构,表示被调度任务的时间线。尽管有这个名字,但运行队列不需要以传统方式实现,如实现为一个 FIFO 列表。CFS 打破了传统,它不使用运行队列以往的旧数据结构,而是使用按时间排序的红黑树(red-black tree)作为可运行任务队列,来构建未来任务执行的时间线。红黑树非常适合这项工作,因为它是一个自平衡的二叉搜索树,具有高效的插入和删除操作,在 O(log N) 时间内执行,其中 N 是树中的节点数。此外,树是一种出色的数据结构,用于根据特定属性(在 CFS 中为 vruntime)将实体组织到层次结构中。

在 CFS 中,树的内部节点表示要调度的任务,而树作为一个整体(与任何运行队列一样)表示任务执行的时间线。红黑树被广泛使用,还用于任务调度之外,例如,Java 使用此数据结构来实现其树状图。

在 CFS 下,每个处理器都有一个自己特定的任务运行队列,同一任务某一时刻只会位于某个运行队列中。每个运行队列都是一棵红黑树。树的内部节点表示任务或任务组,这些节点按其 vruntime 值索引,因此(在整个树或任何子树中)左侧内部节点的 vruntime 值低于右侧节点

    25     ## 25 is a task vruntime
    /\
  17  29   ## 17 roots the left subtree, 29 the right one
  /\  ...
 5  19     ## and so on
...  \
     nil   ## leaf nodes are nil

总之,具有最低 vruntime 的任务(因此对处理器的需求最大)位于左侧子树的某个位置;具有相对较高的 vruntimes 的任务聚集在右侧子树中。抢占的任务将进入右侧子树,从而使其他任务有机会在树中向左移动。具有最小 vruntime 的任务最终出现在树的最左侧(内部)节点中,因此该节点位于运行队列的前面。

/* kernel/sched/sched.h */

/* CFS-related fields in a runqueue */
struct cfs_rq {
	...
	u64 min_vruntime; /* CFS 调度算法 运行队列中 进程 的 最小 虚拟运行时间 */
	...
	/*
	 * CFS 进程 运行队列 红黑树 根节点: 
	 * 按进程的 vruntime 为键值进行排序。 
	 */
	struct rb_root_cached tasks_timeline;
	...
};

/*
 * This is the main, per-CPU runqueue data structure.
 *
 * Locking rule: those places that want to lock multiple runqueues
 * (such as the load balancing or the thread migration code), lock
 * acquire operations must be ordered by ascending &runqueue.
 */
struct rq {
	...
	struct cfs_rq cfs;
	...
};

DECLARE_PER_CPU_SHARED_ALIGNED(struct rq, runqueues); /* 每 CPU 的运行队列 */
/* include/linux/rbtree.h */

struct rb_root_cached {
	struct rb_root rb_root; /* 红黑树 */
	struct rb_node *rb_leftmost; /* 缓存 红黑树 @rb_root 最左边的节点 */
};

红黑树的每个内部节点都有指向父节点和两个子节点的指针,叶节点的值为 NULL

struct rb_node {
	unsigned long  __rb_parent_color; /* 父节点颜色: 红 或 黑 */
	struct rb_node *rb_right;
	struct rb_node *rb_left;
/* The alignment might seem pointless, but allegedly CRIS needs it */
} __attribute__((aligned(sizeof(long))));

struct rb_root { /* 红黑树 的 根 */
	struct rb_node *rb_node;
};

CFS 调度器用 task_struct 来描述一个任务,task_struct 用于跟踪有关要调度的每个任务的详细信息。此结构嵌入了一个 sched_entity 结构,该结构又具有特定于调度的信息,特别是每个任务或任务组的 vruntime,即 rq->cfs.min_vruntime 值,该值是一个单调递增值,用于跟踪运行队列所有任务中的最小 vruntime。该值用于尽可能将新激活的调度实体(struct sched_entity,即任务)置在红黑树左侧。由于该值一直使用 min_vruntime 单调递增累加,所以也可用来跟踪系统完成的工作总量。

/* include/linux/sched.h */

struct sched_entity {
	...
	u64	vruntime;
	...
};

struct task_struct {
	...
	struct sched_entity se;  /* vruntime, etc. */
	...
};

运行队列的权重通过 rq->cfs.load 值进行统计,该值是运行队列上排队的任务权重的总和:

/* kernel/sched/sched.h */

/* CFS-related fields in a runqueue */
struct cfs_rq {
	/*
	 * 运行队列的权重, 等于其上所有进程的权重之和。
	 * 进程在入队出队时,也会相应地从运行队列中加上减去其自身的权重。
	 */
	struct load_weight load;
	...
};

CFS 维护一个按时间排序的 红黑树(rbtree),即所有可运行的任务都按 p->se.vruntime 键排序,然后CFS 从这棵树中挑选最左边(即 p->se.vruntime 最小)的任务执行。随着时间往后推移,执行的任务越来越多地被放入树中,执行时间更少的任务逐渐往红黑树左边移动,执行时间更多的任务往红黑树右边移动,如此循环往复。

创建新任务时,将它们插入红黑树,初始 vruntime 值赋为 min_vruntime,使得它们尽可能快的得到执行:vruntime 越小,越快得到执行。

使用红黑树还有一个优点,那就是如果一个进程是 I/O 密集型的,那么它的虚拟时间将非常少,并且它显示为红黑树中最左边的节点,因此首先执行。因此,CFS 很容易找出哪些是 I/O 密集型的进程,哪些是 CPU 密集型的进程,并且它让 I/O 密集型的进程具有更高的优先级,从而避免了饥饿。

2.3.1 CFS 运行队列红黑树的时间复杂度

  • 插入红黑树需要 O(logn)
  • 查找虚拟时间最短的节点是 O(1)

所以整体时间复杂度O(logn)

假设红黑树一旦创建,然后我们就有了 N 个进程的红黑树,所以调度时间复杂度O(logn)

2.4 CFS 的一些特征

CFS 使用纳秒级精度,对任务的虚拟运行时间的统计,它不依赖于 jiffiesHZ 值。因此,CFS 调度器没有像以前的调度器那样的时间片概念,也没有任何启发式方法。不使用启发式方法这一点,这使得 CFS 不容易受到针对启发式调度的攻击。

CFS任务 nice 值SCHED_BATCH 策略任务 的处理比以往的调度器更优。

CFSSMP 架构下的负载均衡代码进行了清理,使得负载均衡逻辑更加简单。

2.1.1 小节中提到 CFS 的 最小粒度目标延迟nice 优先级权重 等概念,这里分别对它们一一加以简要说明。

2.4.1 最小粒度

CFS 最小粒度,指任务一次调度期间,运行的最小时长。定义于 kernel/sched/fair.c

/*
 * sysctl_sched_min_granularity:
 * 调度粒度, 一次调度中, 进程至少运行时长(主动调度除外)。
 */ 
unsigned int sysctl_sched_min_granularity		= 750000ULL;

其值默认为 0.75 ms

2.4.2 目标延迟

CFS 目标延迟,更多的时候,可能习惯称它为 调度周期,指当前系统中 N 个可运行任务至少都被调度一次的时长。定义于 kernel/sched/fair.c

/*
 * 一般概念上,调度延迟是指一个进程从加入到运行队列,到被放到 CPU 上
 * 去执行之间的时间差。显然这个时间差受进程本身和运行队列长短的影响。
 *
 * 而在 CFS 中,调度延迟的概念完全变了,调度延迟变成了调度周期的最小值。
 */ 
unsigned int sysctl_sched_latency			= 6000000ULL;

CFS 目标延迟的默认值为 6 ms,这是在系统中任务小于等于 6000000 / 750000 = 8 个时候的情形,保证了系统中任务的最小粒度时间。如果系统的任务大于 8 个,则动态计算目标延迟

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

2.4.3 nice 优先级权重

通过 nice 优先级权重,乘以 1/N 目标延迟的分片,计算除任务的运行时间。通过定义在 kernel/sched/core.c 中的表 sched_prio_to_weight[]sched_prio_to_wmult[] 确定任务 nice 值对应的权重

/* kernel/sched/core.c */

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

/* 计算 nice 对应的权重(这里 nice 已经转换为了优先级) */
static void set_load_weight(struct task_struct *p)
{
	int prio = p->static_prio - MAX_RT_PRIO;
	struct load_weight *load = &p->se.load;

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

然后,将权重转换为(真实)执行时间:

/* kernel/sched/fair.c */

/* 为调度实体(进程) @se 计算实际运行时间的时间片 */
static u64 sched_slice(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
	u64 slice = __sched_period(cfs_rq->nr_running + !se->on_rq);

	for_each_sched_entity(se) {
		struct load_weight *load;
		struct load_weight lw;

		cfs_rq = cfs_rq_of(se);
		load = &cfs_rq->load;

		if (unlikely(!se->on_rq)) {
			lw = cfs_rq->load;

			update_load_add(&lw, se->load.weight);
			load = &lw;
		}
		slice = __calc_delta(slice, se->load.weight/*调度实体(进程)的权重*/, load/*运行队列的权重*/);
	}
	return slice;
}

/*
 * 计算权重为 @weight 进程 真实运行时间增量(@delta_exec) 对应的
 * 虚拟时间增量。
 *
 * @delta_exec: 进程 真实运行时间增量
 */
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--;
	}

	/*
	 * 计算进程 真实运行时间增量(delta_exec) 对应的 虚拟时间增量:
	 *                                             进程的权重
	 * 进程 真实运行时间增量(delta_exec) * ------------------------
	 *                                      进程所在运行队列的权重
	 */
	return mul_u64_u32_shr(delta_exec, fact, shift);
}

2.5 CFS 调度策略

CFS 实现了 3调度策略

  • SCHED_NORMAL,以往也叫 SCHED_OTHER:用于常规任务

  • SCHED_BATCH适用没有用户交互行为的后台进程,用户对该类进程的响应时间要求不高,但对吞吐量要求较高,因此调度器会在完成所有 SCHED_NORMAL 的任务之后让该类任务不受打扰地跑上一段时间,这样能够最大限度地利用缓存。

  • SCHED_IDLE:这类调度策略被用于系统中优先级最低的任务,只有在没有任何其他任务可运行时,调度器才会将运行该类任务。

2.6 调度器类别

Linux 同时支持多种类型调度器类别CFS 调度只是其中的一种,实现在文件 sched/fair.c 中。而像其它的调度类别如实现 SCHED_FIFOSCHED_RR 调度策略的实时调度器,实现在文件 sched/rt.c 中。

调度类是通过 struct sched_class 结构体实现的,该结构体包含一些列回调,在发生特定事件时被调用。struct sched_class 结构体内容如下:

/* kernel/sched/sched.h */

struct sched_class {
	const struct sched_class *next;

	/*
	 * 将进程 @p 放入运行队列 @rq 。
	 * 在任务进入可运行状态时调用。
	 * 它将调度实体(任务)放入红黑树中,并递增 nr_running 变量。
	 */
	void (*enqueue_task) (struct rq *rq, struct task_struct *p, int flags);
	/*
	 * 将进程 @p 移出运行队列 @rq 。
	 * 当任务不再可运行时,将调用此函数以将相应的调度实体移出红黑树,并递减 
	 * nr_running 变量。
	 */
	void (*dequeue_task) (struct rq *rq, struct task_struct *p, int flags);
	/*
	 * 当前进程主动让出 CPU , 即移出运行队列,但 其状态依然是 runnable ,
	 * 然后将另一进程加入运行队列。
	 * 可通过系统调用 sys_sched_yield() 触发。
	 */
	void (*yield_task) (struct rq *rq);
	/* 当前进程主动让出 cpu 给进程组内进程 @p */
	bool (*yield_to_task) (struct rq *rq, struct task_struct *p, bool preempt);

	/*
	 * 被唤醒进程的抢占逻辑:检查 @p 是否会抢占 @rq 中当前正在运行的进程. 
	 * 通常情况下是在 @p 进入 runnable 态时,检查 @p 是否会抢占当前正在运
	 * 行的进程。
     */
	void (*check_preempt_curr) (struct rq *rq, struct task_struct *p, int flags);

	/*
	 * It is the responsibility of the pick_next_task() method that will
	 * return the next task to call put_prev_task() on the @prev task or
	 * something equivalent.
	 *
	 * May return RETRY_TASK when it finds a higher prio class has runnable
	 * tasks.
	 */
	/* 
	 * 挑选下一可执行进程, 且以 @prev 为参数, 调用 put_prev_task() . 
	 *
	 * 通常返回挑选的下一可执行进程, 但当发现更高优先级调度类别有可运行进程时, 
	 * 返回 RETRY_TASK .
	 */ 
	struct task_struct * (*pick_next_task) (struct rq *rq,
						struct task_struct *prev,
						struct rq_flags *rf);
	void (*put_prev_task) (struct rq *rq, struct task_struct *p);

#ifdef CONFIG_SMP
	/*
	 * 为进程 @p 选择运行队列。
	 * 当系统调用 fork() + exec() 创建一个新的进程时,在 SMP 系统中
	 * 选择一个合理的 runqueue 来将该进程入队。Scheduler 此时需要考
	 * 虑 负载均衡 问题。
	 */
	int  (*select_task_rq)(struct task_struct *p, int task_cpu, int sd_flag, int flags);
	void (*migrate_task_rq)(struct task_struct *p);

	void (*task_woken) (struct rq *this_rq, struct task_struct *task);

	/* 设置进程的 cpu affinity */
	void (*set_cpus_allowed)(struct task_struct *p, 
				 const struct cpumask *newmask);

	void (*rq_online)(struct rq *rq); /* cpu online */
	void (*rq_offline)(struct rq *rq); /* cpu offline */
#endif

	void (*set_curr_task) (struct rq *rq);
	void (*task_tick) (struct rq *rq, struct task_struct *p, int queued);
	void (*task_fork) (struct task_struct *p);
	void (*task_dead) (struct task_struct *p);

	/*
	 * The switched_from() call is allowed to drop rq->lock, therefore we
	 * cannot assume the switched_from/switched_to pair is serliazed by
	 * rq->lock. They are however serialized by p->pi_lock.
	 */
	void (*switched_from) (struct rq *this_rq, struct task_struct *task); /* @task 从 当前调度类别 切换到 其他调度类别 */
	void (*switched_to) (struct rq *this_rq, struct task_struct *task); /* @task 从 其他调度类别 切换到 当前调度类别 */
	void (*prio_changed) (struct rq *this_rq, struct task_struct *task, /* 优先级变更时的抢占逻辑 */
			     int oldprio);

	/* 获取分配给进程 @task 的时间片 (sys_sched_rr_get_interval()) */
	unsigned int (*get_rr_interval) (struct rq *rq, 
					 struct task_struct *task);

	/* 更新当前进程运行时间统计信息 */
	void (*update_curr) (struct rq *rq);

#define TASK_SET_GROUP  0
#define TASK_MOVE_GROUP	1

#ifdef CONFIG_FAIR_GROUP_SCHED
	void (*task_change_group) (struct task_struct *p, int type);
#endif
};

CFS 调度器类别定义如下:

/*
 * All the scheduling class methods:
 */
const struct sched_class fair_sched_class = {
	.next			= &idle_sched_class,
	.enqueue_task		= enqueue_task_fair,
	.dequeue_task		= dequeue_task_fair,
	.yield_task		= yield_task_fair,
	.yield_to_task		= yield_to_task_fair,

	.check_preempt_curr	= check_preempt_wakeup,

	.pick_next_task		= pick_next_task_fair,
	.put_prev_task		= put_prev_task_fair,

#ifdef CONFIG_SMP
	.select_task_rq		= select_task_rq_fair,
	.migrate_task_rq	= migrate_task_rq_fair,

	.rq_online		= rq_online_fair,
	.rq_offline		= rq_offline_fair,

	.task_dead		= task_dead_fair,
	.set_cpus_allowed	= set_cpus_allowed_common,
#endif

	.set_curr_task          = set_curr_task_fair,
	.task_tick		= task_tick_fair, /* cfs 类定时器周期调度接口: scheduler_tick() -> task_tick_fair() */
	.task_fork		= task_fork_fair, /* 子进程创建时, 更新其虚拟时间 */

	.prio_changed		= prio_changed_fair,
	.switched_from		= switched_from_fair,
	.switched_to		= switched_to_fair,

	.get_rr_interval	= get_rr_interval_fair,

	.update_curr		= update_curr_fair,

#ifdef CONFIG_FAIR_GROUP_SCHED
	.task_change_group	= task_change_group_fair,
#endif
};

2.7 CFS 功能特性

CFS 支持对称多处理器架构 (SMP),其中任何进程(无论是内核还是用户)都可以在任何处理器上执行。然而,可配置的调度域(scheduling domains)可用于对处理器进行分组,以实现负载平衡甚至隔离。如果多个处理器共享相同的调度策略(scheduling policy),则可以选择在它们之间进行负载平衡;如果特定处理器的调度策略与其他处理器不同,则此处理器将在调度方面与其他处理器隔离。

可配置的调度组(scheduling groups)是 CFS 的另一个功能。例如,考虑在电脑上运行的 Nginx Web 服务器的场景。在启动时,此服务器有一个主进程和四个工作进程,它们负责处理 HTTP 请求。对于任何 HTTP 请求,是哪个工作线程处理请求都无关紧要,重要的是及时处理请求,因此四个工作线程一起组成了一个线程池,当请求进入时,可以从中选择处理任务的线程。因此,出于调度目的,将四个 Nginx 工作线程视为一组进行调度,而不是对它们单独调度似乎是公平的,并且可以使用调度组(scheduling groups)来做到这一点。四个 Nginx 工作线程可以配置为它们共享一个 vruntime,而不是每个线程单独一个 vruntime。调度组的配置通过传统的 Linux 方式文件完成。对于 vruntime 共享cgroup 将创建一个名为 cpu.shares 的文件。

通常,CFS 调度器对单个任务进行操作,并努力为每个任务提供公平的 CPU 时间。有时,可能需要对任务进行分组,并为每个此类任务组提供公平的 CPU 时间。例如,可能需要首先为系统上的每个用户提供公平的 CPU 时间,然后再为属于用户的每个任务提供公平的 CPU 时间。

CONFIG_CGROUP_SCHED 配置涵盖的功能,试图实现这一目标:它允许对任务进行分组,并在这些组之间公平地分配 CPU 时间。

CONFIG_RT_GROUP_SCHED 允许对实时任务(即使用 SCHED_FIFOSCHED_RR 调度策略的任务)进行分组。

CONFIG_FAIR_GROUP_SCHED 允许对 CFS 任务(即使用 SCHED_NORMALSCHED_BATCH 调度策略的任务)进行分组。

开启了 CONFIG_FAIR_GROUP_SCHED 后,将为使用 cgroups 伪文件系统创建的每个组创建一个 cpu.shares 文件。请参阅以下示例步骤,以创建任务组并使用 cgroups 伪文件系统 修改 CPU 份额

# mount -t tmpfs cgroup_root /sys/fs/cgroup
# mkdir /sys/fs/cgroup/cpu
# mount -t cgroup -ocpu none /sys/fs/cgroup/cpu
# cd /sys/fs/cgroup/cpu

# mkdir multimedia      # create "multimedia" group of tasks
# mkdir browser         # create "browser" group of tasks

# #Configure the multimedia group to receive twice the CPU bandwidth
# #that of browser group

# echo 2048 > multimedia/cpu.shares
# echo 1024 > browser/cpu.shares

# firefox &     # Launch firefox and move it to "browser" group
# echo <firefox_pid> > browser/tasks

# #Launch gmplayer (or your favourite movie player)
# echo <movie_player_pid> > multimedia/tasks

以上这一系列操作,分别为 多媒体程序浏览器程序 创建了两个任务分组,并指定了它们各自的 CPU 配额

注意,以上这些配置项都依赖于 CONFIG_CGROUPS,并允许管理员使用 cgroup 伪文件系统创建任务组。更多 cgroups 功能,读者可查阅相关资料。

2.8 小结

CFS 说明了如何以一种简单但高效的方式实现一个简单的想法——为每个任务提供公平的处理器资源份额。值得重申的是,CFS 实现了公平高效的调度,而无需固定的时间片和显式的任务优先级等。当然,对更好的调度器的追求仍在继续,如目前最新的
EEVDF, Earliest Eligible Virtual Deadline Firstsched_ext

3. 参考资料

[1] CFS Scheduler
[2] Completely Fair Scheduler
[3] 2.2.3 核心概念 - 调度策略
[4] CFS: Completely fair process scheduling in Linux
[5] Completely fair Scheduler (CFS) and Brain Fuck Scheduler (BFS)
[6] An EEVDF CPU scheduler for Linux
[7] What’s scheduled for sched_ext
[8] sched: Implement BPF extensible scheduler class

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值