20135327郭皓--读书笔记五
第四章 进程调度
- 调度程序负责决定将哪个进程投入运行,何时运行以及运行多长时闹。进程调度程序(常常 简称调度程序)可看做在可运行态进程之间分配有限的处理器时间资源的内核子系统。
- 调度程序没有太复杂的原理。最大限度地利用处理器时间的原则是,只要有可以执行的进 程,那么就总会有进程正在执行。
4.1 多任务
- 多任务操作系统就是能同时并发地交互执行多个进程的操作系统。
- 多任务系统可以划分为两类:非抢占式多任务(cooperative multitasking)和抢占式多任务 (preemptive multitasking)。
- 这个强制的挂起动作就叫做抢占(preemption).进程在被抢占之前能够运行 的时间是预先设置好的,而且有一个专门的名字,叫进程的时间片(timeslice )。时间片实际上 就是分配给每个可运行进程的处理器时间段。
- 在非抢占式多任务模式下,除非进程自己主动停止运行,否则它会一直执行。进程主动挂起自己的操作称为让步(yielding)。
4.2 Linux 的进程调度
- 反转楼梯最后期限调度算法——RSDL
- 完全公平调度算撞——CFS
4.3 策略
- 策略决定调度程序在何时让什么进程运行
4.3.1 I/O 消耗型和处理器消耗型的进程
进程可以被分为I/O消耗型和处理器消耗型。前者指进程的大部分时间用来提交I/O请求或是等待I/O请求。相反,处理器消耗型进程把时间大多用在执行代码上。除非被抢占,否则它们通常都一直 不停地运行,因为它们没有太多的I/O需求。
调度策略通常要在两个矛盾的目标中间寻找平衡:进程响应迅速(响应时间短)和最大系统利用率(高吞吐量)。
4.3.2 进程优先级
- 调度算站中最基本的一类就是基于优先级的调度。
- 调度程序总是选择时间片采用尽而且优先级最高的进程运行。用户和系统都可以通过设置进程的优先级来影响系统的调度。
- Linux 采用了两种不同的优先级范围。第一种是用nice值,色的范围是从-20到+19,默 认值为0 :越大的 nice 值意味着更低的优先级。
- 第二种范围是实时优先级,其值是可配置的,默认情况下它的变化范围是从0到99(包括0和99)。与nice值意义相反,越高的实时优先级数值意味着进程优先级越高。
4.3.3 时间片
- 时间片是一个数值,它表明进程在被抢占前所能持续运行的时间。
- 从上面的争论中可以看出,任何长时间片都将导致系统交互表现欠佳。
-
在Linux中使用新的CFS调度器,其抢占时机取决于新的可运行程序消耗了多少处理器使用比。如果消耗的使用比比当前进程小,刷新进程立刻拉入运行,抢占当前进程。否则,将推迟其运行。
4.4 Linux 调度算法
4.4.1 调度器类
-
Linux调度器是以模块方式提供的,这样做的目的是允许不同类型的进程可以有针对性地选择调度算法。这种模块化结构被称为调度器类(scheduler classes ),它允许多种不同的可动态添加的调度算法并存,调度属于自己范畴的进程。每个调度器都有一个优先级,基础的调度器代码定义在kemel/scbed.c文件中,它会按照优先级顺序遍历调度类,拥有一个可执行进程的最高优先级的调度器类胜出,去选择下面要执行的那一个程序。
完全公平调度(CFS)是一个针对普通进程的调度类,在Linux中称为SCHED_NORMAL(在 POSIX 中称为 SCHED_OTHER) , CFS算法实现定义在文件kernel/sched_ fair.c中。
4.4.2 Unix系统中的进程调度
CFS 采用的方越是对时间片分配方式进行根本性的重新设计(就进程调度器而言):完全摒弃时间片而是分配给进程一个处理器使用比重。通过这种方式, CFS确保了进 程调度中能有恒定的公平性,而将切换频率置于不断变动中。
4.4.3 公平调度
CFS的出发点基于一个简单的理念:进程调度的效果应如同系统具备一个理想中的完美多任务处理器。
任何进程所获得的处理器时间是由它自己和其他所有可运行进程nice值的相对差值决定的。 nice值对时间片的作用不再是算数加权,而是几何加权。任何回回值对应的绝对时间不再是一个绝对值,而是处理器的使用比。 CFS称为公平调度器是因为它确保给每个进程公平 的处理器使用比。正如我们知道的,CFS不是完美的公平,它只是近乎完美的多任务。但是它确实在多进程环境下,降低了调度延迟带来的不公平性。
4.5 Linux调度的实现
在讨论了采用 CFS调度算法的动机和其内部逻辑后,我们现在可以开始具体探索CFS是如何得以实现的。其相关代码位于文件 kernel/sched_fair.c 中.我们将特别关注其四个组成部分:
- 时间记账
- 进程选择
- 调度器入口
- 睡眠和唤醒
4.5.1 时间记账
所有的调度器都必须对进程运行时间做记账。
- 调度器实体结构
CFS不再有时间片的概念,但是它也必须维护每个进程运行的时间记账,因为它需要确保每个进程只在公平分配给它的处理器时间内运行。CFS使用调度器实体结构(定义在文件<linux/sched.h>的 struct_sched _entity中)来追踪进程运行记账
2. 虚拟实时
vruntime变量存放进程的虚拟运行时间,该运行时间(花在运行上的时间和)的计算是经过了所有可运行进程总数的标准化(或者说是被加权的)。虚拟时间是以因为单位的,所以vruntime 和定时器节拍不再相关。
因此CFS使用vruntime变量来 记录一个程序到底运行了多长时间以及它还应该再运行多久。
4.5.2 进程选择
CFS使用红黑树来组织可运行进程队列,并利用其迅速找到最小vruntime值的进程。在Linux中,红黑树称为rbtree,它是一个自平衡二叉搜索树。
1. 挑选下一个任务
2. 向树中加入进程
3. 从树中则除进程
4.5.3 调度器入口
进程调度的主要入口点是函数 schedule(),它定义在文件kemel/sched.c中。它正是内核其他部分用子调用进程调度器的入口:选择哪个进程可以运行,何时将其投入运行。 Schedule()通常都需要和一个具体的调度类相关联,也就是说,它会找到一个最高优先级的调度类一一后者需要有自己的可运行队列,然后问后者谁才是下一个该运行的进程。
4.5.4 睡眠和唤醒
1. 等待队列
1 )调用宏DEFINE_WAIT()创建一个等待队列的项。
2)调用 add_wait_ queue()把自己加入到队列中。该队列会在进程等待的条件满足时唤醒它。 当然我们必须在其他地方撰写相关代码,在事件发生时,对等待队列执行 wake_up()操作。
3)调用 prepare_to_ wait()方法将进程的状态变更为TASK_INTERRUPTIBLE 或TASK_ UNINTERRUPTIBLE。而且该函数如果有必要的话会将进程加回到等待队列,这是在接下来的循环遍历中所需要的。
4)如果状态被设置为 TASK_INTERRUPTIBLE,则信号唤醒进程。这就是所谓的伪唤醒(唤醒不是因为事件的发生),因此检查并处理倍号。
5)当进程被唤醒的时候,它会再次检查条件是否为真。如果是,它就退出循环:如果不是,它再次调用 schedule()并一直重复这步操作。
6)当条件满足后,进程将自己设置为TASK_RUNNING并调用 finish_wait()方法把自己移出等待队列。
2. 唤醒
唤醒操作通过函数wake_upO 进行,它会唤醒指定的等待队列上的所有进程。
4.6 抢占和上下文切换
上下文切换,也就是从一个可执行进程切换到另一个可执行进程,由定义在 kernel/ sched.c 中的 context_switch()函数负责处理。每当一个新的进程被选出来准备投入运行的时候, schedule() 就会调用该函数。它完成了两项基本的工作:
- 调用声明在 <asm/mmu_ context.h>中的 switch_mm(), 该函数负责把虚拟内存从上一个进程映射切换到新进程中。
-
调用声明在 <asm/system.h> 中的 switch_to(),该函数负责从上一个进程的处理器状态切换 到新进程的处理器状态。这包括保存、恢复检信息和寄存器信息,还有其他任何与体系结 ’ 构相关的状态信息,都必须以每个进程为对象进行管理和保存。
内核必须知道在什么时候调用 schedule()。如果仅靠用户程序代码显式地调用 schedule(),它 们可能就会永远地执行下去。相反,内核提供了一个 need_resched标志来表明是否需要重新执行 一次调度
4.6.1 用户抢占
内核即将返回用户空间的时候,如果 need_resched标志被设置,会导致 schedule()被调用, 此时就会发生用户抢占。
用户抢占在以下情况时产生:
- 从系统调返回用户空间时。
- 从中断处理程序返回用户空间时。
4.6.2 内核抢占
为了支持内核抢占所傲的第一处变动,就是为每个进程的thread_info引入preempt_count 计数器。该计数器初始值为0,每当使用锁的时候数值加1,释放锁的时候数值减1。当数值为0的时候,内核就可执行抢占。从中断返回内核空间的时候,内核会检查 need_resched 和preempt_ count 的值。如果 need_resched被设置,并且 preempt_count 为0的话,这说明有一个更为重要的任务需要执行并且可以安全地抢占,此时,调度程序就会被调用。如果 preempt_ count不为0, 说明当前任务持有锁,所以抢占是不安全的。这时,内核就会像通常那样直接从中断返回当前执行进程.如果当前进程持有的所有的锁都被释放了,preempt_count就会重新为0。此时,释放锁的代码会检查 need_resched是否被设置。如果是的话,就会调用调度程序。
内核抢占会发生在:
- 中断处理程序正在执行,且返回内核空间之前。
- 内核代码再一次具有可抢占性的时候。
- 如果内核中的任务显式地调用 schedule()。
- 如果内核中的任务阻塞(这同样也会导敖调用 schedule())。
4.7 实时调度策略
- Linux 提供了两种实时调度策略:SCHED_FIFO 和 SCHED_RR。而普通的、非实时的调度策略是 SCHED_NORMAL。
- SCHED_FIFO 实现了一种简单的、先入先出的调度算法:它不使用时间片.处于可运行状态的SCHED_FIFO级的进程会比任何SCHED_NORMAL级的进程都先得到调度。
- SCHED_RR与 SCHED_FIFO 大体相同,只是SCHED_RR级的进程在耗尽事先分配给它的时间后就不能再继续执行了。
- 这两种实时算法实现的都是静态优先级。内核不为实时进程计算动态优先级.这能保证给定优先级别的实时进程总能抢占优先级比它低的进程。
- Linux的实时调度算挂提供了一种软实时工作方式。软实时的含义是,内核调度进程,尽力使进程在它的限定时间到来前运行,但内核不保证总能满足这些进程的要求。
4.8 与调度相关的系统调用
4.8.1 与调度策略和优先组相关的系统调用
sched _ setscheduler()和 sched__getscheduler()分别用于设置和获取进程的调度策略和实时优先级。与其他的系统调用相似,它们的实现也是由许多参数检查、初始化和清理构成的。其实最重要的工作在于读取或改写进程 tast_struct的policy和rt_priority的值。
4.8.2 与处理器绑定有关的系统调用
Linux调度程序提供强制的处理器绑定(processor affinity)机制。也就是说,虽然它尽力通过一种软的(或者说自然的)亲和性试图使进程尽量在同一个处理器上运行,但它也允许用户 强制指定“这个进程无论如何都必须在这些处理器上运行”。
4.8.3 放弃处理器时间
Linux通过 sched_yield()系统调用,提供了一种让进程显式地将处理器时间让给其他等待执行进程的机制。它是通过将进程从活动队列中(因为进程正在执行,所以它肯定位于此队列当中)移到过期队列中实现的。
4.9 小结
进程调度程序是内核重要的组成部分,因为运行着的进程首先在使用计算机。 然而,满足进程调度的各种需要绝不是轻而易举的:很难找到“一刀切”的算法,既适合众多的可运行进程,又具有可伸缩性,还能在调度周期和吞吐量之间求得平衡,同时还满足各种负载的需求。不过,Linux 内核的新CFS调度程序尽量满足了各个方面的需求,并以较完善的可伸缩性和新颖的方挫提供了最佳的解决方案。