Linux——匿名管道
- 什么是管道
- 匿名管道的底层原理
- 观察匿名管道现象
- 读写端的几种情况
- 写端慢,读端快
- 写端快,读端慢
- 管道的大小
- 写端关闭,读端一直读
- 写端一直写,读端关闭
我们之前一直用的是vim来编写代码,现在有了vscode这样强大的编辑器,我们可以把我们的vim放一边了,如果还有小伙伴还没有配置好vscode的远端,可以点击这里:
https://blog.csdn.net/qq_67693066/article/details/136368891
我们今天进入管道的学习:
什么是管道
在计算机领域,管道(Pipeline)是一种将多个命令连接在一起以形成数据流的机制。它允许一个命令的输出成为另一个命令的输入,从而实现命令之间的数据传递和处理。
在 Unix/Linux 系统中,管道通常用竖线符号 | 表示。通过管道,可以将一个命令的输出传递给另一个命令进行处理,从而构建复杂的数据处理流程。
例如,假设我们有两个命令 command1 和 command2,我们可以使用管道将它们连接起来:
command1 | command2
这将会把 command1 的输出作为 command2 的输入,command2 将处理 command1 的输出并生成最终的结果。
管道的优势包括:
简化复杂任务: 管道可以将多个简单的命令组合成一个复杂的任务,使得任务的实现更加简单和高效。
模块化和可重用性: 通过将命令连接在一起,可以更好地组织代码并提高代码的可重用性。每个命令都可以专注于完成一个特定的任务。
减少临时文件: 管道可以避免将数据存储到临时文件中,从而减少了文件 I/O 的开销和磁盘空间的占用。
实时处理: 管道允许命令之间的实时数据传递,这对于需要连续处理数据的任务非常有用,比如日志处理、数据流分析等。
简单来说,管道就是连接多个指令。我们之前也在频繁使用管道:比如我们想统计当前登录到系统的用户数量。
who指令的结果作为wc -l的输入。
匿名管道的底层原理
我们这里讲的简单一点,现在我们有一个进程,它自身会被以读和写的方式分别打开一次:
然后这个读和写都会往一个缓冲区输入输出数据:
这个时候父进程创建子进程,子进程发生浅拷贝,指向没有发生变化:
这里注意一下,管道一般是单向的,所以我们现在想让父进程读,让子进程写:
这样形成了一个单向通道,这个就是一个基本的匿名管道。
匿名管道(Anonymous Pipe)是一种用于进程间通信的机制,特别是在 Unix 和类 Unix 系统中。它允许一个进程将输出发送到另一个进程的输入,从而实现进程间的数据传输。
以下是匿名管道的一些关键特点:
单向通信:匿名管道是单向的,只能支持单向数据流。它只能用于单一方向的通信,通常是父进程到子进程或者相反。
创建:匿名管道通过调用系统调用 pipe() 来创建。这个系统调用创建了一个管道,返回两个文件描述符,其中一个用于读取管道,另一个用于写入管道。
父子进程通信:通常,匿名管道用于父子进程之间的通信。在创建子进程后,父进程可以将数据写入管道,而子进程则可以从管道中读取这些数据。
半双工:匿名管道是半双工的,意味着数据只能在一个方向上流动。如果需要双向通信,则需要创建两个管道,或者使用其他的进程间通信机制,比如命名管道或套接字。
进程同步:匿名管道通常用于进程间的同步和协作。一个进程可能会阻塞在读取管道上,直到另一个进程写入数据到管道中为止。
匿名管道在 Unix 系统中被广泛应用,特别是在 shell 编程和进程间通信方面。它提供了一种简单而有效的方式,允许不同进程之间进行数据交换和协作
我也有专门创建管道的函数pipe:
我们可以来试一下:
#include<iostream>
#include<unistd.h>
#include<cassert>
using namespace std;int main()
{int pipefd[2] = {0};int n = pipe(pipefd);assert(n == 0);cout<<"pipefd[0]"<<"--->"<<pipefd[0]<<"pipefd[1]"<<"--->"<<pipefd[1]<<endl;return 0;
}
运行:
这里我们发现pipefd[0]指代的是3,而我们的pipefd[1]指代的是4。其实也很好理解,因为0,1,2被标准输入,标准输出,标准错误占了。所以从3开始。
同时,如果我么查手册会看到这样一段话:
这段话的主要意思是pipefd[0]是读端,而pipefd[1]是写端。这为我们以后哪个开哪个关提供了依据。
观察匿名管道现象
我们先搭建架子来观察我们匿名管道的现象:
#include<iostream>
#include<unistd.h>
#include<cassert>
#include<sys/types.h>
#include<sys/wait.h>
using namespace std;int main()
{//建立管道int pipefd[2] = {0};int n = pipe(pipefd);assert(n == 0);//创建子进程pid_t id = fork();//子进程if(id < 0){perror("fork fail");}if(id ==0){//子进程要做的事exit(0);}//父进程要做的事//回收子进程pid_t rid = waitpid(id,nullptr,0);if(rid == id){cout<<"wait success"<<endl;}return 0;
}
现在我们想让子进程写,父进程读,我们把相应用不到的管道关闭:
#include<iostream>
#include<unistd.h>
#include<cassert>
#include<sys/types.h>
#include<sys/wait.h>
using namespace std;int main()
{//建立管道int pipefd[2] = {0};int n = pipe(pipefd);assert(n == 0);//创建子进程pid_t id = fork();//子进程if(id < 0){perror("fork fail");return 1;}if(id ==0){//子进程要做的事close(pipefd[0]); //关闭读的通道exit(0);}//父进程要做的事close(pipefd[1]); //关闭写的通道//回收子进程pid_t rid = waitpid(id,nullptr,0);if(rid == id){cout<<"wait success"<<endl;}return 0;
}
我们让子进程写入一些东西,然后让父进程来读,看看行不行:
#include<iostream>
#include<unistd.h>
#include<cassert>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdio.h>
#include<string.h>
using namespace std;
#define MAX 1024int main()
{//建立管道int pipefd[2] = {0};int n = pipe(pipefd);assert(n == 0);//创建子进程pid_t id = fork();//子进程if(id < 0){perror("fork fail");return 1;}if(id ==0){//子进程要做的事close(pipefd[0]); //关闭读的通道//向管道写入int cnt = 10;while(cnt){//缓冲区char message[MAX];//向缓冲区里写snprintf(message,sizeof(message),"hello father I am child my pid:%d cnt:%d ", getpid(),cnt);cnt--;//向管道写write(pipefd[1],message,strlen(message));sleep(1);}exit(0);}//父进程要做的事close(pipefd[1]); //关闭写的通道//从管道中读取数据char buffer[MAX];while(true){ssize_t n = read(pipefd[0],buffer,sizeof(buffer));if(n > 0){cout << getpid() << "," << "chid say :" << buffer << "to me" << endl;}}//回收子进程pid_t rid = waitpid(id,nullptr,0);if(rid == id){cout<<"wait success"<<endl;}return 0;
}
我们看到父进程真的拿到了子进程写的东西,这就是一个最基本的管道的应用。
读写端的几种情况
写端慢,读端快
我们模拟一下,写端慢,读端快的情况
#include<iostream>
#include<unistd.h>
#include<cassert>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdio.h>
#include<string.h>
using namespace std;
#define MAX 1024int main()
{//建立管道int pipefd[2] = {0};int n = pipe(pipefd);assert(n == 0);//创建子进程pid_t id = fork();//子进程if(id < 0){perror("fork fail");return 1;}if(id ==0){//子进程要做的事close(pipefd[0]); //关闭读的通道//向管道写入int cnt = 10;while(cnt){//缓冲区char message[MAX];//向缓冲区里写snprintf(message,sizeof(message),"hello father I am child my pid:%d cnt:%d ", getpid(),cnt);cnt--;//向管道写write(pipefd[1],message,strlen(message));sleep(100); //模拟写端慢}exit(0);}//父进程要做的事close(pipefd[1]); //关闭写的通道//从管道中读取数据char buffer[MAX];while(true){ssize_t n = read(pipefd[0],buffer,sizeof(buffer));if(n > 0){cout << getpid() << "," << "chid say :" << buffer << "to me" << endl;}}//回收子进程pid_t rid = waitpid(id,nullptr,0);if(rid == id){cout<<"wait success"<<endl;}return 0;
}
我们发现父进程处于一个休眠的状态,很明显,它是在等待我们的子进程进行写入。
这里我们可以得出匿名管道具有同步机制,读端和写端是协同工作的。
写端快,读端慢
我们调换一下,让写端快,读端快:
#include<iostream>
#include<unistd.h>
#include<cassert>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdio.h>
#include<string.h>
using namespace std;
#define MAX 1024int main()
{//建立管道int pipefd[2] = {0};int n = pipe(pipefd);assert(n == 0);//创建子进程pid_t id = fork();//子进程if(id < 0){perror("fork fail");return 1;}if(id ==0){//子进程要做的事close(pipefd[0]); //关闭读的通道//向管道写入int cnt = 10000;while(cnt){//缓冲区char message[MAX];//向缓冲区里写snprintf(message,sizeof(message),"hello father I am child my pid:%d cnt:%d ", getpid(),cnt);cnt--;//向管道写write(pipefd[1],message,strlen(message));cout<<"writing......"<<endl;}exit(0);}//父进程要做的事close(pipefd[1]); //关闭写的通道//从管道中读取数据char buffer[MAX];while(true){sleep(2); //睡眠2秒ssize_t n = read(pipefd[0],buffer,sizeof(buffer));if(n > 0){cout << getpid() << "," << "chid say :" << buffer << "to me" << endl;}}//回收子进程pid_t rid = waitpid(id,nullptr,0);if(rid == id){cout<<"wait success"<<endl;}return 0;
}
执行:
过了2秒之后:
数据一瞬间出来了。
这里我们可以得出匿名管道是面向字节流的,它没有硬性规定我写一条你必须马上读一条,而是以字节流的形式读或写。
管道的大小
我们可以写一段代码来测试我们管道的大小:
#include<iostream>
#include<unistd.h>
#include<cassert>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdio.h>
#include<string.h>
using namespace std;
#define MAX 1024int main()
{//建立管道int pipefd[2] = {0};int n = pipe(pipefd);assert(n == 0);//创建子进程pid_t id = fork();//子进程if(id < 0){perror("fork fail");return 1;}if(id ==0){//子进程要做的事close(pipefd[0]); //关闭读的通道//向管道写入int cnt = 0;while(1){// //缓冲区// char message[MAX];// //向缓冲区里写// snprintf(message,sizeof(message),"hello father I am child my pid:%d cnt:%d ", getpid(),cnt);// cnt--;// //向管道写// write(pipefd[1],message,strlen(message));// cout<<"writing......"<<endl;char c = 'a';write(pipefd[1], &c, 1);cnt++;cout << "write ....: " << cnt << endl;}exit(0);}//父进程要做的事close(pipefd[1]); //关闭写的通道//从管道中读取数据char buffer[MAX];while(true){// sleep(2); //睡眠2秒// ssize_t n = read(pipefd[0],buffer,sizeof(buffer));// if(n > 0)// {// cout << getpid() << "," << "chid say :" << buffer << "to me" << endl;// }}//回收子进程pid_t rid = waitpid(id,nullptr,0);if(rid == id){cout<<"wait success"<<endl;}return 0;
}
我们发现最后结果是65536,折合下来也就是64kb左右的大小。
我们也可以用指令来查看管道大小:ulimit -a:
我们查看的管道大小为512 * 8 = 4kb,好像比我们看到的小。这个其实不是真正的大小。
写端关闭,读端一直读
我们现在让写段写一段时间后直接关闭,但是读端没有关闭:
#include<iostream>
#include<unistd.h>
#include<cassert>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdio.h>
#include<string.h>
using namespace std;
#define MAX 1024int main()
{//建立管道int pipefd[2] = {0};int n = pipe(pipefd);assert(n == 0);//创建子进程pid_t id = fork();//子进程if(id < 0){perror("fork fail");return 1;}if(id ==0){//子进程要做的事close(pipefd[0]); //关闭读的通道//向管道写入int cnt = 0;while(1){//缓冲区char message[MAX];//向缓冲区里写snprintf(message,sizeof(message),"hello father I am child my pid:%d cnt:%d ", getpid(),cnt);cnt++;//向管道写write(pipefd[1],message,strlen(message));//跳出if(cnt > 3) break;// char c = 'a';// write(pipefd[1], &c, 1);// cnt++;// cout << "write ....: " << cnt << endl;}//关闭写端close(pipefd[1]);exit(0);}//父进程要做的事close(pipefd[1]); //关闭写的通道//从管道中读取数据char buffer[MAX];while(true){//sleep(2); //睡眠2秒ssize_t n = read(pipefd[0],buffer,sizeof(buffer));if(n > 0){cout << getpid() << "," << "chid say :" << buffer << "to me" << endl;}cout<<"father return value:"<< n << endl;sleep(1);}//回收子进程pid_t rid = waitpid(id,nullptr,0);if(rid == id){cout<<"wait success"<<endl;}return 0;
}
这样表示:写端关闭,读端一直读取, 读端会读到read返回值为0, 表示读到文件结尾。
同时注意,进程退出,管道自动关闭。
写端一直写,读端关闭
#include<iostream>
#include<unistd.h>
#include<cassert>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdio.h>
#include<string.h>
using namespace std;
#define MAX 1024int main()
{//建立管道int pipefd[2] = {0};int n = pipe(pipefd);assert(n == 0);//创建子进程pid_t id = fork();//子进程if(id < 0){perror("fork fail");return 1;}if(id ==0){//子进程要做的事close(pipefd[0]); //关闭读的通道//向管道写入int cnt = 0;while(true){//缓冲区char message[MAX];//向缓冲区里写snprintf(message,sizeof(message),"hello father I am child my pid:%d cnt:%d ", getpid(),cnt);cnt++;//向管道写write(pipefd[1],message,strlen(message));sleep(1);//跳出//if(cnt > 3) break;// char c = 'a';// write(pipefd[1], &c, 1);// cnt++;// cout << "write ....: " << cnt << endl;sleep(1);}//关闭写端//close(pipefd[1]);exit(0);}//父进程要做的事close(pipefd[1]); //关闭写的通道//从管道中读取数据char buffer[MAX];while(true){//sleep(2); //睡眠2秒ssize_t n = read(pipefd[0],buffer,sizeof(buffer));if(n > 0){cout << getpid() << "," << "chid say :" << buffer << "to me" << endl;}cout<<"father return value:"<< n << endl;sleep(1);//直接跳出break;}//关闭读端close(pipefd[0]);sleep(5);int status = 0;pid_t rid = waitpid(id, &status, 0);if (rid == id){cout << "wait success, child exit sig: " << (status&0x7F) << endl;}// //回收子进程// pid_t rid = waitpid(id,nullptr,0);// if(rid == id)// {// cout<<"wait success"<<endl;// }return 0;
}
我们得到一下它的信号:
我们查一下13号信号:
13号信号是:SIGPIPE:
SIGPIPE 是在进程向一个已经被关闭的管道(或者其他的类似的通信方式)写入数据时,内核向该进程发送的信号。这个信号的默认行为是终止进程。
常见的场景是,一个进程向另一个进程通过管道发送数据,但接收数据的进程提前退出,导致写入数据的进程尝试往已关闭的管道写入数据。在这种情况下,内核会发送 SIGPIPE 信号给写入数据的进程,通知它目标进程已经退出,不再接收数据。
所以我们才有上述现象。
总结一下管道有4种情况:
管道的4种情况
- 正常情况,如果管道没有数据了,读端必须等待,直到有数据为止(写端写入数据了)
- 正常情况,如果管道被写满了,写端必须等待,直到有空间为止(读端读走数据)
- 写端关闭,读端一直读取, 读端会读到read返回值为0, 表示读到文件结尾
- 读端关闭,写端一直写入,OS会直接杀掉写端进程,通过想目标进程发送SIGPIPE(13)信号,终止目标进程
5种特性:
管道的5种特性
- 匿名管道,可以允许具有血缘关系的进程之间进行进程间通信,常用与父子,仅限于此
- 匿名管道,默认给读写端要提供同步机制 — 了解现象就行
- 面向字节流的 — 了解现象就行
- 管道的生命周期是随进程的
- 管道是单向通信的,半双工通信的一种特殊情况