Linux进程通信(管道通信)

举报
卖寂寞的小男孩 发表于 2022/10/11 08:37:49 2022/10/11
【摘要】 本文主要介绍Linux下的管道通信

@[toc]

零、前言

本文将介绍进程通信的概念,以及进程之间通过匿名管道进行通信的原理。匿名管道通信一共有五个特点和四种情况,一一在文章中进行了验证。

一、进程通信的概念

进程通信本质上就是不同的进程之间的信息交换,从而使不同的进程进行协同工作。

cat test.c | wc -l

这段代码就是一个简单的进程通信,即将进程cat的数据传给进程wc,从而计算出代码的函数:
在这里插入图片描述

二、进程通信的条件

因为进程是具有独立性的,因此两个进程如果直接进行数据交换的代价会很高,所以需要操作系统来设计通信的方式。
由于进程具有独立性,两个进程没法看到对方的数据,因此,操作系统会开辟一块内存来作为两个进程通信的媒介。这块内存可能是一个文件,也可能是一个队列,或者就是最原始的内存块,这也就是通信方式有很多种的原因。

三、匿名管道通信

1.原理

匿名管道的通信常用于有亲属关系的进程,最常用在父子进程中。
在这里插入图片描述
以上的图就解释了匿名管道通信的原理,由于子进程是以父进程为模板的,因此files结构体也会被继承下来,父子进程的文件描述符3都指的是同一个文件。当父进程上层要向磁盘中写数据的时候,数据会首先被存放在内核缓冲区,再调用磁盘驱动的方法来刷新到磁盘。当OS命令内核缓冲区不向磁盘中刷新数据时,父进程上层的数据(hello world)就会存放在内核缓冲区中,此时子进程访问内核缓冲区进行读数据。就实现了数据在父子进程之间的交换。
这种方式基于文件的内核缓冲区的进程通信方式我们就称之为管道通信,内核缓冲区是由操作系统所提供的。

2.匿名管道的实现

为实现上述过程,我们通常要使用两个文件操作符分别以读形式和写形式来两次打开文件:
在这里插入图片描述
首先父进程以读写两种方式打开一个文件,此时它的文件描述符3表示以读打开,文件描述符4表示以写打开。生成子进程之后,子进程拷贝父进程的files结构体,因此文件读写与文件操作符的对应与父进程一致。然后父进程关闭写文件的文件描述符,子进程关闭读文件的文件描述符,这样就可以实现子进程向管道中写数据,父进程向管道中读数据了。
C语言为我们提供了pipe这一系统调用接口来实现管道。
在这里插入图片描述
其中它的参数pipefd数组中存放的是以读或者写打开文件的文件操作符,0下标存储的是以读方式打开文件的文件操作符,1存储的是以写方式打开文件的文件操作符。当创建管道成功返回0,失败返回-1。
下面验证一下0和1下标存储的文件操作符:

#include<stdio.h>    
#include<unistd.h>    
int main()    
{    
  int pipefd[2]={0};    
  if(pipe(pipefd)!=0)    
  {    
    perror("pipe error\n");    
    return 1;    
  }    
  printf("pipefd[0]:%d\n",pipefd[0]);    
  printf("pipefd[1]:%d\n",pipefd[1]);                                                                                                                    
}  

打印的结果是:
在这里插入图片描述
此时我们发现函数pipe改变它的参数pipefd数组的值。且0下标对应的文件描述符是3,1下标对应的文件描述符是4。而0下标是以读打开,1下标是以写打开,因此文件描述符3表示的是以读方式打开文件,文件描述符4对应的是以写方式打开文件。
了解了这些,我们就可以建立父子进程使他们来进行通信了:

#include<stdio.h>                                                                                                                                      
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
int main()
{
  int pipefd[2]={0};
  if(pipe(pipefd)!=0)
  {
    perror("pipe error\n");
    return 1;
  }
  printf("pipefd[0]:%d\n",pipefd[0]);
  printf("pipefd[1]:%d\n",pipefd[1]);
  if(fork()==0)//子进程
  {
    close(pipefd[0]);//关闭读文件
    const char* msg="hello pipe";
    while(1)
    {
        write(pipefd[1],msg,strlen(msg));
        sleep(1);
    }
    exit(0);
  }
  close(pipefd[1]);//父进程关闭写文件
  while(1)
  {
    char buffer[64]={0};
   ssize_t s=read(pipefd[0],buffer,sizeof(buffer));
  if(s==0)
  {
    break;
  }
  else if(s>0)
  {
    buffer[s]=0;
    printf("child say:# %s\n",buffer);
  }
  else 
  {
    printf("read error!\n");
    break;
  }
  }
}                                                       

使用父进程的buffer来接收子进程的数据,我们令子进程每沉睡一秒写一次数据,此时打印的结果为:
在这里插入图片描述
可以看出完成了进程的通信。

3.匿名管道的特点

1.管道是一个单向传输的通道。
2.管道是面向字节流的,(只要管道里有数据,就可以一直进行读取)
3.匿名管道常用于父子进程通信。
4.管道自带同步性机制,以原子性写入(有读阻塞和写阻塞)
5.管道是文件,属于进程,当进程退出时被打开的管道会被关闭。

下面将使用四种情况,来验证管道的这些特点。

4.四种情况

(1)读端不读或者读的慢,写端要等待读端。

我们将上述代码修改如下:

#include<stdio.h>                                                                                                                                      
#include<unistd.h>
#include<string.h>                   
#include<stdlib.h>                   
int main()                           
{                                    
  int pipefd[2]={0};                 
  if(pipe(pipefd)!=0)                
  {                                  
    perror("pipe error\n");          
    return 1;                        
  }                                  
  printf("pipefd[0]:%d\n",pipefd[0]);
  printf("pipefd[1]:%d\n",pipefd[1]);
  if(fork()==0)//子进程              
  {                                  
    close(pipefd[0]);//关闭读文件    
//    const char* msg="hello pipe";  
    int  count=0;                    
    while(1)                         
    {                                
         write(pipefd[1],"a",1);     
         count++;                    
         printf("%d\n",count);       
//        sleep(1);                  
    }                                
    exit(0);                         
  }                                  
  close(pipefd[1]);//父进程关闭写文件    
  while(1)
  {
//    char buffer[64]={0};
//   ssize_t s=read(pipefd[0],buffer,sizeof(buffer));
//  
//  if(s==0)
//  {
//    break;
//  }
//  else if(s>0)
//  {
//    buffer[s]=0;
//    printf("child say:# %s\n",buffer);
//  }
//  else 
//  {
//    printf("read error!\n");
//    break;
//  }
  }
}          

此时我们使用count来记录子进程写入的次数,而父进程不进行读取,此时我们发现子进程写入65536次后,不再进行写入:
在这里插入图片描述
这是因为内核缓冲区也是有大小的,65536个比特位正好是64KB的大小。此时不会再进行写入了(不会覆盖数据),因为在等待读端来读数据。
当我们让读端缓慢读取代码的时候呢?我们将父进程部分的代码更改如下:

  while(1)    
  {    
    sleep(10);    
    char c=0;    
    read(pipefd[0],&c,1);    
    printf("father take:%c\n",c);
  }     

当我们让父进程每隔十秒读一个字节的时候,我们发现并没有什么反应。不妨将每次读的数值放大一些:

    sleep(10);    
    char c[1024*4+1]={0};    
    ssize_t s=read(pipefd[0],c,1024*4);    
    c[s]=0;    
    printf("father take:%s\n",c);    

当每次读取4kb个数据的时候,我们发现写入的数据进行更新了:
在这里插入图片描述
如果我们每次读取2KB的数据呢?此时每隔两个读取周期,写入的数据会发生更新。
这就说明,匿名管道通信的特点4,即管道通信是自带同步机制,并以原子的方式来进行写入的。只有读取了4KB的数据之后,管道才会被重新写入数据。

(2)读端关闭,写端收到sigpipe信号,直接终止

  while(1)    
  {    
          
    sleep(5);    
    char c[64]={0};    
    ssize_t s=read(pipefd[0],c,63);    
//    c[s]=0;    
//    printf("father take:%s\n",c);    
//    char buffer[64]={0};    
//   ssize_t s=read(pipefd[0],buffer,sizeof(buffer));    
//      
  if(s==0)    
  {    
    break;    
  }    
  else if(s>0)    
  {    
    c[s]=0;    
    printf("child say:# %s\n",c);    
  }    
  else     
  {    
    printf("read error!\n");    
    break;    
  }    
  break;    
  }    
  close(pipefd[0]);    
    return  0;    
}                                           

当我们使得父进程读一次数据之后,将父进程的读操作关闭的时候,观察子进程:
需要使用命令行脚本来观察这两个进程运行的情况:

while :;do ps axj|grep mytest|grep -v grep;sleep 1;echo “###############################################”;done

此时我们发现当父进程不再进行读操作之后,子进程也会发生退出:
在这里插入图片描述
当我们将读端进行关闭时,写端还在继续写入,在操作系统的角度分析,这是不合理的。本质上就是在浪费操作系统的资源,OS会直接终止子进程,通过给子进程发送sigpipe的信号的方式来终止它。
我们可以查看一下该信号:
在这里插入图片描述
通过kill -l查看所有信号,我们发现该信号在第十三个位置上。同时在子进程退出之后,我们可以在父进程处接收一下子进程的退出码和退出信号:

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

此时就可以得到子进程的退出码为0,退出信号为13:
在这里插入图片描述

(3)写端不写或写得慢,读端需要等写端

验证方式就是将写端的频率调低,使子进程进行sleep操作,而读端一直在读:
此时读端会等待写端写入数据之后再进行读数据,如果写端不写,则读端会一直等待。
在这里插入图片描述
这与写端在读端读取4kb的数据是一样的,都验证了匿名管道读写的原子性。

(4)写端关闭,读端读完全部数据,读到0说明读到结尾

  if(fork()==0)//子进程    
  {    
    close(pipefd[0]);//关闭读文件    
//    const char* msg="hello pipe";    
    int  count=0;    
    while(1)    
    {    
         write(pipefd[1],"a",1);    
         count++;    
         printf("%d\n",count);    
         sleep(1);    
         break;    
    }    
    close(pipefd[1]);    
    exit(0);    
  }    

此时将子进程退出,父进程读到了0后可以控制进行退出操作:
在这里插入图片描述

四、命名管道通信

1.原理

进程之间进行通信的条件是两个通信的进程同时看到同一块空间,并通过该空间来进行通信,在管道通信中这块空间是一个文件。在进行匿名通信时,由于是父子进程之间的通信,所以可以通过相同的文件描述符找到这个文件,而是在命名管道中必须将该文件进行命名之后,两个进程(可以非亲非故)才能找到该文件。
注意,管道通信的特点在于,数据并没有被写到磁盘上,而是在文件中进行临时保存。与通过普通文件的进程通信是不同的。
在这里插入图片描述
命名管道的通信就是将A和B分别以读和写的方式同时打开同一个文件,并且该文件中的数据不被刷新到磁盘上。
这个文件就是命名管道文件。

2.命名管道的实现

(1)通过命令行实现

mkfifo pipe

使用mkfifo来建立命名管道,命名为pipe。
在这里插入图片描述
当我们向管道输入数据的时候,发现管道的大小是0,这说明数据并没有刷新到磁盘上。
同时我们也可以看见它的文件类型,是以p开头的文件。

(2)通过代码实现

创建命名管道的函数名也为mkfifo,我们可以使用man手册来进行查询:
在这里插入图片描述
它的第一个参数pathname表示的是 路径/命名管道,mode表示的是权限,注意这里的权限是需要与umask来进行计算的在这里插入图片描述
当创建成功返回0,创建失败返回-1。我们可以通过一段简单的代码来创创建一个管道。
在这里插入图片描述
执行这段代码之后可以发现已经创建成功了管道文件。
同时我们发现,我们规定的权限是666,但是实际的权限是664,这是因为权限最终要与umask进行计算,如果要修改需要先修改系统的umask。

3.利用命名管道进行通信

有了管道就可以进行通信了,我们再建立一个client来进行通信,只需要进行正常的文件操作即可,将管道文件看成一个文件。
建议使用系统调用接口,因为没有用户缓冲区的干扰。

(1)head.h

#include<stdio.h>                                                                            
#include<sys/stat.h>    
#include<sys/types.h>    
#include<fcntl.h>    
#include<unistd.h>    
#define MY_FIFO "./fifo"     

(2)server.c

#include "head.h"                                                         
int main()    
{    
   if(mkfifo(MY_FIFO,0666)<0)    
   {    
      perror("mkfifo");    
      return 1;    
   }    
int fd=open(MY_FIFO,O_RDONLY);    
   if(fd<0)    
   {    
     perror("open");    
     return 2;    
   }    
   while(1)    
   {    
     char buffer[64]={0};    
     ssize_t s=read(fd,buffer,sizeof(buffer)-1);    
     if(s>0)    
     {    
       buffer[s]=0;    
       printf("client say# %s\n",buffer);    
     }    
     else if(s==0)    
     {    
       printf("client quit!\n");    
       break;    
     }    
     else     
     {    
       perror("read");    
       break;    
     }    
   close(fd);
   return 0;
}                                                   

(3)client.c

#include"head.h"    
#include<string.h>    
int main()    
{    
  int fd=open(MY_FIFO,O_WRONLY);    
  if(fd<0)    
  {    
    perror("open");    
    return 1;    
  }    
  while(1)    
  {    
    printf("请输入:");    
    fflush(stdout);    
    char buffer[64]={0};    
    ssize_t s=read(0,buffer,sizeof(buffer)-1);//从命令行读入数据    
    if(s>0)    
    {    
      buffer[s-1]=0;//使用0将读入的回车覆盖    
      printf("%s\n",buffer);    
      write(fd,buffer,strlen(buffer));    
    }    
  }    
  close(fd);                                                              
}    

可以看到,当建立完管道文件之后,读写方式与文件的读写是一致的。
此时我们就可以完成进程之间的通信了:
在这里插入图片描述

4.进程间通信的目的

其实进程间通信的目的不仅仅只有传输数据,我们还可以通过进程间的通信来控制进程。
我们可以通过client端输入的数据来令客户端执行一些操作:
对客户端代码进行一下修改:

       if(strcmp(buffer,"show")==0)    
       {    
         if(fork()==0)    
         {    
         execl("/usr/bin/ls","ls","-l",NULL);    
         exit(-1);    
         }    
         waitpid(-1,NULL,0);    
       }    
       else if(strcmp(buffer,"run")==0)    
       {    
         if(fork()==0)    
         {    
           execl("/usr/bin/sl","sl",NULL);                                
           exit(-2);    
         }    
         waitpid(-1,NULL,0);    
       }    

此时我们通过client来控制server来执行相应的程序了:
在这里插入图片描述

5.命名管道通信的特点

当server端接收数据慢的时候,数据会暂存到管道中,但是管道大小为0。数据没有刷新到磁盘上:
我们可以再复制一个会话进行查看管道的大小:
在server中sleep(50)再接收数据:
在这里插入图片描述

五、总结

进程之间的通信必要的前提是两个进程需要看到同一块资源,并且这块资源不属于任何一个进程而是由操作系统来提供的。
当使用管道来进行通信的时候,这块资源表现为一个文件,可以是匿名文件(匿名管道通信),也可以是命名文件(命名管道通信),该文件的特点是在其中的数据不会被刷新到磁盘上。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。