1. 为什么需要 IPC

进程是程序执行的过程,操作系统会为每个进程分配独立的内存资源与 CPU 资源。Linux 通过虚拟内存和 MMU(内存管理单元)实现了进程之间的隔离:A 进程无法直接访问 B 进程的内存空间,这保证了安全性和权限管理。

但实际开发中,进程与进程之间往往存在数据共享或交换的需求。于是就有了 IPC(Interprocess Communication,进程间通信)。

IPC 的常见种类大致可以分为三类:

  • 古老的通信方式:无名管道、有名管道、信号
  • System V IPC 对象:消息队列、共享内存、信号量集
  • socket 通信:用于网络通信

本文只聚焦其中最基础、也最常用的两种:无名管道和有名管道。

2. 管道的本质

管道的底层实现是队列。它有几个共同特性,理解这些特性对后面写代码非常关键:

  1. 管道是半双工的工作模式,实际编程中通常当作单工使用(一端只读、一端只写)。
  2. 所有管道都是特殊文件,不支持定位操作(lseek 对管道无效)。
  3. 管道读写首选文件 IO(open、read、write、close),也可以使用带缓冲区的标准 IO(fgets、fread、fgetc)。

3. 无名管道(unnamed pipe)

无名管道只能用于有亲缘关系的进程之间通信(典型场景是父子进程),因为它没有文件名,无法被无关进程通过路径打开。

3.1 创建无名管道

#include <unistd.h>

int pipe(int pipefd[2]);
  • 功能:创建并打开一个无名管道
  • 参数:pipefd[0] 是固定的读端,pipefd[1] 是固定的写端
  • 返回值:成功返回 0,失败返回 -1

3.2 编程顺序

创建管道 → fork 出子进程 → 父子进程分别读写管道 → 关闭管道。

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

int main(void)
{
    int fd[2];
    if (pipe(fd) == -1) {
        perror("pipe");
        return -1;
    }

    pid_t pid = fork();
    if (pid == 0) {
        /* 子进程:关闭读端,只写 */
        close(fd[0]);
        const char *msg = "hello from child";
        write(fd[1], msg, strlen(msg));
        close(fd[1]);
    } else if (pid > 0) {
        /* 父进程:关闭写端,只读 */
        close(fd[1]);
        char buf[128] = {0};
        ssize_t n = read(fd[0], buf, sizeof(buf) - 1);
        if (n > 0) {
            printf("parent recv: %s\n", buf);
        }
        close(fd[0]);
    }

    return 0;
}

3.3 管道的四种典型行为

  1. 读端存在,一直写:写入超过 64K 后,写操作会阻塞(写得太快)。
  2. 写端存在,读空管道:管道为空时读操作会阻塞(读得太快)。
  3. 管道破裂:读端已关闭,此时再写管道,会导致写端进程退出(收到 SIGPIPE)。
  4. 写端关闭:read 返回 0,表示进程间通信结束。

4. 有名管道(FIFO)

有名管道在文件系统中可见,有文件名,因此可以用于任意单机进程之间的通信,不要求有亲缘关系。

4.1 创建有名管道

#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);
  • 功能:在指定路径下创建一个权限为 mode 的有名管道文件
  • 参数:pathname 为路径 + 名称,mode 为八进制文件权限
  • 返回值:成功返回 0,失败返回 -1

4.2 编程步骤

创建有名管道 → 打开有名管道 → 读写管道 → 关闭管道 → 卸载(删除)有名管道。

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/stat.h>

int main(void)
{
    const char *fifo = "/tmp/myfifo";

    /* 创建有名管道,若已存在则忽略错误 */
    if (mkfifo(fifo, 0666) == -1) {
        perror("mkfifo");
    }

    pid_t pid = fork();
    if (pid == 0) {
        /* 子进程写 */
        int fd = open(fifo, O_WRONLY);
        const char *msg = "hello fifo";
        write(fd, msg, strlen(msg));
        close(fd);
    } else if (pid > 0) {
        /* 父进程读 */
        int fd = open(fifo, O_RDONLY);
        char buf[128] = {0};
        read(fd, buf, sizeof(buf) - 1);
        printf("parent recv: %s\n", buf);
        close(fd);
    }

    unlink(fifo);
    return 0;
}

4.3 与无名管道的异同

有名管道的读写特性与无名管道完全一致,唯一的额外特性是:如果有一端没有打开,open 函数会阻塞,直到另一端也被打开为止。

5. 无名管道与有名管道对比

对比项无名管道有名管道
创建函数pipe()mkfifo()
文件系统中是否可见否是
适用进程有亲缘关系的进程任意单机进程
打开方式创建即打开需要 open
半双工是是
支持定位否否
一端未打开时不涉及open 阻塞

6. 小结

  • 管道底层是队列,半双工、不支持定位,读写优先使用文件 IO。
  • 无名管道用 pipe() 创建,只能用于有亲缘关系的进程,通常配合 fork 使用。
  • 有名管道用 mkfifo() 创建,在文件系统中可见,可用于任意单机进程,但一端未打开时 open 会阻塞。
  • 使用管道时要注意读写阻塞、管道破裂(SIGPIPE)以及写端关闭后 read 返回 0 这几种边界情况。
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐