《Linux内核设计与实现》第八周读书笔记——第四章 进程调度

《Linux内核设计与实现》第八周读书笔记——第四章 进程调度

第4章 进程调度

调度程序负责决定将哪个进程投入运行,何时运行以及运行多长时间,进程调度程序可看做在可运行态进程之间分配有限的处理器时间资源的内核子系统。度程序没有太复杂的原理,最大限度地利用处理器时间的原则是只要有可以执行的进程,那么就总会有进程正在执行,但是只要系统中可运行的进程的数目比处理器的个数多,就注定某一给定时刻会有一些进程不能执行,这些进程在等待运行,在一组处于可运行状态的进程中,选择―个来执行,是调度程序所需完成的基本工作。-

4.1 多任务

  • 多任务操作系统:同时并发地交互执行多个进程的操作系统

    多任务操作系统会使多个进程处于堵塞或者睡眠状态。这些任务尽管位于内存,但是并不处于可运行状态。这些进程利用内核堵塞自己,直到某一事件发生。

    多任务系统可以划分为两类:非抢占式和抢占式。

    抢占:强制挂起。

    时间片:分配给每个可运行进程的处理器时间段。

4.2 Linux 的进程调度

  •  从1991年Linux的第1版到后来的2.4内核系列,Linux的调度程序都相当简陋,设计近乎原始,当然它很容易理解,但是它在众多可运行进程或者多处理器的环境下都难以胜任,正因为如此,在Linux 2.5开发系列的内核中,调度程序做了大手术,开始采用了一种叫做O(1)调度程序的新调度程序——它是因为其算法的行为而得名的。
  • 它解决了先前版本Linux调度程序的许多不足,引入了许多强大的新特性和性能特征,这里主要要感谢静态时间片算法和针对每一处理器的运行队列,它们帮助我们摆脱了先前调度程序设计上的限制。
  • O(1)调度器虽然在拥有数以十计(不是数以百计)的多处理器的环境下尚能表现出近乎完美的性能和可扩展性,但是时间证明该调度算法对于调度那些响应时间敏感的程序却有一些先天不足,这些程序我们称其为交互进程一它无疑包括了所有需要用户交互的程序,正因为如此,O(1)调度程序虽然对于大服务器的工作负载很理想,但是在有很多交互程序要运行的桌面系统上则表现不佳,因为其缺少交互进程,自2.6内核系统开发初期,开发人员为了提高对交互程序的调度性能引入了新的进程调度算法,其中最为著名的是“反转楼梯最后期限调度算法,该算法吸取了队列理论,将公平调度的概念引入了Linux调度程序。并且最终在2.6.23内核版本中替代了O(1)调度算法,它此刻被称为“完全公平调度算法”,或者简称CFS。

4.3 策略

1.I/O消耗型和处理器消耗型的进程

  1. I/O消耗型进程
    • 进程的大部分时间用来提交I/O请求或者等待I/O请求
    • 多数用户图形界面(GUI)都属于I/O密集型
  2. 处理器耗费型
    • 时间大多数用在执行代码上
    • 例如MATLAB
    • 往往要延长运行时间并降低调度频率

2.进程优先级

调度算法中最基本的一类就是基于优先级的调度,这是一种根据进程的价值和其对处理器时间的需求来对进程分级的想法。

通常做法:

优先级高的进程先运行,低的后运行,相同优先级的进程按轮转方式进行调度(一个接一个,重复进行)。

优先级分为两类:

  • 第一种是用nice值(―20~+19)
    • 默认值为0;数值越大意味着优先级越低;可以通过 ps-el查看系统进程列表并找到NI标记列对应的优先级
  • 第二种范围是实时优先级(0~99):
    • 越高的实时优先级级数意味着进程优先级越高

3.时间片

时间片:一个数值,它表明进程在被抢占前所能持续运行的时间。

调度策略必须确定一个默认的时间片。

Linux的CFS调度器并没有直接划分时间片到进程,而是将处理器的使用比例划分给了进程。

4.调度策略的活动

4.4 Linux调度算法

4.4.1 调度器类

  • Linux调度器是以模块方式提供的,这样做的目的是允许不同类型的进程可以有针对性地选择调度算法。这种模块化结构被称为调度器类。
  • 它允许多种不同的可动态添加的调度算法并存,调度属于自己范畴的进程。
  • 每个调度器都有一个优先级,基础的调度器代码定义在sched_fair.c文件中,它会按照优先级顺序遍历调度类,拥有一个可执行进程的最高优先级的调度器类胜出,去选择下面要执行的那一个程序。完全公平调度(CS)是一个针对普通进程的调度类,在Linux中称为SCHED_NORMAL。 

4.4.2 Unix 系统中的进程调度

问题中的绝大多数都可以通过对传统Unix调度器进行改造解决,虽然这种改造修改不小,但也并非是结构性调整,比如,将nice值呈几何增加而非算数增加的方式解决第二个问题:采用一个新的度量机制将从nice值到时间片的映射与定时器节拍分离开来以此解决第三个问题。但是这些解决方案都回避了实质问题——即分配绝对的时间片引发的固定的切换频率,给公平性造成了很大变数,CFS采用的方法是对时间片分配方式进行根本性的重新设计(就进程调度器而言)完全摒弃时间片而是分配给进程一个处理器使用比重,通过这种方式,CFS的确保了进程调度中能有恒定的公平性,而将切换频率置于不断变动中。 

4.4.3 公平调度

CFS的出发点基于一个简单的理念:进程调度的效果应如同系统具备―个备一个理想中的完美多任务同时,我们可以调度给它们无限小的时间周期,所以在任何可测量周期内,我们给予一个进程中每个进程同样多的运行时间。举例来说,假如我们有两个运行进程,在标准unⅸ调度模型中,我们先运行其中一个5ms,然后再运行另一个,5ms。但它们任何一个运行时都将占有100%的处理器。而在理想情况下,完美的多任务处理器模型应该是这样的:我们能在5ms内同时运行两个进程,它们各自使用处理器一半的能力。 

4.5  Linux调度的实现

4.5.1 时间记账

所有的调度器都必须对进程运行时间做记账。多数Unix系统,分配一个时间片给每一个进程。那么当每次系统时钟节拍发生时,时间片都会被减少一个节拍周期。
  • 1.调度器实体结构
  • 2.虚拟实时

4.5.2 进程选择

 在前面内容中我们的讨论中谈到若存在一个完美的多任务处理器,所有可运行进程的vruntime值将一致,但事实上我们没有找到完美的多任务处理器,因此CFS试图利用一个简单的规则去均衡进程的虚拟运行时间:当CFS的需要选择下一个运行进程时,它会挑一个具有最小vruntime的进程,这其实就是CSF调度算法的核心:选择具有最小、vruntime的任务。那么剩下的内容我们就来讨论到底是如何实现选择具有最小、vruntime值的进程。Linux中,红黑树称为rbtree,它是一个自平衡二叉搜索树,红黑树是一种以树节点形式存储的数据,这些数据都会对应一个键值,我们可以通过这些键值来快速检索节点上的数据(重要的是,通过键值检索到对应节点的速度与整个树的节点见模成指数比关系) 
  • 1.挑选下一个任务
  • 2.向树中加入进程

4.5.3 调度器入口

进程调度的主要入口点是schedule(),它定义在文件kernel/sched.c中。

4.5.4 睡眠和唤醒

休眠(被阻塞)的进程处于一个特殊的不可执行状态,这点非常重要,如果没有这种特殊状态的话,调度程序就可能选出一个本不愿意被执行的进程,更糟糕的是,休眠就必须以轮询的方式实现了,进程休眠有多种原因,但肯定都是为了等待一些事件,事件可能是一段时间从文件读更多数据,或者是某个硬件事件,一个进程还有可能在尝试获取一个已被占用的内核信号量时被迫进入休眠,休眠的一个常见原因就是文件I/O——如进程对个文件执行了read()操作,而这需要从磁盘里读取,还有,进程在获取键盘输入的时候也需要等待,无论哪种情况,内核的操作都相同:进程把自己标记成休跳态,从可执照课树黑树中移出,放入等待队列,然后调用schedule()选择和执行―个其他进程,唤醒的过程刚好相反:进程被设置为可执行状态,然后再从等待队列中移到可晰红黑树中。 

4.6 抢占和上下文切换

由定义在kernel/sched.c中的context_switch()函数负责处理。当一个新的进程被选出来准备投入运行的时候,schedule()就会调用该函数。它完成了下面两项基本工作:

调用声明在<asm/mmu_contesxt.h>中的switch_mm(),该函数负责把虚拟内存从上一个进程映射切换到新进程中。

调用声明在<asm/system.h>中的switch_to(),该函数负责从上一个进程的处理器状态切换到新进程的处理器状态。

4.6.1用户抢占

发生在:

从系统调用返回用户空间时。

从中断处理程序返回用户空间时。

4.6.2内核抢占

只要没有持有锁,内核就可以发生抢占。

发生在:

中断处理程序正在执行,且返回内核空间之前。

内核代码再一次具有可抢占性的时候。

如果内核空间中的任务显式地调用schedule()。

如果内核中的任务阻塞。(也会调用schedule())。

4.7  实时调度策略

Linux的实时调度算法提供了―种软实时工作方式,软实时的含义是,内核调度进程,尽力使进程在它的限定时间到来前运行,但内核不保证总能满足这些进程的要求。相反,硬实时系统保证在一定条件下,可以满足任何调度的要求。Linux对于实时任务的调度不做任何保证。虽然不能保证硬实时工作方式,但Linux的实时调度算法的性能还是很不错的。2.6版的内核可以满足严格的时间要求。 

4.8 与调度相关的系统调用

4.8.1 与调度策略和优先级相关的系统调用

  • sched_setscheduler()和 sched_getscheduler()分别用于设置和获取进程的调度策略和实时优先级。与其他的系统调用相似,它们的实现也是由许多参数检查、初始化和清理构成的。其实最重要的工作在于读取或改写进程task_struct的policy和rt_priority的值。
  • sched_setscheduler()和 sched_getscheduler()分别用于设置和获取进程的实时优先级。这两个系统调用获取封装在sched_param特殊结构体的rt_priority中。实时调度策略的的最大优先级:是MAX_ USERRT_PRIO减1。最小优先级等于1。
  • 对于―个普通的进程,nice函数可以将给定进程的静态优先级增加一个给定的量。只有超级用户才能在调用它时使用负值,从而提高进程的优先级。nice函数会调用内核的set_user_nice函数,这个函数会设置进程的的task_struct的static_prio值。

4.8.2 与处理器绑定有关的系统调用

Linux调度程序提供强制的处理器绑定机制。也就是说,虽然它尽力通过一种软的(或者说自然的)亲和性试图使进程尽量在同一个处理器上运行,但它也允许用户强制指定“这个进程无论如何都必须在这些处理器上运行”。这种强制的亲和性保存在进程的一个位掩码标志中。该掩码标志的每一位对应一个系统可用的处理器,默认情况下,所有的位都被设置。 

4.8.3 放弃处理器时间

Linux通过sched_yield()系统调用,提供了一种让进程显式地将处理器时间让给其他等待执行进程的机制,它是通过将进程从活动队列中(因为进程正在执行,所以它肯定位于此队列当中)移到过期队列中实现的,由此产生的效果不仅抢占了该进程并将其放入优先级队列的最后面,还将其放入过期队列中—这样能确保在一段时间内它都不会再被执行了,由于实时进程不会过期,所以属于例外,它们只被移动到其优先级队列的最后面(不会放到过期队列中)在Linux以的早期版本中,进程只会被放置到优先级队列的末尾,放弃的时间往往不会太长,现在,应用程序甚至内核代码在调用sched_yield()前,应该仔细考虑是否真的希望放弃处理器时间。内核代码为了方便,可以直接调用sched_yield(),先要确定给定进程确实处于可执行状态,然后再调用sched_yield(),用户空间的应用程序直接使用sched_yield()系统调用就可以 。

 

posted @ 2016-04-12 16:51  黄伯伯  阅读(217)  评论(0编辑  收藏  举报