深蓝黑背景封面。左侧自上而下五张圆角卡片列出五种 IPC 方式及一句话特征:管道——父子进程间的无格式字节流;消息队列——带类型的消息,可异步解耦;共享内存——速度最快,但同步要自己做,此卡以青色描边高亮;信号量——不传数据,专管同步互斥;Socket——唯一能跨机器的方式。卡片顶部标注「五种 IPC,各有所长」,底部一句话「没有银弹:先定场景,再选方式」。右侧大标题「进程间通信有哪些方式,各自适合什么场景」,副标题「选型先看:速度、数据形态、要不要跨机」。左上标注「面试经典 · 操作系统」,右上圆角胶囊标注「Day 09 · 11-26」。 图 1:本文封面,五种 IPC 方式的一句话特征对比。

先说明属性:这是一篇经典题解析,题目出自本地历史面经资料,与任何公司的当前招聘无关。「进程间通信有哪些方式」是操作系统部分的保留题,难不倒背书的人,但面试官想听的不是罗列名词,而是每种方式在速度、数据形态、同步语义上的差异,以及「这个场景为什么选它」。

管道:最简单,也最受限。

匿名管道由 pipe() 创建,给父子进程之间搭一条单向字节流通道:一头写、一头读,数据没有消息边界,读端空了就阻塞、写端满了也阻塞,同步语义是自带的。想要无关进程之间用,就换命名管道(FIFO),它在文件系统里有个名字,任何进程按权限打开即可。Linux 手册里还有一个常被追问的细节:不超过 PIPE_BUF 字节的写入保证原子性,超过则可能与其他写者的数据交错。管道的短板很明显:单向、字节流无格式、只能本机。

消息队列:带类型的消息,天然解耦。

消息队列由内核维护,收发双方按「消息」为单位交换数据,每条消息可以带类型字段,接收方可以按类型挑着收。相比管道,它保留了消息边界,而且发送方写完就走,不必等接收方在线——内核缓冲区兜着,天然支持异步解耦和削峰。代价是每次收发都要在用户态和内核态之间拷贝数据,大数据量时开销可观。

共享内存:最快的路,但锁要自己造。

共享内存把同一段物理内存映射进多个进程的地址空间,数据根本不经过内核拷贝,一方写入、另一方立刻可见,是五种方式里速度最快的,适合大数据量、高频交换的场景。但它只解决「数据怎么到」,不解决「什么时候读」——两个进程同时写就乱套,所以共享内存几乎总是和信号量成对出现。

信号量:不传数据,专管同步。

信号量本质是内核维护的计数器,P 操作减一(不足则阻塞)、V 操作加一,用来做互斥和条件同步。它不搬运任何业务数据,角色是给共享内存这类「裸通道」配上交通规则。把它和消息队列对比着记就不容易混:一个管同步,一个管数据。

Socket:唯一能跨机器的选项。

Socket 走网络协议栈,支持字节流(TCP)和数据报(UDP)两种形态,本机通信可以用 Unix domain socket 减少开销。它是五种方式里唯一能跨机器的,所以分布式系统的节点间通信没得选;但在同机场景下它的开销最大,协议栈封装、拷贝、上下文切换一样不少。

选型矩阵:先定场景,再选方式。

把五个维度摆在一起看:本机父子进程传小数据,管道最省事;要异步解耦、削峰填谷,消息队列;数据量大且交换频繁,共享内存配信号量;要跨机器,只有 Socket。面试里能主动把「数据形态」(字节流还是带边界消息)和「同步语义」(自带阻塞还是要自己加锁)这两个维度讲出来,就是明显超出背诵水平的信号。

用代码验证管道的字节流与阻塞语义。

下面这段 C 程序在 macOS 与 Linux 上都能编译,用 clang 实测(clang -o pipe_demo pipe_demo.c && ./pipe_demo):父进程往管道写一句话,子进程阻塞读出,写端关闭后再读返回 0(EOF)。

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>

int main(void) {
    int fd[2];
    pipe(fd);               /* fd[0] 读端,fd[1] 写端 */
    pid_t pid = fork();
    if (pid == 0) {         /* 子进程:读 */
        close(fd[1]);
        char buf[64] = {0};
        ssize_t n = read(fd[0], buf, sizeof(buf) - 1);  /* 无数据时阻塞 */
        printf("子进程读到 %zd 字节:%s\n", n, buf);
        printf("再次 read 返回值 = %zd(写端关闭,读到 EOF)\n", read(fd[0], buf, 1));
        close(fd[0]);
    } else {                /* 父进程:写 */
        close(fd[0]);
        const char *msg = "hello via pipe";
        write(fd[1], msg, strlen(msg));
        close(fd[1]);       /* 关闭写端,子进程才能读到 EOF */
        wait(NULL);
    }
    return 0;
}

实测输出:

子进程读到 14 字节:hello via pipe
再次 read 返回值 = 0(写端关闭,读到 EOF)

两次 read 的行为印证了管道的两条核心语义:读端无数据时阻塞等待,所有写端关闭后读到 EOF。

深蓝黑背景概念图。主体是一张六列五行的选型矩阵表,列为方式、速度、数据形态、同步语义、能否跨机、典型场景。五行内容:管道——快、无格式字节流、自带阻塞同步、否、父子进程与 shell 管道;消息队列——中、带类型的消息、内核缓冲可异步、否、进程解耦与削峰;共享内存——最快、任意数据结构、需配合信号量、否、大数据量高频交换,该行以青色描边高亮;信号量——不传数据、计数同步原语、否、配合共享内存做互斥;Socket——最慢、字节流或数据报、面向连接或报文、是、跨机通信与本机通用,「是」字以紫色高亮。底部总结条:「选型口诀:本机要快要共享内存,要省事要消息队列,要跨机只能 Socket」。 图 2:五种 IPC 方式在四个维度上的选型矩阵。

面试现场的答题结构。

先用一句话总起「五种方式没有银弹,选型看速度、数据形态、同步语义、是否跨机」;然后按管道、消息队列、共享内存、信号量、Socket 逐一讲机制与短板;重点展开共享内存为什么快(不经过内核拷贝)以及为什么必须配信号量;最后落到场景选型口诀收尾。你的项目里用哪种 IPC 最多?有没有踩过共享内存没配好同步的坑?欢迎讲讲。