zoukankan      html  css  js  c++  java
  • 操作系统3:CPU调度

    CPU调度

    基础

    image-20200203112441613.png
    CPU利用率=CPU充分利用时间/CPU使用总时间

    image-20200203112546833.png
    image-20200203112903936.png
    image-20200203113038944.png

    来源:https://www.zhihu.com/question/266544961

    Schedule和dispatch的区别?

    用“主要业务逻辑”做在哪里来区分。

    schedule: 调度。 scheduler是具有重业务逻辑的,也就是调度算法。例如,linux kernel从中断出来后,会切入到scheduler routine,因为这里面的逻辑很多(CFS,RR,FIFO等调度算法),所以叫做 调度。

    dispatch: 分发、派发。 dispatcher是轻业务逻辑或者没有业务逻辑,纯粹简单的转发、派发。例如,负载均衡的dispatcher收到请求后,通过哈希表找到response server,直接将请求派发下去,本身并不做业务逻辑。

    有的操作系统scheduler和dispatcher是分开的,有的是合二为一的,例如后面要讲的linux版本中就是

    调度器调度的时机举例:

    image-20200203114451302.png
    第一种是因为该进程需要资源,所以处于等待状态

    第二种是时间片被抢走了,例如有一个高优先级的进程进入就绪队列

    第三种是需要的资源到了,因为优先级比较高,抢夺了时间片。这种和第二种其实是一回事

    还有其他的许多情况需要调用调度器,例如CPU时间片到期等等

    非抢占式:进程自愿交出CPU资源引起的调度

    image-20200203115419459.png
    分配器做的事情不涉及到决策,是固定的事情

    分配延时应该尽可能减少

    image-20200203163540711.png
    等待时间:一个进程的任务很可能是不能在一次就执行完的,可能需要多次等待,所以这是个累计时间

    响应时间:到调度器开始响应就算完,不需要完成响应

    先来先服务算法(FCFS)

    image-20200203164249872.png
    原理和名称一样,用一个队列就可以了

    显然这个例子里平均等待时间不是最优的,但是CPU利用率的确是100%

    换一下顺序,平均等待时间就好多了:

    image-20200203165005591.png

    最短优先算法(SJF)

    image-20200203165123066.png
    这个算法倒是能使得平均等待时间最短

    image-20200203165226261.png
    也可以是抢占式的:

    image-20200203165413043.png
    每次有进程进入就绪状态时,比较时是和原来在执行的进程的剩余执行所需时间相比

    两种策略对比:

    image-20200203165642348.png
    但是这种算法有致命问题:进程是没办法准确预报自己花费的CPU时间的(执行时间可能和资源和实际情况有关),所以只能停留在理论上23333

    但是也不是完全不能预估,只不过就是要基于概率模型了,也就是有预估错误的可能性:

    image-20200203170201707.png
    tn表示第n次的真实时间

    image-20200203170454445.png

    最高响应比优先算法(HRN)

    image-20200203170549226.png
    W表示该进程自从进入就绪队列至今的等待时间

    HRN是大于等于1的,基准值是1

    总是为相对等待时间较长的进程服务,这个“相对”表示的是相对于它的预估的CPU时间:需要CPU时间长的,就应该等待的长一点,反之,就应该短一点

    优先权法

    image-20200203214343361.png
    这是比较流行的一种方法

    在PCB中加一个优先数

    可以使用堆来每次获得优先级最高的进程

    优先权法的一个问题:

    image-20200203215041393.png
    从上也可以看出来不是什么大问题,所以这种算法是比较好的,也是在实际操作系统中比较常用的一种算法

    轮转法

    image-20200203220744051.png
    这种方法最原始的思想就是这样的,众生平等,轮流来

    这种思路的一个好处是,任何一个进程的等待时间是有上限的,所以这种算法也是很流行的

    image-20200203220753726.png
    分配给不同的进程时每次切换都要做一次上下文切换,对效率是有影响的

    image-20200203221530909.png
    所以时间片长度的选择是很重要的

    image-20200203220800515.png
    q大的话完成一个进程不需要做切换,就是先到显得了(FCFS),也不好

    多层队列法

    就绪队列中的进程是有不同的诉求的,有的侧重于低响应时间,有的侧重于充足的处理时间

    前台侧重于响应时间,使用轮转法;后台对响应时间不那么敏感,使用先来先服务算法

    image-20200203221756002.png
    不同队列间有不同的优先级

    image-20200203221805599.png

    • 系统调度要尽量实时响应
    • 交互要尽量及时
    • 批处理和用户交互没什么关系,所以响应时间没那么重要

    image-20200203221813358.png
    现在时间片的竞争主要是队列间的竞争

    多层反馈队列法

    image-20200203222431647.png
    反馈指的是就绪进程在队列间的迁移,这主要考虑到进程在不同情况下、任务下对实时性的需求是有变化的,有时候重交互,有时候重计算

    举一个例子:

    image-20200203222448144.png
    image-20200203222454058.png
    image-20200203222501462.png
    注意这只是调度方法中的一种,是否合理要看实际需求能否被满足

    如何设计一个多层反馈队列:

    image-20200203222440402.png

    实时调度

    希望调度要很及时,但是不同任务的及时性的要求是不一样的

    但是这一点是很难的,特别是通用性的操作系统,几乎不可能实现及时性(因为不同的进程要求不一样,用统一的调度方法当然不可能完全合适)

    • 在嵌入式系统中使用硬实时调度,也就是时间节点是明确的,如果没有按时完成就作废
    • 通用操作系统一般满足的是软实时调度,只能是尽量满足

    image-20200203223146648.png

    调度算法的评估

    要设计评估方法,尽量在早期就能对调度算法进行评估,不能依赖实际测试(因为开发周期长),所以下图中的第三种方案一般不靠谱。由于实际情况是非常复杂的,要确定和理论计算非常难,所以确定模型法也一般不靠谱

    image-20200203223155423.png
    排队模型:先建立一个模型来模拟实际情况对算法进行测试,但是这对于模型的准确性就有了要求,要求其能尽可能反映实际使用场景,但是模型的准确率也是不容易很高的

    image-20200203223203871.png
    仿真:采集实际情况的数据,临时搭建平台进行测试,采集指标

    这种方法的效果还是不错的,理论难度也比较低,是一种偏工程性的方法

    linux调度算法

    内存空间有4G,其中3G是用户空间,1G是内核空间

    将优先权法和轮转法相结合:时间片到了之后引起中断响应,中断时从当前执行的进程的PCB拿出来,访问counter并减一,如果不等于0,就该进程继续响应;如果减到0,就重新设置counter(不设置统一的值,可以给每个进程设置一个值)、调用scheduler基于优先级重新调度

    这是Linux较为早期时候的调度算法

    image-20200204102415127.png
    sched_data:表示该CPU最近在为哪个进程在服务,该进程的上下文就保存在这里,这是为了多CPU而设置的

    prev指向调度前的进程的PCB,next是将要调度给的进程的PCB,p是一个中间变量

    tmp:用于优化用,没有实际含义

    this_cpu:当前进程使用的是哪个CPU

    c:临时变量,保存进程的优先权

    image-20200204102819506.png
    和调度无关,如果有错就跳出,不继续调度

    image-20200204103118141.png
    current指向当前执行进程的PCB。正如前面所说的那样,Linux操作系统中没有状态字来表示进程是正在执行的,TASK_RUNNING的含义是就绪状态。所以这里就用current来指向正在执行的进程的PCB

    image-20200204103346872.png
    如果正在处于中断,就不继续调度了,直接返回

    image-20200204103440441

    和同步相关

    image-20200204103556587.png
    softirq:软中断。这里表示如果有软中断的工作还没有完成,就先去做这些工作

    image-20200204103712923.png
    image-20200204103908081.png
    image-20200204103935799.png
    和调度直接相关。

    轮转法:

    image-20200204104125215.png
    如果prev的counter变成了0,就重新设置,注意设置时候传入的参数是prev->nice,也就是那个进程自己给自己设置的优先级,基于此(当然不光只考虑这一个参数)来重新设置counter

    image-20200204112206507.png
    image-20200204112213554.png
    state表示进程状态

    default处理其他状态的情况,从就绪队列中删除,不再继续执行

    TASK_INTERRUPTABLE这个是怎么回事呢?它是为了处理这样一种特殊情况:

    当前进程在执行的过程中需要进行一次中断,例如访问IO,但是正在它将自己的状态由TASK_RUNNING换为TASK_INTERRUPTABLE后,CPU时间片到期,通过中断发生了进程调度,它的counter也变成了0,被从current变成prev,加入了就绪队列的末尾。此时将它从TASK_INTERRUPTABLE状态恢复(也就是从等待队列中除去),也就是对于它的原有中断要放在之后再响应了

    image-20200204113650564.png
    因为正在做调度(因为调度也是需要CPU时间的),所以不允许在下个时间片发生schedule()

    image-20200204113858823.png
    先将next的初值置为idle task,也就是初始进程,并将其优先级设置为最小的。意思就是一般不调度这个原始进程,如果没有其他进程需要调度的话才调度,这个原始进程做一些进程监测和管理之类的工作

    image-20200204114110059.png
    image-20200204114338322.png
    选择最高优先级的部分在still_running_back中完成

    看算法,就是一次遍历

    image-20200204163117215.png
    image-20200204163130361.png
    这种情况出现在所有进程的goodness都是0的情况下,意思就是就绪队列中的所有进程的counter都是0了,也就是时间片的时间都已经到了

    recalculate:

    image-20200204170941284.png
    注意这里使用了一个C语言的特性——复合语句,就是花括号括起来的部分,和代码块的概念比较类似。这里定义的局部变量只能在这里面使用,外部无法访问

    >>1表示右移一位,也就是除2

    image-20200204171340858.png
    然后再执行上面的still_running_back,就一定能选出c和next了

    image-20200204171635263.png
    开始布置现场

    image-20200204175052088.png
    这种不需要内存管理切换和上下文切换,所以之后到最后一步

    image-20200204175118673.png
    kstat.content_switch记录了从开机以来的上下文切换次数

    image-20200204175214231.png
    prepare_to_switch是做上下文切换的准备工作

    image-20200204175253992.png
    image-20200204175259996.png
    image-20200204175502994.png
    注意一点,648行完了执行的还是649行吗?其实不是的,因为在648行已经完成了寄存器的转换,此时PC寄存器中保存的已经是另外的进程的PCB中的PC值了。649行何时执行呢?只有当这一次失去时间片的进程再次被调度才会被执行(会不会有不切换的情况呢,也就是获得调度的进程就是当前进程?这是不会的,因为看前面,如果不切换的话会直接从前面就调到same_process)

    image-20200204175513167.png
    判断是否有调度需求,如果没有的话schedule()函数就结束了,返回内核中调用该函数处。

    由此可见,schedule函数其实还做了dispatch的工作

    goodness函数

    image-20200204114445336.png

    什么是实时进程

    https://blog.csdn.net/helloanthea/article/details/28877221

    Linux的进程分普通进程和实时进程,普通进程即非实时进程SCHED_OTHER或SCHED_NORMAL,而实时进程又分SCHED_FIFO与SCHED_RR,实时进程的优先级(099)都比普通进程的优先级(100139)高,且直到死亡之前始终是活动进程,当系统中有实时进程运行时,普通进程几乎是无法分到时间片的(只能分到5%的CPU时间)。

    image-20200204114839692.png
    image-20200204114848140.png
    out是什么呢:

    image-20200204115623623.png
    out其实就是程序出口

    image-20200204114940024.png
    image-20200204114955770.png
    将时间片的剩余值作为优先权,剩余时间越多越优先执行

    mm是进程中管理内存的数据结构

    image-20200204115048317.png
    image-20200204115055120.png
    优先权增加是因为不需要切换内存管理空间,上下文切换起来会更快

    image-20200204115111786.png
    由此可见这里的nice是越小越好

    image-20200204115609681.png

    评价

    时间复杂度为O(n),n是就绪队列中的进程个数,这个应该比较好想,这个时间开销也还行,所以就一直用了。

    但是有更好的:

    优化

    创建一个链表数组,数组的每个位置代表一个优先级,就绪状态的进程直接挂到数组的对应位置上。然后设置一个字节,表示当前有进程的最高的优先级是多少,这样调度的时候直接去找就好了。这种方法的复杂度是O(1)的

    但是如果只使用一个数组的话,就会发生,高优先级的进程未完成的话会被反复调度,低优先级的进程无法获得时间片。所以实际上是设置了两个数组的,一个是active数组,一个是non_active数组,选择只在active数组进行,调度完成的进程回到non_active数组。只有当active数组上的进程都被调度一遍后,active数组和non_active数组交换(这是很简单的,只需要指向数组的指针交换一下就好)

    但是这其实也不是十全十美的,例如它还有公平性的问题:有的应用程序可以开多进程从而有较大的可能性被调度执行,之后版本的Linux在此有了优化

  • 相关阅读:
    四则运算2
    大二第二学期阅读计划
    第一周课堂练习
    《人月神话》读后感
    软件工程概论总结第十一章
    软件工程概论总结第十章
    软件工程概论总结第九章
    软件工程概论总结第八章
    软件工程概论总结第七章
    第五章、软件过程中的形式化方法
  • 原文地址:https://www.cnblogs.com/jiading/p/12289036.html
Copyright © 2011-2022 走看看