悍魔之战·网游指挥部

虚拟化:CPU虚拟化

虚拟化(virtualization)是操作系统的关键概念之一,具体来说,操作系统通过“虚拟化”,将物理资源(如CPU、内存、磁盘等)转换为更通用、更强大且更易于使用的虚拟形式。本文将讲解CPU虚拟化的具体细节。

什么是CPU虚拟化?将单个CPU转换为看似无限数量的CPU,从而让许多程序看似同时运行,这就是所谓的虚拟化CPU

为什么要虚拟化CPU?一个CPU在某一时刻只能运行一个程序的代码,如果想要同时(看似)运行多道程序,此时就需要OS来提供一种假象:这台机器具有许多CPU。这样一来,就能够让用户同时使用多个程序,从而提升用户体验。

进程要讲解CPU虚拟化,就不得不先介绍一个概念:进程。

所谓的进程,就是正在运行的程序。

简单来说,进程可以处在以下三种状态之一:

运行(running):进程正在处理器上运行,它正在执行指令。就绪(ready):进程已经准备好运行,但由于某些原因,操作系统选择不在此时运行。阻塞(blocked):在此状态下,一个进程执行了某种操作,直到发生其他事件时才会准备运行。例如,当进程向磁盘发起I/O请求时,它会被阻塞。知道了进程的基本概念之后,OS进行CPU虚拟化的方法就显而易见了:通过在不同的进程之间不断地来回切换,使得一定时间内能够有多个进程被运行。这样一来,就营造出了具有多个CPU的假象。

受限直接执行(limited direct execution, LDE)通过时分共享(time sharing)CPU,就实现了虚拟化。但是,现在有几个问题需要解决。

性能:如何能在不增加额外的系统开销的情况下实现虚拟化?控制权:如何在高效地运行程序时,同时让OS保留对CPU的控制权?问题1解决方法是受限直接执行

所谓的直接执行,顾名思义,就是直接在CPU上运行程序。OS希望程序启动时,它会在进程列表中添加该进程,然后为它分配内存,并将程序代码加载到内存中,跳转到main()函数并开始运行程序代码。当main()函数return时,就会回到内核,OS会释放该进程的内存,并从进程列表中删除该进程。

但这样做又会产生一个新的问题:OS该如何确保程序不做任何我们不希望它做的事,同时仍然高效地运行它?

我们采用的方法是,让硬件提供不同的执行模式来协助OS。

内核模式(kernel mode):操作系统可以访问机器的全部资源。用户模式(user mode):应用程序不能完全访问硬件资源。在用户模式下运行的代码会受到限制,例如在用户模式下运行时,进程不能发出I/O请求。这样做会导致处理器引发异常,操作系统可能会终止进程。

与此同时,硬件还提供了用户程序执行系统调用的能力。系统调用允许内核小心地向用户程序暴露某些关键功能,例如访问文件系统、创建和销毁进程、与其他进程通信,以及分配更多内存,以此来满足用户希望执行某种特权操作(如从磁盘中读取)的需要。

要执行系统调用,程序必须执行特殊的陷阱(trap)指令,该指令跳入内核并将特权级别提升到内核模式,此时就可以执行需要的特权操作。完成时,OS调用从陷阱返回(return-from-trap)指令,该指令返回到发起调用的用户程序中,并将特权级别降低,回到用户模式。要注意的是,执行陷阱指令时,硬件必须保存足够多的调用者的寄存器(注意,这里的概念不是“调用者保存寄存器”,Caller-saved register,而是调用者 的 寄存器),以便在OS发出从陷阱返回指令时能够正确返回。

此处还有一个概念,陷阱表(trap table),作用是让硬件知道在发生系统调用或者其他异常事件时需要做什么(即跳转到哪段代码)。

问题2如果一个程序在CPU上运行,这就意味着OS没有运行。如果OS没有运行,那它该如何才能够获得CPU的控制权,使它可以不断地切换进程?

这里有两种方式。

协作方式:等待系统调用

OS会假定进程是可以信任的,让它自由的运行,直到它发起了系统调用或者出现异常,此时就会陷入(trap)操作系统,于是OS就重新取得了控制权。

但弊端是,如果一个进程陷入无限循环并且从不发起系统调用,那OS将无能为力。

非协作方式:操作系统进行控制

通过时钟中断(timer interrupt),操作系统可以在进程不协作的情况下接管控制权。

时钟设备每隔一段设定时间就会产生一次中断,当前正在运行的进程停止,OS中的中断处理程序(interrupt handler)会运行,此时OS就重新获得了控制权,并可以决定继续运行当前进程还是切换另一个进程。、

就像系统调用一样,OS必须让硬件知道发生中断时该跳转到哪段代码。中断发生时,硬件也必须为当前进程保存足够多的状态,以便能够正确恢复。

在OS获得控制权后,必须决定该运行哪一个进程,此时就需要调度程序(scheduler)来做出决定。如果决定切换进程,OS就会执行上下文切换(context switch):为当前正在执行的进程保存一些寄存器的值(到它的内核栈),并为即将执行的进程恢复一些寄存器的值(从它的内核栈)。

这里的内核栈是操作系统内核为每个进程或线程分配的一种数据结构,用于在执行内核代码时存储函数调用的相关信息、局部变量以及中断处理等上下文数据。

调度策略简单介绍四种调度策略。

先进先出(FIFO):先到的任务先工作。

最短任务优先(SJF):同时到达的任务,先完成小任务,再完成大任务。不过它是非抢占性的,这意味着必须先完成当前的任务才能切换到下一个任务。

最短完成时间优先(STCF):每当有新工作加入时,确定剩余工作和新工作中,谁的剩余时间最少,然后调度该工作。

轮转(Round-Robin,RR):在一个时间片(time slice)内运行一个工作,然后切换到运行队列中的下一个任务,而不是运行一个任务直到结束。它反复执行直到所有任务完成。时间片长度必须是时钟中断周期的整数倍。

在进程调度中,有两个常用调度指标:

周转时间 = 完成时间 - 到达时间响应时间 = 首次运行时间 - 到达时间SJF和STCF优化周转时间,RR则优化响应时间。

上面的四种策略都是建立在一个假设上:每个任务的运行时间都已知。但在实际情况中,OS无法提前知道每个任务会运行多久。所以,在不知道任务时间的基础上,如何能够优化周转时间和响应时间?

多级反馈队列(Multi-level Feedback Queue, MLFQ)就是这样的一个调度程序.

MLFQ中有许多独立的队列,每个队列有不同的优先级。任何时刻,一个任务只能存在于一个队列中。MLFQ总是先执行较高优先级的工作。当然,每个队列可能同时存在多个工作,因此这些工作具有同样的优先级。在这种情况下,我们就对这些工作采用轮转调度(RR)。

MLFQ有许多不同的实现方法,这里介绍的一种实现方法的规则是:

如果A的优先级 > B的优先级,运行A如果A的优先级 = B的优先级,轮转运行A和B工作进入系统时,放在最高优先级。一旦工作用完了其在某一层中的时间配额,就降低其优先级。经过一段时间S,就将系统中所有工作重新加入最高优先级队列。还有一种不同类型的调度程序——比例份额(proportional-share)调度程序,其中有一个有趣的例子:彩票调度(lottery scheduling)。基本思想是:每隔一段时间,都会举行一次彩票抽奖,以确定接下来应该运行哪个进程。越是应该频繁运行的进程,越是应该拥有更多地赢得彩票的机会。这里不再详细介绍细节。

总结本文简要介绍了OS如何虚拟化CPU,介绍了关键概念——进程,以及进程切换的机制和调度策略。还介绍了许多重要机制:时钟中断、陷阱和从陷阱返回、上下文切换。

同时不难发现,操作系统在让程序高效运行的同时,还能够有效应对程序的异常行为,既确保了效率,又提高了机器的安全性。

Copyright © 2022 悍魔之战·网游指挥部 All Rights Reserved.