第十讲 进程间通信

Inter Process Communication, IPC

第一节 进程间通信(IPC)概述



向勇 陈渝 李国良 任炬

2024年春季

课程幻灯片列表

提纲

1. 进程间通信概述

  1. 管道(pipe)
  2. 消息队列(Message Queue)
  3. 共享内存(shared memory)
  4. 信号(Signal)

进程间通信的需求

  • 挑战:单个程序的功能有限
  • IPC的目标:多进程协作完成复杂应用需求
    • 功能模块化
    • 程序之间相对隔离
    • 多个程序合作可完成复杂任务

进程间通信的定义:进程间通过数据交换(共享或传递)进行交互的行为

进程间的交互关系

  • 独立进程:与其它进程无交互
  • 协作进程:两个或多个进程之间有交互
    • 发送者 接收者 / 客户端 服务端
❯ cat README.md | grep rcore
$ git clone https://github.com/rcore-os/rCore-Tutorial-v3.git
...
* [x] expand the fs image size generated by `rcore-fs-fuse` to 128MiB
  • grep依赖cat
    • grepcat产生的输出作为其输入,来匹配字符串

进程通信方式

  • 直接通信:两个进程间不需要通过内核的中转,就可以相互传递信息
  • 间接通信:两个进程间通过系统调用和内核的中转,来相互传递消息

IPC机制

进程间能共享或传递数据就算是进程间通信。

IPC机制 含义 通信方式
信号 (Signal) 异步发送信号给进程处理 间接通信
管道 (Pipe) 单方向传输字节流 间接通信
消息队列 (Message Queue) 通过队列中转收/发消息 间接通信
套接字 (Socket) 多/单机进程间网络通信 间接通信
共享内存 (Shared Memory) 多个进程共享一块物理内存 直接通信
文件 (File) 多个进程可访问同一文件 间接通信

UNIX的典型IPC机制

进程间能共享或传递数据就是进程间通信。

消息传递的基本接口

  • 发送(send)消息
  • 接收(recv)消息
  • 远程过程调用(RPC)
  • 回复(reply)消息

Remote Procedure Call, RPC = send + recv

阻塞或非阻塞通信

  • 阻塞通信:
    • 阻塞发送、阻塞接收
  • 非阻塞通信
    • 非阻塞发送、非阻塞接收

IPC的缓冲方式

  • 无限容量:发送方不需要等待
  • 有限容量:通信链路缓冲队列满时,发送方必须等待
  • 0容量: 发送方必须等待接收方

提纲

  1. 进程间通信概述

2. 管道(pipe)

  1. 消息队列(Message Queue)
  2. 共享内存(shared memory)
  3. 信号(Signal)

管道(pipe)

管道是一种进程间通信机制, 也称为匿名管道(anonymous pipe)

  • 有读写端的一定大小的字节队列
  • 读端只能用来从管道中读取
  • 写端只能用来将数据写入管道
  • 读/写端通过不同文件描述符表示

创建管道

int pipe(int pipefd[2])

  • 管道可表示为两个文件描述符加一段内核空间中的内存
  • 创建管道时,返回两个文件描述符
    • 读管道
    • 写管道

管道(pipe)的应用场景

  • 支持有关系的进程间通信
    • 父子进程、兄弟进程等
  • 父进程创建管道(两个文件描述符)
    • 子进程会继承文件描述符,执行读写管道

管道(pipe)的应用场景

  • 通常管道两端的进程会各自关闭管道的一个文件描述符,如
    • 父进程关闭读描述符,只能向管道写数据
    • 子进程关闭写描述符,只能从管道读数据

管道实现机制

管道示例

$ gcc -o ex1 ex1.c
$ ./ex1
parent
write: the 0 message. 
write: the 1 message. 
...
children
read: the 0 message. 
read: the 1 message. 
...

建议:同学们可在课后在自己的开发环境中实践一下

Shell中的管道

只需使用一根竖线 "|" 连接两个命令即

rCore-Tutorial-v3 on ch7
❯ cat README.md | grep rcore
$ git clone https://github.com/rcore-os/rCore-Tutorial-v3.git
...
* [x] expand the fs image size generated by `rcore-fs-fuse` to 128MiB
  • 对于编写灵活的命令行脚本非常方便
  • 不支持任意两个进程间的通信

命名管道(named pipe)

在shell中可用mkfifo命令创建命名管道,也称为FIFO。

匿名管道与命名管道都属于单向通信机制。两者的不同是:

  • 命名管道可以支持任意两个进程间的通信
  • 匿名管道只支持父子进程和兄弟进程间的通信

命名管道是阻塞式的单向通信管道

  • 任意一方都可以读、写
  • 只有读、写端同时打开了命名管道时,数据才会写入并被读取

命名管道

shell A

$ mkfifo name.fifo
$ echo README > name.fifo  #文件类型为p,写命名管道阻塞

shell B

$ cat name.fifo

但:字节流形态,不支持任意两个进程间的双向通信
命名管道示例

提纲

  1. 进程间通信概述
  2. 管道(pipe)

3. 消息队列(Message Queue)

  1. 共享内存(shared memory)
  2. 信号(Signal)

消息队列(Message Queue)

消息队列是由操作系统维护的以结构数据为基本单位的间接通信机制

  • 每个消息(Message)是一个字节序列,有自己的类型标识
  • 相同类型标识的消息组成按先进先出顺序组成一个消息队列

消息队列实现机制

消息队列实现机制

消息队列的系统调用

  • 消息队列的系统调用
    • msgget ( key, flags) //获取消息队列标识
    • msgsnd ( QID, buf, size, flags ) //发送消息
    • msgrcv ( QID, buf, size, type, flags ) //接收消息
    • msgctl( … ) // 消息队列控制

消息的结构

struct msgbuf {
	long mtype;         /* 消息的类型 */
	char mtext[1];      /* 消息正文 */
};

创建消息队列

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgget(key_t key, int msgflg);

参数:

  • key: 某个消息队列的名字
  • msgflg:由九个权限标志构成,用法和创建文件时使用的mode模式标志是一样的,IPC_CREAT or IPC_EXCL等

创建消息队列

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgget(key_t key, int msgflg);

返回值:

  • 成功:msgget将返回一个非负整数,即该消息队列的标识码;
  • 失败:则返回“-1”

创建消息队列

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgget(key_t key, int msgflg);

那么如何获取key值?

  • 通过宏定义key值
  • 通过ftok函数生成key值

发送消息

int  msgsnd(int msgid, const void *msg_ptr, size_t msg_sz, int msgflg);

参数:

  • msgid: 由msgget函数返回的消息队列标识码
  • msg_ptr:是指向待发送数据的指针
  • msg_sz:是msg_ptr指向的数据长度
  • msgflg:控制着当前消息队列满或到达系统上限时的行为
    如:IPC_NOWAIT 表示队列满不等待,返回EAGAIN错误

发送消息

int  msgsnd(int msgid, const void *msg_ptr, size_t msg_sz, int msgflg);
  • 成功返回0
  • 失败则返回-1

接收消息

int  msgrcv(int msgid, void *msg_ptr, size_t msgsz,long int msgtype, int msgflg);
  • msgid: 由msgget函数返回的消息队列标识码
  • msg_ptr:是指向准备接收的消息的指针
  • msgsz:是msg_ptr指向的消息长度
  • msgtype:它可以实现接收优先级的简单形式
    • msgtype=0返回队列第一条信息
    • msgtype>0返回队列第一条类型等于msgtype的消息
    • msgtype<0返回队列第一条类型小于等于msgtype绝对值的消息

接收消息

int  msgrcv(int msgid, void *msg_ptr, size_t msgsz,long int msgtype, int msgflg);
  • msgflg:控制着队列中没有相应类型的消息可供接收时的行为
    • IPC_NOWAIT,队列没有可读消息不等待,返回ENOMSG错误
    • MSG_NOERROR,消息大小超过msgsz时被截断

返回值:

  • 成功:返回实际放到接收缓冲区里去的字符个数
  • 失败:则返回-1

消息队列示例程序

$ gcc ex1.c 
$ ./a.out
Parent: input message type:
1
Parent: input message to be sent:
test
Parent: input message type:
Child: read msg:test
0

建议:同学们可在课后在自己的开发环境中实践一下

提纲

  1. 进程间通信概述
  2. 管道(pipe)
  3. 消息队列(Message Queue)

4. 共享内存(shared memory)

  1. 信号(Signal)

共享内存(shared memory, shmem)

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

  • 每个进程的内存地址空间需明确设置共享内存段
  • 优点:快速、方便地共享数据
  • 不足:需要同步机制协调数据访问

共享内存的系统调用

  • shmget( key, size, flags) //创建共享段
  • shmat( shmid, *shmaddr, flags) //把共享段映射到进程地址空间
  • shmdt( *shmaddr)//取消共享段到进程地址空间的映射
  • shmctl( …) //共享段控制

注:需要信号量等同步机制协调共享内存的访问冲突

共享内存实现机制

创建共享内存

#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size,int shmflg);
  • key:进程间通信键值,ftok() 的返回值。
  • size:该共享存储段的长度(字节)。
  • shmflg:标识函数的行为及共享内存的权限,其取值如下:
    • IPC_CREAT:如果不存在就创建
    • IPC_EXCL: 如果已经存在则返回失败
  • 返回值:成功:共享内存标识符; 失败:-1。

共享内存映射

#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);

将一个共享内存段映射到调用进程的数据段中。即:让进程和共享内存建立一种联系,让进程某个指针指向此共享内存。

返回值:

  • 成功:共享内存段映射地址( 相当于这个指针就指向此共享内存 )
  • 失败:-1

共享内存映射

void *shmat(int shmid, const void *shmaddr, int shmflg);
  • shmid:共享内存标识符,shmget() 的返回值。
  • shmaddr:共享内存映射地址,若为 NULL 则由系统自动指定
  • shmflg:共享内存段的访问权限和映射条件,取值如下:
    • 0:共享内存具有可读可写权限。
    • SHM_RDONLY:只读。
    • SHM_RND:(shmaddr 非空时才有效)

共享内存示例程序

$ gcc writer.c -o w
$ gcc reader.c -o r
$ ./w
 Writer: copy data to shared-memory

共享内存示例

$ ./r
------------ 共享内存段 --------------
键        shmid      拥有者  权限     字节     连接数  状态
0xdf20482b 1          chyyuu     666        512        0

data = [ How are you, mike: from Writer ]
deleted shared-memory

------------ 共享内存段 --------------
键        shmid      拥有者  权限     字节     连接数  状态

建议:同学们可在课后在自己的开发环境中实践一下

提纲

  1. 进程间通信概述
  2. 管道(pipe)
  3. 消息队列(Message Queue)
  4. 共享内存(shared memory)

5. 信号(Signal)

信号(Signal)

  • 信号是中断正在运行的进程的异步消息或事件
  • 信号机制是一种进程间异步通知机制

问题:

  • Ctrl+C为什么可以结束进程?
  • kill命令是怎么结束进程的?

信号发送和响应过程

信号命名

  • 信号是一个整数编号,这些整数编号都定义了对应的宏名,宏名都是以SIG开头,比如SIGABRT, SIGKILL, SIGSTOP, SIGCONT

信号发送

  • 进程通过内核发出信号
    • shell通过kill命令向某个进程发送一个信号将其终止
  • 内核直接发出信号
    • 某进程从管道读取数据,但是管道的读权限被关闭了,内核会给进程发送一个SIGPIPE信号,提示读管道出错

信号发送

  • 外设通过内核发出
    • 比如按下Ctrl+C按键时,内核收到包含Ctrl+C按键的外设中断,会向正在运行的进程发送SIGINT信号,将其异常终止

信号接收进程的处理方式

  • 忽略:信号没有发生过
  • 捕获:进程会调用相应的处理函数进行处理
  • 默认:如果不忽略也不捕获,此时进程会使用内核默认的处理方式来处理信号
    • 内核默认的信号处理:在大多情况下就是杀死进程或者直接忽略信号

Linux信号

Linux有哪些信号? -- 62个

Linux信号

为什么这么多信号?

  • 每个信号代表着某种事件,一般情况下,当进程收到某个信号时,就表示该信号所代表的事件发生了。
  • 对1~34中的常用信号,要求是理解,而不是记忆。当忘记了信号名字时,kill -l查看即可。

Linux常用信号

  • SIGKILL
  • SIGINT
  • SIGSEGV

信号实现机制

信号实现机制

信号实现机制

  • 注册用户态信号处理函数sig_handler;
  • 内核在返回用户态前,发现有信号要处理;
  • 内核在用户栈压入sig_handler函数栈信息;
    • 模拟用户代码调用sig_handler函数
  • 内核在陷入上下文中修改用户态返回地址;
  • 内核返回用户态,直接跳到sig_handler;
  • sig_handler函数返回到旧代码位置继续执行

信号实现机制

信号应用编程

小结

  • 管道的机制和实现原理
  • 消息队列的机制和实现原理
  • 共享内存的机制和实现原理
  • 信号的机制和实现原理
  • 上述机制与进程控制与管理的关系

https://zhuanlan.zhihu.com/p/268389190 Linux进程间通信——消息队列

https://zhuanlan.zhihu.com/p/147826545 Linux系统编程之进程间通信:共享内存

Ref: Understanding the Linux Kernel Signals and Inter-Process Communication https://compas.cs.stonybrook.edu/~nhonarmand/courses/fa14/cse506.2/slides/ipc.pdf