核心调度器
调度器的实现基于两个函数:周期性调度器函数和主调度器函数。这些函数根据现有进程的优先级分配CPU时间。这也是为什么整个方法称之为优先调度的原因。
周期性调度器函数
周期性调度器在scheduler_tick中实现,如果系统正在活动中,内核会按照频率HZ自动调用该函数。该函数主
要有两个任务如下:
更新统计量函数:update_rq_clock()/calc_global_load_tick()
主调度器函数
在内核中的许多地方,如果要将CPU分配给与当前活动进程不同的另一个进程,都会直接调用主调度器函数(schedule)
主调度器负责将CPU的使用权从一个进程切换到另一个进程。周期性调度器只是定时更新调度相关的统计信息。
cfs队列实际上是用红黑树组织的,rt队列是用链表组织的。
调度类
为方便添加新的调度策略,Linux内核抽象了一个调度类sched_class,目前为止实现5种调度类:
调度类 | 调度策略 | 调度算法 | 调度对象 | task_tick函数 |
---|---|---|---|---|
stop_sched_class(停机调度类) | 无 | 无 | 停机进程 | task_tick_stop |
dl_sched_class(限期调度类) | SCHED_DEADLINE | 最早期限有限 | 限期进程 | task_tick_dl |
rt_sched_class(实时调度类) | SCHED_FIFO | 先进先出 | 实时进程 | task_tick_rt |
SCHED_RR | 轮流调度 | 实时进程 | task_tick_rt | |
fair_sched_class(公平调度类) | SCHED_NORMAL | 完全公平调度 | 普通进程 | task_tick_fair |
SCHED_IDLE | 完全公平调度 | 普通进程 | task_tick_fair | |
idle_sched_class(空闲调度类) | 无 | 无 | 空闲进程 | task_tick_idle |
运行队列
每个处理器有一个运行队列,结构体是rq,定义的全局变量如下:
rq是描述就绪队列,其设计是为每一个CPU就绪队列,本地进程在本地队列上排序:
调度进程
主动调度进程的而函数是schedule(),它会把工作委托给__schedule去处理
函数__shcedule的主要处理过程如下:
context_swtich函数如下
/* * context_switch - switch to the new MM and the new thread's register state. */ static __always_inline struct rq * context_switch(struct rq *rq, struct task_struct *prev, struct task_struct *next, struct rq_flags *rf) { struct mm_struct *mm, *oldmm; // 执行进程切换的准备工作 prepare_task_switch(rq, prev, next); mm = next->mm; oldmm = prev->active_mm; /* * For paravirt, this is coupled with an exit in switch_to to * combine the page table reload and the switch backend into * one hypercall. */ // 开始上下文切换,是每种处理器架构必须定义的函数 arch_start_context_switch(prev); // 如果下一个进程是内核进程(成员mm是空指针),内核线程 没有用户虚拟地址空间 if (!mm) { next->active_mm = oldmm; mmgrab(oldmm); // 通知处理器架构不需要切换用户虚拟地址空间。这种加速进程切换技术TLB enter_lazy_tlb(oldmm, next); } else // 如果下一个调度进程是用户进程,那么就调用此函数,切换进程用户虚拟地址空间 switch_mm_irqs_off(oldmm, mm, next); // 如果上一个进程是内核线程,把成员active_mm为空,断开它与用户虚拟地址空间的联系。把它借用的 // 的用户虚拟地址空间,保存在运行队列成员prev_mm中 if (!prev->mm) { prev->active_mm = NULL; rq->prev_mm = oldmm; } rq->clock_update_flags &= ~(RQCF_ACT_SKIP|RQCF_REQ_SKIP); /* * Since the runqueue lock will be released by the next * task (which is an invalid locking op but in the case * of the scheduler it's an obvious special-case), so we * do an early lockdep release here: */ rq_unpin_lock(rq, rf); spin_release(&rq->lock.dep_map, 1, _THIS_IP_); /* Here we just switch the register state and the stack. */ switch_to(prev, next, prev); barrier(); return finish_task_switch(prev); }
context_switch里做的主要有亮点
切换用户虚拟地址空间,ARM64架构使用默认的switch_mm_irqs_off,其实就是调用了一个switch_mm的函数
切换寄存器,宏switch_to把该工作委托给__switch_to去做
继承调度的时间如下:
主动调度
进程在用户模式下运行的时候,无法直接调用schedule()函数,只能通过系统调用进入内核模式,如果系统调
用需要等待某个资源,如互斥锁或信号量,就会把进程的状态设置为睡眠状态,然后调用schedule()函数来调
度进程。
周期调度
有些进程不主动让出处理器,内核只能依靠周期性的时钟中断夺回处理器的控制权,时钟中断是调度器的脉博。时钟中断处理程序检查当前进程的执行时间有没有超过限额,如果超过限额,设置需要重新调度的标志。当时钟中断处理程序准备返点处理器还给被打断的进程时,如果被打断的进程在用户模式下运行,就检查有没有设置需要重新调度的标志,如果设置了,调用schedule函数以调度进程。
SMP调度
在SMP系统中,进程调度器必须支持如下: