进程与线程:进程

从本章开始,我们将深入考察操作系统 是如何设计和构造的。操作系统中最核心的概念是进程 :这是对正在运行程序的一个抽象。操作系统的其他所有内容都是围绕着进程的概念展开的,所以,让操作 系统的设计者(及学生)尽快井透彻地理解进程是非常重要的 。
进程是操作系统提供的最古老的也是最重要的抽象概念之一 。即使可以使用的CPU只有一个 , 但它们也具有支持( 伪)并发操作的能力 ,它们将一个单独的CPU变换成多个虚拟的CPU 。没有进程的抽象,现代计算将不复存在 。

2.1 进程

所有现代的计算机经常会在同一时间做许多件事 。习惯于在个人计算机上工作的人们也许不会十分注意这个事实 ,因此列举一些例子可以更清楚地说明这一问题。先考虑一个网络服务器,一些网页请求 从各处进入 。当一个请求进入时 ,服务器检查其需要的网页是否在缓存中 。如果是,则把网页发送回 去,如果不是,则启动一个磁盘请求 以获取网页。然而,从CPU的角度来看,磁盘请求需要漫长的时间。 当等待磁盘请求完成时,其他更多的请求将会进入 。如果有多个磁盘存在,可以在满足第一个请求之前 就接二连三地对其他的磁盘发出部分或全部请求。很明显,需要一些方怯去模拟井控制这种并发 。进程(特别是线程) 在这里就可 以发挥作用。
现在考虑只有一个用户的PC 。一般用户不知道,当启动系统肘 ,会秘密启动许多进程 。例如,启 动一个进程用来等待进入的 电子邮件;或者启动另一个防病毒进程周期性地检查是否有病毒库更新 。另外,某个用户进程可能会在所有用户上网的时候打印文件以及刻录CD-ROM 。这些活动都需要管理,于是一个支持多进程的多道程序系统在这里就显得很有用了。
在任何多道程序设计系统中 ,CPU由一个进程快速切换至另一个进程 ,使每个进程各运行几十或几百毫秒。严格地说,在某一个瞬间 ,CPU只能运行一个进程 。但在1秒钟内,它可能运行多个进程 ,这样就产生并行的错觉。有时人们所说的伪并行就是指这种情形 ,以此来区分多处理器系统(该系统有两 个或多个CPU共享同一个物理内存)的真正硬件并行 。人们很难对多个并行活动进行跟踪 ,因此,经过多年的努力,操作系统的设计者开发了用于描述并行的一种概念模型 (顺序进程),使得并行更容易处理。

2.1.1 进程模型(The Process Model)

在进程模型中 ,计算机上所有可运行的软件,通常也包括操作系统 ,被组织成若干顺序进程 ( sequential process ),简称进程(process ) 。一个进程就是一个正在执行程序的实例,包括程序计数器、寄存器和变量的当前值 。从概念上说,每个进程拥有它自己的虚拟CPU。当然,实际上真正的CPU在各进程之间来回切换 。但为了理解这种系统 ,考虑在(伪)并行情况下运行的进程集 ,要比试图跟踪CPU 如何在程序间来回切换简单得多 。正如在第1章所看到的 ,这种快速的切换称作多道程序设计 。
在图2-1(a)中可以看到 ,在一台多道程序计算机的内存中有4道程序。在图2-1(b)中,这4道程序被抽象为4个各自拥有自己控制流程(即每个程序自己的逻辑程序计数器)的进程,并且每个程序都独立地运行。当然,实际上只有一个物理程序计数器 ,所以在每个程序运行时,它的逻辑程序计数器被装入实际的程序计数器中。当该程序执行结束(或暂停执行)时,物理程序计数器被保存在 内存中该进程的逻辑 程序计数器中 。在图2-1©中可以看到,在观察足够长的一段时间后,所有的进程都运行了,但在任何一 个给定的瞬间仅有一个进程真正在运行。
进程与线程:进程
在本章 ,我们假设只有一个CPU。当然,逐渐这个假设就不为真了 ,因为新的芯片经常是多核的, 包含2个 、4个或更多的CPU 。第8章将会介绍多核芯片 以及多处理器 ,但是现在,一次只考虑一个CPU 会更简单一些。因此,当我们说一个CPU 只能真正一次运行一个进程的时候 ,即使有2个核(或CPU ) , 每一个核也只能一次运行一个进程。

由于CPU在各进程之间来回快速切换 ,所以每个进程执行其运算的速度是不确定的。而且当同一进程再次运行时,其运算速度通常也不可再现。所以,在对进程编程时决不能对时序做任何想当然的假设。 例如,考虑一个I/O进程 ,它用流式磁带机恢复备份的 文件 ,它执行一个10 000次的空循环以等待磁带机达到正常速度 ,然后发出命令读取第一个记录。如果CPU决定在空循环期间切换到其他进程 ,则磁带机进程可能在第一条记录通过磁头之后 还未被再次运行 。当一个进程具有此类严格的实时要求时 ,也就是 一些特定事件一定要在所指定的若干毫秒内发生 ,那么必须采取特殊措施以保证它们一定在这段时间中 发生。然而,通常大多数进程并不受CPU 多道程序设计或其他进程相对速度的影响。

进程和程序间的区别是很微妙的,但非常重要。用一个比喻可以更容易理解这一点。想象一位有一 手好厨艺的计算机科学家正在为他的女儿烘制生日蛋糕 。他有做生日蛋糕的食谱,厨房里有所需的原料: 面粉 、鸡蛋 、糖、香草汁等 。在这个比喻中 ,做蛋糕的食谱就是程序(即用适当形式描述的算法),计 算机科学家就是处理器( CPU ) ,而做蛋糕的各种原料就是输入数据。进程就是厨师阅读食谱、取来各种原料以及烘制蛋糕等一系列动作的总和。

现在假设计算机科学家的儿子哭着跑了进来 ,说他的头被一只蜜蜂蛰了。计算机科学家就记录下他照着食谱做到哪儿了 (保存进程的当前状态),然后拿出一本急救手册 ,按照其中的指示处理蛰伤。这里,处理 机从一个进程 (做蛋糕) 切换到另一个高优先级的进程 (实施医疗救治) ,每个进程拥有各自的程序 (食谱 和急救手册)。当蜜蜂蛰伤处理完之后,这位计算机科学家又回来做蛋糕,从他离开时的那一步继续做下去。
这里的关键思想是:一个进程是某种类型的一个活动 ,它有程序、输入、输出以及状态。单个处理器 可以被若干进程共享,它使用某种调度算法决定何时停止一个进程的工作,井转而为另一个进程提供服务 。

值得注意的是,如果一个程序运行了两遍 ,则算作两个进程 。例如,人们可能经常两次启动同一个 字处理软件 ,或在有两个可用的打印机的情况下同时打印两个文件 。像“两个进程恰好运行同一个程序” 这样的事实其实无关紧要 ,因为它们是不同的进程。操作系统能够使它们共享代码 ,因此只有一个副本 放在内存中 ,但那只是一个技术性的细节,不会改变有两个进程正在运行的概念。

2.1.2 进程的创建(Process Creation)

操作系统需要有一种方式来创建进程 一些非常简单的系统,即那种只为运行一个应用程序设计的 系统 (例如 ,微波炉中的控制器) ,可能在系统启动之时 ,以后所需要的所有进程都已 存在。然而 ,在 通用系统中 ,需要有某种方法在运行时按需要创建或撤销进程 ,现在开始考察这个问题。

4种主要事件会导致进程的创建 :
(1) 系统初始化 。
(2) 正在运行的程序执行了创建进程的系统调用 。
(3) 用户请求创建一个新进程 。
(4) 一个批处理作业的初始化 。

启动操作系统时,通常会创建若干个进程 。其中有些是前台进程,也就是同用户 (人类) 交互并且 替他们完成工作的那些进程 。其他的是后台进程 ,这些进程与特定的用户没有关系 ,相反 ,却具有某些 专门的功能。例如,设计一个后台进程来接收发来的电子邮件,这个进程在一天的大部分时间都在睡眠, 但是当电子邮件到达时就突然被唤醒了。也可以设计另一个后台进程来接收对该机器中 Web页面的访问 请求 ,在请求到达时唤醒该进程以 便服务该请求。停留在后台处理诸如电子邮件、Web页面、新闻、打印之类活动的进程称为守护进程( daemon )。在大型系统中通常有很多守护进程。在UNIX8 中,可以用 ps 程序列出正在运行的进程 ,在Windows 中,可使用任务管理器。

除了在启动阶段创建进程之外 ,新的进程也可以以后创建 。一个正在运行的进程经常发出系统调用 , 以便创建一个或多个新进程协助其工作 。在所要从事的工作可以容易地划分成若干相关的但没有相互作 用的进程时 ,创建新的进程就特别有效果 。例如 ,如果有大量的数据要通过网络调取并进行顺序处理, 那么创建一个进程取数据 ,并把数据放入共享缓冲区中 ,而让第二个进程取走数据项并处理之 ,应该比 较容易。在多处理机中,让每个进程在不同 的CPU上运行会使整个作业运行得更快 。

在交互式系统中,键入一个命令或者点(双)击一个图标就可以启动一个程序。这两个动作中的任何一 个都会开始一个新的进程,并在其中运行所选择的程序。在基于命令行的UNIX系统中运行程序X ,新的进程 会从该进程接管开启它的窗口。在Microsoft Windows中,多数情形都是这样的,在一个进程开始时,它并没 有窗口,但是它可以创建一个 (或多个) 窗口。在UNIX和Windows系统中,用户可以同时打开多个窗口,每 个窗口都运行一个进程。通过鼠标用户可以选择一个窗口并且与该进程交互,例如,在需要时提供输入。

最后一种创建进程的情形仅在大型机的批处理系统中应用 。用户在这种系统中(可能是远程地) 提交批处理作业。在操作系统认为有资源可运行另一个作业时,它创建一个新的进程 ,并运行其输入队列中的下一个作业 。

从技术上看 ,在所有这些情形中 ,新进程都是由于一个己存在的进程执行了一个用于创建进程的系 统调用而创建的。这个进程可以是一个运行的用户进程、一个由键盘或鼠标启动的系统进程或者一个批 处理管理进程。这个进程所做的工作是,执行一个用来创建新进程的系统调用。这个系统调用通知操作 系统创建一个新进程 ,并且直接或间接地指定在该进程中运行的程序 。

在UNIX系统中 ,只有一个系统调用可以用来创建新进程 :fork 。这个系统调用会创建一个与调用进 程相同的副本。在调用了fork后,这两个进程( 父进程和子进程) 拥有相同的内存映像、同样的环境字符串和同样的打开文件 。这就是全部情形。通常,子进程接着执行 execve或一个类似的系统调用,以修 改其内存映像并运行一个新的程序。例如 ,当一个用户在shell 中键入命令sort时,shell就创建一个子进程 , 然后 ,这个子进程执行 sort。之所以要安排两步建立进程 ,是为了在fork之后但在 execve之前允许该子进程处理其文件描述符 ,这样可以完成对标准输入文件 、标准输出文件和标准错误文件的重定向。

在Windows中,情形正相反,一个Win32 函数调用CreateProcess既处理进程的创建 ,也负责把正 确的程序装入新的进程。该调用有10个参数 ,其中包括要执行的程序、输入给该程序的命令行参数 、各 种安全属性、有关打开的文件是否继承的控制位 、优先级信息 、该进程( 若有的话)所需要创建的窗口规格以及指向一个结构的指针,在该结构中新创建进程的信息被返回给调用者。除了CreateProcess,Win32中有大约100个其他的函数用于处理进程的管理、同步以及相关的事务。

在UNIX和Windows 中,进程创建之后,父进程和子进程有各自不同的地址空间 。如果其中某个进程在其地址空间中修改了一个字 ,这个修改对其他进程而 言是不可见的。在UNIX 中,子进程的初始地址空间是父进程的一个副本 ,但是这里涉及两个不同的地址空间 ,不可写的内存区是共享的。某些 UNIX 的实现使程序正文在两者间共享 ,因为它不能被修改。或者,子进程共享父进程的所有内存 ,但这种情况下内存通过写时复制(copy-on-write ) 共享 ,这意味着一且两者之一想要修改部分内存 ,则这 块内存首先被明确地复制 ,以确保修改发生在私有内存区域。再次强调 ,可写的内存是不可以共享的。 但是 ,对于一个新创建的进程而言 ,确实有可能共享其创建者的其他资源,诸如打开的文件等。在 Windows中,从一开始父进程的地址空间和子进程的地址空间就是不同的。

2.1.3 进程的终止(Process Termination)

进程在创建之后 ,它开始运行 ,完成其工作。但永恒是不存在的,进程也一样 。迟早这个新的进程 会终止 ,通常由下列条件引起 :
1) 正常退出 (自愿的)。
2) 出错退出 (自愿的)。
3) 严重错误 (非自愿)。
4) 被其他进程杀死 (非自愿)。

多数进程是由于完成了它们的工作而终止 。当编译器完成了所给定程序的编译之后 ,编译器执行一 个系统调用 ,通知操作系统它的工作已经完成。在UNIX 中该调用是exit ,而在Windows 中,相关的调用 是ExitProcess。面向屏幕的程序也支持自愿终止。字处理软件、Internet 浏览器和类似的程序中总有一 个供用户点击的图标或菜单项 ,用来通知进程删除它所打开的任何临时文件 ,然后终止。

进程终止的第二个原因是进程发现了严重错误 。例如,如果用户键入命令
cc foo.c

要编译程序foo.c ,但是该文件井不存在 ,于是编译器就会退出 。在给出了错误参数时 ,面向屏幕的交互 式进程通常并不退出 。相反,这些程序会弹出一个对话框,并要求用户再试一次。

进程终止的第三个原因是由进程引起的错误 ,通常是由于程序中的错误所致。例如,执行了一条非法指令、引用不存在的内存 ,或除数是零等。有些系统中(如UNIX ) ,进程可以通知操作系统 ,它希望 自行处理某些类型的错误 ,在这类错误中 ,进程会收到信号 (被中断) ,而不是在这类错误出现时终止 。

第四种终止进程的原因是,某个进程执行一个系统调用通知操作系统杀死某个其他进程 。在UNIX 中,这个系统调用是 kill 。在Win32中对应的函数是Terminate Process。在这两种情形中,“杀手” 都必须获得确定的授权以便进行动作。在有些系统中,当一个进程终止时,不论是自愿的还是其他原因 ,由该进程所创建 的所有进程也一律立即被杀死。不过,UNIX和Windows都不是这种工作方式。

2.1.4 进程的层次结构(Process Hierarchies)

某些系统中 ,当进程创建了另一个进程后 ,父进程和子进程就以某种形式继续保持关联 。子进程自身可以创建更多的进程 ,组成一个进程的层次结构。请注意,这与植物和动物的有性繁殖不同 ,进程只有一个父进程 (但是可以有零个、一个、两个或多个子进程)。

在UNIX中,进程和它的所有子进程以及后裔共同组成一个进程组 。当用户从键盘发出一个信号时, 该信号被送给当前与键盘相关的进程组中的所有成员 ( 它们通常是在当前窗口创建的所有活动进程) 。 每个进程可 以分别捕获该信号、忽略该信号或采取默认的动作,即被该信号杀死。

这里有另一个例子,可以用来说明进程层次的作用 ,考虑UNIX在启动时如何初始化自己 。一个称为init的特殊进程出现在启动映像中。当它开始运行时,读入一个说明终端数量的文件 。接着,为每个终端创建一个新进程。这些进程等待用户登录。如果有一个用户登录成功 ,该登录进程就执行一个shell 准备接收命令。所接收的这些命令会启动更多的进程 ,以此类推 。这样,在整个系统中 ,所有的进程都 属于以 init 为根的一棵树。

相反 ,Window s中没有进程层次的概念 ,所有的进程都是地位相同的。唯一类似于进程层次的暗示 是在创建进程的时候 ,父进程得到 一个特别的令牌 (称为句柄) ,该句柄可以用来控制子进程 。但是, 它有权把这个令牌传送给某个其 他进程 ,这样就不存在进程层次了 。在UNIX中,进程就不能剥夺其子继承的 “继承权”。

2.1.5 进程的状态(Process States)

尽管每个进程是一个独立的实体,有其自己的程序计数器和内部状态,但是,进程之间经常需要相互作用。一个进程的输出结果可能作为另一个进程的输入。在shell命令
cat chapter 1 chapter2 chapter3 I grep tree
中,第一个进程运行cat ,将三个文件连接并输出 。第二个进程运行grep ,它从输入中选择所有包含单词 “tree” 的那些行。根据这两个进程的相对速度 (这取决于这两个程序的相对复杂度和各自所分配到的CPU时间),可能发生这种情况 :grep准备就绪可以运行,但输入还没有完成。于是必须阻塞grep,直到 输入到来。

当一个进程在逻辑上不能继续运行时,它就会被阻塞 ,典型的例子是它在等待可 以使用的输入。还 可能有这样的情况 :一个概念上能够运行的进程*停止 ,因为操作系统调度另一个进程占用了CPU 。 这两种情况是完全不同 的。在第一种情况下 ,进程挂起是程序自身固有的原因 (在键入用户命令行之前 , 无法执行命令) 。第二种情况则是由系统技术上的原因引起的 (由于没有足够的CPU,所以不能使每个进程都有一台私用的处理器)。在图2-2中可以看到显示进程的三种状态的状态图,这三种状态是:
1) 运行态 (该时刻进程实际占用CPU ) 。
2) 就绪态 (可运行,但因为其他进程正在运行而暂时停止)。
3) 阻塞态 (除非某种外部事件发生,否则进程不能运行)。

前两种状态在逻辑上是类似的 。处于 这两种状态的进程都可以运行 ,只是对于 第二种状态暂时没有 CPU分配给它。第三种状态与前两种状态不同 ,处于该状态的进程不能运行 ,即使CPU空闲也不行。
进程与线程:进程
进程的三种状态之间有四种可能的转换关系 ,如图2-2所示。在操作系统发现进程不能继续运行下去肘 ,发生转换1。在某些系统中,进程可 以执行一个诸如pause的系统调用来进入阻塞状态 。在其他系统中 ,包括UNIX ,当一个进程从管道或 设备文件 (例如终端) 读取数据时,如果没有有效的输入存在,则进程会被自动阻塞。

转换2和3是由进程 调度程序(Scheduler)引起的,进程调度程序是操作系统 的一部分,进程甚至感觉不到调度程序的存在。系统认为一个运行进程占用处理器的时间已经过长 ,决定让其他进程使用CPU时间时,会发 生转换2。在系统已经让所有其他进程享有了它们应有的公平待遇而重新轮到第一个进程再次 占用CPU 运行时,会发生转换3。调度程序的主要工作就是决定应当运行哪个进程、何时运行及它应该运行多长时间,这是很重要的一点,我们将在本章的后面部分进行讨论。目前已经提出了许多算法,这些算法力 图在整体效率和进程的竞争公平性之间取得平衡。我们将在本章稍后部分研究其中的一些问题。

当进程等待的一个外部事件发生时 (如一些输入到达),则发生转换4。如果此时没有其他进程运行 ,则立即触发转换 3,该进程便开始运行。否则该进程将处于就绪态 ,等待CPU空闲并 且轮到它运行。

使用进程模型使得我们易于想象系统内部的操作状况。一些进程正在运行执行用户键入命令所对应 的程序。另一些进程是系 统的一部分 ,它们的任务是完成下列一些工作:比如,执行文件服务请求、管理磁盘驱动器和磁带机 的运行细节等 。当发生一个磁盘中断时 ,系统会做出决定,停止运行当前进程 ,转而运行磁盘进程 ,该进程在此之前因等待中断而处 于阻塞态。这样就可以不再考虑中断 ,而只是考虑用户进程 、磁盘进程、终端进程等 。这些进程在等待 时总是处于阻塞状态。在已经读入磁盘或键入字符后,等待它们的进程就被解除阻塞,并成为可调度运 行的进程。

从这个观点引出了图2-3所示的模型。在图2-3中,操作系统的最底层是调度程序 ,在它上面有许多 进程。所有关于中断处理、启动进程和停止进程的具体细节都隐藏在调度程序中 。实际上,调度程序是 一段非常短小的程序。操作系统的其他部分被简单地组织成进程的形式。不过 ,很少有真实的系统是 以 这样的理想方式构造的。
进程与线程:进程

2.1.6 进程的实现(Implementation of Processes)

为了实现进程模型 ,操作系统维护着一张表格 (一个结构数组) ,即进程表(process table) 。每个进 程占用一个进程表项 。(有些作者称这些表项为进程控制块。) 该表项包含了迸程状态的重要信息,包括 程序计数器 、堆钱指针、内存分配状况 、所打开文件的状态、账号和调度信息,以及其他在进程由运行 态转换到就绪态或阻塞态时必须保存的信息,从而保证该进程随后能再次启动,就像从未被中断过一样。

图2-4中展示了在一个典型系统中的关键字段 。第一列中的字段与进程管理有关。其他两列分别与 存储管理和文件管理有关 。应该注意到进程表中的字段是与系统密切相关的,不过该图给出了所需要信 息的大致介绍。

图2-4 典型的进程表表项中的一些字段:

序号 进程管理(Process management) 存储管理(Memory management ) 文件管理(File management)
1 寄存器(Registers) 正文段 指针(Pointer to text segment info) 根目录(Root directory)
2 程序计数器(Program counter) 数据段 指针(Pointer to data segment info) 工作目录(Wor king director y)
3 程序状态字(Program status word ) 堆栈段 指针(Pointer to stack segment info) 文件描述符(File descriptors)
4 堆栈指针(Stack pointer) 用户ID(User ID)
5 进程状态(Process state) 组ID(Group ID)
6 优先级(Priority)
7 调度参数(Scheduling parameters)
8 进程ID(Process ID)
9 父进程(Parent process)
10 迸程组(Process group)
11 信号(Signals)
12 进程开始时间(Time when process started)
13 使用的CPU时间(CPU time used)
14 子进程的CPU时间(Children’s CPU time)
15 下次定时器时间(Time of next alarm)

在了解进程表后 ,就可以对在单个 (或每一个) CPU上如何维持多个顺序进程的 错觉做更多的阐述。 与每I/O类关联的是一个称作中断向量(interrupt vector ) 的位置(靠近内存底部的固定区域)。它包 含中断服务程序的入口地址 。假设当一个磁盘中断发生时,用户进程3正在运行 ,则中断硬件将程序计数器 、程序状态字 、有时还有一个或多个寄存器压入堆栈 ,计算机随即跳转到中断向量所指示的地址 。 这些是硬件完成的所有操作,然后软件,特别是中断服务例程就接管一切剩余的工作。

所有的中断都从保存寄存器开始 ,对于当前进程而言 ,通常是保存在进程表项中 。随后,会从堆栈中删除由中断硬件机制存入堆栈的那部分信息 ,并将堆栈指针指向一个由进程处理程序所使用 的临时堆栈。一些诸如保存寄存器值和设置堆栈指针等操作,无法用C语言这一类高级语言描述,所以这些操作通 过一个短小的汇编语言例程来完成 ,通常该例程可以供所有的中断使用 ,因为无论中断是怎样引起的,有关保存寄存器的工作则是完全一样的。

当该例程结束后 ,它调用一个C过程处理某个特定的中断类型剩下的工作。(假定操作系统由C语言编写,通常这是所有真实操作系统的选择)。在完成有关工作之后 ,大概就会使某些进程就绪,接着调用调度程序 ,决定随后该运行哪个进程 。随后将控制转给一段汇编语言代码,为当前的进程装入寄存器值以及内存映射并启动该进程运行。图2-5 中总结了中断处理和调度的过程 。值得注意的是 ,各种系统之间 某些细节会有所不 同。

一个进程在执行过程中可能被中断数千次 ,但关键是每次中断后,被中断的进程都返回到与中断发生前完全相同的状态。

图2-5 中断发生时操作系统最底层的工作步骤:

序号 步骤 Procedure
1 硬件压入堆栈程序计数器等 Hardware stacks program counter, etc.
2 硬件从中断向量装入新的程序计数器 Hardware loads new program counter from interrupt vector.
3 汇编语言过程 保存寄存器值 Assembly-language procedure saves registers.
4 汇编语言过程 设置新的堆栈 Assembly-language procedure sets up new stack.
5 C中断服务例程运行(典型地读和缓冲输入) C interrupt service runs (typically reads and buffers input).
6 调度程序决定下一个将运行的进程 Scheduler decides which process is to run next.
7 C过程返回至汇编代码 C procedure returns to the assembly code.
8 汇编语言过程开始运行新的当前进程 Assembly-language procedure starts up new current process.

2.1.7 多道程序设计模型(Modeling Multiprogramming)

采用多道程序设计可以提高CPU的利用率。严格地说 ,如果进程用于计算的平均时间是进程在内存 中停留时间的20% ,且内存中同时有5个进程 ,则CPU将一直满负载运行 。然而,这个模型在现实中过于乐观 ,因为它假设这5个进程不会同时等待I/O。

更好的模型是从概率的角度来看 CPU的利用率 。假设一个进程等待l/O操作的时间与 其停留在内存中时间 的比为p。当内存中同时有n个进程时 ,则所有n个进程都在等待I/O(此时CPU空转) 的概率是pn。CPU 的利用率 由下面的公式给出:
CPU利用率 =1 - pn

图2-6 以n为变量的函数表示了 CPU的利 用率 ,n称为 多道程序设计的道数 ( degree of multiprogramming ) 。
进程与线程:进程
从图2-6中可以清楚地看到 ,如果进程花 费80%的时问等待I/O ,为使CPU 的浪费低于10% ,至少要有10个进程同时在内存中。当读者认识到一个等待用户从终端输入的交互式进程是处于I/O等待状态时 ,那么很明显,80%甚至更多的I/O等待时间是普遍的 。即使是在服务器中 ,做大量磁盘I/O操作的进程也会花费同样或更多的等待时间。

从完全精确的角度考虑,应该指出此概率模型只是描述了一个大致的状况 。它假设所有n个进程是 独立的 ,即内存中的5个进程中 ,3个运行 ,2个等待,是完全可接受的。但在单CPU 中,不能同时运行3 个进程 ,所以当CPU忙时,己就绪的进程也必须等待CPU。因而,进程不是独立的。更精确的模型应该 用排队论构建 ,但我们的模型 (当进程就绪时,给进程分配CPU ,否则让CPU空转) 仍然是有效的,即使真实曲线会与图 2-6中所画的略有不同。

虽然图2-6的模型很简单、很粗略 ,它依然对预测CPU 的性能很有效 。例如,假设计算机有8GB 内 存,操作系统及相关表格占用2GB ,每个用户程序也占用2GB。这些内存空间允许3个用户程序同时驻 留在内存中。若80%的时间用于I/0等待 ,则CPU的利用率 (忽略操作系统开销) 大约是1-0.83,即大约 49% 。在增加8GB字节的内存后 ,可从3道程序设计提高到7道程序设计 ,因而CPU利用率提高到79%。 换言之 ,第二个8GB内存提高了30%的吞吐量。
增加第三个8GB内存只能将CPU利用率从79%提高到91% ,吞吐量的提高仅为 12%。通过这一模型 , 计算机用户可以确定,第一次增加内存是一个划算的投资 ,而第二个则不是 。