目录
什么是管道
管道是Unix中最古老的进程间通信的形式。
我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”
匿名管道
匿名管道接口
pipe函数可以用来创建匿名管道,从而实现进程间通信,函数如下:
先来理解一下pipe()函数的参数到底是什么意思,调用完pipe函数后将参数打印出来:
输出了"3,4",这是两个小正数,这很容易让我们想到了文件描述符,那么到底是不是呢?我们使用指令进入以该进程号命名的文件夹中:
显然,这两个参数正是文件描述符,那么这两个文件描述符与匿名管道的关系呢?请看下图来理解:
知道了这层关系之后,就可以来用一用这个缓冲区了,如下代码:
代码完成的功能只是在同一个进程中使用管道,没有体现进程间通信,那么想要用匿名管道完成进程间通信,需要怎么做呢?
使用匿名管道通信
通过以上图解分析,要让不同的进程使用匿名管道进行通信,那么这些进程必须拥有匿名管道读写两端的文件描述符,也就是说这些进程有亲缘关系,比如父子进程,满足下图对应的关系:
如下代码中,父进程往管道中写,子进程从管道中读:
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <string.h>
4 int main(){
5 int fd[2];
6 pipe(fd);//创建匿名管道
7 pid_t ret=fork();
8 if(ret<0){
9 return 0;
10 }else if(ret==0){
11 char arr[10]={0};
12 read(fd[0],arr,sizeof(arr)-1);//从匿名管道中读
13 printf("%s\n",arr);
14 while(1){
15 }
16 }else{
17 const char* str="Hello";
18 write(fd[1],str,strlen(str));//往匿名管道中写
19 while(1){
20 }
21 }
22 return 0;
23 }
代码执行后:
成功打印了"Hello",说明子进程读到了管道中的内容,进程间通信成功了,这时我们再来查看父子进程是否都拥有文件描述符呢?如下:
此时,父子进程同时拥有匿名管道两端的文件描述符,进程通信成功。
匿名管道特性
1.管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道。
2.只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
3.一般而言,进程退出,管道释放,所以管道的生命周期随进程。
4.管道大小是64KB,如下代码中一直往管道中写,一次写一个字节,直到将管道写满,可以看到最终打印的数字是65536,也就是65536个字节,等于64KB。
5.管道提供字节流服务,也就是说从读端进行读的时候,是将管道中的内容读走了,且读端可以自定义读多少内容。
6.当读写小于pipe_size时,管道保证原子性。pipe_size是4096字节,通过命令:"ulimit -a"可以查看,原子性的意思就是说:一个操作要么不间断的全部被执行,要么一个也没有执行,不存在中间状态。如下:
7.调用pipe创建出来的读写两端的文件描述符,默认都是阻塞属性。就是说:调用write一直写,读端不去读,写满之后write会阻塞。调用read一直读,管道内部被读完后,read会阻塞。如下代码来验证写阻塞,读阻塞与之相类似。使用命令:"pstack [pid]"可以查看进程当前正在干什么。
fcntl函数
函数接口
fcntl函数可以用来设置管道为非阻塞属性,该函数如下:
来测试下这个函数,用来获取管道读写两端的:
此时输出的0和1就分别代表读写两端文件描述符的属性信息不同。
设置非阻塞属性
1.写不关闭,读设置为非阻塞属性。让子进程来读,看看会发生什么现象,代码如下:
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <fcntl.h>
4 int main(){
5 int fd[2];
6 int ret=pipe(fd);
7 if(ret<0){
8 perror("pipe");
9 return 0;
10 }
11 pid_t pid=fork();
12 if(pid<0){
13 perror("fork");
14 return 0;
15 }else if(pid==0){
16 close(fd[1]);
17 int flag=fcntl(fd[0],F_GETFL);
18 fcntl(fd[0],F_SETFL,flag|O_NONBLOCK);
19 char str[1000]={0};
20 ssize_t size_read=read(fd[0],str,sizeof(str)-1);
21 printf("%ld\n",size_read);
22 perror("read");
23 }else{
24 close(fd[0]);
25 while(1){}
26 }
27 return 0;
28 }
运行后:
此时read的返回值为-1,而errno被设置为EAGAIN,读失败。
2. 写关闭,读设置为非阻塞属性。让子进程来读,上述代码中父进程关闭写端就可以了,直接来看运行结果:
read的返回值为0,读成功了,读到了0个字节。
3.读不关闭,写设置为非阻塞。让子进程来写,观察当把管道写满后的现象,代码如下:
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <fcntl.h>
4 int main(){
5 int fd[2];
6 int ret=pipe(fd);
7 if(ret<0){
8 perror("pipe");
9 return 0;
10 }
11 pid_t pid=fork();
12 if(pid<0){
13 perror("fork");
14 return 0;
15 }else if(pid==0){
16 close(fd[0]);
17 int flag=fcntl(fd[1],F_GETFL);
18 fcntl(fd[1],F_SETFL,flag|O_NONBLOCK);
19 while(1){
20 ssize_t size_write=write(fd[1],"s",1);
21 printf("%ld\n",size_write);
22 perror("write");
23 }
24 }else{
25 close(fd[1]);
26 while(1){}
27 }
28 return 0;
29 }
运行代码后:
此时write返回-1,告诉我们资源不可用,写失败。
4.读关闭,写设置为非阻塞。让子进程来写,在上述代码父进程中将读写都关闭即可,运行结果如下:
好像什么都没有发生?我们使用命令来查看当前进程的状态:
子进程中有死循环,父进程没有退出,子进程不会自动退出的,但是此时的现象表明子进程退出了,为什么呢?
本质原因是没有进程在从管道中读了,写入的内容永远不会被读出来,此时继续写入,管道就会破碎,写端进程就会收到一个SIGPIPE信号,导致写端进程崩溃。
命名管道
创建命名管道
1.mkfifo命令创建
2.mkfifo函数创建
下面来用这个函数创建一个命名管道:
命名管道特性
支持不同的进程进行进程间通信,不依赖亲缘性了。
代码验证
拥有了命名管道后,就可以让不同的进程进行进程间通信,如下代码中,命名管道文件存在于当前代码所在目录的上级目录,用open函数打开后,通过各自的文件描述符就可以进行进程间通信了。
观察现象,成功进行了进程间通信。