1. 什么是进程间通信
进程是操作系统隔离的独立程序,每个进程有自己独立虚拟地址空间。 默认情况下:A 进程不能直接读写 B 进程的内存,互相看不见对方的数据。
IPC:让多个进程之间交换数据、传递消息、同步协作的一套机制。
2. 为什么需要 IPC?为什么要这么做
① 数据共享
一个进程产生数据,另一个进程要拿来用。 例子:
- shell 执行管道命令
ls | grep txtls进程输出结果,交给grep进程做过滤,两个进程必须传数据,没有 IPC 就做不到。
② 任务拆分,分工协作
把一件大任务拆成多个进程并行干活。 比如:父进程 fork 出子进程,子进程负责计算,父进程负责接收结果打印;子进程干活,父进程负责调度。 多个进程分工,就要互相传递结果。
③ 事件通知、状态同步
进程 A 告诉进程 B:"我做完了,你可以开始执行"。 实现进程之间的等待、通知。
④ 资源协调
多个进程抢同一个资源,通过 IPC 做互斥,防止乱抢出错。
如果没有 IPC:进程之间完全孤岛,fork 出来子进程复制一份内存,fork 之后父子进程修改各自变量互不影响,父子不能直接互通。
3.怎么做
进程间是有独立性的,

首先让不同的进程,要先看到同一份资源

该功能由OS提供
通信原理

参数
pipefd[2] 是输出型参数,传入一个 int 数组,pipe 函数会往里面填入两个文件描述符:
pipefd[0]:读端 read end,从管道读取数据pipefd[1]:写端 write end,向管道写入数据
💡 管道是单向半双工 :数据只能从
pipefd[1]写入,从pipefd[0]读出,不能反向。
cpp
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include <sys/wait.h>
int main()
{
//创建管道
int pipefd[2]={0};
int n=pipe(pipefd);
if(n<0)
{
perror("pipe");
return 1;
}
printf("pipefd[0]:%d,pipefd[1]:%d\n",pipefd[0],pipefd[1]);
//创建子进程
pid_t id=fork();
if(id==0)
{
//子进程
close(pipefd[0]);
char msg[]="hello world\n";
int cnt=10;
char outbuffer[1024];
while(cnt)
{
snprintf(outbuffer,sizeof(outbuffer),"f->c# %s %d %d\n",msg,cnt--,getpid());//snprintf->最后加\0
write(pipefd[1],outbuffer,strlen(outbuffer));//要不要+1??
}
// write(pipefd[1],msg,strlen(msg));
exit(0);
}
//父进程
close(pipefd[1]);
char inbuffer[1024];
while (1)
{
size_t n=read(pipefd[0],inbuffer,sizeof(inbuffer)-1);//read不管什么都要读,我们最后\0
if(n>0)
{
inbuffer[n]=0;//0->\0
printf("%s\n",inbuffer);
}
else if(n==0)
{
}
else
{
perror("read");
break;
}
}
pid_t rid=waitpid(id,NULL,0);
(void)rid;
return 0;
}

管道特性
1.管道只能单向通信,单工通信
2.匿名管道只能用来进行拥有血缘关系的进程之间通信,常用父子通信
3.管道是面向字节流的
4.管道生命周期随进程
5.管道通信,对于多进程而言,是自自带互斥与同步机制
管道通信前提是让不同进程看到同一份资源--文件--文件内的缓冲区--内存块,这是属于父子进程的共享资源,存在并发访问的问题----互斥与同步
互斥:保护共享资源,同一时刻,只允许一个进程访问临界资源。
临界资源:管道缓冲区、共享内存,一次只能一个人改,不能多人同时写。
同步:进程之间执行时序上的等待、通知,协调先后顺序。 简单说:A 进程要等 B 进程做完某件事,A 才继续干活。
管道天然自带同步机制(阻塞):
- 读端
read():管道缓冲区为空,读进程阻塞休眠,等待别的进程往管道写数据。 - 写端
write():管道缓冲区满,写进程阻塞休眠,等待别的进程把数据读走,腾出空间
4种情况
1.子进程写的慢,父进程要阻塞等待,扽管道有数据,父进程才能读
2.子进程写的快,父进程不读,管道一旦被写满,子进程就必须阻塞了
管道为空,读就要阻塞,给写机会;管道为满,写就要阻塞,给读机会。
让子进程一直向管道里面去写

让父进程一直等待

- 子进程:循环不停向管道写字符
'a',不关闭写端 - 父进程:
sleep(100)睡很久,才调用read读取管道
现象
- 管道缓冲区有空:
write直接返回,子进程疯狂循环,size 持续上涨。 - 管道缓冲区写满之后 :
write(pipefd[1])会阻塞,子进程卡在 write 系统调用,不再 printf 打印 size。 - 父进程还在
sleep(100),迟迟不调用 read;此时子进程被内核挂起,不再跑。 - 100 秒之后父进程 read,一次性把缓冲区的数据全部读出来,打印。


3.读端在读,写端关闭,读端读完管道中的数据,在读就会读到空串,read返回值为0,表明读管道读到文件结尾
子进程写完,然后关闭

父进程一直读


- 子进程:write 写数据 →
close(pipefd[1])(子进程写端关闭) - 父进程早已执行
close(pipefd[1]),所有写端全部关闭。 - 第一次 read:读到管道缓冲区数据,n=13,inbuffer 被填充为
hello world\n。 - 第二次及以后 read:管道空,全部写端已关闭 →
read返回0。内核不会往 inbuffer 拷贝任何字节
4.写端一直在写,读端不读关闭 fd OS 会直接杀掉写的进程!!

子进程一直在往管道写

父进程读了一次后,不在读取管道

信号 13 = SIGPIPE,就是管道写的时候,所有读端已经关闭,内核发送管道破裂信号杀死子进程
一个进程控制一批进程

read的时候进程会阻塞等待

父进程
- 循环创建 4 个子进程,每 fork 一次,创建一根 pipe。
- 父进程保留每根管道的写端,关闭读端。
- 每个子进程保留对应管道的读端,关闭写端。
- 需要派发任务时:父进程选中某一根管道,写入 1 字节任务编号,对应子进程收到后运行任务。
这是 Linux 经典主从模式进程池,利用匿名管道完成父子进程任务分发。

Master‑Slave 进程池
- Master(父进程,主) :负责任务调度,拥有 4 根管道的写端,向管道写入任务编号。
- Slave(子进程,从,共 4 个):每个子进程绑定一根管道的读端,阻塞等待读取任务号,拿到编号执行对应函数。
func_t tasks[4]:函数指针数组,存放 4 个任务函数,tasks[ch]就取出对应任务执行。