管道
什么是管道
管道是Unix
中最古老的进程间通信的形式。
我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”
|
就是管道 wc
统计有几行
匿名管道
#include <unistd.h>
功能:创建一无名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码
用fork来共享管道原理
站在文件描述符角度-深度理解管道
父进程把文件打开两次(分别以r/w
方式打开)
站在内核角度-管道本质
所以,看待管道,就如同看待文件一样!管道的使用和文件一致,迎合了“Linux
一切皆文件”思想。
如果两个进程没有关系就不能使用以上的原理进行通信!!!!
进程间必须是父子关系、兄弟关系、爷孙关系……!!
具有血缘关系的进程才能用上面原理进行通信。(常用于父子)
以上的工作是建立通信信道,还没有进行通信!
– 为什么建立信道的过程这么费劲?因为进程具有独立性,进程间通信是有成本的!
接口
open
打开的是磁盘级文件
pipe
打开的是内存级文件,以读/写方式把内存级文件打开。
返回值
参数
pipefd -> 是一个只有两个int型元素的数组
pipefd -> 还是一个输出型参数!
分别以读/写方式打开的文件的文件描述符数字带出来,让用户使用!
不考虑其他,默认情况下,是3和4
pipefd[0] : 读下标
pipefd[1] : 写下标
实例代码
创建文件
makefile
testpipe:testpipe.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -rf testpipe
验证 pipefd
的两个参数为3和4:
testpipe.cc
#include <iostream>
#include <unistd.h>
using namespace std;
#define N 2
int main()
{
int pipefd[N]={0};
int n=pipe(pipefd);
if(n<0)return 1;
cout<<"pipefd[0]="<<pipefd[0]<<" , pipefd[1]="<<pipefd[1]<<endl;
return 0;
}
c语言的接口:安全格式化的接口
把按照格式的内容(size
大小的)写到字符串里。
testpipe.cc
#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <string>
#include <cstdio>
#include <string.h>
#include<sys/types.h>
#include<sys/wait.h>
using namespace std;
#define N 2
#define NUM 1024
//child
void Writer(int wfd)
{
string s="hello,I am child";
pid_t id=getpid();
int number=0;
char buffer[NUM];
while(true)
{
buffer[0]=0;//字符串清空,只是为了提醒阅读代码的人,把这个数组当祖字符串了。
snprintf(buffer,strlen(buffer),"%s-%d-%d",s.c_str(),id,number++);
write(wfd,buffer,strlen(buffer));
//不需要+1,管道是文件!c语言的规定与文件无关!只需要把内容写入!
sleep(1);
}
}
//father
void Reader(int rfd)
{
char buffer[NUM];
while(true)
{
buffer[0]=0;
ssize_t n=(rfd,buffer,sizeof(buffer));//sizeof!=strlen
//sizeof代表缓冲区的大小
if(n>0)
{
buffer[n]=0;//把buffer当字符串使用。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[0]<<" , pipefd[1]="<<pipefd[1]<<endl;
// child->w father->r
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;
}
经历了几次拷贝?
写:buffer(用户缓冲区) -> 系统缓冲区
读:系统缓冲区 -> buffer(用户缓冲区)
更新完善后的代码:
#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <string>
#include <cstdio>
#include <string.h>
#include<sys/types.h>
#include<sys/wait.h>
using namespace std;
#define N 2
#define NUM 1024
//child
void Writer(int wfd)
{
string s="hello,I am child";
pid_t id=getpid();
int number=0;
char buffer[NUM];
while(true)
{
sleep(1);
// buffer[0]=0;//字符串清空,只是为了提醒阅读代码的人,把这个数组当祖字符串了。
// snprintf(buffer,sizeof(buffer),"%s-%d-%d",s.c_str(),id,number++);
//资源是系统提供的,所以必须使用系统调用接口,system call
// write(wfd,buffer,strlen(buffer));
char c='a';
write(wfd,&c,1);
//不需要+1,管道是文件!c语言的规定与文件无关!只需要把内容写入!
// sleep(1);
number++;
cout<<number<<endl;
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));//sizeof!=strlen
//sizeof代表缓冲区的大小
if(n>0)
{
buffer[n]=0;//把buffer当字符串使用。0=='\0'
cout<<"father get a message["<<getpid()<<"]# "<<buffer<<endl;
}
else if(n==0)
{
cout<<"father read done!\n"<<endl;
break;
}
else break;
// cout<<"n : "<<n<<endl;
}
}
int main()
{
int pipefd[N]={0};
int n=pipe(pipefd);
if(n<0)return 1;
// cout<<"pipefd[0]="<<pipefd[0]<<" , pipefd[1]="<<pipefd[1]<<endl;
// child->w father->r
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]);
sleep(5);
return 0;
}
管道特点
-
具有亲缘关系的进程之间进行通信
通常,一个管道由一个进程创建,然后该进程调用
fork
,此后父、子进程之间就可应用该管道。 -
管道只能进行单向通信
管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道
-
管道通信需要让不同的进程看到同一份资源,多执行流是共享的,难免会出现访问冲突的问题。
临界资源竞争。父子进程是会进行协同的,内核会对管道操作进行同步与互斥。
– 保护管道文件的数据安全。
- 管道是面向字节流的。管道提供流式服务。
不管写了多少次,是以字节的方式一次就读完。
读端不管格式,读端看来就是一个一个的字节。(由上层区分内容)
- 管道是基于文件的,而文件的生命周期是随进程的!
如果进程退出了,文件会怎么办?
文件会被操作系统自动回收。
两个进程都退出了,管道就会被操作系统回收释放,所以管道的生命周期随进程。
管道的4种情况
- 读写端正常,写很慢,读很快,管道为空,读端就要阻塞。
- 读写端正常,写很快,读很慢,管道为满,写端就要阻塞。
写完之后打印number
read
读之前先sleep(5)
void Writer(int wfd)
{
string s="hello,I am child";
pid_t id=getpid();
int number=0;
char buffer[NUM];
while(true)
{
buffer[0]=0;//字符串清空,只是为了提醒阅读代码的人,把这个数组当祖字符串了。
snprintf(buffer,sizeof(buffer),"%s-%d-%d",s.c_str(),id,number++);
//资源是系统提供的,所以必须使用系统调用接口,system call
write(wfd,buffer,strlen(buffer));
//不需要+1,管道是文件!c语言的规定与文件无关!只需要把内容写入!
// sleep(1);
cout<<number<<endl;
}
}
//father
void Reader(int rfd)
{
char buffer[NUM];
while(true)
{
sleep(5);
buffer[0]=0;
ssize_t n=read(rfd,buffer,sizeof(buffer));//sizeof!=strlen
//sizeof代表缓冲区的大小
if(n>0)
{
buffer[n]=0;//把buffer当字符串使用。0=='\0'
cout<<"father get a message["<<getpid()<<"]# "<<buffer<<endl;
}
}
}
很明显,管道是有大小的!
-
读端正常,写端关闭,读端就会读到0,表明读到了文件(
pipe
)结尾,不会被阻塞。//child void Writer(int wfd) { string s="hello,I am child"; pid_t id=getpid(); int number=0; char buffer[NUM]; while(true) { char c='a'; write(wfd,&c,1); number++; cout<<number<<endl; if(number>=5) { break; } } } //father void Reader(int rfd) { char buffer[NUM]; while(true) { sleep(1); buffer[0]=0; ssize_t n=read(rfd,buffer,sizeof(buffer));//sizeof!=strlen //sizeof代表缓冲区的大小 if(n>0) { buffer[n]=0;//把buffer当字符串使用。0=='\0' cout<<"father get a message["<<getpid()<<"]# "<<buffer<<endl; } } }
-
写端正常,读端关闭。操作系统就要杀掉正在写入的进程。通过13号信号杀掉。
操作系统是不会做低效、浪费等类似的工作的。如果做了就是系统bug
。
验证:
#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <string>
#include <cstdio>
#include <string.h>
#include<sys/types.h>
#include<sys/wait.h>
using namespace std;
#define N 2
#define NUM 1024
//child
void Writer(int wfd)
{
string s="hello,I am child";
pid_t id=getpid();
int number=0;
char buffer[NUM];
while(true)
{
sleep(1);
buffer[0]=0;//字符串清空,只是为了提醒阅读代码的人,把这个数组当祖字符串了。
snprintf(buffer,sizeof(buffer),"%s-%d-%d",s.c_str(),id,number++);
//资源是系统提供的,所以必须使用系统调用接口,system call
write(wfd,buffer,strlen(buffer));
// char c='a';
// write(wfd,&c,1);
//不需要+1,管道是文件!c语言的规定与文件无关!只需要把内容写入!
// sleep(1);
// number++;
// cout<<number<<endl;
// 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));//sizeof!=strlen
//sizeof代表缓冲区的大小
if(n>0)
{
buffer[n]=0;//把buffer当字符串使用。0=='\0'
cout<<"father get a message["<<getpid()<<"]# "<<buffer<<endl;
}
else if(n==0)
{
cout<<"father read done!\n"<<endl;
break;
}
else break;
// 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[0]<<" , pipefd[1]="<<pipefd[1]<<endl;
// child->w father->r
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]);
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<<"father quit"<<endl;
return 0;
}
管道读写规则
管道大小
在不同内核里,管道大小有差别。
//child
void Writer(int wfd)
{
string s="hello,I am child";
pid_t id=getpid();
int number=0;
char buffer[NUM];
while(true)
{
char c='a';
write(wfd,&c,1);
number++;
cout<<number<<endl;
}
}
//father
void Reader(int rfd)
{
char buffer[NUM];
while(true)
{
sleep(50);
buffer[0]=0;
ssize_t n=read(rfd,buffer,sizeof(buffer));//sizeof!=strlen
//sizeof代表缓冲区的大小
if(n>0)
{
buffer[n]=0;//把buffer当字符串使用。0=='\0'
cout<<"father get a message["<<getpid()<<"]# "<<buffer<<endl;
}
}
}
由此可知管道的大小是65536字节 也就是64KB
PIPE_BUF
单次向管道写入的大小。
当没有数据可读时
O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
当管道满的时候
O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
O_NONBLOCK enable:调用返回-1,errno值为EAGAIN
如果所有管道写端对应的文件描述符被关闭,则read
返回0
如果所有管道读端对应的文件描述符被关闭,则write
操作会产生信号SIGPIPE
,进而可能导致write
进程退出
当要写入的数据量不大于PIPE_BUF
时,linux
将保证写入的原子性。
原子性:要读就把规定的数据全部读走,要么就不读。
当要写入的数据量大于PIPE_BUF
时,linux
将不再保证写入的原子性。
这里的pipe size
就是 PIPE_BUF
(4KB)
应用场景
管道与我们之前学的知识哪些是有关系的呢?
1.cat test.txt | head -10 | tail -5
这批 sleep
进程具有血缘关系!创建两个管道,三个进程,然后输入输出重定向。
命令行 |
底层就是pipe。
以上就是匿名管道。
-
自定义shell – 我们想让我们的shell支持
|
管道,代码该如何写。1.分析输入的命令字符串,获取有几个|,分割出多个子命令字符串 2.malloc申请空间,pipe先申请多个管道 3.循环创建多个子进程,分析每一个子进程的重定向情况。 a.首个:就是输出重定向,1->指定的管道写端 b.中间:输入输出重定向,0标准输入重定向到上一个管道的读端 1标准输出重定向到下一个管道的写端 c.末尾:输入重定向,0标准输入重定向到最后一个管道的读端。 4.分别让不同的子进程执行不同的命令 -- exec* -- exec* 不会影响该进程曾经打开的文件,不会影响预先设置好的管道重定向。