【知识讲解】 Linux进程间通信之管道与共享内存


目录

前言

[Part1. 进程池使用匿名管道容易潜藏的问题](#Part1. 进程池使用匿名管道容易潜藏的问题)

[Part1.1. 问题根源](#Part1.1. 问题根源)

[Part1.2. 解决方案](#Part1.2. 解决方案)

[Part1.3. 子进程拷贝vector](#Part1.3. 子进程拷贝vector)

[Part2. 命名管道](#Part2. 命名管道)

[Part2.1. 命名管道的意义](#Part2.1. 命名管道的意义)

[Part2.2. 创建命名管道 mkfifo](#Part2.2. 创建命名管道 mkfifo)

[Part2.3. FIFO通信模型](#Part2.3. FIFO通信模型)

[Part2.4. FIFO的阻塞特性](#Part2.4. FIFO的阻塞特性)

[Part3. 共享内存](#Part3. 共享内存)

[Part3.1. 共享内存原理](#Part3.1. 共享内存原理)

[Part3.2. 共享内存相关API](#Part3.2. 共享内存相关API)

[Part3.3. shmget](#Part3.3. shmget)

[Part3.4. shmat](#Part3.4. shmat)

[Part3.5. shmdt](#Part3.5. shmdt)

[Part3.6. shmctl](#Part3.6. shmctl)

[Part3.7. 命令行相关](#Part3.7. 命令行相关)

[Part4. 共享内存的特性、优缺点与同步问题](#Part4. 共享内存的特性、优缺点与同步问题)

[Part5. 结语](#Part5. 结语)


前言

Linux下对于进程间通信的了解是非常重要的,接下来来跟随小编的视角来看看吧。


let's go!!!!!!!!


Part1. 进程池使用匿名管道容易潜藏的问题

Part1.1. 问题根源

父进程持续循环创建写端,每次fork()产生子进程,子进程会继承父进程已经打开的文件描述符。
这就会造成一个严重问题:管道写端引用计数不断累加。

管道内核缓冲区只有所有写端全部关闭的时候,读端read()才会返回0,表示读到末尾,通信结束。
如果大量继承的写端fd没有关闭,引用计数不为0,读端会一直阻塞,无法正常感知通信结束,程序卡住。


Part1.2. 解决方案

倒着关:在fork()创建子进程之后,子进程手动关闭自己不需要的写端文件描述符,把多余fd关闭,保证管道引用计数正确。


Part1.3. 子进程拷贝vector

疑问:父进程保存fd的vector,fork之后子进程拿到的vector,修改子进程里面的vector会不会影响父进程?
答案:不会。
fork采用写时拷贝(Copy On Write)。子进程拿到的是fork瞬间父进程vector的快照,子进程对vector修改的时候,才会分配新内存,父子进程vector相互独立,互不干扰。


Part2. 命名管道

Part2.1. 命名管道的意义

匿名管道pipe只能用于有亲缘关系(父子、兄弟)的进程通信。
命名管道FIFO解决这个痛点:毫无血缘关系的独立进程之间,也可以使用管道通信。

原理:
多个独立进程打开同一个FIFO文件。但是这个文件并不会把数据重复加载到磁盘。文件仅仅是一个入口标识,多个进程的struct file指向内核里面同一份管道缓冲区,数据保存在内核内存,不会重复加载,实现多个进程访问同一个管道缓冲区。

一句话区分:

• pipe匿名管道:无文件名,仅亲缘进程

• FIFO命名管道:磁盘上存在文件路径,任意进程均可打开通信


Part2.2. 创建命名管道 mkfifo

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

• 返回值:成功返回0;失败返回-1,设置errno

• 参数:

◦ filename:管道文件路径名称

◦ mode:文件权限,例如0666


Part2.3. FIFO通信模型

典型C/S模型

1. 服务端(S端):调用mkfifo创建管道文件,以读方式打开,阻塞等待接收数据,处理业务,最后关闭文件。

2. 客户端(C端):以写方式打开FIFO,写入请求数据,写完关闭。

可以实现:客户端发送请求,服务端接收请求,处理之后返回结果,完成一次完整通信。


Part2.4. FIFO的阻塞特性

1. 如果写端还没有调用open打开管道文件,那么读端在open()函数内部就会阻塞,等待写端打开。

2. 当所有写端全部关闭之后,读端read()返回0,代表通信结束,和匿名管道行为保持一致。
基于这个特性,我们可以把FIFO封装成简易的请求通道,实现跨无关进程通信。


Part3. 共享内存

共享内存是所有IPC里面速度最快的进程间通信方案。


Part3.1. 共享内存原理

在物理内存开辟一块独立的共享内存区域,然后把这块物理内存,分别映射到多个不同进程的虚拟地址空间。

多个进程虚拟地址最终指向同一块物理内存,进程之间直接读写这块内存,完成通信。

优势:数据不需要在内核缓冲区、用户缓冲区之间来回拷贝,进程直接读写内存,省去大量数据拷贝开销,所以速度远快于管道、消息队列。


Part3.2. 共享内存相关API

函数 作用
shmget 创建 / 获取一块共享内存,得到shmid
shmat 将共享内存挂载(attach)映射到当前进程虚拟地址空间
shmdt 解除挂载(detach),断开进程与共享内存映射
shmctl 共享内存控制:获取属性、删除共享内存等操作


Part3.3. shmget

cpp 复制代码
int shmget(key_t key, size_t size, int shmflg);

1. key:共享内存的唯一标识。多个进程传入相同key,就能找到同一块共享内存。
为什么不用进程pid作为key?进程pid每次启动会变化,进程退出之后pid会被复用,无法用来稳定标识一块共享内存。

生成key工具函数:ftok

cpp 复制代码
key_t ftok(const char *pathname, int id);

根据指定文件路径+项目id,稳定生成唯一key,多个进程使用同样pathname、id,拿到同一个key。

2. size:共享内存大小,内核会自动向上取整到内存页大小(4096字节一页)
例:你申请4097字节,内核实际分配 2 * 4096 = 8192字节,但是对外标识大小仍然是4097。

3. shmflg:标志位,常用组合:

◦ IPC_CREAT:不存在就创建;已经存在直接获取

◦ IPC_CREAT | IPC_EXCL:必须是新建,如果这块共享内存已经存在,直接报错返回

◦ 后面按位或权限,如 0666

返回值:成功返回共享内存id(shmid),失败返回 -1


Part3.4. shmat

cpp 复制代码
void *shmat(int shmid, const void *shmaddr, int shmflg);

• shmid:shmget返回的共享内存id

• shmaddr:指定想要挂载到进程虚拟地址哪个位置,绝大多数场景直接传NULL,交给操作系统自动选择合适虚拟地址(推荐写法)

• shmflg:挂载标记,可以设置只读等权限;一般传0,可读可写

返回值:成功返回共享内存映射之后起始虚拟地址,失败返回(void *)-1

使用:拿到返回地址之后,直接强转,就可以像普通内存一样读写

cpp 复制代码
char* mem = (char*)shmat(shmid, NULL, 0);
strcpy(mem, "hello shm");
printf("%s", mem);

Part3.5. shmdt

cpp 复制代码
int shmdt(const void *shmaddr);

• 参数:shmat返回的内存首地址

• 返回:成功返回0;失败返回-1
作用:解除当前进程和这块共享内存之间的映射,引用计数减一。
注意:shmdt仅仅是断开本进程的映射,不会直接删除这块共享内存!


Part3.6. shmctl

cpp 复制代码
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

常用cmd:

1. IPC_RMID:标记删除这块共享内存。
注意:调用IPC_RMID并不会立刻销毁。只有当所有进程全部shmdt解除挂载,引用计数变成0的时候,内核才真正释放这块共享内存。
删除的时候buf参数直接传NULL。

2. IPC_STAT:读取共享内存属性,填充struct shmid_ds结构体,拿到共享内存权限、大小、引用计数等信息。


Part3.7. 命令行相关

cpp 复制代码
# 查看系统当前所有共享内存
ipcs -m
# 删除指定shmid的共享内存
ipcrm -m shmid

重大坑点:
进程退出,如果没有主动调用shmctl(IPC_RMID),共享内存不会自动被操作系统释放,会永久残留在内核,直到手动ipcrm删除或者关机重启,这是System V IPC非常经典的大坑!管道随进程销毁,但是共享内存生命周期独立于进程。


Part4. 共享内存的特性、优缺点与同步问题

优点

1. 速度最快,没有多余的数据拷贝,进程直接读写物理内存,高性能IPC首选。

2. 无关进程之间可以通信。


致命缺陷:本身不带任何同步互斥机制

管道自带阻塞等待,但是共享内存没有内置等待机制。
问题场景:进程A还没有写完数据,进程B直接读取,读到残缺、不完整的数据,产生并发数据混乱。

解决方案

共享内存本身不提供同步,必须额外搭配同步工具:信号量、互斥锁等,保护临界区,保证写完成之后,读进程再读取。

简单业务模型示例
A写,B读
A拿到锁 → 向共享内存写入数据 → 写完释放锁,通知B
B拿到锁 → 读取完整数据


Part5. 结语

这篇文章我们深入的知道了进程间通信的相关知识,接下来,小编还会带来更多Linux相关的知识,敬请期待~
最后,祝大家可以:春风得意马蹄疾,一日看尽长安花!

最后的最后,要是觉得本文还可以的话,可以点点赞,关注小编一波,谢谢大家!~

相关推荐
承渊政道1 小时前
Linux网络学习【Reactor反应堆模式】
linux·网络·学习·ubuntu·reactor
且从容.4 小时前
ubuntu安装flink
linux·ubuntu·flink
爱和冰阔落6 小时前
【Linux网络】TCP 服务器如何并发处理多个客户端?从 fork、僵尸进程到双重 fork
linux·运维·服务器·网络
程序员沉梦听雨6 小时前
WSL2的安装与使用
linux·wsl
微小的鱼6 小时前
Linux的渲染与显示系统链路
android·java·linux
lisanmengmeng6 小时前
alertmanger 标签 rules 打上分组标签
linux·运维·服务器
Android系统攻城狮7 小时前
Linux FFmpeg深度解析之av_write_frame调用流程与实战(三十三)
linux·运维·服务器·ffmpeg·音视频进阶·ffmpeg音视频
Linux-lucky19 小时前
45-Linux学习之旅之防火墙与WEB加固
linux·前端·网络·学习·ubuntu
禁默19 小时前
启元 Q1 二次开发实践:Ubuntu 部署与仿真验证,从 PrimeConsole 到 ROS 2 Humble + RViz
linux·运维·ubuntu
skywalk816319 小时前
把 去也网站 从 Ubuntu 搬到 FreeBSD:一次 Service Jail + nginx 的部署实录
linux·运维·nginx·ubuntu·freebsd