管道内部如何实现-大小,组织方式,环形队列?
一.进程间通信有多种方式,本文主要讲解对管道的理解。管道分为匿名管道和命名管道。
(1)管道( pipe ):又称匿名管道。是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
(2)命名管道 (named pipe或FIFO) : 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
二.管道
1. 管道的特点:
(1)管道是半双工的,数据只能向一个方向流动;双方通信时,需要建立起两个管道;
(2)只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程);
(3)单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在与内存中。
(4)数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。
2.通信步骤
2.1创建管道
利用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 }
运行结果:
解释:因为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 }
运行结果:
三.命名管道
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)
命名管道创建后就可以使用了,命名管道和管道的使用方法基本是相同的。只是使用命名管道时,必须先调用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 }
输出结果: