第四章 进程调度

为什么要进行进程调度:因为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()函数负责,完成两个基本工作:

  1. 把虚拟内存从上一个进程映射到下一个进程。
  2. 从上一个处理器状态切换到新进程的处理器状态。需要保存、恢复栈信息和寄存器信息,还有其他相关状态信息。

need_resched标志:某个进程应该被抢占或者当一个优先级高的进程进入可执行状态时,进程的这个标志就会被设置。可以向内核表明信息,要尽快调用调度程序。
在返回用户空间以及中断返回时,内核也会检测该标志,如果被设置,内核会在继续执行前先调用调度程序。

用户抢占

指的是在内核返回用户空间时,如果need_resched被设置,会调用schedule(),发生用户抢占。

用户抢占发生的情况

  1. 从系统调用返回用户空间
  2. 从中断处理程序返回用户空间

内核抢占

不支持内核抢占的系统:
内核代码一直运行,直到完成。调度程序不能在一个内核级任务执行时重新调度。
内核抢占:
只要重新调度是安全的(没有持有锁),内核可以在任何事件抢占正在执行的任务。
为每个进程的thread_info引入了计数器preempt_count。初值为0,使用锁时就加1,释放时减1。当数值为0时,内核就可以抢占。
从中断返回内核空间时,内核检查need_resched和preempt_count,如果need_resched被设置,preempt_count为0,那么可以安全抢占;如果preempt_count不为0,则当前任务持有锁,抢占不安全,内核会从中断返回当前执行程序。当锁被释放,检查need_resched,如果被设置,则重新调度。
如果内核中的进程阻塞了,会显式地调用了schedule(),内核抢占也会显式发生。

内核抢占情况:

  1. 中断处理程序正在执行,且返回内核空间前
  2. 内核代码再一次具有可抢占性地时候
  3. 内核显示调用schedule()
  4. 内核中的进程阻塞了

实时调度策略

两种调度策略:

  • 实时调度策略: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()系统调用

  • 提供一种让进程显式将处理器时间让给其他等待执行进程的机制。
  • 通过将进程从活动队列移到过期队列实现,过期队列确保在一段时间不再执行。
  • 实时进程是例外,不会过期,只是被移动到优先级队列最后面。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值