笔记篇:操作系统第二章 进程管理
笔记篇:操作系统第二章 进程管理
目录
- 笔记篇:操作系统第二章 进程管理
- 2.1 进程的基本概念
- 2.1.1 程序的顺序执行及其特征
- 2.1.2 前驱图
- 2.1.3 程序的并发执行及其特征
- 2.1.4 进程的特征与状态
- 2.5.1 进程控制块(PCB)
- 2.2 进程控制
- 2.2.1 进程的创建
- 2.2.2 进程的终止
- 2.2.3 进程的阻塞与唤醒
- 2.2.4 进程的挂起与激活
- 2.3 进程同步
- 2.3.1 进程同步的基本概念
- 2.3.2 信号量机制
- 2.3.3 信号量的应用
- 2.4 经典进程的同步问题
- 2.4.1 生产者—消费者问题
- 2.4.2 哲学家进餐问题
- 2.4.3 读者-写者问题
- 2.5 管程机制
- 2.5.1 管程的基本概念
- 2.5.2 利用管程解决生产者-消费者问题
- 2.6 进程通信
- 2.6.1 进程通信的类型
- 2.6.2 消息传递通信的实现方法
- 2.6.3 消息传递系统实现中的若干问题
- 2.6.4 消息缓冲队列通信机制
- 2.7 线程
- 2.7.1 线程的基本概念
- 2.7.2 线程间的同步和通信
- 2.7.3 内核支持线程和用户级线程
- 2.7.4 线程控制
2.1 进程的基本概念
2.1.1 程序的顺序执行及其特征
程序的顺序执行
仅当前一操作(程序段)执行完后,才能执行后继操作。
例如,在进行计算时,总须先输入用户的程序和数据,然后进行计算,最后才能打印计算结果。
- 程序顺序执行时的特征
- 顺序性:指处理机严格地按照程序所规定的顺序执行, 即每一操作必须在下一个操作开始之前结束;
- 封闭性: 指程序在封闭的环境下运行, 即程序运行时独占全机资源, 资源的状态(除初始状态外)只有本程序才能改变它, 程序一旦开始执行, 其执行结果不受外界因素影响
- 可再现性:指只要程序执行时的环境和初始条件相同, 当程序重复执行时, 不论它是从头到尾不停顿地执行, 还是“停停走走” 地执行, 都可获得相同的结果。
2.1.2 前驱图
前趋图(Precedence Graph)是一个有向无循环图
,记为DAG(Directed Acyclic Graph),用于描述进程之间执行的前后关系。
图中的每个结点
可用于描述一个程序段或进程,乃至一条语句;结点间的有向边则用于表示两个结点之间存在的偏序(Partial Order)或前趋关系(Precedence Relation)。
进程(或程序)之间的前趋关系可用“→” 来表示,
→={(Pi, Pj)|Pi must complete before Pj may start}, 如果(Pi, Pj)∈→,可写成Pi→Pj
,称Pi是Pj的直接前趋
,而称Pj是Pi的直接后继
。
在前趋图中,把没有前趋的结点称为初始结点
(Initial Node),把没有后继的结点称为终止结点
(Final Node)。
每个结点还具有一个重量(Weight),用于表示该结点所含有的程序量或结点的执行时间。
对于图 2-2(a)所示的前趋图, 存在下述前趋关系:
P1→P2, P1→P3, P1→P4, P2→P5, P3→P5, P4→P6, P4→P7, P5→P8, P6→P8, P7→P9, P8→P9
或表示为:
P={P1, P2, P3, P4, P5, P6, P7, P8, P9}
→={ (P1, P2), (P1, P3), (P1, P4), (P2, P5), (P3, P5), (P4, P6), (P4, P7),(P5, P8), (P6, P8), (P7, P9), (P8, P9)}
应当注意,前趋图中必须不存在循环
,但在图2-2(b)中却有着下述的前趋关系:
S2→S3, S3→S2
即,S3运行之前,S2要运行完了,但S2运行之前,S3也需要运行完才可,这显然是不可能实现的。
2.1.3 程序的并发执行及其特征
- 程序的并发执行
在该例中存在下述前趋关系:
Ii→Ci,Ii→Ii+1, Ci→Pi, Ci→Ci+1,Pi→Pi+1
而Ii+1和Ci及Pi-1是重叠的,亦即在Pi-1和Ci以及Ii+1之间,可以并发执行。
对于具有下述四条语句的程序段:
S1: a∶=x+2
S2: b∶=y+4
S3: c∶=a+b
S4: d∶=c+b
我们可以画出下面的前趋图
可以看出: S3必须在a和b被赋值后方能执行: S4必须在S3之后执行; 但S1和S2则可以并发执行, 因为它们彼此互不依赖。
- 程序并发执行时的特征
间断性
相互制约将导致并发程序具有“执行——停止——执行” 这种间断性的活动规律。失去封闭性
当系统中存在着多个可以并发执行的程序时,系统中的各种资源将为它们所共享,而这些资源的状态也由这些程序来改变,致使其中任一程序在运行时,其环境都必然会受到其它程序的影响。例如, 当处理机已被分配给某个进程运行时,其它程序必须等待。显然,程序的运行已失去了封闭性。不可再现性
程序在并发执行时, 由于失去了封闭性, 也将导致其又失去可再现性。例如,有两个循环程序A和B,它们共享一个变量N。程序A每执行一次时,都要做N=N+1操作;程序B每执行一次时, 都要执行Print(N)操作,然后再将N置成“0”。程序A和B以不同的速度运行。 则结果可能有:(1) N=N+1在Print(N)和N=0之前,此时得到的**N值**分别为n+1, n+1, 0。(2) N=N+1在Print(N)和N=0之后,此时得到的**N值**分别为n, 0, 1。(3) N=N+1在Print(N)和N=0之间,此时得到的**N值**分别为n, n+1, 0。
由上可以看出,**程序的不能随便执行并发控制,因为在多道程序环境下, 程序的执行属于并发执行, 此时它们将失去其封闭性, 并具有间断性, 以及其运行结果不可再现性的特征。 **
由此, 决定了通常的程序是不能参与并发执行的, 否则, 程序的运行也就失去了意义。 为了能使程序并发执行, 并且可以对并发执行的程序加以描述和控制, 人们引入了 “进程” 的概念。
2.1.4 进程的特征与状态
- 进程的定义
对于进程的定义, 从不同的角度可以有不同的定义, 其中较典型的定义有:
进程是程序的一次
执行
进程是一个程序及其数据在处理机上
顺序执行
时所发生的活动
进程是程序在一个数据集合上运行的
过程
,它是系统进行资源分配和调度的一个独立单位
。为了使参与并发执行的每个程序(含数据)都能独立地运行, 在操作系统中必须为之配置一个专门的数据结构, 称为进程控制块(Process Control Block, PCB)。
系统利用PCB来描述进程的基本情况和活动过程, 进而控制和管理进程。 这样, 由程序段、 相关的数据段和PCB三部分便构成了
进程实体(又称进程映像)
。一般情况下, 我们把进程实体就简称为进程, 例如, 所谓创建进程, 实质上是创建进程实体中的PCB;而撤消进程, 实质上是撤消进程的PCB。
在引入了进程实体的概念后, 我们可以把传统OS中的进程定义为: “
进程是进程实体的运行过程, 是系统进行资源分配和调度的一个独立单位。
”
进程的特征
进程和程序是两个截然不同的概念, 除了进程具有程序所没有的PCB结构外, 还具有下面一些特征:- 动态性
- 并发性
- 独立性
- 异步性
进程的三种基本状态
- 就绪(Ready)状态
- 执行状态
- 阻塞状态
创建态和终止态
创建状态:首先由进程申请一个空白PCB,并向PCB中填写用于控制和管理进程的信息;然后为该进程分配运行时所必须的资源: 最后, 把该进程转入就绪状态并插入就绪队列之中。对于处于创建状态的进程, 当其获得了所需的资源以及对其PCB的初始化工作完成后, 便可由创建状态转入就绪状态
终止状态:
首先, 是等待操作系统进行善后处理, 最后将其PCB清零, 并将PCB空间返还系统。当一个进程到达了自然结束点, 或是出现了无法克服的错误, 或是被操作系统所终结, 或是被其他有终止权的进程所终结, 它将进入终止状态。
进入终止态的进程以后不能再执行, 但在操作系统中依然保留一个记录, 其中保存状态码和一些计时统计数据, 供其他进程收集。
一旦其他进程完成了对其信息的提取之后, 操作系统将删除该进程, 即将其PCB清零, 并将该空白PCB返还系统。
挂起操作
引入挂起操作的原因
- 终端用户的请求。
- 父进程请求。
- 负荷调节的需要。
- 操作系统的需要。
引入挂起原语操作后三个进程状态的转换
在引入挂起原语Suspend和激活原语Active后, 在它们的作用下, 进程将可能发生以下几种状态的转换:
活动就绪→静止就绪。
当进程处于未被挂起的就绪状态
时, 称此为活动就绪状态
,表示为Readya,此时进程可以接受调度
。 当用挂起原语Suspend将该进程挂起
后, 该进程便转变为静止就绪状态
, 表示为Readys,处于Readys状态的进程不再被调度执行
。活动阻塞→静止阻塞。
当进程处于未被挂起的阻塞状态
时, 称它是处于活动阻塞状态
, 表示为Blockeda。 当用Suspend原语将它挂起后
, 进程便转变为静止阻塞状态
,表示为Blockeds。处于该状态的进程在其所期待的事件出现后, 它将从静止阻塞变为静止就绪Readys状态静止就绪→活动就绪。
处于Readys状态的进程若用激活原语Active激活后, 该进程将转变为Readya状态。静止阻塞→活动阻塞。
处于Blockeds状态的进程若用激活原语Active激活后,进程将转变为Blockeda状态
引入挂起操作后五个进程状态的转换
NULL-创建: 一个新进程产生时, 该进程处于创建状态。
创建→活动就绪: 在当前系统的性能和内存的容量均允许的情况下, 完成对进程创建的必要操作后, 相应的系统进程将进程的状态转换为活动就绪状态。
创建→静止就绪: 考虑到系统当前资源状况和性能的要求, 不分配给新建进程所需资源,主要是主存,相应的系统将进程状态转为静止就绪状态,被安置在外存,不参与调度, 此时进程创建工作尚未完成。
执行→终止: 当一个进程己完成任务时, 或是出现了无法克服的错误, 或是被OS或是被其他进程所终结, 此时将进程的状态转换为终止状态。
2.5.1 进程控制块(PCB)
操作系统中用于管理控制的数据结构
在计算机系统中, 对于每个资源和每个进程都设置了一个数据结构, 用于表征其实体,我们称之为资源信息表
或进程信息表
, 其中包含了资源或进程的标识、 描述、 状态等信息以及一批指针。
通过这些指针, 可以将同类资源或进程的信息表, 或者同一进程所占用的资源信息表分类链接成不同的队列,便于操作系统进行查找。
OS管理的这些数据结构一般分为以下四类: 内存表、 设备表、 文件表和用于进程管理的进程表, 通常进程表又被称为进程控制块PCB
。
进程控制块的作用
进程控制块的作用是使一个在多道程序环境下不能独立运行的程序(含数据),成为一个能独立运行的基本单位,一个能与其它进程并发执行的进程。
或者说,OS是根据PCB来对并发执行的进程进行控制和管理的。
进程控制块中的信息
进程标识符
进程标识符用于惟一地标识一个进程。
一个进程通常有两种标识符:- 内部标识符
在所有的操作系统中,都为每一个进 程赋予一个惟一的数字标识符,它通常是一个进程的序号。 设置内部标识符主要是为了方便系统使用。 - 外部标识符
它由创建者提供,通常是由字母、数字组成,往往是由用户(进程)在访问该进程时使用。
为了描述进程的家族关系, 还应设置父进程标识及子进程标识。
此外,还可设置用户标识,以指示拥有该进程的用户。
- 内部标识符
处理机状态
处理机状态信息主要是由处理机的各种寄存器中的内容组成的。- ① 通用寄存器,又称为用户可视寄存器,它们是用户程序可以访问的,用于暂存信息, 在大多数处理机中,有 8~32 个通用寄存器,在RISC结构的计算机中可超过 100 个;
- ② 指令计数器,其中存放了要访问的下一条指令的地址;
- ③ 程序状态字PSW,其中含有状态信息,如条件码、执行方式、 中断屏蔽标志等;
- ④ 用户栈指针, 指每个用户进程都有一个或若干个与之相关的系统栈,用于存放过程和系统调用参数及调用地址。栈指针指向该栈的栈顶。
进程调度信息
在PCB中还存放一些与进程调度和进程对换有关的信息,包括:- 进程状态,指明进程的当前状态, 作为进程调度和对换时的依据;
- 进程优先级,用于描述进程使用处理机的优先级别的一个整数, 优先级高的进程应优先获得处理机;
- 进程调度所需的其它信息,它们与所采用的进程调度算法有关,比如,进程已等待CPU的时间总和、 进程已执行的时间总和等;
- 事件,是指进程由执行状态转变为阻塞状态所等待发生的事件,即阻塞原因。
进程控制信息
进程控制信息包括- 程序和数据的地址,是指进程的程序和数据所在的内存或外存地(首)址,以便再调度到该进程执行时,能从PCB中找到其程序和数据;
- 进程同步和通信机制,指实现进程同步和进程通信时必需的机制, 如消息队列指针、信号量等,它们可能全部或部分地放在PCB中;
- 资源清单,是一张列出了除CPU以外的、进程所需的全部资源及已经分配到该进程的资源的清单;
- 链接指针, 它给出了本进程(PCB)所在队列中的下一个进程的PCB的首地址。
- 进程控制块的组织方式
线性方式
即将系统中所有的PCB都组织在一张线性表中, 将该表的首址存放在内存的一个专用区域中。该方式实现简单、 开销小, 但每次查找时都需要扫描整张表, 因此只适合进程数目不多的系统。
PCB线性表示意图
链接方式
即把具有相同状态进程的PCB分别通过PCB中的链接字链接成一个队列。
这样, 可以形成就绪队列、 若干个阻塞队列和空白队列等。
索引方式
即系统根据所有进程状态的不同, 建立几张索引表, 例如, 就绪索引表、 阻塞索引表等, 并把各索引表在内存的首地址记录在内存的一些专用单元中。
2.2 进程控制
进程控制是进程管理中最基本的功能, 主要包括创建新进程、 终止已完成的进程、 将因发生异常情况而无法继续运行的进程置于阻塞状态、 负责进程运行中的状态转换等功能。
如当一个正在执行的进程因等待某事件而暂时不能继续执行时, 将其转变为阻塞状态,而在该进程所期待的事件出现后, 又将该进程转换为就绪状态等。 进程控制一般是由os的内核中的原语来实现的。
2.2.1 进程的创建
- 进程图
为了形象地描述一个进程的家族关系而引入了进程图(Process Graph)。 所谓进程图就是用于描述进程间关系的一棵有向树, 如图2-13所示。 图中的结点代表进程。
根结点A是这个进程家族的祖先。
- 引起创建进程的事件
- 用户登录。
- 作业调度。
- 提供服务。
- 应用请求。
- 进程的创建过程
- 申请空白PCB。
- 为新进程分配资源。
- 初始化进程控制块。
- 将新进程插入就绪队列,如果进程就绪队列能够接纳新进程,便将新进程插入就绪队列。
2.2.2 进程的终止
引起进程终止(Termination of Process)的事件
正常结束
在任何计算机系统中,都应有一个用于表示进程已经运行完成的指示。
例如,在批处理系统中,通常在程序的最后安排一条Holt指令或终止的系统调用。当程序运行到Holt指令时,将产生一个中断,去通知OS本进程已经完成。 在分时系统中,用户可利用Logs off去表示进程运行完毕, 此时同样可产生一个中断,去通知OS进程已运行完毕。
异常结束
在进程运行期间,由于出现某些错误和故障而迫使进程终止。
这类异常事件很多,常见的有:
① 越界错误。这是指程序所访问的存储区,已越出该进程的区域;
② 保护错误。进程试图去访问一个不允许访问的资源或文件,或者以不适当的方式进行访问,例如,进程试图去写一个只读文件;
③ 非法指令。程序试图去执行一条不存在的指令。出现该错误的原因,可能是程序错误地转移到数据区,把数据当成了指令;
④ 特权指令错。用户进程试图去执行一条只允许OS执行的指令;
⑤ 运行超时。进程的执行时间超过了指定的最大值;
⑥ 等待超时。进程等待某事件的时间, 超过了规定的最大值;
⑦ 算术运算错。进程试图去执行一个被禁止的运算,例如,被0除;
⑧ I/O故障。这是指在I/O过程中发生了错误等。外界干预
外界干预并非指在本进程运行中出现了异常事件,而是指
进程应外界的请求而终止运行
。
这些干预有:① 操作员或操作系统干预。 由于某种原因,例如,发生了死锁, 由操作员或操作系统终止该进程;
② 父进程请求。 由于父进程具有终止自己的任何子孙进程的权利, 因而当父进程提出请求时,系统将终止该进程;
③ 父进程终止。 当父进程终止时,OS也将他的所有子孙进程终止。
进程的终止过程
(1) 根据被终止进程的标识符,从PCB集合中检索出该进程的PCB,从中读出该进程的状态
。(2) 若被终止进程正处于执行状态,应立即终止该进程的执行,并
置调度标志为真
,用于指示该进程被终止后应重新进行调度。(3) 若该进程还有子孙进程,还应
将其所有子孙进程予以终止
,以防他们成为不可控的进程。(4) 将被终止进程所拥有的
全部资源,或者归还给其父进程, 或者归还给系统
。(5)
将被终止进程(它的PCB)从所在队列(或链表)中移出
, 等待其他程序来搜集信息。
2.2.3 进程的阻塞与唤醒
- 引起进程阻塞和唤醒的事件
- 请求系统服务
- 启动某种操作
- 新数据尚未到达
- 无新工作可做
进程阻塞过程
正在执行的进程,当发现上述某事件时,由于无法继续执行,于是进程便通过调用阻塞原语block把自己阻塞。
可见,
进程的阻塞是进程自身的一种主动行为
。进入block过程后,由于此时该进程还处于执行状态,所以应先立即停止执行,把进程控制块中的现行状态由“执行”改为阻塞,并将PCB插入阻塞队列。
如果系统中设置了因不同事件而阻塞的多个阻塞队列,则应将本进程插入到具有相同事件的阻塞(等待)队列。
最后,转调度程序进行重新调度,将处理机分配给另一就绪进程,并进行切换,亦即,保留被阻塞进程的处理机状态(在PCB中),再按新进程的PCB中的处理机状态设置CPU的环境。
进程唤醒过程
当被阻塞进程所期待的事件出现时,如I/O完成或其所期待的数据已经到达,则由有关进程(比如,用完并释放了该I/O设备的进程)调用唤醒原语wakeup( ),将等待该事件的进程唤醒。
唤醒原语执行的过程是:首先把被阻塞的进程从等待该事件的阻塞队列中移出,将其PCB中的现行状态由阻塞改为就绪,然后再将该PCB插入到就绪队列中。
2.2.4 进程的挂起与激活
进程的挂起
当出现了引起进程挂起的事件时,比如,用户进程请求将自己挂起,或父进程请求将自己的某个子进程挂起, 系统将利用挂起原语suspend( )将指定进程或处于阻塞状态的进程挂起。
挂起原语的执行过程是:
首先检查被挂起进程的状态,若处于活动就绪状态,便将其改为静止就绪;对于活动阻塞状态的进程,则将之改为静止阻塞。
为了方便用户或父进程考查该进程的运行情况而把该进程的PCB复制到某指定的内存区域。
最后,若被挂起的进程正在执行,则转向调度程序重新调度。
进程的激活过程
当发生激活进程的事件时,例如,父进程或用户进程请求激活指定进程,若该进程驻留在外存而内存中已有足够的空间时,则可将在外存上处于静止就绪状态的进程换入内存。
这时,系统将利用激活原语active( )将指定进程激活。
激活原语先将进程从外存调入内存,检查该进程的现行状态,若是静止就绪,便将之改为活动就绪;若为静止阻塞便将之改为活动阻塞。
假如采用的是抢占调度策略,则每当有新进程进入就绪队列时,应检查是否要进行重新调度,即由调度程序将被激活进程与当前进程进行优先级的比较,如果被激活进程的优先级更低,就不必重新调度;否则,立即剥夺当前进程的运行,把处理机分配给刚被激活的进程。
2.3 进程同步
为保证多个进程能有条不紊地运行, 在多道程序系统中, 必须引入进程同步机制。
2.3.1 进程同步的基本概念
进程同步机制的主要任务, 是对多个相关进程在执行次序上进行协调, 使并发执行的诸进程之间能按照一定的规则(或时序)共享系统资源, 并能很好地相互合作, 从而使程序的执行具有可再现性。
- 两种形式的制约关系
间接相互制约关系
多个程序在并发执行时, 由于共享系统资源, 如CPU、 I/O设备等, 致使在这些并发执行的程序之间形成相互制约的关系。 对于像打印机、 磁带机这样的临界资源, 必须保证多个进程对之只能互斥地访问, 由此, 在这些进程间形成了源于对该类资源共享的所谓间接相互制约关系。直接相互制约关系
某些应用程序, 为了完成某任务而建立了两个或多个进程。 这些进程将为完成同一项任务而相互合作。 进程间的直接制约关系就是源于它们之间的相互合作。例如, 有两个相互合作的进程— —输入进程A和计算进程B, 它们之间共享一个缓冲区。 进程A通过缓冲向进程B提供数据。 进程B从缓冲中取出数据, 并对数据进行处理。 但如果该缓冲空时,计算进程因不能获得所需数据而被阻塞。 一旦进程A把数据输入缓冲区后便将进程B唤醒;反之, 当缓冲区己满时, 进程A因不能再向缓冲区投放数据而被阻塞, 当进程B将缓冲区数据取走后便可唤醒A。
在多道程序环境下, 由于存在着上述两类相互制约关系, 进程在运行过程中是否能获得处理机运行与以怎样的速度运行, 并不能由进程自身所控制, 此即进程的异步性
。 由此会产生对共享变量或数据结构等资源不正确的访问次序, 从而造成进程每次执行结果的不一致。 这种差错往往与时间有关, 故称为“与时间有关的错误” 。 为了杜绝这种差错, 必须对进程的执行次序进行协调, 保证诸进程能按序执行。
- 临界资源(Critical Resouce)
生产者-消费者(producer-consumer)问题
描述
有一群生产者进程在生产产品,并将这些产品提供给消费者进程去消费。为使生产者进程与消费者进程能并发执行,在两者之间设置了一个具有n个缓冲区的缓冲池,生产者进程将它所生产的产品放入一个缓冲区中; 消费者进程可从一个缓冲区中取走产品去消费。尽管所有的生产者进程和消费者进程都是以异步方式运行的,但它们之间必须保持同步,即不允许消费者进程到一个空缓冲区去取产品;也不允许生产者进程向一个已装满产品且尚未被取走的缓冲区中投放产品。
我们可利用一个数组来表示上述的具有n个(0,1,…,n-1)缓冲区的缓冲池,用输入指针in来指示下一个可投放产品的缓冲区,每当生产者进程生产并投放一个产品后,输入指针加1;用一个输出指针out来指示下一个可从中获取产品的缓冲区,每当消费者进程取走一个产品后,输出指针加1。
由于这里的缓冲池是组织成循环缓冲的,故应把输入指针加1表示成 in=(in+1)%n;输出指针加1表示成out=(out+1)%n。当(in+1)%n=out时表示缓冲池满;而in=out则表示缓冲池空。此外,还引入了一个整型变量counter, 其初始值为0。每当生产者进程向缓冲池中投放一个产品后,使counter加1;反之,每当消费者进程从中取走一个产品时, 使counter减1。
生产者和消费者两进程共享下面的变量:int in=0, out=0, count=0; item buffer[n];
指针in和out初始化为0。
在生产者进程中使用一局部变量nextp,用于暂时存放每次刚生产出来的产品;而在消费者进程中,则使用一个局部变量nextc,用于存放每次要消费的产品。
void producer() {while(1){produce an item in nextp;...while (counter==n);buffer [in] = nextp;in = (in+1) % n;counter++;}};void consumer(){while(1){while (counter==0);nextc=buffer [out];out = (out+1) % n;counter--;consumer the item in nextc;...}};
虽然上面的生产者程序和消费者程序,在分别看时都是正确的,而且两者在顺序执行时其结果也会是正确的,但若并发执行时,就会出现差错,问题就在于这两个进程共享变量counter。生产者对它做加1操作,消费者对它做减1操作,这两个操作在用机器语言实现时, 常可用下面的形式描述:
假设:counter的当前值是5。如果生产者进程先执行左列的三条机器语言语句,然后消费者进程再执行右列的三条语句, 则最后共享变量counter的值仍为5;反之,如果让消费者进程先执行右列的三条语句,然后再让生产者进程执行左列的三条语句,counter值也还是5,但是,如果按下述顺序执行:
正确的counter值应当是5,但现在是4。倘若再将两段程序中各语句交叉执行的顺序改变, 将可看到又可能得到counter=6的答案,这表明程序的执行已经失去了再现性。为了预防产生这种错误, 解决此问题的关键是应把变量counter作为临界资源处理,亦即, 令生产者进程和消费者进程互斥地访问变量counter。
临界区(critical section)
由前所述可知, 不论是硬件临界资源还是软件临界资源, 多个进程必须互斥地对它进行访问。
人们把在每个进程中访问临界资源的那段代码称为临界区(critical section)。
显然,若能保证诸进程互斥地进入自己的临界区, 便可实现诸进程对临界资源的互斥访问。
为此,每个进程在进入临界区之前, 应先对欲访问的临界资源进行检査,看它是否正被访问。
如果此刻临界资源未被访问, 进程便可进入临界区对该资源进行访问, 并设置它正被访问的标志; 如果此刻该临界资源正被某进程访问, 则本进程不能进入临界区。
因此, 必须在临界区前面增加一段用于进行上述检查的代码, 把这段代码称为进入区(entry section)。
相应地, 在临界区后面也要加上一段称为退出区(exit section)的代码, 用于将临界区正被访问的标志恢复为未被访问的标志。
进程中除上述进入区、临界区及退出区之外的其它部分的代码在这里都称为剩余区。
这样, 可把一个访问临界资源的循环进程描述如下:
while(TURE){进入区临界区退出区剩余区}
同步机制应遵循的规则
为实现进程互斥地进入自己的临界区, 可用软件方法,更多的是在系统中设置专门的同步机构来协调各进程间的运行。所有同步机制都应遵循下述四条准则:
- 空闲让进
当无进程处于临界区
时,表明临界资源处于空闲状态, 应允许一个请求进入临界区的进程立即进入自己的临界区, 以有效地利用临界资源。 - 忙则等待
当已有进程进入临界区
时, 表明临界资源正在被访问, 因而其它试图进入临界区的进程必须等待, 以保证对临界资源的互斥访问。 - 有限等待
对要求访问临界资源的进程, 应保证在有限时间内能进入自己的临界区
, 以免陷入“死等” 状态。 - 让权等待
当进程不能进入自己的临界区时, 应立即释放处理机
, 以免进程陷入“忙等” 状态
- 空闲让进
2.3.2 信号量机制
信号量(Semaphores)机制是一种卓有成效的进程同步工具
- 整型信号量
最初由Dijkstra把整型信号量定义为一个用于表示资源数目的整型量S,它与一般整型量不同, 除初始化外,仅能通过两个标准的原子操作(Atomic Operation) wait(S)和signal(S)来访问。这两个操作一直被分别称为P、V操作。 wait和signal操作可描述为:
wait(S){while (S<=0); /*do no-op*/S--;}
signal(S)
{S++;
}
wait(S)和signal(S)是两个原子操作, 因此, 它们在执行时是不可中断的。
亦即, 当一个进程在修改某信号量时, 没有其它进程可同时对该信号量进行修改。 此外, 在wait操作中, 对S值的测试和做S:=S- 1操作时都不可中断。
记录型信号量
在整型信号量机制中的wait操作,只要是信号量S≤0, 就会不断地测试。
因此,该机制并未遵循“
让权等待
”的准则,而是使进程处于“忙等”的状态。记录型信号量机制,则是一种不存在“忙等”现象的进程同步机制。
但在采取了“让权等待”的策略后,又会出现多个进程等待访问同一临界资源的情况。
为此,在信号量机制中,除了需要一个用于代表资源数目的整型变量value外,还应增加一个进程链表L,用于链接上述的所有等待进程。
记录型信号量是由于它采用了记录型的数据结构而得名的。
它所包含的上述两个数据项可描述为:
typedef struct {int value;//代表资源数目struct process_control_block *list;//链接所有等待进程 }semaphore;
相应地,wait(S)和signal(S)操作可描述为:
wait(semaphore *S) {S->value--;if (S->value < 0) block(S->list); } signal(semaphore *S) {S->vaIue++;if (S->value<=0) wakeup(S->list); }
在记录型信号量机制中, S -> value的初值表示系统中某类资源的数目, 因而又称为
资源信号量
,对它的每次wait操作,意味着进程请求一个单位的该类资源,使系统中可供分配的该类资源数减少一个,因此描述为S -> value–;当S.value < 0时, 表示该类资源己分配完毕, 因此进程应调用block原语进行自我阻塞, 放弃处理机, 并插入到信号量链表S->list中。可见, 该机制遵循了 “让权等待” 准则。
此时S->value的绝对值表示在该信号量链表中己阻塞进程的数目。
对信号量的每次signal操作表示执行进程释放一个单位资源, 使系统中可供分配的该类资源数增加一个, 故S -> value++操作表示资源数目加1。若加1后仍是S -> value≤0,则表示在该信号量链表中仍有等待该资源的进程被阻塞, 故还应调用wakeup原语, 将S -> list链表中的第一个等待进程唤醒。如果S -> value的初值为1, 表示只允许一个进程访问临界资源, 此时的信号量转化为互斥信号量, 用于进程互斥。
王道数据结构视频链接,加深记录型信号量的理解:从16:30开始看,20:30结束
AND型信号量
前面所述的进程互斥问题针对的是多个并发进程仅共享一个临界资源的情况。在有些应用场合,是一个进程往往需要获得两个或更多的共享资源后方能执行其任务。
假定现有两个进程A和B,它们都要求访问共享数据D和E,当然,共享数据都应作为临界资源。
为此,可为这两个数据分别设置用于互斥的信号量Dmutex和Emutex,并令它们的初值都是1。 相应地, 在两个进程中都要包含两个对Dmutex和Emutex的操作, 即
若进程A和B按下述次序交替执行wait操作:
最后, 进程A和B就将处于僵持状态。 在无外力作用下, 两者都将无法从僵持状态中解脱出来。 我们称此时的进程A和B已进入死锁状态。 显然, 当进程同时要求的共享资源愈多时, 发生进程死锁的可能性也就愈大。AND同步机制的基本思想是
将进程在整个运行过程中需要的所有资源,一次性全部地分配给进程,待进程使用完后再一起释放。只要尚有一个资源未能分配给进程,其它所有可能为之分配的资源,也不分配给他。
亦即,对若干个临界资源的分配,采取原子操作方式:要么全部分配到进程,要么一个也不分配。
由死锁理论可知,这样就可避免上述死锁情况的发生。为此,在wait操作中,增加了一个“AND”条件,故称为
AND同步
,或称为同时wait操作
, 即Swait(Simultaneous wait)。Swait定义如下:
Swait(Sl, S2,…, Sn)
{while (TRUE)
{if (Si>=1 && ... && Sn>=1){for (i =1; i<=n; i++) Si--;break;}else {place the process in the waiting queue associated with the first Si found with Si<1, and set the program count of this process to the beginning of Swait operation}}
}Ssignal(S1,S2,...,Sn) {while (TRUE) {for (i=l; i<=n; i++) {Si++;Remove all the process waiting in the queue associated with Si into the ready queue}}
}
信号量集
在前面所述的记录型信号量机制中, wait(S)或signal(S)操作仅能对信号量施以加1或
减1操作,意味着每次只能对某类临界资源进行一个单位的申请或释放。当一次需要N个
单位时,便要进行N次wait(S)操作,这显然是低效的,甚至会增加死锁的概率。此外,在有些情况下,为确保系统的安全性,当所申请的资源数量低于某一下限值时, 还必须进行管制,不予以分配。
因此,当进程申请某类临界资源时,在每次分配之前,都必须测试资源的数量, 判断是否大于可分配的下限值,决定是否予以分配。
基于上述两点,可以对AND信号量机制加以扩充,对进程所申请的所有资源以及每类资源不同的资源需求量,在一次P、V原语操作中完成申请或释放。
进程对信号量Si的测试值不再是1,而是该资源的分配下限值ti,即要求Si≥ti,否则不予分配。 一旦允许分配, 进程对该资源的需求值为di,即表示资源占用量,进行Si := Si - di操作,而不是简单的Si=Si-1。
由此形成一般化的“信号量集” 机制。
对应的Swait和Ssignal格式为:
Swait(S1, t1, d1, …, Sn, tn, dn)if Si≥t1 && … && Sn≥tn thenfor i∶=1 to n doSi∶=Si-di;endforelsePlace the executing process in the waiting queue of the first Si with Si<ti and set its program counter to the beginning of the Swait Operation. endifsignal(S1, d1, …, Sn, dn)for i∶=1 to n doSi ∶=Si+di; Remove all the process waiting in the queue associated with Si into the ready queueendfor;
一般“信号量集”的几种特殊情况:
(1) Swait(S, d, d)。 此时在信号量集中只有一个信号量S, 但允许它每次申请d个资源,当现有资源数少于d时,不予分配。
(2) Swait(S, 1, 1)。 此时的信号量集已蜕化为一般的记录型信号量(S>1时)或互斥信号量(S=1时)。
(3) Swait(S, 1, 0)。这是一种很特殊且很有用的信号量操作。当S≥1时,允许多个进程进入某特定区;当S变为0后,将阻止任何进程进入特定区。换言之,它相当于一个可控开关。
2.3.3 信号量的应用
- 利用信号量实现进程互斥
- 设mutex为互斥信号量, 其初值为1,取值范围为(-1,0, 1)。
当mutex = 1时, 表示两个进程皆未进入需要互斥的临界区;
当mutex = 0时, 表示有一个进程进入临界区运行,另外一个必须等待, 挂入阻塞队列;
当mutex = -1时, 表示有一个进程正在临界区运行, 另外一个进程因等待而阻塞在信号量队列中,需要被当前已在临界区运行的进程退出时唤醒。
-代码描述
在利用信号量机制实现进程互斥时应该注意,wait(mutex)和signal(mutex)必须成对地出现。 缺少wait(mutex)将会导致系统混乱,不能保证对临界资源的互斥访问; 而缺少signal(mutex)将会使临界资源永远不被释放,从而使因等待该资源而阻塞的进程不能被唤醒。
利用信号量实现前趋关系
-还可利用信号量来描述程序或语句之间的前趋关系。设有两个并发执行的进程P1和2。
P1中有语句S1; P2中有语句S2。
我们希望在S1执行后再执行S2。
为实现这种前趋关系,只需使进程P1和P2共享一个公用信号量S,并赋予其初值为0,将signal(S)操作放在语句S1后面,而在S2语句前面插入wait(S)操作,即
在进程 P1中,用 S1; signal(S);
在进程P2中, 用wait(S);S2;由于S被初始化为0,这样,若P2先执行必定阻塞,只有在进程P1执行完S1;signal(S);
操作后使S增为1时,P2进程方能成功执行语句S2。
同样,我们可以利用信号量按照语句间的前趋关系,写出一个更为复杂的可并发执行的程序。
上图中,S1,S2,S3,…,S6是最简单的程序段(只有一条语句)。 为使各程序段能正确执行, 应设置若干个初始值为“0”的信号量。如为保证S1→S2,S1→S3的前趋关系,应分别设置信号量a和b,同样,为了保证S2→S4,S2→S5,S3→S6,S4→S6和S5→S6,应设置信号量 c,d,e, f, g。
代码框架描述如下:
p1(){S1;singal(a);singal(b);} p2(){wait(a);S2;singal(c);singal(d);} p3(){wait(b);S3;singal(e);} p4(){wait(c);S4;singal(f);} p5(){wait(d);S5;singal(g);} p6(){wait(e);wait(f);wait(g);S6;} main(){semaphore a,b,c,d,e,f,g; a.value=b.value=c.value=O; d.value=e.value=O; f.value=g.value=O; cobegin p1();p2();p3();p4();p5();p6(); coend }
2.4 经典进程的同步问题
2.4.1 生产者—消费者问题
前面我们已经对生产者—消费者问题(The proceducer-consumer problem)做了一些描述,但未考虑进程的互斥与同步问题,因而造成了数据Counter的不定性。
由于生产者—消费者问题是相互合作的进程关系的一种抽象,例如, 在输入时,输入进程是生产者,计算进程是消费者;而在输出时,则计算进程是生产者,而打印进程是消费者, 因此,该问题有很大的代表性及实用价值。
利用记录型信号量解决生产者—消费者问题
假定在生产者和消费者之间的公用缓冲池中,具有n个缓冲区,这时可利用互斥信号量mutex实现诸进程对缓冲池的互斥使用;利用信号量empty和full分别表示缓冲池中空缓冲区和满缓冲区的数量。
又假定这些生产者和消费者相互等效,只要缓冲池未满,生产者便可将消息送入缓冲池;只要缓冲池未空,消费者便可从缓冲池中取走一个消息。
对生产者—消费者问题可描述如下:
int in=0, out=0; item buffer[n]; semaphore mutex=1, empty=n, full=0; void proceducer() {do {producer an item nextp;...wait(empty);//空减一wait(mutex);//使进程独占资源buffer[in] =nextp;in :=(in+l) % n;signal(mutex);//释放资源signal(full);//满加一}while(TRUE); }
void consumer() {
do {
wait(full);
wait(mutex);
nextc= buffer[out];
out =(out+l) % n;
signal(mutex);
signal(empty);
consumer the item in nextc;
}while(TRUE);
}
void main() {
cobegin
proceducer(); consumer();
coend
}在生产者—消费者问题中应注意:
- 首先,在每个程序中用于实现互斥的wait(mutex)和signal(mutex)必须成对地出现;
- 其次,对资源信号量empty和full的wait和signal操作,同样需要成对地出现,但它们分别处于不同的程序中。
- 例如,wait(empty)在计算进程中,而signal(empty)则在打印进程中,计算进程若因执行wait(empty)而阻塞, 则以后将由打印进程将它唤醒;
- 最后,在每个程序中的多个wait操作顺序不能颠倒。
- 应先执行对资源信号量的wait操作,然后再执行对互斥信号量的wait操作,否则可能引起进程死锁。
- 利用AND信号量解决生产者—消费者问题
对于生产者-消费者问题,也可利用AND信号量来解决,即
- 用Swait(empty, mutex)来代替 wait(empty)和 wait(mutex);
- 用 Ssignal(mutex, full)来代替 signal(mutex)和 signal(full);
- 用 Swait(full, mutex)代替 wait(full)和 wait(mutex)
- 用 Ssignal(mutex, empty)代替Signal(mutex)和Signal(empty)。
利用AND信号量来解决生产者-消费者问题的算法中的生产者和消费者可描述如下:
int in=0, out=0;
item buffer[n];
semaphore mutex=1, empty=n, full=0;
void proceducer() {do {producer an item nextp;Swait(empty, mutex);buffer[in] =nextp;in :=(in+1) % n;Ssignal(mutex, full);}while(TRUE);
}void consumer() {do {Swait(full, mutex);nextc= buffer[out];out =(out+l) % n;Ssignal(mutex, empty);consumer the item in nextc;}while(TRUE);
}
2.4.2 哲学家进餐问题
由Dijkstra提出并解决的哲学家进餐问题(The Dinning Philosophers Problem)是典型的同步问题。
该问题是描述有五个哲学家共用一张圆桌,分别坐在周围的五张椅子上,在圆桌上有五个碗和五只筷子,他们的生活方式是交替地进行思考和进餐。
平时, 一个哲学家进行思考,饥饿时便试图取用其左右最靠近他的筷子,只有在他拿到两只筷子时才能进餐。
进餐毕, 放下筷子继续思考。
利用记录型信号量解决哲学家进餐问题
经分析可知,放在桌子上的筷子是临界资源,在一段时间内只允许一位哲学家使用。
为了实现对筷子的互斥使用, 可以用一个信号量表示一只筷子,由这五个信号量构成信号量数组。
其描述如下:
semaphore chopstick[5]={ 1, 1, 1, 1, 1};
所有信号量均被初始化为1, 第i位哲学家的活动可描述为:
do {wait(chopstick[i]);wait(chopstick[(i+1)%5]);//eatsignal(chopstick[i]);signal(chopstick[(i+l)%5]);//think }while[TRUE];
在以上描述中,当哲学家饥饿时,总是先去拿他左边的筷子,即执行wait(chopstick[i]);
成功后,再去拿他右边的筷子,即执行wait(chopstick[(i+l)%5]);
又成功后便可进餐。
进餐毕, 又先放下他左边的筷子,然后再放他右边的筷子。
虽然, 上述解法可保证不会有两个相邻的哲学家同时进餐,但却有可能引起死锁。
假如五位哲学家同时饥饿而各自拿起左边的筷子时, 就会使五个信号量chopstick均为0;当他们再试图去拿右边的筷子时,都将因无筷子可拿而无限期地等待。
对于这样的死锁问题,可采取以下几种解决方法:
至多只允许有四位哲学家同时去拿左边的筷子, 最终能保证至少有一位哲学家能够进餐,并在用毕时能释放出他用过的两只筷子, 从而使更多的哲学家能够进餐。
仅当哲学家的左、 右两只筷子均可用时,才允许他拿起筷子进餐。
规定奇数号哲学家先拿他左边的筷子,然后再去拿右边的筷子;而偶数号哲学家则相反。按此规定,将是1、 2号哲学家竞争1号筷子; 3、 4号哲学家竞争3号筷子。
即五位哲学家都先竞争奇数号筷子, 获得后, 再去竞争偶数号筷子, 最后总会有一位哲学家能获得两只筷子而进餐。
利用AND信号量机制解决哲学家进餐问题
在哲学家进餐问题中,要求每个哲学家先获得两个临界资源(筷子)后方能进餐,这在本质上就是前面所介绍的AND同步问题,故用AND信号量机制可获得最简洁的解法。
semaphore chopstick chopstick[5]={ 1, 1, 1, 1, 1);
do {...//thinkSswait(chopstick[(i+1)%5], chopstick[i]);//eatSsignal(chopstick[(i+1)%5], chopstick[i]);
}while[TRUE];
2.4.3 读者-写者问题
一个数据文件或记录可被多个进程共享, 我们把只要求读该文件的进程称为“Reader
进程” , 其他进程则称为“Writer进程”。
允许多个进程同时读一个共享对象, 因为读操作不会使数据文件混乱。
但不允许一个Writer进程和其他Reader进程或Writer进程同时访问共享对象。因为这种访问将会引起混乱。
所谓“读者-写者(Reader-Writer Problem)问题”,是指保证一个Writer进程必须与其他进程互斥地访问共享对象的同步问题。
读者-写者问题常被用来测试新同步原语
利用记录型信号量解决读者-写者问题
为实现Reader与Writer进程间在读或写时的互斥而设置了一个互斥信号量 Wmutex。
另外,再设置一个整型变量Readcount表示正在读的进程数目。
由于只要有一个Reader进程在读,便不允许Writer进程去写。因此,仅当Readcount=0, 表示尚无Reader进程在读时,Reader进程才需要执行Wait(Wmutex)操作。
若wait(Wmutex)操作成功,Reader进程便可去读,相应地,做Readcount+1操作。
同理,仅当Reader进程在执行了Readcount减1操作后其值为0时,才须执行signal(Wmutex)操作,以便让Writer进程写。
又因为Readcount是一个可被多个Reader进程访问的临界资源,因此,应该为它设置一个互斥信号量rmutex,以防止同时有两个读进程对readcount变量做更改。
读者-写者问题可描述如下:
semaphore rmutex= 1, wmutex= 1; int readcount=0;//读进程 void reader() {do {//进入临界区开始读wait(rmutex);//申请读资源if (readcount==0) wait(wmutex);//如果没人读,申请它readcount++;signal(rmutex);//释放读资源...perform read operation;...//读完准备撤wait(rmutex);readcount--;if (readcount=0) signal(wmutex);//如果没进程读了,释放它signal(rmutex);}while(TRUE); }
//写进程
void writer()
{
do {
wait(wmutex);
perform write operation;
signal(wmutex);
}while(TRUE);
}
void main()
{
cobegin
reader(); writer();
coend
}利用信号量集机制解决读者—写者问题
这里的读者—写者问题,与前面的略有不同, 它增加了一个限制, 即最多只允许RN个读者同时读。
为此, 又引入了一个信号量L,并赋予其初值为RN, 通过执行wait(L,1,1)操作来控制读者的数目, 每当有一个读者进入时, 就要先执行wait(L, 1,1)操作,使L的值减1。 当有RN个读者进入读后, L便减为0,第RN + 1个读者要进入读时,必然会因wait(L, 1, 1)操作失败而阻塞。
对利用信号量集来解决读者-写者问题的描述如下
int RN; semaphore L=RN, mx=1; void reader() {do {Swait(L, 1, 1);Swait(mx, 1, 0);...perform read operation;...Ssignal(L, 1);}while(TRUE); }void writer() {do {Swait(mx, 1, 1; L, RN, 0);perform write operation;Ssignal(mx, 1);}while(TRUE); }
void main()
{
cobegin
reader(); writer();
coend
}
其中,Swait(mx, 1,0)语句起着开关的作用。 只要无writer进程进入写操作, mx=1,reader进程就都可以进入读操作。
但只要一旦有writer进程进入写操作时, 其mx = 0,则任何reader进程就都无法进入读操作。
Swait(mx, 1, 1, L, RN, 0)语句表示仅当既无writer进程在写操作(mx = 1)、 又无reader进程在读操作(L=RN)时, writer进程才能进入临界区进行写操作。
2.5 管程机制
2.5.1 管程的基本概念
管程的定义
系统中的各种硬件资源和软件资源均可用数据结构抽象地描述其资源特性,即用少量信息和对该资源所执行的操作来表征该资源, 而忽略它们的内部结构和实现细节。因此,可以利用共享数据结构抽象地表示系统中的共享资源,并且将对该共享数据结构实施的特定操作定义为一组过程。 进程对共享资源的申请、 释放和其它操作必须通过这组过程,间接地对共享数据结构实现操作。
对于请求访问共享资源的诸多并发进程,可以根据资源的情况接受或阻塞, 确保每次仅有一个进程进入管程, 执行这组过程,使用共享资源, 达到对共享资源所有访问的统一管理, 有效地实现进程互斥。
代表共享资源的数据结构以及由对该共享数据结构实施操作的一组过程所组成的资源管理程序共同构成了一个操作系统的资源管理模块, 我们称之为
管程
。管程被请求和释放资源的进程所调用。
Hansan为管程所下的定义是: “一
个管程定义了一个数据结构和能为并发进程所执行(在该数据结构上) 的一组操作, 这组操作能同步进程和改变管程中的数据。 ”
由上述的定义可知, 管程由四部分组成:
- 管程的名称;
- 局部于管程的共享数据结构说明;
- 对该数据结构进行操作的一组过程;
- 对局部于管程的共享数据设置初始值的语句。
管程的语法如下:管程的语法描述如下:
Monitor monitor_name{ //管程名share variable declarations; //共享变量说明cond declarations; //能被进程调用的过程(即一些操作)public:void P1(.......) //对数据结构操作的过程{......}void P2(....... ){.....}void (....... ){......}......{ //管程主体initialization code; //初始化代码......}
}
条件变量
考虑一种情况: 当一个进程调用了管程,在管程中时被阻塞或挂起, 直到阻塞或挂起的原因解除, 而在此期间, 如果该进程不释放管程,则其它进程无法进入管程,被迫长时间的等待。为了解决这个问题, 引入了条件变量condition。
通常,一个进程被阻塞或挂起的条件(原因)可有多个,因此在管程中设置了多个条件变量, 对这些条件变量的访问只能在管程中进行。
管程中对每个条件变量都须予以说明,其形式为:condition x, y;
对条件变量的操作仅仅是wait和signal,因此条件变量也是一种抽象数据类型, 每个条件变量保存了一个链表,用于记录因该条件变量而阻塞的所有进程, 同时提供的两个操作即可表示为x.wait和x.signal
其含义为:
x.wait:**正在调用管程的进程因x条件需要被阻塞或挂起,则调用x.wait将自己插入到x条件的等待队列上,并释放管程,直到x条件变化。**此时其它进程可以使用该管程。
x.signal:正在调用管程的进程发现x条件发生了变化, 则调用x.signal,重新启动一个因x条件而阻塞或挂起的进程, 如果存在多个这样的进程, 则选择其中的一个, 如果没有,继续执行原进程,而不产生任何结果。
这与信号量机制中的signal操作不同。因为后者总是要执行s :=s+1操作, 因而总会改变信号量的状态。
如果有进程Q因x条件处于阻塞状态, 当正在调用管程的进程P执行了 x.signal操作后, 进程Q被重新启动, 此时两个进程P和Q,如何确定哪个执行哪个等待, 可采用下述两种方式之一进行处理:
(1) P等待,直至Q离开管程或等待另一条件。
(2) Q等待, 直至P离开管程或等待另一条件。采用哪种处理方式, 当然是各执一词。
Hoare采用了第一种处理方式, 而Hansan选择了两者的折中, 他规定管程中的过程所执行的signal操作是过程体的最后一个操作, 于是,进程P执行signal操作后立即退出管程, 因而, 进程Q马上被恢复执行。+
xxzxzxzzxzxxxzx
2.5.2 利用管程解决生产者-消费者问题
在利用管程方法来解决生产者-消费者问题时,首先便是为它们建立一个管程,并命名为ProclucerConsumer, 或简称为PC。其中包括两个过程:
- put(x)过程。
生产者利用该过程将自己生产的产品投放到缓冲池中,并用整型变量count来表示在缓冲池中已有的产品数目,当count≥n时, 表示缓冲池已满,生产者须等待。 - get(item)过程。
消费者利用该过程从缓冲池中取出一个产品,当count≤0时,表示缓冲池中已无可取用的产品, 消费者应等待。
对于条件变量notfull和notempty,分别有两个过程cwait和csignal对它们进行操作:
- cwait(condition)过程: 当管程被一个进程占用时, 其他进程调用该过程时阻塞, 并挂在条件condition的队列上。
(2) csignal(condition)过程: 唤醒在cwait执行后阻塞在条件condition队列上的进程,如果这样的进程不止一个,则选择其中一个实施唤醒操作;如果队列为空,则无操作而返回。
PC管程可描述如下:
Monitor procducerconsumer {item buffer[N];int in, out;condition notfull, notempty;int count;public:void put(item x) {if (count>=N) cwait(notfull);buffer[in] = x;in = (in+1) % N;count++;csignal(notempty);}void get(item x) {if (count<=0) cwait(notempty);x = buffer[out];out = (out+1) % N;count--;csignal(notfull);}{in=0;out=0;count=0; }}PC;
在利用管程解决生产者-消费者问题时, 其中的生产者和消费者可描述为:
void producer() {item x;while(TRUE) {...produce an item in nextp;PC.put(x);}}void consumer() {item x;while(TRUE){PC.get(x);consume the item in nextc;...}}void main() {cobeginproceducer(); consumer();coend}
2.6 进程通信
进程通信是指进程之间的信息交换。
在进程之间要传送大量数据时, 应当利用OS提供的高级通信工具, 该工具最主要的特点是:
- 使用方便。
OS隐藏了实现进程通信的具体细节,向用户提供了一组用于实现高级通信的命令(原语),用户可方便地直接利用它实现进程之间的通信。或者说,通信过程对用户是透明的。 这样就大大减少了通信程序编制上的复杂性。 - 高效地传送大量数据。
用户可直接利用高级通信命令(原语)高效地传送大量的数据
2.6.1 进程通信的类型
- 共享存储器系统(Shared-Memory System)
在共享存储器系统中,相互通信的进程共享某些数据结构或共享存储区,进程之间能够通过这些空间进行通信。据此,又可把它们分成以下两种类型:
基于共享数据结构的通信方式。
在这种通信方式中, 要求诸进程公用某些数据结 构,借以实现诸进程间的信息交换,如在生产者-消费者问题中的有界缓冲区。
操作系统仅提供共享存储器,由程序员负责对公用数据结构的设置及对进程间同步的处理。
这种通信方式仅适于传递相对少量的数据, 通信效率低下,属于低级通信。基于共享存储区的通信方式。
为了传输大量数据, 在内存中划出了一块共享存储区域,诸进程可通过对该共享区的读或写交换信息,实现通信, 数据的形式和位置甚至访问控制都是由进程负责,而不是OS。
这种通信方式属于高级通信,需要通信的进程在通信前,先向系统申请获得共享存储区中的一个分区,并将其附加到自己的地址空间中, 便可对其中的数据进行正常读、 写。
读写完成或不再需要时, 将其归还给共享存储区。
- 消息传递系统(Message passing system)
不论是单机系统、多机系统,还是计算机网络,消息传递机制都是用得最广泛的一种进程间通信的机制。
在消息传递系统中,进程间的数据交换,是以格式化的消息(message)为单位的;在计算机网络中,又把message称为报文。程序员直接利用系统提供的一组通信命令(原语)进行通信。
操作系统隐藏了通信的实现细节,大大减化了通信程序编制的复杂性,而获得广泛的应用。消息传递系统的通信方式属于高级通信方式。又因其实现方式的不同而进一步分成直接通信方式和间接通信方式两种。
直接通信方式:
是指发送进程利用OS所提供的发送原语,直接把消息发送给目标进程
间接通信方式:
是指发送和接收进程,都通过共享中间实体(称为邮箱) 的方式进行消息的发送和接收, 完成进程间的通信。 - 管道(Pipe)通信
所谓“管道”,是指用于连接一个读进程和一个写进程以实现他们之间通信的一个共享文件,又名pipe文件。
向管道(共享文件)提供输入的发送进程(即写进程),以字符流形式将大量的数据送入管道;而接受管道输出的接收进程(即读进程),则从管道中接收(读)数据。
由于发送进程和接收进程是利用管道进行通信的,故又称为管道通信。
这种方式首创于UNIX系统,由于它能有效地传送大量数据,因而又被引入到许多其它操作系统中。
为了协调双方的通信,管道机制必须提供以下三方面的协调能力:- 互斥,即当一个进程正在对pipe执行读/写操作时,其它(另一)进程必须等待。
- 同步,指当写(输入)进程把一定数量(如4 KB)的数据写入pipe,便去睡眠等待,直到读(输出)进程取走数据后,再把他唤醒。当读进程读一空pipe时,也应睡眠等待,直至写进程将数据写入管道后,才将之唤醒。
- 确定对方是否存在,只有确定了对方已存在时,才能进行通信。
2.6.2 消息传递通信的实现方法
直接通信方式
这是指发送进程利用OS所提供的发送命令,直接把消息发送给目标进程。
此时,要求发送进程和接收进程都以显式方式提供对方的标识符。
通常,系统提供下述两条通信命令(原语):send(receiver, message); //发送一个消息给接收进程 receive(sender, message); //接收 Sender 发来的消息
例如,原语Send(P2, m1)表示将消息m1发送给接收进程P2; 而原语Receive(P1,m1)则表示接收由P1发来的消息m1。
在某些情况下,接收进程可与多个发送进程通信,因此,它不可能事先指定发送进程。例如,用于提供打印服务的进程,它可以接收来自任何一个进程的“打印请求”消息。对于这样的应用,在接收进程接收消息的原语中的源进程参数,是完成通信后的返回值,接收原语可表示为:
Receive (id, message);
我们还可以利用直接通信原语,来解决生产者-消费者问题。当生产者生产出一个产品(消息)后,便用Send原语将消息发送给消费者进程;而消费者进程则利用Receive原语来得到一个消息。如果消息尚未生产出来,消费者必须等待,直至生产者进程将消息发送过来。生产者-消费者的通信过程可分别描述如下: ```cpprepeat…produce an item in nextp;…send(consumer,nextp);until false;repeatreceive(producer,nextc);…consume the item in nextc;until false;
- 间接通信方式(信箱通信)
(1)信箱通信属于间接通信方式, 即进程之间的通信, 需要通过某种中间实体(如共享数据结构等)来完成。
信箱的创建和撤消。
进程可利用信箱创建原语来建立一个新信箱。创建者进程应给出信箱名字、信箱属性(公用、私用或共享);对于共享信箱, 还应给出共享者的名字。当进程不再需要读信箱时,可用信箱撤消原语将之撤消。消息的发送和接收。
当进程之间要利用信箱进行通信时,必须使用共享信箱,并利用系统提供的下述通信原语进行通信。
Send(mailbox, message);// 将一个消息发送到指定信箱; Receive(mailbox, message); //从指定信箱中接收一个消息;
(2)信箱可由操作系统创建,也可由用户进程创建,创建者是信箱的拥有者。据此,可把信箱分为以下三类。
1) 私用信箱
用户进程可为自己建立一个新信箱,并作为该进程的一部分。
信箱的拥有者有权从信箱中读取消息,其他用户则只能将自己构成的消息发送到该信箱中。
这种私用信箱可采用单向通信链路的信箱来实现。
当拥有该信箱的进程结束时,信箱也随之消失。
2) 公用信箱
它由操作系统创建,并提供给系统中的所有核准进程使用。
核准进程既可把消息发送到该信箱中,也可从信箱中读取发送给自己的消息。
显然,公用信箱应采用双向通信链路的信箱来实现。
通常,公用信箱在系统运行期间始终存在。
3) 共享信箱
它由某进程创建,在创建时或创建后,指明它是可共享的,同时须指出共享进程(用户)的名字。
信箱的拥有者和共享者,都有权从信箱中取走发送给自己的消息。
(3)利用信箱通信时,在发送进程和接收进程之间,存在以下四种关系:
- 一对一关系。这时可为发送进程和接收进程建立一条两者专用的通信链路,使两者之间的交互不受其他进程的干扰。
- 多对一关系。允许提供服务的进程与多个用户进程之间进行交互,也称为客户/服务器交互(client/server interaction)。
- 一对多关系。允许一个发送进程与多个接收进程进行交互,使发送进程可用广播方式,向接收者(多个)发送消息。
- 多对多关系。允许建立一个公用信箱,让多个进程都能向信箱中投递消息;也可从信箱中取走属于自己的消息。
2.6.3 消息传递系统实现中的若干问题
通信链路(communication link)
为使在发送进程和接收进程之间能进行通信,必须在两者之间建立一条通信链路。
有两种方式建立通信链路。由发送进程在通信之前,用显式的“建立连接”命令(原语)请求系统为之建立一条通信链路;在链路使用完后,也用显式方式拆除链路。这种方式主要用于计算机网络中。
发送进程无须明确提出建立链路的请求,只须利用系统提供的发送命令(原语),系统会自动地为之建立一条链路。这种方式主要用于单机系统中。
通信链路分类
根据通信链路的连接方法
① 点—点连接通信链路,这时的一条链路只连接两个结点(进程);
② 多点连接链路,指用一条链路连接多个(n>2)结点(进程)。而根据通信方式的不同
① 单向通信链路,只允许发送进程向接收进程发送消息;
② 双向链路,既允许由进程A向进程B发送消息,也允许进程B同时向进程A发送消息。
消息的格式
在某些OS中,消息是采用比较短的定长消息格式,这减少了对消息的处理和存储开销。这种方式可用于办公自动化系统中,为用户提供快速的便笺式通信;但这对要发送较长消息的用户是不方便的。
在有的OS中,采用另一种变长的消息格式,即进程所发送消息的长度是可变的。系统在处理和存储变长消息时,须付出更多的开销,但方便了用户。
这两种消息格式各有其优缺点,故在很多系统(包括计算机网络)中,是同时都用的。
进程同步方式
- 发送进程阻塞、 接收进程阻塞。
- 发送进程不阻塞、 接收进程阻塞。
- 发送进程和接收进程均不阻塞。
2.6.4 消息缓冲队列通信机制
消息缓冲队列通信机制首先由美国的Hansan提出, 并在RC 4000系统上实现, 后来被广泛应用于本地进程之间的通信中。
在这种通信机制中,发送进程利用Send原语将消息直接发送给接收进程; 接收进程则利用Receive原语接收消息。
消息缓冲队列通信机制中的数据结构
消息缓冲区。
在消息缓冲队列通信方式中,主要利用的数据结构是消息缓冲区。
它可描述如下:type struct message_buffer {int sender; //发送者进程标识符int size; //消息长度char *text; //消息正文struct message_buffer *next; //指向下一个消息缓冲区的指针
PCB中有关通信的数据项。
在利用消息缓冲队列通信机制时,在设置消息缓冲队列的同时,还应增加用于对消息队列进行操作和实现同步的信号量,并将它们置入进程的PCB中。
在PCB中应增加的数据项可描述如下:type struct processcontrol_block {...struct message_buffer *mq; //消息队列队首指针semaphore mutex; //消息队列互斥信号量semaphore sm; //消息队列资源信号量... }PCB;
发送原语
发送进程在利用发送原语发送消息之前,应先在自己的内存空间,设置一发送区a,见图 2 - 12 所示,把待发送的消息正文、发送进程标识符、消息长度等信息填入其中,然后调用发送原语,把消息发送给目标(接收)进程。发送原语首先根据发送区a中所设置的消息长度a.size来申请一缓冲区i,接着,把发送区a中的信息复制到缓冲区i中。
为了能将i挂在接收进程的消息队列mq上,应先获得接收进程的内部标识符j,然后将i挂在j.mq上。由于该队列属于临界资源, 故在执行insert操作的前后,都要执行wait和signal操作。
发送原语可描述如下:void send(receiver, a) //receiver为接收进程标识符, a为发送区首址; { getbuf(a.size, i); //根据a.size申请缓冲区;copy(i.sender, a.sender); //将发送区a中的信息复制到消息缓冲区i中;i.size=a.size;copy(i.text, a.text);i.next=0;getid(PCBset, receiver.j); //获得接收进程内部的标识符;wait(j.mutex);insert(&j.mq, i); //将消息缓冲区插入消息队列;signal(j.mutex);signal(j.sm); }
接收原语
接收进程调用接收原语receive(b),从自己的消息缓冲队列mq中摘下第一个消息缓冲区i,并将其中的数据复制到以b为首址的指定消息接收区内。
接收原语描述如下:void receive(b) {j = internal name; //j为接收进程内部的标识符;wait(j.sm);wait(j.mutex);remove(j.mq, i); //将消息队列中第一个消息移出;signal(j.mutex);copy(b.sender, i.sender); //将消息缓冲区i中的信息复制到接收区b;b.size =i.size:copy(b.text, i.text);releasebuf(i); //释放消息缓冲区 }
2.7 线程
首先让我们来回顾进程的两个基本属性
① 进程是一个可拥有资源的独立单位,一个进程要能独立运行, 它必须拥有一定的资源, 包括用于存放程序正文、 数据的磁盘和内存地址空间,以及它在运行时所需要的I/O设备、已打开的文件、信号量等;
② 进程同时又是一个可独立调度和分派的基本单位,一个进程要能独立运行,它还必须是一个可独立调度和分派的基本单位。
每个进程在系统中有唯一的PCB,系统可根据其PCB感知进程的存在,也可以根据其PCB中的信息,对进程进行调度,还可将断点信息保存在其PCB中。反之, 再利用进程PCB中的信息来恢复进程运行的现场。
正是由于进程有这两个基本属性,才使进程成为一个能独立运行的基本单位, 从而也就构成了进程并发执行的基础。
但是进程也有它的不足之处。
2.7.1 线程的基本概念
程序并发执行所需付出的时空开销
为使程序能并发执行,系统还必须进行以下的一系列操作。创建进程
系统在创建一个进程时,必须为它分配其所必需的、除处理机以外的所有资源,如内存空间、 I/O设备,以及建立相应的PCB;撤消进程
系统在撤消进程时,又必须先对其所占有的资源执行回收操作,然后再撤消PCB。进程切换
对进程进行上下文切换时,需要保留当前进程的CPU环境,设置新选中进程的CPU环境,因而须花费不少的处理机时间。
据此可知,由于进程是一个资源的拥有者 因而在创建、撤消和切换中,系统必须为之付出较大的时空开销。
这就限制了系统中所设置进程的数目,而且进程切换也不宜过于频繁,从而限制了并发程度的进一步提高。正是在这种思想的指导下, 形成了线程的概念
线程
由于线程具有许多传统进程所具有的特征,所以又称之为轻型进程(Light-Weight Process)或进程元,相应地,把传统进程称为重型进程(Heavy-Weight Process)。它相当于只有一个线程的任务。
线程的属性
- 轻型实体。
- 独立调度和分派的基本单位。
- 可并发执行。
- 共享进程资源。线程的状态
状态参数。
在OS中的每一个线程都可以利用线程标识符和一组状态参数进行描述。
线程运行状态。
如同传统的进程一样,在各线程之间也存在着共享资源和相互合作的制约关系,致使线程在运行时也具有间断性。
相应地,线程在运行时,也具有下述三种基本状态:
线程的创建和终止
在多线程OS环境下,应用程序在启动时,通常仅有一个线程在执行,该线程被人们称为“初始化线程”,它可根据需要再去创建若干个线程。
在创建新线程时,需要利用一个线程创建函数(或系统调用),并提供相应的参数,如指向线程主程序的入口指针、堆栈的大小,以及用于调度的优先级等。在线程创建函数执行完后,将返回一个线程标识符供以后使用。
终止线程的方式有两种:一种是在线程完成了自己的工作后自愿退出;另一种是线程在运行中出现错误或由于某种原因而被其它线程强行终止。
多线程OS中的进程
在多线程OS中,进程是作为拥有系统资源的基本单位,通常的进程都包含多个线程并为它们提供资源,但此时的进程就不再作为一个执行的实体。多线程OS中的进程有以下属性:
- 作为系统资源分配的单位。
- 可包括多个线程。
- 进程不是一个可执行的实体。
2.7.2 线程间的同步和通信
互斥锁(mutex)
互斥锁是一种比较简单的、用于实现进程间对资源互斥访问的机制。
由于操作互斥锁的时间和空间开锁都较低,因而较适合于高频度使用的关键共享数据和程序段。
互斥锁可以有两种状态,即开锁(unlock)和关锁(lock)状态。
相应地,可用两条命令(函数)对互斥锁进行操作。其中的关锁lock操作用于将mutex关上,开锁操作unlock则用于打开mutex。
条件变量
每一个条件变量通常都与一个互斥锁一起使用,亦即,在创建一个互斥锁时便联系着一个条件变量。
单纯的互斥锁用于短期锁定,主要是用来保证对临界区的互斥进入。而条件变量则用于线程的长期等待, 直至所等待的资源成为可用的。
线程首先对mutex执行关锁操作,若成功便进入临界区,然后查找用于描述资源状态的数据结构,以了解资源的情况。 只要发现所需资源R正处于忙碌状态,线程便转为等待状态, 并对mutex执行开锁操作后,等待该资源被释放; 若资源处于空闲状态,表明线程可以使用该资源,于是将该资源设置为忙碌状态,再对mutex执行开锁操作。
下面给出了对上述资源的申请(左半部分)和释放(右半部分)操作的描述。
信号量机制
私用信号量(private samephore)。
当某线程需利用信号量来实现同一进程中各线程之间的同步时,可调用创建信号量的命令来创建一私用信号量,其数据结构是存放在应用程序的地址空间中。
私用信号量属于特定的进程所有,OS并不知道私用信号量的存在,因此,一旦发生私用信号量的占用者异常结束或正常结束,但并未释放该信号量所占有空间的情况时,系统将无法使它恢复为0(空), 也不能将它传送给下一个请求它的线程。公用信号量(public semaphort)。
公用信号量是为实现不同进程间或不同进程中各线程之间的同步而设置的。
由于它有着一个公开的名字供所有的进程使用,故而把它称为公用信号量。
其数据结构是存放在受保护的系统存储区中,由OS为它分配空间并进行管理,故也称为系统信号量。
如果信号量的占有者在结束时未释放该公用信号量,则OS会自动将该信号量空间回收,并通知下一进程。
可见,公用信号量是一种比较安全的同步机制。
2.7.3 内核支持线程和用户级线程
内核支持线程
这里所谓的内核支持线程,也都同样是在内核的支持下运行的,即无论是用户进程中的线程,还是系统进程中的线程,他们的创建、撤消和切换等,也是依靠内核实现的。
此外,在内核空间还为每一个内核支持线程设置了一个线程控制块, 内核是根据该控制块而感知某线程的存在的,并对其加以控制。
用户级线程
用户级线程仅存在于用户空间中。
对于这种线程的创建、 撤消、线程之间的同步与通信等功能,都无须利用系统调用来实现。
对于用户级线程的切换,通常是发生在一个应用进程的诸多线程之间,这时,也同样无须内核的支持。
由于切换的规则远比进程调度和切换的规则简单,因而使线程的切换速度特别快。可见,这种线程是与内核无关的。
2.7.4 线程控制
内核支持线程的实现
用户级线程的实现
运行时系统(Runtime System)
所谓“运行时系统”,实质上是用于管理和控制线程的函数(过程)的集合, 其中包括用于创建和撤消线程的函数、 线程同步和通信的函数以及实现线程调度的函数等。
正因为有这些函数,才能使用户级线程与内核无关。
运行时系统中的所有函数都驻留在用户空间,并作为用户级线程与内核之间的接口。
内核控制线程
这种线程又称为轻型进程LWP(Light Weight Process)。
每一个进程都可拥有多个LWP, 同用户级线程一样, 每个LWP都有自己的数据结构(如
TCB
),其中包括线程标识符、优先级、 状态, 另外还有栈和局部存储区等。 它们也可以共享进程所拥有的资源。LWP可通过系统调用来获得内核提供的服务,这样,当一个用户级线程运行时,只要将它连接到一个LWP上,此时它便具有了内核支持线程的所有属性。
笔记篇:操作系统第二章 进程管理相关推荐
- (王道408考研操作系统)第二章进程管理-第三节10:经典同步问题之哲学家进餐问题
本文接: (王道408考研操作系统)第二章进程管理-第三节6:经典同步问题之生产者与消费者问题 ((王道408考研操作系统)第二章进程管理-第三节7:经典同步问题之多生产者与多消费者问题 (王道408 ...
- (王道408考研操作系统)第二章进程管理-第三节8:经典同步问题之吸烟者问题
本文接: (王道408考研操作系统)第二章进程管理-第三节6:经典同步问题之生产者与消费者问题 ((王道408考研操作系统)第二章进程管理-第三节7:经典同步问题之多生产者与多消费者问题 文章目录 一 ...
- (王道408考研操作系统)第二章进程管理-第三节7:经典同步问题之多生产者与多消费者问题
注意:生产者与消费者问题Linux系统编程专栏有案例讲解 Linux系统编程39:多线程之基于阻塞队列生产者与消费者模型 Linux系统编程40:多线程之基于环形队列的生产者与消费者模型 本文接:(王 ...
- 操作系统第二章 进程管理
写在前面:本文参考王道论坛的 操作系统考研复习指导单科书 文章目录 第二章 进程管理 进程同步 读者写者问题 哲学家就餐问题 练习题 哲学家就餐:加碗(2019真题) 既是生产者又是消费者 和尚取水( ...
- 操作系统 第二章 进程管理
2.1 进程与线程 第一节零碎知识比较多,关键在于进程状态的切换.进程线程的关系. 第一章中提到过的多道程序环境,由于程序的特点,不能让程序并发,所以引入了进程的概念,让进程来并发,从而实现了多道程序 ...
- 3 操作系统第二章 进程管理 进程定义、特征、组织、状态与转换
文章目录 1 进程的定义和特征 2 进程的组织 3 进程的状态与转换 3.1 进程的状态 3.2 进程状态转换 1 进程的定义和特征 引入进程的原因 为了使程序能够并发执行,并且可以对并发执行的程序加 ...
- (王道408考研操作系统)第二章进程管理-第一节4:进程通信(配合Linux)
文章目录 一:什么是进程通信 二:如何实现进程间通信及其分类 三:通信方式1-共享存储(共享内存) (1)课本基础内容 (2)补充-Linux中的进程通信 四:通信方式2-管道 (1)管道是什么 (2 ...
- (王道408考研操作系统)第二章进程管理-第一节3:进程控制(配合Linux讲解)
文章目录 一:如何实现进程控制 二:进程控制原语 (1)进程创建 A:概述 B:补充-Linux中的创建进程操作 ①:fork() ②:fork()相关问题 (2)进程终止 A:概述 B:补充-僵尸进 ...
- 7 操作系统第二章 进程管理 进程同步与互斥
文章目录 1 进程同步与互斥 1.1 进程同步 1.2 进程互斥 1.3 进程同步机制遵循的原则 1.3 进程同步.互斥小结 2 进程互斥实现方法 2.1 互斥的软件实现方法 2.1.1 单标志法 2 ...
最新文章
- 从源码分析DEARGUI之add_plot和add_line_series
- 打开金蝶K/3控制台提示“连接中间层加密服务失败,请确认中间层加密服务已启动”...
- WebSocket的C++服务器端实现
- 他手边有太多事要做了
- VTK:Disk用法实战
- Hadoop_计算框架MapReduce
- Android开发第二次课 布局方式
- (王道408考研操作系统)第五章输入/输出(I/O)管理-第一节5:假脱机(SPOOLing)技术
- 微信小程序制作课程表_课表微信小程序实现(纯技术文)
- python画相关性可视化图_Python可视化matplotlibseborn16-相关性热图
- android小游戏模版—重力感应
- winform定义数据源名称_C#中数据源绑定DataSource以及相关控件(DataGridView)的使用总结...
- 首次启动优美新手指引tip
- 蒙特卡罗方法计算圆周率C语言,用蒙特卡罗方法计算圆周率
- 通用对话框Dialog
- 浏览器开启WebGL
- 关于同构关系的一些证明(1)
- SpringCloud这35问,弄懂了面试官都不得不夸你一句
- 【云原生之Docker实战】使用Docker部署NodeBB社区平台
- [故障集合] Linux的必遇到的故障及原因