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 1024

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]); //关闭读的通道

       //向管道写入
       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 1024

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]); //关闭读的通道

       //向管道写入
       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 1024

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]); //关闭读的通道

       //向管道写入
       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 1024

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]); //关闭读的通道

       //向管道写入
       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 1024

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]); //关闭读的通道

       //向管道写入
       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 1024

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]); //关闭读的通道

       //向管道写入
       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种情况

  1. 正常情况,如果管道没有数据了,读端必须等待,直到有数据为止(写端写入数据了)
  2. 正常情况,如果管道被写满了,写端必须等待,直到有空间为止(读端读走数据)
  3. 写端关闭,读端一直读取, 读端会读到read返回值为0, 表示读到文件结尾
  4. 读端关闭,写端一直写入,OS会直接杀掉写端进程,通过想目标进程发送SIGPIPE(13)信号,终止目标进程

5种特性:

管道的5种特性

  1. 匿名管道,可以允许具有血缘关系的进程之间进行进程间通信,常用与父子,仅限于此
  2. 匿名管道,默认给读写端要提供同步机制 — 了解现象就行
  3. 面向字节流的 — 了解现象就行
  4. 管道的生命周期是随进程的
  5. 管道是单向通信的,半双工通信的一种特殊情况
  • 27
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值