前言
每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程A把数据从用户空间拷到内核缓冲区,进程B再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcess Communication)。
一、概念
管道是 Unix 中最古老的进程间通信方式,其实质上就是在内核开辟出一块缓冲区,每个进程都和这块缓冲区建立起通道,从而让不同进程之间形成一条数据流,而这样的数据流就被称为管道。
二、特点
-
管道是针对于本地计算机的两个进程之间的通信而设计的通信方法,建立管道后,实际获得两个文件描述符,一个用于写入,而另一个用于读取;
-
由于管道是半双工的,所以它只允许单向通信,若要双方同时进行通信,则需要建立起两个管道;
-
管道是面向字节流的,它提供流式服务;
-
一般而言,内核会对管道操作进行同步与互斥(前提是数据量不大于PIPE_BUF);
同步机制:对临界资源访问的可控时序性
互斥机制:对临界资源访问的唯一性 -
一般而言,进程退出,管道释放,所以管道的生命周期随进程。
三、分类
管道有两种类型,一种叫做匿名管道,另一种叫做命名管道,而我们经常提到的管道其实指的是匿名管道,它们两者具体的区别如下:
类别 | 区别 |
---|---|
匿名管道 | 1. 由 pipe 函数创建并打开2. 实质上是内核中的一块缓冲区,拥有读端和写端 3. 用于具有共同祖先(具有亲缘关系)的进程间通信 |
命名管道 | 1. 由 mkfifo 函数创建,而由 open 函数打开2. 实质上是在文件系统中以一个特殊的设备文件存在,即管道文件(FIFO) 3. 用于任意两个或多个进程间通信 |
四、匿名管道
1. 创建
- 头文件:
#include <unistd.h>
- 函数原型:
int pipe(int pipefd[2]);
- 功能:创建一个匿名管道
- 参数:
pipefd
指的是一个文件描述符数组,其中pipefd[0]
表示读端,pipefd[1]
表示写端- 返回值:成功返回0;否则将返回-1,并将errno设置为错误标识符
2. 通信过程
当创建好了一个管道后,进程间又是如何通过这个管道进行通信的呢?在这里,我们通过下面这个示例来进行演示。
【示例】:
创建一个匿名管道,子进程向管道中写入数据,父进程从管道中读取数据,从而让父子进程间进行数据通信。
【代码】:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main() {
int pipefd[2];
int fd = pipe(pipefd); //创建匿名管道
if(fd < 0) {
perror("pipe error");
return -1;
}
int pid = fork(); //创建子进程
if(pid < 0) {
perror("fork error");
return -1;
}
else if(pid == 0) { //子进程向管道中写入数据
close(pipefd[0]); //关闭读端
write(pipefd[1], "Hello pipe", 10);
printf("Child process write:Hello pipe\n");
close(pipefd[1]);
}
else { //父进程从管道中读取数据
close(pipefd[1]); //关闭写端
char buf[20] = {0};
read(pipefd[0], buf, 20);
printf("Parent process read:%s\n", buf);
}
return 0;
}
【执行结果】:
【分析】:
- 父进程首先调用
pipe
创建管道,得到两个文件描述符分别指向管道的读端和写端;- 接着父进程调用
fork
创建子进程,由于子进程继承了父进程中的管道文件描述符,那么子进程也会得到两个文件描述符并指向与父进程相同的管道;- 父进程关闭管道写端,子进程关闭管道读端。子进程向管道写入数据,父进程从管道读取数据,从而在父子进程之间形成一条数据流,这就实现了进程间通信。
3. 模拟实现 ls | wc -w
在实现之前,我们先来理解一下这条命令的作用。
我们都知道 ls
这个命令是将当前目录下的内容输出到标准输出设备上,wc -w
这个命令是从标准输入设备读取数据,然后计算这些数据的字数并输出,而位于两条命令之间的 |
,它叫做管道符,其作用是将它左边命令的输出作为右边命令的输入。所以,对于 ls | wc -w
这条命令,它的具体含义就是计算当前目录下所有内容的字数。接下来,我们再进一步实现它。
【示例】:利用 pipe
和 dup2
函数模拟实现命令行 ls | wc -w
。
【代码】:
#include <stdio.h>
#include <unistd.h>
int main() {
int pipefd[2];
if(pipe(pipefd) < 0) { //创建管道
perror("pipe error");
return -1;
}
int pid = fork(); //创建子进程
if(pid < 0) {
perror("fork error");
return -1;
}
else if(pid == 0) { //子进程执行ls命令,将结果写入到管道
close(pipefd[0]);
dup2(pipefd[1], STDOUT_FILENO); //将标准输出重定向到管道写端
close(pipefd[1]);
execlp("ls", "ls", NULL);
}
else { //父进程从管道读取数据,执行wc命令
close(pipefd[1]);
dup2(pipefd[0], STDIN_FILENO); //将标准输入重定向到管道读端
close(pipefd[0]);
execlp("wc", "wc", "-w", NULL);
}
return 0;
}
【执行结果】:
【分析】:
- 子进程执行
ls
命令,将结果输出到标准输出设备上,但是在这里我们将标准输出重定向到了管道写端 ,所以执行 ls 命令后的结果被写入到了管道里;- 父进程执行
wc -w
命令,从标准输入设备上读取数据,但是在这里我们又将标准输入重定向到了管道读端,所以 wc -w 会从管道中读取数据,之后再将计算后的字数显示到标准输出设备上;- 在这段代码中,因为默认是阻塞I/O操作,所以即使父进程先被调度,wc -w 也会 read 阻塞,直到管道被子进程 ls 写入了数据。
4. 读写规则
点击标题查看
注:命名管道与匿名管道的读写规则是一样的
五、命名管道
1. 介绍
前面讲过的匿名管道只能用于具有共同祖先(具有亲缘关系)的进程间通信,但若我们想让任意两个或多个进程间通信又该怎么办呢?
在解决这个问题之前,让我们先来想想为什么匿名管道只能用于具有共同祖先的进程间通信?
我们知道匿名管道是通过 pipe
函数来创建的,本质上,它创建的是两个文件描述符,一个读端,一个写端。倘若想要和创建该匿名管道的进程通信,就必须获得这两个文件描述符,但是不同进程是无法直接获得的,这也就是为什么匿名管道不能用于任意进程间通信的原因。这难道就没有办法了吗?
其实不然,当一个进程创建了一个子进程,那么这个子进程就会继承父进程那里的文件描述符,这样父子进程就会共享该匿名管道,从而可以进行数据通信。而且也只有这一种办法,所以匿名管道只能用于具有共同祖先的进程间通信。
接下来,我们再来解决如何让任意两个或多个进程间通信的问题。
为了能让任意几个进程间进行通信,我们必须提供一条能让这些进程都能访问到的通路。在内核中,由于文件系统中的路径名是全局的,那么每个进程都可以访问到,因此我们可以用文件系统中的路径名来标识一个进程通信通道,这样就能让任意几个进程进行通信。
实际上,上面所提到的一条由全路径名构成的通道其实就是命名管道的由来,它在文件系统中以一种特殊的设备文件形式存在,即管道文件(FIFO),由于它的限制小,所以使用范围也相对广一些。
2. 创建
【方式一】:在命令行上创建
mkfifo filename
【方式二】:在程序中创建
- 头文件:
#include <sys/types.h>
和#include <sys/stat.h>
- 函数原型:
int mkfifo(const char *pathname, mode_t mode);
- 功能:创建一个命名管道,本质是一个管道文件
- 参数:
pathname
为创建的管道文件的路径名,mode
为该管道文件的存储权限- 返回值:成功返回0;否则将返回-1,并将errno设置为错误标识符
3. 应用
【示例一】:通过建立命名管道实现文件拷贝
【代码】:
/*
* write.c
* 将源文件source.txt的内容写入到管道中
*/
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main() {
int ret = mkfifo("fifo", 0664); //创建命名管道
if(ret < 0) {
perror("mkfifo error");
return -1;
}
int in_fd = open("source.txt", O_RDONLY); //打开源文件
if(in_fd < 0) {
perror("open source.txt error");
return -1;
}
int out_fd = open("fifo", O_WRONLY); //打开命名管道
if(out_fd < 0) {
perror("open fifo error");
return -1;
}
char buf[1024] = {0};
int len = 0;
while((len = read(in_fd, buf, 1024)) > 0) { //将源文件中的数据写入到命名管道中去
write(out_fd, buf, len);
}
close(in_fd);
close(out_fd);
return 0;
}
/*
* read.c
* 将管道内容写入到目标文件object.txt中
*/
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main() {
int in_fd = open("fifo", O_RDONLY); //打开命名管道
if(in_fd < 0) {
perror("open fifo error");
return -1;
}
int out_fd = open("object.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644); //打开目标文件
if(out_fd < 0) {
perror("open object.txt error");
return -1;
}
char buf[1024] = {0};
int len = 0;
while((len = read(in_fd, buf, 1024)) > 0) { //将命名管道中的数据写入到目标文件中去
write(out_fd, buf, len);
}
close(in_fd);
close(out_fd);
unlink("fifo");
return 0;
}
【执行结果】:
【示例二】:通过建立命名管道实现服务端和客户端通信
【代码】:
/*
* server.c
* 服务端从管道中读取客户端写入的数据
*/
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main() {
if(mkfifo("fifo", 0664) < 0) { //创建命名管道
perror("mkfifo error");
return -1;
}
int fd = open("fifo", O_RDONLY); //打开命名管道
if(fd < 0) {
perror("open error");
return -1;
}
printf("Start reading data from the pipeline...\n");
while(1) {
char buf[1024] = {0};
printf("please wait...\n");
int ret = read(fd, buf, 1023); //从管道读取数据
if(ret > 0) {
buf[ret-1] = 0;
printf("client says: %s\n", buf);
}
else if(ret == 0) {
printf("client closed!\n");
return -1;
}
else {
perror("read error");
return -1;
}
}
close(fd);
return 0;
}
/*
* client.c
* 客户端向管道中写入数据
*/
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <fcntl.h>
int main() {
int fd=open("fifo", O_WRONLY); //打开命名管道
if(fd < 0) {
perror("open error");
return -1;
}
printf("Start writing data into the pipeline...\n");
while(1) {
char buf[1024] = {0};
printf("please enter: ");
fflush(stdout);
int ret = read(0, buf, 1023); //从标准输入接收数据
if(ret > 0) {
buf[ret] = 0;
write(fd, buf, strlen(buf)); //向管道写入数据
}
else {
perror("enter error");
return -1;
}
}
close(fd);
return 0;
}
【执行结果】:
4. 打开规则
点击标题查看