在进程地址空间那篇博客中提到,进程具有独立性,一个进程的崩溃不会影响其他进程,那怎么该让两个进程之间相互通信呢?
首先我们要有一个共识,进程间想要相互通信的前提是,两个进程要先看到同一份资源,这份资源可以是一个文件或者一块内存(共享内存,下篇博客写),我们试想一下,A进程先打开一份文件,B进程再打开这份文件,A进程往文件中写入后,B文件再向其读取,这样是不是就可以让两个进程看到同一份资源并进行通信了呢?是的,但如果向磁盘中写入后在读取,这样效率就太低了,在OS内核中,每打开一个文件,都会有一个struct file结构体将其组织起来,里面包含了缓冲区,当我们使用write系统调用写入时,实际上是往内核缓冲区里copy,至于什么时候往磁盘中写入,是由操作系统决定的,所以我们如果想要让两个进程看到同一份资源,直接让他们看到文件缓冲区的内容即可,而不必与磁盘进行交互,而这种没有向磁盘中写入,只使用缓冲区的文件,就被称之为管道文件(pipe fiile)
管道又分为匿名管道,和命名管道,我们分别来介绍这两种管道的使用及其特点,并且会在博客结尾附测试代码
一.匿名管道
从名字中我们可知,这是没有名字的,通常用于父子进程通信时使用,创建匿名管道的函数为pipe
int pipe(int pipefd[2]);
pipefd是一个传出型参数,pipe会传出两个文件描述符,pipefd[0]为读端,pipefd[1]为写端,由调用者来决定该进程是读还是写,是的,没错,读写只能选择其一,管道是半双工通信,无法一份管道既读又写
再创建好管道后,再fork子进程,子进程会继承父进程所拥有的文件描述符,我们以父进程向子进程发送信息为例,也就是父进程写,子进程读,再创建好子进程后,父进程应当关闭读端,也就是close(fds[0]),子进程也应当关闭写端,即close(fds[1]),之后父进程调用write向管道文件写入,子进程再read管道文件,即可进行通信,下面是测试代码
#include <iostream>
#include <unistd.h>
#include <cassert>
#include <cstring>
using namespace std;
int main()
{
int fds[2];
int ret = pipe(fds);
assert(ret == 0); // pipe 成功返回0,失败返回-1并设置errno
char buffer[1024];
int id = fork();
if (id == 0)
{
while (1)
{
ssize_t n = read(fds[0], buffer, sizeof(buffer));
buffer[n] = 0;
cout << getpid() << "receive: "<< buffer << endl;
}
}
while (1)
{
snprintf(buffer, sizeof(buffer), "hello world");
write(fds[1], buffer, strlen(buffer));
sleep(1);
}
return 0;
}
执行结果:
这里还有一些细节需要补充,即在读快,写慢会是什么情况,同样,写快读慢,读时写端关闭,写时读端关闭,我就不一个个放测试代码测试了,只需要改变一下上述测试案例中的sleep时间或者读写关闭时间即可,我就直接写答案了
读快写慢:读端会阻塞等待写端写入
写快读慢:写端会将缓冲区写满后等待读端读取部分数据后再进行写入
读时写端关闭: 读端会读到0时返回
写时读端关闭: 写端会被操作系统发送异常信号,杀死该进程 注意:如果父进程被杀死,子进程不会中断,所以测试该案例时可以换成父进程读,子进程写
二.命名管道:
理解匿名管道后再看命名管道就非常简单了,匿名管道主要用于父子进程之间进行通信,如果想让两个毫不相干的进程使用管道文件进行通信的话,那我们就需要一个拥有名字的管道文件,再让两个进程分别使用open系统调用打开该管道文件再进行读写,这里有个细节,在文件系统博客中提到过,目录也是一种文件,里面存放的是文件名和inode的映射关系,而管道文件只存在文件名和inode,不存在任何文件内容,所以管道文件属于内存级文件,下图是管道文件的属性,可以看到管道文件确实不占用任何磁盘空间
命名管道,其实就是多了个名字,让毫不相干的两个进程打开并进行读写,使用的函数为mkfifo
int mkfifo(const char *pathname, mode_t mode);
第一参数为文件路径,默认为当前路径,第二参数为文件权限,成功返回0,失败返回-1并设置errno
下面两个进程模拟client向server发送信息
#include"command.hpp"
//server
int main()
{
//创建管道并打开
createpipe(PIPE_PATH);
int pipefd = open(PIPE_PATH,O_RDONLY);
char buffer[1024];
snprintf(buffer,sizeof(buffer),NULL);
while(true)
{
ssize_t n = read(pipefd,buffer,sizeof(buffer));
assert(n >= 0);
if(n>0)
{
buffer[n-1] = 0;
cout << "client: "<< buffer << endl;
}
else
{
break;
}
}
unlink(PIPE_PATH);
return 0;
}
#include"command.hpp"
//client
int main()
{
int pipefd = open(PIPE_PATH,O_WRONLY);
char buffer[1024];
while(true)
{
cout << "client send# ";
fgets(buffer,sizeof(buffer),stdin);
ssize_t n = write(pipefd,buffer,strlen(buffer));
assert(n >= 0);
}
return 0;
}
#include<iostream>
#include<string>
#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
#include<fcntl.h>
#include<cassert>
#include<cstring>
#define PIPE_PATH "/tmp/named_pipe"
using namespace std;
//command.hpp
bool createpipe(const string& path)
{
umask(0);
int n = mkfifo(path.c_str(),0600);
if(!n) return true;
else
{
cout << "创建管道失败,程序已退出"<<endl;
exit(-1);
}
}
执行结果: