【hello Linux】进程间通信——匿名管道

news2024/11/14 20:48:22

目录

前言:

总结下上述的内容:

1. 进程间通信目的

2. 进程间通信的分类

1. 匿名管道

2. 匿名管道的使用

1. 匿名管道的创建

2. 使用匿名管道进行父子间通信


Linux🌷 

前言:

进程具有独立性,拥有独立的数据、代码及其他资源,为什么要让相互独立的进程间进行通信呢?

如何让相互独立的进程进行通信?

先说为什么?:

首先在此提一下协同的概念:

协同:本质就是多人合作完成同一件事情。

用一个APP的上市来说吧!

首先由产品经理了解用户的需求—>程序员对APP进行开发—>测试人员对APP进行测试—>发布!

大致流程就如上述所示!

这便是协同工作的场景,多个部门人员进行沟通协作完成了一款APP。

如果让一个部门人员完成上述工作,那势必耗时又耗力,专门的事还是应该交给专业的人做。

进程间在一定场景下也会发生协同工作,完成某种事情,这便是为什么的原因。

如下给出一个例子以便更好地理解:

 第一条命令行解释:我们可以使用 ll 命令查看当前目录下的文件信息;

 第二条命令行解释:我们将 ll 命令展示的信息 利用管道传递给 grep 命令 最后在grep命令的协助下完成了只筛选出包含5的信息;

这便是两条命令间的共同协作。

如何进行进程间通信呢?

进程间相互协同工作,一个进程把自己的数据交付给另一个进程,让其进行处理,这便是进程间通

信,操作系统便要设计进程间的通信方式。因为进程间是具有独立性的,如果交互数据,成本一定

很高,这是因为一个进程是看不到另一个进程的资源的。那么OS便要设计必须得让两个进程先看

到一份公共的资源,这里的资源其实是由OS提供得一段内存!

只不过这段内存可能以文件的方式提供(管道)、可能以队列的方式提供(消息队列)、也可能就

是原始的内存块(共享内存),由此可见进程间通信方式是有多种的。

终于开始我们正文了!🤦‍♀️


总结下上述的内容:

1. 进程间通信目的

  • 数据传输:一个进程要把自己的数据交给另一个进程,让其继续进行处理;
  • 资源共享:多个进程之间共享同样的资源;
  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程);
  • 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变;

2. 进程间通信的分类

  • 管道
    • 匿名管道pipe
    • 命名管道pipe
  • System V标准 进程间通信
    • System V 消息队列
    • System V 共享内存
    • System V 信号量
  • POSIX标准 进程间通信(多线程详谈)
    • 消息队列
    • 共享内存
    • 信号量
    • 互斥量
    • 条件变量
    • 读写锁

在今天这篇博客中谈的是:管道中的匿名管道通信方式。

管道:

管道是Unix中最古老的进程间通信的方式;

我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”;

1. 匿名管道

管道是用于进程间通信的,匿名管道主要用于“具有亲缘关系”的进程间通信的,一般用于父子进程。

父进程创建子进程,子进程以父进程为模板创建自己的和进程相关的数据结构,和父进程共同分享一份代码,如果不发生写时拷贝,数据也是共同分享一份的。

今天主要探讨的是 file_struct:

 我们平常使用write系统调用,最开始的理解是:直接将数据写至内核缓冲区。

今天我们要对它进行更深一步的理解,write系统调用实际上干了两件事:

1. 拷贝数据到内核缓冲区;

2. 触发底层的写入函数在合适的时机刷新到外设,如write_disk()到磁盘;

如果只是将数据拷贝至内核缓冲区,而不进行刷新,另一个进程从缓冲区读,那么这个缓冲区便相当于管道(一份临界资源)(公共资源)。

这种基于文件的通信方式叫做管道

2. 匿名管道的使用

1. 匿名管道的创建

#include <unistd.h>

int pipe(int pipefd[2]);

参数pipefd:是输出型参数,通过这个参数拿到两个未被分配的文件标识符fd;

如果没有文件打开则 pipefd[0] = 3 ,pipefd[1] = 4,因为0、1、2 已经被占用;

其中pipefd[0]是读端,pipefd[1]是写端;

0很像嘴巴,嘴巴是用来读的,1像一支笔,笔是用来写的,这样是不是很好区分了😁

返回值:创建成功返回0,失败返回-1;

 经过证实确实如此!

2. 使用匿名管道进行父子间通信

1. 总体流程:

上述是一个子进程来写,父进程来读的大致图示;

 其实父子进程本没有谁应该读、谁应该写之分,上述分配只是为了帮助我们更好验证一些东西;

 2. 读阻塞:

子进程写的慢,父进程读的快,会发生什么情况呢?

#include <stdio.h>
#include <unistd.h>
#include <string.h>

int main()
{
  //创建管道
  int pipedf[2]={0};
  if(pipe(pipedf)!=0)
  {
    perror("pipe fail");
    return 1;
  }
  
  //创建子进程
  if(fork()==0)    
  {    
    //child    
    close(pipedf[0]);    
    const char* msg = "hello pipe";    
    while(1)    
    {    
      write(pipedf[1],msg,strlen(msg));    
      sleep(3);    
    }                                                                                                                                              
    close(pipedf[1]);
    return 0;    
  }    

  //parent                                                                                                                                         
  close(pipedf[1]);
  char buffer[64]={0};
  while(1)
  {
    read(pipedf[0],buffer,sizeof(buffer));
    printf("child say # %s\n",buffer);
  }
  close(pipedf[0]);
  return 0;
}

上述代码:

创建了一个管道,让子进程每隔3秒往管道里写数据,父进程一直从管道中读取数据并输出;

经过实验我们可以看到,大概每隔3秒显示器便会输出数据;

由此可以说明: 在写的慢读的快的情况下,读端会等写端;

3. 写阻塞

子进程写的快,父进程读的慢,会发生什么情况呢?

#include <stdio.h>
#include <unistd.h>
#include <string.h>

int main()
{
  //创建管道
  int pipedf[2]={0};
  if(pipe(pipedf)!=0)
  {
    perror("pipe fail");    
    return 1;    
  }    
    
  //创建子进程    
  if(fork()==0)    
  {    
    //child    
    close(pipedf[0]);    
    const char* msg = "hello pipe";    
    while(1)    
    {    
      write(pipedf[1],msg,strlen(msg));    
    }    
    close(pipedf[1]);                                                                                                                              
    return 0;
  }    

  //parent
  close(pipedf[1]);                                                                                                                                
  char buffer[64]={0};
  while(1)
  {
    read(pipedf[0],buffer,sizeof(buffer));
    printf("child say # %s\n",buffer);
    sleep(3);
  }
  close(pipedf[0]);
  return 0;
}

上述代码:

创建了一个管道,让子进程一直往管道里写数据,父进程每隔3秒从管道中读取数据并输出;

经过实验我们可以看到,显示器上输出了一大批数据;

这是因为管道文件在写入的时候:只要有缓冲区(空闲的)就写入;在读的时候:只要有内容就会读取;

管道是面向字节流的,没有明确的界限划分,是以字节为单位进行读取的,我们可以通过制定协议的方法来达到父子进程正常的一个通信,在学习网络的时候再详谈; 

4.验证管道的大小

管道也有自身的大小,利用如下代码我们可以验证管道缓冲区的大小为 64KB;

  #include <stdio.h>
  #include <unistd.h>
  #include <string.h>
  
  int main()
  {
    //创建管道
    int pipedf[2]={0};
    if(pipe(pipedf)!=0)
    {
      perror("pipe fail");
      return 1;
    }
  
    //创建子进程
    if(fork()==0)
    {
      //child
      close(pipedf[0]);
      char* msg ="a";
      int count=0;
      while(1)    
      {    
        write(pipedf[1],msg,1);
        count++;                                                                                                                                   
        printf("count:%d\n",count);    
      }
      close(pipedf[1]);    
      return 0;    
    }    
    //parent
    close(pipedf[1]);
    while(1)
    {
  
    }
    close(pipedf[0]);
    return 0;
  }

经过证实我使用的云服务器上管道容量是64KB

在这里我们还有一个疑惑:在管道写满的时候,我们难道不能覆盖前面的内容然后继续写吗?

我们是不能覆盖之前的内容重新写入的,因为我们写数据就是为了让读端来读,如果覆盖掉之前的数据,那不相当于之前写的工作就白费了,违背了进程通信的初衷。

事实上,管道是自带同步机制的,父子进程在读写时会相互等待,这种机制很好的保证了数据安全。

5. 写数据的时机

对于读进程来说,只要管道中有数据,读进程便可以从管道中读取到数据;

但对于写进程来说,必须有4KB大小的空闲缓冲区时,写进程才可以写入数据。

下面我们来验证下:

  #include <stdio.h>
  #include <unistd.h>
  
  int main()
  {
    //创建管道
    int pipedf[2]={0};
    if(pipe(pipedf)!=0)
    {
      perror("pipe fail");
      return 1;
    }
  
    //创建子进程
    if(fork()==0)    
    {    
      //child    
      close(pipedf[0]);    
      char* msg = "a";    
      int count = 0;    
      while(1)    
      {    
        //往管道中以字节为单位进行写入计数    
        write(pipedf[1],msg,1);    
        printf("count:%d\n",count);                                                                                                                
        count++;    
      }
      close(pipedf[1]);    
      return 0;    
    }

    //parent
    close(pipedf[1]);
    char buffer[64]={0};
    while(1)
    {                                                                                                                                              
      //每秒读取64字节数据
      ssize_t s = read(pipedf[0],buffer,sizeof(buffer));
      sleep(1);
      if(s==0)
      {
        printf("写端关闭\n");
        break;
      }
      else if(s>0)
      {
        printf("child say # %s\n",buffer);
      }
      else 
      {
        perror("read fail");
        return 1;
      }
    }
    close(pipedf[0]);
    return 0;
  }

上述代码:一直往管道中写入数据,每秒中读取64字节数据;

经过实验我们看到读取了一定的数据后,我们还没有看到写进程写入数据;

由此:并不只是有空闲缓冲区写进程就会写入数据的,而是有一定的时机;

我们试着每次读取2KB的数据在进行如下实验:

#include <stdio.h>
  #include <unistd.h>
  
  int main()
  {
    //创建管道
    int pipedf[2]={0};
    if(pipe(pipedf)!=0)
    {
      perror("pipe fail");
      return 1;
    }
  
    //创建子进程
    if(fork()==0)    
    {    
      //child    
      close(pipedf[0]);    
      char* msg = "a";    
      int count = 0;    
      while(1)    
      {    
        //往管道中以字节为单位进行写入计数    
        write(pipedf[1],msg,1);    
        printf("count:%d\n",count);                                                                                                                
        count++;    
      }
      close(pipedf[1]);    
      return 0;    
    }

    //parent
    close(pipedf[1]);
    char buffer[1024*2+1]={0};
    while(1)
    {                                                                                                                                              
      //每秒读取2KB数据
      ssize_t s = read(pipedf[0],buffer,sizeof(buffer));
      sleep(1);
      if(s==0)
      {
        printf("写端关闭\n");
        break;
      }
      else if(s>0)
      {
        printf("child say # %c\n",buffer[0]);
      }
      else 
      {
        perror("read fail");
        return 1;
      }
    }
    close(pipedf[0]);
    return 0;
  }

如上:我们可以看到在经过两次2KB的读数据后,写进程才会写入数据。

这样做是为了保证写入的一个原子性

6. 写端关闭

#include <stdio.h>
#include <unistd.h>
#include <string.h>

int main()
{
  //创建管道
  int pipedf[2]={0};
  if(pipe(pipedf)!=0)
  {
    perror("pipe fail");
    return 1;
  }

  //创建子进程
  if(fork()==0)
  {
    //child
    close(pipedf[0]);
    const char* msg = "hello 管道";
    int count = 3;
    while(count)
    {    
      //往管道中以字节为单位进行写入计数    
      write(pipedf[1],msg,strlen(msg));                                                                                                            
      count--;
    }    
    close(pipedf[1]);    
    return 0;
  }    
  //parent
  close(pipedf[1]);
  char buffer[64]={0};
  while(1)
  {
    //每秒读取64字节数据
    ssize_t s = read(pipedf[0],buffer,sizeof(buffer));
    sleep(1);                                                                                                                                      
    if(s==0)
    {
      printf("写端关闭\n");
      break;
    }
    else if(s>0)
    {
      printf("child say # %s\n",buffer);
    }
    else 
    {
      perror("read fail");
      return 1;
    }
  }
  close(pipedf[0]);
  printf("读取完毕\n");
  return 0;
}

上述代码:写端写入3次数据,读端每隔1秒读取64字节数据;

在此我们可以看到:读端读取完写端写入的数据后,继续执行进程中的后续代码;

7. 读端关闭 

#include <stdio.h>
#include <unistd.h>
#include <string.h>

int main()
{
  //创建管道
  int pipedf[2]={0};
  if(pipe(pipedf)!=0)
  {
    perror("pipe fail");
    return 1;
  }

  //创建子进程
  if(fork()==0)
  {
    //child
    close(pipedf[0]);
    const char* msg = "hello 管道";
    while(1)
    {
      //往管道中以字节为单位进行写入计数
      write(pipedf[1],msg,strlen(msg));
    }                                                                                                                                              
    close(pipedf[1]);
    return 0;
  }    

  //parent
  close(pipedf[1]);
  char buffer[64]={0};
  int count = 3;                                                                                                                                   
  while(count)
  {
    ssize_t s = read(pipedf[0],buffer,sizeof(buffer));
    if(s==0)
    {
      printf("写端关闭\n");
      break;
    }
    else if(s>0)
    {
      printf("child say # %s\n",buffer);
      count--;
    }
    else 
    {
      perror("read fail");
      return 1;
    }
  }
  close(pipedf[0]);
  printf("读取完毕\n");
  return 0;
}

 子进程一直写入,父进程读取三次后关闭管道,我们可以看出父子进程都退出了;

这是因为:当我们读端关闭,写端还在写时,此时对于OS来说,是对资源的一种浪费;

因此OS便会在读进程关闭读端口时,向写进程发送 13)SIGPIPE 信号杀死该进程;

在上述代码后添加如下代码便可验证:

//使用waitpid时要包含这两个头文件
#include <stdlib.h>
#include <sys/wait.h>


int status=0;
waitpid(-1,&status,0);
printf("exit code:%d\n",(status>>8)&0xff);
printf("signal:%d\n",status&0x7f);

总结一下整篇博客的内容:

管道有4种情况:

a. 读端不读或者读的慢,写端要等待读端;

b. 写端不写或写的慢,读端要等写端;

c. 读端关闭,写端收到SIGPIPE信号直接终止;

d. 写端关闭,读端读完pipe内部的数据然后再读,会读到0,表示读到文件结尾;

匿名管道的5个特点:

1. 管道是一个只能单向通信的通信信道;

2. 管道是面向字节流的;

3. 仅限于具有血缘关系的进程进行进程间通信,通常用于父子进程通信;

4. 管道是自带同步机制的,且原子性写入;

5. 管道的生命周期是随进程的:管道是文件,如果一个文件只被一些进程打开,相关进程都退出了,那么被打开的文件会被OS自动关闭;

坚持打卡!😀

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/464661.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

人工智能时代来临,殊不知低代码早已出手

科普一下人工智能的等级划分&#xff0c;按照实力&#xff0c;人工智能可以分为弱人工智能(Artificial Narrow Intelligence&#xff0c;简称ANI)、强人工智能(Artificial General Intelligence简称AGI)、超人工智能(Artificial Superintelligence简称ASI)三个等级。 弱人工智能…

Windows下 批量重命名文件【bat实现】

目录 前言 一、Windows简单实现重命名 二、使用命令行和Excel实现重命名 前言 在实际应用中&#xff0c;我们经常会遇到将指定文件夹下的文件重命名&#xff0c;以便程序读写。 本文介绍了两种方式&#xff0c;都是在Windows系统中自带的重命名方式。 一、Windows简单实现…

面试题目 002

分享一位读者面试美团 java 岗位的面经。主要在考察 javamysql 算法题目 最长回文串 根据前序中序恢复二叉树 说一说 MySQL 的索引 MySQL 的索引是一种存储数据结构&#xff0c; 按照数据结构划分&#xff0c;MySQL 可以分为 B 树索引、Hash 索引、全文索引 按照 B 树的叶子…

Windows云主机崩溃了怎么办?

​  无法避免服务器崩溃已不是什么秘密&#xff0c;无论选择Windows 云主机还是 Linux 云主机。但不可否认的是&#xff0c;任何错误都可能给企业带来灾难性的后果。该怎么办?持续监控服务器是可能的解决方案之一。即便如此&#xff0c;如果服务器已经处于关闭阶段&#xff…

机器学习实战教程(十):逻辑回归

概述 逻辑回归&#xff08;Logistic Regression&#xff09;是一种用于解决二分类或多分类问题的统计学习方法。它以自变量线性组合的形式进行建模&#xff0c;并使用Sigmoid函数将结果映射到[0, 1]的值域内&#xff0c;表示样本属于某个类别的概率。 Logistic Regression是最…

6个好用的企业管理软件推荐

企业管理软件的范围很广&#xff0c;财务、人力、客户关系管理、ERP、客户体验管理等等。国内来看&#xff0c;有些企业管理软件产品能覆盖企业数字化所有部分&#xff0c;在每个领域&#xff0c;也有很突出的头部厂商&#xff0c;产品功能和服务都大幅领先于竞对&#xff0c;我…

Redis学习笔记大全

文章目录 1、redis概述和安装1.1、安装redis1.2、启动redis方式1&#xff1a;前台启动&#xff08;不推荐&#xff09;方式2&#xff1a;后端启动&#xff08;推荐&#xff09; 1.3、关闭redis1.4、进入redis命令窗口1.5、redis命令大全1.6、redis介绍相关知识 2、redis 5大数据…

PyTorch实战3:天气识别

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f366; 参考文章&#xff1a;365天深度学习训练营-第P3周&#xff1a;天气识别&#x1f356; 原作者&#xff1a;K同学啊|接辅导、项目定制 目录 一、前期准备1、导入数据2、transforms.Compose详…

JavaWeb+JSP内置对象+Session+Cookie+ 过滤器Filter+ 监听器Listener(超详细)

&#x1f648;作者简介&#xff1a;练习时长两年半的Java up主 &#x1f649;个人主页&#xff1a;老茶icon &#x1f64a; ps:点赞&#x1f44d;是免费的&#xff0c;却可以让写博客的作者开兴好久好久&#x1f60e; &#x1f4da;系列专栏&#xff1a;Java全栈&#xff0c;计…

从 Milvus 2.2 到 2.2.6,我们是如何持续稳定升级的

最近&#xff0c;Milvus 发布了 2.2.6 版本&#xff0c;在修复了一些 bug 后&#xff0c;Milvus 变得越发稳定。 事实上&#xff0c;自 Milvus 升级至 2.X 版本以来&#xff0c;我们一直在努力改进及优化&#xff0c;推出了诸如从文件中批量导入数据、基于磁盘的近似最近邻&…

通过ppt制作圆形图标及自定义形状图形制作

今天做PPT时&#xff0c;需要一个圆形的截图&#xff0c;日常截图都是方形的&#xff0c;能不能把截图裁剪为一个圆形呢&#xff1f;于时从网上找了一下&#xff0c;发现还真有。 制作工具&#xff1a;PPT 制作步骤&#xff1a; 1、准备图片 将截图或图片插入PPT中 2、在…

(04)基础强化:接口,类型转换cast/convert,异常处理,传参params/ref/out,判断同一对象

一、复习 1、New的截断是指什么&#xff1f; new除了新开空间创建初始化对象外&#xff0c;还有一个隐藏父类同名方法的作用。 当子类想要隐藏父类同名的方法时用new&#xff0c;用了new后父类同名方法将到此为止&#xff0c;后面 继承的…

electron+vue3全家桶+vite项目搭建【15】vue3+sass实现多主题一键切换,支持electron多窗口同步更新

文章目录 引入实现效果展示实现思路整理实现步骤1.定义全局主题样式变量2.定义主题模板3.封装颜色工具类4.初始化主题色5.主进程监听颜色修改6.补充主题状态管理7.主题一键切换组件8.测试案例 引入 我们之前在这篇文章中集成了 sass&#xff0c;接下来我们结合sass的变量定义&…

银行数字化转型导师坚鹏:宏观经济形势分析与银行发展模式创新

宏观经济形势分析与银行发展模式创新 课程背景&#xff1a; 很多学员存在以下问题&#xff1a; 不知道我国目前的宏观经济形势&#xff1f; 不清楚宏观环境对我国经济的影响&#xff1f; 不知道银行未来主要的发展模式&#xff1f; 课程特色&#xff1a; 精彩解读宏…

最新:机器学习在生态、环境经济学中的实践技术应用及论文写作

查看原文>>>最新&#xff1a;机器学习在生态、环境经济学中的实践技术应用及论文写作 目录 专题一、理论基础与软件介绍 专题二、数据的获取与整理 专题三、常用评价方法与相关软件详细教学&#xff08;案例详解&#xff09; 专题四、写作要点与案例的讲解 近年来…

Redis数据库常用语句

Redis数据库常用语句 前言1. 键(Key)的基本操作1.1 增加新的键值对1.2 访问键的值1.3 修改键值对1.4 键值对的删除1.5 判断键值对是否存在1.6 获取所有键1.7 删除所有的键&#xff1a; 2. Redis 中的列表2.1 列表加入新元素2.2 获取列表长度2.3 获取指定下标的元素2.4 获取指定…

Android App 架构 面试专题,你可能会被问到的 20 个问题

iveData 是否已经被弃用? 没有被弃用。在可以预见的未来也没有废弃的计划。 LiveData 可以使用简单的方式获取一个易于观察、状态安全的对象。虽然其缺少一些丰富的操作符&#xff0c;但是对于一些简单的 UI 业务场景已经足够。 Flow 有 LiveData 相同的功能&#xff0c;其…

1.栈的介绍-C语言调用函数(二)

1.栈的介绍-C语言调用函数&#xff08;一&#xff09;_双层小牛堡的博客-CSDN博客 接着上面 函数调用的约定 在栈帧中 主要的是主调函数如何存入实参 让被调用函数能够访问 这种是通过函数见的调用规定来规范的 并且 调用规定还规范了 函数执行完后应该由主函数实现 清除参…

[测试猿课堂]小白怎么学测试?史上最全《软件测试》学习路线

熬夜3天&#xff0c;联合3位猿计划教育的总监级授课老师&#xff0c;整理了这份《软件测试小白学习路线》&#xff0c;全文接近6000字&#xff0c;请大家耐心看完&#xff01; 对于很多想通过自学转行软件测试的同学&#xff0c;痛点并不是学习动力&#xff0c;而是找不到清晰…

Apache SeaTunnel 3 分钟入门指南

简介 新一代分布式超高性能云原生数据同步工具 - Apache SeaTunnel 已经在B站、腾讯云、字节等数百家公司使用。 SeaTunnel 是 Apache 软件基金会下的一个高性能开源大数据集成工具&#xff0c;为数据集成场景提供灵活易用、易扩展并支持千亿级数据集成的解决方案。SeaTunnel …