
进程间通信之匿名管道
一、基本概念
我们知道多个进程之间是互相独立的,但是有时候我们需要将一个进程的数据传递到另一个进程,实现数据传输 的效果,有的时候多个进程之间要共享 同样的资源 ,有的时候一个进程要对其他进程发送消息,实现通知事件 ,还有的时候一个进程要完全控制另一个进程的执行,实现进程控制
因为进程间相互独立,所以进程通信是有较高成本的
进程间通信的本质就是让不同的进程看到同一份资源,这份资源一定是由操作系统提供的第三方空间,不能是某个进程的,因为这样会破坏进程独立性,我们进程访问第三方空间本质上就是访问操作系统
一般操作系统会有一个独立的通信模块,隶属于文件系统,它被制定者制定了两个标准system V 和 posix ,其中system V 是本机内部进程间的通信,分为消息队列、共享内存、信号量,posix 是网络进程通信,分为消息队列、共享内存、信号量、互斥量、条件变量、读写锁
在进程间通信的规则指定之前,还没有system V 和 posix 的时候,我们是通过管道进行进程间通信的,这是一种基于文件的通信方式
二、管道
1、温故知新
我们在之前的学习命令行的过程中学习过管道,那里的管道与这里的管道是一致的,本质上就是一个管子,在两头位置处有两种处理方式,在进入管道前处理一次,在管道中的内容就是已经被处理过一次的内容,然后离开管道后再处理一次,得出的结果就是一个数据被前面的命令处理一次的结果被后面的命令处理
当时学习的时候只浮于表面,实际上管道就是起到一个传递数据流的作用 ,两边为两个进程,进程A发出的信息可以通过管道到达进程B,管道本身没有处理数据的功能,只有传递数据的功能
2、实现方式
我们说管道是一个基于文件的通信方式,我们来看一下我们文件管理的内容

进程中的PCB中有一个struct files_struct
指针,指向结构体files_struct
,files_struct
结构体中存在一个文件描述符指针数组,指向对应的struct file
对象,每个struct file
都有inode
描述文件属性,file_operators
定义操作文件的函数接口,文件缓冲区缓冲文件,硬盘当中的文件如果要加载到内存中需要先加载到文件缓冲区,如果我们的管道文件在硬盘上,那么IO的速度将非常慢,不利于我们进行进程间的快速通信,那什么地方既速度快又能存放文件呢?答案就是内存

我们把写入或者读取硬盘的IO操作去掉,将管道文件保存在缓冲区,其他进程再通过文件描述符读取缓冲区的内容,就可以实现进程间的管道通信 ,这里的管道文件就是匿名管道
管道文件的存放问题我们解决了,下一个问题就是其他进程怎么通过文件描述符读取缓冲区的内容
我们知道子进程被父进程创建后,如果不做修改,相当于是浅拷贝,父进程的PCB复制一份,files_struct
也复制一份,那么它们就同时指向已经同一个struct file
,如果父进程fd==3
以读方式打开管道文件,fd==4
以写方式打开管道文件,那么子进程也一样,然后父进程close(3)
子进程close(4)
实现父写子读 ,父进程close(4)
子进程close(3)
实现父读子写
因为一个文件是没法进行读写交替一起的,所以匿名管道其实是一种半双工的通信方式,即单向通信,当然我们可以通过建立多个匿名管道来实现双向通信
管道通信常用于父子进程通信,可用于兄弟进程、爷孙进程等有"血缘"的进程进行通信
3、匿名管道
c
#include <unistd.h>
int pipe(int pipefd[2]);
//pipefd:文件描述符数组,其中pipefd[0]表示读端,pipefd[1]表示写端,值为对应的文件描述符
//返回值:成功返回0,失败返回错误代码
在pipe函数中,int fd[2]
是一个输出型参数
我们来实现一个父读子写这样一个管道通信
c
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string>
#include <cstring>
#include <cstdlib>
#define N 2
#define NUM 1024
using namespace std;
void Writer(int wfd)
{
//定义要发送的字符串
string s = "this is your child";
//获取当前进程的pid
pid_t myid = getpid();
int number = 0;
char buffer[NUM];
while(1)
{
//此处相当于buffer[0] = '\0';意思是将整个数组当做字符串用并且清空字符串
buffer[0] = 0;
//将字符串、pid、以及计数器number按照"%s-%d-%d"格式写到buffer当中
snprintf(buffer,sizeof(buffer),"%s-%d-%d",s.c_str(),myid,number++);
//这里传过来的wfd为对应的文件描述符,然后将buffer中的有效内容写到管道文件缓冲区中
write(wfd,buffer,strlen(buffer));
sleep(5);
}
}
void Reader(int rfd)
{
char buffer[NUM];
while(1)
{
//同上
buffer[0] = 0;
//将文件描述符rfd读取的内容存储到buffer中,并返回读取到的字符个数n
ssize_t n = read(rfd,buffer,sizeof(buffer));
//如果有内容则打印出来
if(n > 0)
{
buffer[n] = 0;
cout << "parent get a message[" << getpid() << "]# " << buffer << endl;
}
//没有内容即读取完成
else if(n == 0)
{
printf("parent read file done!\n");
break;
}
//其他情况就是有bug了
else break;
}
}
int main()
{
//pipefd用来存放输出型参数
int pipefd[N] = {0};
//成功验证
int n = pipe(pipefd);
if(n < 0)
{
return 1;
}
//创建子进程
pid_t id = fork();
//错误情况
if(id < 0)
{
return 2;
}
//子进程执行段,把读写函数打包一下,写到一个函数里,立体分明
else if(id == 0)
{
//child
//子进程要写不读,关掉pipefd[0],写pipefd[1],写完再关掉pipefd[1],然后退出
close(pipefd[0]);
Writer(pipefd[1]);
close(pipefd[1]);
exit(0);
}
//父进程执行段
else{
//parent
//父进程要读不写,关掉pipefd[1],读pipefd[0],等待子进程结束再关掉pipefd[0]
close(pipefd[1]);
Reader(pipefd[0]);
pid_t rid = waitpid(id,NULL,0);
if(rid < 0)
{
return 3;
}
close(pipefd[0]);
}
return 0;
}

这里父进程只在子进程写入的时候才读取,没有出现子进程写一半父进程就读取的情况,所以父子进程直接是会进行协同的,有同步和互斥性
(一)管道中的四种情况
对管道中可能出现的四种情况做说明:
读写端正常,如果管道为空,读端就要被阻塞(上面印证)
读写端正常,如果管道被写满,写端就要被阻塞(在管道特性这里印证)
读端正常,写端关闭,读端可以读到0,表明读到了文件结尾,不堵塞
写端正常,读端关闭,操作系统会杀死正在写入的进程,用信号SIGPIPE
,也就是kill -13
注释掉main函数中子进程中的Writer函数,它会读到文件结尾并打印done信息
写端一秒写入一次,读端一秒读一次,读端读5秒后退出读模式,关闭读端,然后静待5秒,等待子进程结束,然后打印它的退出码和收到的信号
c
int main()
{
//......
if(id < 0)
{
return 2;
}
else if(id == 0)
{
//child
close(pipefd[0]);
Writer(pipefd[1]);
close(pipefd[1]);
exit(0);
}
else{
//parent
close(pipefd[1]);
Reader(pipefd[0]);
close(pipefd[0]);
cout << "father close read fd: " << pipefd[0] << endl;
sleep(5);
int status = 0;
pid_t rid = waitpid(id,&status,0);
if(rid < 0)
{
return 3;
}
cout << "wait child success: " << rid << " exit code: " << ((status>>8)&0xFF) << " exit signal: " << (status&0x7F) << endl;
sleep(5);
cout << "parent quit" << endl;
}
return 0;
}

(二)管道的特性
c
//子进程一直写
void Writer(int wfd)
{
string s = "this is your child";
pid_t myid = getpid();
int number = 0;
char buffer[NUM];
while(1)
{
//buffer[0] = '\0';
buffer[0] = 0;
snprintf(buffer,sizeof(buffer),"%s-%d-%d",s.c_str(),myid,number++);
write(wfd,buffer,strlen(buffer));
}
}
//父进程5秒读一次数据
void Reader(int rfd)
{
char buffer[NUM];
while(1)
{
sleep(5);
buffer[0] = 0;
ssize_t n = read(rfd,buffer,sizeof(buffer));
if(n > 0)
{
buffer[n] = 0;
cout << "parent get a message[" << getpid() << "]# " << buffer << endl;
}
else if(n == 0)
{
printf("parent read file done!\n");
break;
}
else break;
}
}

我们发现它的读取是杂乱无章的,说明管道是面向字节流的,这里与前面并不矛盾,有人说这里不是没写完就读取吗,你看这个句子一段一段的,其实这里是缓冲区写满了,写不下了,写入端堵塞导致的,在读取端读取之后写入端才继续写入,正好也印证了上面的说法
今日分享就到这里了~
