进程之间通信的方式有很多种,主要包括

  1. 管道
  2. 命名管道
  3. 信号
  4. 消息队列
  5. 共享内存
  6. 信号量
  7. 套接字

其中,管道是最早的一种进程间通信机制,主要适用于具有亲缘关系之间的进程间通信,比如,父进程与子进程之间,或者同一个父进程的两个子进程之间。同时,管道是一中半双工的通信,数据只能单向流动,从一段写入,另外一段读出。下面通过几个例子来看一下管道如何使用。

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)技术,也就是“写时复制”,就是当子进程对某个变量进行写操作时,才进行复制。这个技术对用户程序是不可见的,因此,在用户程序层面上,认为子进程完全复制父进程的虚拟内存空间是完全没有问题的。