-
进程是什么,进程如何创建
-
虚拟化如何实现——时分共享
-
如何实现时分共享——上下文切换
-
如何在不增加系统开销的情况下实现虚拟化?
-
如何有效地运行进程,同时保留对CPU的控制?
第三章:关于虚拟化的对话
每个进程都需要使用CPU。
有限的CPU和多个进程的需求造就了虚拟化的需求
以最基本的计算机资源 CPU 为例,假设一个计算机只有一个 CPU(尽管现代计算机一般拥有 2 个、4 个或者更多 CPU),虚拟化要做的就是将这个 CPU 虚拟成多个虚拟 CPU 并分给每一个进程使用,因此,每个应用都以为自己在独占 CPU,但实际上只有一个 CPU。这样操作系统就创造了美丽的假象——它虚拟化了 CPU。
第4章抽象:进程
虚拟化实现采用时分共享的方式。
将对CPU的使用权抽象为多个时间片
这让操作系统能够停止运行一个程序,并开始运行另一个程序
提示:使用时分共享(和空分共享)
时分共享(time sharing)是操作系统共享资源所使用的最基本的技术之一。通过允许资源由一个实体使用一小段时间,然后由另一个实体使用一小段时间,如此下去,所谓的资源(例如,CPU 或网络链接)可以被许多人共享。时分共享的自然对应技术是空分共享,资源在空间上被划分给希望使用它的人。
例如,磁盘空间自然是一个空分共享资源,因为一旦将块分配给文件,在用户删除文件之前,不可能将它分配给其他文件。
时分共享的实现带来了两个问题。
- 我们如何实现上下文切换,也就是当前执行的结果要得到保留
- 如何调度程序
前者是机制(功能),后者是策略(算法)
提示:分离策略和机制
在许多操作系统中,一个通用的设计范式是将高级策略与其低级机制分开[L+75]。你可以将机制看
成为系统的“如何(how)”问题提供答案。例如,操作系统如何执行上下文切换?策略为“哪个(which)”
问题提供答案。例如,操作系统现在应该运行哪个进程?将两者分开可以轻松地改变策略,而不必重新
考虑机制,因此这是一种模块化(modularity)的形式,一种通用的软件设计原则。
4.1 进程是什么
本质上来看,进程就是程序,是一系列指令的集合。
但是进程是由什么构成的呢?
-
内存
- 指令存在内存中。正在运行的程序读取和写入的数据也在内存中。因此进程可以访问的内存(称为地址空间,address space)是该进程的一部分。
-
寄存器
- 许多指令明确地读取或更新寄存器
4.2进程API
-
创建(create):操作系统必须包含一些创建新进程的方法。在 shell 中键入命令或双击应用程序图标时,会调用操作系统来创建新进程,运行指定的程序。
-
销毁(destroy):由于存在创建进程的接口,因此系统还提供了一个强制销毁进程的接口。当然,很多进程会在运行完成后自行退出。但是,如果它们不退出,用户可能希望终止它们,因此停止失控进程的接口非常有用。
-
等待(wait):有时等待进程停止运行是有用的,因此经常提供某种等待接口。
-
其他控制(miscellaneous control):除了杀死或等待进程外,有时还可能有其他控制。例如,大多数操作系统提供某种方法来暂停进程(停止运行一段时间),然后恢复(继续运行)。
-
状态(statu):通常也有一些接口可以获得有关进程的状态信息,例如运行了多长时间,或者处于什么状态。
4.3进程创建:更多细节
进程的创建
- 将代码和静态数据从硬盘加载到内存中
- 创建和初始化栈(方便上下文切换)
- I/O设置(在unix中,默认情况下每个进程都有 3 个打开的文件描述符(file descriptor),用于标准输入、输出和错误。)
4.4 进程状态
-
运行(running):在运行状态下,进程正在处理器上运行。这意味着它正在执行指令。
-
就绪(ready):在就绪状态下,进程已准备好运行,但由于某种原因,操作系统选择不在此时运行。
-
阻塞(blocked):在阻塞状态下,一个进程执行了某种操作,直到发生其他事件时才会准备运行。一个常见的例子是,当进程向磁盘发起 I/O 请求时,它会被阻塞,因此其他进程可以使用处理器。
在阻塞期间,也会将CPU的使用权转让给其他进程,这样才能提高资源利用率
同时在阻塞结束后,不会立马将执行权限交割该进程。
4.5数据结构
为了跟踪进程的状态,需要设计数据结构来实现
而之所以要跟踪状态,则是因为需要实现上下文切换
对于停止的进程,寄存器上下文将保存其寄存器的内容。
当一个进程停止时,它的寄存器将被保存到这个内存位置。
通过恢复这些寄存器(将它们的值放回实际的物理寄存器中),操作系统可以恢复运行该进程。
补充:数据结构——进程列表
操作系统充满了我们将在这些讲义中讨论的各种重要数据结构(data structure)。进程列表(process list)是第一个这样的结构。这是比较简单的一种,但是,任何能够同时运行多个程序的操作系统当然都会有类似这种结构的东西,以便跟踪系统中正在运行的所有程序。有时候人们会将存储关于进程的信息的个体结构称为进程控制块(Process Control Block,PCB),这是谈论包含每个进程信息的 C 结构的一种方式。
第5章插叙:进程API
关键问题:如何创建并控制进程
操作系统应该提供怎样的进程来创建及控制接口?如何设计这些接口才能既方便又实用?
5.1 fork()系统调用
- 会产生一个和父进程一样的进程
- 父进程会返回子进程的PID,子进程会返回0
- 产生进程后,并不会直接执行子进程,而是会返回fork,就好像是子进程自己调用了fork一样
5.2 Wait()系统调用
父进程调用 wait(),延迟自己的执行,直到子进程执行完毕。当子进程结束,wait()才返回父进程。
- 如果当前执行的正好是子进程,继续执行
- 如果当前执行的是父进程,就会将控制权转让给子进程
5.3 exec()系统调用
给定可执行程序的名称(如 wc)及需要的参数(如 p3.c)后,exec()会从可执行程序中加载代码和静态数据,并用它覆写自己的代码段(以及静态数据),堆、栈及其他内存空间也会被重新初始化。
然后操作系统就执行该程序,将参数通过 argv 传递给该进程。因此,它并谁有创建新进程,而是直接将当前运行的程序(以前的 p3)替换为我同的运行程序(wc)。子进程执行 exec()之后,几乎就像p3.c 从未运行过一样。对 exec()的成功调用永远不会返回。
之所以将Create_task拆分为fork和exec两个部分,是为了在二者状态之间做一些事情
第6章 机制:受限直接执行
在构建这样的虚拟化机制时存在一些挑战。
第一个是性能:如何在不增加系统开销的情况下实现虚拟化?
第二个是控制权:如何有效地运行进程,同时保留对CPU的控制?
控制权对于操作系统尤为重要,因为操作系统负责资源管理。
如果没有控制权,一个进程可以简单地无限制运行并接管机器,或访问没有权限的信息。
因此,在保持控制权的同时获得高性能,这是构建操作系统的主要挑战之一。
关键问题:如何高效、可控地虚拟化CPU
6.1 基本技巧:受限直接执行
为了使程序尽可能快地运行,操作系统开发人员想出了一种技术——我们称之为受限的直接执行(limited direct execution)。这个概念的“直接执行”部分很简单:只需直接在CPU上运行程序即可(也就是将操作系统这一隔离层去掉)。因此,
当OS希望启动程序运行时,它会在进程列表中为其创建一个进程条目,为其分配一些内存,将程序代码(从磁盘)加载到内存中,找到入口点(main()函数或类似的),跳转到那里,并开始运行用户的代码。

ok,通过直接运行一个程序的思路,我们实现了进程的运行,但是我们需要考虑两个问题
- 在前面我们一直提到的,如何实现上下文切换
- 如何实现安全隔离,不让进程做错误的事情
6.2 问题1:受限制的操作
对于IO操作等重要的操作,如果不受限制。
一些错误的进程可能会损坏底层的硬件。
关键问题:如何执行受限制的操作一个进程必须能够执行I/O和其他一些受限制的操作,但又不能让进程完全控制系统。操作系统和硬件如何协作实现这一点?
我们的解决办法是权力转移
提示:采用受保护的控制权转移硬件通过提供不同的执行模式来协助操作系统。在用户模式(user mode)下,应用程序不能完全访问硬件资源。在内核模式(kernel mode)下,操作系统可以访问机器的全部资源。还提供了陷入(trap)内核和从陷阱返回(return-from-trap)到用户模式程序的特别说明,以及一些指令,让操作系统告诉硬件陷阱表(trap table)在内存中的位置。
因为我们需要取得一个平衡,即相对的自由和相对的控制
这种一般方法也在现实生活中采用。例如,那些有孩子或至少听说过孩子的人可能会熟悉宝宝防护(baby proofing)房间的概念——锁好包含危险物品的柜子,并掩盖电源插座。当这些都准备妥当时,你可以让宝宝自由行动,确保房间最危险的方面受到限制。
但是,我们如何实现从用户空间到内核空间的操作呢,也就是进程如何执行系统调用呢
通过暴露一些API实现
要执行系统调用,程序必须执行特殊的陷阱(trap)指令。该指令同时跳入内核并将特权级别提升到内核模式。一旦进入内核,系统就可以执行任何需要的特权操作(如果允许),从而为调用进程执行所需的工作。完成后,操作系统调用一个特殊的从陷阱返回(return-from-trap)指令,如你期望的那样,该指令返回到发起调用的用户程序中,同时将特权级别降低,回到用户模式。

陷阱trap是如何知道自己要看什么的
一般来说trap有三种情况
- exception:也就是异常,一般会panic,kill掉进程
- syscall:会有一个系统调用数组来实现这一操作
- 设备中断
内核通过在启动时设置陷阱表(trap table)来实现。当机器启动时,它在特权(内核)模式下执行,因此可以根据需要自由配置机器硬件。操作系统做的第一件事,就是告诉硬件在发生某些异常事件时要运行哪些代码。例如,当发生硬盘中断,发生键盘中断或程序进行系统调用时,应该运行哪些代码?操作系统通常通过某种特殊的指令,通知硬件这些陷阱处理程序的位置。一旦硬件被通知,它就会记住这些处理程序的位置,直到下一次重新启动机器,并且硬件知道在发生系统调用和其他异常事件时要做什么(即跳转到哪段代码)。

LDE协议有两个阶段。第一个阶段(在系统引导时),内核初始化陷阱表,并且CPU记住它的位置以供随后使用。内核通过特权指令来执行此操作(所有特权指令均以粗体突出显示)。第二个阶段(运行进程时),在使用从陷阱返回指令开始执行进程之前,内核设置了一些内容(例如,在进程列表中分配一个节点,分配内存)。这会将CPU切换到用户模式并开始运行该进程。当进程希望发出系统调用时,它会重新陷入操作系统,然后再次通过从陷阱返回,将控制权还给进程。该进程然后完成它的工作,并从main()返回。这通常会返回到一些存根代码,它将正确退出该程序(例如,通过调用exit()系统调用,这将陷入OS中)。此时,OS清理干净,任务完成了。
6.3 问题2:在进程之间切换
为什么说进程切换是一个问题。
- 如何实现上下文切换
- 如果是在CPU上直接运行一个进程,也就是此时没有运行操作系统。如何调度进程
- 在每个进程看来,整个CPU上都只由自己一个进程在运行。就算知道有其他进程存在,也会偏向于运行自己,而不是交出权限
关键问题:如何重获CPU的控制权操作系统如何重新获得CPU的控制权(regain control),以便它可以在进程之间切换?
协作方式:等待系统调用
过去某些系统采用的一种方式(例如,早期版本的Macintosh操作系统[M11]或旧的Xerox Alto系统[A79])称为协作(cooperative)方式。在这种风格下,操作系统相信系统的进程会合理运行。运行时间过长的进程被假定会定期放弃CPU,以便操作系统可以决定运行其他任务。
或许我们应该先知道,权力的交接是如何实现的。
在这个虚拟的世界中,一个友好的进程如何放弃CPU?事实证明,大多数进程通过进行系统调用,将CPU的控制权转移给操作系统,例如打开文件并随后读取文件,或者向另一台机器发送消息或创建新进程。像这样的系统通常包括一个显式的yield系统调用,它什么都不干,只是将控制权交给操作系统,以便系统可以运行其他进程。
但问题是,可能会存在恶意程序,比如,故意触发异常操作
非协作方式:操作系统进行控制
事实证明,没有硬件的额外帮助,如果进程拒绝进行系统调用(也不出错),从而将控制权交还给操作系统,那么操作系统无法做任何事情。
事实上,在协作方式中,当进程陷入无限循环时,唯一的办法就是使用古老的解决方案来解决计算机系统中的所有问题——重新启动计算机。
因此,我们又遇到了请求获得CPU控制权的一个子问题。
关键问题:如何在没有协作的情况下获得控制权即使进程不协作,操作系统如何获得CPU的控制权?操作系统可以做什么来确保流氓进程不会占用机器?
嘿,正好我们存在一种硬件方式可以交出控制权。
也就是通过时钟中断来实现
时钟中断(timer interrupt)[M+63]。时钟设备可以编程为每隔几毫秒产生一次中断。产生中断时,当前正在运行的进程停止,操作系统中预先配置的中断处理程序(interrupt handler)会运行。此时,操作系统重新获得CPU的控制权,因此可以做它想做的事:停止当前进程,并启动另一个进程。提示:利用时钟中断重新获得控制权即使进程以非协作的方式运行,添加时钟中断(timer interrupt)也让操作系统能够在CPU上重新运行。因此,该硬件功能对于帮助操作系统维持机器的控制权至关重要。
保存和恢复上下文
上下文切换在概念上很简单:操作系统要做的就是为当前正在执行的进程保存一些寄存器的值(例如,到它的内核栈),并为即将执行的进程恢复一些寄存器的值(从它的内核栈)。这样一来,操作系统就可以确保最后执行从陷阱返回指令时,不是返回到之前运行的进程,而是继续执行另一个进程。
