目录
第3章 进程管理
基本概念
-
程序的顺序执行的特点:
-
顺序性:上一条指令执行结束是下一条指令开始执行的充要条件
-
封闭性:结果只与初始条件决定
-
可再现性:结果与执行速度无关
-
-
多道程序系统中的执行环境的特点:
-
独立性:每道程序逻辑上独立
-
随机性:程序和数据的输入与执行开始时间都是随机的
-
资源共享性:多道程序共享软硬件资源
-
-
程序的并发执行分为两类:
-
多道程序的并发执行
即多道程序系统中的各程序轮流进入运行态,宏观上像是同时执行的,但微观上其实是串行的
-
真正的并发
某道程序的几个程序段中包含一部分可以同时执行或顺序颠倒执行的代码
-
-
并发执行的判断方法——Bernstein条件
R ( S ) R(S) R(S)、 W ( S ) W(S) W(S)分别表示语句S需要读取、写入的变量的集合。语句 S 1 S_1 S1、 S 2 S_2 S2可以并发执行的条件:
-
R ( S 1 ) ∩ W ( S 2 ) = { ∅ } R(S_1)\cap W(S_2)=\{\varnothing\} R(S1)∩W(S2)={∅}
-
W ( S 1 ) ∩ R ( S 2 ) = { ∅ } W(S_1)\cap R(S_2)=\{\varnothing\} W(S1)∩R(S2)={∅}
-
W ( S 1 ) ∩ W ( S 2 ) = { ∅ } W(S_1)\cap W(S_2)=\{\varnothing\} W(S1)∩W(S2)={∅}
简单来说,任何变量读取的操作不能和其他的操作一起。显然,不满足并发执行条件的两条语句去并发,会发生错误
-
进程的描述及其状态
基本概念
-
进程是并发执行的程序在执行过程中分配和管理资源的基本单位
-
进程的特征:动态性(创建和消亡是动态的)、并发性、独立性、异步性(推进速度不可预知)
-
不同进程可以包含同一程序,只要该程序对应的数据集不同
-
进程的静态描述的组成(进程映像):
-
进程控制块PCB:系统通过PCB感知进程,其中包含了该进程的相关信息
-
程序段:描述进程要完成的功能
-
数据结构集:程序执行时的工作区和操作对象
-
-
每一进程都有其自己的地址空间,称为进程空间或虚空间。进程空间的大小(即为进程的大小)只与机器位数有关(即字长),例如32位机器的进程空间大小为 2 32 2^{32} 232。有些操作系统中进程空间会被划分为用户空间和系统空间两部分
进程控制块PCB的组成
-
描述信息:进程名/进程标识号、用户名/用户标识号、家族关系
-
控制信息:进程当前状态、进程优先级、程序开始地址、各种计时信息、通信信息
-
资源管理信息:占用内存大小及其管理用数据结构指针、对换覆盖有关信息、共享程序段大小及起始地址、输入输出的设备号及要传送数据的相关信息、指向文件系统的指针及有关标识
-
CPU现场保护结构:存储进程中断时的现场信息
进程上下文
-
进程上下文事实上是个抽象概念,其包含指令、数据、寄存器、堆栈等内容
-
上文:已执行过的进程指令和数据在相关寄存器与堆栈中的内容
正文:正在执行的进程指令和数据在相关寄存器与堆栈中的内容
下文:待执行的进程指令和数据在相关寄存器与堆栈中的内容
-
进程上下文切换发生在不同进程之间,是由于进程调度产生的
-
进程上下文切换的过程
- 保存被切换进程的正文至有关存储区
- 操作系统调度及资源分配,并选取新的进程
- 将被选中进程原先保存至有关存储区的正文恢复出来,并激活该进程执行
进程的状态
-
进程的五状态模型
-
初始状态:进程刚被创建,还未放入就绪队列,这个状态的进程往往还在外存中
-
就绪状态:进程在内存中,运行所需的外部条件均满足,只要给CPU时间就能运行
-
执行状态:进程运行所需的外部条件均满足且正在占用CPU运行
-
等待(阻塞)状态:因等待某种事件发生而不能运行
-
终止状态:执行结束,退出执行被终止,从各队列中移除
-
-
进程状态转换过程
-
进程被创建,处于初始状态
-
初始态→就绪态:若内存中还能放进程,则把一个进程从初始状态转换到就绪状态(装入内存)
-
就绪态→运行态:CPU空闲时,系统调度程序从就绪队列中选择一个新的进程,使其进入执行态
-
运行态→就绪态:当时间片耗尽/有更高优先级的进程处于就绪态/进程自愿释放CPU,运行的进程中断,转入就绪态
-
运行态→终止态:由于进程正常完成/出现错误导致非正常终止/外界干预,进程从运行态转入终止态
-
运行态→阻塞态:进程需要等待某一事件(对资源的访问请求、I/O等)的发生,从运行态转到阻塞态
-
阻塞态→就绪态:该进程等待的事件发生了
-
就绪态→终止态、阻塞态→终止态:父进程(父进程自身终止、父进程要求其子进程终止)导致子进程终止
-
-
当有更多的进程被挂起,内存很可能不够大,无法继续容纳从初始态要转到就绪队列中的进程了,可以引入一个额外的状态“挂起态”,处于挂起态的进程位于外存
当等待队列中的进程太多时,可以将一些进程进入挂起态,换出到外存,之后再将其换入到内存,使其处于就绪态
进程的控制、互斥与同步
进程控制的原语
-
原语是在系统态下执行的某些具有特定功能的程序段,在执行过程中不会被中断(可以通过关中断实现),可以分为机器指令级和功能级两类
-
上述的五状态进程,其各状态之间的转换是通过进程控制原语完成的
创建原语
一个进程可以由系统程序模块统一创建,也可以由父进程创建。
当需要创建进程时,创建原语会先在系统的PCB链表中查看是否有空闲的,若有空闲的,则填入调用者提供的有关参数,然后令该PCB进入就绪队列(内存空间足够情况下)
撤销原语
一个进程可以由于完成工作、发生错误导致非正常终止、父进程要求其终止而被撤销
当需要撤销进程时,撤销原语会先检查该进程是否存在,存在则将该进程从进程链或进程家族中摘下,重新放入空闲可用的PCB队列中,若该进程存在子进程,撤销原语会先撤销其所有子进程
阻塞原语
一个进程期待还未发生的某事件发生时,会自己调用阻塞原语阻塞自己。
当某进程需要阻塞自身时,阻塞原语会中断处理机并保护该进程的现场,该进程置自身为阻塞态后,插入等待队列中,再转到进程调度,使得新的就绪进程得以运行
唤醒原语
当等待队列中的进程期待的事件发生时,期待该事件的所有进程都会被唤醒。
处于等待队列中的进程可以由系统进程唤醒,也可以由事件发生进程唤醒,调用唤醒原语的进程称为唤醒进程。在将被唤醒进程送入就绪队列后,唤醒原语可以返回原调用程序,也可以直接转到进程调度
进程互斥
进程可以并发,但很多资源是有限的,同时只能给一个进程使用,需要使用该资源的进程之间会构成竞争关系,这显然会制约进程的并发
基本概念
-
临界资源
在一段时间内只允许一个进程访问或使用的硬软件资源
-
临界区
临界区就是进程中访问临界资源的一段代码(即不允许多个并发进程交叉执行的一段程序),显然多个进程访问同一临界资源的代码不能交叉执行
-
间接制约
间接制约是由于共享某一公有资源而引起的在临界区内不允许并发进程交叉执行的现象。之所以称为“间接”制约,是因为并发进程通过公有资源而相互制约
-
互斥
不允许两个及以上共享某资源的并发进程同时进入临界区
并发进程互斥执行的准则
-
安全性:互斥使用
-
活动性:不可独占、有限等待
-
公平性:平等竞争、有限等待
平等竞争(空闲让进)
各进程应有平等独立的竞争共享资源的权利
不可独占(让权等待)
并发进程中的某个进程不在临界区时,不能阻止其他进程进入临界区
互斥使用(忙则等待)
当有若干个并发进程申请进入临界区时,只能允许一个进程进入
有限等待
某个并发进程申请进入临界区时,应在有限时间内得以进入
软件实现互斥——Dekker算法
详见下面的文章
硬件加锁实现互斥
即通过对临界资源加锁,实现互斥。显然,在上锁和开锁时,必须不能被中断。
可以使用关中断实现这种锁,还有些计算机在硬件中专门设置了测试与设置指令(Test-and-Set),使得上锁与开锁的操作能够“一气呵成”
硬件加锁法的缺陷
-
可以发现,测试与设置指令中有“返回继续测试”的操作,这会产生“忙等”,浪费CPU的时间
-
由于每个进程自己使用调用加锁开锁,很可能会使得只有被调度执行的进程才有机会进行测试,从而产生饥饿现象
-
关中断的滥用;关中断时间较长;在多CPU系统中,关中断只能在一个CPU中锁住,无法防止其他CPU的进程进入相同的临界区
用信号量和PV原语实现锁
-
信号量
信号量是一个整数,用于表示当前可用的某类临界资源的数量,也就是说,可以为每一类临界资源都设置一个信号量。
-
信号量大于0:表示空闲的该类临界资源的个数
-
信号量等于0:表示该类临界资源全部被占用,且没有进程在等待该临界资源
-
信号量小于0:信号量的绝对值表示正在等待该类临界资源的进程个数
-
-
P原语:也称为wait操作,占用临界资源前调用之
-
V原语:也称为signal操作,用好临界资源后调用之
-
当两个进程需要访问同样的几个临界资源时,普通的记录型信号量可能会由于进程PV操作资源顺序的不同导致死锁,即PV原语不能避免死锁
从流程图中可以发现,PV原语操作都是先对信号量进行操作,再判断信号量与零的关系,所以是大于等于0和小于等于0
另外,还可以注意到,V原语操作中当唤醒一个等待队列中的进程后可以返回调用V原语的程序,也可以转进程调度去轮换执行下一个进程
// PV原语使用示意,其中的s代表某类临界资源的信号量
Process A:
P(s)
(临界区)
V(s)
Process B:
P(s)
临界区
V(s)
可以发现,进程的阻塞永远是自己阻塞自己,但是在PV原语中,唤醒是由其他进程通知的,是相互协作的
进程同步
上述的进程互斥是由于不同进程要访问同一临界资源而导致间接制约,事实上,有些进程之间还存在着直接制约,即有些进程的运行必须按一定顺序来,这称为进程的同步,这时进程各自的执行结果互为对方的执行条件
私有信号量
为了让进程不“忙等”,可以使用“通知”的方法完成同步的目的,这时候需要用到信号量作为进程状态的描述
一般来说,会给每个需要同步的进程设定一个信号量,此处设置的信号量只会被制约进程和被制约进程使用到,称为私有信号量,这点和临界资源的信号量非常不同
用PV原语实现同步
总的来说,对于在一定执行顺序中的一些进程,进程在开始工作前,对自身的私有信号量做P操作,在工作结束后,对下一个进程的私有信号量做V操作。
例子
计算进程与输出进程共用一个缓冲区Buf,信号量Bufempty表示缓冲区空;Buffull表示缓冲区装满数据,初始化Bufempty=1、Buffull=0
Pc为计算进程、Pp为输出进程
可见,两个进程在完成后会互相通知
生产者-消费者问题
将进程大体上分为两类,一类使用数据资源、称为消费者进程;另一类释放数据资源,称为生产者进程。他们进行沟通的区域是一个分为好多块的有界缓冲区
在这个模型中,生产者和消费者进程之间需要同步,即消费者要接收数据时,缓冲区中至少有一个单元是有内容的;生产者要发送数据时,缓冲区中至少有一个单元是空的。另外,还要注意有界缓冲区是个临界资源,生产者和消费者进程访问这个缓冲区时是互斥的
取缓冲区的公用信号量为mutex,表示可用缓冲区的数量,初值为1;生产者进程的私有信号量为avail,表示缓冲区空闲的个数,初值为n;消费者进程的私有信号量为full,表示缓冲区有占用的个数,初值为0
下面假设生产者执行过程deposit,消费者执行过程remove
通过观察,可以得到几个结论
-
用于同步的PV原语是交叉的,即不在同一进程中出现
-
用户互斥的PV原语在同一进程中
-
当用于同步的P操作和用于互斥的P操作在一起时,同步的私有信号量要在互斥的公用信号量之前
进程通信
根据通信内容,可以将进程间的通信分为两种
-
低级通信:传送控制信息
-
高级通信:传送大批量数据
单机系统中的进程通信可以分为4种形式
-
主从式
-
会话式
-
消息或邮箱机制
-
共享存储区方式
消息缓冲机制
这是一种最简单的,进程间直接进行消息传递的方式
在这种方式中,需要用到一个公用缓冲区(一般分为多块),两个进程传送消息的过程如下
-
发送进程准备发送消息,在自身内存空间中设置一个发送区,存入待发送消息
-
发送进程调用发送过程将发送区中的内容发送出去
-
接收进程准备接收消息,在自身内存空间中设置一个接收区
-
接收进程调用接收过程从公用缓冲区中将信息读入接收区
下面是发送过程和接收过程的大致代码描述,其中mutex是整个公用缓冲区的公用信号量,初值为1;SM为接收进程的私用信号量,表示等待接收的消息个数,初值为0
Send(m):
begin
// 此处一定是先申请缓冲区再对mutex进行P操作
// 否则当缓冲区没有空闲块时会造成死锁
向系统申请一块缓冲区(可以理解为给该块缓冲区标记上占用)
P(mutex)
将发送进程发送区中的消息m送入申请的缓冲区
将缓冲区挂入接收进程的消息队列
V(mutex)
V(SM)
end
Receive(n):
begin
P(SM)
P(mutex)
摘下消息队列中的信息
将消息n从缓冲区存入接收区
释放缓冲区(可以理解为给该块缓冲区标记上不占用)
V(mutex)
end
主从式
常用于终端控制进程和终端进程
特点
-
主进程可以自由使用从进程的资源或数据
-
从进程动作受主进程控制
-
主进程和从进程的关系是固定的
会话式
会话系统中,通信进程双方分别称为使用进程和服务进程,常用于用户进程和磁盘管理进程之间的通信。在通信过程中有以下特点
-
使用进程在使用服务进程提供的服务前,需要得到服务进程的许可
-
服务进程根据使用进程的要求提供服务,但对服务的控制仍由服务进程完成
-
使用进程和服务进程在通信时有固定的连接关系
消息或邮箱机制
无论接收进程是否准备好,发送进程都会把要发送的消息送入缓冲区或邮箱。一般来说,消息由发送进程名、接收进程名、数据、有关数据的操作四部分组成。这种方式在通信过程中有以下特点
-
只要存在空的缓冲区或邮箱,发送进程就可以发送消息
-
发送进程与接收进程之间没有直接连接关系
-
双方之间有缓冲区/邮箱用于存放传送的消息
与消息缓冲机制中消息缓冲区被系统中所有进程共享不同,邮箱通信中用于双方数据传送的“邮箱”是属于一对发送/接收进程的大小固定的数据结构
邮箱包括邮箱头和邮箱体两部分,邮箱头描述了邮箱名称、大小、方向、拥有该邮箱的进程名;邮箱体则用于存消息,如下图所示
这时,发送进程和接收进程之间呈现生产者-消费者模型,互相是同步的关系。以下给出示意代码,其中fromnum为发送进程的私用信号量,可代表邮箱体中空闲的块数,初值为邮箱体总块数n;mesnum为接收进程的私用信号量,可代表待接收的消息条数,初值为0
可见,对私用信号量的PV操作是交叉的,两个进程确实是同步的
共享存储区方式
要通信的进程双方通过操作同一共享数据区来进行数据传送,这个共享数据区是每个互相通信进程的一个组成部分
以Unix中的管道通信为例
在Unix类系统中,进程之间可以使用管道进行通信,分为有名管道和无名管道两种。无名管道用于父子进程之间的通信;有名管道可以用于无亲缘关系的进程的通信。以下是管道的一些特点
-
管道逻辑上在文件系统中以特殊文件的形式存在,但是物理上是由高速缓冲区构成,这点与消息缓冲机制逻辑上就在缓冲区不同
-
管道传送消息是先进先出的,并且是单向的消息传送
使用pipe(fd)
来创建个管道,其中fd为int[2]
类型
如上图,fd[0]为管道的读取端、fd[1]为管道的写入端,使用write(fd[1],buf,size)
把buf中长度为size的内容写入管道,使用read(fd[0],buf,size)
把长度为size的内容读出到buf中
死锁
基本概念
-
死锁:各并发进程互相等待对方拥有的资源,且这些并发进程在得到对方的资源前不释放自己已拥有的资源
-
死锁产生的根本原因:系统提供的资源个数小于并发进程要求的该类资源数
-
死锁产生的必要条件
-
互斥条件:并发进程要求的一些资源是临界的,只允许一个进程同时使用
-
不剥夺条件:进程所占有的资源在未使用完成前,不能被其他进程剥夺,只能等在使用的进程主动释放
-
部分分配:进程每次申请资源时,申请的是所需的一部分资源,在等待新资源同时,会继续占用已分配到的资源
-
环路条件:存在进程循环链,其中的每个进程已获得的资源同时被下一个进程请求
-
死锁消除方法
死锁预防
-
破坏“不剥夺条件”
强行打断已经占用某资源的进程,可能会导致该进程前段工作失效
-
破坏“部分分配”
预先给进程分配其所需要的所有资源,显然会造成资源浪费、并发性降低
-
破坏“环路条件”
将资源分类并按顺利排列,规定按递增顺序请求资源。请求时,若需要多个资源,必须按“先低后高”的规则请求,即当占有某资源时,不能申请比该资源低的资源;释放资源时,必须按“先高后低”的规则释放。也就是说,当占用了高的资源而请求低的资源时,必须先释放高的资源
死锁避免
也称为动态预防,关键在于在资源的动态分配中使用一些资源分配算法,从而预测死锁发生的可能性并加以避免,大致可以分为两种策略
-
进程启动拒绝:若进程对资源的申请可能导致死锁,就不启动这个进程
-
资源分配拒绝:若进程对资源的申请可能导致死锁,就不给进程分配该资源
安全序列
安全序列,即一个进程执行的序列,按照该序列执行不会出现死锁
银行家算法
银行家算法采取的是资源分配拒绝的策略,先引入一些需要用到的变量,再叙述算法描述
A v a i l a b l e [ j ] Available[j] Available[j]表示 j j j号资源的个数、 A l l o c a t i o n [ i , j ] Allocation[i,j] Allocation[i,j]表示进程 i i i已经有的资源 j j j的个数、 N e e d [ i , j ] Need[i,j] Need[i,j]表示 i i i进程还需要资源 j j j的个数、 R e q u e s t [ i , j ] Request[i,j] Request[i,j]表示进程 i i i本次申请资源 j j j的数量
整体算法描述如下:
① 若 R e q u e s t [ i , j ] ≤ N e e d [ i , j ] Request[i,j]\le Need[i,j] Request[i,j]≤Need[i,j]则转向②,否则报错(请求资源数超过需要资源数,显然不合理)
② 若 R e q u e s t [ i , j ] ≤ A v a i l a b l e [ j ] Request[i,j]\le Available[j] Request[i,j]≤Available[j]则转向③,否则进程 i i i进入等待态(已有资源数不足)
③ 尝试分配资源,修改相应数据
A v a i l a b l e [ j ] = A v a i l a b l e [ j ] − R e q u e s t [ i , j ] A l l o c a t i o n [ i , j ] = A l l o c a t i o n [ i , j ] + R e q u e s t [ i , j ] N e e d [ i , j ] = N e e d [ i , j ] − R e q u e s t [ i , j ] Available[j]=Available[j]-Request[i,j]\\ Allocation[i,j]=Allocation[i,j]+Request[i,j]\\ Need[i,j]=Need[i,j]-Request[i,j] Available[j]=Available[j]−Request[i,j]Allocation[i,j]=Allocation[i,j]+Request[i,j]Need[i,j]=Need[i,j]−Request[i,j]
④ 执行安全性算法检查如此分配是否安全,若安全,则真正进行分配,否则恢复相应数据的值,使进程 i i i进入等待态
安全性算法会模拟将资源分配给进程 i i i之后的情况,判断是否存在安全序列以保证不会发生死锁,描述如下:
检查当前剩余的资源能否满足某个进程的需求,若可以,将该进程加入安全序列,并回收该进程持有的全部资源。不断重复这个操作,直到不存在满足条件的进程,此时查看是否所有进程都加入了安全序列
死锁检测与恢复
系统需要提供发生死锁后的补救措施,即先通过死锁检测算法发现死锁的存在,再通过死锁解除算法恢复系统
死锁检测算法可以使用资源分配图的简化来完成
死锁恢复常用方法
-
终止各锁住的进程
-
按一定顺序中止进程序列,直到释放的资源足够完成剩下的进程
-
从被锁住进程处强制剥夺资源
线程
基本概念
-
由于进程过于笨重,当并发的进程数多了之后,会大大影响系统的性能
-
与进程相同,线程也是CPU调度的一个基本单位,但是它不作为资源分配的单位,而是共享其所属进程的资源
-
没有线程的进程可以看作一个单线程的进程
-
线程没有自己的地址空间
-
线程有自己的控制块——线程控制块TCB,其中的状态信息主要是相关指针用的堆栈以及寄存器的状态数据
-
线程典型应用:服务器中的文件管理或通信控制、前后台处理、异步处理
线程的分类
总的来说,线程可以分为两类,即用户级线程和核心级线程,分别表示由用户完成操作(使用线程库)以及操作系统核心提供了API的线程处理
内核级线程的上下文切换时间大于用户级线程的上下文切换时间
用户级线程
-
用户级线程只使用用户堆栈和分配给所属进程的用户寄存器
-
线程创建过程与进程类似
-
用户级线程的调度算法与调度过程全部由用户自行选择与确定
-
用户级线程的调度算法只进行线程切换而不进行处理机切换
-
所属进程即使退出CPU,其中的用户级线程可能仍处于执行状态
内核级线程
-
内核级线程由操作系统内核管理,内核给应用程序提供相应系统调用及API
-
内核级线程可以调度到一个CPU上并发执行,也可以被调度到不同CPU上执行
-
用户级线程中第5条的情况在内核级线程中不会出现
线程的执行
线程的基本状态:执行、就绪、阻塞
线程的生命周期
-
派生:线程在进程内派生出,也可以由线程派生,派生后放入就绪队列
-
阻塞:线程在执行过程中需要等待某事件发生,就进入阻塞态
-
激活:线程等待的事件发生,就被激活,重新进入就绪队列
-
调度:即选择一个就绪线程开始执行
-
结束:线程执行结束,其寄存器、堆栈信息都会等待被释放