大部分内容基于中国大学MOOC的2021考研数据结构课程所做的笔记,后续又根据2023年考研的大纲增加了一些内容,主要有操作系统引导、虚拟机、多级队列调度算法、互斥锁、调度器和闲逛进程、内存映射文件、文件系统的全局结构、虚拟文件系统、固态硬盘SSD、输入输出应用程序接口 、驱动程序接口等等。
感谢我的室友HXN,他帮我写了一部分第五章的内容。
课程内容和西电平时讲课的内容大致重合,西电可能每章会多讲一点UNIX系统的实例,可以听完这课再快速过一遍老师的课件防止漏掉什么内容。
这门课讲的其实不算特别硬核,没怎么涉及具体的代码。不过我其实感觉操作系统是个大无底洞,能学到多深基本取决于愿意花多少时间和精力。如果有闲心,推荐看下南大蒋炎岩老师的《操作系统:设计与实现》和哈工大李治军老师的《操作系统》,讲的更深入,当然难度也相应的大的多。
其他各章节的链接如下:
进程同步,进程互斥
什么是进程同步
知识点回顾:进程具有异步性的特征。异步性是指,各并发执行的进程以各自独立的,不可预知的速度向前推进
假设有两个并发执行的进程,每一个进程会有一系列要求执行的指令,由于这些并发执行的进程具有异步性,所以这些指令的推进顺序是我们不可预知的
有的时候这种异步性是我们必须要解决的一个问题,我们必须要保证各个进程之间的推进次序是按我们想要的那种顺序来依次推进的。那么操作系统就需要提供一个叫做进程同步的机制来实现这种需求
再看另一个例子:进程通信 —— 管道通信
什么是进程互斥
“同时”指的是宏观上的同时,微观上可能这些进程是交替地在访问这些共享资源的
当“上锁”后,其他的进程想要访问临界资源时,在进入区进行检查就会发现此时已经有一个进程正在访问临界资源,那么其他的进程就会被阻止进入临界区访问临界资源
要通过打印机打印输出的话,对打印机执行写操作这段代码就需要写在临界区里
忙等待:这个进程暂时没办法往下推进了,但是这个进程还一直占用着处理机,使处理机一直处于忙碌的状态,没有办法给别的进程进行服务
进程互斥的软件实现方式
如果没有注意进程互斥?
单标志法
双标志先检查法
双标志后检查法
Peterson 算法
尽管各个进程是并发运行的,但是它们对turn值的设置肯定有先后顺序,最后是谁设置了turn值,就说明最后是谁作出了“谦让”的动作,这个进程就会失去行动的优先权,它就会让对方优先进入临界区
如果此时进程进不了临界区,那么就应该立即释放处理机资源而不是继续在CPU上跑,但是该算法的处理方式是让进程一直被卡在while循环,其实自己还一直在CPU上执行,不断地检查while循环的条件是否满足,所以虽然这个进程此时进不了临界区,但它依然会占用CPU资源,故没有满足让权等待的原则
进程互斥的硬件实现方式
中断屏蔽方法
关中断指令只对执行关中断指令的处理机有用
TestAndSet指令
Swap指令
互斥锁
进程互斥:锁
信号量机制
之前学习的那些进程互斥的解决方案分别存在哪些问题?
进程互斥的四种软件实现方式(单标志法,双标志先检查,双标志后检查,Peterson算法)
进程互斥的三种硬件实现方式(中断屏蔽方法,TS/TSL指令,Swap/XCHG指令)
1.在双标志先检查法中,进去区的”检查“,”上锁“操作无法一气呵成,中间有可能先执行了检查就进行了进程切换,从而导致了两个进程有可能同时进入临界区的问题;
2.所有的解决方案都无法实现”让权等待“
其中单标志法,双标志先检查,双标志后检查都存在着比较严重的一些问题的隐患。Peterson算法还有后面的三种硬件实现方式其实问题都不大,但是这些方式也都无法解决”让权等待“原则
1965年,荷兰学者Dijkstra提出了一种有效的实现进程互斥,同步的方法 —— 信号量机制
信号量机制
信号量机制 —— 整型信号
用原语来实现“检查”和“上锁”,避免了双标志先检查法那种两个进程同时进入临界区的问题
如果一个进程暂时进不了临界区,它会一直占用处理机循环检查从而导致忙等
信号量机制 —— 记录型信号
在这种信号量当中它还会保持一个指向等待这种系统资源的等待队列,指向等待它的那些进程
如果减1之后导致 S.value<0,就说明它在减1之前其实已经没有这种系统资源来分配给当前申请这种资源的进程了
进程主动地执行block原语把自己阻塞放弃处理机并且把它挂到这个信号量对应的等待队列当中
如果加1之后 S.value<=0,就说明在这个进程释放资源之前依然还有一些进程是处于等待队列的,所以就需要再调用wakeup原语从信号量对应的等待队列当中唤醒其中的某一个进程(一般是队头的进程),让它从阻塞态回到就绪态,并且把它所申请所等待的资源分配给它
可以把信号量的初始值设置为2,刚开始等待这个打印机资源的等待队列肯定是空的(null)。各个进程在使用打印机之前需要先用wait原语来申请打印机资源,在使用完之后又需要执行signal原语来释放一个打印机资源
对这个例子的具体讲解略过不记,应该不难理解,不懂自己去看下视频
S.value+1后>0说明已没有进程在等待该资源
S.value=0,资源恰好分配完
信号量实现互斥,同步,前驱关系
一个信号量对应一种资源
信号量的值 = 这种资源的剩余数量(信号量的值如果小于0,说明此时有进程在等待这种资源)
P(S) —— 申请一个资源S,如果资源不够进程就执行block原语主动阻塞等待
V(S) —— 释放一个资源S,如果有进程在等待该资源,则用wakeup原语唤醒一个正在等待的阻塞进程
信号量机制实现进程互斥
划定临界区:哪一段代码是用于访问临界资源的
可以认为互斥信号量mutex所表示的资源是进入临界区的名额,初始值为1表示刚开始可以进入临界区的名额只有1个。P(mutex)是申请一个进入临界区的名额,V(mutex)归还名额
信号量机制实现进程同步
比如说有P1,P2这两个进程,当它们在系统当中并发地执行的时候由于系统的环境很复杂,所以操作系统调度时有可能是P1先上处理机运行,也有可能是P2先上处理机运行。比如说P2先上处理机运行了代码4和代码5,而此时它时间片用完了那又切换回P1,P1运行了代码1和代码2接下来又切换回P2运行了代码6
总之由于这两个进程在系统中并发运行,因此它们之间的代码执行先后顺序是我们所不可预知的
具体是什么资源没必要关心
前V:设置一个信号量初始值为0,表示刚开始这种资源是没有的,而只有执行前面那个操作的进程可以释放这种资源
后P:执行后面那个操作的进程在它的操作之前需要申请一个这个资源,当它申请的这个资源得不到满足的时候,这个进程就会阻塞,只能由前面那个进程把它唤醒
下面看怎么利用前V后P这个关键技巧来解决更复杂的进程同步问题
信号量机制实现前驱关系
在这个前驱图当中包含了很多对的进程同步关系,每一条线其实就是代表一个一前一后的同步问题
生产者消费者问题
问题描述
缓冲区是用来存放数据的一片区域。大小为n表示有n个小格子,可以放n个产品
如果各个进程同时访问缓冲区的话有可能会出现一系列的问题,比如说数据覆盖
通过以上分析找出了这个问题里面所隐含的一系列同步和互斥关系
问题分析
根据各进程的操作流程确定P,V操作的大致顺序:前V后P
只有当缓冲区里有产品的时候,消费者进程才可以消费;只有缓冲区没满的时候,生产者进程才可以生产。这样的两对一前一后的同步关系,需要给它们分别设置一个同步信号量,并且在前面这个动作完成之后需要对这个同步信号量执行一个V操作,在后面这个动作开始之前需要对这个同步信号量执行一个P操作
对缓冲区临界资源的互斥访问只需要设置一个互斥信号量,并且让它的初值为1然后在临界区的前后分别对互斥信号量执行PV操作就可以了
如何实现
- 生产者进程要做的事情是不断地生产一个产品并且把产品放到缓冲区,而消费者进程要做的事情是不断地从缓冲区里取走一个产品并且使用这个产品
- 生产者进程在把产品放入缓冲区之前,需要申请一个空闲的缓冲区,因此当它放入产品之前需要执行P(empty)。而它把产品放入缓冲区之后,相当于产品这种资源的数量加1,或者说非空缓冲区的数量加1,因此当它放入产品之后需要执行V(full)表示增加一个这种资源
- 而消费者进程的分析也类似,当它从缓冲区取走一个产品之前,需要执行P(full),也就是要申请消耗一个产品这种资源。而当它取走了一个产品之后空闲缓冲区的数量就会加1,因此在这个操作之后需要执行V(empty)表示要增加一个空闲缓冲区
- 另外题目中要求缓冲区必须互斥访问,所以在访问缓冲区前后分别要对mutex这个互斥信号量执行PV操作用于实现对缓冲区的互斥访问
这个代码其实可以完全根据同步关系的前驱图来得出
思考:能否改变相邻P,V操作的顺序?
生产者生产一个产品和消费者使用一个产品是否可以放到PV操作之间?
逻辑上可以放到PV操作这里边,但是如果我们把这两部分的处理都放到PV操作里边就会导致临界区代码变得更长,也就是说一个进程对临界区上锁的时间会增长,这样肯定不利于各个进程交替地使用临界区资源
多生产者-多消费者问题
把进程同步问题理解为某一个事件一定要求发生在另一个事件之前,而不是某一个进程的行为要发生在另一个进程的行为之前
问题描述
“多”不是指“多个”,而是“多类”。不同类别的生产者和不同类别的消费者,它们所需要生产的和所需要消费的产品是不一样的。而上一小节中所有生产者生产的都是一种东西,所有消费者也都是消费同一种东西
问题分析
关系分析。找出题目中描述的各个进程,分析它们之间的同步,互斥关系。
实现互斥很简单,无非就是在访问临界资源之前和访问临界资源之后分别对互斥变量实行一个P操作和一个V操作
而实现同步关系,只需要遵循前V后P原则。也就是前面的这个事件发生了之后需要执行一个V操作,而后面的这个事件发生之前需要执行一个P操作
如何实现
- 父亲进程和母亲进程做的事情就是不断地准备一个自己的水果,然后再把自己的水果放到盘子里。而女儿进程和儿子进程做的事情就是不断地从盘子中取出自己喜欢的水果并且把这个水果给吃掉
- 父亲进程再放入水果之前需要先检查这个盘子是否为空,所以在苹果放入盘子之前父亲进程需要执行P(plate)来检查此时盘子中到底还可以放多少个水果,如果此时这个盘子中已经有别的水果,那么父亲进程会被阻塞。那如果这个苹果已经放入盘子之后,父亲又需要对执行V(apple)用来让apple的值加1,来告诉女儿进程此时盘子中的苹果数已经加1了。而母亲进程也类似
- 女儿进程和儿子进程在取出自己喜欢的水果之前分别需要检查此时这个盘子当中是否已经有自己喜欢的水果,所以分别需要执行P(apple)和P(orange),如果没有将被阻塞。取出水果之后需要执行V(plate),用来告诉父亲进程和母亲进程此时盘子已经变空了
- 另外还需要实现各个进程对盘子这种缓冲区的互斥访问,所以我们在这些进程访问盘子之前执行P(mutex),访问之后又执行V(mutex),分别对临界区进行“加锁”和“解锁”
问题:可不可以不用互斥信号量mutex?
把互斥信号量去掉,并把对互斥信号量的PV操作也都去掉,接下来分析这样这些进程如何并发执行
分析:刚开始,儿子女儿进程即使上处理机运行也会被阻塞(apple和orange这两个信号量的数量都为0)。如果刚开始是父亲进程先上处理机运行,则:
父亲P(plate),可以访问盘子 → \to →母亲P(plate),阻塞等待盘子 → \to →父亲放入苹果V(apple),女儿进程被唤醒,其他进程即使运行也都会被阻塞,暂时不可能访问临界资源(盘子) → \to →女儿P(apple),访问盘子,V(plate),等待盘子的母亲进程被唤醒 → \to →母亲进程访问盘子(其他进程暂时都无法进入临界区) → \to →…
结论:即使不设置专门的互斥变量mutex,也不会出现多个进程同时访问盘子的现象
原因在于:本题中的缓冲区大小为1,在任何时刻,apple,orange,plate三个同步信号量中最多只有一个是1。而这几个进程刚开始都需要对其中的某一个信号量执行P操作,因此在任何时刻,最多只有一个进程的P操作不会被阻塞,并顺利地进入临界区
如果盘子(缓冲区)容量为2
父亲P(plate),可以访问盘子 → \to →母亲P(plate),可以访问盘子 → \to →父亲在往盘子里放苹果,同时母亲也可以往盘子里放橘子。于是就出现了两个进程同时访问缓冲区的情况,有可能导致两个进程写入缓冲区的数据相互覆盖的情况。因此,在生产者消费者问题当中如果缓冲区大小大于1,就必须专门设置一个互斥信号量mutex来保证互斥访问缓冲区
通过上一小节的讲解,如果两个生产者进程同时对一个缓冲区进行访问有可能会导致数据覆盖
吸烟者问题
“每次随机地让一个吸烟者吸烟”:可设置一个random随机数变量
前驱事件:这个事件发生了之后,另一个事件才可以发生
问题描述
问题分析
由于供应者进程每次只能往桌子上放一种材料的组合,所以可以把桌子看作是一种容量为1初始为空的缓冲区,而对缓冲区的访问需要互斥地进行
为什么说它的容量为1呢?不是说桌子上每次会放两种材料吗?
这个题目当中不能把两种材料看作是两种独立的物品,我们应该把它看作是一种组合。比如说把“纸+胶水”的组合看作一个整体称为组合一,组合一是抽烟者一需要使用的。不应该说这个桌子上可以同时放两种材料,而应该说这个桌子上同一时刻只能放其中某一种组合
只有“桌子上有组合一”这个事件发生的时候,才可以发生“第一个抽烟者取走东西”这个事件,其他同理。对于这三对一前一后的这种同步关系来说,我们需要对这些关系各自设置一个同步变量
对于互斥问题来说,由于这个缓冲区大小为1,所以我们即使不设置专门的互斥变量也可以实现互斥访问临界区
如何实现
设置一个整型变量 i i i,并且让其按照0,1,2,0,1,2,…这样的顺序循环地改变其值,这样就实现了“轮流让各个吸烟者吸烟”这个需求
当供应者在桌上放入了某一种组合之后,它需要对这个组合对应的同步信号量执行一个V操作用来通知等待这种组合的吸烟者。另外供应者把材料放到桌上之后需要等待吸烟者向他发出完成吸烟的信号,所以在这个地方我们又需要执行P(finish)
而各个吸烟者在从桌子上拿走材料之前,需要检查此时桌子上放的是不是自己所需要的材料。当把这些材料拿走并且卷烟抽掉之后,又需要向供应者发出一个完成信号来通知供应者可以开始放下一个材料了
读者-写者问题
问题描述
当一个写者进程正在对文件进行写操作的时候,其他进程是不能访问这个文件的。或者说当一个写进程想要写这个共享文件的时候,它必须先等到其他进程对这个文件的操作结束之后
问题分析
如何实现
设置rw信号量用于实现各个进程对于共享文件的互斥访问
写者进程要做的事情就是不断地写文件,读者进程要做的就是不断地读文件
由于写者和读者之间需要互斥地访问文件这个共享资源,所以写者在写文件之前需要“加锁”,写完文件之后再“解锁”。读者进程也一样,在读之前需要“加锁”,读之后把它“解锁”。这样的话就可以实现读者和写者之间互斥地访问这个文件
但是如果只是简单地在读文件之前“加锁”,读完文件之后“解锁”,会导致读者和读者之间不可以同时访问这个共享文件,这个问题怎么解决呢?
设置count变量,记录当前有几个读进程正在访问这个文件
count初始值为0,意味着刚开始并没有读进程在读这个文件。当一个读进程要对这个文件进行“加锁”动作之前,需要进行检查看一下自己是不是第一个想要读这个文件的进程,如果是就执行进行“加锁”,然后令count加1,表示此时正在访问文件的读进程数量加1
而当一个读进程读完了文件之后,需要令count减1表示此时访问这个文件的读进程数量减1,之后再对count的值进行判断,如果count=0,就说明此时已经没有别的读进程正在读这个文件了,这种情况下这个读进程就是最后一个正在读文件的进程,它读完之后需要“解锁”
用这种逻辑就可以实现读进程和读进程之间可以同时访问这个共享文件,因为第一个读进程才需要对这个文件“加锁”,而只要此时有一个读者正在读这个文件,也就是说只要此时count>0,那么接下来的读者进程就不会再“加锁”,它可以直接跳过这个条件判断进行count加1操作
不过这个方案也存在一个问题,见上图“思考”左下部分。当一个读进程对count的值进行检查之后有可能切换成第二个读进程,它也进行检查操作
为了解决这种“无法一气呵成”的问题,可以让各个读进程互斥地来访问count,故可以再多增加一个信号量mutex,在对count进行操作之前执行P操作,操作完执行V操作,以保证对count的互斥访问
为了解决“潜在的问题”,可以再设置一个互斥信号量w
上面几种情况自己推导试试,具体讲解此处略过不记
哲学家进餐问题
问题描述
为了描述方便,为5个筷子依次编上0$\sim$4号
问题分析
各个哲学家想要做的事情就两件,吃饭和思考
在吃饭之前,哲学家需要先拿起他左右两边的筷子。如果按比较直接的想法,会让每个哲学家在吃饭之前依次拿起自己左边和右边的两只筷子,所以在吃饭操作之前需要分别对左边筷子和右边筷子对应的互斥信号量执行P操作,也就是申请占用这两个资源。吃饭结束之后再依次对这两个资源进行释放
但是这么做的问题在于假如说此时是5个哲学家都并发地执行“吃饭”,第一个哲学家拿起它左边的筷子之后,又切换回第二个哲学家也拿起左边的筷子,接下来又切换回第三个哲学家也拿起左边的筷子…当每个哲学家在尝试拿自己右边的筷子时会发现自己右边的筷子已经被别人占用,所以所有的哲学家进程都会被阻塞,它们会循环地等待自己右边的那个人放下自己想要的那只筷子,出现“死锁”现象。每一个哲学家进程都阻塞地互相等待对方来释放资源但是又不主动释放自己手里的资源,导致所有的哲学家进程都无法往下推进。所以这种解决方案是不合理的
如何实现
如何防止死锁的发生呢?
1.可以对哲学家进程施加一些限制条件,比如最多允许四个哲学家同时进餐。这样可以保证至少有一个哲学家是可以拿到左右两只筷子的
设置一个初始值为4的同步信号量
2.要求奇数号哲学家先拿左边的筷子,然后再拿右边的筷子,而偶数号哲学家刚好相反。用这种方法可 以保证如果相邻的两个奇偶号哲学家都想吃饭,那么只会有其中一个可以拿起第一只筷子,另一个会直接阻塞。这就避免了占有一支后再等待另一只的情况
在每一个哲学家拿筷子之前先判断一下他们的序号是奇数号还是偶数号,然后再根据自己的序号来做下面的一些处理
3.仅当一个哲学家左右两支筷子都可用时才允许他抓起筷子
semaphore chopstick[5]={1,1,1,1,1};
semaphore mutex=1; //互斥地取筷子
Pi (){ //i号哲学家的进程
while(1){
P(mutex);
P(chopstick[i]); //拿左
P(chopstick[(i+1)%5]); //拿右
V(mutex);
吃饭…
V(chopstick[i]); //放左
V(chopstick[(i+1)%5]); //放右
思考…
}
}
这种方法并不能保证只有两边的筷子都可用时,才允许哲学家拿起筷子,即使一个哲学家两边的筷子其中某一边不能用,该哲学家依然可能拿起其中的一只筷子。更准确的说法应该是:各哲学家拿筷子这件事必须互斥的执行。这就保证了即使一个哲学家在拿筷子拿到一半时被阻塞,也不会有别的哲学家会继续尝试拿筷子。这样的话,当前正在吃饭的哲学家放下筷子后,被阻塞的哲学家就可以获得等待的筷子了
具体讲解略过不记
管程
为什么要引入管程
实现互斥的P操作在实现同步的P操作之前
管程的定义和基本特征
管程和之前学过的PV操作一样也是用来实现进程的互斥和同步的。而进程之间要实现互斥和同步是因为进程之间可能会共享某些数据资源。比如说像生产者消费者问题当中生产者和消费者都需要共享地访问缓冲区这种资源,所以为了实现各个进程对一些共享资源的互斥或者同步的访问,管程就要由以下这些部分组成
局部于管程的共享数据结构说明:生产者消费者问题当中,生产者和消费者都需要共享访问的那个缓冲区可以用一种数据结构来表示,对缓冲区进行管理。所以在管程当中需要定义一种和这种共享资源相对应的共享数据结构
对局部于管程的共享数据结构设置初始值的语句:对这个数据结构要进行初始化的一些语句也需要在管程当中说明
局部于管程的数据只能被局部于管程的过程所访问,一个进程只有通过调用管程内的过程才能进入管程访问共享数据:管程当中定义的这些共享的数据结构只能被管程当中定义的这些函数所修改,所以如果我们想要修改管程当中的这些共享数据结构只能通过调用管程提供的这些函数来间接地修改
每次仅允许一个进程在管程内执行某个内部过程:管程当中虽然定义了许多函数,但是同一时刻肯定只有一个进程在使用管程当中的某一个函数,每一次对共享数据结构的访问肯定只有一个进程正在进行
拓展1:用管程解决生产者消费者问题
此处并没有按照某一种严格的语法规则来进行表述,只是为了方便理解所以用了类C语言的伪代码来表示管程当中的这一系列逻辑
可以用程序设计语言当中提供的某一种特殊的语法,比如说monitor, end monitor这样一对关键字来定义一个管程,中间的部分是管程的内容,管程的名字叫ProducerConsumer
另外可以定义一些条件变量来实现同步,还可以定义一些普通变量用来表示我们想要记录的信息,比如缓冲区中的产品数
除此之外还需要定义对缓冲区进行描述的一些数据结构,只不过为了方便,这里就省去了
生产者进程想要往缓冲区里放入一个新生产的产品可以直接调用管程当中定义的insert函数,同样消费者进程也可以调用管程当中定义的remove函数实现从缓冲区当中取出一个产品,而从缓冲区当中取出一个产品时缓冲区空了怎么办,还有对缓冲区的互斥怎么办这些消费者进程都不需要关心,剩下的都是管程会负责解决的问题
//生产者进程
producer (){
while(1){
item = 生产一个产品;
ProducerConsumer.insert (item);
}
}
//消费者进程
consumer (){
while(1){
item = ProducerConsumer.remove ();
消费产品item;
}
}
monitor ProducerConsumer
condition full,empty; //条件变量用来实现同步(排队)
int count=0; //缓冲区中的产品数
void insert (Item item) { //把产品item放入缓冲区
if (count == N) //判断缓冲区是否已满
wait (full);
count++;
insert_item (item); //把生产的产品放入缓冲区
if (count == 1) //此时是否有一些消费者进程需要唤醒
signal(empty);
}
Item remove () { //从缓冲区中取出一个产品
if (count == 0) //判断此时缓冲区里是否有可用的产品
wait (empty);
count--;
if(count == N-1) //在取走产品之前缓冲区是否已满
signal(full);
return remove_item();
}
end monitor;
定义了管程之后,在编译时由编译器负责实现各进程互斥地进入管程中的过程
例1:两个生产者进程并发执行,依次调用了insert过程…
由于刚开始没有任何一个进程正在访问这个管程当中的某一个函数,所以第一个生产者进程在调用insert函数时可以顺利地执行下去。而如果在第一个进程没有执行完这个函数相应的一系列逻辑时第二个进程就尝试着也想调用insert函数,那么由于编译器实现的这些功能,它会暂时阻止第二个生产者进程进入insert函数,把第二个进程阻塞在insert函数后面,就类似于一个排队器让它先等待。等第一个进程访问完了insert函数之后才会让第二个进程开始进入insert函数然后执行相应的这一系列逻辑
互斥地使用某一些共享数据是由编译器负责实现的。程序员在写程序的时候不需要再关心如何实现互斥,只需要直接调用管程提供的这一系列过程
除了互斥之外,管程还可以实现进程的同步。可以在管程当中设置一些条件变量(full,empty)还有与它们对应的等待(wait)和唤醒(signal)操作用来实现进程的同步问题
例2:两个消费者进程先执行,生产者进程后执行…
第一个消费者进程在执行的时候首先是调用了管程的remove函数。首先需要判断此时缓冲区里是否有可用的产品,由于刚开始count的值为0,所以第一个消费者进程需要执行等待(wait)操作,等待在empty这个变量相关的队列当中。同样地第二个消费者进程开始执行remove函数时也会发现此时count的值为0,也会插入到empty这个条件变量对应的队尾
之后生产者进程会执行管程的insert函数,它会把生产的产品放入缓冲区并且检查放入的产品是不是缓冲区当中的第一个产品,如果是就意味着此时有可能有别的消费者进程正在等待产品,接下来会执行唤醒(signal)操作唤醒等待在empty这个条件变量对应的等待队列当中的某一个进程(一般是队头进程,也就是第一个消费者进程)令其开始往下执行,首先执行count–令count值由1又变回了0,然后再检查在自己取走产品之前缓冲区是否已满,如果已满就意味着有可能会有生产者进程需要被唤醒。最后remove函数会返回一个消费者进程想要的产品对应的一个指针
第一个消费者进程就可以通过以上步骤取出它想要的产品,而在取产品的过程当中如何实现对缓冲区的互斥访问或者当缓冲区当中没有产品的时候自己的这个消费者进程应该怎么处理这一切都不需要消费者进程再来关心,由管程负责解决
拓展2:Java中类似于管程的机制
死锁的概念
什么是死锁
每个人都占有一个资源,同时又在等待另一个人手里的资源。发生“死锁”
在并发环境下,各进程因竞争资源而造成的一种 互相等待对方手里的资源,导致各进程都阻塞,都无法向前推进 的现象,就是“死锁”。发生死锁后若无外力干涉,这些进程都将无法向前推进
死锁,饥饿,死循环的区别
死锁产生的必要条件
什么时候会产生死锁
死锁的处理策略
预防死锁
死锁的产生必须满足四个必要条件,只要其中一个或者几个条件不满足,死锁就不会发生
破坏互斥条件
关于SPOOLing技术,在第五章会详细说明
破坏不剥夺条件
在CPU资源被剥夺时,之前在CPU上运行的进程它的CPU寄存器之类的一些中间数据就要被保存
破坏请求和保持条件
一个系统当中有两种资源,三类进程。A类进程只需要使用资源1就可以开始投入运行,而B类进程只需要使用资源2就可以开始运行,而C类进程需要同时拥有资源1和资源2才可以投入运行,如果系统当中有源源不断的A类和B类进程到达的话那么就有可能导致C类进程饥饿
破坏循环条件
如果发生进程的相互等待,那么只有可能是拥有小编号资源的进程在等待大编号资源的进程
必须按规定次序申请资源,用户编程麻烦:以P3进程为例,比如说在一个系统中打印机的编号是5号,扫描仪的编号是7号,那一个用户程序如果既需要使用打印机又需要使用扫描仪,用户编程时就需要先编写申请使用打印机的代码。而如果换一个系统,另一个系统对扫描仪和打印机的编号刚好相反,那么用户的程序就需要为此发生改变
避免死锁(银行家算法)
安全序列,不安全状态,死锁的联系
如果系统处于安全状态,就一定不会发生死锁。如果系统进入不安全状态,就可能发生死锁(处于不安全状态未必就是发生了死锁,但发生死锁时一定是在不安全状态)
银行家算法
直接见下列截图即可,具体讲解略过不记,应该不难理解
总结:
假设系统中有 n n n 个进程, m m m 种资源
每个进程在运行前先声明对各种资源的最大需求数,则可用一个 n × m n\times m n×m的矩阵(可用二维数组实现)表示所有进程对各种资源的最大需求数。不妨称为最大需求矩阵 M a x Max Max, M a x [ i , j ] = K Max[i,j]=K Max[i,j]=K 表示进程 P i P_i Pi 最多需要 K K K个资源 R j R_j Rj
同理,系统可以用一个 n × m n\times m n×m的分配矩阵 A l l o c a t i o n Allocation Allocation 表示对所有进程的资源分配情况。 M a x − A l l o c a t i o n Max - Allocation Max−Allocation 表示对所有进程的资源分配情况。 M a x − A l l o c a t i o n = N e e d Max - Allocation = Need Max−Allocation=Need 矩阵,表示各进程最多需要多少各类资源
另外,还要用一个长度为 m m m 的一维数组 A v a i l a b l e Available Available 表示当前系统中还有多少可用资源
某进程向系统申请资源,可用一个长度为 m m m 的一维数组 R e q u e s t i Request_i Requesti 表示本次申请的各种资源量
可用银行家算法预判本次分配是否会导致系统进入不安全状态:
1.如果 R e q u e s t i [ j ] ≤ N e e d [ i , j ] ( 0 ≤ j ≤ m ) Request_i[j]\le Need[i,j](0\le j\le m) Requesti[j]≤Need[i,j](0≤j≤m) 便转向2,否则认为出错
因为它所需要的资源数已超过它所宣布的最大值
2.如果 R e q u e s t i [ j ] ≤ A v a i l a b l e [ j ] ( 0 ≤ j ≤ m ) Request_i[j]\le Available[j](0\le j\le m) Requesti[j]≤Available[j](0≤j≤m),便转向3;否则表示尚无足够资源, P i P_i Pi必须等待
3.系统试探性着把资源分配给进程 P i P_i Pi,并修改相应的数据(并非真的分配,修改数值只是为了做预判):
A v a i l a b l e = A v a i l a b l e − R e q u e s t Available=Available-Request Available=Available−Request
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 ] Allocation[i,j]=Allocation[i,j]+Request_i[j] Allocation[i,j]=Allocation[i,j]+Requesti[j]
N e e d [ i , j ] = N e e d [ i , j ] − R e q u e s t i [ j ] Need[i,j]=Need[i,j]-Request_i[j] Need[i,j]=Need[i,j]−Requesti[j]
4.操作系统执行安全性算法,检查此次资源分配后,系统是否处于安全状态。若安全,才正式分配;否则,恢复相应数据,让进程阻塞等待
死锁的检验和解除
如果系统中既不采取预防死锁的措施,也不采取避免死锁的措施,系统就很可能发生死锁。在这种
情况下,系统应当提供两个算法:
1.死锁检测算法:用于检测系统状态,以确定系统中是否发生了死锁
2.死锁解除算法:当认定系统中已经发生了死锁,利用该算法可将系统从死锁状态中解脱出来
死锁的检测
死锁的解除
可以优先对拥有更多资源的进程,批处理式的进程“动手”