进程间通信

154 阅读4分钟

管道

管道有两种局限性:

  1. 历史上,管道是半双工的(即数据只能在一个方向上流动,现在某些系统提供全双工管道)。
  2. 管道只能在具有公共祖先的两个进程之间使用。(通常一个管道由一个进程创建,在进程调用 fork 之后,这个管道就能在父进程和子进程之间使用)。

管道通过调用 pipe 函数创建:

#include <unistd.h>

int pipe(int fd[2]);

返回值:

  • 成功,返回 0
  • 失败,返回 -1

经由参数 fd 返回两个文件描述符:fd[0]为读而打开,fd[1]为写而打开。fd[1]的输出是fd[0]的输入。

通常,进程会调用 pipe,接着调用 fork,从而创建从父进程到子进程的 IPC 通道,反之亦然。

fork之后的半双工管道

fork之后做什么取决于我们想要的数据流方向。对于从父进程到子进程的管道,父进程关闭管道的读端(fd[0]),子进程关闭写端(fd[1])。

image.png

当管道的一端被关闭后,下列两条规则起作用:

  1. 当读(read)一个写端已被关闭的管道时,在所有数据都被读取后,read 返回 0,表示文件结束。
  2. 如果写(write)一个读端已被关闭的管道,则产生信号SIGPIP。如果忽略该信号或者捕捉该信号并从其处理程序返回,则write返回-1,errno 设置为 EPIPE 。

实例

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void) {
    int n;
    int fd[2];

    pid_t pid;
    char line[2048];

    if (pipe(fd) < 0) {
        printf("pipe error\n");
        exit(-1);
    }
    if ((pid = fork()) < 0) {
        printf("fork error\n");
        exit(-1);
    } else if (pid > 0) {
        // parent
        close(fd[0]);   // close read
        write(fd[1], "hello world\n", 12);
    } else {
        // child
        close(fd[1]);   // close write
        n = read(fd[0], line, 2048);
        write(STDOUT_FILENO, line, n);
    }

    exit(0);
}

上面的例子中,直接对管道描述符调用了 read 和 write,更有趣的是将管道描述符复制到了标准输入或标准输出上。

协同进程

UNIX 系统过滤程序从标准输入读取数据,向标准输出写数据。几个过滤程序通常在 shell 管道中线性连接。当一个过滤程序即产生某个过滤程序的输入,又读取该过滤程序的输出时,它就变成了协同进程(coprocess)。

协同程序通常在 shell 的后台运行,其标准输入和标准输出通过管道连接到另一个程序。

协同进程有连接到另一个进程的两个单向管道:一个接到其标准输入,另一个则来自其标准输出。我们想将数据写到其标准输入,将其处理后,再从其标准输出读取数据。

image.png

实例

该实例是一个简单地协同进程,它从其它标准输入读取两个数,计算它们的和,然后将和写至其标准输出。

// add.c

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

#define MAXLINE 4096

int main(void) {

    int n, int1, int2;
    char line[MAXLINE];

    while ((n = read(STDIN_FILENO, line, MAXLINE)) > 0) {
        line[n] = 0;
        if (sscanf(line, "%d%d", &int1, &int2) == 2) {
            sprintf(line, "%d\n", int1 + int2);
            n = strlen(line);
            if (write(STDOUT_FILENO, line, n) != n) {
                perror("write error");
                exit(1);
            }
        } else {
            if (write(STDOUT_FILENO, "invalid args\n", 13) != 13) {
                perror("write error");
                exit(1);
            }
        }
    }
    exit(0);
}

对此程序进行编译,将其可执行目标代码存入名为 add2 的文件。

下面的程序从其标准输入读取两个数之后调用 add2 协同进程,并将协同进程送来的值写到其标准输出。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

#define MAXLINE 4096

static void sig_pipe(int);

int main(void) {
    int n, fd1[2], fd2[2];

    pid_t pid;
    char line[MAXLINE];

    if (signal(SIGPIPE, sig_pipe) == SIG_ERR) {
        perror("signal error\n");
        exit(1);
    }

    if (pipe(fd1) < 0 || pipe(fd2) < 0) {
        perror("pipe error\n");
        exit(1);
    }

    if ((pid = fork()) < 0) {
        perror("fork error\n");
        exit(1);
    } else if (pid > 0) {
        // parent
        close(fd1[0]); // 关闭 fd1 读(read)
        close(fd2[1]); // 关闭 fd2 写(write)

        while (fgets(line, MAXLINE, stdin) != NULL) {
            n = strlen(line);
            printf("pid %d write to child: %s\n", pid, line);
            if (write(fd1[1], line, n) != n) {
                perror("write error to pipe\n");
                exit(1);
            }
            if ((n = read(fd2[0], line, MAXLINE)) < 0) {
                perror("read error from pipe");
                exit(1);
            }
            if (n == 0) {
                perror("child closed pipe\n");
                break;
            }
            line[n] = 0;
            if (fputs(line, stdout) == EOF) {
                perror("fputs error");
                exit(1);
            }
        }
        if (ferror(stdin)) {
            perror("fgets error on stdin\n");
            exit(1);
        }
        exit(0);
    } else {
        // child
        close(fd1[1]);
        close(fd2[0]);
        if (fd1[0] != STDIN_FILENO) {
            if (dup2(fd1[0], STDIN_FILENO) != STDIN_FILENO) {
                perror("dup2 error to stdin\n");
                exit(1);
            }
            close(fd1[0]);
        }
        if (fd2[1] != STDOUT_FILENO) {
            if (dup2(fd2[1], STDOUT_FILENO) != STDOUT_FILENO) {
                perror("dup2 error to stdout");
                exit(1);
            }
            close(fd2[1]);
        }
        if (execl("./add2", "add2", (char *) 0) < 0) {
            perror("execl error\n");
            exit(1);
        }
    }
    exit(0);
}

static void sig_pipe(int signo) {
    printf("SIGPIPE caught\n");
    exit(1);
}

这个程序创建了两个管道,父进程、子进程各自关闭它们不需使用的管道端,必须使用两个管道:一个用作协同进程的标准输入,另一个则用作它的标准输出。然后,子进程调用 dup2 使管道描述符转移至其标准输入和输出,最后调用 execl 。

参考

《UNIX 环境高级编程》
进程间的通信方式——pipe(管道)
进程间8种通信方式详解
Linux 进程间通信