作者:小康,C/C++编程博主关键词:Linux、IPC、进程通信、管道、共享内存、消息队列、信号量、Socket
面试被问烂了的一道题:"Linux 进程间通信有哪几种方式?"
大多数人能背出来:管道、消息队列、共享内存、信号量、Socket……
但真正能说清楚原理、适用场景、性能差异的人,少之又少。
这篇文章,我们不背八股,从底层原理出发,配合 图解 + 精简代码,彻底搞懂 Linux IPC。
进程是操作系统资源分配的基本单位。每个进程拥有独立的虚拟地址空间,进程 A 无法直接读写进程 B 的内存。
进程A 进程B┌─────────────────┐ ┌─────────────────┐│ 虚拟地址空间 │ │ 虚拟地址空间 ││ 0x0000~0xFFFF │ ✗ │ 0x0000~0xFFFF ││ [数据隔离] │◄─────►│ [数据隔离] │└─────────────────┘ └─────────────────┘ ↑ ↑ └──────── 内核空间 ────────┘ (共享区域)这种隔离保证了系统稳定性,但也带来了通信难题。IPC(Inter-Process Communication)就是解决这个问题的。
┌──────────────────────────────────────────────────────┐│ Linux IPC 机制 ││ ││ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ ││ │ 管道 │ │ 消息队列 │ │ 共享内存 │ ││ │ Pipe │ │ MsgQueue │ │ Shared Memory │ ││ └──────────┘ └──────────┘ └──────────────────┘ ││ ││ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ ││ │ 信号量 │ │ 信号 │ │ Socket │ ││ │Semaphore │ │ Signal │ │ (Unix/TCP/UDP) │ ││ └──────────┘ └──────────┘ └─────────────────-┘ │└──────────────────────────────────────────────────────┘ 速度:共享内存 > 管道 > 消息队列 > Socket 灵活性:Socket > 消息队列 > 管道管道本质是内核中的一块环形缓冲区(默认 64KB),通过文件描述符访问,单向通信,只能用于有亲缘关系的进程(父子进程)。
父进程 子进程┌──────────────┐ ┌──────────────┐│ fd[1](写端) │──→ [内核缓冲] │ fd[0](读端) ││ write() │ ┌────────┐ │ read() │└──────────────┘ │▓▓▓▓░░░░│ └──────────────┘ └────────┘ 环形缓冲区 (64KB默认)关键特性:
write() 阻塞;缓冲区空时 read() 阻塞#include<unistd.h>#include<stdio.h>intmain(){int fd[2]; pipe(fd); // fd[0]=读端, fd[1]=写端if (fork() == 0) { // 子进程:读 close(fd[1]);char buf[64]; read(fd[0], buf, sizeof(buf));printf("子进程收到: %s\n", buf); } else { // 父进程:写 close(fd[0]); write(fd[1], "Hello IPC!", 10); }return0;}适用场景:shell 中的 ls | grep,父子进程单向数据流。
匿名管道的限制是只能用于亲缘进程。命名管道通过文件系统路径,让任意进程通信。
进程A (writer) 文件系统 进程B (reader)┌─────────────┐ ┌──────────┐ ┌─────────────┐│ open(FIFO, │──写──→ │/tmp/myf │ ──读──→│ open(FIFO, ││ O_WRONLY) │ │ ifo │ │ O_RDONLY) │└─────────────┘ └──────────┘ └─────────────┘ (伪文件,数据不落盘,存在内核)// 创建 FIFOmkfifo("/tmp/myfifo", 0666);// 写进程int wfd = open("/tmp/myfifo", O_WRONLY);write(wfd, "data", 4);// 读进程int rfd = open("/tmp/myfifo", O_RDONLY);char buf[64];read(rfd, buf, sizeof(buf));消息队列是内核维护的链表结构,每条消息有类型(type),支持按类型选择性读取,这是管道做不到的。
┌─────────────────────────────────────────────┐│ 内核消息队列 ││ ││ [type=1, data] → [type=2, data] → [type=1, data] → NULL│ ││ 进程A: msgsnd(type=1, "Hello") ││ 进程B: msgrcv(type=1, ...) ← 只取type=1 ││ 进程C: msgrcv(type=2, ...) ← 只取type=2 │└─────────────────────────────────────────────┘#include<sys/msg.h>structmsgbuf {long mtype; // 消息类型(>0)char mtext[128]; // 消息内容};// 创建/获取消息队列int msgid = msgget(IPC_PRIVATE, IPC_CREAT | 0666);// 发送structmsgbufmsg = {.mtype = 1, .mtext = "Hello"};msgsnd(msgid, &msg, sizeof(msg.mtext), 0);// 接收(只接收 type=1 的消息)structmsgbufrecv;msgrcv(msgid, &recv, sizeof(recv.mtext), 1, 0);printf("收到: %s\n", recv.mtext);消息队列 vs 管道:
最快的 IPC 方式。内核将同一块物理内存映射到多个进程的虚拟地址空间,进程直接读写,无需系统调用拷贝。
进程A 虚拟空间 物理内存 进程B 虚拟空间┌────────────┐ ┌────────────┐│ │ ┌────────┐ │ ││ 0x7f000000 │──映射──│ 共享内存│──映射──│ 0x7e000000 ││ shm_ptr │ │ 物理页 │ │ shm_ptr ││ │ └────────┘ │ │└────────────┘ └────────────┘ 直接读写内存,零拷贝,速度最快#include<sys/shm.h>// 创建共享内存(1024 字节)int shmid = shmget(IPC_PRIVATE, 1024, IPC_CREAT | 0666);// 附加到进程地址空间char *shm = (char *)shmat(shmid, NULL, 0);// 写数据sprintf(shm, "Shared data!");// 另一个进程:相同 shmid attach 后直接读printf("%s\n", shm);// 解除附加 & 删除shmdt(shm);shmctl(shmid, IPC_RMID, NULL);⚠️ 共享内存本身没有同步机制,通常需要配合信号量使用,否则会产生竞争条件。
信号量不是用来传数据的,而是用来同步与互斥的。本质是内核中的一个计数器。
信号量 S = 1(互斥锁) ┌──────────┐进程A │ S = 1 │ 进程B sem_wait() →│ S = 0 │← sem_wait() (阻塞) [临界区] │ │ sem_post() →│ S = 1 │→ 进程B 被唤醒 └──────────┘ S>0: 资源可用 S=0: 资源被占用,等待#include<semaphore.h> // POSIX 信号量sem_t sem;sem_init(&sem, 1, 1); // pshared=1 进程间共享,初值=1// 进程Asem_wait(&sem); // P操作,S-1// ... 访问共享内存 ...sem_post(&sem); // V操作,S+1sem_destroy(&sem);进程A 进程B │ │ ├─ sem_wait(mutex) │ ├─ 写共享内存 │ ├─ sem_post(mutex) │ │ ├─ sem_wait(mutex) │ ├─ 读共享内存 │ ├─ sem_post(mutex)Socket 通常用于网络,但Unix Domain Socket(UDS) 专为本机进程通信设计,性能远高于 TCP Loopback,且支持传递文件描述符(这是其他 IPC 做不到的)。
进程A (client) 进程B (server)┌────────────┐ ┌────────────┐│ connect() │──────→ │ accept() ││ send() │──数据──→ │ recv() ││ │ (内核) │ │└────────────┘ └────────────┘ 通过 /tmp/xxx.sock 文件标识 (数据不经过网络协议栈,更快)// server 端核心代码int server_fd = socket(AF_UNIX, SOCK_STREAM, 0);structsockaddr_unaddr;addr.sun_family = AF_UNIX;strcpy(addr.sun_path, "/tmp/myapp.sock");bind(server_fd, (struct sockaddr*)&addr, sizeof(addr));listen(server_fd, 5);int client_fd = accept(server_fd, NULL, NULL);char buf[256];recv(client_fd, buf, sizeof(buf), 0);printf("收到: %s\n", buf);Nginx、Redis、Docker 等都大量使用 Unix Domain Socket 提升本地通信性能。
信号是最古老的 IPC,用于事件通知,不能携带数据(除了 sigqueue 的实时信号)。
进程A 内核 进程B │ │ │ ├─ kill(pidB, SIGUSR1) │ │ │ ─────────────────→ │ │ │ ├─ 投递 SIGUSR1 ──────→│ │ │ ├─ signal_handler() │ │ │ 处理信号#include<signal.h>voidhandler(int sig){printf("收到信号: %d\n", sig);}signal(SIGUSR1, handler); // 注册处理函数// 另一个进程发送kill(target_pid, SIGUSR1);┌─────────────┬──────┬──────┬──────┬──────┬──────┐│ IPC方式 │ 速度 │ 容量 │ 同步 │跨主机│ 难度 │├─────────────┼──────┼──────┼──────┼──────┼──────┤│ 匿名管道 │ 中 │ 64KB │ 自带 │ ✗ │ 低 ││ 命名管道 │ 中 │ 64KB │ 自带 │ ✗ │ 低 ││ 消息队列 │ 中 │ 8KB │ 自带 │ ✗ │ 中 ││ 共享内存 │ 最快 │ 自定 │ 需要 │ ✗ │ 高 ││ 信号量 │ — │ — │ 是 │ ✗ │ 中 ││ Unix Socket │ 快 │ 自定 │ 自带 │ ✗ │ 中 ││ TCP Socket │ 慢 │ 自定 │ 自带 │ ✓ │ 中 ││ 信号 │ 快 │ 无 │ — │ ✗ │ 低 │└─────────────┴──────┴──────┴──────┴──────┴──────┘选型口诀:
亲缘简单通信 → 匿名管道非亲缘单向流 → 命名管道(FIFO)需要消息分类 → 消息队列追求极致性能 → 共享内存 + 信号量灵活全双工 → Unix Domain Socket跨机器通信 → TCP Socket纯通知事件 → 信号(Signal)Q1:管道和消息队列的本质区别?
管道是字节流,无消息边界,先进先出;消息队列有类型,可按需读取,有消息边界。
Q2:共享内存为什么是最快的 IPC?
其他 IPC 数据需要在用户空间→内核→用户空间之间拷贝(至少2次),而共享内存直接映射物理页,零拷贝。
Q3:信号量和互斥锁的区别?
互斥锁(mutex)只能由加锁的线程解锁;信号量可以由任意线程/进程释放,适合进程间同步。
Q4:为什么 Nginx 用 Unix Socket 而不是 TCP Loopback(127.0.0.1)?
UDS 不经过网络协议栈(无 TCP 握手、校验等开销),延迟更低,吞吐更高,实测性能提升约 **20~40%**。
Linux IPC 是系统编程的基石,也是高性能服务开发的核心能力。
理解这些机制不仅让你在面试中脱颖而出,更能在实际项目中做出正确的架构决策——是用共享内存追求极致性能,还是用 Socket 换取灵活性,都需要深刻理解背后的原理。
下篇预告:深入 mmap —— 比共享内存更强大的内存映射技术
如果你觉得这篇文章有收获,那你一定会喜欢我精心打造的 C++ 项目实战课程合集。
不讲玩具代码,全是工业级项目:
每个项目都是真实可用的工程代码,不是教学玩具。
详情点击 C++ 项目合集课程链接:为什么同样是"学过C++",有人面试碾压,有人开口就怂?差距在这18个C++硬核项目

觉得有帮助,点赞、在看和转发,让更多程序员看到 🙏