管道内部如何实现-大小,组织方式,环形队列?


一.进程间通信有多种方式,本文主要讲解对管道的理解。管道分为匿名管道和命名管道。

 (1)管道( pipe ):又称匿名管道。是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
 (2)命名管道 (named pipe或FIFO) : 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。


二.管道   

      1. 管道的特点:

(1)管道是半双工的,数据只能向一个方向流动;双方通信时,需要建立起两个管道;

(2)只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程);

(3)单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在与内存中。

(4)数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。


  2.通信步骤

   2.1创建管道

26833883_133829008404bu.png

     利用pipe()函数创建管道,即在内核中开辟一块缓冲区(称为管道)用于通信。pipe函数调用成功返回0,调用失败返回-1。

     管道是基于文件描述符的通信方式。当一个管道建立时,它会创建两个文件描述符fd[0]和fd[1]。其中fd[0]固定用于读管道,而fd[1]固定用于写管道,一般文件I/O的函数都可以用来操作管道(lseek除外)。管道的读写规则如下:


从管道中读取数据:

  • 如果管道的写端不存在,则认为已经读到了数据的末尾,读函数返回的读出字节数为0;

  • 当管道的写端存在时,如果请求的字节数目大于PIPE_BUF,则返回管道中现有的数据字节数,如果请求的字节数目小于PIPE_BUF,返回请求的字节数(此时,管道中数据量大于请求的数据量)。注:(PIPE_BUF在include/linux/limits.h中定义,不同的内核版本可能会有所不同。Posix.1要求PIPE_BUF至少为512字节,red hat 7.2中为4096)。


向管道中写入数据:

  • 向管道中写入数据时,linux将不保证写入的原子性,管道缓冲区一有空闲区域,写进程就会试图向管道写入数据。如果读进程不读走管道缓冲区中的数据,那么写操作将一直阻塞。 
    注:只有在管道的读端存在时,向管道中写入数据才有意义。否则,向管道中写入数据的进程将收到内核传来的SIFPIPE信号,应用程序可以处理该信号,也可以忽略(默认动作则是应用程序终止)。



      2.2创建子进程 

   单独创建一个无名管道,并没有实际的意义。我们再fork一个子进程,然后通过管道实现父子进程间的通信(即两个进程中存在亲缘关系,这里的亲缘关系指的是具有共同的祖先)。 

 

  2.3父进程关闭管道读端,子进程关闭管道写端


  3.代码验证

   3.1检验管道大小

  1 #include<stdio.h>
  2 #include<stdlib.h>
  3 #include<unistd.h>
  4 int main()
  5 {
  6     int fd[2];
  7     int count=0;
  8     if(pipe(fd)<0)
  9     {
 10         perror("Fail to create pipe");
 11         exit(EXIT_FAILURE);
 12     }
 13     while(1)
 14     {
 15         write(fd[1],"a",sizeof(char));
 16         printf("count=%d.\n",++count);
 17     }
 18     return 0;
 19 }

运行结果:65536=64K


   3.2读写规则探究

     A.从管道中读取数据

    <1>写端不存在时

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<stdlib.h>
  4 int main()
  5 {
  6     int n;
  7     int fd[2];
  8     int count=0;
  9     char buf[100]={0};
 10 
 11     if(pipe(fd)<0)
 12     {
 13         perror("Fail to create pipe");
 14         exit(EXIT_FAILURE);
 15     }
 16     close(fd[1]);
 17 
 18     if((n=read(fd[0],buf,sizeof(buf)))<0)
 19     {
 20         perror("Fail to read pipe");
 21         exit(EXIT_FAILURE);
 22     }
 23     printf("read %d bytes:%s\n",n,buf);
 24     return 0;
 25 }

运行结果:read 0 bytes:

  <2>写端存在时

   父进程向管道中写数据,子进程从管道中读取数据

  1 #include<stdio.h>
  2 #include<errno.h>
  3 #include<stdlib.h>
  4 #include<string.h>
  5 
  6 #define N 10
  7 #define MAX 100
  8 int child_read(int fd)
  9 {
 10     char buf[N];
 11     int n=0;
 12     while(1)
 13     {
 14         n=read(fd,buf,sizeof(buf));
 15         buf[n]='\0';
 16         printf("Read %d bytes:%s\n",n,buf);
 17         if(strncmp(buf,"quit",4)==0)
 18             break;
 19     }
 20     return 0;
 21 }
 22 
 23 int father_write(int fd)
 24 {
 25     char buf[MAX]={0};
 26     while(1)
 27     {
 28         printf(">");
 29         fgets(buf,sizeof(buf),stdin);
 30         buf[strlen(buf)-1]='\0';
 31         write(fd,buf,strlen(buf));
 32         sleep(1);
 33         if(strncmp(buf,"quit",4)==0)
 34             break;
 35     }
 36     return 0;
 37 }
 38 
 39 int main()
 40 {
 41     int fd[2];
 42     if(pipe(fd)<0)
 43     {
 44         perror("Fail to create pipe");
 45         exit(EXIT_FAILURE);
 46     }
 47     pid_t pid=fork();
 48     if(pid<0)
 49     {
 50         perror("Fail to fork");
 51         exit(EXIT_FAILURE);
 52     }
 53     else if(pid==0)
 54     {
 55         close(fd[1]);
 56         child_read(fd[0]);
 57     }
 58     else
 59     {
 60         close(fd[0]);
 61         father_write(fd[1]);
 62     }
 63     exit(EXIT_SUCCESS);
 64     return 0;
 65 
 66 }

运行结果:

wKioL1cMtTTh_4EwAAAipKiShqY918.png

解释:因为buf大小为N=10,后面还加了一个'\0',超出了数组分配的内存空间(这是代码的一个错误),所以这儿的0应为10.


从以上验证我们可以看到:

<1>当写端存在时,管道中没有数据时,读取管道时将阻塞

<2>当读端请求读取的数据大于管道中的数据时,此时读取管道中实际大小的数据

<3>当读端请求读取的数据小于管道中的数据时,此时放回请求读取的大小数据



  B.向管道中写入数据:

  向管道中写入数据时,linux将不保证写入的原子性,管道缓冲区一有空闲区域,写进程就会试图向管道写入数据。当管道满时,读进程不读走管道缓冲区中的数据,那么写操作将一直阻塞。

注意:只有管道的读端存在时,向管道中写入数据才有意义。否则,向管道中写入数据的进程将收到内核传来的SIGPIPE信号,应用程序可以处理该信号,也可以忽略(默认动作则是使应用程序终止)。

  1 #include<stdio.h>
  2 #include<errno.h>
  3 #include<stdlib.h>
  4 #include<string.h>
  5 
  6 int main()
  7 { 
  8     int n=0;
  9     char buf[1000*6]={0};
 10     int fd[2];
 11     if(pipe(fd)<0)
 12     { 
 13         perror("Fail to create pipe");
 14         exit(EXIT_FAILURE);
 15     }
 16     pid_t pid=fork(); 
 17     if(pid<0) 
 18     { 
 19         perror("Fail to fork"); 
 20         exit(EXIT_FAILURE);
 21     }
 22     else if(pid==0) 
 23     { 
 24         close(fd[1]);
 25         sleep(3);
 26         close(fd[0]);
 27         printf("Read port close\n");
 28         sleep(1);
 29     }
 30     else
 31     {
 32         close(fd[0]);
 33         while(1)
 34         {
 35             n=write(fd[1],buf,sizeof(buf));
 36             printf("write %d bytes to pipe\n",n);
 37         }
 38     }
 39     exit(EXIT_SUCCESS);
 40     return 0;
 41 }

运行结果:

wKioL1cMxfeiLsOxAAAqBa8g3WY734.png


三.命名管道

   1. 管道的特点:

(1)FIFO不同于管道之处在于它提供一个路径名与之关联。

(2)以FIFO的文件形式存储于文件系统中。命名管道是一个设备文件,因此,即使进程与创建FIFO的进程不存在亲缘关系,只要可以访问该路径,就能够通过FIFO相互通信。

(3)FIFO(first input first output)总是按照先进先出的原则工作


 2.通信步骤

   2.1创建管道

Linux下有两种方式创建:

(1)在Shell下交互地建立一个命名管道;(用mknod或mkfifo命令,例如:mknod namedpipe)

(2)在程序中使用系统函数建立命名管道。(系统函数有两个:mknod和mkfifo)

  wKioL1cMyYThuC4GAAAgqRbWRw4534.png  

命名管道创建后就可以使用了,命名管道和管道的使用方法基本是相同的。只是使用命名管道时,必须先调用open()将其打开。因为命名管道是一个存在于硬盘上的文件,而管道是存在于内存中的特殊文件。


  3.代码验证

  //写端
  1 #include<stdio.h>
  2 #include<sys/types.h>
  3 #include<sys/stat.h>
  4 #include<unistd.h>
  5 #include<fcntl.h>
  6 #include<string.h>
  7 
  8 
  9 #define _PATH_ "/tmp/file.tmp"
 10 #define _SIZE_ 100
 11 
 12 int main()
 13 {
 14     int ret=mkfifo(_PATH_,0666|S_IFIFO);
 15     if(ret==-1)
 16     {
 17         printf("mkfifo error|n");
 18         return 1;
 19     }
 20     int fd=open(_PATH_,O_WRONLY);
 21     if(fd<0)
 22     {
 23         printf("open file error!\n");
 24     }
 25     char buf[_SIZE_];
 26     memset(buf,'\0',sizeof(buf));
 27     while(1)
 28     {
 29         scanf("%s",&buf);
 30         int ret=write(fd,buf,sizeof(buf));
 31         if(ret<0)
 32         {
 33             printf("write error!\n");
 34             break;
 35         }
 36         if(strncmp(buf,"quit",4)==0)
 37         {
 38             break;
 39         }
 40     }
 41 
 42     close(fd);
 43     return 0;
 44 }
 45

  //读端
  1 #include<stdio.h>
  2 #include<sys/types.h>
  3 #include<sys/stat.h>
  4 #include<unistd.h>
  5 #include<fcntl.h>
  6 #include<string.h>
  7 
  8 #define _PATH_ "/tmp/file.tmp"
  9 #define _SIZE_ 100
 10 
 11 int main()
 12 {
 13     int fd=open(_PATH_,O_RDONLY);
 14     if(fd<0)
 15     {
 16         printf("open file error!\n");
 17         return 1;
 18     }
 19     char buf[_SIZE_];
 20     memset(buf,'\0',sizeof(buf));
 21     while(1)
 22     {
 23         int ret=read(fd,buf,sizeof(buf));
 24         if(ret<=0)
 25         {
 26             printf("read end or error!\n");
 27             break;
 28         }
 29         printf("%s\n",buf);
 30         if(strncmp(buf,"quit",4)==0)
 31         {
 32             break;
 33         }
 34     }
 35 
 36     close(fd);
 37     return 0;
 38 }

输出结果:

wKiom1cM9V_x6iQxAABWanMz8pM893.png