管道内部如何实现-大小,组织方式,环形队列?
一.进程间通信有多种方式,本文主要讲解对管道的理解。管道分为匿名管道和命名管道。
(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
2 #include
3 #include
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
2 #include
3 #include
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
2 #include
3 #include
4 #include
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
2 #include
3 #include
4 #include
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
2 #include
3 #include
4 #include
5 #include
6 #include
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
2 #include
3 #include
4 #include
5 #include
6 #include
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 }
输出结果: