🔥 本文定位 :面向已经掌握进程创建、文件描述符和基础 I/O 的同学,从"两个进程为什么不能直接访问彼此变量"出发,系统梳理 Linux IPC 的数据通路、同步边界和资源生命周期。
💡 学习目标 :不仅会调用
pipe()、mkfifo()、shmget()、shmat(),还要真正理解 EOF 与SIGPIPE为什么依赖所有端点是否关闭、PIPE_BUF为什么不等于管道容量、共享内存为什么最快却仍然需要同步,以及 System V IPC 为什么不会随创建进程退出而自动消失。

文章目录
- 一、为什么需要进程间通信
- [二、如何选择 IPC 机制](#二、如何选择 IPC 机制)
- 三、匿名管道:内核中的单向字节流
- [四、fork 之后为什么必须关闭无用端](#四、fork 之后为什么必须关闭无用端)
- 五、管道读写的完整边界条件
- 六、使用管道构建一个简洁的进程池
- [七、命名管道 FIFO:让无亲缘进程相遇](#七、命名管道 FIFO:让无亲缘进程相遇)
- [八、System V 共享内存](#八、System V 共享内存)
- 九、共享内存为什么必须配合同步机制
- [十、System V 消息队列](#十、System V 消息队列)
- [十一、System V 信号量与 P/V 操作](#十一、System V 信号量与 P/V 操作)
- [十二、内核如何标识和管理 System V IPC](#十二、内核如何标识和管理 System V IPC)
- [十三、把 IPC 接入 Shell 管道](#十三、把 IPC 接入 Shell 管道)
- 十四、常见错误与排查工具
- 十五、高频面试题
- 总结
一、为什么需要进程间通信
1.1 进程隔离带来了安全,也带来了通信问题
每个用户进程通常拥有独立的虚拟地址空间。进程 A 中地址为 0x400000 的变量,与进程 B 中相同虚拟地址的变量没有天然关系。一个进程不能把普通指针直接交给另一个进程使用。
因此,进程间通信必须借助双方都能访问的"中间层":
text
进程 A ── 系统调用 ──> 内核对象 / 共享映射 ── 系统调用或内存访问 ──> 进程 B
IPC 通常解决五类问题:
- 数据传输:把字节、消息或结构化数据交给另一个进程;
- 事件通知:告诉对方"任务到达""状态变化"或"应该退出";
- 资源共享:共同访问内存、文件、设备或服务;
- 同步与互斥:规定多个执行流访问临界资源的顺序;
- 进程控制:父进程向工作进程分派任务并回收结果。
1.2 IPC 不只是在"传数据"
分析一种 IPC 时,至少要问四个问题:
| 维度 | 关键问题 |
|---|---|
| 数据模型 | 字节流、消息,还是共享地址空间? |
| 等待语义 | 没数据、空间不足或对端不存在时会怎样? |
| 身份发现 | 两个进程怎样找到同一个通信对象? |
| 生命周期 | 进程退出后,内核对象或文件名是否仍存在? |
只记 API 而忽略这四点,最容易写出"偶尔能跑、压力一大就挂"的程序。
二、如何选择 IPC 机制
Linux 同时提供匿名管道、FIFO、共享内存、消息队列、信号量、Socket、信号等机制。它们不是谁完全替代谁,而是针对不同通信模型。

| 机制 | 数据模型 | 典型范围 | 优点 | 需要注意 |
|---|---|---|---|---|
| 匿名管道 | 单向字节流 | 有亲缘关系的进程 | 简单、天然可配合 fork/exec |
无消息边界,端点关闭很关键 |
| FIFO | 单向字节流 | 同一主机任意进程 | 有路径名,易于会合 | open() 本身可能阻塞 |
| 共享内存 | 共享页 | 同一主机进程 | 传输开销低、适合大数据 | 不提供同步和消息边界 |
| 消息队列 | 有边界、有类型的消息 | 同一主机进程 | 接收端可按类型筛选 | 有内核配额,System V 生命周期独立 |
| 信号量 | 计数/同步状态 | 同一主机进程 | 保护临界区、表达资源数量 | 它不负责承载业务数据 |
| Unix 域 Socket | 字节流或数据报 | 同一主机任意进程 | 双向、可传凭据/描述符 | 接口比管道稍复杂 |
| TCP/UDP Socket | 流或数据报 | 可跨主机 | 网络透明、扩展性强 | 协议、序列化与异常处理成本更高 |
一个实用决策法:
- 父子进程传少量顺序数据:先考虑匿名管道;
- 无亲缘进程做简单本地字节流:FIFO;
- 大块数据被反复读写:共享内存 + 同步原语;
- 需要保留消息边界或按类型收取:消息队列;
- 需要双向、可扩展的本地服务接口:Unix 域 Socket;
- 需要跨机器:网络 Socket。
三、匿名管道:内核中的单向字节流
3.1 创建管道
c
#include <unistd.h>
int pipe(int pipefd[2]);
调用成功返回 0:
pipefd[0]:读端;pipefd[1]:写端。
写入 pipefd[1] 的字节由内核暂存,随后从 pipefd[0] 读出。管道本身没有普通磁盘文件路径,也不能 lseek()。
c
int pipefd[2];
if (pipe(pipefd) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}
在 Linux 上还可以使用:
c
pipe2(pipefd, O_CLOEXEC | O_NONBLOCK);
它能在创建时原子设置 FD_CLOEXEC 或非阻塞状态,避免多线程程序中"先创建、再 fcntl()"的竞态窗口。
3.2 管道不是消息队列
普通管道提供的是字节流,没有消息边界:
text
write("ABC", 3)
write("DEF", 3)
接收端可能一次 read() 得到 "ABCDEF",也可能分多次读到。
因此,应用层必须自己规定协议,例如:
- 固定长度记录;
- 长度字段 + 负载;
- 特殊分隔符;
- 读满 N 字节的循环。
3.3 一个正确的父写子读示例

c
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
int main(void)
{
int pipefd[2];
if (pipe(pipefd) == -1) {
perror("pipe");
return EXIT_FAILURE;
}
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return EXIT_FAILURE;
}
if (pid == 0) {
close(pipefd[1]); // 子进程不用写端
char buffer[128];
ssize_t count;
while ((count = read(pipefd[0], buffer, sizeof(buffer))) > 0) {
ssize_t offset = 0;
while (offset < count) {
ssize_t written = write(STDOUT_FILENO,
buffer + offset,
(size_t)(count - offset));
if (written == -1 && errno == EINTR)
continue;
if (written <= 0)
_exit(EXIT_FAILURE);
offset += written;
}
}
close(pipefd[0]);
_exit(count == -1 ? EXIT_FAILURE : EXIT_SUCCESS);
}
close(pipefd[0]); // 父进程不用读端
const char message[] = "hello from parent\n";
if (write(pipefd[1], message, sizeof(message) - 1) == -1)
perror("write");
close(pipefd[1]); // 让子进程最终读到 EOF
waitpid(pid, NULL, 0);
return 0;
}
真实工程还要系统处理短写、EINTR、非阻塞 I/O 和错误传播;示例重点是端点关系。
四、fork 之后为什么必须关闭无用端
fork() 会让子进程继承父进程的文件描述符。父子两份描述符指向同一个内核管道对象,因此 fork() 后最初是:
text
父进程:read end 打开,write end 打开
子进程:read end 打开,write end 打开
若设计为"父写子读",应立即变成:
text
父进程:关闭 read end,仅保留 write end
子进程:关闭 write end,仅保留 read end
这不是代码洁癖,而是语义正确性的必要条件:
- 只有当所有指向写端的描述符都关闭后,读端才会在数据耗尽后得到 EOF;
- 只有当所有 指向读端的描述符都关闭后,写端才会触发
SIGPIPE/EPIPE; - 多余端点会让进程永远等待一个实际上不会到来的数据或 EOF。
❌ 常见死锁:父进程写完后直接
waitpid(),却忘记关闭自己的写端;子进程一直read()等 EOF,父进程一直等子进程退出。
如果随后执行 exec*(),还应使用 O_CLOEXEC 或 FD_CLOEXEC,防止无关程序意外继承管道端点。
五、管道读写的完整边界条件

5.1 阻塞模式
| 条件 | read() |
write() |
|---|---|---|
| 管道有数据 | 返回实际读取字节数 | - |
| 管道为空,但仍有写端 | 阻塞等待数据 | - |
| 管道为空,所有写端已关闭 | 返回 0,即 EOF |
- |
| 管道有足够空间 | - | 写入并返回字节数 |
| 管道已满,但仍有读端 | - | 阻塞等待空间 |
| 所有读端已关闭 | - | 产生 SIGPIPE;若忽略/阻塞该信号则失败并置 EPIPE |
5.2 非阻塞模式
开启 O_NONBLOCK 后:
- 空管道仍有写端:
read()返回-1,errno == EAGAIN; - 满管道写入:可能返回
-1/EAGAIN; - 大于
PIPE_BUF的写入在有部分空间时可能发生短写; - 没有写端时,空管道仍返回 EOF,而不是
EAGAIN。
5.3 PIPE_BUF 不是管道容量
这是原理题中最常见的混淆:
- 管道容量 :内核能为该管道缓冲多少尚未读取的数据;Linux 可通过
F_GETPIPE_SZ查询; PIPE_BUF:多写者场景下,POSIX 保证一次写入不会与其他写者交错的上限。
Linux 上 PIPE_BUF 通常为 4096 字节,但不要把它写死为通用常量。对于 n <= PIPE_BUF 的一次写入,原子性意味着该段字节不会和另一写者的字节交织;这并不代表调用永远不阻塞,也不代表管道总容量只有 4096 字节。
5.4 原子写入不等于"有消息边界"
即使多个工作进程每次都原子写入一个固定大小结构,读端仍然面对字节流。一次 read() 可能读取多条记录,也可能只拿到一条记录的一部分,因此仍需循环拼包。
六、使用管道构建一个简洁的进程池
课程原稿使用多个父到子管道构建进程池,这个设计非常适合理解文件描述符与任务分派。

6.1 基本结构
text
父进程
├── channel[0].write_fd ──> worker 0 stdin/read_fd
├── channel[1].write_fd ──> worker 1 stdin/read_fd
└── channel[2].write_fd ──> worker 2 stdin/read_fd
每创建一个 worker:
pipe()创建专属任务通道;fork();- 子进程关闭写端,仅保留读端;
- 父进程关闭读端,把
{pid, write_fd}放入通道表; - 父进程轮询或随机选择通道写入任务编号。
cpp
struct Channel {
pid_t pid;
int write_fd;
};
void dispatch(const std::vector<Channel>& channels, int command)
{
static size_t next = 0;
const Channel& channel = channels[next++ % channels.size()];
const char* data = reinterpret_cast<const char*>(&command);
size_t left = sizeof(command);
while (left > 0) {
ssize_t n = write(channel.write_fd, data, left);
if (n == -1 && errno == EINTR)
continue;
if (n <= 0)
throw std::runtime_error("worker channel closed");
data += n;
left -= static_cast<size_t>(n);
}
}
6.2 进程池最容易忽略的三个点
- 关闭从其他轮次继承的描述符:后创建的子进程可能继承前面 worker 的父端描述符,导致 EOF 无法到达;
- 先关闭全部任务写端,再等待 worker:关闭代表"不再有任务",worker 才能退出读取循环;
- 任务协议要可分帧:固定大小命令可以循环读满;变长任务应增加长度头。
更现代的实现还会加入 poll/epoll、结果通道、超时、worker 崩溃重建和背压控制。
七、命名管道 FIFO:让无亲缘进程相遇
匿名管道的端点通常依靠 fork() 继承。FIFO 则在文件系统命名空间中拥有路径,因此无亲缘进程只要权限允许,就能打开同一个通信对象。
bash
mkfifo /tmp/ipc-demo.fifo
或者:
c
#include <sys/stat.h>
if (mkfifo("/tmp/ipc-demo.fifo", 0660) == -1 && errno != EEXIST) {
perror("mkfifo");
}
FIFO 路径用于发现与打开;数据仍通过内核管道对象传递,不会像普通文件一样写入底层磁盘文件。
7.1 open() 本身就有会合语义

| 打开方式 | 默认阻塞行为 | O_NONBLOCK 行为 |
|---|---|---|
O_RDONLY |
等待至少一个写者打开 | 立即成功;无写者且无数据时读取可返回 EOF |
O_WRONLY |
等待至少一个读者打开 | 无读者时失败,errno == ENXIO |
Linux 允许进程以 O_RDWR 打开 FIFO,即使没有对端也会成功;POSIX 对这种用法未作定义,不应把它当作可移植的"绕过阻塞"方案,还要警惕自己读到自己写入的数据或造成死锁。
7.2 FIFO 与匿名管道的关系
创建/打开方式不同,但打开完成后,其 I/O 语义基本相同:
- 都是字节流;
- 都有容量与
PIPE_BUF原子写入规则; - 都需要正确处理 EOF、
SIGPIPE、非阻塞和短读写。
使用完毕后,close() 关闭本进程端点;unlink() 删除的是 FIFO 的目录项。已经打开的端点可继续使用,直到全部关闭。
八、System V 共享内存
管道与消息队列的数据通常要经过系统调用在用户缓冲区和内核缓冲区之间移动。共享内存让多个进程的虚拟地址映射到同一组物理页,进程可以直接通过普通读写指令访问共享内容。

"共享"指底层页相同,不代表两个进程中的虚拟地址必须相同。每个进程应使用
shmat()返回的本地地址,避免在共享区保存只对某个进程有效的裸指针。
8.1 从 key 到 shmid
c
#include <sys/ipc.h>
#include <sys/shm.h>
key_t key = ftok("/tmp/ipc-token", 0x42);
int shmid = shmget(key, 4096, IPC_CREAT | IPC_EXCL | 0660);
key:用于在 System V IPC 命名空间中查找对象;shmid:内核返回的对象标识符,后续 API 使用它;IPC_CREAT | IPC_EXCL:要求创建全新对象,已存在则以EEXIST失败;- 仅
IPC_CREAT:不存在则创建,存在则尝试取得已有对象。
ftok() 只是按路径元数据和项目编号生成 key,可能碰撞。程序必须检查返回值,并验证取得对象的权限、大小和协议版本。
8.2 关联共享内存
c
void *address = shmat(shmid, NULL, 0);
if (address == (void *)-1) {
perror("shmat");
exit(EXIT_FAILURE);
}
通常把 shmaddr 传 NULL,让内核选择合适地址。失败返回 (void *)-1,不是 NULL。
如果共享区包含跨进程引用,优先保存相对于共享区起始位置的偏移量,而不是绝对指针:
c
struct SharedHeader {
size_t payload_offset;
size_t payload_length;
};
8.3 分离、标记删除与最终销毁

c
shmdt(address); // 当前进程解除映射
shmctl(shmid, IPC_RMID, NULL); // 标记删除
必须分清:
shmdt():仅解除当前进程的映射;对象仍可能存在,其他进程也可继续关联;IPC_RMID:把对象标记为删除;当最后一个关联者分离后,内核最终回收对象;- 创建进程退出并不会自动删除 System V 共享内存。
因此,创建者应明确约定清理责任,并在错误路径上也执行清理。不要把日常清理依赖于 ipcrm 人工补救。
8.4 一个最小共享区协议
c
#include <stdatomic.h>
#include <stdint.h>
enum { SHM_MAGIC = 0x49504331 };
struct SharedBlock {
uint32_t magic;
uint32_t version;
atomic_uint ready;
size_t length;
char payload[4000];
};
共享数据应该包含魔数、版本、长度和状态,不要让双方仅凭"大家都知道结构长什么样"来猜测。
九、共享内存为什么必须配合同步机制
共享内存只解决"双方能看到同一份数据",不保证:
- 谁先写、谁后读;
- 复合更新是否原子;
- 编译器和 CPU 是否按预期顺序发布数据;
- 一个进程写到一半时另一个进程会不会读到中间状态。

9.1 错误模型
c
shared->length = length;
memcpy(shared->payload, source, length);
shared->ready = 1;
如果 ready 只是普通整数,上述代码并不足以建立跨进程的可靠发布/获取关系。并发读写还可能造成数据竞争。
9.2 可选的同步方案
- System V 信号量;
- POSIX 命名信号量;
- 放在共享内存中并设置
PTHREAD_PROCESS_SHARED的 pthread mutex/condition variable; - 结合 C11 原子操作与 futex 的自定义协议;
- 用管道/eventfd 只做通知,共享内存承载大数据。
最后一种很常见:
text
共享内存:数据面,负责高吞吐负载
pipe/eventfd:控制面,负责"新数据已就绪"的通知与等待
同步不是简单地 sleep(1)。睡眠既不能保证正确顺序,也会引入延迟和偶发失败。
十、System V 消息队列
消息队列保留消息边界,并允许消息带一个正的 long 类型字段:
c
struct Message {
long type;
char text[128];
};
典型 API:
c
int msgid = msgget(key, IPC_CREAT | 0660);
msgsnd(msgid, &message, payload_size, 0);
msgrcv(msgid, &message, sizeof(message.text), wanted_type, 0);
msgctl(msgid, IPC_RMID, NULL);
msgsz 不包含 long type 字段。接收端可以按类型筛选消息,这一点与普通管道不同。

使用消息队列时仍需考虑:
- 队列满时发送是否阻塞,或使用
IPC_NOWAIT; - 接收缓冲区过小时的
E2BIG与MSG_NOERROR截断策略; - 内核的消息大小、队列字节数和队列数量限制;
- 创建者退出后队列仍存在,必须
IPC_RMID。
消息队列适合中小型控制消息;大块数据通常更适合共享内存,消息队列只传描述符、偏移量或任务元数据。
十一、System V 信号量与 P/V 操作
信号量是一个由内核管理的计数同步对象。它通常不传递业务负载,而是表示"可用资源数量"或"是否允许进入临界区"。

11.1 P/V 操作
以值为 1 的二元信号量为例:
text
P / wait:申请资源,计数从 1 变为 0;若无法完成则等待
临界区:访问共享资源
V / post:释放资源,计数从 0 变为 1,并可能唤醒等待者
System V 使用 semop() 提交一个或多个 sembuf 操作:
c
struct sembuf lock_op = { .sem_num = 0, .sem_op = -1, .sem_flg = SEM_UNDO };
struct sembuf unlock_op = { .sem_num = 0, .sem_op = 1, .sem_flg = SEM_UNDO };
semop(semid, &lock_op, 1);
/* critical section */
semop(semid, &unlock_op, 1);
semop() 对一个数组中的整组操作具有原子性:要么全部立即完成,要么按规则等待/失败,不会只完成前一半。
11.2 计数信号量不是"加锁函数"的同义词
- 初值为 1:可以表达互斥;
- 初值为 N:可以表达 N 份同类资源;
sem_op == 0:等待信号量值变为 0;IPC_NOWAIT:不能立即完成时返回EAGAIN;SEM_UNDO:让内核记录进程退出时需要撤销的调整,但有系统限制,也不能代替严谨的恢复协议。
初始化和使用之间同样存在竞态。创建者应通过明确的"唯一创建者 + 初始化完成协议"保证其他进程不会使用尚未初始化的信号量集合。
十二、内核如何标识和管理 System V IPC

System V 消息队列、共享内存和信号量拥有相似的管理模型:
text
key_t key
↓ msgget / shmget / semget
整数标识符 msgid / shmid / semid
↓
内核 IPC 对象 + ipc_perm 权限与所有者信息
需要区分:
- key 用于查找或创建;
- id 是本次内核对象的句柄,包含槽位与序列信息,陈旧 id 不应被长期保存;
- 权限位类似文件权限,但 System V IPC 对象并不是普通文件描述符;
- IPC namespace 会影响进程能看到哪一组 System V IPC 对象,容器环境下尤其重要。
原稿展示的 Linux 2.6.x 内核结构图适合说明"内核会统一组织 IPC 对象",但结构字段是实现细节,现代内核已经演进。应用程序应依赖稳定的系统调用 ABI,而不是依赖某个旧版本内核的内部结构布局。
12.1 生命周期对比
| 对象 | 名称/发现方式 | 进程退出后 | 清理方式 |
|---|---|---|---|
| 匿名管道 | 继承的 fd | 所有引用关闭后回收 | close() |
| FIFO | 文件路径 | 路径可继续存在 | close() + unlink() |
| SysV 共享内存 | key → shmid | 通常继续存在 | shmctl(IPC_RMID) |
| SysV 消息队列 | key → msgid | 通常继续存在 | msgctl(IPC_RMID) |
| SysV 信号量 | key → semid | 通常继续存在 | semctl(IPC_RMID) |
十三、把 IPC 接入 Shell 管道
Shell 中:
bash
who | wc -l
本质上是:
- Shell 创建管道;
- fork 左、右两个子进程;
- 左侧子进程
dup2(pipefd[1], STDOUT_FILENO); - 右侧子进程
dup2(pipefd[0], STDIN_FILENO); - 各进程关闭所有不再使用的原始管道描述符;
- 分别
execvp()执行命令; - 父 Shell 关闭自己的管道端点并等待子进程。
c
if (left_child == 0) {
dup2(pipefd[1], STDOUT_FILENO);
close(pipefd[0]);
close(pipefd[1]);
execvp(left_argv[0], left_argv);
_exit(127);
}
if (right_child == 0) {
dup2(pipefd[0], STDIN_FILENO);
close(pipefd[0]);
close(pipefd[1]);
execvp(right_argv[0], right_argv);
_exit(127);
}
多级管道 a | b | c 需要 N-1 条管道,并确保每个子进程只保留相邻输入/输出。任何多余写端遗留,都可能导致最右侧命令收不到 EOF。
生产级 Shell 还要处理进程组、作业控制、终端前台组、重定向优先级和错误回收,这已超出本文主线。
十四、常见错误与排查工具
14.1 管道一直不返回 EOF
排查所有相关进程是否仍持有写端:
bash
ls -l /proc/<pid>/fd
lsof -p <pid>
strace -f -e trace=pipe,pipe2,dup2,close,read,write,execve ./app
14.2 程序被 SIGPIPE 终止
说明所有读端已经关闭。修复通信生命周期,而不是第一时间粗暴忽略信号。若选择忽略/处理 SIGPIPE,必须检查 write() 的 EPIPE。
14.3 共享内存创建时报 File exists
bash
ipcs -m
ipcrm -m <shmid>
这常常说明上次程序未执行 IPC_RMID。调试时可以手工清理,正式程序应定义所有退出路径的清理策略。
14.4 查看 System V IPC
bash
ipcs -m # 共享内存
ipcs -q # 消息队列
ipcs -s # 信号量
cat /proc/sysvipc/shm
cat /proc/sysvipc/msg
cat /proc/sysvipc/sem
14.5 共享内存偶发乱码或半条数据
先检查是否真的存在同步协议,而不是只检查 memcpy()。使用原子变量、信号量或进程共享 mutex 建立发布/获取关系;协议中加入长度、版本和状态,并对状态转换画出状态机。
14.6 安全细节
- 避免对可被攻击者替换的路径盲目
mkfifo()/ftok(); - 在受控目录中创建 FIFO,使用最小权限并检查对象类型和所有者;
- 共享内存不要使用
0666作为无脑默认值; - 校验来自其他进程的长度、类型、偏移和版本;
- 新代码优先考虑
pipe2(O_CLOEXEC),减少描述符泄漏到exec()后程序。
十五、高频面试题
15.1 pipefd[0] 和 pipefd[1] 分别是什么?
pipefd[0] 是读端,pipefd[1] 是写端。普通可移植管道按单向通道理解。
15.2 为什么父子进程要关闭不用的端点?
EOF 和 SIGPIPE/EPIPE 判断的是内核中所有读/写端引用,而不是"业务上谁打算再使用"。多余引用会改变阻塞和结束语义。
15.3 read() 从管道返回 0 表示什么?
缓冲数据已经读完,并且所有写端引用都已关闭,即到达 EOF。空但仍存在写者的阻塞管道不会返回 0,而是等待。
15.4 什么情况下产生 SIGPIPE?
所有读端都已关闭后继续写管道或 FIFO。若该信号被忽略或阻塞,write() 返回 -1 并设置 EPIPE。
15.5 PIPE_BUF 是管道容量吗?
不是。它是多写者条件下原子写入保证的大小边界;管道容量是可以容纳的未读数据总量,两者概念不同。
15.6 一次 write() 对应一次 read() 吗?
不对应。管道是字节流,读取和写入调用边界可以重新组合。应用层必须自行分帧。
15.7 匿名管道只能用于父子进程吗?
本质要求是不同进程能获得指向同一管道对象的描述符。最常见方式是通过共同祖先 fork() 继承,也可以通过 Unix 域 Socket 传递文件描述符等方式交给无亲缘进程,但那时通常已有更合适的 IPC 设计。
15.8 FIFO 和普通文件有什么区别?
FIFO 有路径和权限,但没有普通文件的数据块内容;打开后数据在内核管道中流动,不能随机定位,读写语义与管道相同。
15.9 共享内存为什么快?
建立映射后,进程通过普通内存访问共享页,不必为每次数据交换都在用户缓冲区与内核 IPC 缓冲区间复制。同步、缓存一致性和缺页等成本仍然存在。
15.10 两个进程关联同一共享内存后,地址相同吗?
不保证。每个进程拥有独立虚拟地址空间,内核可选择不同虚拟地址映射同一物理页,因此共享数据结构应避免进程私有绝对指针。
15.11 shmdt() 会删除共享内存吗?
不会。它只解除当前进程的关联。删除要使用 shmctl(shmid, IPC_RMID, NULL),并在最后一个关联者离开后完成回收。
15.12 ftok() 会生成唯一 key 吗?
不保证。它可能产生碰撞,也依赖路径对应文件的元数据。必须检查返回值,并让上层协议验证实际对象。
15.13 共享内存自带互斥吗?
不带。共享内存只提供共同可见的存储,需要配合信号量、进程共享 mutex、原子变量或其他同步机制。
15.14 System V IPC 的生命周期跟随创建进程吗?
通常不跟随。共享内存、消息队列和信号量对象可在创建进程退出后继续存在,直到显式删除或系统关闭,所以必须设计清理责任。
15.15 消息队列和管道最大的模型差异是什么?
管道是无消息边界的顺序字节流;System V 消息队列保存离散消息,并允许接收端按消息类型选择。
总结
把本篇串成一张心智图:
text
独立进程地址空间
↓ 需要可共同访问的中介
匿名管道 / FIFO
├── 单向字节流
├── EOF 与 SIGPIPE 取决于所有端点引用
├── PIPE_BUF 描述原子写入边界,不是总容量
└── FIFO 通过路径让无亲缘进程会合
System V 共享内存
├── 多个虚拟地址映射同一组共享页
├── shmdt 只分离,IPC_RMID 才标记删除
└── 数据面很快,但必须额外同步
System V 消息队列
└── 保留消息边界,可按类型接收
System V 信号量
└── 计数与等待,用 P/V 操作保护临界资源
共同原则
├── 先定义数据模型
├── 再定义阻塞/非阻塞边界
├── 明确发现方式与权限
└── 明确由谁、在何时清理资源
真正理解 IPC,不是背出一串函数,而是能在任何异常现场回答:谁还持有端点、谁在等待谁、数据有没有边界、对象何时销毁、共享访问由谁同步。
参考资料
- Linux man-pages:pipe(2)
- Linux man-pages:pipe(7)
- Linux man-pages:fifo(7)
- Linux man-pages:shmget(2)
- Linux man-pages:shmat/shmdt(2)
- Linux man-pages:shmctl(2)
- Linux man-pages:msgop(2)
- Linux man-pages:semop(2)
- Linux man-pages:sysvipc(7)
推荐标签:
Linux进程间通信管道FIFO共享内存消息队列信号量系统编程