进程通信(1)管道
进程之间通信的方式有很多种,主要包括
- 管道
- 命名管道
- 信号
- 消息队列
- 共享内存
- 信号量
- 套接字
其中,管道是最早的一种进程间通信机制,主要适用于具有亲缘关系之间的进程间通信,比如,父进程与子进程之间,或者同一个父进程的两个子进程之间。同时,管道是一中半双工的通信,数据只能单向流动,从一段写入,另外一段读出。下面通过几个例子来看一下管道如何使用。
1. 函数原型
#include <unistd.h>
int pipe(int fd[2]);
pipe函数创建一个管道,其声明在unistd.h当中,传入参数是一个int[2]数组,返回值如果为0表示,pipe创建成功,同时fd数组中存储两个文件描述符,fd[1]指向管道的写端,fd[0]指向管道的读端;如果小于0,表示创建失败。
2. 第一个例子
下面看一个最简单的例子
#include<unistd.h>
#include<stdio.h>
int main()
{
int n ;
int fd[2];
char buf[1024];
// 创建管道
if (pipe(fd) < 0) {
perror("pipe error");
}
write(fd[1], "hello world\n", 12);
n = read(fd[0], buf, 1024);
printf("%s",buf);
return 0;
}
上面的代码很简单,就是创建了一个管道,然后向写端写入“hello world\n”字符串,然后从读端读出,存储到buf数组中,最后打印到屏幕。这个例子可以用下面的示意图来表示:
管道像一根单向的水管,数据像水一样从一端流入,从另一端流出。管道也是有缓冲空间的,如果一直写入不读取,那么缓冲空间会被占满,再往里面写数据就会失败(就像水管的流出端被关闭,水不能再流入一样),同样的,如果只读不写,那么数据被读完之后,就没有东西可读了,再次读取也会失败。这个例子显然是没什么用途的,但是可以帮助我们理解什么是管道。
2. 第二个例子 进程间通信
再来看第二个例子
#include<unistd.h>
#include<stdio.h>
int main()
{
int n ;
int status;
pid_t pid;
int fd[2];
char buf[1024];
// 创建管道
if (pipe(fd) < 0) {
perror("pipe error");
}
// 创建子进程
if ((pid = fork()) < 0) {
perror("fork error");
} else if (pid == 0) {
// 子进程读取管道
n = read(fd[0], buf, 1024);
printf("%s",buf);
} else {
// 父进程写入管道
write(fd[1], "hello world\n", 12);
// 父进程等待子进程结束
if (wait(&status) < 0) {
perror("wait error");
}
}
return 0;
}
第二个例子比第一个例子稍微复杂了一些。首先,父进程创建了一个管道,然后fork出一个子进程。在子进程中读取管道内容,并打印内容,然后返回结束进程;在父进程中向管道写入字符串,然后等待子进程结束,最后结束进程。这个例子可以用下面的图来表示
执行fork之后,主进程创建了一个子进程,子进程完全复制父进程的虚拟内存空间(这个说法其实不严谨,见后文),同时继承父进程打开的文件等资源,所以两个描述符也被继承下来,两个进程的fd数组具有相同的值,并且指向同样的pipe端口。因此,父子进程之前可以通过pipe实现通信。一般地,我们会在pipe写入端进程关闭读端,在pipe读入端关闭写端。在上面的代码中,加入两行
#include<unistd.h>
#include<stdio.h>
int main()
{
int n ;
int status;
pid_t pid;
int fd[2];
char buf[1024];
// 创建管道
if (pipe(fd) < 0) {
perror("pipe error");
}
// 创建子进程
if ((pid = fork()) < 0) {
perror("fork error");
} else if (pid == 0) {
// 关闭写入端文件描述符
close(fd[1]);
// 子进程读取管道
n = read(fd[0], buf, 1024);
printf("%s",buf);
} else {
// 关闭写入端文件描述符
close(fd[0]);
// 父进程写入管道
write(fd[1], "hello world\n", 12);
// 父进程等待子进程结束
if (wait(&status) < 0) {
perror("wait error");
}
}
return 0;
}
那么进程模型变成这样
3. 第三个例子,管道与重定向
第三个例子,我们把子进程的标准输入重定向到管道的读端口
#include<unistd.h>
#include<stdio.h>
int main()
{
int n ;
int status;
pid_t pid;
int fd[2];
char buf[1024];
if (pipe(fd) < 0) {
perror("pipe error");
}
if ((pid = fork()) < 0) {
perror("fork error");
} else if (pid == 0) {
close(fd[1]);
if (fd[0] != STDIN_FILENO) {
// 把stdin重定向到管道的输入端
if (dup2(fd[0], STDIN_FILENO) != STDIN_FILENO) {
perror("dup2 error");
}
// 重定向成功的话,那么读端就有了两个文件描述符,
//分别是STDIN_FILENO和fd[0],此时可以关闭fd[0],保留STDIN_FILENO即可
close(fd[0]);
}
//此时可以通过STDIN_FILENO读取管道内容
n = read(STDIN_FILENO, buf, 1024);
printf("%s",buf);
} else {
close(fd[0]);
write(fd[1], "hello world\n", 12);
if (wait(&status) < 0) {
perror("wait error");
}
}
return 0;
}
TIPS:
主进程创建了一个子进程,子进程完全复制父进程的虚拟内存空间这个说法其实是不严谨的,父子进程的代码段实际上是共用的,另外完全复制父进程的虚拟内存空间,会造成时间和内存上的浪费,有的时候根本没有必要完全复制,因此出现了cow(copy on write)技术,也就是“写时复制”,就是当子进程对某个变量进行写操作时,才进行复制。这个技术对用户程序是不可见的,因此,在用户程序层面上,认为子进程完全复制父进程的虚拟内存空间是完全没有问题的。