进程间通信

一、进程间通信介绍

(1) 进程间通信目的

  • 数据传输:一个进程需要将它的数据发送给另一个进程
  • 资源共享:多个进程之间共享同样的资源
  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)
  • 进程控制:有些进程希望完全控制另一个进程的(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变

(2) 进程间通信发展

  • 管道
  • System V进程间通信
  • POSIX进程间通信

(3) 进程间通信分类

  • 管道
  1. 匿名管道 pipe
  2. 命名管道
  • System V IPC
  1. System V 消息队列
  2. System V 共享内存
  3. System V 信号量
  • POSIX IPC
  1. 消息队列
  2. 共享内存
  3. 信号量
  4. 互斥量
  5. 条件变量
  6. 读写锁

二、管道

什么是管道

  • 管道是Unix中最古老的进程间通信的形式
  • 我们把从一个进程连接到另一个进程的一个数据流称为一个"管道"

三、匿名管道

cpp 复制代码
#include <unistd.h>
功能:创建⼀⽆名管道
原型
int pipe(int fd[2]);
参数
fd:⽂件描述符数组,其中fd[0]表⽰读端, fd[1]表⽰写端
返回值:成功返回0,失败返回错误代码(返回-1)

(1) 用fork来共享管道原理

我们默认0是读端,1是写端

(2) 站在文件描述符角度 --- 深度理解管道

(3) 站在内核角度 --- 管道本质

  • 所以,看待管道,就如同看待文件⼀样!管道的使用和文件⼀致,迎合了"Linux⼀切皆文件思想"

(4) 管道读写规则

  • 当没有数据可读时
  1. O_NONBLOCK disable(阻塞模式):read调用阻塞,即进程暂停执行,⼀直等到有数据来到为止
  2. O_NONBLOCK enable(非阻塞模式):read调用返回-1,errno值为EAGAIN
  • 当管道满时
  1. O_NONBLOCK disable:write调用阻塞,直到有进程读走数据
  2. O_NONBLOCK enable:调用返回-1,errno值为EAGAIN
  • 如果所有管道写端对应的文件描述符被关闭,则read返回0(读不了)
  • 如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出(写不了)
  • 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性
  • 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性

(5) 管道的特点

  • 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,⼀个管道由⼀个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道
  • 管道提供流式服务
  • ⼀般而言,进程退出,管道释放,所以管道的生命周期随进程
  • ⼀般而言,内核会对管道操作进行同步与互斥
  • 管道是半双工的,数据只能向⼀个方向流动;需要双方通信时,需要建立起两个管道

(6) 验证管道通信的4种情况

  • 读正常 && 写满
  • 写正常 && 读空
  • 写关闭 && 读正常
  • 读关闭 && 写正常

四、命名管道

  • 管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信
  • 如果我们想在不相干的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道
  • 命名管道是一种特殊类型文件

(1) 创建一个命名管道

  • 命名管道可以从命令行上创建,命令行方法是使用下面这个命令:
cpp 复制代码
    mkfifo filename
  • 命名管道也可以从程序里创建,相关函数有:
cpp 复制代码
    int mkfifo(const char *filename,mode_t mode);
  • 创建命名管道
cpp 复制代码
int main(int argc, char *argv[])
{
    mkfifo("p2", 0644);
    return 0;
}

(2) 匿名管道与命名管道的区别

  1. 匿名管道由pipe函数创建并打开
  2. 命名管道由mkfifo函数创建,打开用open
  3. FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成后,它们具有相同的语义

(3) 命名管道的打开规则

  • 如果当前打开是为了读而打开FIFO时
  1. O_NONBLOCK disable(阻塞模式):阻塞直到有相应进程为写而打开该FIFO
  2. O_NONBLOCK enable(非阻塞模式):立刻返回成功
  • 如果当前打开是为了写而打开FIFO时
  1. O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO
  2. O_NONBLOCK enable:立刻返回失败,错误码为ENXIO

五、system V共享内存

共享内存区时最快的IPC形式;一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据(在进程的共享内存种发生)

(1) 共享内存示意图

(2) 共享内存数据结构

cpp 复制代码
struct shmid_ds 
{
    struct ipc_perm shm_perm; /* operation perms */
    int shm_segsz; /* size of segment (bytes) */
    __kernel_time_t shm_atime; /* last attach time */
    __kernel_time_t shm_dtime; /* last detach time */
    __kernel_time_t shm_ctime; /* last change time */
    __kernel_ipc_pid_t shm_cpid; /* pid of creator */
    __kernel_ipc_pid_t shm_lpid; /* pid of last operator */
    unsigned short shm_nattch; /* no. of current attaches */unsigned short shm_unused; 
    /* compatibility */
    void shm_unused2; / ditto - used by DIPC */
    void shm_unused3; / unused */
};

(3) 共享内存函数

  • shmget函数
cpp 复制代码
功能:⽤来创建共享内存
原型
    int shmget(key_t key, size_t size, int shmflg);
参数
    key:这个共享内存段名字
    size:共享内存⼤⼩
    shmflg:由九个权限标志构成,它们的⽤法和创建⽂件时使⽤的mode模式标志是⼀样的
    取值为IPC_CREAT:共享内存不存在,创建并返回;共享内存已存在,获取并返回。
    取值为IPC_CREAT | IPC_EXCL:共享内存不存在,创建并返回;共享内存已存在,出错返回。    
    返回值:成功返回⼀个⾮负整数,即该共享内存段的标识码;失败返回-1
  • shmat函数
cpp 复制代码
功能:将共享内存段连接到进程地址空间
原型
    void *shmat(int shmid, const void *shmaddr, int shmflg);
参数
    shmid: 共享内存标识
    shmaddr:指定连接的地址
    shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY
    返回值:成功返回⼀个指针,指向共享内存第⼀个节;失败返回-1
  • 说明
cpp 复制代码
shmaddr为NULL,核⼼⾃动选择⼀个地址
shmaddr不为NULL且shmflg⽆SHM_RND标记,则以shmaddr为连接地址。
shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会⾃动向下调整为SHMLBA的整数
倍。公式:shmaddr - (shmaddr % SHMLBA)
shmflg=SHM_RDONLY,表⽰连接操作⽤来只读共享内存
  • shmdt函数
cpp 复制代码
功能:将共享内存段与当前进程脱离
原型
    int shmdt(const void *shmaddr);
参数
    shmaddr: 由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段
  • shmctl函数
cpp 复制代码
功能:⽤于控制共享内存
原型
    int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数
    shmid:由shmget返回的共享内存标识码
    cmd:将要采取的动作(有三个可取值)
    buf:指向⼀个保存着共享内存的模式状态和访问权限的数据结构    
    返回值:成功返回0;失败返回-1

六、system V消息队列 --- 选学了解即可

  • 消息队列提供一个从一个进程向另外一个进程发送一块数据的方法
  • 每个数据块都被认为是有一个类型,接收者进程收到的数据块可以有不同的类型值
  • 特性方面:IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核

七、system V信号量 --- 选学了解即可

信号量主要用于同步和互斥的,下面先来看看什么是同步和互斥

(1) 并发编程,概念铺垫

  • 多个执行流(进程),能看到的同一份公共资源:共享资源
  • 被保护起来的共享资源叫做临界资源
  • 保护的方式常见:互斥与同步
  • 任何时刻,只允许一个执行流访问资源,叫做互斥
  • 多个执行流,访问临界资源的时候,具有一定的顺序性,叫做同步
  • 系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源
  • 在进程中涉及到互斥资源的程序段叫做临界区;你写的代码 = 访问临界资源的代码(临界区)+ 不访问临界资源的代码(非临界区)
  • 所谓的对共享资源进行保护,本质是对访问共享资源的代码进行保护

(2) 信号量

特性方面

  • IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核

理解方面

  • 信号量是一个计数器

作用方面

  • 保护临界区

本质方面

  • 信号量本质是对资源的预定机制

操作方面

  • 申请资源,计数器 --,P操作
  • 释放资源,计数器 ++,V操作

八、内核是如何组织管理IPC资源的

  • 参考Linux内核 2.6.11 源码,其他源码实现可能有差别
  • 课堂看源码可以看 2.6.18 ,这块从11到18变化不⼤

九、补充

  • 进程间通信,需要让不同的进程,看见同一份资源--->是OS建立的内存块
  • 管道
  1. fork的时候,file要进行拷贝
  2. 管道是单向通信的
  3. 管道是一个内存级文件,不需要打开磁盘、文件之类!没有路径,不需要文件名(匿名管道)
  • 看待管道,就如同看待文件一样,管道的使用和文件一致
  • 问:父子进程之间通信,为什么要关闭对应的读写段?不关闭可以么?
  1. 因为防止误操作,防止同时写入
  2. 可以,但不建议;因为同时写入同一个管道会导致数据被覆盖
  • 管道,基于文件,进行内核级进程间通信
  • 通信原理(IPC)
  1. 基于内核中转:内核在自己的地址空间中开辟一块缓冲区,作为通信双方的"中转站",发送进程先把数据从用户空间拷贝到内核缓冲区,接收进程再从内核缓冲区拷贝到自己的用户空间;典型如管道、消息队列、信号,这类方式通用性强,但存在两次数据拷贝的开销,效率相对较低
  2. 基于内存共享:让多个进程直接映射同一块物理空间到各自的虚拟地址空间,进程可直接读写这一块内存,无需经过内核中转,典型的如共享内存;这类方式是效率最高的IPC,但需要额外的同步机制来避免多个进程同时读写导致的竞态条件(需要锁)
  • 匿名管道:没有对英国的文件系统节点,生命周期与创建它的内存一致,只能用于具有亲缘关系的进程间通信,由pipe()函数创建
  • 命名管道:有对应的文件系统路径,是一个特殊的管道文件,可用于任意两个无亲缘关系的进程通信,由mkfifo()创建
  • 匿名管道的特性和情况做总结

五种特性

  1. 管道只能单向通信、单工通信
  2. 匿名管道只能进行具有血缘关系的进行通信(继承内核资源)
  3. 管道是面向字节流的
  4. 管道的生命周期是跟随进程的
  5. 管道通信对于多进程而言,是自带互斥与同步机制

四种情况

  1. 子进程写的慢,父进程就要阻塞等待,等管道有数据,父进程才能读
  2. 子进程写的快,父进程不读,管道一旦被写满,子进程就必须阻塞
  3. 读端在读,写端关闭,读端读完管道中的剩余数据,再读,就会读取" ",read返回值为0,表明读管道,读到文件结尾
  4. 写端一直在写,读端不读关闭fd(OS会直接杀掉写进程,13号信号,异常杀掉,即管道破裂)

📍管道通信,前提是让不同的进程,看到同一份资源 --- 文件 --- 内核文件缓冲区 --- 内存块

  • 细节

Ⅰ. 字节流

  1. 字节流是指字节为基本单位、按顺序传输的连续数据
  2. 无结构、有序性;面向连接、传输;阻塞、非阻塞模式
  3. 目前掌握:面向字节流的读写不存在"数据块大小各自对应"的强性约束

Ⅱ. 引用计数的核心作用是管理共享资源的生命周期,避免资源泄漏或重复释放,其实现依赖内核态共享数据结构或用户态同步机制

作用:

  1. 判断资源的存活状态 --- (防止进程提前退出)
  2. 避免无效的I / O操作 --- (写端为0 / 读端为0)
  3. 协调多进程的资源释放 --- (能确保最后一个使用资源的进程执行释放)
  • 一个进程控制一批进程的过程(主仆关系)

Ⅰ. 创建一批子进程:父进程通过循环调用fork()生成多个子进程,每个子进程都会继承父进程的文件描述符、内存空间副本等资源

Ⅱ. 管理子进程的运行状态:通过IPC控制子进程行为

  1. 信号控制:父进程调用 kill() 向子进程发送信号,子进程通过 signal() / sigaction()注册信号处理函数
  2. 管道 / 共享内存通信:父进程通过管道向子进程下发指令,或通过共享内存空间传递任务参数;子进程执行完后,再以同样方式返回
  3. 同步控制:使用semaphore(信号量)实现父子进程的同步,比如父进程下发任务后阻塞等待,子进程完成后唤醒父进程

Ⅲ. 回收子进程资源:父进程回收子进程的退出状态,避免长时间停留在Z状态

  1. 阻塞回收:调用wait() / waitpid()阻塞等待子进程退出
  2. 异步回收:适用于子进程异步退出(利用信号向父进程发送)
  • 父子进程的读与写约定统一4字节
  • 选择进程机制 ---> 负载均衡(轮询、随机、权重)
  • 问题1:如果父进程要求子进程退出,如果管道中还有任务没有处理完,怎么办?

答:优雅退出 ---> 子进程在函数中先完成管道剩余数据的读取与处理,再调用exit()正常退出,并且回收子进程

  • 问题2:进程池中,为什么要等子进程全部关闭,再wait回收子进程?

📍核心是保证进程池的并发效率和避免父进程被阻塞

答:(1)避免父进程阻塞,维持并发能力:若父进程创建一个子,立即调用wait(),会被阻塞直到该子进程退出,期间创建新的子进程处理任务,进程池的并发特性会完全丧失;只有等所有子进程完成任务后再批量wait,才能保持并发性

(2)简化子进程管理逻辑,降低资源消耗:统一等待所有子进程退出后再回收,无需为每个子进程单独维护wait时机与状态;对系统资源影响极小

(3)便于统一处理子进程退出后的收尾工作:可以集中处理所有子进程的退出状态,还能统一清理管道、共享内存等进程间通信资源,避免因零散回收导致资源清理不彻底问题

  • 问题3:进程池基于fork()创建子进程时,子进程会完全复制父进程的文件描述符表,导致所有子进程共享父进程创建的管道读写端,出现误读 / 误写的问题,该怎么解决

📍核心:按通信需求关闭无关的管道端 ---> 1:1=r:w

  1. 父进程提前规划管道结构,避免全局共享进程池推荐使用1对1私有管道:父进程为每一个子进程单独创建一个pipefd[2]管道,而非创建一个全局管道供所有子进程使用;fork()保持有所有管道的读写端,fork()后每一个子进程只保留与自身通信的管道端,其余管道端全部关闭
  2. 子进程fork之后立即清理无关文件描述符

Ⅰ. 若子进程时任务执行端,只需要从父进程接受任务,就关闭写端,保留读端

Ⅱ. 若子进程需要向父进程返回结果,按需保留写端,但必须关闭其他子进程对应的管道描述符

Ⅲ. 父进程再fork所有子进程后,关闭每个子进程管道中不需要的读端,避免管道引用计数异常导致的阻塞

  • 共享内存 = 结构体+内存块(先描述、再组织)
  • 因为会存在很多共享内存,所以共享内存一定会被OS管理
  • 让不同的进程,看到同一份资源:让不同的进程,把同一个内存块,映射到自己的虚拟地址空间,每一个进程得到自己的虚拟地址空间内存块的起始地址

Ⅰ. 理解(被映射到虚拟地址空间中的共享区 ---> 内存映射段)

  1. 映射到进程的虚拟地址空间,共享区
  2. 共享内存原理,是一个简化版本的动态映射
  3. 共享内存结构体+共享内存本身 = 共享内存
  4. 使用步骤(1)创建(2)关联链接(3)使用接口(4)去关联(5)释放共享内存

Ⅱ. 特点

  1. 访问共享内存不需要系统调用,因为shm已经映射到了进程的用户共享区
  2. 写端数据拷贝shm,其他端可以立马看到(共享内存时所有IPC中最快的)(拷贝次数少,直接映射,不需要系统调用)
  3. 缺点:没有资源的保护机制,没有同步或者互斥(shm由用户自己完成保护)

Ⅲ. what、why、how

  • 采用多个进程,使用虚拟地址空间映射方式,让不同的进程看到同一个内存块

宏(1)IPC_EXCL:不能单独使用

(2)IPC_CREAT:可以单独传递,如果创建的共享内存不存在,就创建;存在,就获取它(我总要获得一个shm)

(3)IPC_EXCL | IPC_CREAT:如果创建的共享内存不存在,就创建;存在,就报错(我只要新的shm)

  • 共享内存中的各种参数

key VS shmid(类似fd 与 inode number)

先用 key 找 / 创建 → 得到 shmid → 所有后续操作只用 shmid

  1. shmid只在内核中,标识共享内存的唯一性!用户使用共享内存,不用这个key
  2. shmid只在用户中使用,你自己代码中,使用shmid来访问共享内存
  3. 删除共享内存:ipcrm -m不能跟key,而是跟shmid(指令的执行,本质也是用户代码在执行)
  4. key是系统全局内唯一,shmid是内核内部唯一
  • 共享内存冲突了,本质是key(使用 ftok() 获取)值冲突了
  • 共享内存(包括:system IPC)它的生命周期跟随内核
  • shmctl中的buff缓冲区:是用于与内核交换共享内存段的属性信息的载体
  • nattch:挂接数(类似引用计数)(表达的是,当前有多少个进程把我attach到它的地址空间)
  • 访问共享内存,进程一定要有权限,否则无法操作且会失败并返回错误码
  • 创建与获取消息队列

📍基于一个共享的队列,进程之间,可以实现基于有类型数据块(节点是一块一块的)级别的进程间通信

  1. 消息队列可能同时存在多份,OS要对消息队列进行管理
  2. 消息队列一定要有一个队列头,struct描述消息队列
  3. 进程A与进程B它们两个进程是怎么看见同一个队列的?

(1)核心原因:OS的统一标识定位和共享资源映射机制,再配合内核的统一管理

(2)次要原因:数据块是具有类型的

📍System V是一个标准:接口、返回值、参数、数据结构、key共享方式、创建和删除、底层原理具有共性

问:system V的shm,msg、sem为什么相似?

答:IPC资源=内核数据结构+资源本身(内存块,队列,计数器)

  • system V信号量

进程间需要IPC ---> 进程具有独立性 ---> 让不同的进程看到同一份资源 ---> OS提供

  1. 多个执行流,能同时看到并访问的公共资源 ---> 共享资源
  2. 任何时刻,只能有一个执行流访问公共资源 ---> 互斥
  • 原子性:要么不做,要么做完(汇编完只有一行代码)
  • 信号量、信号灯本质:是一个计数器,描述临界资源中,资源数量的多少!(申请信号量的本质:是对资源的预定机制)
  • 二元信号量:是一种特殊的信号量,其取值仅取0与1,核心用于实现互斥访问临界资源,是多进程 / 多线程同步的基础机制
  • 细节1:每个进程都要申请信号量,也就是每一个进程都必须看到同一个信号量
  1. 信号量本身就是共享资源
  2. 内核负责保护信号量的安全,以保证P / V操作是原子性的,避免出现竞态条件
  3. P操作(--):是原子的;V操作(++):是原子性的
  1. 二元信号量是互斥保护
  • sem的++、--操作不是原子的
  • 怎么保护多个进程看到的是同一个sem(类似管道)
  • 面试题:C语言实现多态

📍核心:通过以函数指针+结构体模拟面向对象的多态特性

  1. 定义基类结构体
  2. 定义派生类结构体
  3. 实现派生类的具体函数
  4. 统一接口调用:通过基类指针接收
  • 核心理解:内存映射(结构体层面)

📍核心:是将磁盘文件的一段数据直接映射到进程虚拟地址空间,并通过结构体指针直接访问和操作这段内存无需 read / write系统调用,本事是让struct与磁盘数据内存布局各自对应

  1. 磁盘文件的结构与内存结构对齐
  2. mmap建立映射关系
  3. 强制类型转化成结构体指针

📍管理虚拟地址空间的结构体与共享内存结构体都会指向同一个文件结构体,只要有文件就有文件缓冲区,缓存区即为共享内存的位置

相关推荐
艾莉丝努力练剑2 小时前
【Linux系统:信号】线程安全不等于可重入:深度拆解变量作用域与原子操作
java·linux·运维·服务器·开发语言·c++·学习
楼田莉子2 小时前
同步/异步日志系统:日志的工程意义及其实现思想
linux·服务器·开发语言·数据结构·c++
胖好白2 小时前
【ZYNQ的Linux开发】移植Ubuntu根文件系统
linux·ubuntu
泰白聊AI2 小时前
AI 编程时代的规范驱动开发:OpenSpec 实践指南
服务器·人工智能·驱动开发·ai·aigc·ai编程
w6100104662 小时前
CKAD-2026-Secret
运维·k8s·ckad
无巧不成书02182 小时前
基于WSL 2的Docker远程开发全栈实战指南
运维·docker·容器·docker desktop·wsl 2·vs code远程开发·容器化开发
一个天蝎座 白勺 程序猿2 小时前
踩坑生产后整理:KingbaseES表空间管理、auto_createtblspcdir参数深度解析与运维最佳实践
运维·数据库·kingbasees
赵庆明老师2 小时前
Linux Docker打包
linux·运维·docker
Eloudy2 小时前
docker pull ubuntu:22.04 失败的解决记录
运维·docker·容器