前言
OS中CPU虚拟化的实现主要依赖于
- 低级机制 决定进程切换的方法
- 高级策略 决定进程切换的对象
本文就上述低级机制进行具体阐述。
上下文切换(context switch)
OS科学家们为了保证程序运行时高效可控,开发出了一种协议——受限直接执行(limited direct execution)。其中“直接执行”的步骤如下:
① 输入执行命令后,OS首先在其进程列表中为待运行进程创建一个进程条目,为其分配一些内存。
② 将程序代码从磁盘加载到内存中,找到程序运行点(main函数),开始运行代码。运行完毕后return返回值。
③ OS释放进程的内存,删除进程表中此进程条目。
这个过程中存在如下二个问题:
进程在独占CPU时发生高权限行为时该如何处理?(如I/O请求,分配内存请求等)
一个进程的时间片消耗完毕后OS如何重新得到CPU的控制权?
question 1
针对第一个问题,我们能想到的就是能不能给进程加一些限制,比如说给它贴上标签,低权限标签的进程不能执行某些高权限操作,这样便能较好地控制好进程的行为。
实际上计算机也是按这个思路实现的,不过计算机为了保证效果以及后续OS重新得到CPU控制权,采用了OS和硬件协同的方式来处理这一情况。
系统调用
处理器有7种工作模式,可分为用户模式和系统模式(图中下方六种模式都能称为“系统模式”),也可称为用户态和核心态。绝大部分的程序都运行在“用户模式”下,而OS运行在“系统模式”下。因此当进程需要完成一些高权限操作时,它需要使处理器运行在“系统模式”下才能完成。
CPU的工作模式可以简单的理解为当前CPU的工作状态,比如:当前操作系统正在执行用户程序,那么当前CPU工作在用户模式,这时网卡上有数据到达,产生中断信号,CPU自动切换到一般中断模式下处理网卡数据(普通应用程序没有权限直接访问硬件),处理完网卡数据,返回到用户模式下继续执行用户程序。
由上文可知进程需要通过某个命令或者调用才能让处理器运行在“核心态”。这时需要OS发挥作用了。OS通过向进程暴露一些关键性的接口,例如访问文件,创建和销毁进程,与其他进程通信和分配更多内存等来使处理器进入“核心态”。这些接口被称为系统调用。
陷阱
如果处理器运行在“用户”模式下,那么进程不能完全访问硬件资源。需要一个“入口”来负责进程与硬件的沟通。而“系统模式”可以直接访问全部硬件资源,因此进程要从“用户模式”通过一个“入口”进入“系统模式”,这个入口就被称为“陷阱(trap)”。执行系统调用的过程陷入内核态的过程。
当进程在“系统模式”下执行完相应的指令后,OS会调用一个“return-from-trap”的指令从核心态返回到用户态。
我们在程序中写的系统调用都是直接调用一些函数如open(),read()等。这使得系统调用看起来像过程调用。OS怎么知道这是系统调用呢?
原因如下:它其实是一个包含了系统调用的过程调用,调用的是C库中的函数,而C库中函数的具体实现包含了陷入内核的指令(trap)以及一些必须的指令。并且C库中的系统调用的部分是用汇编代码来实现的。这使得我们平时调用的系统调用都是对它们封装。
通过陷阱进入内核之后,此时OS如何知道要运行哪些代码以确保任务完成呢?这时就需要了解中断这一术语了。
中断
中断是指计算机运行过程中,出现某些意外情况需主机干预时,机器能自动停止正在运行的程序并转入处理新情况的程序,处理完毕后又返回原被暂停的程序继续运行。
引起中断发生的事件被称为中断源。中断源向CPU发出的请求中断处理信号称为中断请求,而CPU收到中断请求后转到相应的事件处理程序称为中断响应。
在有些情况下,尽管产生了中断源和发出了中断请求,但CPU内部的处理器状态字PSW的中断允许位已被清除,从而不允许CPU响应中断。这种情况称为禁止中断。CPU禁止中断后只有等到PSW的中断允许位被重新设置后才能接收中断。禁止中断也称为关中断,PSW的中断允许位的设置也被称为开中断。开中断和关中断是为了保证某段程序执行的原子性。
还有一个比较常用的概念是中断屏蔽。中断屏蔽是指在中断请求产生之后,系统有选择地封锁一部分中断而允许另一部分中断仍能得到响应。不过,有些中断请求是不能屏蔽甚至不能禁止的,也就是说,这些中断具有最高优先级,只要这些中断请求一旦提出,CPU必须立即响应。例如,电源掉电事件所引起的中断就是不可禁止和不可屏蔽的。
中断可以分为三类:
- 外部中断
- 内部中断
- 陷阱
第一类是外部中断,如I/O中断、时钟中断、控制台中断等。
第二类是来自CPU的内部事件或程序执行中的事件引起的过程,称作异常,如由于CPU本身故障(电源电压低于1.05V或频率在47~63Hz之外)、程序故障(非法操作码、地址越界、浮点溢出等)等引起的过程。
第三类是进入核心态的陷阱。与前两类不同是,陷阱是主动、有意的,且其发生时间是确定的。而中断是被动,无意的,它的发生时间是不可预测的。
具体区别如下:
陷阱通常由处理器正在执行的现行指令引起,而中断则是由与现行指令无关的中断源引起的。陷阱处理程序提供的服务为当前进程所用,而中断处理程序提供的服务则不是为了当前进程的。中断是由硬件引起的,而异常是由软件引起的;中断是异步的,而异常是同步的。
按照事件发生的顺序,中断过程包括 :
① 中断源发出中断请求;
② 判断当前处理机是否允许中断和该中断源是否被屏蔽;
③ 优先权排队;
④ 处理机执行完当前指令或当前指令无法执行完,则立即停止当前程序,保护断点地址和处理机当前状态,转入相应的中断服务程序;
⑤ 执行中断服务程序;
⑥ 恢复被保护的状态,执行“中断返回”指令回到被中断的程序或转入其他程序。
上述过程中前四项操作是由硬件完成的,后两项是由软件完成的。
陷阱表
现在回到上述问题,转入到内核态后,OS如何知道要执行哪些代码呢?这通过trap table来实现。我们的计算机开机时,OS会初始化一个陷阱表,表中内容是陷阱指令与对应的陷阱处理程序的地址,每当系统调用时,OS就会到陷阱表中查找对应的处理程序地址。接下来CPU就运行处理程序。
question 2
OS如何获取CPU控制权以完成进程切换呢?我们知道进程在运行时是独占CPU的,此时OS没有运行。那么如何保证之后OS会重新取得CPU的控制权呢?
有如下两种方式:
协作方式:等待系统调用
早期一些OS的设计哲学是:相信进程是合理的。它假定进程会以某种系统调用的方式陷入(trap)OS,从而使OS获取控制权。或者进程会执行了某些引发异常的操作,使得OS重获控制权。这种通过异常或系统调用的方式太过于被动,面对恶意进程时,它将束手无策。我们迫切需要一种更强硬的方法来处理类似情况。
非协作方式:时钟中断
如果使用协作方式,当进程陷入无限循环时,唯一的方法就是重启。而在非协作方式中,可以使用时钟中断(timer interrupt)来使OS重新拿到控制权。这需要硬件来实现。在使用时钟中断这种进程切换方式时,需要2次寄存器的保存和恢复。第1次是中断发生时,将寄存器保存到内核栈。第2次是准备切换时将寄存器保存到进程结构的内存中。
进程切换上下文环境
处理器总处于以下状态中的一种:
1、内核态,运行于进程上下文,内核代表进程运行于内核空间;
2、内核态,运行于中断上下文,内核代表硬件运行于内核空间;
3、用户态,运行于用户空间。
用户空间的应用程序,通过系统调用,进入内核空间。这个时候用户空间的进程要传递很多变量、参数的值给内核,内核态运行的时候也要保存用户进程的一些寄存器值、变量等。所谓的“进程上下文”,可以看作是用户进程传递给内核的这些参数以及内核要保存的那一整套的变量和寄存器值和当时的环境等。
硬件通过触发信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的一些变量和参数也要传递给内核,内核通过这些参数进行中断处理。所谓的“中断上下文”,其实也可以看作就是硬件传递过来的这些参数和内核需要保存的一些其他环境(主要是当前被打断执行的进程环境)。
总结
上下文切换:进程在进行切换时,OS会保存进程A的寄存器的值,然后恢复待执行进程B的寄存器,OS将控制权移交给待执行进程B,进程B开始运行。
系统调用是导致进程切换的一种方式。如果系统调用期间又发生了时钟中断,或是处理一个中断期间又发生了另一个中断,这会发生什么呢?我们会在并发章节学习到这些情况的处理办法。