操作系统ch3 并发程序设计

200 阅读14分钟

进程同步

进程具有异步性的特征,各并发执行的进程以各自独立的、不可预知的速度向前推进。操作系统要提供进程同步机制来协调它们的工作次序而产生的直接制约关系,实现各个进程之间的异步执行,比如怎么保证读数据在写数据之后执行。

判断两个进程是否能同步执行的方法: 1bf5a614e8f83c56bbef91d6c2bb660.jpg

进程互斥

进程的并发需要共享的支持,有两种资源共享方式,分别是互斥共享方式(一个时间段内只允许一个进程访问该资源)和同时共享方式(一个时间段内多个进程同时对它们进行访问)。
我们把一个时间段内只允许一个进程使用的资源称为临界资源,比如摄像头打印机、一些变量数据内存缓冲区等。对临界资源的访问必须互斥地进行,也称为间接制约关系。进程互斥指一个进程访问临界资源时,另一个想访问该临界资源的进程必需等待。
对临界资源的访问分为四个部分:进入区(设置正在访问临界资源的标志)、临界区(访问临界资源的代码)、退出区(解除正在访问临界资源的标志)、剩余区。进入区和退出区实现进程互斥。
互斥访问有四个原则:

  • 空闲让进:临界区空闲时,可以允许一个请求进入临界区的进程立即进入临界区
  • 忙则等待:当已有进程进入临界区时,其他试图进入临界区的进程必须等待
  • 有限等待:对请求访问的进程,应保证能在有限时间内进入临界区,即不会饥饿
  • 让权等待:当进程不能进入临界区时,应立即释放处理机,防止进程忙等待

进程互斥的软件实现

单标志法

两个进程在进入区时会不断轮询检查turn标志码是不是自己,在访问完临界区之后会把临界区的使用权限转交给另一个进程,也就是每个进程进入临界区的权限只能被另一个进进程赋予,可以实现同一时刻最多一个进程访问临界区。但是如果p1想访问,而p0一直不访问,就无法赋值给p1,违背空闲让进原则

双标志先检查法

设计一个bool数组,用来标记各个进程想进入临界区的意愿,开始时设置两个进程都不想进临界区。在进入区时会不断轮询检查对方想不想进临界区,如果对方想进就一直等待,对方不想进就把自己的标志码设成true,相当于上锁,访问完临界区再修改标记为false。但是检查和上锁两个操作不是一气呵成的,如果两个进程并发执行,在刚检查完还没上锁就切到了另一个进程,就会违反忙则等待原则

双标志后检查法

与先检查法不同的是,这个先上锁后检查。如果还没检查别的进程就上锁,两边就会一直卡在轮询检查,无法访问临界资源,产生饥饿现象,违背空闲让进和忙则等待原则

Peterson算法

结合单双标志法的思想,既有bool意愿数组,也有谦让标识符。进程首先把自己的bool设为true,表达自己想要使用临界资源的意愿,再把turn设为1,表示谦让的意愿,最后轮询检查对方的bool和ture,如果都为1,则自己while循环让对方先访问。解决了进程互斥问题,遵循了空闲让进、忙则等待、有限等待三个原则,但是仍未遵循让权等待的原则。

进程互斥的硬件实现

中断屏蔽方法

使用开关中断指令,某进程开始访问临界区到结束访问期间都不允许被中断,也不能发生进程切换。简单高效但不适用于多处理机,只适用于系统内核进程,不适用于用户进程,因为开关中断指令只能运行在内核态。

TestAndSet指令

old记录是否已被上锁,再将lock设置为true,检查了临界区是否已被上锁,若已上锁则循环重复前面几步。TSL指令把上锁和检查用硬件的方式变成了一气呵成的原子操作。实现简单,无需检查逻辑漏洞,适用于多处理机环境,但是不满足让权等待原则,会导致忙等。

Swap指令

和TSL没什么区别,优缺点也一样

互斥锁

TSL和Swap都是锁,常用于多处理器系统,一个核忙等,其他核照常工作,并快速释放临界区。

信号量机制

用户进程通过操作系统提供的一对原语和信号量来进行操作,来实现进程互斥进程同步。

  • 信号量S:是一个变量,用来表示系统中某种资源的数量。
  • 原语:是一种特殊的程序段,执行要一气呵成不可中断。wait/P是进入区申请,signal/V是退出区释放。

整型信号量

与普通整型变量不同的是只能进行初始化、P操作、V操作,不满足让权等待,会出现忙等。 image.png

记录型信号量

用记录型数据表示的信号量,不仅有剩余资源数还有等待队列,可以实现系统资源的申请和释放以及进程互斥进程同步,不会出现忙等。
value初值表示系统中某种资源的数目。在wait中会先给value-1,如果是负数说明当前没有空闲资源,要block进入等待队列,把自己变为阻塞态并释放CPU。signal会先给value+1,如果小于等于0说明队列中有进程在等待资源,wakeup把刚刚空闲出的资源给等待队列的队头使用,使之从阻塞态变为就绪态。 image.png

实现进程互斥

  1. 分析并发进程的关键活动,划定临界区
  2. 设置互斥信号量mutex,初值为1
  3. 在进入区P(mutex)申请资源
  4. 在退出区V(mutex)释放资源

实现进程同步

让各并发进程按要求有序地推进

  1. 分析什么地方需要实现同步关系,即必须保证一前一后执行的两个操作
  2. 设置同步信号量S,初值为0,代表某种资源,P1产生,P2使用
  3. 在前操作之后执行V(S)
  4. 在后操作之前执行P(S)

image.png

实现前驱关系

类似拓扑排序,每一对前驱关系都是一个进程同步问题

  1. 分析问题,画出前驱图,把每一对前驱关系都看成一个同步问题
  2. 为每一对前驱关系设置同步信号量,初值为0
  3. 在每个前操作之后执行V操作
  4. 在每个后操作之前执行P操作

image.png

生产者消费者问题

系统中有一组生产者进程和一组消费者进程,生产者进程每次生产一个产品放入缓冲区,消费者进程每次从缓冲区中取出一个产品并使用。
生产者消费者共享一个初始为空,大小为n的缓冲区,缓冲区是临界资源,各进程必须互斥地访问
只有缓冲区没满时,生产者才能把产品放入缓冲区,只有些缓冲区不为空时,消费者才从中取出产品,否则必须等待。 解题思路:

  1. 找出题目中的各个进程,分析之间的同步互斥关系
  2. 根据各进程的操作流程确定P、V操作的大致顺序
  3. 设置信号量,并根据题目条件确定信号量初值

实现互斥的P操作一定要在实现同步的P操作之后,否则会造成死锁。两个V操作顺序可以交换 image.png

多生产者多消费者问题

有多类生产者和消费者 image.png 盘子大小是几,互斥信号量就是几,如果盘子大小为1,互斥信号量也可以不写 image.png

吸烟者问题

可以生产多个产品的单生产者 image.png image.png

读者写者问题

多个读者可以同时对文件执行操作;只允许一个写者往文件中信息;任一写者在写操作完成之前不允许其他读者或写者工作;写者执行写操作前,应让已有的读者和写者全部退出
互斥关系:写-写、写-读
使用一个计数器count用来记录当前正在访问共享文件的进程数,用count的值来判断当前进程是否是第一个/最后一个读进程,从而做出不同处理。另外,对count的赋值和检查不能一气呵成会导致一些错误,吐过需要实现一气呵成,应使用互斥信号量,起到类似于中断的效果

image.png image.png

哲学家进餐问题

关键在于解决问题死锁,每个问题都需要同时持有两个及以上的临界资源
具体代码不贴了,比较复杂看不太懂,p36,有需要再去看吧 image.png image.png

管程

管程是一种特殊的软件模块,用封装的思想,类似于面向的对象的类,用来更方便地实现进程互斥和同步。
由下面这些部分组成:

  • 局部于管程的共享数据结构说明
  • 一组用来访问数据结构的函数
  • 对局部管程的共享数据设置初值的语句
  • 管程有一个名字 基本特征:
  • 一个进程/线程只有通过调用管程内部的函数的才能进入管程访问共享数据
  • 每次仅允许一个进程在管程内执行某个内部函数,互斥访问是由编译器实现的

另外,可以在管程中设置条件变量等待/唤醒操作来解决同步问题,可以让一个进程或者线程在条件变量上等待(此时该进程应先释放管程的使用权,也就是让出入口),可以通过唤醒操作将等待在条件变量上的进程或线程唤醒

死锁

  • 死锁:各进程互相等待对方手里的资源,导致各进程都阻塞,无法向前推进的现象。至少有两个及以上进程同时发生死锁
  • 饥饿:由于长期得不到想要的资源,某进程无法向前推进的现象。可能只有一个进程发生饥饿
  • 死循环:某进程执行过程中一直跳不出某个虚幻的现象,有时是因为程序逻辑bug,有时是故意的。可能只有一个进程发生死循环,死循环的进程可上处理机
  • 死锁和饥饿是操作系统要解决的问题,死循环是程序员要解决的

产生死锁有几个条件,必须同时满足:

  • 互斥条件:只有互斥的资源抢夺才会发生死锁
  • 不剥夺条件:进程保持的资源只能主动释放,不能强行剥夺
  • 请求和保持条件:保持着某些资源不放的同时,请求别的资源
  • 循环等待条件:存在一种进程资源的循环等待链,循环等待未必死锁,死锁一定循环等待

对不可剥夺资源的不合理分配时会发生死锁
死锁的处理策略:

  • 预防死锁:破坏死锁产生的四个必要条件
  • 避免死锁:避免系统进入不安全状态(银行家算法)
  • 死锁的检测和解除:允许死锁发生,系统负责检测出死锁并解除

预防死锁

image.png

  • 破坏互斥条件:把只能互斥使用的资源改造为允许共享使用,比如SPOOLing技术。但并不是所有的资源都可以被改造,因此很多时候无法破坏互斥条件
  • 破破坏不剥夺条件:实现复杂;剥夺资源可能导致部分工作失效;反复申请和释放导致系统开销大;可能导致饥饿
    • 申请的资源得不到满足时,立即释放拥有的所有资源
    • 申请的资源被其他进程占用时,由操作系统协助剥夺
  • 破坏请求和保持条件:采用静态分配方法,运行前分配好所有需要的资源,之后一直保持。资源利用率低,可能导致饥饿
  • 破坏循环等待条件:给资源编号,必须按从小到大的顺序申请资源。不方便增加新设备;会导致资源浪费;用户编程麻烦

避免死锁

  • 安全序列:如果系统按这种序列分配资源,则每个进程都能顺利完成。安全序列可能有多个,只要找出一个安全序列,系统就是安全状态,一定不会发生死锁
  • 不安全序列:按这种序列分配资源后找不出安全序列,就进入了不安全状态,可能发生死锁。但是如果有进程提前归还资源,也可能回到安全状态。
  • 银行家算法在资源分配之前预先判断这次分配是否会导致系统进入不安全状态,来决定是否答应分配资源的请求。 image.png 具体的代码和思路见下图: image.png

image.png

检测和解除死锁

如果系统中既不采取预防死锁的措施,也不采取避免死锁的措施,系统就有可能发生死锁,在这种情况下,系统应提供两个算法:

  • 死锁检测算法:用于检测系统状态,以确定系统中是否发生了死锁
  • 死锁解除算法:当认定系统中已经发生了死锁,利用该算法可以将系统从死锁状态中解脱出来

死锁的检测

  1. 需要一种数据结构来保存资源的请求和分配信息
    • 进程节点:对应一个进程,圆形
    • 资源节点:对应一类资源,方形,一个点代表一个资源
    • 进程节点→资源节点:表示进程想申请几个资源,一条边代表一个资源
    • 资源节点→进程节点:表示已经为进程分配了几个资源,每条边代表一个资源
  2. 需要一种算法来检测系统是否已经进入死锁状态

如果系统中剩余资源能满足进程要求,呢么就不会阻塞,执行完毕后归还资源,划掉相关的边,再执行别的进程,也就是依次消除于与不阻塞进程相连的边,直到无边可消。如果这样化简后能消除所有的边,就称这个图是可完全简化的,一定没有发生死锁;如果被卡住不能消除所有边,就发生了死锁,图就是不可完全简化的,连着边的进程就是死锁的进程。

死锁的解除

要对死锁的进程动手要考虑进程优先级、已执行时间、还要多久才能完成、已经使用的资源、进程的类型等等。

  • 资源剥夺法:挂起某些死锁进程并抢占资源,把资源分配给其他进程,但要防止饥饿
  • 撤销进程(终止进程)法:强制撤销部分或全部死锁进程,但是代价可能很大
  • 进程回退法:让一个或多个进程回退到足以避免死锁的地步,这要求系统记录进程的历史信息