第四章 进程调度
调度程序负责决定将哪个进程投入运行,何时运行以及运行多长时间。进程调度程序可看做在可运行态进程之间分配有限的处理器时间资源的内核子系统。
最大限度利用处理器时间的原则:只要有可以执行的进程,那么总会有程序正在执行。
一、多任务
1.概念:多任务操作系统就是能同时并发地交互执行多个进程的操作系统,在单处理器机器上这会产生多个进程在同时运行的幻觉,在多处理器机器上,这会使多个进程在不同的处理机上真正同时、并行地运行。
- 无论在单处理器或者多处理器机器上,多任务操作系统都能使多个进程处于堵塞或者睡眠状态,也就是说,实际上不被投入执行,直到工作确实就绪
- 这些任务尽管位于内存,但并不处于可运行状态。相反,这些进程利用内核阻塞自己,直到某一事件(键盘输入网络数据、过一段时间等)发生。因此,现代Linux系统也许有100个进程在内存,但是只有一个处于可运行状态
2. 分类:多任务系统可以划分为两类
- 非抢占式多任务。进程会一直执行直到自己主动停止运行(这一步骤称为让步)
- 抢占式多任务。Linux/Unix使用的是抢占式的方式;强制的挂起进程的动作就叫做抢占。进程在被抢占之前能够运行的时间是预先设置好的(也就是进程的时间片)
二、Linux的进程调度
开始采用了一种叫做O(1)调度程序的新调度程序——因其算法的行为而得名。
O(1)调度程序:对于调度那些响应时间敏感的程序---交互进程存在不足。
RSDL反转楼梯最后期限调度算法。
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调度器并没有直接划分时间片到进程,而是将处理器的使用比例划分给了进程。即其抢占时机取决于新的可执行程序消耗了多少处理器使用比,如果消耗的使用比比当前进程小,则新进程立即投入运行抢占当前进程。
- Linux调度器是以模块方式提供的(也就是调度器类),目的是允许不同类型的进程可以有针对性地选择调度算法。
- 调度器类允许多种不同的可动态添加的调度算法并存,调度属于自己范畴的进程。
- 每个调度器都有一个优先级,基础的调度器代码定义在sched_ fair.c文件中,它会按照优先级顺序遍历调度类,拥有一个可执行进程的最高优先级的调度器类胜出,去选择下面要执行的那一个程序。完全公平调度(CS)是一个针对普通进程的调度类,在Linux中称为SCHED_NORMAL。
- 将nice值映射到时间片的话,就必须将nice值对应到处理器的绝对时间;这样会导致进程切换无法最优进行。
- 如果使用相对nice值,所带来的效果将会极大取决于其nice的初始值。
- 如果执行nice值到时间片的映射,时间片极大受制于定时器。
3.4 调度策略的活动
四、Linux调度算法
4.1 调度器类
4.2 Unix系统中的进程调度
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()函数负责处理。
内核提供了一个need_resched标志来表明是否需要重新执行一次调度。
完成了两项工作:
- 调用switch_mm(),负责把虚拟内存从上一个进程映射切换到新的进程中
- 调用switch_to(),负责从上一个进程的处理器状态切换到新进程的处理器状态
6.1 用户抢占
用户抢占在以下情况时产生:
- 从系统调返回用户空间时。
- 从中断处理程序返回用户空间时
6.2 内核抢占
锁是非抢占区域的标志,只要没有持有锁,内核就可以进行抢占。
每个进程的thread_info引人preempt_count计数器。该计数器韧始值为0,每当使用锁的时候数值加1,释放锁的时候数值减1。当数值为0的时候,内核就可执行抢占。
内核抢占发生在:
·中断处理程序正在执行,且返回内核空间之前;
·内核代码再一次具有可抢占性的时候;
·如果内核中的任务显式地调用schedule();
·如果内核中的任务阻塞(这同样也会导敖调用schedule())。
七、实时调度策略
Linux 提供了两种实时调度策略:SCHED_FIF0和SCHED_RR。而普通的、非实时的调度策略是SCHED_NORMAL。
SCHED_FIFO实现了一种简单的、先入先出的调度算法:它不使用时间片。
SCHED_RR是带有时间片的SCHED_FIFO---这是一种实时轮流调度算法。
对于SCHED_FIFO进程,高优先级总是立即抢占低优先级,但低优先级进程决不能抢占SCHED_RR 任务,即使它的时间片耗尽。
这两种实时算法实现的都是静态优先级。
Linux的实时调度算法提供了一种软实时工作方式。软实时的含义是,内核调度进程,尽力使进程在它的限定时间到来前运行,但内核不保证总能满足这些进程的要求。相反,硬实时系统保证在一定条件下,可以满足任何调度的要求。
八、与调度相关的系统调用
8.1 与调度策略和优先级相关的系统调用
- sched_ setscheduler()和 sched_ getscheduler()分别用于设置和获取进程的调度策略和实时优先级。与其他的系统调用相似,它们的实现也是由许多参数检查、初始化和清理构成的。其实最重要的工作在于读取或改写进程task_ struct的policy和rt_ priority的值
- sched_ setscheduler()和 sched_ getscheduler()分别用于设置和获取进程的实时优先级。这两个系统调用获取封装在sched_ param特殊结构体的rt_ priority中。实时调度策略的的最大优先级:是MAX_ USERRT_PRIO减1。最小优先级等于1
- sched_get_priority_max()和sched_get_priority_min()分别用于返回给定调度策略的最大和最小优先级。
- 对于―个普通的进程,nice函数可以将给定进程的静态优先级增加一个给定的量。只有超级用户才能在调用它时使用负值,从而提高进程的优先级。nice函数会调用内核的set_ user_ nice函数,这个函数会设置进程的的task_ struct的static_ prio值
8.2 与处理器绑定有关的系统调用
Linux调度程序提供强制的处理器绑定机制。这种强制的亲和性保存在进程task_struct的cpus_allowed 这个位掩码标志中。
虽然它尽力通过一种软的(或者说自然的)亲和性试图使进程尽量在同一个处理器上运行,但它也允许用户强制指定“这个进程无论如何都必须在这些处理器上运行”。这种强制的亲和性保存在进程的一个位掩码标志中。该掩码标志的每一位对应一个系统可用的处理器,默认情况下所有的位都被设置。
8.3 放弃处理器时间
Linux通过sched_ yield()系统调用,提供了一种让进程显式地将处理器时间让给其他等待执行进程的机制,它是通过将进程从活动队列中(因为进程正在执行,所以它肯定位于此队列当中)移到过期队列中实现的,由此产生的效果不仅抢占了该进程并将其放入优先级队列的最后面,还将其放入过期队列中—这样能确保在一段时间内它都不会再被执行了,由于实时进程不会过期,所以属于例外,它们只被移动到其优先级队列的最后面(不会放到过期队列中)。
内核代码为了方便,可以直接调用yield(),先要确定给定进程确实处于可执行状态,然后再调用sched_yield()。
用户空间的应用程序直接使用sched_yield()系统调用就可以了。
在Linux以的早期版本中,进程只会被放置到优先级队列的末尾,放弃的时间往往不会太长,现在,应用程序甚至内核代码在调用sched_ yield()前,应该仔细考虑是否真的希望放弃处理器时间。内核代码为了方便,可以直接调用sched_ yield(),先要确定给定进程确实处于可执行状态,然后再调用sched_ yield(),用户空间的应用程序直接使用sched_ yield()系统调用就可以。