调度器
调度:就算按照某种调度的算法设计,从进程的就绪队列中部选取进程分配CPU,主要是协调对CPU等等相关资管的使用。进程调度的目的:最大限度利用CPU事件。
如果调度器支持就绪状态切换到执行状态,同时支持执行状态切换到就绪状态,就称该调度器为抢时间调度器。
Linux内核中用来安排调度进程(一段程序的执行过程)执行的模块称为调度器(Scheduler),它可以切换进程状态(Process Status)。比如:执行、可中断操作、退出、暂停等。
调度器相当于CPU中央处理器的管理员,主要负责完成做一下两件事:
- 选择某些就绪程序来执行
- 打断某些执行的进程让他们变成就绪状态
调度器分配CPU时间的基本依据就是进程的优先级。上下文 切换(context switch):将进程在CPU中切换执行的过程,内核承担
此任务,负责重建和存储被切换掉之前的CPU状态。
调度器类sched_class结构体
Linux内核抽象一个调度器类struct sched_class结构体表示调度类,具体内核源码如下:
struct sched_class {
//当系统中有多个调度类时,按照调度优先级排成一个链表,下一优先级的高类
const struct sched_class *next;
#ifdef CONFIG_UCLAMP_TASK
int uclamp_enabled;
#endif
//将进程加入到执行队列中,即将调度实体放到红黑树中,并对nr_running变量自动加1
void (*enqueue_task) (struct rq *rq, struct task_struct *p, int flags);
//从执行队列中删除进程,并对nr_running变量自动减1
void (*dequeue_task) (struct rq *rq, struct task_struct *p, int flags);
//放弃CPU执行权,实际上该函数执行先出队后入队,在这种情况下,它直接将调度实体放到红黑树的最右端
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);
//选择下一个要运行的进程
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
int (*balance)(struct rq *rq, struct task_struct *prev, struct rq_flags *rf);
//为进程选择一个合适的CPU
int (*select_task_rq)(struct task_struct *p, int task_cpu, int sd_flag, int flags);
//迁移任务到另一个CPU
void (*migrate_task_rq)(struct task_struct *p, int new_cpu);
//专门用于唤醒进程
void (*task_woken)(struct rq *this_rq, struct task_struct *task);
//修改进程在CPU的亲和力
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
//调用time_tick函数,它可能引起进程切换,将驱动运行时抢占
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
成员解析:
- enqueue task:向就绪队列添加一个进程,某个任务进入可运行状态时,该函数将会调用,它将调度实体放入到红黑树当中。
- dequeue_ task:将一个进程从就绪队列中进行删除, 当某个任务退出可运行状态时调用该函数,它将从红黑树中去掉对应调度实体。
- yield_ task: 在进程想要资源放弃对处理器的控制权时,可使用在sched_ yiled系统调用,会调用内核API去处理操作。
- check_ preempt_ curr: 检查当前运行的任务是否被抢占。
- pick_ next_ task: 选择下来要运行的最合适的实体(进程)。
- put_ prev_ task:用于另一个进程代替当前运行的进程。
- set_ curr_ task:当任务修改它调用类或修改它的任务组时,将调用这个函数。
- task_ _tick: 在每次激活周期调度器时,由周期性调度器调用。
调度器类可以分为:stop_sched_class、dl_sched_class、rt_sched_class、fair_sched_class及idle_sched_class
这5种调度类的优先级丛高到低依次为:停机调度类、限期调度类、实时调度类公平调度类、空闲调度类。
- 停机调度类:优先级是最高的调度类,停机进程是优先级最高的进程,可以抢占所有其它进程,其他进程不可能抢占停机进程。
- 限期调度类:最早使用优先算法,使用红黑树把进程按照绝对截止期限丛小到大排序,每次调度时选择绝对截止期限最小的进程。
- 实时调度类:为每个调度优先级维护一个队列。
- 公平调度类:使用完全公平调度算法。完全公平调度算法引入虚拟运行时间的相关概念:虚拟运行时间=实际运行时间*nice0对应的权重/进程的权重。
- 空闲调度类:每个CPU上有一个空闲线程,即0号线程。空闲调度类优先级别最低,仅当没有其他进程可以调度的时候,才会调度空闲线程。
进程的优先级
Linux内核优先级源码如下:
#define MAX_USER_RT_PRIO 100
#define MAX_RT_PRIO MAX_USER_RT_PRIO
#define MAX_PRIO (MAX_RT_PRIO + NICE_WIDTH)
#define DEFAULT_PRIO (MAX_RT_PRIO + NICE_WIDTH / 2)
task_ struct结构体中采用三个成员表示进程的优先级: prio和normal_prio表示动态优先级, static_ prio表示进程的静态优先级。
内核将任务优先级划分,实时优先级范围是0到MAX_ RT_ PRIO-1 (即99) ,而普通进程的静态优先级范围是从MAX_RT_PRIO到MAX PRIO-1 (即100到139)。
进程分类:
实时进程(Real-Time Process) :优先级高、需要立即被执行的进程。
普通进程(Normal Process) :优先级低、更长执行时间的进程。
进程的优先级是一个0- 139的整数来表示。数字越小,优先级越高。其中优先级0-99留给实时进程,100-139留给普通进程。
调度策略
Linux内核提供一些策略共用户应用程序来选择调度器。Linux内核调度策略源码分析如下:
/*
* 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
/* Can be ORed in to make sure the process is reverted back to SCHED_NORMAL on fork */
#define SCHED_RESET_ON_FORK 0x40000000
- SCHED_NORMAL:普通进程调度策略,使task选择CFS调度器来调度运行:
- SCHED_FIFO: 实时进程调度策略,先进先出调度没有时间片,没有更高优先级的状态下,只有等待主动让出CPU;
- SCHED_RR:实时进程调度策略,时间片轮转,进程使用完时间片之后加入优先级对应运行队列当中的尾部,把CPU让给同等优先级的其它进程:
- SCHED_BATCH: 普通进程调度策略,批量处理,使task选择CFS调度器来调度运行:
- SCHED_IDLE: 普通进程调度策略,使task以最低优先级选择CFS调度器来调度运行:
- SCHED_DEADLINE: 限期进程调度策略,使task选择Deadline调度器来调度运行:
其中Stop调度器和IDLE-task调度器仅使用于内核,用户没有办法进行选择。
CFS调度器
CFS是Completely Fair Scheduler 简称,完全公平调度器。在实际当中必须会有进程优先级高或者进程优先级低,CFS调度器引入权重,使用权重代表进
程的优先级,各个进程按照权重比例分配CPU时间。
完全公平调度算法体现在对待每个进程都是公平的,让每个进程都运行一段相同的时间片,这就是基于时间片轮询调度算法。CFS定义一种新调度模型,它给cfs_rq(cfs的run queue)中的每一个进程都设置一个虚拟时钟-virtual runtime(vruntime)。如果一个进程得以执行,随着执行时间的不断增长,其vruntime也将不断增大,没有得到执行的进程vruntime将保持不变。
实际运行时间
假设有2个进程X和Y,X权重为1024,Y权重为2048。
X获得CPU时间比例为: 1024/ (1024+2048)=33%左右,
Y获得CPU时间比例为: 2048/(1024+2048)=66%左右
在引入权重之后分配给进程的时间计算公式如下:实际运行时间=调度曲*进程权重/所有进程权重之和。
虚拟运行时间
运行时间 = 实际运行时间*NICE 0 LOAD/进程权重= (调度周期进程权重/所有进程权重之后) *NICE 0 LOAD/进程权重=调度周期1024/所有进程总权重。
在一个调度周期里面,所有进程的虚拟运行时间是相同的,所以在进程调度时,只需要找到虚拟运行时间最小的进程调度运行即可。
调度器结构分析
进程调度任务:合理分配CPU赶时间给运行的进程。
调度器目标:有效地分配CPU时间片。
调度器通过各个组件模块及一系列数据结构,来排序和管理系统中的进程。他们之间关系图下:
//Linux内核源码如下
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,
.set_next_task = set_next_task_fair,
#ifdef CONFIG_SMP
.balance = balance_fair,
.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
.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
#ifdef CONFIG_UCLAMP_TASK
.uclamp_enabled = 1,
#endif
};
调度器管理是各个调度器的职责,CFS的顶级调度队列struct cts_rq。具体Linu内核结构源码如下:
struct cfs_rq {
struct load_weight load;
unsigned long runnable_weight;
unsigned int nr_running;
unsigned int h_nr_running; /* SCHED_{NORMAL,BATCH,IDLE} */
unsigned int idle_h_nr_running; /* SCHED_IDLE */
u64 exec_clock;
u64 min_vruntime;
#ifndef CONFIG_64BIT
u64 min_vruntime_copy;
#endif
struct rb_root_cached tasks_timeline;
/*
* 'curr' points to currently running entity on this cfs_rq.
* It is set to NULL otherwise (i.e when none are currently running).
*/
struct sched_entity *curr;
struct sched_entity *next;
struct sched_entity *last;
struct sched_entity *skip;
#ifdef CONFIG_SCHED_DEBUG
unsigned int nr_spread_over;
#endif
#ifdef CONFIG_SMP
/*
* CFS load tracking
*/
struct sched_avg avg;
#ifndef CONFIG_64BIT
u64 load_last_update_time_copy;
#endif
struct {
raw_spinlock_t lock ____cacheline_aligned;
int nr;
unsigned long load_avg;
unsigned long util_avg;
unsigned long runnable_sum;
} removed;
#ifdef CONFIG_FAIR_GROUP_SCHED
unsigned long tg_load_avg_contrib;
long propagate;
long prop_runnable_sum;
/*
* h_load = weight * f(tg)
*
* Where f(tg) is the recursive weight fraction assigned to
* this group.
*/
unsigned long h_load;
u64 last_h_load_update;
struct sched_entity *h_load_next;
#endif /* CONFIG_FAIR_GROUP_SCHED */
#endif /* CONFIG_SMP */
#ifdef CONFIG_FAIR_GROUP_SCHED
struct rq *rq; /* CPU runqueue to which this cfs_rq is attached */
/*
* leaf cfs_rqs are those that hold tasks (lowest schedulable entity in
* a hierarchy). Non-leaf lrqs hold other higher schedulable entities
* (like users, containers etc.)
*
* leaf_cfs_rq_list ties together list of leaf cfs_rq's in a CPU.
* This list is used during load balance.
*/
int on_list;
struct list_head leaf_cfs_rq_list;
struct task_group *tg; /* group that "owns" this runqueue */
#ifdef CONFIG_CFS_BANDWIDTH
int runtime_enabled;
s64 runtime_remaining;
u64 throttled_clock;
u64 throttled_clock_task;
u64 throttled_clock_task_time;
int throttled;
int throttle_count;
struct list_head throttled_list;
#endif /* CONFIG_CFS_BANDWIDTH */
#endif /* CONFIG_FAIR_GROUP_SCHED */
};
cfs_rq:跟踪就绪队列信息以及管理就绪态调度实体,并维护找查按照虚拟时间排序的红黑树。tasks_timeline->rbroot是红黑树的根,tasks_ timeline->rb_leftmost指向红黑树中最左边的调度实体,即虚拟赶时间最小的调度实体。