(注:内核版本为4.4.42)
一、任务调度
1.1 需求分析
几乎所有现代的操作系统都支持多个任务并发运行,即使系统只有一个cpu,内核与处理器的合作也能够给用户建立多任务的错觉。其中的奥秘就在于任务之间的快速切换,使得用户根本感觉不到短暂的停滞。这种任务之间快速切换的效果就是通过内核中任务调度模块来实现的。
任务调度需要保证不同类型任务可以得到合理的cpu时间,既不会影响自身的工作延误,也不会造成其他任务的饥饿。
1.2 内核调度策略
任务调度从早期的基于优先级的调度先后经历过O(1)调度、电梯调度、RSDL调度到目前的CFS公平调度已经发展到了linux-4.X版本。目前的内核中采用了分组、分策略的调度方式:(1) 在引入了组调度的情况下调度器操作的对象已经抽象为调度实体,调度实体作为一个组又可以在下一层挂多个子层级的调度实体;
(2) 不同的任务有不同的优先级和调度策略,目前系统中有诸如rt_sched_class、fair_sched_class、dl_sched_class等调度类,不同调度类的任务,其入队/出队,任务选择等等操作都随之不同;
(3) 主调度器在关键路径(如异常、中断返回)调用schedule()和pick_next_task()来释放当前任务、选择一个更适合的任务来运行;
(4) 周期调度器在每个周期先后遍历不同调度策略上的调度实体,对其调度实体的调度信息进行更新。
二、初识实时调度
实时调度的设计目标要求这类任务严格按照优先级的顺序执行,其优先级通过人为配置好以后不会发生动态改变。优先级越高,说明设计者对此任务的实时性要求越是紧迫,所以其实时性也需要得到相应的保障。2.1 实时调度的优先级
任务调度依赖于优先级的思想由来已久,不同的任务类型有不同的优先级;同一种类型的不同任务也有不同的优先级。目前的系统中优先级可取的范围从[0,140),其中实时任务只在[0,99]之间,而[100,140)的则为普通任务。而任务结构task_struct中却有四个优先级相关的字段,我们需要来区分一下这些字段的含义:
struct task_struct {
......
int prio, static_prio, normal_prio;
unsigned int rt_priority;
......
}
其中static_prio与普通任务的相关,其值在[100, 140),数字越小优先级越高;而rt_priority顾名思义记录的是RT类型任务的[0, 99] ,数字越大优先级越高。rt_priority对于普通任务无意义,static_prio对于实时任务无意义。
这样两种优先级的表示方式容易让人搞混,不过还有一个normal_prio,整合了rt_priority和static_prio。其取值范围为[0,140),而且统一规定normal_prio值越小其优先级越高。这样normal_prio值在[0,99]就表示实时优先级;而[100,140)则表示普通优先级。
最后一个prio,一般称为动态优先级。内核在发生优先级翻转的现象时,任务的优先级优先级会临时升高,这时就用piro来表示这个由于优先级翻转而动态升高的优先级。通过 比较任务的prio与normal_prio可以知道是否发生了优先级翻转,如果prio与normal_prio不同则表明发生了优先级继承,否则它们具有相同的值。
当然优先级值并非是划定任务类型的唯一标准,还需要考虑任务的调度策略,即task_struct.sched_class。
2.2、实时任务的调度思想
实时任务的调度思想比较简单,按照优先级来调度。每次调度时机到来时(思考:何时发生调度?)选择优先级最高的那个任务来运行。如果系统中优先级最高任务不止一个,那么不同的调度策略就会有不同的调度方式。内核中两种主要的两类任务普通任务和实时任务,它们各自又有不同的调度策略,这是通过task_struct.policy来表示的。
普通任务有SCHED_NORMAL、SCHED_BATCH和SCHED_IDLE几种策略;实时任务有SCHED_RR和SCHED_FIFO两种不同的策略。
其中SCHED_RR为轮转调度,这类任务有时间片的概念。每次时钟中断到来时都会检查RR类型的任务时间片是否用尽,如果用尽则挂到队列尾部,之后要等到当前队列上其他RR任务再执行一遍后它才能再次得到执行;时间片用sched_rt_entity.time_slice来表示;
而SCHED_FIFO任务则没有时间片概念,采取的则是高优先级的任务先执行,优先级相同的任务按照先来先执行的顺序进行调度执行。
2.3 实时调度任务的组织
讲完了优先级,我们再来看看实时任务是如何组织的。在现代的调度器中引入了组调度的概念,因而对调度的对象进行了抽象,将调度的对象抽象为调度实体。对于rt任务,就是sched_rt_entity,它有可能代表一个调度组,也有可能代表一个task。
(1) rt调度实体sched_rt_entity
调度实体可以是一个任务,也可以是一个组。不论他们的身份如何,都需要挂到运行队列上接受调度。
struct sched_rt_entity {
struct list_head run_list; /* 调度实体通过此链挂到运行队列上 */
unsigned long timeout;
unsigned long watchdog_stamp;
unsigned int time_slice; /* RR策略任务的时间片 */
struct sched_rt_entity *back;
#ifdef CONFIG_RT_GROUP_SCHED
struct sched_rt_entity *parent; /* 上一层级的调度实体 */
/* rq on which this entity is (to be) queued: */
struct rt_rq *rt_rq; /* 所属组的运行队列 */
/* rq "owned" by this entity/group: */
struct rt_rq *my_q; /*
* 本宝宝自己的运行队列:里面挂了好多朕的子民;
* 如果自己是一个task,则my_q为NULL。
*/
#endif
};
(2) rt运行队列rt_rq
所有处于就绪状态的任务只有挂到运行队列rt_rq上才有机会得到cpu的调度,实际上rt_rq上挂接的是调度实体。
/* Real-Time classes' related field in a runqueue: */
struct rt_rq {
struct rt_prio_array active; /* 优先级队列,调度实体就通过run_list挂到active.queue[prio]上 */
unsigned int rt_nr_running; /* rt_rq上的调度实体个数 */
#if defined CONFIG_SMP || defined CONFIG_RT_GROUP_SCHED
struct {
int curr; /* highest queued rt task prio */
#ifdef CONFIG_SMP
int next; /* next highest */
#endif
} highest_prio; /* 运行队列上最高和次高的优先级,对应的是任务的prio成员 */
#endif
......
int rt_throttled; /* 调度限制,如果置为1,则限制调度 */
u64 rt_time; /* 本运行队列上所有rt_ses已经消耗的时间 */
u64 rt_runtime; /* 本运行队列上的时间额度 */
.....
struct rq *rq; /* rt_rq所在的就绪队列 */
struct task_group *tg; /* 此运行队列所属的组 */
}
/*
* This is the priority-queue data structure of the RT scheduling class:
*/
struct rt_prio_array {
/* 优先级位图:表示优先级为prio的队列上挂有调度实体 */
DECLARE_BITMAP(bitmap, MAX_RT_PRIO+1); /* include 1 bit for delimiter */
struct list_head queue[MAX_RT_PRIO]; /* RT调度实体按优先级挂到相应的queue[prio]队列上 */
};
(3) 就绪队列
每一个调度实体都挂接到所属的上一层的rt_se.rt_rq上;而如果调度实体是一个组,它也有自己的运行队列,所有其下属的调度实体都挂接到自己的rt_se.my_q上;
这样一个cpu上的调度实体都通过rt_rq结构按组层次组织起来,最顶层的rt_rq我们称作top rt_rq,里面挂接的是所有第一层的调度实体,而这个top rt_rq是放在一个称作就绪队列的struct rq中。
就绪队列是一个静态的per_cpu变量,称为runqueues,即在每个cpu上都有唯一的就绪队列rq。我们可以根据cpu号,就能找到对应的rq,再沿着top rt_rq、rt_rq->active.queue[prio]、rt_se->my_q一层层遍历到此cpu上的所有任务(即这颗层级树上的叶子节点)。
/*
* 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;
struct rt_rq rt; /* 就绪队列上的top rt_rq */
struct dl_rq dl;
......
struct task_struct *curr, *idle, *stop; /* curr为rq上正在运行的任务 */
......
/* cpu of this runqueue: */
int cpu; /* 所在的cpu */
int online;
}
(4) 任务组
上面介绍的各个结构 rq --> rt_rq -->rt_se 都是针对一个特定的cpu按层级关系组织起来,而任务组则关注了的是某一个层级上所有cpu的调度对象。
/* task group related information */
struct task_group {
......
#ifdef CONFIG_RT_GROUP_SCHED
struct sched_rt_entity **rt_se; /* 调度实体指针数组,每个cpu一个rt_se数组 */
struct rt_rq **rt_rq; /* 与上面的rt_se类似,每个cpu一个rt_rq数组,它们的关系:rt_rq[cpu] == rt_se[cpu]->my_q */
struct rt_bandwidth rt_bandwidth; /* 任务组的带宽,用以限制实时任务的运行时间 */
#endif
struct rcu_head rcu;
struct list_head list;
struct task_group *parent; /* 上层级的task_group */
struct list_head siblings;
struct list_head children;
};
整个组织结构简图如下所示:
图1 RT任务就绪队列到调度实体的结构关系