第四章 进程调度
【学习时间:1小时45分 撰写博客时间:2小时10分钟】
【学习内容:Linux的进程调度实现、抢占和上下文切换、与调度相关的系统调用】
调度程序负责决定将哪个进程投入运行,何时运行以及运行多长时间。进程调度程序可看做在可运行态进程之间分配有限的处理器时间资源的内核子系统。
最大限度利用处理器时间的原则:只要有可以执行的进程,那么总会有程序正在执行。
一、多任务
1.概念:多任务操作系统就是能同时并发地交互执行多个进程的操作系统,在单处理器机器上这会产生多个进程在同时运行的幻觉,在多处理器机器上,这会使多个进程在不同的处理机上真正同时、并行地运行。
- 无论在单处理器或者多处理器机器上,多任务操作系统都能使多个进程处于堵塞或者睡眠状态,也就是说,实际上不被投入执行,直到工作确实就绪
- 这些任务尽管位于内存,但并不处于可运行状态。相反,这些进程利用内核阻塞自己,直到某一事件(键盘输入网络数据、过一段时间等)发生。因此,现代Linux系统也许有100个进程在内存,但是只有一个处于可运行状态
2. 分类:多任务系统可以划分为两类
- 非抢占式多任务。进程会一直执行直到自己主动停止运行(这一步骤称为让步)
- 抢占式多任务。Linux/Unix使用的是抢占式的方式;强制的挂起进程的动作就叫做抢占。进程在被抢占之前能够运行的时间是预先设置好的(也就是进程的时间片)
二、Linux的进程调度
在Linux 2.5开发系列的内核中,调度程序做了大手术,开始采用了一种叫做O(1)调度程序的新调度程序——它是因为其算法的行为而得名的。
- 它解决了先前版本Linux调度程序的许多不足,引入了许多强大的新特性和性能特征,这里主要要感谢静态时间片算法和针对每一处理器的运行队列,它们帮助我们摆脱了先前调度程序设计上的限制
- O(1)调度程序虽然对于大服务器的工作负载很理想,但是在有很多交互程序要运行的桌面系统上则表现不佳,因为其缺少交互进程,自2.6内核系统开发初期,开发人员为了提高对交互程序的调度性能引入了新的进程调度算法,其中最为著名的是“反转楼梯最后期限调度算法,该算法吸取了队列理论,将公平调度的概念引入了Linux调度程序。并且最终在2.6.23内核版本中替代了O(1)调度算法,它此刻被称为“完全公平调度算法”,或者简称CFS
三、策略
策略决定调度程序在何时让什么进程运行。
3.1 I/O消耗型和处理器消耗型的进程
1. I/O消耗型进程
- 进程的大部分时间用来提交I/O请求或者等待I/O请求
- 多数用户图形界面(GUI)都属于I/O密集型
2. 处理器耗费型
- 时间大多数用在执行代码上
- 例如MATLAB
- 往往要延长运行时间并降低调度频率
3. 调度策略通常要在两个矛盾的目标中间寻找平衡:进程响应迅速(响应时间短)和最大系统利用率(高吞吐量),为了满足上述需求,调度程序通常采用一套非常复杂的算法来决定最值得运行的进程投入运行,但是它往往并不保证低优先级进程会被公平对待,Unⅸ系统的调度程序更倾向于I/O消耗型程序,以提供更好的程序响应速度,Linux为了保证交互式应用和桌面系统的性能,所以对进程的响应做了优化(缩短响应想间)更倾向于优先调度I/O消耗型进程,虽然如此,调度程序也并未忽略处理器消耗型的进程。
3.2 进程优先级
1. 基于优先级的调度:优先极高的进程先运行,相同优先级的进程按照轮转方式进行调度。
2. 优先级分为两类:
- nice值(从-20——+19):默认值为0;数值越大意味着优先级越低;可以通过 ps-el查看系统进程列表并找到NI标记列对应的优先级
- 实时优先级(从0——99):越高的实时优先级级数意味着进程优先级越高
注:二者互不交互。
3.3 时间片
- 时间片表示进程在被抢占之前所能够持续运行的时间。
- 调度策略必须确定一个默认的时间片。
- Linux的CFS调度器并没有直接划分时间片到进程,而是将处理器的使用比例划分给了进程。即其抢占时机取决于新的可执行程序消耗了多少处理器使用比,如果消耗的使用比比当前进程小,则新进程立即投入运行抢占当前进程。
3.4 调度策略的活动
四、Linux调度算法
4.1 调度器类
- Linux调度器是以模块方式提供的(也就是调度器类),目的是允许不同类型的进程可以有针对性地选择调度算法。
- 调度器类允许多种不同的可动态添加的调度算法并存,调度属于自己范畴的进程。
- 每个调度器都有一个优先级,基础的调度器代码定义在sched_ fair.c文件中,它会按照优先级顺序遍历调度类,拥有一个可执行进程的最高优先级的调度器类胜出,去选择下面要执行的那一个程序。完全公平调度(CS)是一个针对普通进程的调度类,在Linux中称为SCHED_NORMAL。
4.2 Unix系统中的进程调度
- 将nice值映射到时间片的话,就必须将nice值对应到处理器的绝对时间;这样会导致进程切换无法最优进行。
- 如果使用相对nice值,所带来的效果将会极大取决于其nice的初始值。
- 如果执行nice值到时间片的映射,时间片极大受制于定时器。
CFS采用的方法是对时间片分配方式进行根本性的重新设计(就进程调度器而言)完全摒弃时间片而是分配给进程一个处理器使用比重,通过这种方式,CFS的确保了进程调度中能有恒定的公平性,而将切换频率置于不断变动中。
4.3 公平调度
CFS基于一个简单的理念:进程调度的效果应当如同系统具备一个理想中的完美任务处理器。CFS的做法如下:
- 允许每个进程运行一段时间、循环轮转、选择运行最少的进程作为下一个运行进程
- nice值作为进程获得的处理器运行比的权重(而不是完全由nice决定时间片)
- 每个进程都按照其权重在全部的可运行进程中所占的比例对应的“时间片”来运行
在理想情况下,完美的多任务处理器模型应该是这样的:我们能在5ms内同时运行两个进程,它们各自使用处理器一半的能力。
五、Linux调度的实现
CFS相关代码位于kernel/sched_fair.c中。它有四个组成部分:
- 时间记账
- 进程选择
- 调度器入口
- 睡眠和唤醒
5.1 时间记账
所有的调度器都必须对进程运行时间做记账。多数Unix系统,分配一个时间片给每一个进程。那么当每次系统时钟节拍发生时,时间片都会被减少一个节拍周期。
1. 调度器实体结构
调度器实体结构作为一个名为se的成员变量,嵌入在进程描述符struct task_ struct内。
2. 虚拟实时
- vruntime变量存放进程的虚拟运行时间
- update_ curr()计算当前进程的执行时间,是由系统定时器周期性调用的
5.2 进程选择
1. CFS算法核心:选择具有最小vrntime的任务
2. 具体做法:利用红黑树rbtree(以节点形式存储数据的二叉树)
3. 举例:
- 选择下一个任务:从根节点中序遍历二叉树,一直到叶子节点(也就是vrntime最小的进程)
- 向树中加入进程:在进程变为可执行状态或者通过fork()调用第一次创建进程
- 从树中删除进程:发生在进程阻塞或者终止的时候
5.3 调度器入口
- 进程调度的主要入口点是函数schedule(),定义在kernel/sched.c中。这正是内和其他部分用于调度进程调度器的入口。
- 这一函数最重要的工作就是调用pick_ next_ state(),依次检查每一个调度类,并从最高优先级的调度类中,选择最高优先级进程。
5.4 睡眠和唤醒
1. 等待队列:休眠通过等待队列进行处理,等待队列是由某些事件发生的进程组成的简单链表。
- 进程把自己标记成休眠状态,从可执行红黑树中移除
- 放入等待队列——由等待某些时间发生的进程组成的链表,内核用wake_ queue_ head_ t来代表等待队列
- 将进程加入到一个等待队列中:
2. 唤醒
唤醒操作由函数wake_ up()进行:
- 它会调用函数try_ to _ wake_ up()将进程设置为TASK_ RUNNING状态,调用enqueue_ task()将进程放入红黑树中
- 存在虚假唤醒进程的状态
六、抢占和上下文切换
上下文切换,就是从一个可执行进程切换到另一个可执行进程,由定义在kernel/schedule.c中的context_ switch()函数负责处理。完成了两项工作:
- 调用switch_mm(),负责把虚拟内存从上一个进程映射切换到新的进程中
- 调用switch_to(),负责从上一个进程的处理器状态切换到新进程的处理器状态
6.1 用户抢占
在内核返回用户空间的时候,它知道自己是安全的,因为既然它可以继续去执行当前进程,那么它当然可以再去选择一个新的进程去执行。所以,内核无论是在中断处理程序还是在系统调用后返回,都会检查need_resched标志,如果它被设置了,那么,内核会选择一个其他(更合适的进程投入运行。从中断处理程序或系统调用返回的返回路径都是跟体系结构相关的,在entry.S(此文件不仅包含内核入口部分的程序,内核退出部分的相关代码也在其中)文件中通过汇编语言来实现。简而言之,用户抢占在以下情况时产生:
- 从系统调返回用户空间时。
- 从中断处理程序返回用户空间时
6.2 内核抢占
与其他大部分的Unⅸ变体和其他大部分的操作系统不同,Linux完整地支持内核抢占,在不支持内核抢占的内核中,内核代码可以一直执行,到它完成为止,也就是说,调度程序没有办法在一个内核级的任务正在执行的时候重新调度——内核中的各任务是以协作方式调度的不具备抢占性。内核代码一直要执行到完成(返回用户空间)或明显的阻塞为止,在2.6版的内核中,内核引入了抢占能力;现在,只要重新调度是安全的,内核就可以在任何时间抢占正在执行的任务。
七、实时调度策略
Linux的实时调度算法提供了―种软实时工作方式,软实时的含义是,内核调度进程,尽力使进程在它的限定时间到来前运行,但内核不保证总能满足这些进程的要求。相反,硬实时系统保证在一定条件下,可以满足任何调度的要求。Linux对于实时任务的调度不做任何保证。虽然不能保证硬实时工作方式,但Linux的实时调度算法的性能还是很不错的。2.6版的内核可以满足严格的时间要求。
八、与调度相关的系统调用
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值
8.2 与处理器绑定有关的系统调用
Linux调度程序提供强制的处理器绑定机制。虽然它尽力通过一种软的(或者说自然的)亲和性试图使进程尽量在同一个处理器上运行,但它也允许用户强制指定“这个进程无论如何都必须在这些处理器上运行”。这种强制的亲和性保存在进程的一个位掩码标志中。该掩码标志的每一位对应一个系统可用的处理器,默认情况下所有的位都被设置。
8.3 放弃处理器时间
Linux通过sched_ yield()系统调用,提供了一种让进程显式地将处理器时间让给其他等待执行进程的机制,它是通过将进程从活动队列中(因为进程正在执行,所以它肯定位于此队列当中)移到过期队列中实现的,由此产生的效果不仅抢占了该进程并将其放入优先级队列的最后面,还将其放入过期队列中—这样能确保在一段时间内它都不会再被执行了,由于实时进程不会过期,所以属于例外,它们只被移动到其优先级队列的最后面(不会放到过期队列中)。
在Linux以的早期版本中,进程只会被放置到优先级队列的末尾,放弃的时间往往不会太长,现在,应用程序甚至内核代码在调用sched_ yield()前,应该仔细考虑是否真的希望放弃处理器时间。内核代码为了方便,可以直接调用sched_ yield(),先要确定给定进程确实处于可执行状态,然后再调用sched_ yield(),用户空间的应用程序直接使用sched_ yield()系统调用就可以。
总结
通过对本章进程调度的学习,我了解到进程调度程序是内核重要的组成部分,因为运行着的进程首先在使用计算机。但是满足进程调度的各种需要是较难实现的。例如公平调度中,越小的调度周期就会表现出越好的交互性,也更接近于“同时完成多任务”这一目标。然而系统必须承受更高的切换代价和更差的系统吞吐量,即鱼与熊掌不可兼得。不过,Linux内核的新CFS调度程序尽量满足了各个方面的需求,并以较完善的可伸缩性和新颖的方法提供了最佳的解决方案。