上一篇咱们提到过进程状态,而进程调度主要是针对TASK_RUNNING运行状态进行调度,由于其余状态是不可执行好比睡眠,不须要调度。html
进程调度程序,简称调度程序,它是确保进程能有效工做的一个内核子系统。调度程序负责决定哪一个进程投入运行,什么时候运行以及运行多长时间。算法
多任务操做系统是指能同时并发执行多个进程的操做系统。并发
多任务系统划分为两类:非抢占式多任务(cooperative multitasking)和抢占式多任务(preemptive multitasking)。负载均衡
非抢占式是一种协做的方式,一个进程一直执行直到任务结束或者主动退出才切换到下一个进程。模块化
抢占式是大部分操做系统采用的方式,是指给每一个进程分配一个时间片(time slice),当时运行时间达到规定的时间时则会切换到下一个进程。函数
上面提到的时间片策略是比较传统的方式,后面Linux系统进行了屡次改进,好比O(1)算法、电梯算法和CFS等。那么改进的动机和依据是什么呢,咱们来看看。oop
进程根据资源使用能够分为这两大类。spa
I/O 消耗型:进程的大部分时间用来进行 I/O 的请求或者等待,好比键盘。这种类型的进程常常处于能够运行的状态,可是都只是运行一点点时间,绝大多数的时间都在处于阻塞(睡眠)的状态。操作系统
CPU 消耗型:进程的大部分时间用在执行代码上即CPU运算,好比开启 Matlab 作一个大型的运算。除非被抢占,不然它们能够一直运行,因此它们没有太多的 I/O 需求。调度策略每每是尽可能下降他们的调度频率,而延长其运行时间。.net
固然这种划分不是绝对的,通常的应用程序同时包含两种行为。
因此调度策略一般须要在两个矛盾的目标中寻求平衡:进程响应迅速(响应时间短)和最大系统利用率(高吞吐量)。
Linux 系统为了提高响应的速度,倾向于优先调度 I/O 消耗型。
调度算法中最基本的一类就是基于优先级的调度,根据进程的价值(重要性)和对处理器时间的需求来对进程分级的想法。简单的说是优先级高的先运行,低的后运行。
Linux采用了两种不一样的优先级范围。
(1)nice值
它的范围从-20到+19,默认值0。越大的nice值优先级越低,19优先级最低,-20优先级最高。ps -ef命令中,NI标记就是进程对应的nice值。
这是普通进程的优先级。
(2)实时优先级
范围是 0~99,与 nice 值相反,值越大优先级越高。
这是实时进程的优先级,相对普通进程的,因此任何实时进程的优先级都高于普通进程的优先级。
时间片是一个数值,它代表在抢占前所能持续运行的时间。调度策略必须规定一个默认的时间片,这并不是易事。由于时间片过长I/O消耗型的线程得不到及时响应,而过短CPU消耗型的须要频繁被切换,吞吐量会降低。而最新的Linux调度策略CFS不采用固定的时间片,而是采用了处理器的使用比。咱们接下来详细介绍。
Linux调度器是以分类(模块化)的方式提供的,即对不一样类型的进程进行分组而且分别选择相应的算法。
这种调度结构被称为调度器类(scheduler classes),它容许不一样的可动态添加的调度算法并存,调度属于本身范畴的进程。
以下图Linux调度器包含了多种调度器类。
这些调度器类的优先级顺序为: Stop_Task > Real_Time > Fair > Idle_Task。
开发者能够根据己的设计需求把所属的Task配置到不一样的scheduler classes中。其中的Real_Time和Fair是最经常使用的,也对应了咱们上面提到的实时进程和普通进程。
Fair调度使用的彻底公平调度器(Completely Fair Scheduler,CFS)。
这是一个针对普通进程的调度类,在Linux中称为SCHED_NORMAL(在POSIX中称为SCHED_OTHER)。
传统的时间片方式是每一个进程固定一个时间,那么当进程个数变化时,整个调度周期顺延。时间片还会跟着系统定时器节拍随时改变,那么整个周期再次跟着变化。那么优先级低的进程可能迟迟得不到调度。
而CFS把整个调度周期的时间固定,该周期叫目标延迟(target latency),也再也不采用时间片,而是根据每一个进程的nice值获得的权重再计算获得处理器比例,进而获得进程本身的时间。该时间和节拍没有任何关系,也能够精确到ns。例如“目标延迟”设置为20ms,2个进程各10毫秒,若是4个进程则是各5毫秒。若是100个进程呢,是否是就是0.2毫秒呢?
不必定,CFS引入了一个关键特性:最小粒度。即每一个进程得到时间片的最小值,默认是1毫秒。
为了公平起见,CFS老是选择运行最少(vruntime)的进程做为下一个运行进程。因此这样照顾了I/O消耗型短期处理的需求,也将更多时间留给了CPU消耗型的程序。确实解决了多进程环境下因延迟带来的不公平性。
在 CFS 中,给每个进程安排了一个虚拟时钟vruntime(virtual runtime),这个变量并不是直接等于他的绝对运行时间,而是根据运行时间放大或者缩小一个比例,CFS使用这个vruntime 来表明一个进程的运行时间。若是一个进程得以执行,那么他的vruntime将不断增大,直到它没有执行。没有执行的进程的vruntime不变。调度器为了体现绝对的彻底公平的调度原则,老是选择vruntime最小的进程,让其投入执行。他们被维护到一个以vruntime为顺序的红黑树rbtree中,每次去取最小的vruntime的进程(最左侧的叶子节点)来投入运行。实际运行时间到vruntime的计算公式为:
[ vruntime = 实际运行时间 * 1024 / 进程权重 ]
这里的1024表明nice值为0的进程权重。全部的进程都以nice为0的权重1024做为基准,计算本身的vruntime。
挑选的进程进行运行了,它运行多久?进程运行的时间是根据进程的权重进行分配。
[ 分配给进程的运行时间 = 调度周期 *(进程权重 / 全部进程权重之和) ]
虚拟运行时间是经过进程的实际运行时间和进程权重(weight)计算出来的。在CFS调度器中,将进程优先级这个概念弱化,而是强调进程的权重。一个进程的权重越大,则说明这个进程更须要运行,所以它的虚拟运行时间就越小,这样被调度的机会就越大。
关于nice和进程权重以及vruntime之间的计算方式很是复杂。有兴趣的能够在网上搜索或者看源码。
总之,nice对时间片的做用再也不是算数加权,而是几何加权。
实时调度策略分为两种:SCHED_FIFO 和 SCHED_RR。
这两种实时进程都比任何普通进程的优先级更高(SCHED_NORMAL),都会比他们更先获得调度。
SCHED_FIFO:一个这种类型的进程出于可执行的状态,就会一直执行,直到它本身被阻塞或者主动放弃 CPU;它不基于时间片,能够一直执行下去,只有更高优先级的SCHED_FIFO或者SCHED_RR才能抢占它的任务,若是有两个一样优先级的SCHED_FIFO任务,它们会轮流执行,其余低优先级的只有等它们变为不可执行状态,才有机会执行。
SCHED_RR:与SCHED_FIFO大体相同,只是SCHED_RR级的进程在耗尽事先分配给它的时间后就不能再执行了。因此SCHED_RR是带有时间片的SCHED_FIFO:一种实时轮流调度(Realtime Robin)算法。
上述两种实时算法实现的都是静态优先级。内核不为实时进程计算动态优先级,保证给定的优先级的实时进程总可以抢占比他优先级低的进程。
进程调度的主要入口点是函数schedule(),即实现进程切换的功能:选择哪一个进程能够运行,什么时候投入运行。
该函数的核心是for()循环,它以优先级为序,从最高的优先级调度类开始,遍历全部的调度类。
进程状态能够分为可执行和不可执行,分别放入不一样的结构中。可执行的进程放在红黑树中,而不可执行的放在等待队列。
一个进程可能在两种结构中不断移动。
好比读文件操做,在执行工做时,处在红黑树中,当读完时可能须要等待磁盘,这时会把本身标记成休眠状态,从红黑树中移出,放入等待队列,而后调用schedule()选择和执行一个其余进程。而当磁盘做业完成时,又会被唤醒,进程再次设置为可执行状态,而后从等待队列中移到红黑树中。
上下文切换,就是从一个可执行进程切换到另外一个可执行进程,由context_switch()函数处理。每个新的进程被选出来准备投入运行的时候,schedule()就会调用该函数。
自愿切换意味着进程须要等待某种资源,强制切换则与抢占(Preemption)有关。
抢占(Preemption)是指内核强行切换正在CPU上运行的进程,在抢占的过程当中并不须要获得进程的配合,在随后的某个时刻被抢占的进程还能够恢复运行。发生抢占的缘由主要有:进程的时间片用完了,或者优先级更高的进程来争夺CPU了。
抢占的过程分两步,第一步触发抢占,第二步执行抢占,这两步中间不必定是连续的,有些特殊状况下甚至会间隔至关长的时间:
抢占只在某些特定的时机发生,这是内核的代码决定的。
每一个进程都包含一个TIF_NEED_RESCHED标志,内核根据这个标志判断该进程是否应该被抢占,设置TIF_NEED_RESCHED标志就意味着触发抢占。
直接设置TIF_NEED_RESCHED标志的函数是set_tsk_need_resched();
触发抢占的函数是resched_task()。
TIF_NEED_RESCHED标志何时被设置呢?在如下时刻:
周期性的时钟中断
时钟中断处理函数会调用scheduler_tick(),这是调度器核心层(scheduler core)的函数,它经过调度类(scheduling class)的task_tick方法检查进程的时间片是否耗尽,若是耗尽则触发抢占。
唤醒进程的时候
当进程被唤醒的时候,若是优先级高于CPU上的当前进程,就会触发抢占。相应的内核代码中,try_to_wake_up()最终经过check_preempt_curr()检查是否触发抢占。
新进程建立的时候
若是新进程的优先级高于CPU上的当前进程,会触发抢占。相应的调度器核心层代码是sched_fork(),它再经过调度类的task_fork方法触发抢占。
进程修改nice值的时候
若是进程修改nice值致使优先级高于CPU上的当前进程,也会触发抢占。内核代码参见 set_user_nice()。
进行负载均衡的时候
在多CPU的系统上,进程调度器尽可能使各个CPU之间的负载保持均衡,而负载均衡操做可能会须要触发抢占。
不一样的调度类有不一样的负载均衡算法,涉及的核心代码也不同,好比CFS类在load_balance()中触发抢占;RT类的负载均衡基于overload,若是当前运行队列中的RT进程超过一个,就调用push_rt_task()把进程推给别的CPU,在这里会触发抢占。
(2)执行抢占的时机
触发抢占经过设置进程的TIF_NEED_RESCHED标志告诉调度器须要进行抢占操做了,可是真正执行抢占还要等内核代码发现这个标志才行,而内核代码只在设定的几个点上检查TIF_NEED_RESCHED标志,这也就是执行抢占的时机。
抢占若是发生在进程处于用户态的时候,称为User Preemption(用户态抢占);若是发生在进程处于内核态的时候,则称为Kernel Preemption(内核态抢占)。
执行User Preemption(用户态抢占)的时机
执行Kernel Preemption(内核态抢占)的时机
Linux在2.6版本以后就支持内核抢占了,可是请注意,具体取决于内核编译时的选项:
CONFIG_PREEMPT_NONE=y
不容许内核抢占。这是SLES的默认选项。
CONFIG_PREEMPT_VOLUNTARY=y
在一些耗时较长的内核代码中主动调用cond_resched()让出CPU。这是RHEL的默认选项。
CONFIG_PREEMPT=y
容许彻底内核抢占。
在 CONFIG_PREEMPT=y 的前提下,内核态抢占的时机是:
“抢占”这一部分来自网上,条理比书上更清晰,可是和书上也稍有差异,大致一致,不影响总体理解。
参考资料:
《Linux内核设计与实现》原书第三版