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

一、什么是进程间通信

我们知道,操作系统中每个进程都拥有独立的内存空间,彼此相互隔离。但实际业务中,多个进程往往需要配合完成任务,必须交换数据、同步运行状态。由于进程不能直接访问对方内存,操作系统便设计了专门的交互方案,这就是进程间通信(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;
}
相关推荐
张小姐的猫15 小时前
【Linux】多线程实战 —— 日志类 | 策略模式
linux·运维·服务器·c++·bash·策略模式
love8888_cnsd15 小时前
Git & Linux 速查表
java·linux·git·后端·elasticsearch
handler0115 小时前
【Linux】五种IO模型详解
linux·运维·服务器·c语言·网络·笔记·php
wljy11 天前
二、进制状态转换
linux·运维·服务器·c语言·c++
week@eight1 天前
Linux - Doris
linux·运维·数据库·mysql
平行云1 天前
实时云渲染预启动技术解析:UE数字孪生应用的延迟优化机制(二)
linux·unity·ue5·webgl·实时云渲染·云桌面·像素流
看到代码头都是大的1 天前
CentOS环境下手动升级openssl、openssh
linux·运维·centos
浮生若城1 天前
Linux——Ext系列文件系统
linux·运维·服务器
枳实-叶1 天前
【Linux驱动开发】第16天:按键中断完整实战
linux·运维·驱动开发