《操作系统导论》学习笔记(四):CPU虚拟化(策略)

C P U ( m e c h a n i s m ) ( p o l i c y ) CPU虚拟化包括微观和宏观两个方面,微观层面是实现进程切换的机制(mechanism),宏观层面是进程调度的\\策略(policy)。

工作负载假设(Workload Assumptions)

在介绍进程的调度策略之前,让我们先对系统中运行的进程(有时统称为工作负载(workload))进行一些简化假设。

  1. 作业运行的时间相同
  2. 作业同时到达
  3. 作业一旦开始必须运行至结束
  4. 作业仅使用CPU(即不使用I/O)
  5. 作业运行时间已知

先设立以上假设,并逐步放松假设并放介绍相应的调度策略,最终达到全业务调度规程(fully-operational scheduling discipline)

调度指标(Scheduling Metrics)

为比较各调度策略的性能,我们需要设置调度指标进行判断。不同操作系统的评价指标各不相同,批处理操作系统以周转时间评价作业效率,交互式操作系统以响应时间反映用户体验,而Linux操作系统内核将进程的公平性纳入考量。

(1) 周转时间:从作业提交到作业完成所经历的时间。
(2) 响应时间:从用户提交请求到系统首次响应所需要的时间。
(3) 公平性:保证每个进程都能获得一定程度的CPU时间。

调度指标:周转时间和响应时间

原始策略: 先进先出 (First In First Out, FIFO)

最基本的策略是先进先出(FIFO),也叫先来先服务(FCFS)。采用该策略,先请求CPU的进程先分配到CPU。FIFO策略可以通过队列实现,当一个进程进入到就绪队列,其PCB链接到队列的尾部。当CPU空闲时,CPU分配给位于队列的进程,接着将该进程从队列中删除。

进程A,B,C几乎同时到达,到达顺序为A->B->C,进程运行时间均为10s,则进程调度情况顺序如下:
在这里插入图片描述
进程的到达时间均为0ms,完成时间为10ms、20ms和30ms,则周转时间为10ms、20ms和30ms,平均周转时间为(10+20+30)/3=20ms。

现放松假设1:作业运行的时间相同,修改进程A的运行时间为100s,则进程B和进程C分别需要等待100ms和110ms才能开始工作,平均周转时间为(100+110+120)/3=110ms。
在这里插入图片描述
FIFO的优点是实现简单且容易理解,但放松假设1后容易出现护航效果(convey effect)

C P U 使 C P U 护航效果:所有其他进程都在等待一个大进程释放CPU,从而使得CPU性能显著降低。

假设1:最短作业优先(Shortest Job First, SJF)

解决护航效果的方法是短作业优先算法,它按照作业的工作时间从短到长调度。

进程A,B,C几乎同时到达,到达顺序为A->B->C,进程运行时间为100ms、10ms、10ms,采用SJF算法调度,则进程调度情况如下:
在这里插入图片描述
进程的到达时间均为0ms,完成时间为10ms、20ms和120ms,则周转时间为10ms、20ms和120ms,平均周转时间为(10+20+120)/3=50ms,相比FIFO算法,平均周转时间得到显著减少。

现放松假设2:作业同时到达,进程A在0ms到达,进程B和进程C在10ms到达,完成时间为100ms、110ms、120ms,周转时间为100ms、100ms、110ms,平均周转时间反弹增加为(100+100+110)/3=103.33ms,再次出现护航效果。

假设2、3:最短剩余时间优先(Shortest Time-to-Completion First, STCF)

为了解决放松假设2后SJF算法的缺陷,我们可以采用同时放松假设3: 作业一旦开始必须运行至结束,不限制进程一定要运行直至结束,利用上节讲到的时钟中断(timer interrupt)强制切换CPU运行的进程。切换的依据就是已到达进程剩余执行时间,该调度算法称为可剥夺式最短作业优先(Preemptive Shortest Job First, PSJF)或者最短剩余时间优先(Shortest Time-to-Completion First, STCF)

在这里插入图片描述
进程A率先在0ms到达,运行10ms时,进程B和进程C几乎同时到达,到达顺序为B->C,此时进程剩余时间为90ms、10ms、10ms,调度进程B运行10ms,再调度进程C运行10ms,最后运行进程A至结束,平均周转时间为(120+10+20)/3=50ms。

周转时间仅适用于评价批处理操作系统,所以我们需要增加响应时间作为新的指标以适应交互式操作系统。

假设所有进程均在0ms到达,平均周转时间为(5+10+15)/3=10ms,但平均响应时间为(0+5+10)/3=5ms,这意味着在交互式操作系统中,用户打开应用程序后,平均需要在屏幕前等5ms才能得到第一次响应。
在这里插入图片描述
那么有什么办法可以在放松假设1-3的条件下,减少进程B和进程C的响应时间,让用户进程早些被调度呢?这就引出了接下来的方法——轮询(Round Robin,RR)

响应时间:轮询(Round Robin, RR)

c p u ( t i m e s l i c e ) C P U 便 轮询:通过时分复用将cpu时间划分为固定大小的时间片(time slice),每个进程在CPU运行一个时间片,时间片\\一到便切换运行到下一程序。

确定时间片为1ms,那么每个进程将运行1ms后切换到下一个进程。运行顺序为A->B->C,如此循环直至结束,响应时间为0ms、1ms和2ms,平均响应时间为(0+1+2)/3=1ms,平均周转时间增加4ms,平均响应时间下降4ms。
在这里插入图片描述
如果我们以响应时间为衡量标准,那么轮询是个好的调度策略。但如果我们以周转时间为衡量标准,那么应该选择其他调度策略,因为现实操作系统的频繁调度增加的开销不止4ms这么简单。

假设4:合并I/O(Incorporating I/O)

现在我们放松假设4: 作业仅使用CPU(即不使用I/O),让CPU运行多个进程的同时处理I/O请求。

假设现有进程A和进程B,每个进程都需要50毫秒的CPU时间。然而,有一个明显的区别:A运行10毫秒,然后发出一个I/O请求(这里假设每个I/O需要10毫秒,而B只使用CPU 50毫秒,且不执行I/O。调度器先运行A,然后运行B。
在这里插入图片描述
显然上面的调度策略有改善的余地,常见的方法是将进程A的每个10 ms视为独立进程,采用STCF进行调度每个独立进程和进程B。当系统启动时,CPU选择进程A运行10s后阻塞进程A,就绪队列只剩下B,CPU运行进程B。I/O结束后,进程A抢占B并运行10 ms,如此循环直至结束。这样做允许重叠,一个进程使用CPU,同时等待另一个进程的I/O完成,提高CPU利用率。
在这里插入图片描述

假设5:多级反馈队列(Multi-Level Feedback Queue)

如果需要放松 假设5:作业运行时间已知 ,那么前面的调度策略均不适用,同时需要平衡平均周转时间和平均响应时间,这就需要寻找新的调度策略—— 多级反馈队列(Multi-level Feed- back Queue, MLFQ)

MLFQ: 基本规则
设置一批不同优先级的就绪队列,CPU按照以下规则调度运行

• 规则 1: 若 优先级(A) > 优先级(B), A 运行 (B 不运行)
• 规则 2: 若 优先级(A) = 优先级(B), A 与 B 轮询运行

在这里插入图片描述

多处理机调度

多处理器(multiprocessor)系统逐渐从高端服务器扩散到个人PC、笔记本电脑甚至移动设备上。多处理器极大提高计算机的运算性能,又不增加太多功耗,但同时也带来了一些困难:

  • 典型的应用程序(例如你写的很多C程序)都只使用一个CPU,增加了更多的CPU并没有让这类程序运行得更快。为解决这个问题,需要重写应用程序引入多线程(thread),使之能够并行(parallel)工作,这在后面的章节会详细阐述。
  • 其次,我们已经讨论过单处理机的进程调度,而面对多处理机时时,进程又该如何调度呢? 这正是我们以下需要阐明的。不过,按照惯例依然先说明多处理机调度需要的硬件和机制支持,再阐明具体的进程调度策略。

硬件支持及软件机制

缓存是基于的局部性(locality)的概念,使用缓存能极大提高数据的处理效率,而局部性包括时间局部性和空间局部性。

  • 硬件缓存(hardware cache):介于中央处理器和主存储器之间的高速小容量存储器,保存内存中最常访问的数据备份。相比之下,内存很大且拥有所有的数据,但访问速度较慢。
  • 时间局部性(temporal locality):当一个数据被访问后,它很有可能会在不久的将来被再次访问,比如循环代码中的数据或指令本身。
  • 空间局部性(spatial locality):当程序访问地址为x的数据时,很有可能会紧接着访问x周围的数据,比如遍历数组或指令的顺序执行。
    在这里插入图片描述

多处理机和单处理机的区别在于对硬件缓存(cache)的使用,以及多处理器之间共享数据的方式。

  • 缓存一致性(cache coherence):通过监控内存访问保证多个缓存之间共享数据的一致。每个缓存都通过监听链接所有缓存和内存的总线,如果CPU发现对它放在缓存中的数据更新,则会作废本地副本,并修改为新值。
  • 缓存亲和度(cache affinity):进程在某个CPU上运行时,会在该CPU的缓存中维护许多状态。下次该进程在相同CPU上运行时,由于缓存中的数据而执行得更快。相反,在不同的CPU上执行,由于缓存一致性而不会执行出错,但需要重新加载数据而很慢。
    在这里插入图片描述

同时,软件需要使用互斥原语(比如锁),才能保证共享数据或数据结构的正确性。假设多CPU并发访问一个共享队列,不使用互斥原语,并发地从队列增加或删除元素,缓存一致性所保存的共享数据并非预期结果。

单队列调度((Single Queue Multiprocessor Scheduling,SQMS)

最基本的方式是简单地复用单处理器调度的基本架构,将所有需要调度的工作放入一个单独的队列中,称为单队列多处理器调度(Single Queue Multiprocessor Scheduling,SQMS)。
在这里插入图片描述
SQMS的优点是从单CPU调度程序很简单地发展,缺点则是扩展性不好,而且不能很好地保证缓存亲和度。

  1. 首先,缺乏可扩展性(scalability)。为了保证在多CPU上正常运行,调度程序的开发者需要在每个进程代码中通过加锁(locking)来保证原子性。但随着系统中的CPU数增加时,系统花费了越来越多的时间在锁的开销上。
  2. 其次,SQMS不利于缓存亲和性。比如,假设我们有5个工作(A、B、C、D、E)和4个处理器,4个CPU一次性执行A-D工作一个时间片,然后切换E-C,如此循环执行,可以看出每个工作都不断在不同CPU之间转移,缓存亲和性得不到体现,现象反而与内存抖动类似。
    在这里插入图片描述

多队列调度((Multi-Queue Multiprocessor Scheduling,MQMS)

多队列多处理器调度(Multi-Queue Multiprocessor Scheduling,MQMS)包含多个调度队列,每个队列可以使用不同的调度规则,比如轮转或其他任何可能的算法。
在这里插入图片描述
假设系统中有两个CPU(CPU 0和CPU 1),调度队列Q0和Q1均采用RR策略,调度运行结果如下:
在这里插入图片描述
MQMS相比SQMS有具有可扩展性。队列的数量会随着CPU的增加而增加,因此锁和缓存争用的开销不是大问题。此外,MQMS天生具有良好的缓存亲和度。所有工作都保持在固定的CPU上,因而可以很好地利用缓存数据。

但是,如果作业分配不当,MQMS也有负载不均(load imbalance)的问题。比如,CPU0不分配作业,那么CPU0会一直空闲,而CPU1会一直忙碌。
在这里插入图片描述
解决措施就是采用工作窃取(work stealing)的技术。通过这种方法,工作量较少的源队列不定期地“偷看”其他目标队列是不是比自己的工作多。如果目标队列比源队列更满,就从目标队列“窃取”一个或多个工作,实现负载均衡。

发布了21 篇原创文章 · 获赞 8 · 访问量 1495

猜你喜欢

转载自blog.csdn.net/K_Xin/article/details/104866839