程序:是静态的,存放在磁盘里的可执行文件,即一系列指令集合。
进程:是动态的,是程序的一次执行过程。
在进程被创建时,操作系统会为进程分配一个“唯一且不重复”的标识,并且为了使每个程序都能独立运行,都配置了一个专门的数据结构来存储进程执行所需要的信息。这个数据结构称为进程控制块(PCB)。
所谓创建进程,实质上就是创建进程实体中的PCB。系统利用PCB来对进程进行控制和管理。
PCB:是进程存在的唯一标识,存储了操作系统管理和控制进程的所有信息
进程实体:PCB+程序段+数据段
①动态性:是进程最基本的特征。进程是程序的一次执行过程,是动态产生和消亡的。
②并发性:内存中由多个进程实体,各进程可并发执行。
③独立性:进程是能独立运行、独立获得资源、独立接受调度的基本单位。
④异步性:各进程各自独立、不可预知的速度来向前推进。操作系统需要”进程同步机制“解决异步问题。
⑤结构性:每个进程都配有PCB。进程由PCB、程序段、数据段组成。
运行态→阻塞态:进程自身主动的行为。
阻塞态→就绪态:等待其他资源分配后才能转为就绪态,所以这是一种被动行为。
运行态→就绪态:当时间片到,或处理机被强占时,进行会从运行态转为就绪态。
单核CPU情况下,同一时刻只会有一个进程处于运行态,多核CPU情况下,可能有多个进程处于运行态。
为了对同一个状态下的各个进程进行统一管理,操作系统会把各个进程的PCB组织起来。
进程的组织方式有:
①链式方式:把PCB分为多个队列,操作系统持有指向各个队列的指针。
②索引方式:根据进程状态不同,建立几张索引表,操作系统持有指向各个索引表指针。
①链式(接)方式
②索引方式
进程控制:主要功能是对系统中所有的进程实施有效的管理,它具有创建新进程、撤销已有进程、实现进程状态转换等功能。
简单来说,进程控制就是要实现进程状态的转换。
实现进程控制:用“原语”实现,原语具有”原子性“,即执行必须一气呵成,期间不可中断。可以用“关中断”和“开中断”这两个特权指令实现。
进程控制主要有:进程创建、进程终止、进程阻塞和唤醒。
(1)进程创建:创建态→就绪态
①申请空白PCB → ②为新进程分配需资源 → ③初始化PCB → ④将PCB插入就绪队列
引起进程创建事件:
①用户登录:分时系统中,用户登录成功后,系统会为其建立一个新的进程
②作业调度:多道批处理系统中,有新作业放入内存时,会为其建立一个新进程。
③提供服务:用户向操作系统提出某些请求时,会新建一个进程处理该请求。
④应用请求:由用户进程主动请求创建一个子进程。
(2)进程终止:就绪态/阻塞态/运行态→终止态
系统需要终止一个进程,调用原语Termination终止进程,需要完成功能:
①从PCB集合中找到终止进程的PCB → ②若程序正在进行,剥夺CPU,将CPU分配给其他进程 → ③终止其所有子进程
→④将该进程拥有的所有资源归还给父进程或操作系统 → ⑤删除PCB
引起进程终止的事件:
①正常结束:表示进程的任务已完成并准备退出运行。
②异常结束:在进程执行过程中,发生了某种异常事件,如除0、非法使用特权指令等。
③外界干预:进程应外界的请求终止执行,如系统操作员干预、父进程请求和父进程终止。
(3)进程阻塞和唤醒:运行态→阻塞态;阻塞态→就绪态
进程进入阻塞时,调用原语Block,需要完成功能:
①找到对于的PCB → ②保护进程运行现场,将其设置为“阻塞态” ,暂时停止进程执行→ ③PCB插入相应事件的等待队列
引起进程阻塞事件:
①需要等待系统分配资源
②需要等待合作的其他进程完成工作
阻塞要进入就绪态时,调用原语Wakeup唤醒进程,需要完成功能:
①在事件等待队列中找到PCB → ②将PCB从等待队列移除,设置进程为就绪态 → ③将PCB插入就绪队列,等待被调度
引起进程唤醒事件:等待事件的发生
(4)进程的切换:运行态→就绪态;就绪态→运行态
进程需要切换状态时候,需要完成功能:
①将运行环境信息存入PCB → ②PCB移入相应队列 → ③选择另一个进程执行,更新PCB → ④根据PCB恢复进程所需要环境
引起进程切换事件:
①进程时间片到
②有更高优先级的进程到达
③当前进程主动阻塞
④当前进程终止
进程间通信(IPC):指两个进程之间产生数据交互。
进程是分配系统资源的单位,所以各进程的内存地址空间相互独立。为了保证安全,一个进程不能访问另一个进程的地址空间。若两个进程需要进行数据交互,必须要有操作系统支持才能完成进程通信。
进程通信方式:共享存储、消息传递、管道通信
(1)共享存储
共享存储:通信的进程之间存在一块可直接访问的共享空间,不同进程通过对共享空间进行读/写操作实现进程之间信息交换。
为避免出错,各进程对共享空间的访问是互斥的。各进可使用操作系统内核提供的同步互斥工作(P、V操作)。
基于存储区的共享:操作系统划分出一块共享存储区,数据交互由通信进程控制,不由操作系统。这种方式灵活性高、速度快,是一种高级通信方式。
基于数据结构的共享:如共享空间只能存放一个长度为10的数组,这种共享方式灵活性低、速度慢、限制多,是一种低级通信方式。
(2**)消息传递**
消息传递:进程间的数据交换以“格式化的消息”为单位。通过操作系统提供的“发送消息/接收消息”两个原语进行数据交换。
格式化消息:消息头+消息体
①消息头:发送进程ID、接收进程ID、消息长度等格式化信息
②消息体:要发送的数据
消息传递方式:
①直接通信方式:消息发送进程要指明接收进程的ID。
【例】在PCB中有一个进程的消息队列,进程P要给进程Q发送通信,
进程P通过将数据打包成“格式化消息“,通过原语send发送到进程Q的PCB中消息队列,进程Q通过receive原语接收。
②间接通信方式:通过“信箱“间接的通信,又称”信箱通信方式“。
【例】进程P向操作系统申请一个信箱,可申请多个信箱。
进程P将数据打包成“格式化消息“,通过send原语往信箱发送消息,进程Q使用receive从信箱A接收消息。
(3)管道通信
“管道”是一个特殊的共享文件,又名pipe文件。这里在内存中开辟一个大小固定的内存缓冲,只能单向写数据单向读数据。
管道数据是先进先出调用,也就是数据结构中的“队列”。各个进程要互斥进行管道访问。
与共享存储的区别在于:中间的内存有无读写限制。
管道只能采用半双工通信:即某一个时间内只能实现单向传输,如果要实现双向同时通信,则需要设置两个管道。
①如果管道写满:写进程将被阻塞,直到读进程将管道的数据取走,即可唤醒写进程。
②如果管道读空:读进程将被阻塞,直到写进程将管道写入数据,即可唤醒读进程。
一旦管道数据被读出,就会彻底消失。当多个进程读同一个管道时,出现错乱。对此解决方案:
①一个管道允许多个写进程,只一个读进程。
②允许有多个写进程,多个读进程,但系统会让各个读进程轮流从管道中读数据。
①用户级线程:这是一种由线程库来实现的,在用户态中实现的。
一个简单的线程库如下C语言代码:
int main(){
int i =0;
while(true){
if(i==0){处理第一个线程的代码;}
if(i==1){处理第一个线程的代码;}
if(i==2){处理第一个线程的代码;}
i = (i+1) %3;
}
}
用户级线程使用线程库来实现,不是操作系统。
线程切换不用不需要进程CPU用户态和核心态的转换,不用操作系统进行干涉。
优点:用户级线程不需要切换到核心态,线程管理系统开销小,效率高
缺点:当一个用户级线程被阻塞后,整个进程会被阻塞,并发度不高。多个线程不可在多核处理机上并行运行。
②内核级线程:由操作系统支持的线程
①一对一模型:一个用户级线程映射到一个内核级线程。就像一个纯粹的内核级线程。
②多对一模型:多个用户级线程映射到一个内核级线程,一个进程只分配一个内核级线程。就像一个纯粹的用户级线程。
③多对多模型:n个用户级线程映射到m个内核级线程,一个用户进程分配m个内核级线程。n≥m。
当有一堆任务要处理,由于资源有限没法同时处理。则需要确定某种规则来决定处理这些任务的顺序,即“调度”所要研究的问题。
调度的三个层次:
①高级调度(作业调度):按照一定原则从外存的作业后备队选一个作业调入内存,并创建进程。对于每个作业只调入一次、调出一次。作业调入时创建PCB,调出时才撤销PCB。
*作业:一个具体的任务,提交一个作业相当于让给操作系统启动一个程序
②中级调度(内存调度):按照某种策略将挂起状态的进程重新调入内存。一个进程可能会被多次调出、调入内存。
*当内存不够时,将某些进程调到外存,进程状态为挂起状态。被挂起的进程PCB会组织成挂起队列。
③低级调度(进程调度):按照某种策略从就绪队列中选取一个进程,将处理机分配被它。进程调度是最基本的一种调度,频率很高。
【补充】七状态模型
1.CPU利用率
利用率 = 有效工作时间 有效工作时间 + 等待时间 利用率=\frac{有效工作时间}{有效工作时间+等待时间} 利用率=有效工作时间+等待时间有效工作时间
2.系统吞吐量
系统吞吐量 = 总共完成作业的数量 总共花的时间 系统吞吐量=\frac{总共完成作业的数量}{总共花的时间} 系统吞吐量=总共花的时间总共完成作业的数量
3.周转时间
周转时间 = 作业完成时间点 − 作业提交时间点 平均周转时间 = 各作业周转时间之和 作业数量 带权周转时间 = 作业周转时间 作业实际运行的时间 平均带权周转时间 = 各作业带权周转时间之和 作业数量 周转时间=作业完成时间点-作业提交时间点 \\ 平均周转时间=\frac{各作业周转时间之和}{作业数量} \\ 带权周转时间=\frac{作业周转时间}{作业实际运行的时间} \\平均带权周转时间=\frac{各作业带权周转时间之和}{作业数量} 周转时间=作业完成时间点−作业提交时间点平均周转时间=作业数量各作业周转时间之和带权周转时间=作业实际运行的时间作业周转时间平均带权周转时间=作业数量各作业带权周转时间之和
4.等待时间
5.响应时间
(1)进程调度的时机
需要进行进程调度和切换情况:
①主动放弃:进程正常终止、运行过程中发生异常而终止、进程主动请求阻塞
②被动放弃:分给进程时间片用完、有更紧急的事件要处理、有更高级的进程进入就绪队列
不能进行进程调度与切换情况:
①在处理中断的过程中,由于中断过程复杂且与硬件相关,很难在中断处理过程进行进程切换
②进程在操作系统内核程序临界区中**(注意,这里不是普通的临界区,是内核程序的临界区)**
③在原语执行过程(原子操作过程)中
【注】临界资源:一个时间内只允许一个进程使用的资源,各进程需要互斥访问临界资源。
临界区:访问临界区资源的那段代码
内核程序临界区:用来访问某种内核数据结构,如进程的就绪队列。没有退出访问就绪序列时,就绪序列为上锁状态,此时进程调度也需要访问就绪序列,导致无法顺利进行进程调度。
(2)进程调度方式
①非剥夺调度方式:又称非抢占式。只允许进程主动放弃处理机,若有更紧迫的任务到达,必须等待当前进程终止或者主动请求进入阻塞态才能执行。无法及时处理紧急任务,适用于早起的批处理系统。
②剥夺调度方式:又称抢占式。当一个进程在处理机上执行时,若有更紧迫的任务到达,立刻暂停正在执行的进程,将处理器分给更紧迫的任务。可优先处理紧急任务,适用于分时操作系统、实时操作系统。
1.先来先服务(FCFS)
【算法思想】按照到达顺序先后顺序进行服务,这是一个非抢占式算法。不会导致饥饿。
适用:作业调度、进程调度
优点:算法实现简单。
缺点:对于排在长作业后面的短作业需要等待长时间,对长作业有利,对短作业不利。
2.短作业优先(SJF)
【算法思想】最短的作业/进程优先服务,也就是剩余时间短的进程优先。
适用:作业调度、进程调度
分为非抢占式算法和抢占式算法:
①非抢占式:执行完一个作业/进程后,就绪队列中剩余时间短的优先执行。
②抢占式:每当有进程加入就绪队列,或者当一个进程完成时,需要调度,如果新到达的进程剩余时间比当前剩余时间更短,则抢占处理机。
优点:可以使平均等待时间、平均周转时间“最短”
缺点:不公平。对短作业有利,做长作业不利。可能会出现饥饿现象。
3.高响应比优先(HRRN)
【算法思想】每次调度时计算各作业/进程的响应比,选择响应比最高的作业/进程为其服务。这是一个非抢占式算法。
适用:作业调度、进程调度
响应比=(等待时间+要求服务时间)÷要求服务时间
优点:不会导致饥饿现象
4.时间片轮转(Round-Robin)
【算法思想】按照进程到达就绪队列的顺序,轮流让各个进程执行一个时间片。进程在一个时间片未执行完则剥夺处理机,将进程重新放到就绪队列队尾重新排队。**这是一种抢占式算法。**由时钟装置发出时钟中断来通知CPU时间片已到。
适用:进程调度(只有作业放入内存建立了相应的进程后,才能被分配处理机时间片)
优点:响应时间快,常用于分时操作系统。不会导致饥饿现象。
缺点:进程切换频率较高,有一定开销。
5.优先级调度
【算法思想】根据作业/进程优先级,调度时选择优先级最高的作业/进程。
适用:作业调度、进程调度。
分为非抢占式算法和抢占式算法:
①非抢占式:指需在进程主动放弃处理机时进行调度
②抢占式:在就绪队列变化时,检查是否发生抢占
优点:用优先级来区分紧急、重要程度,适用于实时操作系统。
缺点:可能会导致饥饿现象。
6.多级反馈队列调度算法
【算法思想】
①设置多级就绪队列,队列之间优先级按照从高到低,时间片从小到大。
②新进程到达时先进入第1级队列,按照先来先服务排队分配时间片,时间片用完进程未结束时进入下一级队列队尾。如果已经是最下级,重新放回该队列队尾。
③只有第k级队列空时,才会为k+1级队头进程分配时间片
④这是一种抢占式算法
缺点:会导致饥饿现象
7.多级调度算法
【例子】
队列之间有两种划分方式:
①固定优先级:高优先级队列为空才调度低优先级队列。这是不合理的
②时间片划分:三个队列分配时间,如50%、40%、10%。这样能保证在一段时间内,每个队列都可能会被访问一次。
队列内可采用不同调度策略,例如:
①系统进程队列采用优先级调度
②交互式队列采用时间片轮转
③批处理队列采用先来先服务
(1)进程同步
(2)进程互斥
临界资源:在一段时间内只允许一个进程使用的资源。
互斥:间接制约关系,当一个进程进入临界区使用临界资源,另一个进程必须等待。当前访问的进程访问结束后才能进行访问。
(3)临界资源
对临界资源的互斥访问,在逻辑上分为四个部分:
①进入区:负责检查是否可进入临界区,若可以进入,则设置正在访问临界资源标志(上锁),用来阻止其他进程同时进入临界区。
②临界区:访问临界资源的那段代码。
③退出区:负责解除正在访问临界资源的标志。
④剩余区:做其他处理。
注:临界区是进程访问临界资源代码段;进入区和退出区负责实现互斥的代码段。临界区也称临界段。
do{
entry section; //进入区
critical section; //临界区
exit section; //退出区
remainder section; //剩余区
}while{true}
(4)互斥规则
为了实现临界资源的互斥访问,保证系统整体性能,需要遵循以下规则:
①空闲让进:临界区空闲时,可以允许一个请求进入临界区的进程进入临界区
②忙则等待:当已有进程进入临界区时,其他请求进入临界区的进程必须等待
③有限等待:对请求访问的进程,应保证能在有限时间内进入临界区(保证不会饥饿)
④让权等待:当进程不能进入临界区时,应立即释放处理机,防止进程忙等待。
1.单标志法
【算法思想】两个进程在访问完临界区后会把使用临界区权限转交给另一个进程。即每个进程进入临界区权限只能由另一个进程赋予。
int turn=0; //用来表示当前进入临界区的进程号
//p0进程 //p1进程
while(turn !=0 ); while(turn !=1 ); //进入区
critical section; critical section; //临界区
turn = 1; turn = 0; //退出区
remainder section; remainder section; //剩余区
turn初始值为0,即刚开始只允许0号进程进入临界区。
①若P1进程先上处理机运行,则会卡在while(turn !=1),直到p1时间片用完,发生调度,切换p0上处理机运行。
②代码while(turn !=0),不会不会卡住,p0正常访问临界区。
③如果p0访问临界区期间切换回p1,p1依旧会卡主。
④只有p0在退出区将turn改为1后,p1才能进入临界区。
综上:此算法能够实现**“同一时刻最多只允许一个进程访问临界区”**。
注:这个算法违反了“空闲让进”
2.双标志先检查法
【算法思想】设置一个布尔型数组flag[],用来标记各进程想进入临界区的意愿。如"flag[0] = true"表示0号进程想要进入临界区。在检查后进行上锁。
bool flag[2]; //设置进入临界区意愿的数组
flag[0] = false;
flag[1] = false; //刚开始设置两个进程都不想进入临界区
//p0进程 //p1进程
while(flag[1]); while(flag[0]); //①进入临界区前检查有没有别的进程想进入临界区
flag[0] = true; flag[1] = true; //②如果没有别的进程进入临界区,标记自身想要进入临界区(上锁)
critical section; critical section; //访问临界区
flag[0] = flase; flag[1]=flase; //退出区
remainder section; remainder section;
这里的①合②都属于进入区。
注:这个算法违反了“忙则等待”:如果p0执行完while,p1执行while,接着p0执行标记,p1再进行标记是可以进行标记的。
3.双标志后检查法
【算法思想】设置一个布尔型数组flag[],用来标记各进程想进入临界区的意愿。如"flag[0] = true"表示0号进程想要进入临界区。先上锁后再检查。
bool flag[2]; //设置进入临界区意愿的数组
flag[0] = false;
flag[1] = false; //刚开始设置两个进程都不想进入临界区
//p0进程 //p1进程
flag[0] = true; flag[1] = true; //②如果没有别的进程进入临界区,标记自身想要进入临界区(上锁)
while(flag[1]); while(flag[0]); //①进入临界区前检查有没有别的进程想进入临界区
critical section; critical section; //访问临界区
flag[0] = flase; flag[1]=flase; //退出区
remainder section; remainder section;
注:这个算法解决了“忙则等待”,但是违背了“空闲让进”和“有限等待”,会让进程长期无法访问临界资源产生饥饿现象。:如果先标记p0,然后标记p1,接着两个进程的while都过不去。
4.Peterson算法
【算法思想】先标记自己想进入临界区,然后标记可以让对方先进入。检测对方是否想进入临界资源。
bool flag[2]; //表示进入临界区意愿数组,初始值都是false。
int turn = 0; //表示优先让哪个进程进入临界区
//p0进程 //p1进程
flag[0] = true; flag[1] = true; //标记自己想进入临界资源
turn = 1; turn = 0; //表示对方可以优先进入
while(flag[1]&&turn==1); while(flag[0]&&turn ==1); //如果对方想进入,且turn标记对方优先,则等待。
critical section; critical section; //访问临界区
flag[0] = flase; flag[1]=flase; //退出区
remainder section; remainder section;
1.中断屏蔽方法
...
关中断; 关中断后即不允许当前进程被中断,也必然不会发生进程切换。
临界区;
开中断; 进程访问完临界区,执行开中断指令,才有可能别的进程上处理器并访问临界区
...
2.硬件指令方法
(1)TestAndSet指令
//用C语言描述的逻辑如下
//布尔型共享变量lock表示当前临界区是否被加锁
//true表示被加锁,false表示未加锁。
bool TestAndSet (bool *lock){
bool old;
old = *lock; //old用来存放lock原来的值
*lock = true; //无论之前是否已加锁,都将lock设为true
return old; //返回原来lock的值
}
//以下是使用TSL指令实现互斥的算法逻辑
while(TestAndSet(&lock)); //上锁并检查
临界区代码段...
lock = false; //解锁
剩余区代码段...
(2)Swap指令
//用C语言描述的逻辑如下
//Swap指令是用于交换两个变量值
Swap(bool *a,bool *b){
bool temp;
temp = *a;
*a = *b;
*b = temp;
}
//以下是用Swap指令实现互斥的算法逻辑
//lock表示当前临界区
bool old = true;
while(ole == true)
Swap(&lock,&old);
临界区代码段...
lock = false;
剩余区代码段...
acquire(){
while(!available)
; //忙等待
available = false; //获得锁
}
release(){
availbale = true; //释放锁
}
1.整型信号量
int S=1; //初始化整型信号量S,表示当前系统中某个资源数量
void wait(int S){ //wait原语,也就是“进入区”
while(S <=0 ); //资源不够一直循环等待
S=S-1; //资源够跳出wihle循环,占用一个资源
}
void signal(int S){ //signal原语,也就是“退出区”
S=S+1; //资源使用完,在退出区释放资源。
}
在”进入区“中,”检查“和”上锁“一气呵成,避免了并发、异步导致的问题。
在P操作,即wait操作中,只要信号量S<=0就会一直测试,使进程出现“忙等”不满足“让权等待”。
2.记录型信号量
//定义记录型变量
typedef struct{
int value; //剩余资源数
struct process *L; //等待队列
} semaphore;
//wait操作(P操作)
void wait(semaphore){
S.value--;
if(S.value<0){ //若获取资源,如果资源数小于“0”,表示没有资源给进程使用
block(S.L); //block原语:使进程从运行态进入阻塞态,并挂到信号量S的等待队列(阻塞队列)中。
}
}
//signal操作(V操作)
void signal(semaphore){
S.value++;
if(S.value<=0){ //当释放一个资源后,如果剩余资源数小于等于“0”,表示当前还有进程在等待资源
wakeup(S.L); //wakeup原语:唤醒等待队列中的一个进程,该进程从阻塞态变为就绪态。
}
}
1.实现互斥
//实现互斥
semaphore mutex=1; //初始化信号量
//p1进程
p1(){
...
p(mutex); //使用临界区资源需要加锁
临界区代码段...
V(mutex); //使用临界资源后需要解锁
......
}
对于不同临界资源需要设置不同的互斥信号量。
P、V操作必须成对出现:
①缺少P操作会不能保证临界资源互斥访问
②缺少V操作会导致资源永不释放,等待进程不会被唤醒。
2.实现同步
进程同步:让各并发进程按要求有序地推进。
用信号量实现同步:
①分析需要实现的同步关系,即必须保证“一前一后”执行的两个操作
②设置同步信号量S,初始值为0
③在先操作的步骤之后执行V操作,释放一个信号量
④在后操作的步骤之前执行P操作,获取信号量
//信号量实现同步机制
semaphore S=0; //初始化同步信号量,初始值为0
//假设代码4需要在代码2之后执行
//需要在代码2之后添加一个V操作,释放一个信号量
//在代码4之前添加一个P操作,获取信号量
P1(){
代码1;
代码2;
V(S);
代码3;
}
P2(){
P(S);
代码4;
代码5;
代码6;
}
3.信号量机制实现前驱关系(多级同步)
【例】进程P1中有代码S1,进程P2中有代码S2……,这些代码要求按照前图来执行。
【分析】
每一对前驱关系都是一个进程同步问题,即需要保证”一前一后“操作。
①要对每个前驱关系设置一个同步信号量
②在“前操作”之后执行V操作
③在“后操作”之前执行P操作
例如:S1中的进程信号量
【实现】
//P1进程 //P2进程 //P3进程 //P4进程 //P5进程 //P6进程
P1(){ P2(){ P3(){ P4(){ P5(){ P6(){
... ... ... ... ... ...
S1; P(a); P(b); P(c); P(d); P(e);
V(a); S2; S3; S4; S5; P(f);
V(b); V(c); V(g); V(e); V(f); P(g);
.... V(d); ... ... ... S6;
} ... } } } ...
} }
1.简单的生产者-消费者
【问题描述】
系统中有一组生产者进程和一组消费者进程,生产者每次生产一个产品(一组数据)放入缓冲区,消费者进程每次从缓冲区中取出一个产品使用。
生产者、消费者共享一个初始为空、大小为n的缓冲区。
只有缓冲区没满时,生产者才可以把产品放入缓冲区,若满了需要等待。
只有缓冲区不为空时,生产者才能从中取出产品,若为空需要等待。
缓冲区是临界资源,各进程必须互斥地访问。
【实现】
semaphore mutex = 1; //互斥信号量。实现对缓冲区的互斥访问
semaphore empty = n; //同步信号量,表示空闲缓冲区的数量
semaphore full = 0; //同步信号量,表示产品数量,也就是非空缓冲区数量。
//生产者进程
producer(){
//生产一个产品
P(empty); //消耗一个空闲缓冲区
P(mutex); //对临界资源上锁
把产品放入缓冲区;
V(mutex); //解锁
V(full); //增加一个产品
}
//消费者进程
consumer(){
//消费一个产品
P(full); //消耗一个产品
P(mutex); //对临界区上锁
从缓冲区取一个产品;
V(mutex); //解锁
V(empty); //增加一个空闲缓冲区
使用产品;
}
实现互斥的P操作一定要在实现同步的P操作之后,不然会发生“死锁"现象。
V操作顺序颠倒不会有影响。
2.复杂的生产者-消费者的问题
【问题描述】
桌子上只有一个盘子,每次只能放入一个水果。
爸爸每次向盘子中放苹果,妈妈每次向盘子中放橘子。
女儿从盘子中只拿苹果,儿子从盘子中只拿橘子。
只有盘子为空,爸爸或妈妈菜可以向盘子放一个水果。
当盘子有自己需要的水果时,女儿或儿子才可以从盘子中取水果。
父亲将苹果放入盘子后,女儿才能取苹果。
母亲将橘子放入盘子后,儿子才能取橘子。
只有盘子为空时,父亲或母亲才能放入水果。
对缓冲区(盘子)的访问需要互斥进行。
【实现】
semaphore apple = 0; //盘子中苹果个数
semaphore orange = 0; //盘子中橘子个数
semaphore plate = 1; //盘子还可以放入多少个水果
//父亲放苹果进程
dad(){
while(1){
准备一个苹果;
P(plate);
把苹果放入盘子;
V(apple);
}
}
//母亲放橘子进程
mom(){
while(1){
准备一个橘子;
P(plate);
把橘子放入盘子;
V(orange);
}
}
//女儿拿苹果进程
doughter(){
while(1){
P(apple);
从盘中取苹果;
V(plate);
清空盘子;
}
}
//儿子拿橘子进程
son(){
while(1){
P(orange);
从盘中取橘子;
V(plate);
清空盘子;
}
}
3.吸烟者问题
【问题描述】
假设有三个抽烟者进程和一个供应进程。每个抽烟者进程抽一支烟需要三种材料:烟草、纸、胶水。三个抽烟者中各有一种材料且不相同。供应者进程提供缺少的材料组合放到桌子上。
实际上供应者提供了三种组合材料:
组合一:纸+胶水
组合二:烟草+胶水
组合三:纸+烟草
同步关系:
桌上有组合一→只有“烟草”进程取走东西
桌上有组合二→只有“纸”进程取走东西
桌上有组合三→只有“胶水”进程取走东西
发出完成信号→供应者将下一个组合放到桌上
【实现】
semaphore offer1 = 0; //桌上组合一的数量
semaphore offer2 = 0; //桌上组合二的数量
semaphore offer3 = 0; //桌上组合三的数量
semaphore finish = 0; //抽烟完成信号量
int i = 0; //实现“三个轮流抽烟”
//生产者进程
provider(){
while(1){
if(i==0){
将组合一放桌上;
V(offer1);
}else if(i==1){
将组合二放桌上;
V(offer2);
}else if(i==2){
将组合三放桌上;
V(offer3);
}
i = (i+1)%3;
P(finish);
}
}
//只有“烟草”进程
skmoer1(){
while(1){
P(offer1);
从桌上拿走组合一:纸+胶水;
V(finish);
}
}
//只有“纸”进程
skmoer2(){
while(1){
P(offer2);
从桌上拿走组合二:烟草+胶水;
V(finish);
}
}
//只有“胶水”进程
skmoer3(){
while(1){
P(offer3);
从桌上拿走组合三:纸+烟草;
V(finish);
}
}
【问题描述】
有读者和写者两组并发进程,共享一个文件。当两个或两个以上读进程同时访问共享数据时不会产生错误,如果**某个写进程与其他进程(读进程或者写进程)**同时访问共享数据时可能产生错误。
也就是读进程读数据不会改变数据,写进程才会改变数据。
①允许多个读者可同时对文件执行读操作。
②只允许一个写者往文件中写信息。
③任一写者在完成写操作之前不允许其他读者或写者工作。
④写者执行写操作前,应让已有的读者和写者全部退出。
写进程—写进程;写进程—读进程。
【实现】
semaphore rw=1; //用于实现共享文件互斥访问
int count = 0; //用于记录当前读进程访问文件数量
semaphore mutex = 1; //用户对count互斥访问,以免读进程在上锁时阻塞
//写进程
writer(){
while(1){
P(rw); //写之前加锁
写文件...
V(rw); //解锁
}
}
//读进程
reader(){
while(1){
P(mutex); //第一个读进程负责加锁解锁,所以需要对count变量互斥访问
if(count==0) P(rw); //第一个读进程负责加锁
count ++; //记录读进程数
V(mutex); //对count变量解锁
读文件...
P(mutex); //第一个进程解锁,对count变量互斥访问
count--; //读完文件,进程数-1
if(count == 0) V(rw); //第一个读进程负责解锁
V(mutex); //对count变量解锁
}
}
semaphore rw=1; //用于实现共享文件互斥访问
int count = 0; //用于记录当前读进程访问文件数量
semaphore mutex = 1; //用户对count互斥访问,以免读进程在上锁时阻塞
semaphore w = 1;
//写进程
writer(){
while(1){
p(w); //在上锁之前申请w资源,用来防止不断有读进程访问,写进程发生饿死现象
P(rw); //写之前加锁
写文件...
V(rw); //解锁、
V(w);
}
}
//读进程
reader(){
while(1){
P(w); //在上锁之前申请w资源,用来防止不断有读进程访问,写进程发生饿死现象
P(mutex); //第一个读进程负责加锁解锁,所以需要对count变量互斥访问
if(count==0) P(rw); //第一个读进程负责加锁
count ++; //记录读进程数
V(mutex); //对count变量解锁
V(w);
读文件...
P(mutex); //第一个进程解锁,对count变量互斥访问
count--; //读完文件,进程数-1
if(count == 0) V(rw); //第一个读进程负责解锁
V(mutex); //对count变量解锁
}
}
【问题描述】
一张圆桌上有5名哲学家,有5根筷子,每两个哲学家之间摆一根筷子。
哲学家思考时不影响他人,需要吃饭时识图拿左、右两根筷子(一根一根拿)。
如果筷子在其他人手上,需要等待。且只有拿两根筷子才会吃饭,吃完饭后放下筷子继续思考。
【实现】
semaphore chopstick[5]={1,1,1,1,1};
semaphore mutex = 1; //互斥信号量保证每个哲学家拿筷子时互斥
Pi(){
while(1){
P(mutex);
P(chopstick[i]); //拿左边筷子
P(chopstick[(i+1)%5]);//拿右边筷子
V(mutex);
吃饭...
V(chopstick[i]); //放下左边筷子
P(chopstick[(i+1)%5]);//放下右边筷子
思考...
}
}
死锁:在并发环境下,各进程因竞争资源造成一种互相等待对方手里的资源,导致各进程都阻塞无法向前推进。
饥饿:进程长期得不到想要的资源,无法向前推进。
死循环:某进程执行过程中跳不出某个循环的现象。
死锁一定同时满足四个条件:
①互斥条件:只有对必须互斥使用资源的争抢才会导致死锁
②不剥夺条件:进程在获得资源未使用完之前,不能被其他进程强行剥夺,只能主动释放。
③请求和保持条件:进程已经至少保持了一个资源,且提出新的资源请求且该资源被其他进程占用,此时请求进程被阻塞且不释放已有资源。
④循环等待条件:存在一种进程资源的循环等待链。
注:发生死锁一定有循环等待,但是发生循环等待未必死锁。
总之:对不可剥夺资源的不合理分配,可能导致死锁。
死锁的处理策略:
①预防死锁:破坏死锁产生的四个必要条件中的一个或几个。
②避免死锁:用某种方法防止系统进入不安全状态,如银行家算法
③死锁的检测与解除:允许死锁发生,操作系统负责检测出死锁的发生然后采取某种措施解除死锁。
1.破坏互斥条件
互斥条件:只有对必须互斥资源的争抢才会导致死锁
破坏互斥条件:如果把只能互斥使用资源改造为允许共享使用,则系统不会进入死锁状态。如,SPOOLing技术。
缺点:不是所有资源都可以改造成共享资源。并且为了系统安全,很多地方需要保护这种互斥性,所以应用范围不广。
2.破坏不剥夺条件
不剥夺条件:进程在获得资源未使用完之前,不能被其他进程强行剥夺,只能主动释放。
破坏不剥夺条件:
方案一:当进程请求新的资源得不到满足时,必须立即释放持有的所有资源,需要时再重新申请。
方案二:某个进程需要的资源被其他进程占用时,由操作系统协助,根据进程优先级对资源强行剥夺。
缺点:实现起来复杂;释放已获得资源可能造成前一阶段工作实效;反复申请释放资源增加系统开销降低吞吐量。
采用方案一,可能会导致饥饿。
只适用于已保存和恢复状态的资源,如CPU
3.破坏请求和保持条件
4.破坏循环等待条件
【银行家算法】用于避免死锁
假设:系统有三种资源,总数为{10,5,7},在已经分配给各个进程后剩余可用资源{3,3,2},分配给进程资源如下。
依次检查剩余资源数可否满足各个进程需求:
①满足P1要求,P1加入安全队列。更新可用资源{5,3,2}。
②满足P3要求,P3加入安全队列。更新可用资源{7,4,3}
以此检查得到一个安全序列p1→p3→p0→p2→p4
所以:系统处于安全状态,暂时不可能发生死锁。
若允许死锁发生,对死锁处理应当提供两个算法:
①死锁检测算法:检测系统状态是否已经发生死锁。
②死锁解除算法:当系统确定已经发生死锁,则利用此算法从死锁中解脱出来。
1.死锁检测
资源分配图:
①有两种点:进程结点,资源结点
②两种边:进程结点→资源结点:申请资源;(每个边代表一个)
资源结点→进程结点:已分配资源。(每个边代表一个)
如果一个进程申请的资源能够得到满足,即可消除与它相关的边(申请与分配)。
如果按照分析后最终能消除所有边,这个图是可完全简化的,此时一定没有发生死锁。
如果最终不能消除所有边,那么此时发生了死锁。最终连着的边的进程就是处于死锁状态。
2.死锁解除
①资源剥夺法:挂起某些死锁进程,抢占它的资源,将资源分配给其他死锁进程。且应该防止被挂起的进程长时间得不到资源而饥饿。
②撤销进程法:强制撤销部分、甚至全部死锁进程。
③进程回退法:让一个或多个死锁进程回退到可以避免死锁。
决定对哪个进程动手:
①进程优先级
②已执行时间
③剩余执行时间
④进程已经使用了多少资源
⑤进程是交互式或者批处理式
被下一个进程所请求。
破坏循环等待条件:采用顺序资源分配法。给系统资源编号,规定每个进程必须按编号递增顺序请求资源,同类资源一次申请完。
原理:一个进程只有已占有小编号资源,才有资格申请更大编号资源。持有大编号资源进程不可能逆向申请小编号资源。
缺点:不方便添加新设备;实际使用资源顺序与编号递增不一致会导致资源浪费;必须按照规定申请资源,用户编程麻烦。
【银行家算法】用于避免死锁
假设:系统有三种资源,总数为{10,5,7},在已经分配给各个进程后剩余可用资源{3,3,2},分配给进程资源如下。
[外链图片转存中…(img-L5jSUOQY-1694944895209)]
依次检查剩余资源数可否满足各个进程需求:
①满足P1要求,P1加入安全队列。更新可用资源{5,3,2}。
②满足P3要求,P3加入安全队列。更新可用资源{7,4,3}
以此检查得到一个安全序列p1→p3→p0→p2→p4
所以:系统处于安全状态,暂时不可能发生死锁。
若允许死锁发生,对死锁处理应当提供两个算法:
①死锁检测算法:检测系统状态是否已经发生死锁。
②死锁解除算法:当系统确定已经发生死锁,则利用此算法从死锁中解脱出来。
1.死锁检测
资源分配图:
①有两种点:进程结点,资源结点
②两种边:进程结点→资源结点:申请资源;(每个边代表一个)
资源结点→进程结点:已分配资源。(每个边代表一个)
[外链图片转存中…(img-ntuo9MRL-1694944895209)]
如果一个进程申请的资源能够得到满足,即可消除与它相关的边(申请与分配)。
如果按照分析后最终能消除所有边,这个图是可完全简化的,此时一定没有发生死锁。
如果最终不能消除所有边,那么此时发生了死锁。最终连着的边的进程就是处于死锁状态。
2.死锁解除
①资源剥夺法:挂起某些死锁进程,抢占它的资源,将资源分配给其他死锁进程。且应该防止被挂起的进程长时间得不到资源而饥饿。
②撤销进程法:强制撤销部分、甚至全部死锁进程。
③进程回退法:让一个或多个死锁进程回退到可以避免死锁。
决定对哪个进程动手:
①进程优先级
②已执行时间
③剩余执行时间
④进程已经使用了多少资源
⑤进程是交互式或者批处理式
参考书籍:计算机操作系统(第四版)——汤小丹;王道操作系统复习指导
参考视频:王道计算机考研操作系统