进程间通信
什么是进程间通信
进程间通信(IPC)是指不同进程之间交换数据或协作的机制。由于每个进程都有独立的地址空间,它们不能直接访问彼此的内存,因此需要 IPC 机制来进行数据传输和同步。
进程间通信的方式
常见的通信方式有:管道,消息队列,共享内存,信号量,信号,套接字等等
这期我们主要讲的是管道通信
管道通信
什么是管道通信
管道通信是一种 进程间通信 方式,允许 相关进程 之间通过 管道 进行数据传输。管道本质上是一个 FIFO(First In, First Out) 的数据流,写入数据的一端称为 写端,读取数据的一端称为 读端。
为什么可以实现通信?
我们知道进程之间是相互独立的,每个进程都是一个独立的个体,所以我们不能直接进行进程间通信,很明显,我们需要一个媒介来承载这个信息将一个信息写入到这个媒介当中,然后通过这个媒介传入到另一个进程,这就实现了我们所说的管道通信,这个媒介就叫做管道。
管道通信的原理
首先要实现管道通信肯定不能是进程之间某一个进程提供资源,应该是操作系统提供资源,因为进程之间的资源都是相互独立的,就比如说,之前实验过的,父进程的代码中有一个全局变量,当父子进程不修改只读时,用的都是同一份数据,但是当修改全局变量时会发生写实拷贝,所以父子进程之间的资源是不能直接互通的,所以应该由操作系统提供资源,让两个进程同时看到这个公共资源。
就拿上图为例,上图的公共资源就是内核级缓冲区,上面的进程是父进程,下面是子进程,这里的公共资源就是文件的内核级缓冲区,所以父进程就可以向内核级缓冲区写数据,然后子进程读取内核级缓冲区的数据。
这里内核级缓冲区就充当管道,但是这里内核级缓冲区很显然是牛刀小用了,因为内核级缓冲区还有一个功能就是做刷新,刷新到磁盘当中,很明显没有必要,因为我们实现的是进程之间的通信,应该跟磁盘撇清关系,所以操作系统提供了自己的系统级调用,只用于两个进程之间通信的管道。
管道通信的过程
上图就是管道通信的过程。
第一步:创建管道
第二步:创建子进程
第三步:根据父子进程的通信的要求删除没有必要的端口
第四步:进行通信
用代码实现管道通信
管道接口:
#include <iostream>
#include <unistd.h>
#include <string>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstdlib>
using namespace std;
//father process -> read
//child process -> write
int main()
{
//创建一个管道
int fds[2] = {
0};
int n = pipe(fds);//fds是输出型参数
if(n != 0)
{
cerr<<"pipe error"<<endl;
return 1;
}
//创建子进程
pid_t id = fork();
//创建失败
if(id < 0)
{
cerr<<"fork error"<<endl;
//退出码设为2
return 2;
}
else if(id == 0)
{
//子进程
//3.关闭不需要的fd
close(fds[0]);//子进程关闭读端
int cnt = 0;
while(true)
{
string message = "hello world,hello ";
message += to_string(getpid());
message +=',';
message += to_string(cnt);
//向fds1中写入,写入message,大小是message.size()
write(fds[1],message.c_str(),message.size());
cnt++;
sleep(1);
}
exit(0);
}
if(id > 0)
{
//父进程
//3.关闭不需要的fd
char buffer[1024];
close(fds[1]);//父进程关闭写端
while(true)
{
//从fds0中读取数据读到buffer当中
ssize_t n = read(fds[0],buffer,1024);
if(n > 0)//读取成功
{
buffer[n] = 0;
cout<<"child -> father,message: "<<buffer<<endl;
}
}
//父进程会阻塞等待子进程终止
pid_t rid = waitpid(id,nullptr,0);
cout<<"father wait child success: "<<rid<<endl;
}
return 0;
}
上述代码是子进程发送消息给父进程,子进程是每一秒钟给父进程发送一个消息,所以这里我们可以将中间的时间间隔改长一点。
当上面时间间隔改为每五秒传递一次消息的时候,子进程没有发送消息的时候父进程是阻塞的,阻塞在read,因为read是系统调用,所以设计接口的时候,当管道内的消息读完,写端没有传递新的消息过来,read直接将这个进程设置为阻塞状态防止读取空管道信息出现乱码。
这里我们再做个实验:
原本我们读取的是1024个字符,意思就是我们将传递的信息全覆盖了,这次我们每次只读取五个字符看看效果:
可以看见每次读取5个字符的长度的消息,会分多次将管道内的数据读取完。
我们还需要进行一次实验:就是将读端sleep上100秒,然后让写端不sleep一直发消息。
可以看见最后cnt停在了65536,这个数字很熟悉我们用计算器算一下
可以看见是64字节,意思就是管道也是有大小的,不同的系统是不同的大小,但是我们用的ubuntu是64kb。
通过上面的实验可以得出:读端发送消息不是想发多少就发多少,管道式有大小的,读取的时候也不是发多少就读取多少,而是通过需求来读取,发送的信息可以分2个字节的读取,也可以五个字节的读取,也可以一下把发送的消息全部读取了,如果管道是是满的,则写端就不能发消息,写端呈阻塞状态,当管道为空时,读端呈现阻塞状态
匿名管道的五个特性:
面向字节流(Byte Stream Oriented) 指的是数据在传输过程中以 连续字节(byte) 形式进行读写,而不是按照固定的结构或记录划分。字节流的特点是数据没有边界,所有数据都是按照字节序列存储和传输的。
总结
管道(Pipe)作为 Linux 进程间通信(IPC)机制之一,提供了一种简单而高效的字节流通信方式,特别适用于父子进程之间的数据传输。通过匿名管道,进程可以顺序读写数据,但由于其单向通信、基于字节流、生命周期受限于进程等特性,在实际开发中需要合理设计数据格式,避免读取不完整数据或出现阻塞问题。

对于更复杂的 IPC 需求,如跨无亲缘关系进程通信、多进程数据同步、网络通信等,可以考虑使用命名管道(FIFO)、消息队列、共享内存、Socket 等机制。在不同场景下,选择合适的通信方式,才能充分发挥 Linux 进程间通信的优势,提高程序的稳定性和性能。