调度简介
最近在学习linux 内核调度相关的知识,也看了好多文章,内核代码,下面就学习的内容做个梳理,有助于自己的记忆,可能有的地方自己理解不对,希望多多指教。下面就从几个基本概念开始。
什么是调度
有过点操作系统知识的人都知道,调度的工作主要就是完成操作系统内进程切换的过程,让多个进程多个任务可以轮流的使用cpu的资源。那么在这个过程中涉及到很多调度相关的概念。下面就从这些简单的概念开始
调度的对象–线程
进程和线程的概念这里就不再详细的说,大家都知道。其实在内核里面,对于调度来说是不分进程还是线程概念的,调度的对象就是一个task,对应的结构就是task_strct(如下图所示),这个数据结构很大很大,包含了很多进程相关的信息。
其实在结构内和调度相关的几个成员如下:
struct task_struct {
......
const struct sched_class *sched_class;
struct sched_entity se;
struct sched_rt_entity rt;
......
struct sched_dl_entity dl;
......
unsigned int policy;
int on_rq;
int prio;
int static_prio;
int normal_prio;
unsigned int rt_priority;
......
}
每个成员干啥的主要如下:
sched_class:表示调度器类,内核由于进程类型不同,实时进程和普通进程等,针对不同的进程采用不同的调度策略,和在就绪队列的链表存储上就做了区分,不同类型的进程,以单独的链表存储,又都在每个cpu对应的rq上。所以这里就有了调度器类的概念。而每种调度器如何调度(也就是调度算法)都在上面提到的主调度器和周期性调度器中有体现。
se/rt/dl:分别对应不同调度器类对应的调度实体,se是cfs调度器的调度实体,rt是RT调度器类对应的调度实体,dl是DL调度器类对应的调度实体。这里提到调度实体,什么是调度实体,后面会讲解。
policy:表示这个进程所采用的调度策略,其实也就表示了是什么进程。在内核中的取值如下:
#define SCHED_NORMAL 0//按照优先级进行调度(有些地方也说是CFS调度器)
#define SCHED_FIFO 1//先进先出的调度算法
#define SCHED_RR 2//时间片轮转的调度算法
#define SCHED_BATCH 3//用于非交互的处理机消耗型的进程
#define SCHED_IDLE 5//系统负载很低时的调度算法
也就是内核中对应的调度策略。
on_rq:表示这个task所在的rq队列。
prio:进程的优先级,是进程最终调度的时候使用的优先级。
优先级的范围是0-139,其中0-99是属于实时进程的优先级,100-139是普通进程的优先级,数值越小,优先级越高。
static_prio:静态进程优先级,在进程创建的时候赋值(个人理解和nice值有一定的关系)。可以通过nice()接口修改。
normal_prio:根据静态优先级和调度策略计算出来的优先级,子进程会继承父进程的这个值。
对于普通进程,normal_prio=static_prio,对于实时进程normal_prio=MAX_RT_PRIO(100内核里的宏)-1-rt_prio。
rt_priority:实时进程的优先级,只用于实时进程,数值越大,优先级越高。
疑问:(其实这里有个问题没搞懂明白,pro 是最终用于调度的优先级,那么他和下面的static_prio /normal_prio/rt_prio是啥关系呢?,回头需要研究一下,有了解的可以评论区回答。)
调度器
调度器就是完成任务调度的主题。任务切换的主要工作就是由调度器完成的。在内核里面其实由两个调度器,分别为主调度器核周期性调度器。其实这两个调度器就是指两个函数。主调度器schedule()和周期性调度器scheduler_tick()。其实任务切换的过程都是由这两个函数完成的,schedule()函数是主调度器,大多数场景是任务(task)主动去调用,完成进程切换。其实进程切换的过程,也是一个很复杂的过程,主要包含硬件寄存器上下文的切换和软件上下文的切换。这里不做详细的介绍,后面会单独写一篇上下文切换都做了什么的文章来介绍。而周期性调度器scheduler_tick()是以定时器中断的方式定时触发的,也就是每隔一段时间每个cpu都会触发这个中断,触发这个函数,来检查当前任务时间片是否用完,或者需要调度,如果需要调度就完成调度的工作。其实这个函数还做了很多关于进程切换的统计信息,包括时间信息,任务负载等信息。这里不做详细的介绍。
正如上图所示,主调度器和周期性调度器一起完成了内核里面进程切换的工作。让操作系统里面的每一个任务看似公平的在使用cpu的资源。给人的感觉就是每一个任务都在并行的运行。其实在单核处理器上,都知道每一个task在宏观上是并行的,但是其实微观上都是串行执行的,只有在smp多核处理器上才可以称的上微观上并行处理。
调度器类
内核中调度器类主要有以下下五种,分别为
stop_sched_class/
dl_sched_class/
rt_sched_class/
fair_sched_class/
idle_sched_class
其优先级依次降低,在调度器里面进行任务切换的时候,首先会在rq里面按照这个优先级遍历对应的任务链表,看是否有就绪的任务等待运行,有的话会优先先择高优先级调度器类里面的任务运行。例如:rt_sched_class对应的就绪任务链表里的任务,这个任务肯定要比fair_sched_class对应的就绪任务链表里的任务优先运行。
调度策略
内核里面主要有的调度策略如下:
SCHED_DEADLINE
SCHED_RR
SCHED_FIFO
SCHED_NORMAL
SCHED_BATCH
SCHED_IDLE
每种调度策略都对应这一种调度算法,
SCHED_DEADLINE:对应着deadline调度算法,算法的精确名称是Earliest DeadlineFirst,详细的算法原理这里先不做详细的介绍了。其实每一种调度算法都可以作为一篇文章来详细的讲解一下。
SCHED_RR:对应着Roound-Robin算法
SCHED_FIFO:FIFO算法
这两种是属于实时进程的调度算法,这两种调度算法也是有区别的。
SCHED_NORMAL
SCHED_BATCH
这两个对应这cfs调度算法。
SCHED_IDLE没有算法,表示没有进程调度的时候就会调用idle线程。
调度器类和调度策略的对应关系
下图表示了调度器类和调度策略之间的关系,以及每种调度测率对用的调度算法。
调度器类,调度策略,优先级可以概括如下图的关系:
调度实体
谈到调度实体其实具体就是指
struct sched_entity se/
struct sched_rt_entity rt/
struct sched_dl_entity dl
每一个进程的结构task_struct结构内都包含这三种类型的调度实体,为什么会有三种类型的调度实体呢?其实就是为了方便每种调度算法在选择进程的时候用的计算的key和计算方式不同。当然不同的进程,对应不同的调度器类,也对应这不同的调度算法,不同的调度算法就意味着不同的计算key和计算方式。
cfs 调度实体sched_entity se
下面来看看c普通cfs 调度算法用的se的代码结构:
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;
#ifdef CONFIG_FAIR_GROUP_SCHED
int_______depth;
struct sched_entity___*parent;
struct cfs_rq_____*cfs_rq;
struct cfs_rq_____*my_q;
unsigned long_____runnable_weight;
#endif
#ifdef CONFIG_SMP
struct sched_avg____avg;
#endif
};
其中涉及到的结构类型如下
struct load_weight {
unsigned long_____weight;
u32_______inv_weight;
};
struct rb_node {
unsigned long __rb_parent_color;
struct rb_node *rb_right;
struct rb_node *rb_left;
} __attribute__((aligned(sizeof(long))));
struct sched_avg {
u64_______last_update_time;
u64_______load_sum;
u64_______runnable_sum;
_u32_______util_sum;
u32_______period_contrib;
unsigned long_____load_avg;
unsigned long_____runnable_avg;
unsigned long_____util_avg;struct util_est_____util_est;
}____cacheline_aligned;
struct sched_statistics {
#ifdef CONFIG_SCHEDSTATS
u64_______wait_start;
u64_______wait_max;
_u64_______wait_count;
u64_______wait_sum;
u64_______iowait_count;
u64_______iowait_sum;
u64_______sleep_start;
u64_______sleep_max;
s64_______sum_sleep_runtime;
u64_______block_start;
u64_______block_max;
_u64_______exec_max;
u64_______slice_max;
u64_______nr_migrations_cold;
u64_______nr_failed_migrations_affine;
u64_______nr_failed_migrations_running;
u64_______nr_failed_migrations_hot;
u64_______nr_forced_migrations;
u64_______nr_wakeups;
u64_______nr_wakeups_sync;
u64_______nr_wakeups_migrate;
u64_______nr_wakeups_local;
u64_______nr_wakeups_remote;
u64_______nr_wakeups_affine;
u64_______nr_wakeups_affine_attempts;
u64_______nr_wakeups_passive;
u64_______nr_wakeups_idle;
具体里面每一个变量是干啥用的,这里不做详细的介绍了。后期再讲解cfs调度算法的时候再详细的讲解。
rt 调度实体struct sched_rt_entity rt
下面是struct sched_rt_entity 的结构:
struct sched_rt_entity {
struct list_head____run_list;
unsigned long_____timeout;
unsigned long_____watchdog_stamp;
unsigned int______time_slice;
unsigned short______on_rq;
unsigned short______on_list;
struct sched_rt_entity____*back;
#ifdef CONFIG_RT_GROUP_SCHED
struct sched_rt_entity____*parent;
struct rt_rq______*rt_rq;
struct rt_rq______*my_q;
#endif
}__randomize_layout;
dl调度实体struct sched_dl_entity dl
struct sched_dl_entity结构如下:
runqueue 运行队列struct rq rq
在内核里面,每一个cpu都有一个自己的运行队列,里面存放,就绪的task,每个task以链表节点的形式存在。
每个运行队列中有三个调度队列,task 作为调度实体加入到各自的调度队列中。
这个rq 是每cpu变量,cpu在调度的时候会从自己的rq 里面获取task。
struct rq rq结构
如下图所示:
从上图可知道,这个结构中包含很多变量,这里不详细介绍每一个变量是干啥用的了。
其实主要的就是里面的三种调度器类对应的运行队列。
struct rq {
......
struct cfs_rq cfs;
struct rt_rq rt;
struct dl_rq dl;
......
}
三个调度队列:
struct cfs_rq cfs:CFS调度队列
struct rt_rq rt:RT调度队列
struct dl_rq dl:DL调度队列
下面详细的看一下每种调度队列的具体数据结构:
struct cfs_rq cfs
结构如下所示:
struct rt_rq rt
结构如下:
struct dl_rq dl
struct dl_rq结构如下:
在调度的时候,首先拿到自己的rq指针,然后按照调度器类的优先级依次遍历dl->rt->cfs的运行队列,然后根据不同的调度运行队列里面的调度算法选取se,然后根据se拿到task,最后做进程切换。