第二章 进程与线程
2.1 进程
2.1.1 进程模型
在进程模型中, 计算机上所有可运行的软件, 通常也包括操作系统, 被组织成若干顺序进程(sequential process) , 简称进程(process) 。 一个进程就是一个正在执行程序的实例, 包括程序计数器、寄存器和变量的当前值。
这里的关键思想是: 一个进程是某种类型的一个活动, 它有程序、 输入、 输出以及状态。 单个处理器可以被若干进程共享, 它使用某种调度算法决定何时停止一个进程的工作, 并转而为另一个进程提供服务。
2.1.2 进程的创建
有4种主要事件导致进程的创建:
- 系统初始化。
- 执行了正在运行的进程所调用的进程创建系统调用。
- 用户请求创建一个新进程。
- 一个批处理作业的初始化。
2.1.3 进程的终止
原因:
- 正常退出;
- 出错退出;(异常处理)
- 严重错误;(非法指令,引用错误内存,除零错误)
- 被杀死
2.1.4 进程的层次结构
某些系统中, 当进程创建了另一个进程后, 父进程和子进程就以某种形式继续保持关联。 子进程自身可以创建更多的进程, 组成一个进程的层次结构。 请注意, 这与植物和动物的有性繁殖不同, 进程只有一个父进程(但是可以有零个、 一个、 两个或多个子进程)
- Windows: 没有层次的概念,所有进程地位相同
- Linux:进程及进程的子女们组成进程组
2.1.5 进程的状态
进程的三种状态:
-
运行态(实际占用CPU)
-
就绪态(可运行)
3. **阻塞态(等待外部事件)**
2.1.6 进程的实现
为了实现进程模型, 操作系统维护着一张表格(一个结构数组) , 即进程表(process table) 。 每个进程占用一个进程表项。 (有些作者称这些表项为进程控制块。) 该表项包含了进程状态的重要信息, 包括程
序计数器、 堆栈指针、 内存分配状况、 所打开文件的状态、 账号和调度信息, 以及其他在进程由运行态转换到就绪态或阻塞态时必须保存的信息, 从而保证该进程随后能再次启动, 就像从未被中断过一样。
- 进程表:储存进程状态(程序计数器,堆栈指针,内存分配状况,打开的文件状态。账号等)
- 中断向量:与每一个IO类关联
- 中断发生时,中断硬件程序将进程表中的重要数据压入堆栈,计算机跳到中断向量的地址
- 汇编语言设置新的堆栈(无法用C语言这类高级语言来描述)
2.1.7 多道程序设计模型
采用多道程序设计可以提高CPU的利用率。 严格地说, 如果进程用于计算的平均时间是进程在内存中停留时间的20%, 且内存中同时有5个进程, 则CPU将一直满负载运行。 然而, 这个模型在现实中过于乐观,因为它假设这5个进程不会同时等待I/O。
假设一个进程等待IO与停留在CPU的时间比为p,n个进程时,CPU使用率为
使用率
=
1
–
p
n
使用率 = 1 – p^n
使用率=1–pn
2.2 线程
在传统操作系统中,每个进程有一个地址空间和一个控制线程。事实上,这几乎就是进程的定义。不过,经常存在在同一个地址空间中准并行运行多个控制线程的情形,这些线程就像(差不多)分离的进程(共享地址空间除外)。
2.2.1 线程的使用
为什么人们需要在一个进程中再有一类进程?
- 并行实体共享同一个地址空间和所有可用数据的能力。
- 第二个关于需要多线程的理由是, 由于线程比进程更轻量级, 所以它们比进程更容易(即更快) 创建,也更容易撤销。
- 性能方面。如果存在着大量的计算和大量的I/O处理, 拥有多个线程允许这些活动彼此重叠进行, 从而会加快应用程序执行的速度。
- 在多CPU系统中, 多线程是有益的, 在这样的系统中, 真正的并行有了实现的可能。
2.2.2 经典的线程模型
在多线程的情况下, 进程通常会从当前的单个线程开始。 这个线程有能力通过调用一个库函数(如thread_create) 创建新的线程。
当一个线程完成工作后, 可以通过调用一个库过程(如thread_exit) 退出。 该线程接着消失, 不再可调度。 在某些线程系统中, 通过调用一个过程, 例如(thread_join), 一个线程可以等待一个(特定) 线程退出。
一个常见的线程调用是(thread_yield), 它允许线程自动放弃CPU从而让另一个线程运行。
2.2.3 POSIX线程
为实现可移植的线程程序, IEEE在IEEE标准1003.1c中定义了线程的标准。 它定义的线程包叫做Pthread。 大部分UNIX系统都支持该标准。
2.2.4 在用户空间中实现线程
有两种主要的方法实现线程包: 在用户空间中和在内核中。 这两种方法互有利弊, 不过混合实现方式也是可能的。 我们现在介绍这些方法, 并分析它们的优点和缺点。
优势:
- 保存该线程状态的过程和调度程序都只是本地过程, 所以启动它们比进行内核调用效率更高。 另一方面, 不需要陷入, 不需要上下文切换, 也不需要对内存高速缓存进行刷新, 这就使得线程调度非常快捷。
- 它允许每个进程有自己定制的调度算法。
缺点:
- 如何实现阻塞系统调用。 使用线程的一个主要目标是, 首先要允许每个线程使用阻塞调用, 但是还要避免被阻塞的线程影响其他的线程。
- 如果一个线程开始运行, 那么在该进程中的其他线程就不能运行, 除非第一个线程自动放弃CPU。
2.2.5 在内核中实现线程
在内核中实现线程此时不再需要运行时系统了。 另外, 每个进程中也没有线程表。 相反, 在内核中有用来记录系统中所有线程的线程表。 当某个线程希望创建一个新线程或撤销一个已有线程时, 它进行一个系统调用, 这个系统调用通过对线程表的更新完成线程创建或撤销工作。
优势:
- 所有能够阻塞线程的调用都以系统调用的形式实现, 这与运行时系统过程相比, 代价是相当可观的。 当一个线程阻塞时, 内核根据其选择, 可以运行同一个进程中的另一个线程(若有一个就绪线程) 或者运行另一个进程中的线程。 而在用户级线程中, 运行时系统始终运行自己进程中的线程, 直到内核剥夺它的CPU(或者没有可运行的线程存在了) 为止。
- 内核线程不需要任何新的、 非阻塞系统调用。 另外, 如果某个进程中的线程引起了页面故障, 内核可以很方便地检查该进程是否有任何其他可运行的线程, 如果有, 在等待所需要的页面从磁盘读入时, 就选择一个可运行的线程运行。 这样做的主要缺点是系统调用的代价比较大, 所以如果线程的操作(创建、 终止等)比较多, 就会带来很大的开销
缺点:
-
当一个多线程进程创建新的进程时, 会发生什么?
新进程是拥有与原进程相同数量的线程, 还是只有一个线程? 在很多情况下, 最好的选择取决于进程计划下一步做什么。 如果它要调用exec来启动一个新的程序, 或许一个线程是正确的选择; 但是如果它继续执行, 则应该复制所有的线程。
-
另一个话题是信号。 回忆一下, 信号是发给进程而不是线程的, 至少在经典模型中是这样的。 当一个信号到达时, 应该由哪一个线程处理它?
线程可以“注册”它们感兴趣的某些信号, 因此当一个信号到达的时候, 可把它交给需要它的线程。
2.2.6 混合实现
人们已经研究了各种试图将用户级线程的优点和内核级线程的优点结合起来的方法。 一种方法是使用内核级线程, 然后将用户级线程与某些或者全部内核线程多路复用起来, 如图所示。 如果采用这种方法,编程人员可以决定有多少个内核级线程和多少个用户级线程彼此多路复用。 这一模型带来最大的灵活度。
2.2.7 调度程序激活机制
该机制工作的基本思路是, 当内核了解到一个线程被阻塞之后(例如, 由于执行了一个阻塞系统调用或者产生了一个页面故障), 内核通知该进程的运行时系统, 并且在堆栈中以参数形式传递有问题的线程编号和所发生事件的一个描述。 内核通过在一个已知的起始地址启动运行时系统, 从而发出了通知, 这是对UNIX中信号的一种粗略模拟。 这个机制称为上行调用(upcall) 。
简述如下:
-
内核给每个进程安排一定数量的虚拟处理器并且让运行时系统分到线程上
-
进程被阻塞后,内核通知运行时系统(upcall)
-
根据中断决定是否继续
2.2.8 弹出式线程
弹出式线程的关键好处是, 由于这种线程相当新, 没有历史——没有必须存储的寄存器、 堆栈诸如此类的内容, 每个线程从全新开始, 每一个线程彼此之间都完全一样。 这样, 就有可能快速创建这类线程。 对该新线程指定所要处理的消息。 使用弹出式线程的结果是, 消息到达与处理开始之间的时间非常短。
2.2.9 使单线程代码多线程化
一个线程的代码就像进程一样, 通常包含多个过程, 会有局部变量、 全局变量和过程参数。 局部变量和参数不会引起任何问题, 但是有一个问题是, 对线程而言是全局变量, 并不是对整个程序也是全局的。 有许多变量之所以是全局的, 是因为线程中的许多过程都使用它们(如同它们也可能使用任何全局变量一样) , 但是其他线程在逻辑上和这些变量无关。
- 一种解决方案是为每个线程赋予其私有的全局变量。 在这个方案中, 每个线程有自己的errno以及其他全局变量的私有副本, 这样就避免了冲突。
- 还有另一种方案, 可以引入新的库过程, 以便创建、 设置和读取这些线程范围的全局变量。
- 另一种解决方案是, 为每个过程提供一个包装器, 该包装器设置一个二进制位从而标志某个库处于使用中。 在先前的调用还没有完成之前, 任何试图使用该库的其他线程都会被阻塞。 尽管这个方式可以工作, 但是它会极大地降低系统潜在的并行性。
2.3 进程间通信
进程经常需要与其他进程通信。 例如, 在一个shell管道中, 第一个进程的输出必须传送给第二个进程,这样沿着管道传递下去。 因此在进程之间需要通信, 而且最好使用一种结构良好的方式, 不要使用中断。 在下面几节中, 我们就来讨论一些有关**进程间通信(Inter Process Communication, IPC)**的问题。
简要地说, 有三个问题。
- 第一个问题与上面的叙述有关, 即一个进程如何把信息传递给另一个。
- 第二个要处理的问题是, 确保两个或更多的进程在关键活动中不会出现交叉, 例如, 在飞机订票系统中的两个进程为不同的客户试图争夺飞机上的最后一个座位。
- 第三个问题与正确的顺序有关(如果该顺序是有关联的话) , 比如, 如果进程A产生数据而进程B打印数据, 那么B在打印之前必须等待, 直到A已经产生一些数据。
2.3.1竞争条件
在一些操作系统中, 协作的进程可能共享一些彼此都能读写的公用存储区。 这个公用存储区可能在内存中(可能是在内核数据结构中) , 也可能是一个共享文件。
两个或多个进程读写某些共享数据, 而最后的结果取决于进程运行的精确时序, 称为竞争条件(race condition) 。 调试包含有竞争条件的程序是一件很头痛的事。 大多数的测试运行结果都很好, 但在极少数情况下会发生一些无法解释的奇怪现象。
2.3.2 临界区
怎样避免竞争条件? 实际上凡涉及共享内存、 共享文件以及共享任何资源的情况都会引发与前面类似的错误, 要避免这种错误, 关键是要找出某种途径来阻止多个进程同时读写共享的数据。 换言之, 我们需要的是互斥(mutual exclusion), 即以某种手段确保当一个进程在使用一个共享变量或文件时, 其他进程不能做同样的操作。
在某些时候进程可能需要访问共享内存或共享文件, 或执行另外一些会导致竞争的操作。 我们把对共享内存进行访问的程序片段称作临界区域(critical region) 或临界区(critical section) 。 如果我们能够适当地安排, 使得两个进程不可能同时处于临界区中, 就能够避免竞争条件。
尽管这样的要求避免了竞争条件, 但它还不能保证使用共享数据的并发进程能够正确和高效地进行协作。 对于一个好的解决方案, 需要满足以下4个条件:
- 任何两个进程不能同时处于其临界区。
- 不应对CPU的速度和数量做任何假设。
- 临界区外运行的进程不得阻塞其他进程。
- 不得使进程无限期等待进入临界区。
2.3.3 忙等待的互斥
本节将讨论几种实现互斥的方案。 在这些方案中, 当一个进程在临界区中更新共享内存时, 其他进程将不会进入其临界区, 也不会带来任何麻烦。
1.屏蔽中断
在单处理器系统中, 最简单的方法是使每个进程在刚刚进入临界区后立即屏蔽所有中断, 并在就要离开之前再打开中断。 屏蔽中断后, 时钟中断也被屏蔽。 CPU只有发生时钟中断或其他中断时才会进行进程切换, 这样, 在屏蔽中断之后CPU将不会被切换到其他进程。 于是, 一旦某个进程屏蔽中断之后, 它就可以检查和修改共享内存, 而不必担心其他进程介入。
缺点:
因为把屏蔽中断的权力交给用户进程是不明智的。 设想一下, 若一个进程屏蔽中断后不再打开中断, 其结果将会如何? 整个系统可能会因此终止。 而且, 如果系统是多处理器(有两个或可能更多的处理器) , 则屏蔽中断仅仅对执行disable指令的那个CPU有效。 其他CPU仍将继续运行, 并可以访问共享内存。
2.锁变量
作为第二种尝试, 可以寻找一种软件解决方案。 设想有一个共享(锁) 变量, 其初始值为0。 当一个进程想进入其临界区时, 它首先测试这把锁。 如果该锁的值为0, 则该进程将其设置为1并进入临界区。 若这把锁的值已经为1, 则该进程将等待直到其值变为0。 于是, 0就表示临界区内没有进程, 1表示已经有某个进程进入临界区。
缺点:
但是, 这种想法也包含了与假脱机目录一样的疏漏。 假设一个进程读出锁变量的值并发现它为0, 而恰好在它将其值设置为1之前, 另一个进程被调度运行, 将该锁变量设置为1。 当第一个进程再次能运行时, 它同样也将该锁设置为1, 则此时同时有两个进程进入临界区中。
3.严格轮换法
在下图中, 整型变量turn, 初始值为0, 用于记录轮到哪个进程进入临界区, 并检查或更新共享内存。开始时, 进程0检查turn, 发现其值为0, 于是进入临界区。 进程1也发现其值为0, 所以在一个等待循环中不停地测试turn, 看其值何时变为1。 连续测试一个变量直到某个值出现为止, 称为忙等待(busy waiting) 。
由于这种方式浪费CPU时间, 所以通常应该避免。
只有在有理由认为等待时间是非常短的情形下, 才使用忙等待。 用于忙等待的锁, 称为自旋锁(spin lock) 。
4.Peterson解法
在使用共享变量(即进入其临界区) 之前, 各个进程使用其进程号0或1作为参数来调用enter_region。该调用在需要时将使进程等待, 直到能安全地进入临界区。 在完成对共享变量的操作之后, 进程将调用leave_region, 表示操作已完成, 若其他的进程希望进入临界区, 则现在就可以进入。
5.TSL指令
现在来看需要硬件支持的一种方案。 某些计算机中, 特别是那些设计为多处理器的计算机, 都有下面一条指令:
TSL RX,LOCK称为测试并加锁(Test and Set Lock) , 它将一个内存字lock读到寄存器RX中, 然后在该内存地址上存一个非零值。 读字和写字操作保证是不可分割的, 即该指令结束之前其他处理器均不允许访问该内存字。 执行TSL指令的CPU将锁住内存总线, 以禁止其他CPU在本指令结束之前访问内存。
2.3.4 睡眠与唤醒
Peterson解法和TSL或XCHG解法都是正确的, 但它们都有忙等待的缺点。 这些解法在本质上是这样的:当一个进程想进入临界区时, 先检查是否允许进入, 若不允许, 则该进程将原地等待, 直到允许为止。
这种方法不仅浪费了CPU时间, 而且还可能引起预想不到的结果。 考虑一台计算机有两个进程, H优先级较高, L优先级较低。 调度规则规定, 只要H处于就绪态它就可以运行。 在某一时刻, L处于临界区中, 此时H变到就绪态, 准备运行(例如, 一条I/O操作结束)。现在H开始忙等待, 但由于当H就绪时L不会被调度, 也就无法离开临界区, 所以H将永远忙等待下去。 这种情况有时被称作优先级反转问题(priority inversion problem) 。
2.3.5 信号量
信号量是E.W.Dijkstra在1965年提出的一种方法, 它使用一个整型变量来累计唤醒次数, 供以后使用。
在他的建议中引入了一个新的变量类型, 称作信号量(semaphore) 。 一个信号量的取值可以为0(表示没有保存下来的唤醒操作) 或者为正值(表示有一个或多个唤醒操作) 。
Dijkstra建议设立两种操作: down和up(分别为一般化后的sleep和wakeup) 。 对一信号量执行down操作, 则是检查其值是否大于0。 若该值大于0, 则将其值减1(即用掉一个保存的唤醒信号) 并继续; 若该值为0, 则进程将睡眠, 而且此时down操作并未结束。 检查数值、 修改变量值以及可能发生的睡眠操作均作为一个单一的、 不可分割的原子操作完成。 保证一旦一个信号量操作开始, 则在该操作完成或阻塞之前, 其他进程均不允许访问该信号量。 这种原子性对于解决同步问题和避免竞争条件是绝对必要的。
2.3.6 互斥量
如果不需要信号量的计数能力, 有时可以使用信号量的一个简化版本, 称为互斥量(mutex)。 互斥量仅仅适用于管理共享资源或一小段代码。 由于互斥量在实现时既容易又有效, 这使得互斥量在实现用户空间线程包时非常有用。
互斥量是一个可以处于两态之一的变量: 解锁和加锁。 这样, 只需要一个二进制位表示它, 不过实际上, 常常使用一个整型量, 0表示解锁, 而其他所有的值则表示加锁。 互斥量使用两个过程。 当一个线程(或进程) 需要访问临界区时, 它调用mutex_lock。 如果该互斥量当前是解锁的(即临界区可用) , 此调用成功, 调用线程可以自由进入该临界区。
2.3.7 管程
为了更易于编写正确的程序, Brinch Hansen(1973) 和Hoare(1974) 提出了一种高级同步原语, 称为管程(monitor) 。
一个管程是一个由过程、变量及数据结构等组成的一个集合, 它们组成一个特殊的模块或软件包。 进程可在任何需要的时候调用管程中的过程, 但它们不能在管程之外声明的过程中直接访问管程内的数据结构。
与管程和信号量有关的另一个问题是, 这些机制都是设计用来解决访问公共内存的一个或多个CPU上的互斥问题的。 通过将信号量放在共享内存中并用TSL或XCHG指令来保护它们, 可以避免竞争。 如果一个分布式系统具有多个CPU, 并且每个CPU拥有自己的私有内存, 它们通过一个局域网相连, 那么这些原语将失效。
2.3.8 消息传递
上面提到的其他的方法就是消息传递(message passing) 。 这种进程间通信的方法使用两条原语send和receive, 它们像信号量而不像管程, 是系统调用而不是语言成分。 因此, 可以很容易地将它们加入到库例程中去。
2.3.9 屏障
最后一个同步机制是准备用于进程组而不是用于双进程的生产者-消费者类情形的。 在有些应用中划分了若干阶段, 并且规定, 除非所有的进程都就绪准备着手下一个阶段, 否则任何进程都不能进入下一个阶段。 可以通过在每个阶段的结尾安置屏障(barrier) 来实现这种行为。 当一个进程到达屏障时, 它就被屏障阻拦, 直到所有进程都到达该屏障为止。
2.3.10 避免锁:读一复制一更新
2.4 调度
当计算机系统是多道程序设计系统时, 通常就会有多个进程或线程同时竞争CPU。 只要有两个或更多的进程处于就绪状态, 这种情形就会发生。 如果只有一个CPU可用, 那么就必须选择下一个要运行的进程。 在操作系统中, 完成选择工作的这一部分称为调度程序(scheduler), 该程序使用的算法称为调度算法(scheduling algorithm) 。
2.4.1 调度简介
1.进程行为
几乎所有进程的(磁盘) I/O请求或计算都是交替突发的, 如图下图所示。
某些进程(图2-38a的进程) 花费了绝大多数时间在计算上, 而其他
进程(图2-38b的进程) 则在等待I/O上花费了绝大多数时间。 前者称为计算密集型(compute-bound) , 后者称为I/O密集型(I/O-bound) 。
2.何时调度
- 第一, 在创建一个新进程之后, 需要决定是运行父进程还是运行子进程。
- 第二, 在一个进程退出时必须做出调度决策。 一个进程不再运行(因为它不再存在) , 所以必须从就绪进程集中选择另外某个进程。 如果没有就绪的进程, 通常会运行一个系统提供的空闲进程。
- 第三, 当一个进程阻塞在I/O和信号量上或由于其他原因阻塞时, 必须选择另一个进程运行。
- 第四, 在一个I/O中断发生时, 必须做出调度决策。
3.调度算法分类
-
批处理。
在批处理系统中, 不会有用户不耐烦地在终端旁等待一个短请求的快捷响应。 因此, 非抢占式算法, 或对每个进程都有长时间周期的抢占式算法, 通常都是可接受的。
-
交互式。
在交互式用户环境中, 为了避免一个进程霸占CPU拒绝为其他进程服务, 抢占是必需的。 即便没有进程想永远运行, 但是, 某个进程由于一个程序错误也可能无限期地排斥所有其他进程。 为了避免这种现象发生, 抢占也是必要的。 服务器也归于此类, 因为通常它们要服务多个突发的(远程) 用户。
-
实时。
然而在有实时限制的系统中, 抢占有时是不需要的, 因为进程了解它们可能会长时间得不到运行, 所以通常很快地完成各自的工作并阻塞。实时系统与交互式系统的差别是, 实时系统只运行那些用来推进现有应用的程序, 而交互式系统是通用的, 它可以运行任意的非协作甚至是有恶意的程序。
4.调度算法的目标
2.4.2 批处理系统中的调度
-
先来先服务
所有调度算法中, 最简单的是非抢占式的先来先服务(first-come first-severd) 算法。 使用该算法, 进程按照它们请求CPU的顺序使用CPU。
这个算法的主要优点是易于理解并且便于在程序中运用。 -
最短作业优先
-
最短剩余时间优先
最短作业优先的抢占式版本是最短剩余时间优先(shortest remaining time next) 算法。 使用这个算法,调度程序总是选择剩余运行时间最短的那个进程运行。
2.4.3 交互式系统中的调度
-
轮转调度
一种最古老、 最简单、 最公平且使用最广的算法是轮转调度(round robin) 。 每个进程被分配一个时间段, 称为时间片(quantum) , 即允许该进程在该时间段中运行。 如果在时间片结束时该进程还在运行,则将剥夺CPU并分配给另一个进程。
-
优先级调度
每个进程被赋予一个优先级, 允许优先级最高的可运行进程先运行。
-
多级队列
-
最短进程优先
可以通过首先运行最短的作业来使响应时间最短。
-
保证调度
一种完全不同的调度算法是向用户作出明确的性能保证, 然后去实现它。
-
彩票调度
有一个既可给出类似预测结果而又有非常简单的实现方法的算法, 这个算法称为彩票调度(lottery scheduling)。
-
公平分享调度
到现在为止, 我们假设被调度的都是各个进程自身, 并不关注其所有者是谁。 这样做的结果是, 如果用户1启动9个进程而用户2启动1个进程, 使用轮转或相同优先级调度算法, 那么用户1将得到90%的CPU时间, 而用户2只得到10%的CPU时间。
2.4.4 实时系统中的调度
实时系统是一种时间起着主导作用的系统。
实时系统通常可以分为硬实时(hard real time) 和软实时(soft real time) , 前者的含义是必须满足绝对的截止时间, 后者的含义是虽然不希望偶尔错失截止时间, 但是可以容忍。
2.4.5 策略和机制
一个进程有许多子进程并在其控制下运行。 主进程完全可能掌握哪一个子进程最重要(或最紧迫) 而哪一个最不重要。 但是, 以上讨论的调度算法中没有一个算法从用户进程接收有关的调度决策信息, 这就导致了调度程序很少能够做出最优的选择。
2.4.6 线程调度
当若干进程都有多个线程时, 就存在两个层次的并行: 进程和线程。 在这样的系统中调度处理有本质差别, 这取决于所支持的是用户级线程还是内核级线程(或两者都支持) 。
用户级线程和内核级线程之间的差别在于性能。 用户级线程的线程切换需要少量的机器指令, 而内核级线程需要完整的上下文切换, 修改内存映像, 使高速缓存失效, 这导致了若干数量级的延迟。 另一方面, 在使用内核级线程时, 一旦线程阻塞在I/O上就不需要像在用户级线程中那样将整个进程挂起。
另一个重要因素是用户级线程可以使用专为应用程序定制的线程调度程序。
2.5 经典的IPC问题
2.5.1 哲学家就餐问题
五个哲学家围坐在一张圆桌周围, 每个哲学家面前都有一盘通心粉。 由于通心粉很滑, 所以需要两把叉子才能夹住。 相邻两个盘子之间放有一把叉子, 餐桌如下图所示。
2.5.2 读者一写者问题
例如, 设想一个飞机订票系统, 其中有许多竞争的进程试图读写其中的数据。 多个进程同时读数据库是可以接受的, 但如果一个进程正在更新(写) 数据库, 则所有的其他进程都不能访问该数据库, 即使读操作也不行。 这里的问题是如何对读者和写者进行编程?
若干数量级的延迟。 另一方面, 在使用内核级线程时, 一旦线程阻塞在I/O上就不需要像在用户级线程中那样将整个进程挂起。
另一个重要因素是用户级线程可以使用专为应用程序定制的线程调度程序。
2.5 经典的IPC问题
2.5.1 哲学家就餐问题
五个哲学家围坐在一张圆桌周围, 每个哲学家面前都有一盘通心粉。 由于通心粉很滑, 所以需要两把叉子才能夹住。 相邻两个盘子之间放有一把叉子, 餐桌如下图所示。
[外链图片转存中…(img-Cn3HphoL-1722443152881)]
2.5.2 读者一写者问题
例如, 设想一个飞机订票系统, 其中有许多竞争的进程试图读写其中的数据。 多个进程同时读数据库是可以接受的, 但如果一个进程正在更新(写) 数据库, 则所有的其他进程都不能访问该数据库, 即使读操作也不行。 这里的问题是如何对读者和写者进行编程?
在一个读者到达, 且一个写者在等待时, 读者在写者之后被挂起, 而不是立即允许进入。