6.Mechanism: Limited Direct Execution
- 受限制的直接执行。
- 虚拟化CPU的基本思想:让一个线程上CPU运行一会,下来再让另一个上CPU运行,通过time sharing的方法来实现。
- 两个问题:
- 第一是性能:实现虚拟化时如何能让系统不用额外的开销?
- 第二是控制:如何在高效运行线程时OS可以重新获得对CPU的控制?
6.1 Limited Direct Execution
- 为了解决上述两个问题可采用Limited Direct Execution的方法。
- 先看什么是Direct Execution:单纯地让程序直接运行在CPU上。按照第四章的说法,OS需要在process list中创建进程入口,分配内存,加载代码到内存中,定位程序入口(main函数之类的),然后开始运行,流程如如下:
- 这种方法的问题来了:
- 第一,如果像这样仅仅运行程序,OS怎么能够确保程序不做一些OS不想让程序做的事?
- 第二,在运行程序时,OS如何停止当前进程,让另外一个进程上CPU运行,从而实现time sharing来虚拟化CPU呢?
- 所以我们应该再谈谈”limited”。
6.2 Problem #1: Restricted Operations
Direct Execution是很快没错,但是有个问题:如果进程想执行一些受限制的操作该怎么办?例如提出I/O请求来读写磁盘或者进程想使用更多的系统资源如CPU或内存?也就是说如何能够让进程去执行这些操作,但是又不把整个系统的控制权交给该进程?
先看看一种可行但是不安全实际也不会使用的方法:当进程运行时,让该进程可以执行它想执行的相关操作。明白人都知道,这种做法很不安全。因为这样该进程就可以读写整个磁盘,还谈何保护呢?
- 所以我们来看一种实际可行的方法。将处理器也就是CPU分成两种模式,user mode和kernel mode。
- user mode:运行在user mode的代码的操作是受限的。比如在user mode的时候,进程是不能发起I/O请求的,如果它这么做了,就会导致异常,OS就会杀死该进程。
- kernel mode:OS运行在kernel mode下,在这个模式下的代码可以执行他们想执行的任何操作,包括一些特权操作,比如发起I/O请求操作等。
- 问题又来了,在这种设定模式下,用户的线程想执行特权操作的时候,比如读写磁盘,那应该怎么办呢?
- 解决方法:实际上现代硬件,都给用户的程序提供了执行system call的能力。
- system call实际上给user mode的进程提供了一种方法,用于访问只有在Kernel mode才能访问的资源。
- 执行system call,程序必须执行一条叫trap的指令。这条指令用于转换到Kernel mode,这样系统就可以执行该程序想做的特权操作了。完成之后,OS调用一条叫return-from-trap的指令,返回到该程序,并且返回user mode。
- 执行trap的时候,硬件要做一些事情来确保保存好进程的寄存器,这样当OS调用return-from-trap的时候,才能恢复该进程的信息。例如在X86中,处理器会将程序的寄存器、flags、PC保存到每个程序的kernel stack中,当return-from-trap指令返回时,再从栈中pop出保存的值用于回复进程信息。其他系统做法不同,但核心思想相同。
- 现在我们解决了和权限有关的问题,但是新的风暴已经出现,那就是trap指令如何知道,在换到kernel mode的时候OS要运行什么代码呢?明白人又知道了,那肯定不能由发起trap指令的进程说了算,因为这不安全。
- 解决办法就是trap table。trap table在启动时就设置好了。机器在启动时,在kernel mode下设置,因此可以根据硬件需要来配置机器。trap table里设置了当一些trap指令被调用时,应该运行什么代码来处理。比如当键盘输入或者有磁盘终端时应该做些什么。那么OS就让硬件记住了这些trap handlers的位置,硬件会记住这些trap handlers的位置直到下次重启。这就回答了上一个问题,当系统调用和其他的异常时间发生时硬件应该去执行什么代码。
- 系统调用千千万,如何直到当前进程想发起哪个系统调用呢?答案是system-call number,用于指定系统调用。
- trap table 有了,硬件也可以去记住他,但是怎么告诉硬件trap table在哪里呢?不能说让用户来告诉硬件吧?所以明白人又懂了,这也是一个特权操作。
- 图6.2表明了整个工作流程。在Limited direct execution(LDE)中有两个阶段。
- 第一阶段,kernel初始化trap table,CPU记住它的位置,以便后续使用 。
- 第二阶段,kernel做些设置以便准备让程序上CPU运行,然后执行return-from-trap,转换到user mode,程序开始运行。如果程序想执行特权操作,那么就要发起system call,然后执行trap指令,保存当前进程状态,然后把CPU还给OS,进入kernel mode,执行特权操作。然后就是返回,恢复进程,进程正常结束,OS释放内存。
6.3 Problem #2: Switching Between Processes
- 我们已经直到了如何保护整个系统,程序在CPU上运行的流程应该时怎么样的,现在就该考虑下另一个问题了。我们想要虚拟化CPU,那就要在不同进程之间切换上CPU,那么如何做呢?
- 想想好像很简单,先让一个下去再让另一个上来呗。当一个进程在CPU上运行时,OS是没有在CPU上运行的。但是要让一个进程上CPU又必须是由OS指定的,但是OS不在运行,这怎么办呢?
- 所以要解决的问题就是,要想让别的进程上CPU就要让OS重获对CPU的控制。
6.3.1 A Cooperative Approach: Wait For System Calls
- 要想解决上述问题,一种过去的OS采用的做法是cooperative的方法。这种方法下,OS是信任进程的,OS相信进程可以规范他们的行为。也就是说运行太长的进程会定期地放弃CPU,通过系统调用;或者当程序出错的时候,会执行trap指令,这样CPU就重获了对CPU的控制。但是如果程序死循环呢?如果程序永远也不愿意下来呢?
6.3.2 A Non-Cooperative Approach: The OS Takes Control
- 靠进程自觉不行,那就强制要求呗。如果不强制要求,进程陷入死循环永远不下CPU你就只能重启机器了。
- 这里我们就采用timer interrupt的方法。也就是说一个进程没运行一段时间就必须中断,这样它就下CPU了。当该中断发起时,就代表你该下了,另外一个进程该上了。
- 也就是说像之前讨论的那样,机器启动时,OS就告诉了硬件当timer interrupt的时候应该执行什么代码。在启动时,OS就起一个timer,这样OS才能重获对CPU的控制。
- 当timer interrupt的时候,应该保存当前进程的信息,比如寄存器信息等,这样下次该进程才能被恢复重新上CPU。
6.3.3 Saving and Restoring Context
- 当timer interrupt时,通过scheduler(会在之后讲)来决定是当前进程继续上还是换一个上。如果是换一个进程上的话,那么也就是执行context switch操作,这是由低层的代码实现的。
- context swich的本质就是保存信息并且恢复信息,即保存当前进程的一些信息,恢复即将上CPU的另一个进程的信息。首先将当前进程的寄存器信息保存到进程自己的kernel stack中,然后再恢复下一个进程的kernel stack信息。
- 当然OS还要保存下别的信息,保存当前进程的通用寄存器、PC以及该进程的kernel stack指针,然后恢复下一个进程的这些信息。
- 流程如下:
6.4 有关并发
- 聪明的你可能会问了,如果在处理中断的时候另一个中断发生了怎么办?其实这是之后在并发章节会讨论的事情,现在只需要直到当处理中断的时候,是不会允许其他中断的。
6.5 总结
- 这章的重点就是,第一程序如何运行,第二进程怎么切换。
- 我们通过LED来实现虚拟换CPU,其实就是让一个程序上CPU,但是留一手,通过timer让它不能一直运行。
- 那么如何选择下一个上CPU的进程?这是之后的章节讨论的问题了。