【操作系统】进程间通信

340 阅读10分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第11天,点击查看活动详情

进程间通信: 一种允许进程相互通信并同步其操作的机制。

问题:

  • 1、一个流程如何与另一个流程对话?
    • 共享内存、消息传递
  • 2、确保两个或多个过程不会相互妨碍。
    • 互斥
  • 3、正确排序
    • 同步化
  • 问题2和3也适用于线程

竞争条件 Race Condition

多个进程同时读写某些共享数据,而最后的结果取决于操作系统精确时序,成为竞争条件(类似于数据库的事务)

共享哪些资源?全局变量、动态分配的数据。存储在堆中,可由任何线程访问。

防止手段:互斥、同步化

临界区 Critical Region

对共享资源进行访问的程序片段(具象为代码片段)

image.png

四个条件:

  • Mutual Exclusion (互斥) Safety
    • 两个进程不能同时在临界区
  • Progress(空闲让进) Liveness
    • 临界区之外运行的进程不会阻塞其他进程
  • Bounded Waiting(有限等待) Fairness
    • 不能使进程无限期等待(死锁、饥饿)
  • Speed and Number of CPU
    • 不应该假设CPU的数量和速度

忙等待的互斥

Software Solutions

  • Lock Variables
  • Strict Alternation
  • Peterson’s Solution Hardware Solutions
  • Disabling Interrupts
  • TSL

锁变量 Lock Variables

image.png

问题:锁获取/释放的内部也有临界区(关键部分)

image.png

严格轮换 Strict Alternation

进行忙等待,不断检查turn(用于忙等待的锁也叫做自旋锁spin lock)

image.png

问题:
当进程1执行缓慢时:

  • 进程0最终被阻止,等待进程1完成其非关键部分。
  • 这违反在其关键区域之外运行的任何进程都不会阻止另一个进程的条件

Peterson’s Solution

不需要严格轮换,但仅限于在CSs(临界区)和剩余部分之间交替执行两个进程

image.png

image.png

禁用中断

它是如何工作的? 在进入临界段后立即禁用所有中断,并在离开临界段前重新启用。

为什么它会起作用? 禁用中断时,不会发生时钟中断。(CPU只能在时钟或其他中断后,从一个进程切换到另一个进程,如果中断被禁用,则不会发生切换。)

问题: 如果进程忘记启用中断怎么办? 多处理器? 禁用中断只能影响一个CPU。 不是用户程序的好解决方案(功能太强大且不灵活)

Test-and-Set Lock (TSL) 测试并加锁

(a)TSL指令将存储器字锁的内容读取到寄存器RX中。(b) 在内存地址锁处存储非零值

忙等待的问题:优先级反转问题
L优先级比H低,但L先就绪进入临界区,然后H就绪开始忙等待,由于L优先级低,没有机会被系统调度离开临界区,H永远循环忙等待

Sleep and Wakeup

  • Sleep:引起调用进程阻塞的一种系统调用(挂起),用阻塞替换忙等待,直到被其他进程唤醒
  • Wakeup:可以重试时唤醒,其他进程离开临界区时调用

执行特定功能的代码段。 由原子操作实现。 不可分割的 不可中断的

生产者-消费者问题

问题定义(有界缓冲区问题)

  • 生产者将内容放入共享缓冲区(调度限制:如果已满,请等待)
  • 消费者将其取出(如果为空,请等待)
  • 一次只有一个进程可以操作
  • 在它们之间使用固定大小的缓冲区,以避免锁定步骤。需要同步到缓冲区

解决方案:

  • 如果缓冲区已满,生产者将进入睡眠状态。下次消费者从缓冲区中删除项目时,它会唤醒生产者,后者再次开始填充缓冲区。
  • 如果消费者发现缓冲区为空,就会进入睡眠状态。下次生产者将数据放入缓冲区时,它会唤醒睡眠中的消费者

image.png

问题:会发生死锁,因为发给一个未睡眠的进程的wakeup信号丢失(一个进程想睡眠,而另一个进程试图去唤醒),解决方法是添加唤醒等待位存储wakeup信号,但进程越多唤醒等待位越多,不合适

信号量 semaphore

跟踪“wakeup”信号 信号量S是由两部分组成的结构: (a) 整数计数器,count (b) 阻塞进程的PID队列,Q

image.png

PV操作(down&up)

DOWN(S): 执行到资源为0
S.count = S.count - 1; //apply for a resource 申请一个资源用掉一个保存的wakeup信号
if (S.count < 0) //if no resource  表示现在有其他的在进程在等待
block(P);

(a) enqueue the pid of P in S.Q,
(b) block process P (remove the pid from the ready queue), and
(c) pass control to the scheduler

执行一次P操作后,若S.count<0,则|S.count|等于Q队列中等待S资源的进程数。
UP(S): 
S.count = S.count + 1; //release a resource 释放一个资源
if ( S.count<= 0 ) // have process blocked 表示没有可用资源了
wakeup(P) for some process P in S.Q;  //wakeup就绪队列当中的进程

(a) remove a pid from S.Q (the pid of P),
(b) put the pid in the ready queue, and
(c) pass control to the scheduler.

执行一次V操作后,若S.count0,则表明Q队列中仍有因等待S资源而被阻塞的进程,所以需要唤醒(wakeup)Q队列中的进程。

两种信号量:

  • 计数信号量(0..N)
    • sem初始化为N。
    • N=可用单元数
    • 表示具有多个(相同)可用单元的资源。
    • 允许进程/线程进入,只要有更多的单元可用。
  • 二进制信号量(0,1)(又称互斥信号量)
    • sem初始化为1
    • 保证对资源的互斥访问(例如,关键部分)。
    • 一次只允许一个进程/线程进入。
    • 可以更简单地实现。

信号量的用法:

  • 互斥(mutex)
    • 信号量的初始值为1
    • 在临界区之前调用P()。
    • 在临界区之后调用V()。
  • 同步化(empty和full)
    • 在进程或线程之间强制执行某种顺序。
    • 信号量的初始值通常为0

image.png

为什么需要三个信号量?一个信号量用来互斥,一 个信号量用来记录缓冲区里的商品数量不就可以了 吗?缓冲区里的空格数量不是可以由缓冲区大小和 缓冲区里的商品数量计算出来吗?为什么需要一个 full和empty来记录满的和空的呢?

注意:

  • 信号量S的物理含义
    • S>0表示有S个资源可用
    • S=0表示无资源可用
    • S<0则| S |表示S等待队列中的进程个数
    • P(S):表示申请一个资源
    • V(S):表示释放一个资源。
    • 信号量的初值应该大于等于0
  • P、V操作必须成对出现,有一个P操作就一定有一 个V操作。当为互斥操作时,它们同处于同一进程, 当为同步操作时,则不在同一进程中出现
  • 如果P(S1)和P(S2)两个操作在一起,那么P操作的顺序至关重要
    • 一个同步P操作与一个互斥P操作在 一起时,同步P操作在互斥P操作前,否则死锁(假设empty=0,那么生产者将阻塞,互斥量=0。下次,消费者也会阻止)
    • 而两个V操作无关紧要。

信号量在同步操作中的使用

案例1:顺序执行 仅在Pi中执行A之后在Pj中执行B(A->B) 使用初始化为0的信号量S

image.png

案例2:四个进程,A要在B、C之前,B、C要在D之前,可以使用信号量来同步进程:信号量S1、S2、S3=0,0,0;

要画状态转移图来决定所用信号量,同一个进程V的是同一种信号量,因此可以解释前面生产者消费者用了两种信号量来同步(因为两个进程都有作为另一个进程的条件)

image.png

伪代码示例: 后面要使用几次资源就V几次,同一个进程中顺序P->do->V

Process A: 
- do work of A 
V(S1); /* Let B or C start */ 
V(S1) 

Process B: 
P(S1); /* Block until A is finished */ 
- do work of B V(S2);

Process C: 
P(S1); 
- do work of C V(S3); 

Process D: 
P(S2);
P(S3);
- do work of D

信号量的问题:

  • 信号量本质上是共享的全局变量,它们可以从任何地方访问(糟糕的软件工程)。
  • 信号量和由其控制的数据之间没有连接。无法控制其使用,无法保证正确使用。
  • 因此,信号量容易出现错误。另一个方法:使用编程语言支持。

Monitor 管程

  • 过程、变量和数据结构的集合,这些过程、变量和数据结构都分组在一种特殊的模块或包中。
  • 互斥
    • 允许控制关键资源的获取和释放。
    • 在进入和退出时自动获取并释放单个匿名锁。
  • 数据封装
    • 管程是访问数据的“入口点”。
  • 管程是一种编程语言构造

管程的规则:

  • 进程/线程通过调用其过程之一进入管程。
  • 管程中一次只能有一个进程/线程处于活动状态。(互斥)。
    • 调用管程的任何其他进程都将挂起,等待管程可用。
  • 没有进程可以直接访问管程的局部变量,因此进程进入管程后就能保护共享数据(数据封装)
  • 管程只能访问其局部变量
  • 进入管程前需要满足条件(使用条件变量),使进程在无法继续运行时被阻塞

条件变量

  • 局部变量,只能在监视器内访问。
  • 一个进程/线程队列,在临界区内等待某些内容。
  • 与信号量相比:不能在临界区内等待。

条件变量的操作:

  • wait(c)
    • 1、释放管程锁,以便其他人可以进入。
    • 2、挂起条件变量c的等待队列上的进程/线程
    • 3、等待其他人发出条件变量c的信号。
    • 注意,进程/线程在调用wait时必须持有锁。
  • signal(c)
    • 最多唤醒一个等待的进程/线程。
    • 如果没有等待的进程/线程,则信号丢失。这与信号量不同,没有记录history
  • broadcast(c)
    • 唤醒所有等待的进程/线程

image.png

当有一个进程/线程等待条件变量时,谁在完成signal()时运行?

一个案例: 假设线程T1正在等待条件x,线程T2在监视器中,线程T2调用x.signal。则:

  • 理论上的情况:

    • T2放弃管程,T2阻塞
    • T1接管管程,运行。
    • T1放弃管程。
    • T2接管管程,恢复
  • 实际的情况:假设唤醒T1。

    • T2调用x.signal之后仍继续运行到结束。
    • 当T1有机会运行时,T1接管监视器,运行。
    • T1结束,放弃monitor

管程小结

优势:

  • 简化了数据访问同步(相对于信号量或锁)。
  • 更好的封装encapsulation。

缺点:

  • 死锁仍然可能发生(在监视器代码中)。
  • 程序员仍然可以拙劣地使用监视器。
  • 没有关于机器之间信息交换的规定。

信号量和管程对比:

  • 管程
    • 是高级编程语言概念
    • 使关键部分的互斥“自动”,从而减少出错的可能性。
    • 需要编译器支持,并不广泛支持;因此在实践中不太有用。
  • 信号量
    • 级别更低
    • 需要操作系统支持。
    • 应用程序可以分层,即不需要编译器支持。

这两个概念都需要某种共享内存。

迄今为止讨论的任何机制都不允许在分布式进程之间进行信息交换。

*消息传递

经典问题

这些问题用于测试每个新提出的同步方案:

  • 有界缓冲区(生产者-消费者)问题
  • 哲学家进餐问题
  • 读者和作者问题
  • *睡眠理发师问题

Dining Philosophers

五位哲学家围坐在一张桌子旁。两位哲学家之间各有一把叉子。 条件:吃饭需要两个叉子, 一次挑一把叉子。哲学家有思考和吃两种状态

死锁–两个或多个进程无限期地等待一个事件,而该事件只能由其中一个等待进程引起。

  • 解决方案1:最多允许四位哲学家尝试抓住他们的叉子。
  • 解决方案2:不对称解决方案奇数哲学家首先抓住他们的左叉,而偶数哲学家首先抓住他们的右叉。
  • 解决方案3:仅当两个叉都可用时才拿起叉。

代码:

  • 问题1:为什么在take_forks()过程中状态变量设置为HUNGRY?

  • 解决方案1:如果哲学家堵住了,邻居们可以在测试中通过检查他的状态来判断他是否饿了,这样他就可以在叉子可用时被叫醒。

  • 问题2:考虑程序put_forks()。假设变量state[i]设置为在两次调用测试之后思考,而不是之前。此更改将如何影响解决方案?

  • 解决方案2:这一变化意味着,在一位哲学家停止进食后,他的两个邻居都不能再被选中。事实上,他们永远不会被选中。

Readers and Writers Problem

可以共享读,但只能独占写(类比共享锁排他锁)

两个版本:

  • 读者优先于作者。
    • 没有读者会等待(其他读者完成),即使作者正在等待。
    • 写入程序可能会饥饿。

image.png

  • 作者优先于读者。
    • 一旦writer程序请求访问,则不允许新的reader启动
  • 在大多数解决方案中,优先级低的组可能会挨饿。

*The Sleeping Barber Solution

N个客户座椅 一个理发师可以在任何时候为一位顾客理发。 如果没有顾客等,理发师就睡觉。 如果没有椅子等待,顾客就会离开。 等待的顾客在理发结束之前不能离开。

*互斥量 Mutexes

信号量的简化版本。

  • 互斥用于互斥,在用户空间线程包中尤其有用。
  • 只有2个状态的共享变量 已锁定 已解锁
  • 仅需要1位来表示互斥体。
  • 实际上,通常使用整数,0表示解锁,所有其他值表示锁定。

总结

  • 竞争条件: 多个进程同时访问和操作共享数据的情况。共享数据的最终值取决于最后完成的进程。
  • 临界区: 影响共享资源的流程代码的一部分。 四项要求
  • 使用忙等待的互斥:五种方法
  • 信号量: P、 V操作 两种信号量 互斥 同步化
  • 管程: 互斥:编译器 同步:条件变量