操作系统之进程管理
Posted sasworld
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了操作系统之进程管理相关的知识,希望对你有一定的参考价值。
进程
基本概念
为了保证多道程序设计系统中程序能够正确地运行,引入进程概念用于更好地控制和管理程序的执行。进程包括程序但不只是程序本身,它还包括程序运行过程中的一些状态数据信息以及描述特定进程的数据结构PCB(Process Control Block)。操作系统通过PCB来感知进程的存在,并根据PCB来控制管理进程的运行。
进程状态
进程是程序的一次动态执行过程,不同时刻进程可能处于不同的状态。这些状态通常包含以下几种:
新的(创建):当系统有新的作业到达,在系统资源满足的条件下操作系统为作业创建相应的进程,分配资源并创 建PCB。新的进程将被放入就绪队列等待CPU调度。
运行:进程获取所需的所有资源包括CPU,开始在CPU上执行程序指令。
等待:当进程需要等待某个耗时事件发生时(如I/O请求等),为了提高CPU的利用率,操作系统会让进程让出 CPU并转到相应的等待事件队列中。
就绪:进程等待CPU调度的状态,此时的进程已经获取到所需的除处理机外的所有资源。
终止:进程执行完成,操作系统会释放进程占用的所有资源,并清除进程的PCB。
进程控制块
进程控制块(PCB)记录了进程的相关信息,是进程存在的唯一标志,操作系统通过PCB感知和控制进程。PCB中通常包含以下信息:
- 进程状态:如上。
- 程序计数器:表示进程将要执行的下个指令的地址。
- CPU寄存器:进程运行时的上下文数据信息,用于恢复进程的运行环境。
- CPU调度信息:进程的优先级、调度队列的指针和其它调度参数。
- 内存管理信息:进程在内存中的分布信息。
- 记账信息:CPU时间、实际使用时间、时间期限、记账数据、作业或进程数量等。
- I/O状态信息:分配给进程的I/O设备列表、打开文件列表等。
线程
线程可以看作是对进程更细粒度的划分,在支持线程的系统中,线程是CPU调度的基本单位。同一个进程中可以只包含一个线程也可能同时包含多个线程,多个线程之间可以并发执行。同一进程中的多个线程共享进程的资源,线程本身只拥有少数的堆栈数据。
进程调度
在单处理器系统中,同一时刻只能有一个进程执行,当进程让出CPU后需要从就绪队列中选择一个进程到CPU上执行,进程调度就是依据某种原则从就绪队列中选择进程到CPU上执行的过程。
调度队列
进程被创建后会被加入到就绪队列中,调度程序从就绪队列中选择就绪的进程执行。就绪队列通常由含头节点的链表实现:其头节点有两个指针,用于指向链表的第一个和最后一个PCB块,每个PCB块包含一个指向就绪队列中下一个PCB块的指针。
调度程序
- 长期调度程序(作业调度程序)用于将外存中的作业加载到内存并创建进程以便执行;
- 中期调度程序用于将内存中长期不运行的进程暂时移到外存,等待满足执行条件时再重新调入内存,这种方式能够进一步提高内存的利用率;
- 短期调度程序(CPU调度程序)用于从就绪队列中选择进程到CPU上执行,该调度发生的频率最高。
上下文切换
在多道程序设计系统中,通常情况下多个进程会并发执行,在系统更换运行的进程之前需要将当前进程的运行环境保存到PCB中以便下次运行时恢复,使进程能够继续运行。进程的环境信息通常包括CPU寄存器的值、进程状态和内存管理信息等。
切换CPU到另一个进程需要保存当前进程状态并恢复另一个进程的状态,这个任务称为上下文切换。
进程间通信
基于共享内存的通信方式
采用共享内存的通信方式,需要通信进程建立共享内存区域,该共享内存区域驻留在创建共享内存段的进程地址空间内。其它希望使用这个共享内存段进行通信的进程需要将其附加到自己的地址空间。
基于消息传递的通信方式
消息传递通过在通信进程间创建通信链路来实现通信,消息传递工具需要提供至少两种操作:
send(message)
receive(message)
通信链路的逻辑实现有有以下几种方法:
直接或间接地通信
- 同步或异步的通信
自动或显式的缓冲
直接通信
采用直接通信的方式,每个进程必须明确指定通信的接收者和发送者。那么原语send()和receive()需定义如下:
// 寻址方式具有对称性,通信双方必须直接指定对方
send(P, message); // 向进程P发送message
receive(Q, message); // 从进程Q接收message
// 非对称寻址方式,直需要发送者指定接收者,接收者不需要指定发送者
send(P, message); // 向进程P发送message
receive(id, message); // 从任何进程接收message,这里id设置为与之通信的进程的名称
这种通信链路的属性:
- 在需要通信的每队进程之间自动建立链路,进程只需要知道对方的身份就可以进行交流
- 每个链路只与两个进程相关
- 每队进程之间只有一个链路
直接通信缺点:在发送和接收原语中直接绑定了特定的进程,修改进程的标识符需要分析所有进程,并修改所有对旧标识符的引用。
间接通信
进程间通过邮箱或端口交换消息,每个邮箱都有唯一的标识符,进程可以向邮箱放入消息也可以从中删除消息。通信的两个进程需要共享同一个邮箱来实现通信。那么原语send()和receive()需定义如下:
send(A, message); // 向邮箱A发送message
receive(A, message); // 从邮箱A接收message
这种通信链路的属性:
- 只有在两个进程共享一个邮箱时才能建立通信链路
- 一个链路可以与两个或更多进程相关联,也就是邮箱可以被所有进程使用
- 两个通信进程之间可有多个不同的链路,每个链路对应于一个邮箱,不是一直只用某一个邮箱通信
邮箱的拥有者:
邮箱为进程拥有:此时邮箱是进程地址空间的一部分,使用时需要区分邮箱的所有者(只能从邮箱接收消息)和使用者(只能向邮箱发送消息),邮箱会随着进程的终止而消失。
邮箱由操作系统维护:创建邮箱的进程默认为邮箱的所有者,操作系统提供系统调用可以更改邮箱的拥有权和接收特权。此时操作系统需要提供满足进程如下操作的机制:
- 创建新的邮箱
- 通过邮箱发送和接收消息
- 删除邮箱
同步
进程间通过调用原语send()和receive()通信,这些原语有不同的设计方案。消息传递可以是阻塞或非阻塞,也称为同步或异步:
- 阻塞发送:发送进程阻塞,直到消息被接收进程或邮箱所接收
- 非阻塞发送:发送进程发送消息,并且恢复操作,进程继续执行
- 阻塞接收:接收进程阻塞,直到由消息可用
- 非阻塞接收:接收进程收到一个有效消息或空消息,进程继续执行
多线程编程
多线程编程的优点:
- 响应性:通过多线程可以为部分阻塞或冗长操作开辟线程执行,而主程序可以继续执行,从而不影响对用户的其它操作的做出响应
- 资源共享:同一进程中的线程共享进程的所有资源,线程切换不需要改变运行环境
- 经济:由于线程能够共享它们所属进程的资源,所以创建和切换线程更加经济
- 可伸缩性:在多核处理器体系结构上,多个线程可以同时运行
并行性和并发性
并发性是指多个进程或线程可以在同一个CPU上轮流执行,但任意时刻都只能有一个进程或线程执行。而并行则是指多个进程或线程同时运行。
并行类型
- 数据并行:注重将数据分布于多个计算核上,并在每个核上执行相同的操作
- 任务并行:将任务而不是数据分配到多个计算核,每个线程都执行一个独特的操作,不同的线程可以操作相同的数据也可以操作不同的数据
多线程模型
用户线程
用户线程位于内核之上,它的管理由程序控制,无需内核的支持。
内核线程
内核线程是由操作系统直接支持和管理的。
用户线程和内核线程之间的关系
- 多对一模型:多个用户线程对应一个内核线程,线程的运行由程序管理,运行效率高。但是如果任何一个线程执行阻塞的系统调用都会阻塞整个进程。
- 一对一模型:每个用户线程都有一个对应的内核线程支持,一个线程执行阻塞的系统调用时并不影响其它线程的执行。但是过多的内核线程的创建和切换的开销较大,影响应用程序的性能。
- 多对多模型:多个内核线程共享多个内核线程,这种方式既避免一个线程的阻塞调用影响其它线程的执行,也较少了系统开销。
- 双层模型(混合模型):这种模型中既包含多对多模型也包含一对一模型。
线程库
线程库为程序员提供创建和管理线程的API,实现线程库的主要方法有以下两种:
- 在用户空间中提供一个没有内核支持的库,调用库中的函数都是对用户空间的本地函数调用,不是系统调用。
- 实现由操作系统直接支持的内核级的一个库,库内代码和数据结构位于内核空间,调用库中的函数会导致对内核的系统调用。
主要线程库
- POSIX Pthreads:可以提供用户级或内核级的库
- Windows线程库:用于Windows操作系统的内核级线程库
- Java 线程API:用于在Java程序中直接创建和管理线程,由于Java本身运行在JVM上,Java线程库的底层实现是依赖于JVM的运行平台的。
多线程的创建策略
异步线程
一旦父进程创建了一个子线程后,父线程就恢复自身的执行,父线程与子线程会并发执行。
同步线程
父线程创建一个或多个子线程之后,在恢复之前需要等待所有子线程的终止,通常是由于父线程需要使用子线程返回的数据。
线程池的优点
- 用现有的线程服务请求比等待创建一个线程更快
- 线程池限制了任何时候可用线程的数量,这对不能支持大量并发线程的系统非常重要
- 将要执行任务从创建任务的机制中分离出来,允许我们采用不同的策略运行任务
进程调度
在多道程序设计系统中,一旦CPU处于空闲状态就需要从就绪队列中选择一个进程执行,从而提高CPU的利用率。进程的选择采用短期调度程序(CPU调度程序),调度程序从内存中选择一个能够执行的进程,并为其分配CPU。
抢占和非抢占式调度
需要进行CPU调度的情况:
- 当一个进程从运行状态切换到等待状态时(进程需要进行一些耗时请求,如I/O请求,此时需要进程放弃CPU的使用权)
- 当一个进程从运行状态切换到就绪状态时(当调度采用轮转调度时,时间片到或者系统出现中断时)
- 当一个进程从等待状态切换到就绪状态时(等待进程请求的事件发生)
- 当一个进程终止时
如果调度只能发生在第一种和第4种情况下,则此类调度方案称为非抢占的;否则,调度方案称为抢占的。在非抢占调度下,一旦某个进程分配到CPU,进程就会一直使用CPU,直到终止或者切换到等待状态。而在抢占调度下,系统会根据相应的调度策略(优先级或短作业优先等)在上述四种情况下进行调度,不需要等到运行进程终止或者进入等待状态,调度具有强制性。
调度准则
不同的调度准则具有不同的偏向性,CPU调度算法的设计可以依据不同的准则,那么在不同的场景中就可以选择不同的CPU调度算法以适应场景需求。常见的准则包括:
- CPU使用率:CPU属于计算机的稀缺资源,因此应尽可能提高CPU的利用率;
- 吞吐量:吞吐量是指系统在单位时间内完成的任务数量,系统吞吐量越大越好;
- 周转时间:从进程提交到进程完成所经历的时间称为周转时间,周转时间应尽可能短;
- 等待时间:等待时间是进程在就绪队列中等待CPU所花的时间总和,等待时间应尽可能短;
- 响应时间:响应时间是指从作业提交到产生第一次响应的时间,响应时间越快越好;
调度算法
先到先服务调度算法(First-Come First-Served,FCFS)
先到先服务调度算法是非抢占的,系统调度顺序按照进程进入内存的时间顺序。一旦进程获得CPU,该进程会一直使用CPU直到进程终止或进入等待状态,进程从等待状态变为就绪状态时会被放到就绪队列的最后。
FCFS特点
- FCFS算法对于I/O密集的进程来说不友好,因为每次请求I/O后,进程都需要重新进入就绪队列排队等待调度;
- FCFS算法有利于长作业,不利于短作业;
最短作业优先调度算法(Shortest-Job-First,SJF)
最短作业优先调度算法可以是抢占的也可以是非抢占的,当采用可抢占策略时,每当有新的作业进入就绪队列,系统都需要将其与正在运行的进程的剩余运行时间进行对比,并选择所需时间较小的进程运行。对于非抢占策略则继续运行正在执行的进程。
SJF特点
- 可能导致饥饿现象的发生,例如系统持续进入较短的作业时,内存中的长作业就可能长期不被调度运行;
- SJF有利于短作业,不利于长作业;
优先级调度算法(priority-scheduling)
与SJF调度算法类似,既有抢占式又有非抢占式的。调度的原则是依据进入就绪队列的进程的优先级,SJF调度算法中的作业运行时间就可以看作是优先级的一种延伸。
特点
- 与SJF类似,同样有可能出现饥饿现象;
轮转调度(Round-Robin,RR)
该调度算法通过将处理机时间进行分片,每个进程轮流运行一个时间片。这种调度算法能够尽快的使所有进程都产生响应。
RR特点
- 进程的响应时间与就绪队列中的进程数量和时间片大小相关;
- 应选择合适的时间片大小,当时间片过小时,频繁的进程切换会导致系统开销过大;当时间片过大时,就绪队列后面的进程响应时间过大,当时间片大到一定程度时RR就变成FCFS调度。
多级队列调度算法
将系统中的进程划分到不同类型的进程队列中,不同的进程队列有不同的调度需求。进行调度时根据实际需求设定多个调度队列的顺序,系统完成前面队列中的进程后依次向后调度。
多级反馈队列调度算法
系统中维护多个进程队列,系统允许进程在多个队列之间迁移。
影响多级反馈队列调度程序的参数
- 队列数量
- 每个队列的调度算法
- 用以确定何时升级到更高优先级队列的方法
- 用以确定何时降级到更低优先级队列的方法
- 用以确定进程在需要服务时将会进入哪个队列的方法
进程同步
共享数据的并发访问可能导致数据的不一致,为了保证进程的正确执行,操作系统需要提供一定的机制以便确保共享同一逻辑地址空间的协作进程的有序执行,从而维护数据的一致性。
背景
处理器在每完成一个机器指令后会检查是否有中断请求,一个进程在它的指令流上的任何一点都可能会被中断,并且处理器可能会用于执行其他进程的指令。在这个过程中如果进程使用的共享资源被其它进程修改,那么进程再次运行时会得到不确定的结果,因此操作系统需要对使用同一资源的多个进程的运行加以控制。
竞争条件:多个进程并发访问和操作同一数据并且执行结果与特定访问顺序有关,称为竞争条件。
临界区问题
每个进程中用于操作或修改共享资源的一段代码称为临界区,任何时候都只允许一个进程进入临界区内执行。在进程进入临界区前需要请求许可,实现这一请求的代码区段称为进入区。临界区之后可以有退出区,其余代码称为剩余区。
临界区问题的解决方案应满足的要求
- 互斥:保证任何时候只有一个进程在临界区内执行,遵循忙则等待原则;
- 进步:如果没有进程在临界区内执行,那么就应该让需要进入临界区的进程立即进入,遵循空闲让进原则;
- 有限等待:系统应保证所有进程等待有限的次数,防止出现饥饿现象。另外当进程长时间无法进入临界区时,进程应该主动让出处理机,防止出现忙等,降低处理机的利用率,也就是遵循让权等待原则。
Peterson解决方案
该方案是一个经典的基于软件的临界区问题的解决方案,Peterson方案要求两个进程共享两个数据项:
int turn; // 表示轮到哪个进程可以进入临界区
boolean flag[2]; // 表示进程是否准备好进入临界区
进程P~i~代码结构
do {
flag[i] = true; // 准备进入临界区
turn = j; // 将进入的机会设置为其它进程,由于turn为共享变量,该语句执行后turn的值可能为j也可能为i
while(flag[j] && turn == j); // 如果没有进程准备好,那么当前进程可直接进入临界区;如果有进程准 备好,那么就需要看turn的值来决定哪个进入临界区
临界区;
flag[i] = false; // 退出临界区时,关闭当前进程的准备状态,给进程P_j进入临界区的机会;如果P_j没 有抓住这个机会,由于循环flag[i]会重新变为true,不过此时P_i会设置turn=j, 进程P_j同样能够进入临界区
剩余区;
} while(true);
硬件同步
对于单处理器环境,在修改共享变量时只要禁止中断出现,就能确保当前指令流可以有序执行,且不会被抢占。由于不可能执行其它指令,所以共享变量不会被意外修改。
现代系统中提供了特殊硬件指令,用于检测和修改字的内容,或者用于原子地交换两个字。使用这些指令可以解决临界区问题。这些指令的抽象定义如下:
test_and_set()指令
// 检测传入的目标的值,并返回目标值
boolean test_and_set(boolean *target) {
boolean rv = *target; // 保存传入的目标值并返回
*target = true; // 无论目标值是什么都将目标值置为true,那么除了最先修改目标值的进程得到的返回结果 为false意外,其余进程得到的结果均为true
return rv;
}
采用test_and_set()实现互斥进入临界区
do {
while(test_and_set(&lock)); // lock初始为false,一旦进程修改lock后,其余进程的检测结果均为 true,进程会进入自旋状态
临界区;
lock = false; // 退出临界区时释放lock
剩余区;
}while(true);
compare_and_swap()指令
// 该指令通过比较期望值与真实值来修改真实值,如果与预期一致表示共享变量没有被修改,那么此时可以交换值
int compare_and_swap(int *value, int expected, int new_value) {
int temp = *value;
if(*value == expected)
*value = new_value;
return temp;
}
采用compare_and_swap()指令实现互斥进入临界区
do {
while(compare_and_swap(&lock, 0, 1) != 0); // lock初始为0,如果lock未被修改,则修改lock为 1,并返回0,进程进入临界区
临界区;
lock = 0; // 退出临界区时释放lock
剩余区;
}while(true);
信号量
信号量是一个整型变量,在信号量机制中,除了初始化外只能通过两个标准的原子操作修改信号量的值:wait()和signal()。操作wait()称为P操作,一般用于增加信号量的值;操作signal()称为操作,一般用于减少信号量的值。信号量分为计数信号量和二进制信号量。
计数信号量
计数信号量的值不确定,设置初始值时通常用来表示系统中可用资源的数量。随着系统的运行,每有一个进程申请该资源时,计数信号量就减1,当信号量为负数时其绝对值表示等待资源的进程数量。
二进制信号量
二进制信号量的值只能为0或1,因此二进制信号量可当作互斥锁使用,每次只能有一个进程获取到该信号量,其余进程则必须等待。
信号量的实现
当进程执行P操作时,如果发现信号量不为正数时,进程需要等待。此时如果让进程处于自旋状态会浪费处理机资源,因此应该让进程阻塞自己并进入到与信号量相关的等待队列中,当有其它进程调用V操作释放信号量时在从相应的等待队列中唤醒一个进程获取信号量并进入就绪状态等待运行。PV操作分别定义如下:
wait(semaphore *S) {
S->value--; // 消耗一个信号量
if(S->value < 0) { // 小于0则说明信号量已经用完,那么进程就应该进入相应的等待队列中并调用阻塞原语 阻塞自己
add this process to S->list;
block();
}
}
wait(semaphore *S) {
S->value++; // 释放一个信号量
if(S->value <= 0) { // 释放一个信号量后仍小于或等于0则说明系统中有等待该信号量的进程,那么就应该 从该信号量的等待队列中唤醒一个进程
remove a process P from S->list;
wakeup(P);
}
}
经典同步问题
读者-写着问题
在读者-写者问题中,只对共享数据进行读取的进程为读者进程,修改共享数据的进程称为写者进程。多个读者可同时读取共享数据而不会导致出现错误,但是任何时刻多个写者进程不能同时修改数据,写者进程和读者进程也不能同时访问共享数据。
实现读者-写者同步,需要用到以下共享变量:
semaphore rw_mutex = 1; // 读者与写者互斥访问共享数据的互斥信号量
semaphore mutex = 1; // 多个读者进程互斥修改当前读者进程数量的信号量
int read_count = 0; // 系统当前读者进程数量
写者进程结构
do {
wait(rw_mutex);
...
/* 修改共享数据 */
...
signal(rw_mutex);
}while(true);
读者进程结构
do {
wait(mutex); // 获取修改读者进程数量的互斥信号量,该操作在请求rw_mutex之前,防止出现死锁
read_count++;
if(read_count == 1) // 判断当前是否为第一个读者进程
wait(rw_mutex); // 如果是就需要请求访问共享数据的互斥信号量
signal(mutex); // read_count修改后释放信号量
...
/* 读取数据 */
...
wait(mutex); // 获取修改读者进程数量的互斥信号量
read_count--;
if(read_count == 0) // 判断当前进程是否为最后一个读者进程
signal(rw_mutex); // 如果是则释放共享数据的互斥信号量,以允许写者进程操作共享数据
signal(mutex);
}while(true);
哲学家就餐问题
哲学家进餐问题是在多进程之间分配多个资源,并保证不会出现死锁和饥饿现象的例子,共享数据为
semaphore chopstick[5]; // 五支筷子的互斥信号量
哲学家i的结构
do {
wait(chpostick[i]); // 拿起左边的筷子
wait(chpostick[(i+1) % 5]); // 拿起右边的筷子
/*进餐*/
signal(chopstick[i]);
signal(chopstick[(i+1) % 5]);
/*思考*/
}while(true);
上述进程结构可能会导致死锁,5个哲学家可能同时拿起左边的筷子,为了解决死锁问题可以考虑以下措施:
- 允许最多4个哲学家同时坐在桌子上;
- 只有一个哲学家的两根筷子都可用时,才允许他拿起筷子;
- 使用非对称解决方案,即单号的哲学家先拿起左边的筷子,接着右边的筷子;而双号的哲学家先拿起右边的筷子,接着左边的筷子;
死锁
如果进程所申请的资源被其它等待进程占有,那么该进程有可能再也无法改变状态,这种情况称为死锁。出现死锁时至少涉及两个进程,在无外力作用下,死锁状态将无法解除。
死锁发生的四个必要条件
- 互斥:至少有一个资源必须处于非共享模式,即一次只有一个进程可使用。如果资源都可以共享使用就不会发生死锁;
- 占有并等待:一个进程应占有至少一个资源,并等待另一个资源,而该资源被其它进程所占有;
- 非抢占:资源不能被抢占,即资源只能被进程在完成任务后资源释放,如果可抢占,那么总是有进程能够执行,就不会发生死锁;
- 循环等待:多个进程互相等待其它进程占用的资源,并形成环状等待;
死锁处理方法
- 通过协议来预防或避免死锁,确保系统不会进入死锁状态;
- 允许系统进入死锁状态,然后检测并解除;
- 直接忽视死锁,认为死锁不可能在系统内发生;
死锁预防
通过破坏死锁发生的四个必要条件之一就可以预防死锁的发生。
互斥
通常不能通过破会互斥条件唉预防死锁,互斥是操作系统的重要特性需要加以保护。
占有并等待
进程在申请资源时一次性申请所有资源,那么就不会出现占有并等待资源的情况,但是这样会降低资源的利用率。
非抢占
当资源变为可抢占时,进程会从其它进程占有的资源中直接剥夺,从而保证进程的执行。
循环等待
通过对系统中的所有资源进行编号,进程在申请资源时必须按照顺序获取。如果没有获取到前面的资源系统就不会分配后面的资源给其它进程,从而保证至少有一个进程可以执行。
死锁避免
安全状态
如果系统能够按照一定的顺序为每个进程分配资源,仍然避免死锁,那么系统的状态就是安全的。也就是说如果系统中存在一个进程执行的顺序使得所有进程都能执行完成,那么这个序列称为安全序列,存在安全序列的系统就处于安全状态。
安全状态不是死锁状态,死锁状态是非安全状态,不是所有的非安全状态都能导致死锁状态,只是非安全状态可能导致死锁。
银行家算法
银行家算法用于判断系统内是否存在安全序列,算法中包括:
- 当前系统中各资源可用数量Available;
- 各进程对各资源的最大需求量Max;
- 各进程已经获取到的资源数Allocation;
- 各进程还需各种资源的数量Need,可有Max-Allocation获取;
该算法思路是通过不断迭代,搜索当前系统中的剩余资源能够满足的进程,并进行分配。进程执行完成会释放资源,算法进行新一轮的搜索,知道所有进程都执行完成,那么就能够找到一个安全序列。
死锁检测与解除
检测
死锁检测算法可通过化简系统资源分配图,对于系统能够满足运行条件的进程,删去其在资源分配图中对应的资源申请和资源分配边。如果所有的边都能够消除,则表明系统中没有死锁,否则系统中存在死锁。
解除
进程终止:(1) 终止所有死锁进程;(2) 一次终止一个进程直到消除死锁为止;
资源抢占:不断地抢占一些进程地资源以便给其它进程使用,直到死锁循环被打破为止;
以上是关于操作系统之进程管理的主要内容,如果未能解决你的问题,请参考以下文章
(王道408考研操作系统)第二章进程管理-第三节8:经典同步问题之吸烟者问题