1.调度什么?直接把task_struct直接串起来一个个调度?还是专门设计一个结构来做用于调度的实体
2.什么时候调度?也就是何时该去发动调度器去切换下一个任务?
3.怎么调度?策略是什么?当要切换下一个任务时选择的原则是什么?
调度什么?直接把task_struct直接串起来一个个调度?还是专门设计一个结构来做用于调度的实体?
内核中有一个结构叫运行列队(runqueue),根据实时和完全公平调度器类的需求,里面嵌入了cfs_rq和rt_rq两个列队:
struct rq {
raw_spinlock_t lock;
struct cfs_rq cfs;//完全公平调度器的运行列队
struct rt_rq rt;//实时调度器的运行列队
struct task_struct *curr, *idle, *stop;
u64 clock;
}
再看下task_struct和调度实体相关的结构sched_entity和sched_rt_entity:
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;
}
可以猜测内核通过在task_struct中嵌入sched_[xxx_]entity,以这些entity为"锚点"将进程在rq中的[xxx]_rq组织起来。
先看实时进程的组织方式:
struct rt_rq {
struct rt_prio_array active;
unsigned int rt_nr_running;
unsigned int rr_nr_running;
}
struct rt_prio_array {
DECLARE_BITMAP(bitmap, MAX_RT_PRIO+1); /* include 1 bit for delimiter */
struct list_head queue[MAX_RT_PRIO];
};
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;
}
很明显,sched_rt_entity根据进程的实时优先级通过run_list挂到rt_rq.active.[实时优先级]这条双链表中。
再看下普通进程:
struct cfs_rq {
unsigned int nr_running, h_nr_running;
struct rb_root_cached tasks_timeline;
struct sched_entity *curr, *next, *last, *skip;
}
struct sched_entity {
struct rb_node run_node;
}
很明显,由于普通进程数量比较多,使用红黑树来管理,查找效率会高很多。普通进程中的sched_entity是通过run_node挂到以cfs_rq.tasks_timeline为根节点的红黑树中。
sched.h中在每个CPU中都创建了一个名为runqueues的rq结构:
DECLARE_PER_CPU_SHARED_ALIGNED(struct rq, runqueues);
总结成一张图说明内核如何将进程以调度实体组织起来:
什么时候调度?也就是何时该去发动调度器去切换下一个任务?
假设所有进程都非常自觉,当自己在等资源时会主动去睡眠,或者当自己运行一段时间时会主动放弃CPU让其他进程运行,那么世界就是美好的,大家都相互礼让一片和谐。但是事实是残酷的,这种情况一般都存在于一些封闭系统(封闭系统的软件一般都是精心设计过的)。
在linux这样的开放式内核有两个核心调度器:scheduler_tick和schedule,当一个进程觉得自己无事可做时,可以自觉地调用schedule放弃CPU。当然,对于不够自觉想霸占CPU的进程,内核会周期性的调用scheduler_tick检查当前进程是不是占用CPU太久,进而使它放弃CPU。
怎么调度?策略是什么?当要切换下一个任务时选择的原则是什么?
首先不得不提下linux的三大调度器类(其实是有五个):
rt_sched_class:顾名思义,实时调度器类,用来调度实时任务
fair_sched_class:顾名思义,公平调度器类,用来调度普通任务(这个世界80%是普通人,在一个相对和平合理的规则下,普通人就会简单而又平淡的度过一生)
idle_sched_class:这个有点特殊,每个CPU都只有一个进程属于这个idle调度器类那就是0号进程swapper,它再创建了1和2号进程之后,将自己的调度策略设为SCHED_IDLE,然后就休眠了。
sched_class可以理解成所有调度器类的基类,每个调度器类都实现了自己的一套方法,比如完全公平调度器:
/*
* All the scheduling class methods:
*/
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_curr_task = set_curr_task_fair,
.task_tick = task_tick_fair,
.task_fork = task_fork_fair,
...
.update_curr = update_curr_fair,
#ifdef CONFIG_FAIR_GROUP_SCHED
.task_change_group = task_change_group_fair,
#endif
};
在面对对象编程中有一个重要的特性叫多态,多态即在继承父类之后,子类可以直接继承或者扩展自己的方法和属性,调度器类的设计就有这种思路在里面:
假设一种调度思路,先设计一个主调度器,再设计若干个调度器类(不一定一次性设计出多个可以后续根据需求添加)。主调度器的主要工作负责在确定调度时机,当需要切换下一个任务时根据一定的顺序在各个调度器类中选出一个新的任务进行调度即可,如下面伪代码:
class 主调度器:
@classmethod
def 注册一个调度器类(调度器类):
...
def 调度:
for 调度器类 in 所有调度器类:
if (新进程 = 调度器类.选取一个新进程()) != Null
break
self.切换到新进程(新进程)
def main:
主调度器.注册一个调度器类(新调度器类)
主调度器.注册一个调度器类(新调度器类)
主调度器.注册一个调度器类(新调度器类)
while(true):
...
if 调度时机成熟:
主调度器.调度()
...
...