Linux内核之 进程调度

上一篇咱们提到过进程状态,而进程调度主要是针对TASK_RUNNING运行状态进行调度,由于其余状态是不可执行好比睡眠,不须要调度。html

 

一、进程调度概念

进程调度程序,简称调度程序,它是确保进程能有效工做的一个内核子系统。调度程序负责决定哪一个进程投入运行,什么时候运行以及运行多长时间算法

多任务

多任务操做系统是指能同时并发执行多个进程的操做系统。并发

多任务系统划分为两类:非抢占式多任务(cooperative multitasking)和抢占式多任务(preemptive multitasking)。负载均衡

非抢占式是一种协做的方式,一个进程一直执行直到任务结束或者主动退出才切换到下一个进程。模块化

抢占式是大部分操做系统采用的方式,是指给每一个进程分配一个时间片(time slice),当时运行时间达到规定的时间时则会切换到下一个进程。函数

 

二、调度策略

上面提到的时间片策略是比较传统的方式,后面Linux系统进行了屡次改进,好比O(1)算法、电梯算法和CFS等。那么改进的动机和依据是什么呢,咱们来看看。oop

2.1 I/O 消耗型和 CPU 消耗型

进程根据资源使用能够分为这两大类。spa

I/O 消耗型:进程的大部分时间用来进行 I/O 的请求或者等待,好比键盘。这种类型的进程常常处于能够运行的状态,可是都只是运行一点点时间,绝大多数的时间都在处于阻塞(睡眠)的状态。操作系统

CPU 消耗型:进程的大部分时间用在执行代码上即CPU运算,好比开启 Matlab 作一个大型的运算。除非被抢占,不然它们能够一直运行,因此它们没有太多的 I/O 需求。调度策略每每是尽可能下降他们的调度频率,而延长其运行时间。.net

固然这种划分不是绝对的,通常的应用程序同时包含两种行为。

因此调度策略一般须要在两个矛盾的目标中寻求平衡:进程响应迅速(响应时间短)和最大系统利用率(高吞吐量)

Linux 系统为了提高响应的速度,倾向于优先调度 I/O 消耗型

 

2.2 进程优先级

调度算法中最基本的一类就是基于优先级的调度,根据进程的价值(重要性)和对处理器时间的需求来对进程分级的想法。简单的说是优先级高的先运行,低的后运行。

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_TimeFair是最经常使用的,也对应了咱们上面提到的实时进程普通进程

 

3.1 彻底公平调度

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消耗型的程序。确实解决了多进程环境下因延迟带来的不公平性。

 

vruntime虚拟实时

在 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对时间片的做用再也不是算数加权,而是几何加权

 

3.2 实时调度策略

实时调度策略分为两种: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()选择和执行一个其余进程。而当磁盘做业完成时,又会被唤醒,进程再次设置为可执行状态,而后从等待队列中移到红黑树中

 

4.1 抢占与上下文切换

上下文切换,就是从一个可执行进程切换到另外一个可执行进程,由context_switch()函数处理。每个新的进程被选出来准备投入运行的时候,schedule()就会调用该函数。

自愿切换意味着进程须要等待某种资源,强制切换则与抢占(Preemption)有关

抢占(Preemption)是指内核强行切换正在CPU上运行的进程,在抢占的过程当中并不须要获得进程的配合,在随后的某个时刻被抢占的进程还能够恢复运行。发生抢占的缘由主要有:进程的时间片用完了,或者优先级更高的进程来争夺CPU了。

抢占的过程分两步,第一步触发抢占,第二步执行抢占,这两步中间不必定是连续的,有些特殊状况下甚至会间隔至关长的时间:

  1. 触发抢占:给正在CPU上运行的当前进程设置一个请求从新调度的标志(TIF_NEED_RESCHED),仅此而已,此时进程并无切换。
  2. 执行抢占:在随后的某个时刻,内核会检查TIF_NEED_RESCHED标志并调用schedule()执行抢占。

抢占只在某些特定的时机发生,这是内核的代码决定的。

 

(1)触发抢占的时机

每一个进程都包含一个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(用户态抢占)的时机

  • 系统调用(syscall)返回用户态时;
  • 中断处理程序返回用户态时;

 

执行Kernel Preemption(内核态抢占)的时机

Linux在2.6版本以后就支持内核抢占了,可是请注意,具体取决于内核编译时的选项:

  • CONFIG_PREEMPT_NONE=y

    不容许内核抢占。这是SLES的默认选项。

  • CONFIG_PREEMPT_VOLUNTARY=y

    在一些耗时较长的内核代码中主动调用cond_resched()让出CPU。这是RHEL的默认选项。

  • CONFIG_PREEMPT=y

    容许彻底内核抢占。

在 CONFIG_PREEMPT=y 的前提下,内核态抢占的时机是:

  • 中断处理程序返回内核空间以前会检查TIF_NEED_RESCHED标志,若是置位则调用preempt_schedule_irq()执行抢占。preempt_schedule_irq()是对schedule()的包装。
  • 当内核从non-preemptible(禁止抢占)状态变成preemptible(容许抢占)的时候;在preempt_enable()中,会最终调用preempt_schedule()来执行抢占。preempt_schedule()是对schedule()的包装。 

 

“抢占”这一部分来自网上,条理比书上更清晰,可是和书上也稍有差异,大致一致,不影响总体理解。

 

 

参考资料:

《Linux内核设计与实现》原书第三版

https://blog.csdn.net/zhoutaopower/article/details/86290196

相关文章
相关标签/搜索