进程间通信

进程间通信(IPC Inter-Process Communication)

  • 共享资源互斥访问

  • 条件同步

  • 数据传输

信号

进程间的中断通知和处理机制

  • 例如:SIGKILLSIGSTOPSIGCONT
  • 可以在任一时刻发送给某一个进程且无需直到进程的状态
  • 内核可以保存信号,再传递给进程
  • 信号可以被阻塞

信号的产生

硬件方式

  • 键盘Ctrl+C发送SIGINT信号
  • CPU检测到硬件非法访问(例如内存),通知内核生成信号,发送给发生事件的进程

软件方式

  • 通过系统调用,发送SIGKILL信号等

信号的接收处理

  • 捕获(Catch
    • 默认处理:执行操作系统指定的缺省处理,例如进程终止、进程挂起等
    • 自定义处理:执行进程指定的信号处理函数
  • 忽略(Ignore)
    • 不对信号做任何处理
  • 屏蔽(Mask
    • 禁止进程接受和处理信号
    • 解除屏蔽后可以接受和处理信号

信号的实现

image-20221020151124712

管道

管道的分类

无名管道

  • 进程间基于内存文件的通信机制
    • 内核缓冲区,数据单向流动,半双工通信
    • 读空或写满时,需要有相应的并发机制控制
    • 可以用ReadWrite函数读写
  • 只能在具有亲缘关系的进程间使用,例如父子进程
    • 子进程可以从父进程继承文件描述符

命名管道

  • 基于一种特殊设备文件的进程通信机制
  • 允许无亲缘关系的进程间通信

与管道有关的系统调用

  • 读管道(read(fd,buffer,nbytes))
    • C语言中的scanf()就是基于它实现的
  • 写管道(write(fd,buffer,nbytes))
    • C语言中的printf()就是基于它实现的
  • 创建管道(pipe(fd[2])
    • fd是2个文件描述符组成的数组
      • fd[0]是读文件描述符
      • fd[1]是写文件描述符

管道示例

Shell 可以通过管道重定向输入输出流

  • 可能从键盘、文件、程序读取
  • 可能写入到终端、文件、程序

image-20221020153743147

消息队列

消息队列是由操作系统维护的以字节序列为基本单位的间接通信机制

  • 独立于发送和接收进程

  • 每个消息是一个字节序列

  • 相同标识的消息按照先进先出的顺序组成一个消息队列

image-20221020153928170

消息队列的系统调用

  • msgget(key,flags)
    • 获取消息队列标识
  • msgsnd(QID,buf,size,flags)
    • 发送消息
  • msgrcv(QID,buf,size,type,flags)
    • 接收消息
  • msgctl(...)
    • 消息队列控制

共享内存

共享内存是操作系统把同一个物理内存区域同时映射到多个进程的内存地址空间的通信机制

  • 每个进程将共享内存区域映射到私有地址空间
  • 必须用额外的同步机制来协调数据访问
  • 优点:快速、方便地共享数据

image-20221020155342022

共享内存系统调用

  • shmget(key,size,flags)
    • 创建或获取一个共享段
  • shmat(shmid,*shmaddr,flags)
    • 把共享段映射到进程地址空间
    • 返回指向共享内存的指针
  • shmdt(*shmaddr)
    • 取消共享段到进程地址空间的映射
  • shmctl(...)
    • 共享段控制

套嵌字(Socket API)

TCP/UDP的抽象

  • 寻址:IP地址和端口
  • 创建和关闭socket
    • sockid=socket(af,type,proto);
    • sockerr=close(sockid);
  • 绑定socket到本地地址
    • sockerr=bind(sockid,localaddr,addrlen);
  • 监听与接收
    • listen(sockid,len);
    • accept(sockid,addr,len);
  • 连接socket到目标地址
    • connect(sockid,destaddr,addrlen)

IPC方式对比

  • 信号
    • 传送的信息量小,只有一个信号类型
  • 管道
    • 半双工方式,单向通信;无格式字节流
    • 需进程打开、关闭管道
  • 消息队列
    • 缓冲区大小受限(KB级别)
  • 共享内存
    • 通信效率高,但需要信号量等机制协调共享内存的访问冲突
  • 套嵌字(Socket)
    • 用于不同机器的进程间通信
    • 需要对数据进行封包和解包操作

IPC设计考虑

设计考虑

进程间通信的基本原语

  • Send(msg),Receive(msg)

进程通信流程

  • 建立通信链路
    • 内存、设备文件、网络
  • Send/Recv 交换数据
    • 是否缓冲消息
    • 直接通信 vs 间接通信
    • 同步 vs 异步
    • 例外处理

缓冲消息

无缓冲

  • 发送方必须等到接收方接收消息
  • 每个消息都需要握手

有界缓冲

  • 缓冲区长度有限
  • 缓冲区满则发送方阻塞

无界缓冲

  • “无限”长度
  • 发送方永远不会阻塞
  • 实际应用受限

直接通信 VS 间接通信

直接通信

  • 通信进程明确指定接收者或发送者
    • Send(P,msg)
    • Recv(C,msg)

间接通信

  • 使用信箱
    • 允许多对多的通信
    • 需要打开/关闭信箱
    • Send(A,msg)/Recv(A,msg)
  • 缓冲区
    • 信箱内需要有一个缓冲区、互斥锁和条件变量
  • 消息长度
    • 不确定,可以把大消息切成多个包发送
  • 信箱和管道的对比
    • 信息允许多对多的通信
    • 管道隐含了一个发送,一个接收

同步和异步

发送

  • 同步发送
    • 发送进程阻塞,直到消息由接收进程收到
    • 若使用缓冲,启动数据传输直到源缓冲用完后再阻塞
  • 异步发送
    • 发送进程调用async_send启动数据传输后,继续执行其他操作
    • 结束后
      • 需要应用检查状态
      • 通知或者向应用发送信号

接收

  • 同步接收
    • 接收进程阻塞,直到有消息可用
    • 如果有消息,则返回数据
  • 异步接收
    • 如果有消息,则返回数据
    • 如果无消息,则返回状态

例外处理

进程结束

  • R等待S发生的消息,但S已经结束
    • 问题:同步接收时,R会永久阻塞
    • 解决:等待超时
  • S发送一个消息给R,但R已经结束
    • 问题:同步发送时,S会永久阻塞
    • 解决:发送超时

消息丢失

使用确认(ACK)和超时(TIMEOUT)检测和重传消息

  • 需要接收者没收到一个消息后发送一个确认
  • 发送者阻塞直到ACK到达或者超时
  • 如果超时发生且没收到确认,重发消息

问题:如果消息没有丢失,重复发送了消息怎么办?

重传:接收端收到的消息重复

解决办法:

  • 使用序列号确认是否重复
  • 在接收端删掉重复消息

减少确认消息:

  • 批量传送确认
  • 接收者发送noack

消息损坏

检测方法

  • 发送端计算整个消息的校验和,并随消息发送校验和(CRC)
  • 在接收端重新计算校验和,并和消息中的校验和对比

纠正方法

  • 重传
  • 使用纠错码恢复