进程调度:当可运行的进程数目,大于处理器个数时,就需要在可运行状态进程中,选择一个来执行,调度的目的是为了最大限度地利用处理器时间
多任务系统分为两类:抢占式和非抢占式,基本所有系统都是抢占式的,为了更大效率利用处理器
抢占:由调度程序来决定什么时候停止一个进程,运行另外一个进程
非抢占式:除非进程自己主动停止运行,否则他会一直执行
一、调度策略
进程分为两种:
I/O消耗型:大部分时间用来提交I/O请求,或者灯带I/O请求,运行时间较短
处理器消耗型:大多时间用在执行代码上,系统应尽量降低调度频率,延长其运行时间
进程优先级:
根据进程的价值和其对处理器时间的需求来对进程分级;通常是优先级高的先运行,低的后运行,相同的则按照轮转方式一个接一个。(不同优先级进程,能够使用的时间片也不同)
Linux采用两种不同的优先级范围:
1、nice值:-20~19,nice值越高,优先级越低
2、实时优先级:0~99,值越大,优先级越高。任何实时进程的优先级都高于普通进程
二、调度算法
2.1 完全公平调度(CFS)
CFS针对普通进程的调度类,在Linux中称为SCHED_NORMAL,它的做法是允许每个进程运行一段时间、循环轮转、选择运行最少的进程作为下一个进程运行。
假如目标延时是20ms,当前有两个进程可运行,他们的nice值分别是默认值(0)和5,则nice值为5的进程的权重将是默认nice值进程的1/3,这两个进程将获得15ms和5ms;如果nice值是10和15,那分配到的时间片还是15和5ms。为啥呢,没理解
时间记账:CFS利用sched_entity结构体完成时间记账。分配给一个进程一个时间片后,每当系统时钟节拍发生时,时间片都会被减少一个节拍周期,当减少到0时,它就会被另一个尚未减到0的进程抢占。CFS用vruntime变量存放进程的虚拟运行时间,其计算方式如下:
用当前时间:rq_of(cfs_rq)->clock - cur->exec_start,并调用update_curr更新vruntime,由于update_curr是有系统定时器周期性调用,所以无论进程处于那种状态,都能准确的计算进程运行时间,以及下一个被运行的进程
进程选择:选取最小vruntime的任务。CFS使用rbtree存储所有可运行进程,节点值为vruntime,那vruntime最小值,则是树中最左侧叶子节点。系统已将该节点记录到缓存中cfs_rq->rb_leftmost,如果找不到该节点,证明无进程可运行。
插入进程时,会通过键值判断,如果该进程vruntime最小,则会更新leftmost
删除进程时(当进程变为不可运行、或终止),如果是最左节点,则会调用rb_next函数,查找下一个最左节点
调度器入口:调度器入口函数是schedule(),它的功能很简单,调用pick_next_task()函数,根据调度类的优先级,一次检查每一个调度类,并且从最高优先级的类中,选择最高优先级的进程。
睡眠和唤醒:休眠原因:等待I/O、信号量、锁等;此时该进程会从vruntime进程树中去掉;反之,被唤醒时,进程被设置为可执行状态,再加入到可执行红黑树中。
2.2 抢占和上下文切换
上下文切换,就是从一个可执行进程切换到另一个可执行进程,由context_switch()函数处理,每当新进程被选出来准备投入运行时,schedule会调用该函数,来完成两个基本工作:
1、调用switch_mm(),把虚拟内存从上一个进程切换到新进程中
2、调用switch_to()从上一个进程的处理器状态切换到新进程的处理器状态,这包括保存、恢复栈信息和寄存器信息
内核提供了一个need_resched标志来表明是否需要重新执行一次调度(每个进程,都包含一个need_resched标志,这是因为通过current宏访问进程描述符内的数值要比访问全局变量块的多)。内核通过检查该标志,来决定是否调用schedule来切换到一个新的进程
3、当中断返回内核空间时,会判断need_resched值和preempt_count计数器(该计数器用来判断是否持有锁),如果need_resched=1且preempt_count=0时,才会触发重新调度。