调度策略与调度类
Linux 中进程大概可以分成两种:
- 实时进程
需要尽快执行返回结果,优先级会比较高 - 普通进程
大部分的进程都是这种,按照正常流程完成
调度策略
task_struct 成员变量
\include\linux\sched.h
unsigned int policy;
几个定义
\include\linux\sched.h
#define SCHED_NORMAL 0
#define SCHED_FIFO 1
#define SCHED_RR 2
#define SCHED_BATCH 3
#define SCHED_IDLE 5
#define SCHED_DEADLINE 6
优先级
task_struct 成员变量
\include\linux\sched.h
int prio, static_prio, normal_prio;
unsigned int rt_priority;
优先级就是一个数值
- 优先级的范围是 0~99,对于实时进程
- 优先级的范围是 100~139,对于普通进程
- 数值越小,优先级越高
实时调度策略
SCHED_FIFO、SCHED_RR、SCHED_DEADLINE 是实时进程的调度策略。
- SCHED_FIFO
相同优先级的进程,遵循先来先得 - SCHED_RR
轮流调度算法,采用时间片,相同优先级的任务当用完时间片会被放到队列尾部 - SCHED_DEADLINE
按照任务的 deadline 进行调度,总是选择其 deadline 距离当前时间点最近的那个任务
普通调度策略
SCHED_NORMAL、SCHED_BATCH、SCHED_IDLE
- SCHED_NORMAL
普通的进程 - SCHED_BATCH
后台进程,几乎不需要和前端进行交互 - SCHED_IDLE
特别空闲的时候才跑的进程
调度策略的执行逻辑
task_struct 成员变量
\include\linux\sched.h
const struct sched_class *sched_class;
sched_class 指向封装了调度策略执行逻辑的类
sched_class 的几种实现:
- stop_sched_class
优先级最高的任务会使用这种策略,会中断所有其他线程,且不会被其他任务打断; - dl_sched_class
对应上面的 deadline 调度策略 - rt_sched_class
对应 RR 算法或者 FIFO 算法的调度策略,具体调度策略由进程的 task_struct->policy 指定 - fair_sched_class
普通进程的调度策略 - idle_sched_class
空闲进程的调度策略
完全公平调度算法
在 Linux 里面,实现了一个基于 CFS 的调度算法。CFS 全称 Completely Fair Scheduling,叫完全公平调度。
记录下进程的运行时间,CFS 会为每一个进程安排一个虚拟运行时间 vruntime。
虚拟运行时间 vruntime += 实际运行时间 delta_exec * NICE_0_LOAD/ 权重
虚拟运行时间相同,权重高的(优先级高)实际运行时间多,权重低的(优先级低)实际运行时间少,
vruntime 少的,需要给它补上,所以会优先运行这样的进程。
调度队列与调度实体
以完全公平调度算法为例
CFS 需要一个数据结构来对 vruntime 进行排序,找出最小的那个
- 这个数据结构在查询的时候,能够快速找到最小的,更新的时候也需要能够快速地调整排序
- 能够平衡查询和更新速度的是树,在这里使用的是红黑树
调度实体
红黑树的的节点称为调度实体,包括了 vruntime
task_struct 成员变量
\include\linux\sched.h
struct sched_entity se;
struct sched_rt_entity rt;
struct sched_dl_entity dl;
实时调度实体 sched_rt_entity,Deadline 调度实体 sched_dl_entity,以及完全公平算法调度实体 sched_entity。
进程根据自己是实时的,还是普通的类型,通过这个成员变量,将自己挂在某一个数据结构里面,和其他的进程排序,等待被调度。如果这个进程是个普通进程,则通过 sched_entity,将自己挂在这棵红黑树上。
普通进程的调度实体
\include\linux\sched.h
struct sched_entity {
struct load_weight load;
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;
struct sched_statistics statistics;
......
};
红黑树
所有可运行的进程通过不断地插入操作最终都存储在以时间为顺序的红黑树中,vruntime 最小的在树的左侧,vruntime 最多的在树的右侧。
CFS 调度策略会选择红黑树最左边的叶子节点作为下一个将获得 CPU 的任务。
进程队列
每个 CPU 都有自己的 struct rq 结构,其用于描述在此 CPU 上所运行的所有进程,其包括一个实时进程队列 rt_rq 和一个 CFS 运行队列 cfs_rq,这棵红黑树放在这里。
实时进程队列 rt_rq
\kernel\sched\sched.h
struct rq {
/* runqueue lock: */
raw_spinlock_t lock;
unsigned int nr_running;
unsigned long cpu_load[CPU_LOAD_IDX_MAX];
......
struct load_weight load;
unsigned long nr_load_updates;
u64 nr_switches;
struct cfs_rq cfs;
struct rt_rq rt;
struct dl_rq dl;
......
struct task_struct *curr, *idle, *stop;
......
};
普通进程公平队列 cfs_rq
\kernel\sched\sched.h
/* CFS-related fields in a runqueue */
struct cfs_rq {
struct load_weight load;
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;
struct sched_entity *curr, *next, *last, *skip;
......
};
rb_root 就是红黑树的根节点,rb_leftmost 指向的是最左面的节点
\include\linux\rbtree.h
struct rb_node {
unsigned long __rb_parent_color;
struct rb_node *rb_right;
struct rb_node *rb_left;
} __attribute__((aligned(sizeof(long))));
/* The alignment might seem pointless, but allegedly CRIS needs it */
struct rb_root {
struct rb_node *rb_node;
};
数据结构的关系
调度类是如何工作的
调度类的定义
\kernel\sched\sched.h
struct sched_class {
const struct sched_class *next;
void (*enqueue_task) (struct rq *rq, struct task_struct *p, int flags);
void (*dequeue_task) (struct rq *rq, struct task_struct *p, int flags);
void (*yield_task) (struct rq *rq);
bool (*yield_to_task) (struct rq *rq, struct task_struct *p, bool preempt);
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.
*/
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
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);
void (*set_cpus_allowed)(struct task_struct *p,
const struct cpumask *newmask);
void (*rq_online)(struct rq *rq);
void (*rq_offline)(struct rq *rq);
#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);
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
};
调度类分为这几种
\kernel\sched\sched.h
extern const struct sched_class stop_sched_class;
extern const struct sched_class dl_sched_class;
extern const struct sched_class rt_sched_class;
extern const struct sched_class fair_sched_class;
extern const struct sched_class idle_sched_class;
取下一个任务 pick_next_task
\kernel\sched\sched.h
/*
* Pick up the highest-prio 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;
/*
* Optimization: we know that if all tasks are in the fair class we can
* call that function directly, but only if the @prev task wasn't of a
* higher scheduling class, because otherwise those loose the
* opportunity to pull in more work from other CPUs.
*/
if (likely((prev->sched_class == &idle_sched_class ||
prev->sched_class == &fair_sched_class) &&
rq->nr_running == rq->cfs.h_nr_running)) {
p = fair_sched_class.pick_next_task(rq, prev, rf);
if (unlikely(p == RETRY_TASK))
goto again;
/* Assumes fair_sched_class->next == idle_sched_class */
if (unlikely(!p))
p = idle_sched_class.pick_next_task(rq, prev, rf);
return p;
}
again:
for_each_class(class) {
p = class->pick_next_task(rq, prev, rf);
if (p) {
if (unlikely(p == RETRY_TASK))
goto again;
return p;
}
}
/* The idle class should always have a runnable task: */
BUG();
}
这里面有一个 for_each_class 循环,沿着上面调度类分类的顺序,依次调用每个调度类的方法。调度的时候是从优先级最高的调度类到优先级低的调度类,依次执行
对于每种调度类,有自己的实现,例如,CFS 就有 fair_sched_class。
\kernel\sched\fair.c
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,
.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
};
同理 在 \kernel\sched\rt.c 有 const struct sched_class rt_sched_class
对于同样的 pick_next_task 选取下一个要运行的任务这个动作,不同的调度类有自己的实现。fair_sched_class 的实现是 pick_next_task_fair,rt_sched_class 的实现是 pick_next_task_rt。
- pick_next_task_fair
\kernel\sched\fair.c
static struct task_struct *
pick_next_task_fair(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
- pick_next_task_rt
\kernel\sched\fair.c
static struct task_struct *
pick_next_task_rt(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
sched_class 定义的与调度有关的函数
- enqueue_task 向就绪队列中添加一个进程,当某个进程进入可运行状态时,调用这个函数;
- dequeue_task 将一个进程从就绪队列中删除;
- pick_next_task 选择接下来要运行的进程
- put_prev_task 用另一个进程代替当前运行的进程
- set_curr_task 用于修改调度策略
- task_tick 每次周期性时钟到的时候,这个函数被调用,可能触发调度。
总结
每个 CPU 上都有一个队列 rq,这个队列里面包含多个子队列,例如 rt_rq 和 cfs_rq,不同的队列有不同的实现方式,cfs_rq 就是用红黑树实现的。
某个 CPU 需要找下一个任务执行的时候,会按照优先级依次调用调度类,不同的调度类操作不同的队列。当然 rt_sched_class 先被调用,它会在 rt_rq 上找下一个任务,只有找不到的时候,才轮到 fair_sched_class 被调用,它会在 cfs_rq 上找下一个任务。这样保证了实时任务的优先级永远大于普通任务。
例子:
fair_sched_class 对于 pick_next_task 的实现 pick_next_task_fair
调用路径如下:pick_next_task_fair->pick_next_entity->__pick_first_entity。
\kernel\sched\fair.c
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);
}
一个 CPU 上有一个队列,CFS 的队列是一棵红黑树,树的每一个节点都是一个 sched_entity,每个 sched_entity 都属于一个 task_struct,task_struct 里面有指针指向这个进程属于哪个调度类
参考资料:
趣谈Linux操作系统(极客时间)链接:
http://gk.link/a/10iXZ
欢迎大家来一起交流学习