个人小爱好:Operating System: three easy pieces—第6章第3小节问题2#进程间的切换

问题2#:进程间的切换

直接执行的下一个问题是如何实现进程的切换。进程的切换按理说是挺简单的,对吧?不就是决定哪一个进程应该停止,哪一进程应该开始而已,才多大点事情啊?但,事实上这还真的有点棘手:尤其当一个进程在CPU运行的时候,就意味着操作系统(OS)是是没有执行的(OS程序此时是没有使用CPU的)。那么,问题来了,如果操作系统(OS)没有在CPU运行,那么它究竟是如何完成这些事情的?(看上去,理论上是无法做任何事情的)尽管这听起来就像个哲学问题,但显然:当操作系统(OS)不运行在CPU上时,它没有任何方法去做些什么。因此,我们找到了问题的关键。

关键点:我们如何重新取得CPU的控制权,从而进行进程的切换

协作(合作)的方法:等待系统调用

过去的一些系统(如:Mactintosh 系统的早期版本,旧版的Xerox Alto 系统)采用了一种被称作“协作”(cooperative approach )的方法来实现。在这种方式下,操作系统(OS)信任程序的行为是合理的。当进程运行太久时,就会周期性的让出CPU,以便操作系统(OS)能够决定是否运行其他任务。

因此,你也许会问到:在这个乌托邦的世界,进程是如何让出CPU的?。事实上,对于绝大多数进程,它们通过系统调用(system calls)将CPU的控制权频繁地转移到操作系统(OS)上,比如:打开一个文件并进行入读,或发送一段报文到另一台机器,亦或者创建一个新进程。像这样的系统通常都会有一个显示定义的一个除了转移控制从而让出CPU,其余什么都不干的系统调用(yield system call)以便系统能运行其他进程。

应用程序也会在出现非法操作时转移控制权,如:当一个程序除数是0时或者尝试访问未授权或被禁止访问的内存时,就会产生一个操作系统的trap(emmm,才疏学浅,不知如何翻译好,本人理解:如arm内核中的未定义模式,可以理解为中断吧,或者物理学上的陷阱如粒子陷等,反正感觉翻译成陷阱不是很形象),从而操作系统会再次获得CPU控制权(并有可能会终止这一非法进程)。

因此,在一个协作系统同,操作系统(OS)通过等待系统调用(system call)或者程序的非法操作的发生来重新获取CPU的控制权。你也许会想:这是否有点过于被动了呢?若如果一个程序充满了bug或者本身就是恶意的,导致无限的死循环,并且从不执行系统调用,那又会是什么情况?操作系统又能干什么?

非协作方法:操作系统自己掌控

显而易见,在没有增加一些硬件的协助,在上述情况(程序不执行程序调用,也不犯错,以至返还CPU控制权,就耍流氓般占用CPU)时,操作系统什么都做不了。并且,实时上,在这种协助模式下,当进程陷入了死循环状态时,你唯一能采取的办法就只有古老的解决方法:重启大法(reboot the machine)。以上,我们找到了获取CPU控制权问题的子问题。

关键点:操作系统如何在非协作状态下获取CPU得控制权,以确保一个流氓进程不会一直占用机器

事实上答案挺简单得,并且开发操作系统的那一波人在许多年前就发现了:计时器中断(timer interrupt)。计时器可以被编程设定成间隔n微秒触发一次中断;当中断触发时,正在运行的程序被挂起(暂停),一段操作系统预设好的中断处理函数(interrupt handler)会被执行。此时,操作系统会重新获得CPU控制权,并且能执行它任何想做的事情:停止当前进程,开始另外一个进程。

TIP:使用计时器中断取获取控制权的方法中,额外的计时器中断赋予操作系统即使在非协方式的进行下也能重新在CPU控制的能力。因此,这个硬件特性对于操作系统能够维持对机器的控制起到了十分重要的作用。

如我们前文所讨论的系统调用,操作系统必须告知硬件当中断出现的时候执行那一段程序,因此,在启动阶段,操作系统就是在干这件事情。第二点,在启动的一系列操作中,操作系统必须开启计时器,当然,这是一个特权操作。一旦计时器启动后,操作系统就可以安心了,因为无论如何最终权限都会回到自己手上,所以(操作系统)就可以*的执行用户程序了。当让,计时器也是可以被关掉的(理所应当,这也是一项特权操作),并且我们会在我们对并行有更深入理解之后讨论一些相关问题。

注意!当一个中断出现时,硬件也要承担一部分任务,特别是要保存一个正在运行的程序的足够状态信息,以便随后从 trap 中返回(return from trap)时能够正确地恢复运行程序。硬件的这一系列操作与显式执行系统调用进入内核时,各种寄存器因此被存入内核堆栈(kernel stack),然后执行返回指令(return form trap)时能轻易的恢复寄存器的过程十分相似。

上下文(Context)的保存

额外解释:什么是上下文?通俗的将其实就是程序运行的状态而已。如此时的IP,SP,各个寄存器的值,因为CPU只有一个,当程序被切换时候,就要通过保存这些数据以便下次接着执行,这些数据就叫上下文。

现在,操作系统已经可以由系统调用或者强制性用计时器中断重新取得控制权了,那么下一步就是进行决策:是继续运行当前进程呢?还是切换到另一个?。这个决策由操作系统中被称为调度器(scheduler)的部分做出;并且我们将会在接下来的及章节里仔细讨论调度策略。

若决定要进行进程切换时,操作系统会执行一系列底层代码,我们通常将这部分称为上下文切换(context switch).上下文切换在概念上很简单:所有类似的操作系统都要做的是将几个与当前运行进程相关寄存器的值保存下来(比如,保存到它的内核堆栈),然后恢复将要运行的进程的相关寄存器(从内核堆栈)。这样做,去确保了系统在执行return-from trap指令后,返回到另一个要执行的进程,而不是当前的正在运行的进程。

为了将当前进程的上下文保存,操作系统会执行一写底层的汇编代码去保存通用寄存器,PC(指令计算器、程序计数器,和IP(指令指针)是由区别!)以及当前运行进程的内核堆栈指针,接着恢复将要执行进程的上述寄存器,PC,并切换到内核堆栈。通过堆栈的切换,内核在当前进程的上下文中进入切换代码,并在将要执行的进程的上下文中返回。当操作系统最后执行完返回指令(return-from-trap)后,将要执行的进程就变成了正在执行的进程了。到此,上下文的切换也就完成了。

OS @boot (启动阶段)

内核

硬件

程序

初始化trap 表

记录系统调用处理函数,计时器处理函数的地址

开启计时器中断

启动计时器,X ms中断一次

OS@run(运行阶段)

进程A运行中

计时器中断

1、保存A的寄存器到A内核堆栈

2、切换到内核模式

3、跳转到trap处理函数

Trap处理函数

1、调用switch()函数

(1)保存A的寄存器到A的进程结构体

(2)从B的进程结构体恢复B的寄存器

(3)切换B到内核堆栈

2、执行返回指令到B进程

1、从B的内核堆栈恢复B的寄存器

2、切换到用户模式

3、跳转到B的PC所指指令

进程B运行中

表6.3:有限制的直接运行协议(计时器中断)

表6.3是以时间轴为基础的整个过程。在这个例子中,进程A先处于运行状态,然后被计时器中断打断。接着,硬件将相关寄存器的值保存起来(保存到了它的内核堆栈)然后进入到内核(切换到内核模式)。在计时器中断处理函数中,操作系统进行决策,决定切换到进程B,并调用 swtich() 程序,用以保存当前寄存器(到A的进程结构体),恢复进程B的寄存器(从B的进程结构体),然后切换上下文,特别是将堆栈指针切换到B的内核堆栈(不再是A的)。最后,操作系统执行返回指令(return-from-trap),以恢复B的寄存器状态,进程B开始运行。

可以注意到,上面的过程中有两个类型的寄存器被保存和恢复。第一种是当计数器中断出现时:当前运行进程的用户寄存器被硬件隐式(自动地)保存到了对应的内核堆栈。第二种:当操作系统切换到B时,内核寄存器被软件(操作系统)保存,但此时保存到的地方是进程内存种的进程结构体。接着的操作让系统就像从B进入了内核模式然后,而不是从A进入的。(事实上是由A进入的,但是改变了寄存器等,让它认为是由B进入的)。

为了让你对这种切换有一个更改好的感受,图6.1展示了xv6系统的切换代码。(但你要对x86 和xv6有一些了解)。

个人小爱好:Operating System: three easy pieces—第6章第3小节问题2#进程间的切换

上一篇:ASP.Net Core 里是如何把一个普通的 Action 返回类型转换为某种 IActionResult 的


下一篇:Working with Data » Getting started with ASP.NET Core and Entity Framework Core using Visual Studio » 排序、筛选、分页以及分组