OS: Process: IPC: Pipe

 19th November 2020 at 6:27pm

管道:进程间双向通信的渠道。最早的 IPC 机制之一。

设计考虑

  • 支持双向通信还是单向?POSIX 普通管道一般是单向的
  • 如果支持双向,是半双工(即同时只能有一方在写)还是全双工(双方能同时写)?POSIX 管道是半双工
  • 进程间是否需要有一层关系(比如父子关系)?POSIX 普通管道一般是父子关系(否则没有意义);命名管道则不需要
  • 管道能否通过网络来通讯,还是需要通讯双方进程在同一机器?POSIX 管道需要在同一机器

POSIX 普通管道

POSIX 普通管道(也叫匿名管道)是两种机制的结合:

  • 管道本身,即是两个文件描述符 fds[],使得 write(fd[1], ...) 时,内容可以通过 read(fd[0], ...) 得到
  • 由于 fork() 出来的子进程会继承文件描述符,因此可以实现父进程 write() 而子进程 read(),或者反过来子进程写父进程读

虽然理论上普通管理可以是双向的,但双向会导致混乱(比如进程读到自己写的内容),所以实践上都是父子双方约定好谁写谁读,然后相应的在各自进程中将不需要使用的 fd 关掉。

代码实例
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>

#define BUFFER_SIZE 25
#define READ_END 0
#define WRITE_END 1

int main(void) {
  char write_msg[BUFFER_SIZE] = "Greetings";
  char read_msg[BUFFER_SIZE];
  int fd[2];
  pid_t pid;

  /* create the pipe */
  if (pipe(fd) == -1) {
    fprintf(stderr, "Pipe failed");
    return 1;
  }

  /* fork a child process */
  pid = fork();
  if (pid < 0) { /* error occurred */
    fprintf(stderr, "Fork Failed");
    return 1;
  }

  if (pid > 0) { /* parent process */
    /* close the unused end of the pipe */
    close(fd[READ_END]);
    /* write to the pipe */
    write(fd[WRITE_END], write_msg, strlen(write_msg) + 1);
    /* close the write end of the pipe */
    close(fd[WRITE_END]);
  } else { /* child process */
    /* close the unused end of the pipe */
    close(fd[WRITE_END]);
    /* read from the pipe */
    read(fd[READ_END], read_msg, BUFFER_SIZE);
    printf("read %s", read_msg);

    /* close the read end of the pipe */
    close(fd[READ_END]);
  }

  return 0;
}

原子操作:POSIX 规定 512 字节内的读写是原子的;Linux 把这个限制扩大到 4096。

POSIX 命名管道

POSIX 称它的命名管道为 FIFO(First In First Out)。与普通管道不同之处在于

  • FIFO 是个磁盘上的实际的文件(通过 mkfifo() 创建)
  • 进程间不需要是父子关系也可以互相通信
  • 即使使用它的进程退出了,FIFO 文件仍然存在并且可以被访问

FIFO 虽然可以是双向的(但仍是半双工),但实际用的时候还是建两个 FIFO 用于双向读写(不然就比较混乱,读到自己写的内容)。一个 FIFO 可以被多个进程写以及读。

参考