Linux进程间通信

目录

进程间通信

概念

进程间通信是指不同进程之间交换数据、传递信号或同步操作的机制。由于进程拥有独立的地址空间,无法直接访问彼此的内存,因此需要操作系统提供专门的 IPC 机制。

目的

数据传输: 一个进程需要将它的数据发送给另一个进程。

资源共享: 多个进程之间共享同样的资源。

通知事件: 一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件,比如进程终止时需要通知其父进程。

进程控制: 有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

本质

进程间通信的本质是让不同进程看到同一份资源。

由于进程独立性,不同进程需要通过OS提供的第三方资源进行通信。

方式

进程间通信的本质是让不同的进程看到同一份资源(内存,文件内核缓冲等)。 由于这份资源可以由操作系统中的不同模块提供,因此出现了不同的进程间通信方式。

管道

  • 匿名管道
  • 命名管道
    System V IPC
  • System V 消息队列
  • System V 共享内存
  • System V 信号量
    POSIX IPC
  • 消息队列
  • 共享内存
  • 信号量
  • 互斥量
  • 条件变量
  • 读写锁

管道

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

管道分为匿名管道和命名管道,它们都由操作系统提供给进程。

匿名管道:由内核维护的内存缓冲区,没有文件系统实体,通过文件描述符供进程访问,生命周期与相关进程绑定,适用于亲缘进程间的短期通信。

命名管道:以特殊文件(类型为p)的形式存在于文件系统中,有明确的路径标识,任何进程只要知道路径就能访问,生命周期独立于进程(需手动删除文件),适用于任意进程间的长期通信。

不同命令通过管道通信:

匿名管道

匿名管道的原理

  1. 由父进程调用pipe打开的内存级文件(缓冲区),通过文件描述符表指向其struct file实现连接
  2. fork创建子进程,拷贝父进程的文件描述符表,其中指向与父进程打开的管道文件,由此可以通过该管道与父进程通信。
  3. 父子双方只保留读写的一端(管道是半双工通信,同一时刻只能单向通信),关闭另一端。
    struct file 包含了大量进程特定的状态信息,所以每个进程打开文件时需要独立的实例,而 struct inode 才是真正共享的文件元数据。
    文件描述符角度
    管道是由操作系统提供的,发现数据修改时不会写时拷贝,而是父子都能看到。
    管道是内存级的文件,不会拷贝到磁盘中。
    管道只能进行单向通信,所以子进程创建后,父子双方只保留一端。

pipe函数

pipe函数用于创建匿名管道,pip函数的函数原型如下:

int pipe(int pipefd[2]);

pipe函数的参数是一个输出型参数,数组pipefd用于返回两个指向管道读端和写端的文件描述符:

数组元素 含义

pipefd[0] 管道读端的文件描述符

pipefd[1] 管道写端的文件描述符

pipe函数调用成功时返回0,调用失败时返回-1。

管道读写规则

pipe2函数与pipe函数类似,也是用于创建匿名管道,其函数原型如下:

cpp 复制代码
int pipe2(int pipefd[2], int flags);

pipe2函数的第二个参数用于设置选项。

1、当没有数据可读时:

禁用O_NONBLOCK :read调用阻塞,即进程暂停执行,一直等到有数据来为止。

使用O_NONBLOCK :read调用返回-1,errno值为EAGAIN。

2、当管道满的时候:

禁用O_NONBLOCK :write调用阻塞,直到有进程读走数据。

使用O_NONBLOCK :write调用返回-1,errno值为EAGAIN。

3、如果所有管道写端对应的文件描述符被关闭,则read返回0。

4、如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出。

5、当要写入的数据量不大于PIPE_BUF(管道容量)时,Linux将保证写入的原子性。

6、当要写入的数据量大于PIPE_BUF时,Linux将不再保证写入的原子性。

7、管道中的数据被读取后就删除

管道的四种特殊情况

  1. 读快写慢,读的进程阻塞等待写入
  2. 读慢写快,缓冲区写满后不能继续写,等待读取后删除部分缓存数据才能写
  3. 读关,写端进程再写时被OS信号杀掉
  4. 写关,read读时返回值为0

管道的特点

  1. 管道自带同步机制,由上述情况的阻塞等待可知。
  2. 匿名管道只能用于父子(兄弟)之间通信,因为依赖从父进程拷贝的fd
  3. 数据以连续字节流形式传递,无消息边界(需应用层自行定义分隔符或长度标识),类似 "水流" 持续流动。
  4. 管道是半双工通信,同一时间只能单向通信,一写一读
  5. 管道依赖文件描述符操作(read()/write()/close()),因此生命周期跟随进程
  6. 数据一次性,数据读取后立即删除

管道的特点

  1. 管道自带同步机制,由上述情况的阻塞等待可知。
  2. 匿名管道只能用于父子(兄弟)之间通信,因为依赖从父进程拷贝的fd
  3. 数据以连续字节流形式传递,无消息边界(需应用层自行定义分隔符或长度标识),类似 "水流" 持续流动。
  4. 管道是半双工通信,同一时间只能单向通信,一写一读
  5. 管道依赖文件描述符操作(read()/write()/close()),因此生命周期跟随进程
  6. 数据一次性,数据读取后立即删除

管道的大小

ulimit -a命令,查看当前资源限制的设定。512×8=4096 字节

实现进程池

框架:

创建子进程部分:

出现错误并退出的宏

通过do while(0)简化合并为一个语句,并且能避免在if else语句中的包裹错误情况

父子进程共享管道导致的资源泄漏问题

子进程拷贝文件描述符表时也会引用哥哥进程的写端的file,导致先创建的子进程的写端被父进程和所有子进程持有,从父进程关闭时写端时struct file的引用计数--不会关闭该管道(管道资源泄漏),进而不能关闭子进程。

解决管道资源泄漏的核心: 及时关闭不需要的文件描述符

子进程拿到的vector不会更新之后创建的子进程,因为子进程在创建时拿到父进程的vector,之后父进程再创建子进程修改vector发生写时拷贝,子进程的vector不会更新

理解管道的字节流

读取数据时对缓冲区buffer的处理:

  1. 不处理
  2. 预留一个位置sizeof(buffer)-1 填\0,后续结合strlen(输出)
    前者是将读取的数据作为字节流不处理,后者需要将读取的数据作为字符串所以需要结合字符串的规则(\0结尾)

命名管道

命名管道(FIFO)是一种用于进程间通信(IPC)的特殊文件类型,它允许不相关的进程通过文件系统中的路径名进行通信。与匿名管道不同,命名管道具有持久化的文件系统实体,因此可以用于无亲缘关系的进程间通信。

命名管道(FIFO)确实有文件系统中的 "目录项"(即文件名和元数据),但数据本身仅存储在内存的内核缓冲区中,不会持久化到磁盘

命名管道的特点

命名管道也具有和匿名管道相同的基础特性:

  1. 半双工通信
  2. 字节流传输
  3. 读写同步阻塞
  4. 数据一次性
  5. 打开后依赖文件描述符操作(read()/write()/close())
    不同的是:
  6. (关键区别)命名管道基于文件系统,具有持久化文件系统实例
  7. 允许两个无关联进程进行通信(通过查找FIFO的文件实例)
  8. 生命周期由文件系统管理,不随进程文件描述符关闭而删除
  9. 匿名管道通过usmask控制权限,命名管道直接在创建时设置权限

命名管道创建和打开方式

mkfifo函数创建命名管道

在程序中创建命名管道使用mkfifo函数,mkfifo函数的函数原型如下:

cpp 复制代码
int mkfifo(const char *pathname, mode_t mode);

第一个参数pathname,是创建到的目标路径已经设置管道的文件名。

第二个参数mode,设置管道的权限(命名管道是文件实例,有wrx权限)

p就表示该文件是命名管道文件

open打开命名管道

命名管道创建后,就和打开普通文件一样使用open系统调用打开

也可以设置阻塞打开模式:

命名管道使用

  1. 命名管道实现cliend&server通信
    创建后打开方式和打开普通文件相同
  2. 命名管道实现文件拷贝

systemV

System V共享内存

共享内存让不同进程看到同一份资源的方式就是,在物理内存当中申请一块内存空间,然后将这块内存空间分别与各个进程各自的页表之间建立映射,再在虚拟地址空间当中开辟空间并将虚拟地址填充到各自页表的对应位置,使得虚拟地址和物理地址之间建立起对应关系,至此这些进程便看到了同一份物理内存,这块物理内存就叫做共享内存。

本质:让多个进程接访问同一块物理内存区域,从而实现数据共享。与管道、消息队列等 IPC 方式相比,共享内存的优势在于无需数据拷贝(数据直接在内存中共享),因此通信效率极高 。

共享内存的内核数据结构

OS需要对所有共享内存进行管理,所以内核中需要有对共享内存描述的数据结构,struct _shmid_ds内部包含struct ipc_perm其中包含共享内存的唯一标识符(key)和权限信息

共享内存的建立与释放

共享内存的建立大致包括以下两个过程:

  1. 在物理内存当中申请共享内存空间。
  2. 将申请到的共享内存挂接到地址空间,即建立映射关系。
    共享内存的释放大致包括以下两个过程:
  3. 将共享内存与地址空间去关联,即取消映射关系。
  4. 释放共享内存空间,即将物理内存归还给系统。

共享内存的创建

创建共享内存我们需要用shmget函数,shmget函数的函数原型如下:

int shmget(key_t key, size_t size, int shmflg);

参数说明:

  1. 第一个参数key,表示待创建共享内存在系统当中的唯一标识(类似文件的inode),但这个key需要用户提供,因为需要通信的进程需要提前约定好使用同一个共享内存,就需要自己设定唯一标识,然后使用时打开。
  2. 第二个参数size,表示待创建共享内存的大小,是4096(4kb)的整数倍,向上取整。
  3. 第三个参数shmflg,表示创建共享内存的方式和共享内存的权限。
    两种组合方式:都是根据key创建一个shm,区别是第一个可以已经存在,第二个必须创建新的,如果已存在就返回错误
    共享内存类似文件,在创建时需设置权限否则无法关联
    返回值说明:
  • shmget调用成功,返回一个有效的共享内存标识符(用户层标识符)。
  • shmget调用失败,返回-1。
    返回值就是共享内存的临时句柄。

共享内存的释放

有命令行和系统调用两种方式

命令行删除:使用句柄shmid

ipcrm -m 8

系统调用shmctl

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

参数说明:

  1. 第一个参数shmid,表示所控制共享内存的用户级标识符。
  2. 第二个参数cmd,表示具体的控制动作。
  3. 第三个参数buf,用于获取或设置所控制共享内存的数据结构。
    返回值说明:
    成功,返回0。
    失败,返回-1。

共享内存的关联和去关联

进程需要像加载动态库一样将共享内存映射到自己的进程地址空间,需要使用shmat函数

void *shmat(int shmid, const void shmaddr, int shmflg);
参数说明:
第一个参数shmid,表示待关联共享内存的用户级标识符。
第二个参数shmaddr,指定共享内存映射到进程地址空间的某一地址,通常设置为NULL,表示让内核自己决定一个合适的地址位置。
第三个参数shmflg,表示关联共享内存时设置的某些属性。
返回值说明:
shmat调用成功,返回共享内存映射到进程地址空间中的起始地址。
shmat调用失败,返回(void
)-1。

取消共享内存与进程地址空间之间的关联我们需要用shmdt函数,shmdt函数的函数原型如下:

int shmdt(const void *shmaddr);

参数传共享内存地址,成功返回0失败返回1

总结

  • 共享内存是不同进程都能直接访问的一块物理内存,就和直接使用自己进程的内存一样效应很高。
  • 共享内存和文件的管理方式类似,需要一个内核级的唯一标识key(inode)包含在内核的描述数据结构(struct _shmid_ds)中,这个key由通信双方约定,提前由ftok函数创建。也有一个用户级的句柄shmid(fd),在用户层操作中引用该共享内存资源。
  • 共享内存在创建后还需要和文件一样被进程打开,需要和进程关联,使用完毕后需要去关联。释放时OS会根据关联数>0延迟对shm的资源释放

实现shm通信

框架:

构造函数直接创建并连接共享内存

shmget函数的参数控制

清理工作:

System V消息队列

  • 消息队列提供了⼀个从⼀个进程向另外⼀个进程发送数据块的⽅法
  • 每个数据块都被认为是有⼀个类型,接收者进程接收的数据块可以有不同的类型值
  • 特性⽅⾯ ◦ IPC资源必须删除,否则不会⾃动清除,除⾮重启,所以systemVIPC资源的⽣命周期随内核
  • 消息队列通过 "存储 - 转发" 模式彻底打破了任务间的直接依赖,实现了 "生产者" 与 "消费者" 的完全解耦和异步通信。这种机制的核心逻辑可以概括为:"发送消息不等待,接收消息按需取,中间队列做缓冲"。
  • 消息队列克服了传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点

System V信号量

相关概念

共享资源:多个执行流,能看到的同一份公共资源

临界(互斥)资源:被保护的共享资源

只有 "被修改时会产生冲突" 的共享资源需要被保护,例如两个进程读一个配置文件,不需要被保护

临界区:进程中涉及到互斥资源的代码段

保护临界资源本质是通过保护临界区的代码实现的

互斥和同步都是共享资源在同一时间只能由一方访问,但互斥不涉及顺序,只是先到先得,同步有严格的顺序设置(比如先买菜才能做饭)

原子性与共享资源保护

原子性(是指一个操作或一组操作在执行过程中,要么完全执行完毕(所有步骤都成功),要么完全不执行(任何步骤都未生效),中间不会被其他操作打断,也不会出现 "部分完成" 的中间状态。

共享资源之所以需要保护就是因为执行流对共享资源的操作可能不具有原子性,会被打断导致不安全。

保护共享资源的本质,是通过各种保护措施(如原子操作、互斥锁、读写锁等),使对共享资源的操作在 "逻辑层面" 具备原子性

信号量

信号量是操作系统中用于协调多个执行流(进程 / 线程)对共享资源访问的同步机制,本质是一个 "计数器 + 等待队列",通过控制计数器的增减来实现 "资源占用" 与 "释放" 的管理,避免并发场景下的资源竞争问题。

  • 若资源是 "独占的(仅 1 个)",用二元信号量(互斥);
  • 若资源是 "有限的(多个)" 或需 "协调顺序",用多元信号量(同步 + 并发控制)。
    信号量通过 "P 操作(++)申请资源、V 操作(--)释放资源" 的原子性,结合等待队列实现阻塞 / 唤醒,本质是 "用计数器控制资源访问权限"。
    信号量本质是一个计数器,是对特定资源的预订机制

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

这里用一个数组指向各种IPC结构体的第一个成员,间接指向各种IPC结构体。

本质也是C语言版本多态的实现

相关推荐
UP_Continue12 小时前
Linux--进程控制
linux·运维·服务器
请输入蚊子12 小时前
«操作系统真像还原» 第二章 编写MBR主引导记录
linux·汇编·操作系统·bochs·操作系统真像还原
188号安全攻城狮13 小时前
【PWN】HappyNewYearCTF_8_ret2csu
linux·汇编·安全·网络安全·系统安全
喵叔哟13 小时前
02-CSharp基础语法快速入门
服务器
Yana.nice14 小时前
openssl将证书从p7b转换为crt格式
java·linux
AI逐月14 小时前
tmux 常用命令总结:从入门到稳定使用的一篇实战博客
linux·服务器·ssh·php
想逃离铁厂的老铁14 小时前
Day55 >> 并查集理论基础 + 107、寻找存在的路线
java·服务器
小白跃升坊15 小时前
基于1Panel的AI运维
linux·运维·人工智能·ai大模型·教学·ai agent
跃渊Yuey15 小时前
【Linux】线程同步与互斥
linux·笔记
舰长11515 小时前
linux 实现文件共享的实现方式比较
linux·服务器·网络