【知识讲解】 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文本处理命令理论学习
linux·服务器·前端·数据库·sql·学习·安全
无足鸟ICT1 小时前
【RHCA+】rpm命令(安装、升级、卸载和查询)
linux·运维·服务器
吹什么轩1 小时前
linux系统复习:从冯·诺依曼到操作系统:一切的开场
linux·运维·服务器
程序员-Benothing1 小时前
SSH从入门到精通:密钥登录、config配置、跳板机
linux·运维·服务器·ssh
H.莓飛1 小时前
【Linux】自主 Shell
linux·运维·服务器·c语言·开发语言·visual studio
bigcarp1 小时前
Frida-dexdump 绕过app的Root检测
linux·运维·服务器
ggaofeng1 小时前
linux中的绿色版内存mysql
linux·运维·mysql
草莓熊Lotso2 小时前
【C++ 基于微服务的即时通讯系统】Elasticsearch 从安装到 C++ 客户端二次封装全落地
linux·网络·数据库·c++·elasticsearch·微服务
LiaoWL1232 小时前
从零开始学 Linux(二):Shell 脚本入门与文本处理实战
linux·运维·服务器