目录
一、进程间通信
1.1 概念
进程间通信(Inter-Process Communication,IPC)是指在不同的进程之间共享信息或数据,实现数据层面的交互。
我们知道,进程具有独立性,默认情况下两个进程是无法进行信息交流的,因此我们需要某些技术手段来让不同进程之间能够共享信息
进程间通信的本质,就是让两个进程能够看到同一份“资源”,这份资源一般由操作系统提供,因此进程访问这份资源进行通信,本质上就是在访问操作系统,所以要实现进程间通信,我们需要调用对应的系统调用接口。
操作系统中的两大IPC模块定制标准:System V和Posix,为我们提供了一系列进程间通信方法
1.2 手段
常见的进程间通信手段主要有:
- 匿名管道(Pipe):适用于父子进程间通信,只能单向传输数据,没有同步机制
- 命名管道(Named Pipe):可用于任意两个进程间的通信
- 消息队列(Message Queue):允许进程将消息发送到队列中,其他进程可以从队列中读取这些消息
- 信号(Signal):最古老的进程间通信的方法之一,用于通知进程发生了某个事件
- 共享内存(Shared Memory)在内存中创建一个共享区域,并让多个进程能够访问该区域
- 套接字(Socket):适用于需要进行网络通信的进程
- 信号量(Semaphore):主要用于多进程、多线程之间的同步互斥问题
二、管道
2.1 概念
管道是Linux中的一种进程间通信方式
我们可以把管道想象成连接两个进程间的一条管子,一个进程的数据流就能通过这个管子流向另一个进程。
管道分为匿名管道pipe和命名管道FIFO两种,通常我们所说的管道指匿名管道,二者除了创建、使用等方式不同,原理是相同的,都是通过内核的一块缓冲区实现数据传输
2.2 匿名管道
匿名管道(pipe)是一个临时创建的对象
站在文件描述符角度来看,我们可以把两个进程的文件描述符分别指向管道的读端和写端,一个进程向管道中写,另一个进程从管道中读,就实现了进程间通信。
站在内核角度来看,管道的本质就是两个file结构体(一个用于写一个用于读)、一个临时创建的inode节点加上一个内存的物理页。进程向管道中写入时,数据被写入到了这个共享数据页中;进程从管道中读取时,数据又从这个页中被拷贝出来
理解了管道的本质,我们就理解了为什么管道只能进行单向通信
创建匿名管道的接口:
#include <unistd.h>
int pipe(int fd[2]);
该函数的参数是一个输出型参数,我们需要向函数内传入一个大小为2,元素类型为int的数组。匿名管道创建完毕后,传入的数组内部会存放读端和写端的文件描述符,其中fd[0]为读端,fd[1]为写端
创建匿名管道成功,函数会返回0,创建失败返回-1并设置errno
我们可以用一段简单的代码来验证一下:
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
int fd[2] = {0};
int n = pipe(fd);
if(n < 0)
return 1;
cout << "fd[0]=" << fd[0] << " fd[1]=" << fd[1] << endl;
return 0;
}
运行结果:
我们知道,进程的前三个文件描述符分别被标准输入流、标准输出流和标准错误流占用,所以创建匿名管道时文件描述符只能从3开始,符合预期
但匿名管道又是如何实现父子进程间通信的呢?
通过fork创建子进程后,子进程会继承父进程的各种信息,其中就包括文件描述符表,因此父进程如果在创建子进程时就已经创建了匿名管道,后续子进程的文件描述符也会与该管道对应
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
int fd[2] = {0};
int n = pipe(fd); //父进程建立匿名管道
if(n < 0)
return 1;
pid_t id = fork(); //创建子进程
if(id < 0)
return 2;
return 0;
}
父进程向子进程通信的情况,再将父进程指向管道读端的文件描述符关闭,子进程指向管道写端的文件描述符关闭。如果要实现子进程向父进程通信,则反过来即可
#include <iostream>
#include <cstdlib>
#include <unistd.h>
using namespace std;
int main()
{
int fd[2] = {0};
int n = pipe(fd); //父进程建立匿名管道
if(n < 0)
return 1;
pid_t id = fork(); //创建子进程
if(id < 0)
return 2;
if(id == 0) //子进程
{
close(fd[1]); //关闭写端
//开始通信
//...
close(fd[0]); //通信完毕
exit(0);
}
//父进程
close(fd[0]); //关闭读端
//开始通信
//...
close(fd[1]); //通信完毕
return 0;
}
所以,通过匿名管道实现进程间通信的前提,是两个进程间有血缘关系(文件描述符可被继承)
匿名管道不需要将数据拷贝到磁盘中,属于内存级文件,没有路径、文件名和inode,因此而得名
以上就是使用匿名管道实现进程间通信的前置操作——建立通信信道,接下来我们才开始真正的进程间通信
文件描述符也有了,我们只需要让父子进程一个向管道写入内容,一个从管道读取内容,就可以完成通信了。这里用一段简单的代码来验证:
#include <iostream>
#include <string>
#include <cstring>
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
using namespace std;
void Writer(int wfd)
{
//随便准备一些用于通信的内容
string s = "hello, I am father";
pid_t self = getpid();
char buffer[1024];
while(true)
{
buffer[0] = 0; //清空字符串
snprintf(buffer, sizeof(buffer), "%s, pid:%d", s.c_str(), self); //将内容格式化输入到目标字符串中
write(wfd, buffer, strlen(buffer)); //将字符串写入管道
sleep(1);
}
}
void Reader(int rfd)
{
char buffer[1024];
while(true)
{
buffer[0] = 0;
ssize_t n = read(rfd, buffer, sizeof(buffer)); //从管道读取内容
if(n > 0)
{
buffer[n] = 0; //这里的0相当于'\0'
cout << "child get a massage[" << getpid() << "]#" << buffer << endl; //打印读取到的内容