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

目录

前言:

总结下上述的内容:

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自动关闭;

坚持打卡!😀

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

瞳绣

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值