通过管道来实现进程间的通信的方法很经典,因为多个进程共享3-4G中的内核,所以在内核中存在一个管道(缓冲区),然后进程通过连接管道的两端从而实现通信。假如说我们现在有一根管道,我们从左端放入一个小球,那么它会从右端滚出来,那么如果我们同时向两端都放入一个小球,那么就不可能实现交叉传递了,所以管道是半双工通信(即双方都可以发送信息,但是双方不能同时发送信息),因此管道的两端一端是读端,一端是写端。那么要实现两个进程的同时读写操作,就需要用两个管道。
pipe
首先先来说一下pipe,这是一个匿名管道(为啥叫匿名呢,下面讲命名管道的时候就知道了),实现方式是循环队列,它只能用于有血缘关系的进程间通信。首先我们先来看一下pipe函数的原型:
#include <unistd.h>
int pipe(int pipefd[2]);
复制
传入的参数是一个大小为2的数组,然后就得到了两个文件描述符pipefd[0]和pipefd[1],前者用来指向管道的读端,后者用来指向写端。用一个父子进程来举例,如果要实现父子进程间的通信,在fork前就需要创建一个pipe管道,如果创建成功返回0,如果失败返回-1并设置errno,由于子进程复制了父进程的PCB,所以子进程也有父进程的文件描述符表,因此父子进程的pipefd都指向了同一个pipe管道,然后我们要规定管道的传输方向,如果我们要求父写子读的话,我们就在父进程中close(pipefd[0]),在子进程中close(pipefd[1])就好了,创建好管道后我们通过write和read函数进行读写操作。
那么在使用pipe通信的时候可能会遇到以下的几种情况:
1. 当读管道时,如果管道中没有数据,则会阻塞,直到管道另一端写入数据。
2. 当写管道时,如果管道中已经满了,则会阻塞,直到管道另一端读出数据(可见读出数据时,管道中将不会保留该数据)。
3. 当管道写端关闭时,读端读完管道内的数据时,如果再次去读没有数据的管道会返回0,相当于读到了EOF。
4. 当管道读端关闭时,如果写端在写入数据时,产生SIGPIPE信号,写进程默认情况下会终止进程。
下面以父子进程的例子,来写一个程序来实现一下,代码如下:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
char buf[1024] = "Hello Child\n";
char str[1024];
int fd[2];
if(pipe(fd) == -1){
perror("pipe");
exit(1);
}
pid_t pid = fork();
// 父写子读 0写端 1读端
if(pid > 0){
printf("parent pid\n");
close(fd[0]); // 关闭读端
sleep(5);
write(fd[1], buf, strlen(buf)); // 在写端写入buf中的数据
wait(NULL);
close(fd[1]);
}
else if(pid == 0){
close(fd[1]); // 关闭写端
int len = read(fd[0], str, sizeof(str)); // 在读端将数据读到str
write(STDOUT_FILENO, str, len);
close(fd[0]);
}
else {
perror("fork");
exit(1);
}
return 0;
}
复制
运行结果如下:
parent pid
Hello Child
复制
我们可以用fpathconf来查看管道的缓冲区大小,通过传入文件描述符和_PC_PIPE_BUF两个参数来获得一个long整型的值。代码如下:
printf("%ld\n", fpathconf(fd[0], _PC_PIPE_BUF));
复制
还有就是我们可以通过设置O_NONBLOCK参数来实现非阻塞的情况,也就是说当一个进程还没有写数据时,另一个读进程就会阻塞在那里,那么如果设置了O_NONBLOCK参数,该进程就不会阻塞在那里,会返回-1,并设置errno为EAGAIN,可以用goto语句或者while循环实现,那么设置O_NONBLOCK使用fcntl函数。
fifo
fifo用来创建一个命名管道,可以解决非血缘关系的进程间通信,它的底层的实现原理和匿名管道相同,只不过是生成了一个可见的管道文件。管道文件用mkfifo命令来创建,如下图所示:

这个管道文件连接一个在内核中的管道,那么这个管道文件对于所有的进程都是可见的,那么进程通过打开这个管道文件就可以通过管道文件所连接的管道来实现非血缘关系的进程间通信了。因为这个管道有一个所有进程都可以访问到的管道文件,所以fifo叫做命名管道,那么同理,pipe就只能通过fork的方式来复制文件描述符表来共享管道,而其他的进程却访问不到,所以叫做匿名管道。
下面也通过代码,来简单的实现一下fifo的效果,这里我提前用mkfifo的命令来创建了一个管道文件,当然也可以在代码中使用mkfifo函数来创建,先来看一下写操作的进程的代码:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc,char *argv[])
{
if(argc < 2){
printf("run error\n");
exit(1);
}
char buf[1024] = "Hello, I'm Charles\n";
int fd = open(argv[1], O_WRONLY);
if(fd == -1){
perror("open file");
exit(1);
}
write(fd, buf, strlen(buf));
close(fd);
return 0;
}
复制
下面是执行读操作的代码:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
int main(int argc, char *argv[])
{
if(argc < 2){
printf("run error\n");
exit(1);
}
char buf[1024];
int fd = open(argv[1], O_RDONLY);
int len = read(fd, buf, sizeof(buf));
write(STDOUT_FILENO, buf, len);
close(fd);
return 0;
}
复制
然后我们开两个终端去分别运行这两个程序,我们先运行写操作的程序,然后再运行读操作的程序(当你运行写操作的程序时会阻塞在write,当运行读操作的程序时才会往下执行,这个就是上面所说的四种情况中的一种),最终结果如下: