第2章 进程管理
2.1 进程的基本概念
2.1.1 程序的顺序执行
-
程序的顺序执行及其特征:
顺序执行包括两层含义:
1) 对于多个用户程序来说,所有程序是依次执行的(外部顺序性)
2)对于一个程序来说,它的所有指令都是按顺序执行的(内部顺序性) -
顺序执行的特征:
- 顺序性:处理及的操作严格按照程序所规定的顺序执行,即每一操作必须在下一操作开始之前结束(或者说下一操作必须在当前操作结束后才能开始)
- 封闭性:程序是在封闭的环境下执行的。即:
- 程序运行时独占全机资源,资源的状态(除初始状态外)只有本程序才能改变它
- 程序一旦开始执行,其执行结果不受外界影响
- 可再现性:只要程序执行时的环境和初始条件相同,当程序重复执行时,都将获得相同的结果
2.1.2 程序的并发执行
-
程序的并发执行及其特征
程序的并发性包含两层含义:
1)对于一个程序来说,它的所有指令都是按序执行的(内部顺序性)
2)对于多个程序(进程)来说,所有进程都是交叉执行的(外部并发性) -
并发执行时的特征:
-
间断性:程序在并发执行时,由于它们共享系统资源,以及为完成同一任务而相互合作,致使这些并发执行的程序之间形成了相互制约的关系(互斥关系,同步关系)
相互制约导致并发执行的程序具有:”执行-暂停-执行“的间断性活动规律。
-
失去封闭性:程序并发执行时,由于多个程序共享系统资源,因而这些资源的状态将由多个程序来改变,致使程序的运行已失去封闭性。
某程序的执行过程中,会受到其他程序的影响
-
不可再现性:与时间有关的错误,程序在并发执行时,由于失去封闭性,也将导致其失去可再现性。
计算结果与并发程序的执行速度(或者说是顺序)有关,从而使程序执行时失去了可再现性。
-
2.2 进程描述
2.2.1进程的定义和特征
- 进程定义:进程是程序在一个数据集上的运行过程,是系统进行资源分配和调度的一个独立单位(传统OS定义)
- 结构组成(进程实体/进程映像):
- 程序段
- 相关数据段
- PCB:一般的程序是不能并发执行的,为使程序(含数据)能独立运行,应为之配置一进程控制块(PCB)
在许多情况下所说的进程,实际上是进程实体,例如所谓的创建进程……
- 进程的特征:
- 动态性:进程的实质是进程实体的一次执行过程,因此动态性是进程的最基本的特性。
- 并发性:是指多个进程实体同存在于内存中,且能在一段时间内同时运行。
- 独立性:在传统的OS中,独立性是指进程实体是一个能独立运行、独立分配资源和独立接受调度的基本单位。
- 独立性:是指进程按各自独立的、不可预知的速度向前推进,或者说进程实体按异步方式运行
2.2.2 进程的基本状态和转换
- 进程的三张基本状态
- 就绪(Ready)状态:当进程已分配到除了CPU以外的所有资源后,只要在获得CPU就可以立即执行,此状态称为就绪状态
- 执行(Running)状态:进程已经获得CPU,其程序正在执行
- 阻塞(Blocked)状态:正在执行的进程由于发生某事件而暂时无法继续执行时,便放弃处理机而处于暂停状态,亦即进程的执行受到阻塞,把这种暂停状态称为阻塞状态(或等待状态)。
- 进程的三种基本状态转换:
- 进程调度:就绪态->执行态
- 时间片完:执行态->就绪态
- 请求I/O:执行态->阻塞态
- I/O完成:阻塞态->就绪态
-
Tips: 阻塞状态只有单箭头,入向是执行,出向是就绪
-
- 进程的五种基本状态转换:
- 创建状态:创建是一个复杂的过程,进程需要由创建而产生,大致过程是:申请空白PCB、写入信息、分配资源、转入就绪状态、插入就绪队列。创建工作未完成进程不能被调度的状态称为创建状态
- 终止状态:进程终止需要两个步骤:首先等待操作系统进行善后处理,最后将其PCB清零回收。
2.2.3 挂起操作和进程状态转换
有些系统除了进程的三种基本状态外,还有挂起操作,使系统处于静止状态,与挂起操作相对应的是激活操作
- 挂起操作的引入
- 引入原因:
- 终端用户的请求:当终端用户在自己的程序运行期间发现有可疑问题时,希望暂停执行。
- 父进程请求:希望考察和修改子进程,或者协调各子进程间的活动
- 负荷调节的需要:实时系统中工作的负荷较重,系统可以把一些不重要的进程挂起
- 操作系统的需要:操作系统有时希望挂起某些进程,以便检查运行中的资源使用情况或者进行记账。
- 引入原因:
- 引入挂起原语后的三个进程状态之间的转换
- 活动就绪->静止就绪:挂起原语Suspend
- 活动阻塞->静止阻塞
- 静止就绪->活动就绪:激活原语Active
- 静止阻塞->活动阻塞
- 示意图:
2.2.4 进程管理中的数据结构
为了描述和控制进程的运行,系统为每一个进程定义了一个数据结构——进程控制块(PCB)
进程控制块是进程实体的一部分,是操作系统中最重要的记录型数据结构。
-
PCB作用:PCB是进程存在的唯一标志
使一个在多道程序环境下不能独立运行的程序(含数据),成为一个能独立运行的基本单位,一个能与其它的进程并发执行的进程。或者说,OS是根据PCB来对并发进行控制和管理的eg:进程调度;现场保护和恢复;进程同步和通信
- 作为独立运行基本单位的标志:PCB是在进程存在于系统的唯一标志
- 能实现间断性运行方式:可以将CPU现场信息保存在PCB中
- 提供进程管理所需要的信息:程序、数据和文件的地址信息等
- 提供进程调度所需要的信息:进程的状态的优先级
- 实现与其他进程的同步和通信:信号量和通信队列指针等
-
PCB中的信息
PCB中记录了操作系统所需要的、用于描述进程当前情况以及控制进程运行的全部信息。具体包括以下四方面的信息:-
进程标识符:内部标识符(进程号);外部标识符(名字,创建者提供)
-
处理机状态:处理机状态信息主要是由处理机的各种寄存器中的内容组成。寄存器包括:通用寄存器、指令计数器、程序状态字(PSW)寄存器、用户栈指针。(保护、恢复现场)
当处理机被中断时,这些信息都必须保存在PCB中,以便该进程重新执行时,可以从断点继续执行。
-
进程调度信息:在PCB中还存放一些与进程调度和进程对换有关的信息。包括:
- 进程状态:指明进程当前的状态,作为调度和对换时的依据。
- 进程优先级:用于描述进程使用处理机的优先级别的一个整数,优先级高的进程优先获得处理机。
- 进程调度所需要的其他信息:它们与所采用的进程调度算法有关……
- 事件:即阻塞的原因,进程由执行状态转变为阻塞状态所等待的发生的时间
-
进程控制信息:
- 程序和数据的地址:指进程实体中程序和数据所在的内存或外存首地址;
- 进程同步和通信机制:如信号量、消息队列指针等,它们可能全部或者部分地存放于PCB中;
- 资源清单:是一张列出了除了CPU外的、进程运行所需要的全部资源以及已经分配到该进程的资源的清单;
- 链接指针:给出本进程(PCB)所在的队列中下一个进程序的PCB的首地址。
-
-
PCB的组织方式
常见的组织方式有三种:线性方式、链接方式和索引方式。-
线性方式:将系统中所有的PCB都组织在一张线性表中,将该表的首地址存放在内存的一个专用区域里面。
- 优点:开销小,实现简单
- 缺点:每次查找都需要扫描整张表
-
链接方式:把具有同一状态的PCB分别通过PCB中的链接字链成一个队列。形成:就绪队列、若干阻塞队列和空白队列等
- 对于就绪队列:往往按照优先级从高到低排列,优先级高的在队列前面。
- 示意图
-
索引方式:系统根据所有进程的状态建立几张索引表。如:
- 就绪索引表
- 阻塞索引表等
- 示意图:
-
2.3 进程控制
进程控制是进程管理中最基本的功能。进程控制主要包括:
- 创建进程
- 终止进程
- 进程状态转换
进程控制是由OS的内核完成的,进程控制的操作一般都是原语(一旦执行无法中止,要么全做,要么全不做) 操作。
2.3.1 操作系统的内核
操作系统内核:和硬件紧密相关的模块(如中断处理程序等)、各种常用设备的驱动程序、以及运行频率较高的模块(如时钟管理、进程调度和许多模块所公用的一些基本操作),都安排在紧靠硬件的软件层次中,将他们常驻内存,通常称为OS的内核。
为了保护OS关键数据的安全,通常将处理机的执行状态分为系统态和用户态
- 系统态:又称为管态,也称为内核态。具有较高特权,能执行一切指令,访问所有寄存器和存储区,传统OS都在系统态运行
- 用户态:又称为目态。具有较低特权的执行状态,仅能执行规定的指令,访问特定的寄存器和存储器。
OS的两大方面的功能:
- 支撑功能
- 中断处理:是内核最基本的功能,是整个操作系统赖以活动的基础。
- 时钟管理:是内核的一项基本功能,例如在时间片轮转调度中,每当时间片用完时,便由时钟管理产生一个中断信号促使调度程序重新进行调度。
- 原语操作:所谓原语(Primitive),就是有若干条指令组成的用语完成一定功能的过程。区别于一般程序,它们是原子操作。原子操作在系统态下执行,常驻内存。
- 资源管理功能
- 进程管理:在进程管理中,或由于各个模块运行频率较高,或由于它们为多种模块所需要,通常都放在内核中,以提高OS性能
- 存储器管理:存储器管理软件的运行频率也比较高,通常将它们放在内核里面,以保证存储器管理具有较高的运行速度
- 设备管理:设备管理和硬件(设备)紧密相关,因此大部分也是放在内核里面。
2.3.2 进程的创建
-
引起创建进程的事件:
- 用户登录
- 作业调度
- 提供服务:当用户进程提出某种请求后,系统将专门创建一个进程来提供用户所需的服务。如,文件打印。
上述三种情况,都是由系统内核为它创建一个新进程。
- 应用请求:是基于应用进程的需求,由应用进程自己创建一个新进程,以便新进程以并发运行方式完成特定任务。
-
进程的创建:调用进程创建原语Create(),按照下述步骤创建一个进程:
- 申请一个空白的PCB
- 为新进程分配资源,主要是内存空间
- 初始化PCB,包括:
- 初始化标识符
- 初始化处理机状态信息:程序计数器,堆栈指针等
- 初始化处理机控制信息:进程状态——就绪或静止就绪、优先级等
- 将新进程插入就绪队列。(进入就绪态)
2.3.3 进程的终止
- 引起进程终止的时间:
- 正常结束
- 异常结束,常见异常结束事件有:
- 越界保护
- 保护错:试图访问不允许访问的资源或者文件,或者以不适当方式访问
- 非法指令
- 特权指令错:用户程序试图执行只允许OS执行的指令
- 运行超时
- 等待超时
- 算术运算错:被0除
- I/O故障
- 外界干预
- 操作员或者操作系统干预(如发生死锁)
- 父进程请求
- 父进程终止
- 进程的终止过程:OS调用终止原语,按下述过程终止进程:
- 根据被终止进程的标识,从PCB集合里面找出该进程的PCB,读出该进程的状态
- 若被终止的进程正处于执行状态,应立即终止其执行,并置调度标志为真,用于指示该进程被终止后应重新进行调度
- 若该进程还有子孙进程,应将其所有子孙进程终止,以防止它们成为不可控进程。
- 将被终止的进程的所有资源,后者归还给其父进程,或者归还给系统
- 被终止进程(它的PCB)从所在队列(或者链表)移除,等待其他进程来搜索信息。
2.3.4 进程的阻塞与唤醒
- 引起阻塞与唤醒的事件
- 请求系统服务(向系统请求共享资源失败):进程向OS请求服务时,由于某些原因无法向其提供该服务,此时进程因不能继续运行而转变为阻塞状态(例如打印机请求)
- 启动某种操作(等待某种操作完成):当进程启动某种操作之后,如果该进程必须在该操作完成之后才能继续执行,则应先将该进程阻塞起来,以等待操作完成
- 新数据尚未到达:对于相互合作的进程,如果其中一个进程需要获得另一个进程提供的数据才能运行对于数据的处理操作,则其所需要的数据尚未到达时,该进程只有阻塞(等待)
- 无新工作可做(等待新任务到达):系统往往设置一些具有特定功能的系统进程,每当这种进程完成任务后,便把自己阻塞起来以等待新任务的到来。
- 进程阻塞过程(主动行为):调用阻塞原语block把自己阻塞
- 立即停止执行
- 把PCB中进程状态从“执行“改为”阻塞“
- 将PCB插入因相同事件而阻塞的阻塞队列(如果有多个阻塞队列的话)
- 转进程调度程序进行重新调度,将处理机分配给某一个就绪进程,并进行进程切换——即保留被阻塞进程的处理机状态,按新进程的PCB中的处理机状态设置CPU环境
- 进程唤醒过程:==调用唤醒原语wakeup(),等待事件的进程唤醒。
- 将被唤醒进程的PCB从阻塞队列移除
- 将其PCB中进程状态由”阻塞“改为”就绪“
- 将PCB插入到就绪队列
block()和wakeup()是成对出现的
2.3.5 进程的挂起和激活
- 进程的挂起:当出现了引起进程挂起的事件时(用户进程请求将自己挂起,或父进程请求将子进程挂起),系统就会使用挂起原语suspend(),将指定进程或者处于阻塞状态的进程挂起。过程如下:
- 检查被挂起进程的状态:若是处于活动就绪或者执行状态,则将其转为静止就绪;若是处于活动阻塞,将其转为静止阻塞。
- 把该进程的PCB复制到某指定内存区域(为了方便用户或父进程考查该进程的运行情况)
- 若该进程正在执行,则转进程调度程序重新调度。
- 进程的激活:当发生激活进程的事件时(如父进程或者用户请求激活指定进程,而内存中已有足够空间时),系统利用激活原语active()将指定进程激活。过程如下:
- 将进程从外存调入内存
- 检查该进程现行状态:若是静止就绪,则改为活动就绪;若是精致阻塞,则改为活动阻塞。
- 若是采用的时抢占式调度策略,应检查被激活就绪进程的优先级,若是其优先级比先行执行进程高,则应将处理机分配给被激活的进程。
2.4 进程同步
2.4.1进程同步的基本概念
进程同步机制的主要任务:对多个相关进程在执行次序上进行协调,使并发执行的诸进程之间能按照一定的规则(或者时序)共享系统资源,并能很好地相互合作,从而实现程序的执行具有可再现性。
-
两种形式的制约关系
- 间接制约关系(进程互斥):间接制约关系源于资源共享。如打印机
- 直接制约关系(进程同步):源于进程间的合作
-
临界资源:一段时间内只允许一个进程访问的资源。
- 经典的生产者消费者问题:
- 经典的生产者消费者问题:
-
临界区:每一个进程中访问临界资源的那段代码被称为临界区
A: begin
Input data 1 form I/O 1 ;
Compute……;
Print results 1 by printer ; 临界区A
End
B: begin
Input a data 2 form I/O 2 ;
Compute……;
Print results 2 by printer ; 临界区B
End
进程互斥——不允许两个或两个以上进程同时进入相关临界区。
不管是硬件临界资源,还是软件临界资源,多个进程必须互斥的对他们进行访问
在访问互斥资源时,要对临界区进行检查,进入和退出临界区时都要对访问标志进行检查更新。
因此要在临界区前增加一段用于检查的代码:进入区
在临界区后面也要加一段代码,将临界区正在被访问的标志恢复为未被访问的标志:退出区
- 同步机制应当遵循的原则:为了实现进程互斥进入临界区,一般都是在系统中设置专门的同步机制来协调各进程间的运行。所有的同步机制应当遵循下面四条准则:
- 空闲让进:当无进程处于临界区时,表明临界资源处于空闲状态,应允许一个请求进入临界区的进程立即进入自己的临界区,以便有效的利用临界资源。
- 忙则等待:当已有进程进入临界区时,表明临界资源正在被访问,因而其他试图进入到临界区的进程必须等待,以保证对临界区的互斥访问
- 有限等待:对要求访问临界资源的进程,应保证在有限的时间内能进入自己的临界区,以免陷入”死锁“状态——不死等。
- 让权等待:当进程不能进入自己的临界区时,应立即释放处理机,以免进程陷入“忙等”。——不忙碌等待
2.4.2 硬件同步机制
(我们学校没有作为重点进行讲解,所以就写几个标题保证顺序完整性 : b)
虽然可以利用软件方法解决诸进程互斥进入临界区的问题,但是有一定的难度,并且存在很大的局限性。
目前计算机已经提供了一些特殊的硬件对一个字中的内容进行检测和修正,或者对两个字的内容进行交换等,可以利用这些特殊的指令解决临界区资源。
- 关中断
- 利用Test-and-Set指令实现互斥
- 利用Swap指令实现进程互斥
利用上述硬件指令可以有效实现进程互斥,但是当临界资源忙碌时,其他访问进程必须不断地进行测试,处于一种”忙等“的状态,不符合”让权等待“的原则。造成处理机时间浪费,同时也很难将它们用于解决复杂的进程同步问题。
2.4.3 信号量机制
- 信号量(Semaphores)机制是一种卓有成效的进程同步工具。信号量机制已被广泛应用于单处理机和多处理机系统以及计算机网络中。
- 信号量的产生源于交通信号灯
- 信号量是一种被保护的数据结构,除了信号量的赋初值外,信号量的值仅能由两个同步原语操作。Dijkstra将这两个同步原语命名为“P操作(又称wait操作)”和“V操作(又称signal操作)”(P、V来源于荷兰文的“发信号”和“等待”二词的第一个字母)。
信号量的发展: - 整型信号量:“忙等”,未遵循“让权等待”准则
- 记录型信号量:
- AND型信号量
- 信号量集(后两种是记录型信号量的扩展)
-
整型信号量:定义一个整形量S,除初始化外,仅能通过两个标准的原子操作wait(S)、signal(S)来访问。
wait(S) { while(S<=0) ; S=S-1; } signal(S) { S=S+1; } int s = 1; Printer(){ wait(s); print the document on the paper; signal(s); }
-
记录型信号量:需要一个用于代表临界资源数目的整型变量value;还要一个在该资源上阻塞的队列(链表,指针L)。故信号量应采用记录型(C语言里面的结构体)结构;
struct semaphore { int value; //系统中的资源数 struct semaphore *L; //阻塞进程队列 } //PCB表示进程控制块
value 的初值表示资源的数量,value的绝对值表示被阻塞进程数
wait和signal的c/c++语言描述:
P(S):
struct semaphore S; void wait(S) { S.value = S.value - 1; if (S.value < 0) //表示该资源已经分配完毕 { block(S.L); //该进程设置为阻塞态 }/* 让权等待 将该进程的PCB插入阻塞队列s.queue末尾*/ }
V(S):
struct semaphore S; void signal(S) {{//释放资源 S.value = S.value + 1; if(S.value <= 0) //表示在信号量链表中仍有等待该资源的进程被阻塞,需要调用wakeup原语 { wakeup(S.L); }/*唤醒第一个等待的进程 */ }
wait、signal操作的物理意义:
对信号量S的每一次wait
操作,意味着进程请求一个该类临界资源,因此描述为S.value = S.value - 1;
,当S.value < 0
时,表示资源已经分配完,因此进程应调用block
原语进行自我阻塞,放弃处理机,并插入到信号量链表S.L
(阻塞队列)中,可见该机制遵循了“让权等待”原则
类似的可以结束signal
操作……S.value的初值表示系统中某类资源的数目。——资源信号量。
S.value的初值为1,表示只允许一个进程访问,此时信号量转化为互斥信号量。记录型信号量的应用:
struct semaphore mutex;mutex.value=3; Printer() { wait(mutex); print the document on the paper; signal(mutex); }
- mutex = 3
- p 1 p_1 p1 -> mutex = 2
- p 2 p_2 p2 -> mutex = 1
- p 3 p_3 p3 -> mutex = 0
- p 4 p_4 p4 -> mutex = -1 阻塞
- p 1 p_1 p1 signal(mutex)-> p 4 p_4 p4 mutex = 0
wait和signal操作的物理意义:
- Wait操作是申请资源的操作,是减1的操作
- Signal操作是释放资源的操作,是加1的操作
- 一般每一类资源对应一个信号量
- 整型变量代表可用资源的数目
- 等待队列里面链接的是等待使用这种资源的进程
- 整型变量的值如果大于0,代表有多个资源可用
- 整型变量的值如果等于0,代表无可用的资源
- 整型变量的值如果小于0,代表有进程在等待资源,整型变量的绝对值等于等待队列中进程的数目
-
AND型信号量
以上的进程互斥问题,都是针对各进程之间要共享一个临界资源而言的。在有些应用场合,是一个进程需要先获得两个或者更多的共享资源之后,方能执行其任务。反例:进程A和B共享数据D和E。如果两个信号量Dmutex和Emutex来实现互斥,则有一下情况
Process A: wait(Dmutex); wait(Emutex); Process B: wait(Dmutex); wait(Emutex); // 若是A和B按照以下次序执行wait操作 Process A: wait(Dmutex); 此时 Dmutex=0; Process B: wait(Emutex); 此时 Emutex=0; Process A: wait(Emutex); 此时Emutex=-1; A阻塞; Process B: wait(Dmutex); 此时Dmutex=-1; B阻塞;
陷入僵局:
AND信号量思想:对于若干临界资源的分配,采用原子操作方式:要么全分配给进程,要么一个也不分配
And信号量的P,V操作:
Swait(S1,S2, S3,… , Sn) { while(TRUE) { if S1 >= 1 && S2 >=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, S3,… , Sn){ while(TRUE){ for(i = 1; i <= n; i++){ Si++; remove all the process waiting in the queue associated with Si into the ready queue. } } }
-
信号量集
思想:如果进程一次需要申请多类临界资源,每类临界资源申请多个,并且进行临界资源分配时,先测试各类临界资源是否大于其下限值。若低于下限值,则不予分配。
以下的程序中,S为信号量,d为需求值,t为下限值。Swait(S1, t1, d1, ……, Sn, tn, dn){ if(S1 >= t1 && …… && Sn >= tn){ for(i = 1, i <= n; i++){ Si = Si - di; } }else{ Place 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. } //将正在执行的进程移入到第一个Si < ti等待队列中,并将程序指针指向该进程中Swait操作开始处。 } Ssignal(Si, di, ……, Sn, dn){ for(i = 1; i <= n; i++){ Si = Si + di; Remove all the process waiting in the queue associated with Si into the ready queue; } }
信号量集
- Swait(S,d,d):信号量集中只有一个信号量S,但允许它每次申请d个资源,当现成资源小于d时,不予分配
- Swait(S, 1, 1):等同于一般的记录型信号量或者互斥信号量
- Swait(S, 1, 0):当S>=1时,允许多个进程进入某特定区;当S变为0后,阻止所有进程进入临界区。其功能类似于可控开关。
2.4.4 信号量的应用
在做题之前,一般分为四步:
1. 进程:确定有多少个进程->活动的对象
2. 活动:每一个进程的活动
3. 分析不同进程之间的活动顺序关系
- 互斥
- 同步
- 资源管控
4. 设置信号量
-
利用信号量实现进程互斥
方法要点:- 为临界区设置一个互斥信号量mutex,其初值 为1;
- 各进程访问该资源的临界区置于wait(mutex)和signal(mutex)之间即可
Struct semaphore mutex; mutex.value = 1; p1(){ wait(mutex); 临界区; signal(mutex); } P2(){ wait(mutex); 临界区; signal(mutex); }
eg:某交通路口设置了一个自动计数系统,该系统由“观察者”进程和“报告者”进程组成。观察者进程能识别卡车,并对通过的卡车计数;报告者进程定时(可设为每隔1小时,准点时)将观察者的计数值打印输出,每次打印后把计数值清“0”。两个进程的并发执行可完成对每小时中卡车流量的统计。这两个进程的功能如下:
struct semaphore S; int count = 0; S.value = 1; process observer(){ while(condition){ observe a lorry; waits(S); count = count + 1; signal(S); } } process reporter(){ while(condition){ wait(S); print(count); count = 0; signal(S); } } // 注意:wait(S), signal(S),必须成对出现
-
利用信号量实现进程同步
具有同步关系的一组并发进程称为合作进程,有前后执行顺序关系,合作进程间相互发送的消息称为消息或者事件。如果我们对一个消息或者事件赋予唯一的消息名,则我们可用过程wait(消息名)表示进程等待合作进程发来的消息,而用过程signal(消息名)表示向合作进程发送消息semaphore s1; s1.value = 0; P0(){ A; Signal(s1); } P1(){ Wait(s1); B; } struct semaphore s1,s2; s1.value=0; s2.value=1; P0(){ wait(s2); A; Signal(s1); } P1(){ Wait(s1); B; Signal(s2); }
-
利用信号量控制使用资源的进程数目
系统中有5台打印机可以使用,请使用进程同步机制使得最多可以有5个进程可以同时使用打印机,多于5个进程使用时,要对新申请进程进行阻塞。struct semaphore S; S.value = 5; Printer(){ wait(S); print the document on paper; signal(S); }
练习1:在一个盒子里,混装了数量相等的黑白围棋子。现在用自动分拣系统把黑子、白子分开,设分拣系统有两个进程P1和P2,其中P1拣白子,P2拣黑子。规定当一个进程拣了一子后,必须让另一个进程去拣。用信号量和PV操作协调两进程的活动。
Struct semaphore S1, S2; S1.value=1; S2.value=0; //规定先捡白子,再捡黑子 P1(){ while(true){ P(S1); 捡白子; V(S2); } } P2(){ while(true){ P(S2); 捡黑子; V(S1); } }
2.5 经典进程同步问题
在多道程序环境下,进程同步问题十分重要,引起了不少学者对它进行研究,由此产生了一系列经典的进程同步问题,其中较有代表性的是:
- 生产者-消费者问题
- 读者-写者问题
- 哲学家进餐问题
2.4.1 生产者-消费者问题
生产者-消费者问题从特殊到一般(从易到难)可以分3种形式:
- 一个生产者、一个消费者、一个缓冲区问题;
- 一个生产者、一个消费者、n个缓冲区问题;
- k个生产者、m个消费者、n个缓冲区的问题;
-
最简单的生产者
- 当缓冲区空时,生产者可将产品存入缓冲区;当缓冲区满时,生产者必须等待 (阻塞),待消费者取走产品后将其唤醒后,才能将产品存入。
- 当缓冲区满时,消费者可从缓冲区取出产品进行消费;当缓冲区空时,消费者必须等待(阻塞),待生产者存入产品后将其唤醒后,才能再从缓冲区取产品。
用信号量机制解决进程同步问题的基本方法:
- 设置1个信号量empty,其初值为1,表示有1个空缓冲区;设置1个信号量full,其初值为0,表示开始时没有空缓冲区;(由物理意义确定)
- 生产者将产品存入缓冲区之前,应先测试缓冲区是否空:执行wait(empty)操作;离开临界区(存入产品)后,应通知(可能会唤醒)消费者:执行signal(full)操作;
- 消费者从缓冲区取产品之前,应先测试缓冲区是否满:执行wait(full)操作;离开临界区(取走产品)后,应通知(可能会唤醒)生产者:执行signal(empty)操作
struct semaphore empty,full; empty.value=1; full.value=0; parbegin process Producer(){ produce an item in nextp; //生产一个item放入nextp wait(empty); // 测试 buffer = nextp; signal(full); //通知消费者 } process Consumer(){ wait(full); //测试 nextc = buffer; signal(empty): //通知 consuime the item in nextc; } parend
-
n一个生产者、一个消费者、n个缓冲区的P-C问题
struct semaphore empty,full; empty.value = n; full.value = 0; int in = 0, out = 0; parbegin process Producer(){ produce an item in nextp; wait(empty); buffer[in] = nextp; in = (in + 1) % n; signal(full); //满的缓冲器加1 } process Consumer(){ wait(full); nextc = buffer[out]; out = (out + 1) % n; signal(empty); //空的缓冲区加1 consume the item in nextc; } parend
-
k个生产者、m个消费者、n个缓冲区的问题。
- 用互斥信号量mutex对缓冲区(共享变量in和out)的互斥使用,互斥信号量mutex初值为1;
- 用资源信号量empty表示多缓冲中空缓冲区的数目,empty的初值为n;
- 用资源信号量full表示多缓冲中满缓冲区的数目,full的初值为0;
- 只要多缓冲未满,生产者便可将消息送入缓冲区;
- 只要多缓冲不空,消费者便可从缓冲区取走一个消息。
- 生产者用共享变量in作为下标访问缓冲区,mutex为其互斥信号量;消费者用共享变量out作为下标访问缓冲区,其互斥信号量也用mutex。
semaphore mutex,empty,full ; item buffer[n]; int in = 0, out = 0; mutex.value = 1; empty.value = n; full.value = 0; parbegin //并发执行开始 process producer_i (i=1,2,…,k){ item nextp; while(condition){ …… produce an item in nextp; …… wait(empty); wait(mutex); buffer[in] = nextp; in = (in + 1) % n; signal(mutex); signal(full); } } process consumer_j (j=1,2,…,m){ item nextc; while(conditionn){ wait(full); wait(mutex); nextc = buffer[out]; out = (out + 1) % n; signal(mutex); signal(empty); consume the item in nextc; } } parend //并发执行结束
- 在每个进程中,实现互斥的wait(mutex)和signal(mutex)必须成对出现;
- 对资源信号量empty和full的wait和signal操作也要成对地出现,但它们处于不同的进程中。
- 在每个进程中的多个wait操作顺序不能颠倒,应先执行对资源信号量的wait操作,然后执行对互斥信号量的wait操作,否则可能引起进程死锁。
2.6 进程通信
- 进程通信:进程之间的信息交换
- 进程之间互斥和同步,故而交换的信息量少:低级通信
- 信号量机制作为通信工具不够理想,表现在:
- 效率低
- 通信对用户不透明
- 高级通信:指的是用户可以直接利用OS所提供的一组通信命令,高效地传送大量数据的一种通信方式。
- 高级通信过程对用户是透明的。大大减少了通信程序编制的复杂性。
2.6.1 进程通信的类型
高级通信机制可归结为四类:
- 共享存储器通信 单机(集中式)
- 管道通信(共享文件) 单机(集中式)
- 消息传递通信 -> 适合单机或网络
- 直接通信方式
- 间接通信方式——信箱通信
- 客户机-服务器系统
- 共享存储器系统(Shared-Memory System)
- 基于共享数据结构的通信方式:要求诸进程公用某些数据结构,皆以实现诸进程间的信息交互。但是这种进程仅适用于传递相对少量的数据,通信效率低下,——属于低级通信
- 基于共享存储区的通信方式:在存储区中划出一块共享存储区,诸进程可通过对共享存储区中数据的读和写来实现通信。——属于高级通信方式。
- 管道通信:首创于UNIX系统
管道——用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件,又称为pipe文件。- 写进程以字节流的形式将大量数据送入管道;
- 读进程从管道中接收(读)数据。
为协调双方的通信,管道机制必须提供以下三方面的协调能力: - 互斥——当一个进程正在对pipe执行读/写操作时,其它进程必须等待;
- 同步——当写进程把一定数据写入pipe,便去睡眠等待,直至读进程取走数据后,再把它唤醒;当读进程读一空pipe时,也提供睡眠等待,直至写进程将数据写入pipe后,再把它唤醒。
- 确定对方是否存在——只要确定对方已存在时,才能进行通信
- 消息传递系统
消息传递机制——单机系统、多级系统、计算机网络,用得最广泛的一种高级通信机制- 进程间的数据交换,是以格式化的消息(message)为单位的
- 计算机网络中,message又称为报文
- 利用系统提供的一组通信命令(原语)进行通信
- OS隐蔽了通信实现细节,大大简化了通信程序编制的复杂性,因而得到广泛应用。
- 通信方式:
- 直接通信方式
- 间接通信方式
- 客户机-服务器系统
目前最主流的通信实现机制,主要包括:- 套接字
- 远程调用和远程方法调用
- 套接字(socket):一个套接字就是一个通信标示类型的数据结构。套接字分两类:
- 基于文件型:同一台主机环境,一个套接字关联一个特殊文件,类似管道。
- 基于网络型:不同主机的网络环境。被分配一对套接字,一个属于接收进程(或服务器),一 个属于发送进程(或客户端)。
- 远程过程调用(RPC):允许一台主机系统上进程调用另外一台主机系统上的进程 ,对 程序员表现为常规的过程调用,无需额外地为此编程。如果涉及的软件采用面向对象编程,远程过程调用亦可以成为远程方法调用。
2.6.2 消息传递通信的实现方法
- 直接通信方式:是指发送进程利用OS所提供的命令,直接把消息发送给目标进程。要求发送和接收进程都以显式方式提供对方的标识符。两条通信原语为:
有时,接收进程可能与多个发送进程通信,故不可能事先知道发送进程。对于这样的应用,接收原语中的源进程参数,是完成通信后的返回值。接收原语可表示为:send(Receiver,message); //发送一个消息message给接收进程Receiver receive(Sender,message);//接收Sender发来的消息message
receive(id,message); //id是返回值(标识符)
2. 间接通信方式:是指进程之间的通信,需要通过作为共享数据结构的实体——信箱
信箱通信属于间接通信
信箱暂存发送进程发送给目标进程的消息
接收进程从信箱中取出对方发给自己的消息。
既可以实现实时通信,也可以实现非实时通信
系统为信箱通信提供了若干条原语:
1. 信箱的创建和撤销:信箱可由OS创建,也可由用户用OS命令创建
2. 消息的发送和接收
//将一个消息message发送到指定的信箱mailbox
send (mailbox,message);
//从指定信箱mailbox接收消息message
receive(mailbox,message);
2.6.3 进程传递系统实现中的进程同步方式
三种情况:
- 发送进程阻塞、接收进程阻塞(这同步方式称为汇合)->主要用于发送进程和接收进程之间无缓冲时的进程之间的紧密同步
两个进程平时都处于阻塞状态,直到有消息传递时。 - 发送进程不阻塞、接收进程阻塞(应用最广泛的进程同步方式)
接收进程如服务器上通常都设置了多个服务进程,分别用于提供不同的服务:如打印服务 - 发送进程和接收进程均不阻塞(也是较为常见的进程同步方式)->有消息队列的时候采用
发送进程和接收进程都忙于自己的事情,仅当发生某事件使它无法继续执行时,才把自己阻塞起来等待。
2.6.4 消息缓冲队列通信机制
美国的hansan提出,被广泛应用于本地进程之间的通信。
-
消息缓冲队列通信机制中的数据结构
- 消息缓冲区,结构如下:
struct message_buffer{ long sender; //发送者进程biaoshifu int size; //消息队列 char text[N]; //消息正文 struct message_buffer *nest; //指向下一个消息缓冲区的指针 }
- PCB中有关通信的数据项
- mq :消息队列首指针
- mutex:消息队列互斥信号量
- sm:消息队列资源信号量
- 消息缓冲区,结构如下:
-
发送原语
send(receiver, a){
getbuf(a.size, i);//根据a.size申请缓冲区i
//将a中信息复制到消息缓冲区i中
i.sender = a.sender;
i.size = a.size;
i.text = a.text;
i.next = 0;
getid(PCB set,receiver.j); //获得接收进程内部标识符
wait(j.mutex);
insert(j.mq, i); //将消息缓冲区插入到消息队列中
signal(j.mutex);
signal(j.sm); //资源数目增1(有可能唤醒接收进程)
}
- 接收原语
:从自己的消息缓冲队列mq 中,摘下第一个消息缓冲区i,并将其中的数据复制到以b为首址的指定消息接收区内。
receive(b){
j = internal name; //j为接收进程内部标识符
wait(j.sm);
wait(j.mutex);
remove(j.mq,i); //将消息队列中第一个消息i移出
signal(j.mutex);
//将消息缓冲区i中消息复制到接收区b
b.sender = i.sender
b.size = i.size
b.text = i.text
}
2.7 线程
- 20世纪60年代人们提出了进程概念后,在OS中一直都是以进程为能拥有资源和独立运行的基本单位的。
- n直到20世纪80年代中期,人们又提出了比进程更小的能独立运行的基本单位——线程(Threads)。试图用它来提高系统内程序并发执行的程度,从而进一步提高系统的吞吐量
- 进入20世纪90年代后,多处理机系统得到迅速发展,线程能比进程更好地提高程序的并发执行程度,充分地发挥多处理机的优越性,因而近几年所推出的多处理机OS,都引入了线程,以改善OS的性能。
2.7.1 线程的基本概念
-
线程的引入
:OS引入线程,目的是减少程序在并发执行时所付出的时空开销。
进程“太重”,系统在进程上所花费是时空开销大致表现在:- 创建进程:系统在创建进程时必须为它分配除CPU以外的所有资源,如内存空间、I/O设备,以及建立相应的PCB。
- 撤销进程:系统撤销进程时,必须先收回其所占有的资源,然后再撤销PCB。
- 进程切换:进程切换时,由于要保留当前进程的CPU环境和设置新进程的CPU环境,因而需花费不少处理机时间。
总之,由于进程是一个资源拥有者,因而在创建、撤销、切换中,系统必须为之付出较大的时空开销,故而系统中的进程数目不宜过多,进程切换的频率也不宜太高,这限制了并发程序的进一步提高。
进程既是资源分配的基本单位,又是调度和分派的基本单位。若是能将进程的上述两个属性分开,由操作系统分开处理。即:
- 对于作为调度和分配的基本单位,不同时作为拥有资源的基本单位,以做到“轻装上阵”;
- 而对于拥有资源的的基本单位,又不对之进行频繁切换。
在这种思想的指导下,形成了线程概念。线程只作为调度和分派的基本单位,而不作为资源分配的基本单位。一个进程可以包括多个线程。
-
线程的属性
每一个线程都作为分配CPU的基本单位,是花费最小的实体。
线程具有以下属性:- 轻型实体:线程基本上不拥有系统资源,只是有一点必不可少的、能保证独立调度和分配的资源。如:线程控制块TCB;
- 独立调度和分派的基本单位:切换非常迅速和开销小;
- 可并发执行:一个进程的多个进程甚至全部线程可以并发执行;不同进程之间的线程也能并发执行;
- 共享进程资源:同一进程的所有线程,都可共享该进程所拥有的资源。
-
线程控制块TCB和线程状态
在OS中的每一个线程都可以用线程标识符和一组状态参数进行描述
状态参数包括:- 寄存器状态:程序计数器PC和堆栈指针内容
- 堆栈:通常保存局部变量和返回地址
- 线程运行状态:执行状态、就绪状态、阻塞状态、
- 优先级
- 线程专用存储器:用于保存线程自己的局部变量拷贝
- 信号屏蔽:对某些信号加以屏蔽
线程的三种状态:执行、就绪、阻塞
2.7.2 线程间的同步和通信
- 互斥锁(mutex)
- 条件变量(管程)
- 信号量机制
2.7.3 线程的实现
- 内核级线程: 线程管理的全部工作由OS内核来做,内核专门提供了一个KLT(Kernel Level Threads)应用程序设计接口(API)供开发者使用。例如Windows 2000/XP。
- 用户级线程: 线程管理的全部工作由应用程序来做,在用户空间内实现,内核是不知道线程的存在的。线程库是线程运行的支撑环境。
- 混合式线程(例如Solaris)