为什么要进行进程调度:因为CPU资源是有限的,需要在进程间分配有限的处理器时间资源。
调度程序:决定哪个程序运行,何时运行,运行多久
多任务系统
- 多处理器:可实现并行
- 单处理器:并发
分类
-
抢占式多任务
时间片:分配给进程的运行时间
抢占:指调度程序在进程用尽时间片后,将其强制挂起以便其他进程运行的操作
调度程序利用时间片做出调度决定,避免个别进程独占资源。 -
非抢占式多任务
让步:非抢占模式下,除非进程自己挂起,否则就会一直执行。进程主动挂起自己的操作就是让步。
理想情况下,进程需要做出让步,以便其他进程有足够的时间处理。
缺点:调度程序无法对进程的执行时间做出规定,如果进程不让步的话,会使系统崩溃。
Linux的进程调度程序的发展
- O(1)调度程序
使用静态时间片算法
缺点:对交互程序表现不佳 - 完全公平调度算法(CFS)
调度器的策略进程
进程类型
- I/O消耗性
多数时间在等待I/O请求,但是一旦读取,需要立刻响应
不需要较长时间片 - 处理器消耗型
通常没有太多I/O需求,一直在不停运行
需要较长时间片
调度算法应该尽量降低其调度频率而延长其调度时间
调度策略常常要在进程响应迅速和最大系统利用率之间平衡。
Linux和Unix常常更倾向于I/O消耗型程序。
进程优先级
调度算法中最基本的一类是基于优先级调度。
根据进程价值和其对处理器时间需求设置优先级。
通常优先级高的先运行,低的后运行,相同优先级进行轮转调度。
Linux两种优先级范围:
- nice值:越大优先级越低(-20~19)
- 实时优先级:越大优先级越高(0~99)
时间片
时间片太长:响应差
时间片太短:切换开销
多数操作系统:通过优先级和时间片调度进程
Linux CFS调度器:没有直接分配时间片,通过处理器使用比来调度,将使用比比当前进程小的投入运行。
Linux调度算法
调度器类
模块化结构。有多种不同调度算法并存。
每个调度器都有优先级,按照优先级遍历类,再由最高优先级调度器类调度进程。
Unix系统的进程调度
- 时间片
- 优先级:nice值和时间片的映射是绝对的
缺点:分配绝对的时间,引发固定频率的切换,给公平性造成很大的隐患
CFS调度
不使用时间片,而是分配给进程的处理器使用比重。
nice值作为进程获得处理器运行比的权重。
每个进程获得的处理器时间是自身与其他所有可运行进程nice值得相对差值决定的。
可以调度给它们无限小的时间周期(目标延迟),使得n个进程占用同样的1/n的处理器时间。
最小粒度:1ms。每个进程获得的时间片底线。
Linux调度的实现
时间记账
调度器对每个进程运行时间进行记账,每次系统时钟节拍变化,时间片就减少一个周期,当为0时,被其它不为0的时间片抢占。
虚拟实时:vruntime变量。记录一个程序运行了多久以及还需运行多久
进程选择
CFS调度算法的核心:挑选最小的vruntime进程下一个运行。
实现:使用红黑树组织可运行进程队列,并迅速找到最小的vruntime进程下一个运行。
挑选下一个进程:选择红黑树的最左边叶子结点所代表的进程
向树中加入进程:在进程变为可运行态或通过fork()第一次创建进程时,将进程插入到红黑树中,同时维护一个缓存,存放树的最左边的叶子结点
从树中删除进程:删除动作发生在进程阻塞(变为不可运行状态)或者终止时(结束运行)。如果删除的是最左边的叶子结点,就要寻找下一个结点,更新缓存。
调度器入口
函数schedule():选择哪个程序可以运行,何时将其投入运行
通常和一个具体的调度类相关联,它会找到一个最高优先级的调度类,后者要有自己的可运行队列,然后问后者哪个是下一个运行的进程。
睡眠和唤醒
进程休眠:休眠(被阻塞的)进程进入一个特殊的不可执行状态
休眠原因:等待某些事件
- 执行了某些文件I/O,如read(),需要从磁盘读取
- 获取键盘输入,需要等待
- 尝试获取一个已被占用的内核信号量时被迫休眠
休眠的两种进程状态
- TASK_INTERRUPTIBLE 收到信号,会被提前唤醒并响应信号
- TASK_UNINTERRUPTIBLE 忽略信号,只等待事件唤醒
等待队列:
由等待某些事件发生的进程组成的简单链表。
休眠时内核的操作:
进程将自己标记为休眠状态,从可执行红黑树中移出,放入等待队列,调用schedule()选择和执行下一个进程。
唤醒时内核的操作:
进程被设置为可执行状态,从等待队列移到可执行红黑树中。
抢占和上下文
上下文切换:从一个可执行进程切换到另一个可执行进程
由context_switch()函数负责,完成两个基本工作:
- 把虚拟内存从上一个进程映射到下一个进程。
- 从上一个处理器状态切换到新进程的处理器状态。需要保存、恢复栈信息和寄存器信息,还有其他相关状态信息。
need_resched标志:某个进程应该被抢占或者当一个优先级高的进程进入可执行状态时,进程的这个标志就会被设置。可以向内核表明信息,要尽快调用调度程序。
在返回用户空间以及中断返回时,内核也会检测该标志,如果被设置,内核会在继续执行前先调用调度程序。
用户抢占
指的是在内核返回用户空间时,如果need_resched被设置,会调用schedule(),发生用户抢占。
用户抢占发生的情况
- 从系统调用返回用户空间
- 从中断处理程序返回用户空间
内核抢占
不支持内核抢占的系统:
内核代码一直运行,直到完成。调度程序不能在一个内核级任务执行时重新调度。
内核抢占:
只要重新调度是安全的(没有持有锁),内核可以在任何事件抢占正在执行的任务。
为每个进程的thread_info引入了计数器preempt_count。初值为0,使用锁时就加1,释放时减1。当数值为0时,内核就可以抢占。
从中断返回内核空间时,内核检查need_resched和preempt_count,如果need_resched被设置,preempt_count为0,那么可以安全抢占;如果preempt_count不为0,则当前任务持有锁,抢占不安全,内核会从中断返回当前执行程序。当锁被释放,检查need_resched,如果被设置,则重新调度。
如果内核中的进程阻塞了,会显式地调用了schedule(),内核抢占也会显式发生。
内核抢占情况:
- 中断处理程序正在执行,且返回内核空间前
- 内核代码再一次具有可抢占性地时候
- 内核显示调用schedule()
- 内核中的进程阻塞了
实时调度策略
两种调度策略:
- 实时调度策略:SCHED_FIFO和SCHED_RR
不被完全公平调度器管理,使用特殊的实时调度器管理。
SCHED_FIFO:不使用时间片,SCHED_FIFO级的进程一旦处于可执行状态,会一直运行,直到阻塞或终止。只有更高优先级的SCHED_FIFO和SCHED_RR进程才能抢占,多个同优先级的SCHED_FIFO级进程轮流执行,其他级别低的SCHED_NOMAL级进程只能等待其运行结束。
SCHED_RR:大致同上,只是它是带时间片的,耗尽分配的时间片就不能运行了,时间片用来重新调度同一优先级进程。低优先级进程不能抢占SCHED_RR,即使它时间片耗尽。
默认实时优先级:0~99 - 普通非实时调度策略:SCHED_NOMAL
与调度相关的系统调用
Linux提供了一个系统调用族,用于管理与调度程序相关的函数。如设置或获取调度策略,实时优先级扽的系统调用。
Linux调度程序提供强制的处理器绑定机制。
sched_yield()系统调用
- 提供一种让进程显式将处理器时间让给其他等待执行进程的机制。
- 通过将进程从活动队列移到过期队列实现,过期队列确保在一段时间不再执行。
- 实时进程是例外,不会过期,只是被移动到优先级队列最后面。