进程间通信入门:匿名管道的使用、阻塞场景与避坑指南

一、什么是进程间通信

我们知道,操作系统中每个进程都拥有独立的内存空间,彼此相互隔离。但实际业务中,多个进程往往需要配合完成任务,必须交换数据、同步运行状态。由于进程不能直接访问对方内存,操作系统便设计了专门的交互方案,这就是进程间通信(Inter-Process Communication,简称 IPC)

二、进程通信的目的

进程间通信(IPC)核心目的

  1. 数据传输:进程间互相传递业务数据、文件、指令等信息。
  2. 资源共享:多个进程共用同一文件、内存等系统资源,避免重复创建。
  3. 进程同步:协调执行顺序,防止竞争冲突,保证运行逻辑有序。
  4. 事件通知:一个进程向其他进程发送状态、信号,触发对应动作。
  5. 进程控制:管控其他进程的启停、挂起、终止等行为。

三、通信行为本质

两大类行为本质

  1. 数据拷贝中转(主流方式)

操作系统在进程地址空间之外,开辟中间缓冲区。数据从发送进程拷贝到缓冲区,再从缓冲区拷贝到接收进程。

代表:管道、命名管道、消息队列、Socket。

  1. 内存区域共享(特殊方式)

操作系统划出一块物理内存 ,映射到多个进程的虚拟地址空间。进程直接读写同一片内存,无额外数据拷贝

代表:共享内存(需搭配信号量做同步)。

四、通信方法

1. 匿名管道

1)核心原理

匿名管道是操作系统在内核空间 开辟的一块固定大小的环形缓冲区 ,通过一对文件描述符(fd0读端、fd1写端)实现父子 进程间的半双工通信。(父子之间最常用,有血缘关系也能通信)

  • 单工:固定单向,永远只能一方发、一方收(如广播)
  • 半双工 :可切换方向,但同一时刻只能单向(如对讲机、普通管道)
  • 全双工:双向同时传输,收发互不干扰(如手机通话)

2)创建管道

c 复制代码
// 创建管道
if (pipe(int fd[2]) == -1) {
    perror("pipe创建失败");
    return 1;
}

内核动作:

  1. 创建两个 struct file 对象(都位于内核内存中);

  2. 分配并初始化共享的管道缓冲区

  3. 配置第一个 struct file读端):

    • f_op->read → 指向管道的读。

    • private_data → 指向上面那个共享的 struct inode

    • 不支持写操作,不支持 lseek

  4. 配置第二个 struct file写端):

    • f_op->write → 指向管道的写。
    • private_data → 指向同一个 struct inode
    • 不支持读操作,不支持 lseek
  5. 在当前进程的文件描述符表中分配两个槽位:

    • 第一个空槽位存入读端 struct file 的指针 → fd[0]
    • 第二个空槽位存入写端 struct file 的指针 → fd[1]

对比打开文件时创建的struct file:

  • 相同点 :无论是 open() 还是 pipe(),内核创建的都是同一类数据结构 ------struct file 对象。它们都遵循"一切皆文件"的设计哲学,都有文件操作函数表 (f_op)、私有数据指针等通用字段。
  • 不同点 :它们的作用和内部实现 完全不同:
    • open() 创建的 struct file 背后连接的是磁盘上的文件 (或设备、目录等),主要功能是读写磁盘数据,维护文件偏移量 (f_pos)。
    • pipe() 创建的两个 struct file(读端和写端)背后连接的是内核中的管道环形缓冲区 (pipe_inode_info),主要功能是进程间通信,没有文件偏移量的概念,读写行为基于缓冲区而非磁盘。

3)管道中的环形缓冲区

为什么匿名管道要用环形缓冲区 ?因为管道是 "先进先出(FIFO)" 的字节流,用环形缓冲区能最高效、无浪费、无移动地实现 "读一点、写一点、循环利用空间"。其实说是环形,本质还是线性,只是逻辑结构变成了环形。实现逻辑环形,就是当读写指针走到开辟空间的末尾时,重新回到空间起始位置

工作流程
  • 有固定大小(管道默认 64KB)
  • 写数据 → 写指针往后移
  • 读数据 → 读指针往后移,读过的数据就"释放"了
  • 指针走到末尾 → 回到 0 位置继续
  • 读写指针相遇 = 缓冲区空 / 满
为线性时的缺点:

当读指针往后移之后,已经读过的空间就浪费掉了

4)管道的使用

管道的使用只能在父子进程之间。其使用类似于对文件的读写,之不过管道有两个struct file,一个是读,一个是写;当创建子进程之后,首先要关闭一个 struct file 只保留一个用来读或写。fd[0]/fd[1] 就是他们的文件描述符。

c 复制代码
char buf[1024];  // 读写缓冲区
// 创建子进程
pid_t pid = fork();
if (pid == -1) {
    perror("fork失败");
    return 1;
}

// 子进程:读数据
if (pid == 0) {
    // 子进程只需要读,关闭写端
    close(fd[1]);

    // 从管道读取数据
    int len = read(fd[0], buf, strlen(buf) + 1);
    printf("子进程读取到数据:%s\n", buf);

    // 关闭读端
    close(fd[0]);
    return 0;
}
// 父进程:写数据
else {
    // 父进程只需要写,关闭读端
    close(fd[0]);

    // 向管道写入数据
    char* msg = "Hello, Pipe!";
    write(fd[1], msg, sizeof(msg));
    printf("父进程写入数据:%s\n", msg);

    // 关闭写端
    close(fd[1]);

    // 等待子进程结束
    wait(NULL);
}

5)字节流概念

核心结论

面向字节流 = 没有边界、没有格式、没有消息头,就是一串连续的字节。读多少、写多少、怎么分块,完全由进程自己决定。


1. 最简单的比喻

管道就像一根水管,流的是 "水",不是 "一包一包的东西"。

  • 你写 100 字节 → 变成连续水流进去
  • 你读 20 字节 → 取出前面 20 字节
  • 剩下 80 字节还在流里
  • 没有分割、没有边界、没有编号、没有结构

这就叫字节流


2. 对比一下

面向字节流(管道)

写:ABCDEFGHIJK

读:先读5个 → ABCDE

再读3个 → FGH

剩下 → IJK

没有边界,怎么读都行,连续不断。

面向数据报(消息队列)

写:第一条:ABC 第二条:DEF

读:必须一次读完整一条,不能读一半

有边界、有格式、有消息头。


3. 管道字节流的 3 个特点

① 无边界

一次写 100 字节,对方可以分 10 次读,每次读 10 字节。

怎么拆、怎么拼,管道不管。

② 连续、有序

像水流一样,先进先出,顺序不乱

③ 无格式

管道只认0101 二进制字节,不管是字符串、整数、结构体。


4. 最关键的一句话

面向字节流 = 数据没有 "包" 的概念,只有 "流" 的概念。写进去是一串,读出来可以任意切分,管道不负责划分消息。

6)若进程结束没关闭管道

在文件的 struct file 中,有着引用计数 f_count 专门用来记有几个进程打开了这个文件,多一个文件就++,少一个文件就--。那么管道的 struct file 自然也有这个引用计数,当进程结束,管道的 f_count-- ,管道的生命周期也就结束了。

7)管道的阻塞

1. 读端阻塞:管道为空(无数据)
  • 现象:read() 调用卡住不返回,进程挂起。
  • 触发:管道缓冲区里没有任何数据 ,且所有写端文件描述符都未关闭
  • 逻辑:内核认为还有进程会继续写数据,于是读进程休眠等待数据。
2. 读端不阻塞(正常退出)

管道为空,但所有写端 fd 全部 close

read() 直接返回 0(表示读到文件末尾),读进程结束读取。

3. 写端阻塞:管道缓冲区已满
  • 现象:write() 调用卡住,写进程挂起。
  • 触发:管道环形缓冲区被写满 ,且所有读端 fd 都未关闭
  • 管道默认缓冲区大小:常见 4096 字节(PAGE_SIZE)
  • 逻辑:缓冲区装不下新数据,内核等待读进程取走数据。
4. 写端异常:所有读端已关闭(管道破裂)

致命场景 :管道所有读端都 close,此时再执行 write()

  • 写进程会收到 SIGPIPE 信号(默认行为:进程直接终止)。
  • 若捕获 / 忽略 SIGPIPE:write() 返回 -1errno = EPIPE
  • 俗称:管道断裂(broken pipe)

读全关,写操作的杀死情况

c 复制代码
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>

int main() {
    int fd[2];
    char buffer[1024];

    if (pipe(fd) == -1) {
        perror("pipe");
        return 1;
    }

    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        return 1;
    }

    if (pid == 0) { // Child process
        close(fd[0]);
        sleep(1);

        write(fd[1], "child_write", 11);
        exit(0);
    } else { // Parent process
        close(fd[0]);

        int wstatus;
        waitpid(pid, &wstatus, 0);
        if (WIFEXITED(wstatus)) {
            printf("子进程正常退出\n");
        } 
        else if (WIFSIGNALED(wstatus)) { // 被信号杀死
            printf("子进程被信号杀死!信号编号:%d (SIGPIPE)\n", WTERMSIG(wstatus));
        }

    }
    return 0;
}
相关推荐
A小辣椒1 天前
TShark:Wireshark CLI 功能
linux
A小辣椒1 天前
TShark:基础知识
linux
AlfredZhao1 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao2 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334662 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪2 天前
linux 拷贝文件或目录到指定的位置
linux
摇滚侠3 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush43 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5203 天前
Linux 11 动态监控指令top
linux
不会C语言的男孩3 天前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言