介绍
管道本质上就是一个文件,前面的进程以写方式打开文件,后面的进程以读方式打开。这样前面写完后面读,于是就实现了通信。虽然实现形态上是文件,但是管道本身并不占用磁盘或者其他外部存储的空间。在Linux的实现上,它占用的是内存空间。所以,Linux上的管道就是一个操作方式为文件的内存缓冲区。管道分类:匿名管道、命名管道
命令行中使用
1、mkfifo或mknod命令来创建一个命名管道
[root@VM-90-225-centos ~]# mkfifo pipe
[root@VM-90-225-centos ~]# ls -l pipe
prw-r--r-- 1 root root 0 Feb 28 21:02 pipe
我们现在让一个进程写这个管道文件:
echo 12345 > pipe
此时这个写操作会阻塞,因为管道另一端没有人读。此时如果有进程读这个管道,那么这个写操作的阻塞才会解除:
[root@VM-90-225-centos ~]# cat pipe
12345
当我们cat完这个文件之后,另一端的echo命令也返回了.
Linux系统无论对于命名管道和匿名管道,底层都用的是同一种文件系统的操作行为,这种文件系统叫pipefs,可以通过下面命令查看是否具有这个系统:
[root@VM-90-225-centos ~]# cat /proc/filesystems |grep pipefs
nodev pipefs
nodev rpc_pipefs
系统编程中使用
匿名管道和命名管道分别叫做PIPE和FIFO,创建匿名管道的系统调用是pipe(),而创建命名管道的函数是mkfifo()。
匿名管道:
#include <unistd.h>
int pipe(int pipefd[2]);
这个方法将会创建出两个文件描述符:
pipefd[0]是读方式打开,作为管道的读描述符。pipefd[1]是写方式打开,作为管道的写描述符。从管道写端写入的数据会被内核缓存直到有人从另一端读取为止。
pipe示例一:简单的写入+读出
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>#define STRING "hello world!"int main()
{int pipefd[2];char buf[BUFSIZ];// 创建一组管道if (pipe(pipefd) == -1) {perror("pipe()");exit(1);}// 从[1]写入STRINGif (write(pipefd[1], STRING, strlen(STRING)) < 0) {perror("write()");exit(1);}// 从[0]读出,结果存到bufif (read(pipefd[0], buf, BUFSIZ) < 0) {perror("write()");exit(1);}// 打印读出来的结果printf("%s\n", buf);exit(0);
}
程序创建了一个管道,并且对管道写了一个字符串之后从管道读取,并打印在标准输出上。
当然这不属于进程间通信,实际情况中我们不会在单个进程中使用管道。
进程在pipe创建完管道之后,往往都要fork产生子进程。下面的demo是父子两个进程使用一个管道可以完成半双工通信。此时,父进程可以通过fd[1]给子进程发消息,子进程通过fd[0]读。子进程也可以通过fd[1]给父进程发消息,父进程用fd[0]读。
pipe示例二:父子进程半双工通信
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>#define STRING "hello world!"int main()
{int pipefd[2];pid_t pid;char buf[BUFSIZ];// 创建管道if (pipe(pipefd) == -1) {perror("pipe()");exit(1);}// fork产生子进程pid = fork();if (pid == -1) {perror("fork()");exit(1);}// fork()新进程返回0,旧进程返回新进程的进程ID。if (pid == 0) {/* this is child. */printf("Child pid is: %d\n", getpid());// 子进程会继承父进程对应的文件描述符// 父进程先pipe创建管道之后,子进程也会得到同一个管道的读写文件描述符if (read(pipefd[0], buf, BUFSIZ) < 0) {perror("write()");exit(1);}printf("%s\n", buf);// 清空bufbzero(buf, BUFSIZ);snprintf(buf, BUFSIZ, "Message from child: My pid is: %d", getpid());// 把读取到的数据重新发送给主进程if (write(pipefd[1], buf, strlen(buf)) < 0) {perror("write()");exit(1);}} else {/* this is parent */printf("Parent pid is: %d\n", getpid());snprintf(buf, BUFSIZ, "Message from parent: My pid is: %d", getpid());// 父进程写数据到管道if (write(pipefd[1], buf, strlen(buf)) < 0) {perror("write()");exit(1);}// 等待1ssleep(1);// 清空bufbzero(buf, BUFSIZ);// 读取管道消息到bufif (read(pipefd[0], buf, BUFSIZ) < 0) {perror("write()");exit(1);}printf("%s\n", buf);wait(NULL);}exit(0);
}
打印结果:
Parent pid is: 17697
Child pid is: 17702
Message from parent: My pid is: 17697
Message from child: My pid is: 17702
如果在vscode中debug的话是debug不到if (pid == 0) 的分支的,你只能debug主进程的流程。
从以上的demo中用同一个管道的父子进程可以分时给对方发送消息,我们可以看到对管道读写的一些特点:
1、在管道中没有数据的情况下,对管道的读操作会阻塞,直到管道内有数据为止。
2、当一次写的数据量不超过管道容量的时候,对管道的写操作一般不会阻塞,直接将要写的数据写入管道缓冲区即可。
管道实际上就是内核控制的一个内存缓冲区,既然是缓冲区,就有容量上限。我们把管道一次最多可以缓存的数据量大小叫做PIPESIZE。内核在处理管道数据的时候,底层也要调用类似read和write这样的方法进行数据拷贝,这种内核操作每次可以操作的数据量也是有限的,一般的操作长度为一个page,即默认为4k字节。我们把每次可以操作的数据量长度叫做PIPEBUF。POSIX标准中,对PIPEBUF有长度限制,要求其最小长度不得低于512字节。PIPEBUF的作用是,内核在处理管道的时候,如果每次读写操作的数据长度不大于PIPEBUF时,保证其操作是原子的。而PIPESIZE的影响是,大于其长度的写操作会被阻塞,直到当前管道中的数据被读取为止。
在Linux 2.6.11之前,PIPESIZE和PIPEBUF实际上是一样的。在这之后,Linux重新实现了一个管道缓存,并将它与写操作的PIPEBUF实现成了不同的概念,形成了一个默认长度为65536字节的PIPESIZE,而PIPEBUF只影响相关读写操作的原子性。从Linux 2.6.35之后,在fcntl系统调用方法中实现了F_GETPIPE_SZ和F_SETPIPE_SZ操作,来分别查看当前管道容量和设置管道容量。管道容量容量上限可以在/proc/sys/fs/pipe-max-size进行设置。
在实际情境下,半双工管道管道的两端都可能有多个进程进行读写处理。如果再加上线程,则事情可能变得更复杂。实际上,我们在使用管道的时候,并不推荐这样来用。管道推荐的使用方法是其单工模式:即只有两个进程通信,一个进程只写管道,另一个进程只读管道。
pipe示例三:父子进程单工通信
这个程序实际上比上一个要简单,父进程关闭管道的读端,只写管道。子进程关闭管道的写端,只读管道。
此时两个进程就只用管道实现了一个单工通信,并且这种状态下不用考虑多个进程同时对管道写产生的数据交叉的问题,这是最经典的管道打开方式,也是我们推荐的管道使用方式。
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>#define STRING "hello world!"int main()
{int pipefd[2];pid_t pid;char buf[BUFSIZ];if (pipe(pipefd) == -1) {perror("pipe()");exit(1);}pid = fork();if (pid == -1) {perror("fork()");exit(1);}if (pid == 0) {/* this is child. */close(pipefd[1]);printf("Child pid is: %d\n", getpid());// 读if (read(pipefd[0], buf, BUFSIZ) < 0) {perror("write()");exit(1);}printf("%s\n", buf);} else {/* this is parent */close(pipefd[0]);printf("Parent pid is: %d\n", getpid());snprintf(buf, BUFSIZ, "Message from parent: My pid is: %d", getpid());// 写if (write(pipefd[1], buf, strlen(buf)) < 0) {perror("write()");exit(1);}wait(NULL);}exit(0);
}
命名管道
命名管道在底层的实现跟匿名管道完全一致,区别只是命名管道会有一个全局可见的文件名以供别人open打开使用。再程序中创建一个命名管道文件的方法有两种,一种是使用mkfifo
函数。另一种是使用mknod
系统调用
fifo示例:
client端
/* 这是一个命名管道的实现demo,实现两个进程间聊天功能* */
#include<stdio.h>
#include<unistd.h>
#include<sys/stat.h>
#include<errno.h>
#include<fcntl.h>
#include<string.h>int main()
{char *file = "./test.fifo";umask(0); //设置umask,仅在当前进程有效。if(mkfifo(file,0663)<0){if(errno == EEXIST){printf("fifo exist\n");}else {perror("mkfifo\n");return -1;}}int fd = open(file,O_WRONLY);if(fd<0){perror("open error");return -1;}printf("open fifo success!!!\n");while(1){printf("input: ");fflush(stdout);char buff[1024]={0};scanf("%s",buff);write(fd,buff,strlen(buff));}return 0;
}
server端:
#include<stdio.h>
#include<unistd.h>
#include<sys/stat.h>
#include<errno.h>
#include<fcntl.h>
#include<string.h>int main()
{char *file = "./test.fifo";umask(0); //设置umask,仅在当前进程有效。if(mkfifo(file,0663)<0){if(errno == EEXIST){printf("fifo exist\n");}else {perror("mkfifo\n");return -1;}}int fd = open(file,O_RDONLY);if(fd<0){perror("open error");return -1;}printf("open fifo success!!!\n");while(1){char buff[1024] = {0};int ret = read(fd,buff,1024);if(ret>0){printf("peer say:%s\n",buff);}}return 0;
}
然后在子目录下编译:
g++ ./server.cpp -o server
g++ ./client.cpp -o client
然后在两个终端页面上分别运行:
./server
./client
即可进行单向通信,一般工程中两个进程建立两个fifo就可以进行双向通信了