目录
[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
cppint 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
cppint shmget(key_t key, size_t size, int shmflg);1. key:共享内存的唯一标识。多个进程传入相同key,就能找到同一块共享内存。
为什么不用进程pid作为key?进程pid每次启动会变化,进程退出之后pid会被复用,无法用来稳定标识一块共享内存。生成key工具函数:ftok
cppkey_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
cppvoid *shmat(int shmid, const void *shmaddr, int shmflg);• shmid:shmget返回的共享内存id
• shmaddr:指定想要挂载到进程虚拟地址哪个位置,绝大多数场景直接传NULL,交给操作系统自动选择合适虚拟地址(推荐写法)
• shmflg:挂载标记,可以设置只读等权限;一般传0,可读可写
返回值:成功返回共享内存映射之后起始虚拟地址,失败返回(void *)-1
使用:拿到返回地址之后,直接强转,就可以像普通内存一样读写
cppchar* mem = (char*)shmat(shmid, NULL, 0); strcpy(mem, "hello shm"); printf("%s", mem);
Part3.5. shmdt
cppint shmdt(const void *shmaddr);• 参数:shmat返回的内存首地址
• 返回:成功返回0;失败返回-1
作用:解除当前进程和这块共享内存之间的映射,引用计数减一。
注意:shmdt仅仅是断开本进程的映射,不会直接删除这块共享内存!
Part3.6. shmctl
cppint 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相关的知识,敬请期待~
最后,祝大家可以:春风得意马蹄疾,一日看尽长安花!最后的最后,要是觉得本文还可以的话,可以点点赞,关注小编一波,谢谢大家!~
