进程调度的基本概念
- 抢占式多任务:
- 由调度程序来决定什么时候停止一个进程的运行,以便其他进程能够得到执行机会。这个强制的挂起动作称为抢占。进程在被抢占之前能够运行的时间是预先设置好的,叫做时间片(就是分配给每个可运行进程的处理器时间段)
- 非抢占式多任务:
- 除非进程自己主动停止运行,否则会一直执行。进程主动挂起自己的操作称为让步(yeild)
I/O消耗型:进程大部分时间用来提交I/O请求或是等待I/O请求。
处理器消耗型:把时间大多用在执行代码上,除非被抢占,通常会一直执行。
调度策略需要在两个矛盾的目标中寻找平衡 : 进程响应时间迅速和最大系统利用率/响应时间短,吞吐量高。
Linux采用两种不同的优先级范围
第一种:使用nice值,-20~+19,默认为0,值越大优先级越低。Linux系统中nice代表时间片的比例。
第二种:实时优先级,其值可配置,默认是0~99。和nice值相反,这个值越高,优先级越高。
任何实时进程的优先级都高于普通的进程。实时优先级和nice优先级处于互不相交的两个范畴。
O(1) 调度
从就绪队列选择下一个执行进程的时间为常数,在几十颗CPU时表现良好,但在几百颗CPU时,表现不佳
数据结构
struct prio_array_t{
int nr_active;//本进程组中的进程数
struct list_head queue[MAX_PRIO];//优先级为索引的hash表,140级
bitmap[BITMAP_SIZE];/*加速hash表访问的位图*/
}
- prio_array_t中nr_actice表示本组进程组中的进程数
- queue则相当于优先级队列,queue的索引就是优先级,同优先级的进程位于queue[i]所指向的链表中
- Bitmap用于加速queue的访问,可用于迅速查找第一个不为空的链表置。
其他相关变量
timestamp_last_tick,pre_cpu_load,记录CPU当前状态,可用于负载平衡。
prio_array_t* active, *expired, arrays[2];
arrays二元数组是两类就绪队列的容器。
每个CPU维护自己的就绪队列
主要思路
active,expired分别指向一个队列
一般情况下active中的进程一旦用完自己的时间片,就被转移到expired中,并设置好新的初始时间片。
当active为空时,表示当前所有进程时间片都消耗完了,此时active和expired进行一次对调,重新开始下一轮的时间片递减过程。
而如果进程是交互式进程,调度器会让其保持在active队列上,以提高响应速度,但这个措施不能使其他就绪队列等待时间过长,当expired队列中进程已经等待足够长(时间限制)的时间,即使交互进程也要转移,排空active。
O(1)
选择最佳候选进程只需要在active中选择优先级最高的链表中的第一项,这个操作不超过140次,为O(1).
time_slice时间片的计算
值在变化,代表进程运行时间片剩余大小
- 基准值与静态优先级(2.4的nice值)相关。
- MIN_TIMESLICE + ((MAX_TIMESLICE - MIN_TIMESLICE) * (MAX_PRIO-1 - (p)->static_prio) / (MAX_USER_PRIO-1))
- time_slice时间片的变化:
- 创建时和父进程平分时间,运行过程中递减,归0后,按static_prio重新赋予基准值。
- 进程的退出时(sched_exit()),根据first_time_slice判断自己是否重新分配过时间片,如果从未重新分配,将自己的剩余时间片还给父进程(不超过MAX_SLICE),如果已经用完就没有必要返还了。
时间片的递减和重置在时钟中断sched_tick()中进行。
优先级的计算分散到各个时候:
由 effect_prio() 函数完成
非实时进程的优先级取决于静态优先级(static_prio)和进程的sleep_avg 值两个因素。
实时进程的优先级实际上是在 setscheduler() 中设置的,设置后就不再改变。
非实时进程的动态优先级为
- 【进程睡眠的bonus – 静态优先级】限制在MAX_RT_PRIO和MAX_PRIO sleep_avg 范围是 0~MAX_SLEEP_AVG
- 经过以下公式转MAX_BONUS/2~MAX_BONUS/2 之间
(NS_TO_JIFFIES((p)->sleep_avg) *MAX_BONUS / MAX_SLEEP_AVG) - MAX_BONUS/2
优先级计算时机:不再集中在调度器选择候选进程的时候进行了,只要进程状态发生改变,核心就有可能计算并设置进程的动态优先级
完全公平调度 CFS: 普通进程的调度类
出发点基于一个简单的理念
进程的调度效果应该如同系统具备一个理想的完美多任务处理器。系统中有n个可运行进程,每个进程应该能获得1/n的处理器时间,就好像n个进程同时执行,每个进程拥有处理器1/n的处理能力。CFS为完美调度中的无限小调度周期的近似值设立了目标,称为“目标延迟”。
越小的调度周期交互性越好,也更接近完美多任务。但是进程切换代价高,系统吞吐量低。每个进程获取的时间片有个底线,称为最小粒度。
CFS做法是:允许每个进程运行一段时间,循环轮转,选择运行最少的进程作为下一个运行进程。CFS在所有可运行进程总数基础上计算一个进程应该运行多久,而不是依靠nice值来计算时间片。CFS中nice值作为进程获得的处理器运行比的权重:越高的nice,获得更低的CPU使用权重;越低的nice,获得更高的CPU使用权重。
- 时间记账
使用sched_entity结构体进行记账;vruntime存放进程的虚拟执行时间(ns,根据进程总数标准化过),记录程序到底运行了多长时间以及还需要运行多长时间。
- 进程选择
使用红黑树组织可运行进程队列,以vruntime为键,选择最小vrumtime的进程。
添加进程,删除进程,就是红黑树的插入,删除操作,不过这里缓存了最左叶子节点,提高选择最小vrumtime的进程的速度。
- 调度器入口
schedule()函数调用pick_next_task函数,以优先级为序,从高到低,依次检查每一个调度类,从最高优先级的调度类中,选择最高优先级的进程。
- 睡眠和唤醒
睡眠:进程标记为休眠状态,从可执行红黑树中移出,放入等待队列,调用schedule选择和执行一个其他进程。
唤醒:进程杯设置为可执行状态,从等待队列移到可执行红黑树中。
上下文切换
从一个可执行进程切换到另一个可执行进程。
<asm/mmu-contex.h>
switch_mm()把虚拟内存从行一个进程切换到新进程
<asm/system.h>
switch_to()从上一个处理器状态切换到新处理器状态,保存恢复栈寄存器信息
调用schedule的时机
- 某个进程应该被抢占,scheduer_tick设置need_resched;
- 一个优先级高的进程进入可执行状态,try_wake_up,设置need_resched;
- 从内核返回用户空间或从中断返回,内核检查need_resched.如果已经设置,内核会在继续执行之前调用调度程序。
抢占
(被抢占不一定是睡眠,不一定是在进程上下文)
用户抢占:从系统调用返回用户空间;从中断处理程序返回用户空间
内核抢占:内核中的任务显示调用schedule;内核中的任务睡眠;中断处理程序正在执行,返回到内核空间之前;内核代码具有可抢占性的时候(SMP中,没有锁,就是可以抢占的标志)
实时调度
SCHED_FIFO:不使用时间片,先入先出;有更高优先级的SCHED_FIFO,SCHED_RR进程到来时发生抢占
SCHED_RR:带有时间片的,实时轮流调度。时间片耗尽时,同以优先级的其他进程被轮流调度。
参考
《Linux内核设计与实现》
https://oakbytes.wordpress.com/linux-scheduler/