文章目录
进程的同步与互斥
一、进程互斥和同步的相关概念
进程的异步:各并发进程的执行以各自独立的、不可预知的速度向前推进。
进程的“并发”需要“共享”的支持,各个并发执行的线程不可避免的需要共享一些系统资源(比如内存,打印机、摄像头等IO设备)
两种资源共享方式:
- 互斥共享方式
系统中的某些资源,虽然可以提供给多个进程使用,但是一个时间段内只允许一个进程访问该资源 - 同时共享方式
系统中的某些资源,允许一个时间段内由多个进程“同时”对它们进行访问。
一个时间段内只允许一个进程使用的资源叫做临界资源。对于临界资源的访问,必须互斥进行。
do
{
entry section; //进入区,负责检查是否能进入临界资源,如果可以进入,上锁即设置正在访问临界资源的标志。
critical section; //临界区
exit section; //退出区,解除正在访问临界资源的标志
remainder section; //剩余区
}
while(true)
临界区是进程中访问临界资源的代码段。
为了禁止两个进程同时进入临界区,同步机制应该遵循以下准则:
- 1、空闲让进:临界区空闲的时候,可以允许一个请求进入临界区的进程立即进入临界区。
- 2、忙则等待:当有进程进入临界区的时候,其他试图进入临界区的进程必须等待。
- 3、有限等待:对请求访问的线程,应保证能在有限的时间内进入临界区(保证不会饥饿)
- 4、让权等待:当进入不能进入临界区的时候,应该立即释放处理机,防止进程忙等待。
总结
二、进程互斥的软件实现方法
- 单标志法
- 双标志先检查
- 双标志后检查
- Peterson 算法
1、单标志法
算法思想:两个进程在访问完临界区后会把使用临界区的权限转交给另外一个进程,也就是说每个进程进入临界区的权限只能被另一个进程赋予。
算法存在的问题:如果允许进入临界区的进程是P0,而且P0一直不访问临界区,那么虽然此时临界区空闲,但是并不允许P1访问。
单标志法存在的主要的问题就是违背了“空闲让进”原则。
2、双标志先检查法
算法思想:设置一个布尔型数组flag[],数组中各个元素用来标记各进程想进入临界区的意愿,比如flag=true,表示P0想进入临界区,每个进程进入临界区之前先检查当前有没有其他的进程想进入临界区,如果没有,则把自身对应的标志flag[i]设置为true。之后开始访问临界区。
//如果按照152637…顺序执行,那么P0和P1将会同时访问临界区。
违背了忙则等待的原则/
3、双标志后检查法(先上锁,后检查)
如果按照1526进行执行,则会出现P0和P1都无法进入临界区。
违背了空闲让进和有限等待原则
4、Peterson算法
在双标志后检查法中可能出现都想进入临界区,但是谁都不让谁,最后谁都无法进入,Peterson想到了一种方法,如果双方都想进入临界区,可以让进程尝试“孔融让梨”,主动让对方使用临界区。
总结
三、进程互斥的硬件实现方法
1、中断屏蔽方法
利用“开/关中断指令”实现(与原语的实现思想相同,即在某进程开始访问临界区到访问结束为止都不允许被中断,也就不能发生进程切换,因此也不可能发生两个同时访问临界区的情况。)
关中断、开中断
2、TestAndSet指令
简称TS指令,有的地方也称为TestAndSetLock指令或者TSL指令。
TSL是用硬件来实现的,执行的过程中不允许被中断,只能一气呵成,以下是用C语言描述的逻辑。
3、Swap指令
硬件实现的,执行的过程中不允许被中断,只能一气呵成。
总结
四、信号量机制(整型信号量、记录型信号量)
-
在双标志先检查法中,进入区”检查“、“上锁”操作无法一气呵成,从而导致两个进程有可能同时进入临界区的问题。
-
所有的解决方案都无法实现“让权等待”
1、信号量机制
用户进程可以通过使用操作系统提供的一对原语来对信号量进行操作,从而很方便的实现了进程互斥、进程同步。
信号量其实就是一个变量(可以是一个整数,也可以是一个复杂的记录型变量),可以用一个信号量来表示系统中某种资源的数量。比如系统中只有一台打印机,就可以设置一个初值为1的信号量。
一对原语:wait(S)原语和signal(S)原语,可以把原语理解为我们自己写的函数,函数名为wait和signal,括号里的信号量S就是函数调用的时候传入的一个参数。
==wait和signal原语常称为P、V操作,因此做题的时候把wait(S)和signal(S)两个操作分别写为P(S)、V(S)。
整型信号量
用一个整数型的变量作为信号量,用来表示系统中某种资源的数量。
记录型信号量
总结
2、信号量机制实现进程互斥和同步
信号量机制实现进程互斥
- 1、分析并发进程的关键活动,划定临界区(如:对临界资源打印机的访问就应该在临界区)
- 2、设置互斥信号量mutex,初始值为1
- 3、在临界区之前执行P(mutex)
- 4、在临界区之后执行V(mutex)
P、V操作必须成对出现
信号量机制实现进程同步(前操作之后执行V,后操作之前执行P)
同步:代码执行有先后顺序,互斥没先后顺序
- 1、分析什么地方需要实现同步关系
- 2、设置信号量S,初始值为0
- 3、在“前操作”之后执行V(S)
- 4、早“后操作”之前执行P(S)
总结
五、生产者-消费者问题(包含两个同步和一个互斥)
生产者消费者问题其实就是互斥和同步组合的综合问题
从上图可以看出生产者-消费者进程包含2个同步关系和一个互斥关系:
- 同步:缓冲区满的时候,生产者要等待消费者取走产品
- 同步:缓冲区空的时候,消费者要等待生产者放入产品
- 互斥:缓冲区是临界资源,各进程必须互斥访问。
semaphore mutex = 1; //互斥信号量。实现对缓冲区的互斥访问
semaphore empty = n; //同步信号量,表示空闲缓冲区的数量
semaphore full = 0; //同步信号量,表示产品的数量,也即非空缓冲区的数量
生产者
producer()
{
while(1)
{
生产一个产品;
P(empty); //消耗一个空闲缓冲区
P(mutex);
把产品放入缓冲区;
V(mutex);
V(full); //增加一个产品
}
}
消费者
consumer()
{
while(1)
{
P(full); //消耗一个产品(非空缓冲区)
P(mutex);
从缓冲区取出一个产品;
V(mutex);
V(empty); //增加一个空闲缓冲区
使用产品;
}
}
一定要注意先同步,后互斥。
六、多生产者-多消费者问题
分析步骤:
- 1、分析关系。找出题目中描述的各个进程,分析它们之间的同步、互斥关系。
- 2、整理思路。根据各进程的操作流程确定P、V操作的大致顺序。
- 3、设置信号量。互斥信号量的初值一般为1,同步信号量的初值要看对应资源的初始值是多少。
不同类别的生产者和不同类别的消费者
例子
桌子上有一个盘子,每次只能向盘子中放入一个水果,爸爸专门向盘子中放苹果,妈妈专门向盘子中放橘子,儿子专门等吃盘子中的橘子,女儿专门等吃盘子中的苹果,只有盘子为空的时候,爸爸妈妈才可以向盘子中放入一个水果,当盘子中有自己需要的水果的时候,儿子或女儿可以从盘子中取出水果
- 互斥关系:
对缓冲区(盘子)的访问要互斥进行。 - 同步关系:
1、父亲将苹果放入盘子后,女儿才能取苹果
2、母亲将橘子放入盘子之后,儿子才能取橘子
3、只有盘子为空的时候,父亲或母亲才能放入水果
dad:
dad()
{
while(1)
{
准备一个苹果
P(plate);
P(mutex);
把苹果放入盘子;
V(mutex);
V(apple);
}
}
mom:
mom()
{
while(1)
{
准备一个橘子
P(plate);
P(mutex);
把橘子放入盘子;
V(mutex);
V(orange);
}
}
daughter:
mom()
{
while(1)
{
P(apple);
P(mutex);
从盘子中取出苹果;
V(mutex);
V(plate);
吃掉苹果;
}
}
son:
mom()
{
while(1)
{
P(orange);
P(mutex);
从盘子中取出橘子;
V(mutex);
V(plate);
吃掉橘子;
}
}
总结:在生产者-消费者问题中,如果缓冲区大小为1,那么有可能不需要设置互斥信号量就可以实现互斥访问缓冲区的功能,当然,这不是绝对的,要具体问题具体分析。
建议:在考试中如果来不及仔细分析,可以加上互斥信号量,保证各进程一定会互斥访问缓冲区。但是需要注意的是,实现互斥的P操作一定要在实现同步的P操作之后,否则可能会引起“死锁”。
七、吸烟者问题
一个系统有三个吸烟者进程和一个供应者进程,每个吸烟者不停卷烟并抽掉它,卷一支烟需要三种材料:烟草、纸和胶水。三个抽烟者中,第一个拥有烟草、第二个拥有纸、第三个拥有胶水,供应者每次将两种材料放在桌子上,拥有剩下那种材料的抽烟者卷一根烟并抽掉它,并给供应者进程一个信号告诉完成了,供应这就会放另外两种材料在桌子上,这个过程一直重复。
组合一:纸+胶水
组合二:烟草+胶水
组合三:烟草+纸
- 同步关系(从时间的角度来分析):
1、桌子上有组合一 ---- 第一个抽烟者取走东西
2、桌子上有组合二 ---- 第二个抽烟者取走东西
3、桌子上有组合三 ---- 第三个抽烟者取走东西
4、发出完成信号-------- 供应者将下一个组合放在桌子上
信号量:
semaphore offer1 = 0; //桌子上组合一的数量
semaphore offer2 = 0; //桌子上组合二的数量
semaphore offer3 = 0; //桌子上组合三的数量
semaphore finish = 0; //抽烟是否完成
int i = 0; //用于实现“三个抽烟者轮流抽烟”
provider:
provider()
{
while(1)
{
if(i==0)
{
将组合一放在桌子上;
V(offer1);
}
else if()
{
将组合二放在桌子上;
V(offer2);
}
else
{
将组合三放在桌子上;
V(offer3);
}
i = (i+1)%3;
P(finish);
}
}
smoke1:
smoke1()
{
while(1)
{
P(offer1);
从桌子上拿走组合一,卷烟,抽掉;
V(finsh);
}
}
smoke2:
smoke2()
{
while(1)
{
P(offer2);
从桌子上拿走组合二,卷烟,抽掉;
V(finsh);
}
}
smoke3:
smoke3()
{
while(1)
{
P(offer3);
从桌子上拿走组合三,卷烟,抽掉;
V(finsh);
}
}
读者-写者问题
互斥关系:
- 写进程-写进程、写进程-读进程
设置一个信号量rw,P(rw)和V(rw)其实就是共享文件的“加锁”和“解锁”。可以设置一个整数变量count来记录当前有几个读进程正在访问文件。
信号量:
semaphore rw = 1;
int count = 0;
writer:
writer()
{
while(1)
{
P(rw); //写之前“加锁”
写文件...
V(rw); //写之后"解锁"
}
}
reader:
reader()
{
while(1)
{
//如果这边同时进入两个读进程都通过count==0,会出现第一个读进程进入,第二个读进程阻塞的情况
if(count == 0)
P(rw); //第一个读进程负责“加锁”
count++;
读文件;
count--;
if(count == 0)
V(rw);
}
}
出现上面这个原因的问题是因为count=0的操作和P(rw)不是原子的,所以自然而然会想到加一个互斥变量mutex
改进之后的reader:
reader()
{
while(1)
{
P(mutex); //各读进程互斥访问count
if(count == 0)
P(rw); //第一个读进程负责“加锁”
count++; //访问文件的读进程数+1
V(mutex);
读文件......
P(mutex); //各读进程互斥访问count
count--; //访问文件的读进程-1
if(count == 0)
V(rw); //最后一个读进程负责“解锁”
V(mutex);
}
}
潜在的问题:
- 只要有读进程还在读,写进程就要一直阻塞等待,可能“饿死”,因此这种算法中,读进程是优先的,读进程会用于插入在写进程前面
再次改进之后的reader:
哲学家进餐问题
一张桌子,5个哲学家,每个人左右各一支筷子,只有拿起自己左右两个筷子的时候,才能吃饭。吃饭完毕后,放下筷子重新思考。
如果每个人都拿起自己左边的筷子就会产生死锁:
- 1、可以对哲学家进餐施加一些限制条件,比如最多允许四个哲学家同时进餐,这样可以保证至少有一个哲学家是可以拿到左右两个筷子的
- 2、要求奇数号哲学家拿起左边的筷子,然后再拿右边的筷子,而偶数号哲学家刚好相反。
管程
也是用来完成进程的互斥和同步
管程有点类似于类