管道是Unix中最古老的进程间通信的形式。它是进程之间的一个单向数据流 : 一个进程写入管道的所有数据都由内核定向到另一个进程,另一个进程由此就可以从管道中读取数据。 管道示意图如下:
其实管道的概念非常好理解, 举个例子, 相信大家小时候身边都或多或少有过订早餐奶的经历, 送奶员工每天早上定时将当天新鲜的鲜奶放进家门口的奶箱, 然后大家睡醒就可以在奶箱里拿到新鲜的牛奶。其中奶箱就相当于"管道", 送奶工放入牛奶就相当于给管道里写入数据,而客户取出牛奶就相当于读取数据。
首先,学过Linux命令的话,大家对于管道肯定不陌生, Linux管道使用竖线 | 连接多个命令,这个被称为管道符。如:
$ ls | head -5
以上这行代码就组成了一个管道,它的功能是将前一个命令(ls)的输出,作为后一个命令(head -5)的输入:(原本ls指令会打印当前目录所有文件/目录名,但是通过管道和head命令后就变为只打印前5行文件/目录名了)
从这个功能描述中,我们可以看出管道中的数据只能单向流动,也就是半双工通信,如果想实现相互通信(全双工通信),我们需要创建两个管道才行。
另外,通过管道符 | 创建的管道是匿名管道,用完了就会被自动销毁。需要注意的是,匿名管道只能在具有亲缘关系(父子进程,兄弟进程,爷孙进程)的进程间使用。也就是说,匿名管道只能用于亲缘进程之间的通信。
要构建一个父子进程共享的单项管道文件,我们就先在父进程中以读和写两种方式打开同一个管道文件:
然后我们再创建子进程, 这时子进程就会拷贝父进程的文件控制结构体以及里面的文件指针数据,即它们会指向相同的文件:
然后我们分别关闭父进程对管道文件的读方式,以及子进程对管道文件的写方式,这时,管道文件就成为了一个由父进程写入数据,子进程读取数据的通信管道文件:
当然也可以关闭父进程对管道文件的写方式,以及子进程对管道文件的读方式,这样管道文件就成为了一个由子进程写入数据,父进程读取数据的通信管道文件。
因为管道是内存级文件,并非磁盘级文件,所以当我们想创建一个管道时,不能使用open()函数来打开文件,而是要使用pipe()函数,下面是pipe()函数的手册:
函数定义:
int pipe(int pipefd[2]);
函数参数:
int pipefd[2]
这个参数是一个输出型参数,作用是把我们分别以读方式和写方式打开的文件的文件描述符数字带出来让用户使用。其中, pipefd[0]为读下标, pipefd[1]为写下标。
函数返回值:
int
当函数打开管道文件成功后, 返回0; 出错时, 则返回-1。
管道的实现思路如下:
综上,实现代码如下:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#define N 2
#define NUM 1024
using namespace std;
//child
void Writer(int wfd)
{
string s="hello,I am child";
pid_t self = getpid();
int number = 0;
char buffer[NUM];
while(true)
{
//构建发送字符串
buffer[0] = 0; //字符串清空,仅作提醒,把这个数组当字符串
//字符串安全格式接口
snprintf(buffer,sizeof(buffer),"%s-%d-%d",s.c_str(),self,number++);
//把后面的几个参数按照引号内的格式拼在第一个参数的数组里,长度为第二个参数
//cout << buffer << endl;
//发送/写入给父进程
write(wfd,buffer,strlen(buffer));
sleep(1);
}
}
//father
void Reader(int rfd)
{
char buffer[NUM];
while(true)
{
buffer[0] = 0;
ssize_t n = read(rfd,buffer,sizeof(buffer));
if(n > 0)
{
buffer[n] = 0;//0 =='\0'
cout<<"father get a message["<<getpid()<<"]#"<<buffer<<endl;
}
}
}
int main()
{
//创建管道
int pipefd[N] = {0};
int n = pipe(pipefd);
if(n < 0) return 1;
// cout<<pipefd[0]<<":"<<pipefd[1]<<endl;
//创建子进程
//建立单向信道 child写father读
pid_t id = fork();
if(id < 0) return 2;
if(id == 0)
{
//child写关闭读
close(pipefd[0]);
//IPC code
Writer(pipefd[1]);
//用完把写端也关掉
close(pipefd[1]);
exit(0);
}
//father读关闭写
close(pipefd[1]);
//IPC code
Reader(pipefd[0]);
pid_t rid = waitpid(id,nullptr,0);
if(rid < 0) return 3;
//用完把读端也关掉
close(pipefd[0]);
return 0;
}
运行程序,可以发现父进程可以接收到子进程传来的消息了:
借助我们上面的程序进行验证: 我们让子进程每隔1秒进行写入,而父进程一直在做读操作,运行程序,发现父进程没有疯狂打印消息,而是也每隔一秒打印一次,这说明当管道中没有数据可读时,读取端(read)是会调用阻塞,暂停进程执行,直到有数据来为止:
借助程序进行验证:
我们让子进程一直进行写入, 而父进程每隔5秒做读操作, 运行程序, 发现父进程每隔5秒会打印一大批消息, 而且这些消息是连续的, 并没有断层, 这说明当管道中数据满了的时候, 写入端(write)是会调用阻塞,暂停进程执行,直到所有数据被读取走后才会继续写入:
验证代码如下:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#define N 2
#define NUM 1024
using namespace std;
//child
void Writer(int wfd)
{
string s="hello,I am child";
pid_t self = getpid();
int number = 0;
char buffer[NUM];
while(true)
{
//构建发送字符串
buffer[0] = 0; //字符串清空,仅作提醒,把这个数组当字符串
//字符串安全格式接口
snprintf(buffer,sizeof(buffer),"%s-%d-%d",s.c_str(),self,number++);
//把后面的几个参数按照引号内的格式拼在第一个参数的数组里,长度为第二个参数
//cout << buffer << endl;
//发送/写入给父进程
write(wfd,buffer,strlen(buffer));
//sleep(1);
}
}
//father
void Reader(int rfd)
{
char buffer[NUM];
while(true)
{
buffer[0] = 0;
ssize_t n = read(rfd,buffer,sizeof(buffer));
if(n > 0)
{
buffer[n] = 0;//0 =='\0'
cout<<"father get a message["<<getpid()<<"]#"<<buffer<<endl;
}
sleep(5);
}
}
int main()
{
//创建管道
int pipefd[N] = {0};
int n = pipe(pipefd);
if(n < 0) return 1;
// cout<<pipefd[0]<<":"<<pipefd[1]<<endl;
//创建子进程
//建立单向信道 child写father读
pid_t id = fork();
if(id < 0) return 2;
if(id == 0)
{
//child写关闭读
close(pipefd[0]);
//IPC code
Writer(pipefd[1]);
//用完把写端也关掉
close(pipefd[1]);
exit(0);
}
//father读关闭写
close(pipefd[1]);
//IPC code
Reader(pipefd[0]);
pid_t rid = waitpid(id,nullptr,0);
if(rid < 0) return 3;
//用完把读端也关掉
close(pipefd[0]);
return 0;
}
我们验证一下,让子进程每一秒写一个字符,写5个后就退出,看看运行结果:
结果是当子进程写端被关闭后,父进程的read()就会一直疯狂打印read()的返回值0,表明读到了文件(pipe)结尾,不会被阻塞.
验证代码如下:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#define N 2
#define NUM 1024
using namespace std;
//child
void Writer(int wfd)
{
string s="hello,I am child";
pid_t self = getpid();
int number = 0;
char buffer[NUM];
while(true)
{
sleep(1);
//构建发送字符串
//buffer[0] = 0; //字符串清空,仅作提醒,把这个数组当字符串
//字符串安全格式接口
//snprintf(buffer,sizeof(buffer),"%s-%d-%d",s.c_str(),self,number++);
//把后面的几个参数按照引号内的格式拼在第一个参数的数组里,长度为第二个参数
//cout << buffer << endl;
//发送/写入给父进程
//write(wfd,buffer,strlen(buffer));
//每次只写一字节,写完后计数
char c = 'c';
write(wfd,&c,1);
number++;
cout<<number<<endl;
//sleep(1);
//写端写5个字符就退出
if(number >= 5) break;
}
}
//father
void Reader(int rfd)
{
char buffer[NUM];
while(true)
{
buffer[0] = 0;
ssize_t n = read(rfd,buffer,sizeof(buffer));
if(n > 0)
{
buffer[n] = 0;//0 =='\0'
cout<<"father get a message["<<getpid()<<"]#"<<buffer<<endl;
}
// else if(n == 0)
// {
// cout<<"father read file done!\n"<<endl;
// break; //说明读到了文件末尾,可以返回了
// }
// else break;
// sleep(5);
//cout<<"n:"<<n<<endl;
}
}
int main()
{
//创建管道
int pipefd[N] = {0};
int n = pipe(pipefd);
if(n < 0) return 1;
// cout<<pipefd[0]<<":"<<pipefd[1]<<endl;
//创建子进程
//建立单向信道 child写father读
pid_t id = fork();
if(id < 0) return 2;
if(id == 0)
{
//child写关闭读
close(pipefd[0]);
//IPC code
Writer(pipefd[1]);
//用完把写端也关掉
close(pipefd[1]);
exit(0);
}
//father读关闭写
close(pipefd[1]);
//IPC code
Reader(pipefd[0]);
pid_t rid = waitpid(id,nullptr,0);
if(rid < 0) return 3;
//用完把读端也关掉
close(pipefd[0]);
return 0;
}
我们借助程序验证一下:
验证代码如下:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#define N 2
#define NUM 1024
using namespace std;
//child
void Writer(int wfd)
{
string s="hello,I am child";
pid_t self = getpid();
int number = 0;
char buffer[NUM];
while(true)
{
sleep(1);
//构建发送字符串
buffer[0] = 0; //字符串清空,仅作提醒,把这个数组当字符串
//字符串安全格式接口
snprintf(buffer,sizeof(buffer),"%s-%d-%d",s.c_str(),self,number++);
//把后面的几个参数按照引号内的格式拼在第一个参数的数组里,长度为第二个参数
//cout << buffer << endl;
//发送/写入给父进程
write(wfd,buffer,strlen(buffer));
//每次只写一字节,写完后计数
// char c = 'c';
// write(wfd,&c,1);
// number++;
// cout<<number<<endl;
sleep(1);
//写端写5个字符就退出
//if(number >= 5) break;
}
}
//father
void Reader(int rfd)
{
char buffer[NUM];
int cnt = 0;
while(true)
{
buffer[0] = 0;
ssize_t n = read(rfd,buffer,sizeof(buffer));
if(n > 0)
{
buffer[n] = 0;//0 =='\0'
cout<<"father get a message["<<getpid()<<"]#"<<buffer<<endl;
}
else if(n == 0)
{
cout<<"father read file done!\n"<<endl;
break; //说明读到了文件末尾,可以返回了
}
else break;
// sleep(5);
//cout<<"n:"<<n<<endl;
cnt++;
if(cnt > 5)
{
break;
}
}
}
int main()
{
//创建管道
int pipefd[N] = {0};
int n = pipe(pipefd);
if(n < 0) return 1;
// cout<<pipefd[0]<<":"<<pipefd[1]<<endl;
//创建子进程
//建立单向信道 child写father读
pid_t id = fork();
if(id < 0) return 2;
if(id == 0)
{
//child写关闭读
close(pipefd[0]);
//IPC code
Writer(pipefd[1]);
//用完把写端也关掉
close(pipefd[1]);
exit(0);
}
//father读关闭写
close(pipefd[1]);
//IPC code
Reader(pipefd[0]); //读取5s
//用完把读端也关掉
close(pipefd[0]);
cout<<"father close read fd:"<<pipefd[0]<<endl;
sleep(3);
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;
//用完把读端也关掉
close(pipefd[0]);
return 0;
}
tips:
管道的特点:
tips:
管道是有固定大小的,在不同内核里,大小可能有差别,我们调用ulimit指令查询得到的结果如下:
查询pipe的手册,我们可以知道在不同的内核版本下,管道的大小也不相同:
再使用程序验证时,我们会发现子进程会给父进程写入65536个字节的数据,即64KB数据,这说明我们当前环境下管道的大小是64KB:
验证代码如下:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#define N 2
#define NUM 1024
using namespace std;
//child
void Writer(int wfd)
{
string s="hello,I am child";
pid_t self = getpid();
int number = 0;
char buffer[NUM];
while(true)
{
//构建发送字符串
//buffer[0] = 0; //字符串清空,仅作提醒,把这个数组当字符串
//字符串安全格式接口
//snprintf(buffer,sizeof(buffer),"%s-%d-%d",s.c_str(),self,number++);
//把后面的几个参数按照引号内的格式拼在第一个参数的数组里,长度为第二个参数
//cout << buffer << endl;
//发送/写入给父进程
//write(wfd,buffer,strlen(buffer));
//每次只写一字节,写完后计数
char c = 'c';
write(wfd,&c,1);
number++;
cout<<number<<endl;
//sleep(1);
}
}
//father
void Reader(int rfd)
{
char buffer[NUM];
while(true)
{
sleep(50);
buffer[0] = 0;
ssize_t n = read(rfd,buffer,sizeof(buffer));
if(n > 0)
{
buffer[n] = 0;//0 =='\0'
cout<<"father get a message["<<getpid()<<"]#"<<buffer<<endl;
}
// sleep(5);
}
}
int main()
{
//创建管道
int pipefd[N] = {0};
int n = pipe(pipefd);
if(n < 0) return 1;
// cout<<pipefd[0]<<":"<<pipefd[1]<<endl;
//创建子进程
//建立单向信道 child写father读
pid_t id = fork();
if(id < 0) return 2;
if(id == 0)
{
//child写关闭读
close(pipefd[0]);
//IPC code
Writer(pipefd[1]);
//用完把写端也关掉
close(pipefd[1]);
exit(0);
}
//father读关闭写
close(pipefd[1]);
//IPC code
Reader(pipefd[0]);
pid_t rid = waitpid(id,nullptr,0);
if(rid < 0) return 3;
//用完把读端也关掉
close(pipefd[0]);
return 0;
}
希望这篇关于 进程通信之管道 的博客能对大家有所帮助,欢迎大佬们留言或私信与我交流.
学海漫浩浩,我亦苦作舟!关注我,大家一起学习,一起进步!