系统中可能运行着各种类型的进程,用户对不同种类进程的期望值是不一样的,例如对于实时进程,我们希望进程具备超高的响应速度,进程一旦就绪就立马被调度到;而对于批处理进程,用户更多关注的是其吞吐量,只要他能够在后台默默运行完成就OK, 因此对于调度器而言,不同的进程类型意味着不同的调度逻辑。
Linux 在 2.6 版本中引入了“调度类(Sched Class)”的概念,意在将调度逻辑模块化,内核通过 struct sched_class
抽象出了调度类的通用行为:
/* file: kernel/sched/sched.h */
struct sched_class {
#ifdef CONFIG_UCLAMP_TASK
int uclamp_enabled;
#endif
void (*enqueue_task)(struct rq *rq, struct task_struct *p, int flags);
void (*dequeue_task)(struct rq *rq, struct task_struct *p, int flags);
/* 任务主动让出 CPU, 但是其状态依然是 runnable */
void (*yield_task)(struct rq *rq);
bool (*yield_to_task)(struct rq *rq, struct task_struct *p);
/* 检查 p 是否会抢占 rq 中当前正在运行的 task. 通常情况下是在 p 进入 runnable
* 状态时,检查 p 是否会抢占当前正在运行的 task */
void (*check_preempt_curr)(struct rq *rq, struct task_struct *p, int flags);
/* 从 rq 中选择下一个任务来运行 */
struct task_struct *(*pick_next_task)(struct rq *rq);
void (*put_prev_task)(struct rq *rq, struct task_struct *p);
void (*set_next_task)(struct rq *rq, struct task_struct *p, bool first);
#ifdef CONFIG_SMP
/* 检查选中的 cpu 在其 sched_domain
中是否负载是平衡的,如果不平衡则需要对任务进行迁移。
* 并不是所有的 scheduling class 都会实现该方法
* */
int (*balance)(struct rq *rq, struct task_struct *prev, struct rq_flags *rf);
/* 当系统调用 fork, exec 创建一个新的 task 时,在 SMP 系统中需要选择一个合理的
* runqueue 来将该 task 入队。Scheduler 此时需要考虑负载均衡问题 */
int (*select_task_rq)(struct task_struct *p, int task_cpu, int flags);
/* 将任务迁移到目标 CPU 上 */
void (*migrate_task_rq)(struct task_struct *p, int new_cpu);
/* 当任务被唤醒(wake up)之后调用 */
void (*task_woken)(struct rq *this_rq, struct task_struct *task);
/* 修改任务的 CPU 偏好,即其可以被调度到哪些 CPU 上运行 */
void (*set_cpus_allowed)(struct task_struct *p, const struct cpumask *newmask,
u32 flags);
void (*rq_online)(struct rq *rq);
void (*rq_offline)(struct rq *rq);
struct rq *(*find_lock_rq)(struct task_struct *p, struct rq *rq);
#endif
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);
void (*switched_to)(struct rq *this_rq, struct task_struct *task);
void (*prio_changed)(struct rq *this_rq, struct task_struct *task,
int oldprio);
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
};
结构体的字段几乎都是函数申明,这对于有 OO 编程经验的同学而言是很熟悉的: struct sched_class
实际上定义了一个接口(Interface),而各个调度类来负责具体的实现。
在讨论调度类的具体实现之前,我们先来看一下调度器是如何通过各个调度类来选择下一个任务的。该逻辑定义在主方法 struct task_struct * pick_next_task()
中,与调度类相关的代码如下:
static inline struct task_struct *
pick_next_task(struct rq *rq, struct task_struct *prev, struct rq_flags *rf) {
const struct sched_class *class;
struct task_struct *p;
for_each_class(class) {
p = class->pick_next_task(rq);
if (p)
return p;
}
/* The idle class should always have a runnable task: */
BUG();
}
调度器按照顺序对具体的调度类进行遍历,依次调用每个调度类的 pick_next_task
方法从指定 rq 中寻找下一个可执行任务,如果返回非空,则将该任务返回。
这里引出了两个问题:
顺序遍历实际上意味着调度类之间存在优先级关系,Linux 总共实现了5种调度类,按照优先级有高到底排序依次为:
stop_sched_class
Stop 是特殊的调度类,内核使用该调度类来停止 CPU. 该调度类用来强行停止CPU 上的其他任务,由于该调度类的优先级最高,因此一旦生效就将抢占任何当前正在运行的任务,并且在运行过程中自己不会被抢占。
该调度类只有在SMP架构的系统中存在,内核使用该调度类来完成负载均衡与CPU热插拔等工作。
dl_sched_class
有些任务必须在指定时间窗口内完成。例如视频的编码与解码,CPU 必须以特定频率完成对应的数据处理;这类任务是优先级最高的用户任务,CPU 应该首先满足。
Deadline 调度类用来调度这类任务,dl 便是单词 Deadline 的缩写,因此该调度类的优先级仅仅低于 Stop 调度类。
rt_sched_class
在本节开头我们提到,实时任务(Real-time Task)对响应时间要求更高,例如编辑器软件,它可能由于等待用户输入长期处于睡眠之中,但一旦用户有输入动作,我们就期望编辑器能够立马响应,而不是等系统完成其它任务之后才开始反应,这一点对用户体验十分重要。
RT 调度类用来调度这类任务,该调度类的优先级低于 DL.
fair_sched_class
Fair 调度类用来调度绝大多数用户任务,CFS 实现的就是这种调度类,其核心逻辑是根据任务的优先级公平地分配 CPU 时间。我们会在后续章节中详细讨论 CFS 的实现细节。
idle_sched_class
与 Stop 类似,Idle 调度类也是仅供内核使用的特殊调度类,其优先级最低,只有在没有任何用户任务时才会用到。内核会为每个 CPU 绑定一个内核线程(kthread)来完成该任务,该线程会在队列无事可做的情况下启动该任务,并将 CPU 的功耗降到最低。
对调度类的遍历通过宏 for_each_class
完成,相关代码定义如下:
/* Defined in include/asm-generic/vmlinux.lds.h */
extern struct sched_class __begin_sched_classes[];
extern struct sched_class __end_sched_classes[];
#define sched_class_highest (__end_sched_classes - 1)
#define sched_class_lowest (__begin_sched_classes - 1)
#define for_class_range(class, _from, _to) \
for (class = (_from); class != (_to); class --)
#define for_each_class(class) \
for_class_range(class, sched_class_highest, sched_class_lowest)
代码从变量 __end_sched_classes
开始,反向遍历至变量 __begin_sched_classes
, 这两个变量定义在文件 include/asm-generic/vmlinux.lds.h
中,顺序如下:
#define SCHED_DATA \
STRUCT_ALIGN(); \
__begin_sched_classes = .; \
*(__idle_sched_class) \
*(__fair_sched_class) \
*(__rt_sched_class) \
*(__dl_sched_class) \
*(__stop_sched_class) \
__end_sched_classes = .;
该顺序便是各个调度类的优先级顺序。
接下来我们简单看一下各个调度类的实现及初始化。
每种调度类的实现都在单独的文件中:
idle: kernel/sched/idle.c
fair: kernel/sched/fair.c
dl: kernel/sched/deadline.c
stop: kernel/sched/stop_task.c
每个调度类都实现了 sched_class
中申明的所有方法,以 idle 调度器为例, pick_next_task
方法的实现如下:
struct task_struct *pick_next_task_idle(struct rq *rq)
{
struct task_struct *next = rq->idle;
set_next_task_idle(rq, next, true);
return next;
}
调度类的每个方法实现,都以该调度类的名称为后缀,内核在初始化时将其与 struct sched_class
中定义的方法关联起来。调度类的初始化工作通过宏 DEFINE_SCHED_CLASS
实现:
#define DEFINE_SCHED_CLASS(name) \
const struct sched_class name##_sched_class __aligned(__alignof__( \
struct sched_class)) __section("__" #name "_sched_class")
可以发现各调度类变量的命名格式: __<class name>_sched_class
, 这与前文中看到的调度类变量名一致。以 idle 为例,初始化代码如下:
DEFINE_SCHED_CLASS(idle) = {
/* no enqueue/yield_task for idle tasks */
/* dequeue is not valid, we print a debug message there: */
.dequeue_task = dequeue_task_idle,
.check_preempt_curr = check_preempt_curr_idle,
.pick_next_task = pick_next_task_idle,
.put_prev_task = put_prev_task_idle,
.set_next_task = set_next_task_idle,
#ifdef CONFIG_SMP
.balance = balance_idle,
.select_task_rq = select_task_rq_idle,
.set_cpus_allowed = set_cpus_allowed_common,
#endif
.task_tick = task_tick_idle,
.prio_changed = prio_changed_idle,
.switched_to = switched_to_idle,
.update_curr = update_curr_idle,
};
其中可以清楚地看到各个方法的绑定逻辑。