《Linux内核设计与实现》知识整合与讲解-第四章

进程调度

​ 在上一章中我们讨论过了进程的相关概念,认识了进程从产生到消亡的全过程。在一个系统中进程的数量远远多于处理器的数量,因此,如何利用上有限的处理器资源更好的发挥系统性能以及满足用户需要就成为了一个十分棘手的问题。为了解决这个问题,调度算法应运而生。调度算法的工作就是:决定运行哪一些进程,什么时候开始运行,运行多长时间。进程调度程序可以看作在可运行态下的进程之间分配有限的处理器时间的子系统。

​ 多任务操作系统就是能同时并发的执行多个进程的操作系统。在单处理器的机器上虽然所有的进程就像在同时运行,但是实际上他们处于一种交替并发的运行,每一时刻只有一个进程才能投入运行。在多处理器上才可以同时并行的执行多个进程。实际上,很多进程都在系统中处于堵塞与睡眠,直到他所等待的事件发生他就可以投入运行。

​ 现有的多任务操作系统可以分作两类:抢占式多任务与非抢占式多任务,我们的Linux和其它绝大数的多任务系统一样,采用抢占式的多任务模式。说了这么多什么是抢占呢?调度系统决定一个程序应该什么时候可以结束运行,以便其它的程序可以得到执行的机会,这个强制挂起的过程我们将它称作抢占。为了解决在什么时候应该被抢占的问题,系统提前设计好了在被强占之前这个进程可以运行的时间,并且给了这个时间一个专门的名字,叫作‘时间片’。

​ 在非抢占式多任务系统下,一个进程会一直运行,直到它自己放弃掉处理器的使用权,我们称这种自己将自己挂起的行为为“让步”。在非抢占式系统下,无法对每个进程能够运行的时间进行一个宏观调配,而且如果某进程一直占用处理器而且不将自己挂起,就会直接导致系统的崩溃。

​ 在早期的Linux的调度程序是十分简陋的,为了完善调度程序在Linux2.5开发系列的内核中,引入了一种称为O(1)的调度程序算法,它采用静态时间片算法和针对于每一处处理器的队列,让调度程序的性能显著提升。但是,O(1)调度程序对于那些响应时间敏感的程序却有着先天的不足,我们称这一类进程为交互式进程,包括了用户所需要的交互式要求。因此在2.6内核开发初期,为了提高交互效率,引入了新的调度算法,形成了今天要着重讲解的完全公平调度算法,简称CFS。

​ 我们操作系统内的进程可以大致的看作为两部分:I/O消耗型和处理器消耗型进程;前者指进程的大部分时间用来提交I/O请求或者等待I/O请求,因此这样的进程经常处于可运行状态,但是往往就运行一会儿,因为这样的进程往往在处理一个I/O请求时最后总会阻塞(例如键盘输入之间的间隔,网络I/O之间的请求空挡)。

​ 相反的,处理器耗费型进程把太多的时间运用在执行代码上,除非被抢占否则他们会持续不断的运行。它们并不需要太多的I/O需求,所以,调度器不应该经常选择它们运行,对于这类处理器消耗型的进程,调度策略往往是尽量降低它的调度频率,从而延长其运行时间。代表性的有数学计算程序如:MATLAB。

​ 这是两个矛盾的目标,因此调度算法的一个十分重要的任务是基于优先级的调度。这是一种根据进程的价值和对处理器时间的需求来对进程进行分级的想法。有一种很通常的做法:优先级高的先运行优先级低的后运行,相同的优先级的进程按轮转方式进行调度,在某些系统中优先级高的进程使用的时间片也就越长。调度程序总是选择时间片未用尽而且优先级最高的进程运行。

​ Linux采用了两种不同的优先级值。第一种是用nice值,它的范围是从-20到+19,它的默认值是0;越大的nice值意味着更低的优先级;低nice值意味着进程可以获得更多的处理器时间。nice值是所有的UNIX系统中的标准化概念,但是需要注意的是,由于调度算法的不同因此nice值的使用方式有不同的方式,而Linux系统中nice值则代表时间片的比例。

​ 第二种是指实时优先级,其值是可配置的,默认情况下它的变化范围是从0到99(包括0与99),与nice值相反,实时优先级越高则进程的优先级也就越高。任何的实时进程的实时优先级都高于普通的进程,可以说nice值与实时优先级可以看出是处于两个互不相交的范畴。

​ 前面说了这么多,我们不难发现时间片的长短对多任务系统是一个至关重要的影响因素,我们来详细来讨论一下时间片。时间片是一个数值,他表示进程在被抢占前所能持续运行的时间,调度策略必须规定一个默认的时间片,但是这并不容易,时间片过长会导致系统对交互的相应表现欠佳,让人觉得系统无法并发的执行应用程序,如果时间过短会明显的增大进程切换带来的处理器耗时,这时候两种进程之间的矛盾又进一步的显现了出来。

​ Linux并没有直接分配时间片到进程而选择将处理器的使用比划分给了进程,这样一来进程所获得的处理器时间其实是和系统负载密切相关的。这个比例还进一步会受nice值的影响,低优先级的赋予低权重,高优先级赋予高权重。在多数的操作系统中是否需要将一个进程立即投入运行,完全由进程优先级与是否有时间片来决定。但是Linux使用CFS调度器,其抢占时机取决于新的可运行的程序消耗了多少处理器使用比,如果消耗的使用比比当前进程要小,就立即抢占,否则将推迟运行。

​ Linux调度器是以模块方式提供的,它允许不同类型的进程可以有针对性的选择调度算法。这种模块化结构称作调度器类,它使得多种不同类型的可动态添加的调度算法并存,调度属于自己范畴的进程,每个调度器都有着自己的优先级,基础的调度器代码在kernel/shced.c文件中,他会按照优先级遍历调度类,拥有一个可执行进程的最高优先级的调度器胜出,去选择下面要执行的那一个程序。CFS就是一个针对普通进程的调度类。

​ 在传统的Unix中,现代进程调度器有着两个通用的概念:进程优先级和时间片,时间片是指进程运行多少时间,进程一旦启动就有一个默认的时间片,具有高优先级会有两个好处:1.运行的更加频繁,2.具有更多的时间片。虽说听起来很合理但实际上存在着很多问题:

  1. 如果直接将nice值映射成为时间片的长短,就会导致高优先级的进程具有长时间的时间片且切换频率低,而低优先级的进程就会导致时间片缩短,调度的频率上升。但是在实际情况中,低优先级进程往往是后台进程,且多是计算密集型,而普通优先级的进程则更多是前台的用户任务,这跟我们的结果显然背道而驰;
  2. 第二个问题是不同的nice值导致分配的时间片增长与减少比例严重不等;
  3. 时间片的变化会收到节拍器的影响;
  4. 为了使进程尽快投入运行,而去对新要唤醒的进程提升优先级,即便时间片已经用尽了,这就给了恶意进程一个侵犯其它正常进程的利益;

​ 虽然说有很多方法可以通过改变运用nice值的方法来纠正上面所提到的问题,但是他们都回避了实质上的问题——分配绝对的时间片引发的固定的切换频率。CFS采用的方法是对时间片进行根本性的重新设计,完全的摈弃了时间片,取而代之的是分配给进程一个处理器使用比重,通过这种方式,CFS确保了进程调度有恒定的公平,将切换的频率不断地进行变化。

​ CFS的具体做法是允许每个进程运行一段时间、循环轮转、选择运行最少的进程作为下一个运行进程;CFS在所有的可运行进程的基础上计算出一个进程应该要运行多久,越高的nice值进程获取越低的处理器处理权重;每个进程按照其权重在全部可以运行进程中所占的时间运行;CFS为无限小的调度周期的近似值设立了一个目标,叫作“目标延迟”;但是每个参与分配的进程所运行的最小时间是有规定的,称为最小粒度。

​ 在探讨了这么久的Linux调度原理之后,我们可以稍微具体一点来继续看待问题;我们划分成四个方面探讨:

  • 时间记账:CFS不具有时间片的概念,但是它需要对每一个进程运行的时间进行记账,它需要确保每个进程虚拟时间不可超过分配的时间,CFS使用调度器实体结构来追踪进程运行记账。进程描述符具有调度器实体结构类型的成员变量。vruntime存放进程的虚拟运行时间,它是经过所有可运行的进程总数标准化所得到的,update_curr()计算出当前进程的执行时间,并且将其存放在变量delta_exec中,然后将运行时间传递给了__update_curr(),再对时间进行加权运算与当前的vruntime相加。

  • 进程选择:CFS算法的核心就是选取vruntime最短的投入运行(也就是我们所常说的选取运行时间最少的进程投入运行),所有进程按照红黑树存储。

  • 调度器入口:进程调度的主要入口是schedule()函数,它的作用是选取一个最好的调度类,然后由后者选取一个优先级最高的进程执行,它会调用pick_next_task(),选取最高优先级的调度类,并从中选取优先级最高的进程。每一个调度类都实现了pick_next_task(),在CFS中pick_next_task()会调用pick_next_entity()函数该函数又来调用__pick_next_entity()函数。

  • 睡眠和唤醒:休眠(被阻塞)的进程处于一个特殊的不可执行状态,进程休眠的原因有很多但是归根结底肯定是为了等待某些条件。在进程休眠的时候进程把自己从可执行的红黑树中移出来,放入等待队列,然后调用schedule()挑选并执行下一个进程,唤醒过程则与之相反。

    具体来说,进程将自己放入等待队列要经历以下几个步骤:

    • 调用宏创建一个等待队列的项;
    • 调用接口将自己加入到队列中,该队列会在进程等待的条件满足的时候来唤醒它;
    • 将进程的状态设置成TASK_INTERRUPTIBLE或者TASK_UNINTERRUPTIBLE。如果有必要会将进程加回到等待队列;
    • 如果状态是TASK_INTERRUPTIBLE,则信号唤醒进程,这就是伪唤醒;
    • 如果条件满足会再次,检查条件是否为真,如果是他就会退出循环,如果不是,他将一直重复循环;
    • 条件满足后将自己设置成TASK_RUNNING,调出循环队列;
posted @ 2021-06-25 22:11  成仙的胖子  阅读(86)  评论(0)    收藏  举报