进程间通信(IPC)完整回顾
本文档整理 Linux 下进程间通信(IPC)的完整知识体系,覆盖管道、FIFO、文件 IPC 与 mmap,按「概念 → 原理 → 函数原型 → 案例 → 易错点」逐节展开,适合复习与查阅。
- 环境:Linux + g++(C++17)。
- 编译:
g++ -std=c++17 -Wall -g xxx.cpp -o xxx - 约定:每个「案例」都是完整、可直接编译运行的 C++17 程序(含头文件与
main),一个程序对应一个.cpp文件;代码统一分行、缩进,方便逐行阅读和回顾。 - 说明:IPC 接口本身是 POSIX C 接口,C++ 中直接包含
<unistd.h>、<fcntl.h>等系统头文件即可调用;本文用 C++ 写法(std::cout、nullptr、static_cast、constexpr)来组织代码。
目录
- 进程间通信常见方式
- 管道的特质
- 管道的基本用法
- 管道读写行为(必背)
- 父子进程通信练习分析(ls | wc -l)
- 总结 / 复习
- 父子进程 lswc-l
- 兄弟进程间通信
- 多个读写端操作管道和管道缓冲区大小
- 命名管道 FIFO 的创建和原理
- FIFO 实现非血缘关系进程间通信
- 文件用于进程间通信
- mmap 函数原型
- 复习(mmap 前)
- mmap 建立映射区
- mmap 使用注意事项 1
- mmap 使用注意事项 2
- mmap 总结
- 父子进程间 mmap 通信
- 无血缘关系进程间 mmap 通信
- mmap 总结(二)
- mmap 匿名映射区
- 总结(全章)
1. 进程间通信常见方式
概念
进程间通信(IPC,Inter-Process Communication)指多个进程之间交换数据或同步动作的机制。Linux 下常见方式:
- 管道(pipe):匿名管道,只能用于有血缘关系的进程。
- 命名管道(FIFO):有路径名,可用于无血缘关系进程。
- 信号(signal):异步通知机制,开销小,但不适合传大量数据。
- 文件:多个进程打开同一普通文件读写。
- mmap 存储映射:把文件或匿名内存映射到进程地址空间,多进程映射同一对象即可共享。
- 消息队列:内核维护的消息链表,可按类型收发。
- 共享内存(System V / POSIX):多进程映射同一块物理内存,速度最快,但需同步。
- 信号量:主要用于同步/互斥,不传数据。
- socket:本地套接字(AF_UNIX)可做 IPC,跨主机用网络套接字。
原理
- 管道/FIFO:内核缓冲区 + 文件描述符,读写走 read/write。
- 文件:内核页缓存,多进程打开同一文件共享内容,但无阻塞通知。
- mmap:把文件页或匿名页映射进进程地址空间,访问内存即访问共享对象。
- 信号:内核向进程投递异步事件。
- 消息队列/共享内存/信号量:System V 或 POSIX IPC 对象,由内核维护。
- socket:内核协议栈,AF_UNIX 走本机。
重点注意
- 管道只能亲缘进程;FIFO、文件 mmap 可无血缘。
- mmap 和共享内存本身不提供互斥同步,多写必须加锁/信号量/原子操作。
- 信号适合"通知",不适合"传数据"。
- 普通文件 IPC 没有管道式阻塞/EOF 通知。
- 选型口诀:亲缘小数据用 pipe;无血缘用 FIFO 或文件 mmap;要同步加信号量/锁;跨主机用 socket。
补充
- 文档把 mmap 标为"非血缘关系进程间",是在强调它比匿名管道适用范围广。
- socket 稳定但重;管道简单但有血缘限制;信号开销小但不传数据。
2. 管道的特质
概念
管道是内核中的伪文件,不占磁盘空间,由 pipe() 创建,返回两个文件描述符:fd[0] 读端,fd[1] 写端。
原理
- 内核借助环形队列机制,使用内核缓冲区实现。
- 数据只能一次读取,读走就没了。
- 数据只能单向流动,是半双工通信。
- 管道没有名字,靠
fork继承文件描述符给子进程。
重点注意(文档列的局限性)
- 自己写不能自己读。
- 数据不可反复读。
- 半双工通信。
- 只能用于有血缘关系的进程间。
函数原型
#include <unistd.h>
int pipe(int fd[2]);
// 成功返回 0;失败返回 -1,errno 置位。
// fd[0] 读端,fd[1] 写端。
int pipe2(int fd[2], int flags); // Linux 扩展
// flags: O_NONBLOCK / O_CLOEXEC案例:创建管道,查看 fd 方向与容量
// pipe_basic.cpp
#include <cstdio>
#include <iostream>
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd[2];
// pipe() 成功返回 0;fd[0] 读端,fd[1] 写端
if (pipe(fd) < 0) {
std::perror("pipe");
return 1;
}
std::cout << "fd[0] = " << fd[0] << "(读端)\n";
std::cout << "fd[1] = " << fd[1] << "(写端)\n";
// F_GETPIPE_SZ 查询管道缓冲区大小;g++ 默认已定义 _GNU_SOURCE
int size = fcntl(fd[1], F_GETPIPE_SZ);
if (size < 0) {
std::perror("fcntl");
} else {
std::cout << "pipe capacity = " << size << " bytes\n"; // 常见 65536
}
close(fd[0]);
close(fd[1]);
return 0;
}补充
- 管道是"伪文件",
ls -l看到大小为 0,不能lseek,不能反复读。 - 管道缓冲区大小可用
fcntl(fd, F_GETPIPE_SZ)查,Linux 常见 64KB。 F_GETPIPE_SZ/F_SETPIPE_SZ是 Linux 扩展。g++ 编译时自动定义_GNU_SOURCE,可直接使用;用 gcc 写 C 程序时需手动#define _GNU_SOURCE。
3. 管道的基本用法
概念
pipe() 创建管道后,fork() 子进程继承两端;父子进程按通信方向各自关闭不用的端,然后读写。
原理
fork后子进程复制父进程的 fd 表,fd[0]、fd[1]都指向同一管道。- 通信方向由"谁关哪端"决定:读进程关写端,写进程关读端。
- 所有写端关闭后,读端
read返回 0(EOF)。
重点注意
- 必须关闭不用的端,否则读端等不到 EOF、写端可能一直有读端引用。
fd[0]读、fd[1]写,别反。- 父进程写完可
sleep一秒,避免终端提示符与输出混杂(文档原提醒)。
函数原型
int pipe(int fd[2]);
pid_t fork(void); // 子返回 0,父返回子 pid,失败 -1
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
int close(int fd);
pid_t waitpid(pid_t pid, int *wstatus, int options);案例:父子基本读写
// pipe_rw.cpp:父进程写,子进程读
#include <cstdio>
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
int main() {
int fd[2];
if (pipe(fd) < 0) {
std::perror("pipe");
return 1;
}
pid_t pid = fork();
if (pid < 0) {
std::perror("fork");
return 1;
}
if (pid == 0) {
// 子进程:读
close(fd[1]); // 关写端,否则等不到 EOF
char buf[64];
ssize_t n = read(fd[0], buf, sizeof(buf) - 1);
if (n > 0) {
buf[n] = '\0';
std::cout << "child got: " << buf << '\n';
}
close(fd[0]);
_exit(0);
}
// 父进程:写
close(fd[0]);
write(fd[1], "hello", 5);
close(fd[1]); // 关闭后子进程 read 才返回 0
waitpid(pid, nullptr, 0);
return 0;
}补充
- 若父子都要读写,需两个管道。
read可能部分读,严谨代码要循环。
4. 管道读写行为(必背)
概念
管道的读写行为由"有无数据、有无对端"决定,是管道最核心的考点。
原理 / 行为表
读管道:
| 条件 | 行为 |
|---|---|
| 管道有数据 | read 返回实际读到的字节数 |
| 管道无数据,有写端 | read 阻塞等待 |
| 管道无数据,无写端 | read 返回 0(类似文件尾 EOF) |
写管道:
| 条件 | 行为 |
|---|---|
| 无读端 | 异常终止(SIGPIPE) |
| 有读端,管道已满 | 阻塞等待 |
| 有读端,管道未满 | 返回写出的字节个数 |
重点注意
- 读端全关再写 →
SIGPIPE默认终止进程;忽略后write返回 -1,errno = EPIPE。 - 写端全关,
read返回 0,不是 -1。 - 管道容量从 Linux 2.6.11 起通常 65536 字节(64KB)。
PIPE_BUF在 Linux 上 4096B;≤PIPE_BUF的 write 是原子的,多写端并发不交错。- 大于
PIPE_BUF的写可能交错。
函数原型
#include <csignal>
#include <fcntl.h>
// signal() 原型
void (*signal(int signum, void (*handler)(int)))(int);
// fcntl() 原型(cmd 传 F_GETPIPE_SZ / F_SETPIPE_SZ)
int fcntl(int fd, int cmd, ...);
// 典型用法(写在函数体内):
// signal(SIGPIPE, SIG_IGN); // 忽略 SIGPIPE
// int size = fcntl(fd[1], F_GETPIPE_SZ); // 查容量
#define PIPE_BUF 4096 // Linux 常见值案例 1:写端全关,read 返回 0(EOF)
// eof.cpp
#include <cstdio>
#include <iostream>
#include <unistd.h>
int main() {
int fd[2];
char buf[32];
if (pipe(fd) < 0) {
std::perror("pipe");
return 1;
}
write(fd[1], "hello", 5);
close(fd[1]); // 关闭唯一的写端
ssize_t n = read(fd[0], buf, sizeof(buf));
std::cout << "first read = " << n << '\n'; // 5
n = read(fd[0], buf, sizeof(buf));
std::cout << "second read = " << n << "(0 表示 EOF)\n"; // 0
close(fd[0]);
return 0;
}案例 2:读端全关,write 触发 SIGPIPE / EPIPE
// epipe.cpp
#include <cerrno>
#include <csignal>
#include <cstdio>
#include <iostream>
#include <unistd.h>
int main() {
int fd[2];
if (pipe(fd) < 0) {
std::perror("pipe");
return 1;
}
close(fd[0]); // 关闭唯一的读端
std::signal(SIGPIPE, SIG_IGN); // 不忽略的话进程会被 SIGPIPE 终止
ssize_t n = write(fd[1], "x", 1);
if (n < 0 && errno == EPIPE) {
std::cout << "write failed: EPIPE(没有读端)\n";
} else {
std::cout << "write returned " << n << '\n';
}
close(fd[1]);
return 0;
}补充
- 多写端写大数据可能交错;小数据(≤
PIPE_BUF)原子。 - 所有写端关闭后读端才 EOF;所有读端关闭后写端才 SIGPIPE。
5. 父子进程通信练习分析(ls | wc -l)
概念
用管道 + fork + dup2 + exec 实现 shell 中的 ls -l | wc -l。
原理
- 子进程
ls的 stdout 重定向到管道写端。 - 父进程
wc的 stdin 重定向到管道读端。 dup2把管道 fd 复制到标准输入/输出。
重点注意
dup2后关闭原 fd,避免多余引用。execlp成功不返回,失败才走_exit(127)。- 父进程若需回收子进程,要
waitpid(本案例父进程 exec 后由 shell 等)。 - 关闭不用端是 EOF 正确到达的关键。
函数原型
int dup2(int oldfd, int newfd); // 成功返回 newfd,失败 -1
int execlp(const char *file, const char *arg, ...);案例
// lswc_pipe.cpp:等价于 ls -l | wc -l
#include <cstdio>
#include <unistd.h>
int main() {
int fd[2];
if (pipe(fd) < 0) {
std::perror("pipe");
return 1;
}
pid_t pid = fork();
if (pid < 0) {
std::perror("fork");
return 1;
}
if (pid == 0) {
// 子进程:wc -l,从管道读
close(fd[1]); // 关写端
dup2(fd[0], STDIN_FILENO); // stdin -> 管道读端
close(fd[0]);
execlp("wc", "wc", "-l", nullptr);
_exit(127); // exec 失败才会到这里
}
// 父进程:ls -l,写到管道
close(fd[0]); // 关读端
dup2(fd[1], STDOUT_FILENO); // stdout -> 管道写端
close(fd[1]);
execlp("ls", "ls", "-l", nullptr);
_exit(127);
}补充
- 若要父进程回收两个子进程,可 fork 两个子分别 ls/wc,父关两端并
waitpid两次。 dup2会自动关闭newfd原来打开的内容。
6. 总结 / 复习
概念
复习管道 + fork 五件套:pipe → fork → close → dup2 → exec → wait。
重点注意
- 先画"谁读谁写、哪些端该关"。
- 父进程不用管道时也要关闭两端,保证数据单向流动。
- 循环创建 N 个子进程时,用循环因子
i标识兄弟进程。
补充
- 兄弟进程通信是父进程创建管道、子进程读写、父进程关闭两端并回收的模型。
7. 父子进程 lswc-l
概念
同第 5 节,用管道实现 ls -l | wc -l,是父子进程通信的经典练习。
原理
- 子进程写、父进程读,或反过来,取决于谁
exec谁。 - 管道写端接
ls的 stdout,读端接wc的 stdin。
重点注意
- 子进程写、父进程读时,子关读端、父关写端。
dup2后关闭原 fd。- 关闭不用端是 EOF 正确到达的关键。
案例:子进程 ls 写、父进程 wc 读
// lswc_child.cpp
#include <cstdio>
#include <unistd.h>
int main() {
int fd[2];
if (pipe(fd) < 0) {
std::perror("pipe");
return 1;
}
pid_t pid = fork();
if (pid < 0) {
std::perror("fork");
return 1;
}
if (pid == 0) {
// 子进程:ls -l,写到管道
close(fd[0]); // 关读端
dup2(fd[1], STDOUT_FILENO); // stdout -> 管道写端
close(fd[1]);
execlp("ls", "ls", "-l", nullptr);
_exit(127);
}
// 父进程:wc -l,从管道读
close(fd[1]); // 关写端
dup2(fd[0], STDIN_FILENO); // stdin -> 管道读端
close(fd[0]);
execlp("wc", "wc", "-l", nullptr);
_exit(127);
}补充
- 变体:父 ls 写、子 wc 读,同样可行。
- 父进程 exec 后由 shell 回收子进程;若父进程不 exec,需要自己
waitpid。
8. 兄弟进程间通信
概念
兄弟进程通过父进程创建的管道通信,父进程不参与读写。
原理
- 父进程
pipe()后fork多个子进程。 - 每个子进程继承两端,各自关闭不用端。
- 父进程关闭两端并
waitpid回收。
重点注意
- 父进程不使用管道,一定要关闭父进程的管道两端,保证数据单向流动(文档原话)。
- 每个子进程也要关闭自己不用的端。
- 父进程
waitpid回收所有子进程,避免僵尸。 - 一个 pipe 一个写端多个读端:可;多个写端一个读端:可,但需调控写入顺序。
函数原型
pid_t waitpid(pid_t pid, int *wstatus, int options);案例:兄 ls、弟 wc -l
// siblings.cpp
#include <cstdio>
#include <unistd.h>
#include <sys/wait.h>
int main() {
int fd[2];
if (pipe(fd) < 0) {
std::perror("pipe");
return 1;
}
pid_t writer = fork();
if (writer < 0) {
std::perror("fork");
return 1;
}
if (writer == 0) {
// 兄:ls 写到管道
close(fd[0]);
dup2(fd[1], STDOUT_FILENO);
close(fd[1]);
execlp("ls", "ls", nullptr);
_exit(127);
}
pid_t reader = fork();
if (reader < 0) {
std::perror("fork");
return 1;
}
if (reader == 0) {
// 弟:从管道读给 wc -l
close(fd[1]);
dup2(fd[0], STDIN_FILENO);
close(fd[0]);
execlp("wc", "wc", "-l", nullptr);
_exit(127);
}
// 父进程不使用管道,两端都关掉
close(fd[0]);
close(fd[1]);
waitpid(writer, nullptr, 0);
waitpid(reader, nullptr, 0);
return 0;
}补充
- 循环创建 N 个子进程时,用
i区分兄/弟。 - 父进程不关两端可能导致读端等不到 EOF。
9. 多个读写端操作管道和管道缓冲区大小
概念
测试多写端/多读端并发行为,以及管道缓冲区容量。
原理
- 管道容量从 Linux 2.6.11 起通常 65536 字节(64KB)。
PIPE_BUF在 Linux 上 4096B;≤PIPE_BUF的 write 原子,多写端不交错。- 所有写端关闭后读端才 EOF;所有读端关闭后写端触发 SIGPIPE。
重点注意
- 多写端写大数据可能交错。
- 文档提醒"需要调控写入顺序才行"。
- 一个读端多个写端可行,但并发写要小心交错。
函数原型
int fcntl(int fd, int cmd, ...); // cmd 用 F_GETPIPE_SZ / F_SETPIPE_SZ案例:一读多写
// multi_writer.cpp:一个父进程读,两个子进程写
#include <cstdio>
#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/wait.h>
int main() {
int fd[2];
if (pipe(fd) < 0) {
std::perror("pipe");
return 1;
}
for (int i = 0; i < 2; ++i) {
pid_t pid = fork();
if (pid < 0) {
std::perror("fork");
return 1;
}
if (pid == 0) {
// 子进程:写一条短消息
close(fd[0]);
std::string msg = "child" + std::to_string(i) + "\n";
write(fd[1], msg.data(), msg.size());
close(fd[1]); // 所有写端关闭后读端才 EOF
_exit(0);
}
}
// 父进程:读
close(fd[1]);
char buf[256];
ssize_t n;
while ((n = read(fd[0], buf, sizeof(buf))) > 0) {
std::cout.write(buf, n);
}
close(fd[0]);
while (wait(nullptr) > 0) {
}
return 0;
}补充
- 管道容量可用
fcntl(F_SETPIPE_SZ)调整,但受系统限制。 - 多写端并发时,只有 ≤
PIPE_BUF的写保证不交错。
10. 命名管道 FIFO 的创建和原理
概念
FIFO(命名管道)是文件系统中有路径名的管道,用于无血缘关系进程间通信。
原理
mkfifo创建特殊文件,ls -l显示类型p。- 进程像普通文件一样
open、read、write。 - 内核仍用管道缓冲区,数据一次性读取。
- 有路径名,所以无血缘进程可通过路径打开同一 FIFO。
重点注意
openFIFO 默认阻塞等对端:只读打开等写端,只写打开等读端。O_NONBLOCK可改变行为:非阻塞写打开无读端返回 -1/ENXIO;非阻塞读打开无写端可成功但read返回 0。- 用完
unlink删除名字。 - 数据读走就没了,多读端是"并集"。
函数原型
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
// 成功 0;失败 -1;已存在 errno = EEXIST
int open(const char *pathname, int flags, ...);
int unlink(const char *pathname);案例:创建 FIFO 并写入
// fifo_create.cpp:先运行本程序,它会阻塞在 open,等读端出现后写入再退出
#include <cstdio>
#include <cerrno>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
int main() {
const char *path = "/tmp/demo.fifo";
// 已存在不算错
if (mkfifo(path, 0666) < 0 && errno != EEXIST) {
std::perror("mkfifo");
return 1;
}
int fd = open(path, O_WRONLY); // 阻塞,直到有进程以只读打开
if (fd < 0) {
std::perror("open");
return 1;
}
write(fd, "fifo", 4);
close(fd);
return 0;
}补充
- 命令:
mkfifo /tmp/demo.fifo。 - FIFO 名字存在于文件系统,但数据不落盘。
11. FIFO 实现非血缘关系进程间通信
概念
一个写进程写 FIFO,一个读进程读 FIFO,像操作普通文件。
原理
- 两个无血缘进程打开同一 FIFO 路径,内核缓冲区共享。
- 数据一次性读取,读走即消失。
重点注意
- 数据一旦被读走就没了;多读端的并集才是写端写入的数据(文档原话)。
- 多写端可同时写同一 FIFO。
open阻塞顺序:先启动读端或写端都可能互相等待。- 用完
unlink删除名字。
案例:writer / reader(两个程序,开两个终端运行)
两个文件分别保存:g++ -std=c++17 writer.cpp -o writer、g++ -std=c++17 reader.cpp -o reader。
先开终端 A 运行 ./reader(阻塞等写端),再开终端 B 运行 ./writer(配对成功后写入)。
// writer.cpp
#include <cstdio>
#include <cerrno>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
int main() {
const char *path = "/tmp/demo.fifo";
if (mkfifo(path, 0666) < 0 && errno != EEXIST) {
std::perror("mkfifo");
return 1;
}
int fd = open(path, O_WRONLY); // 阻塞,等待 reader 打开读端
if (fd < 0) {
std::perror("open");
return 1;
}
write(fd, "hello", 5);
close(fd);
return 0;
}// reader.cpp
#include <cstdio>
#include <iostream>
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd = open("/tmp/demo.fifo", O_RDONLY); // 阻塞,等待 writer
if (fd < 0) {
std::perror("open");
return 1;
}
char buf[32];
ssize_t n = read(fd, buf, sizeof(buf) - 1);
if (n > 0) {
buf[n] = '\0';
std::cout << "reader got: " << buf << '\n';
}
close(fd);
unlink("/tmp/demo.fifo"); // 用完删除名字
return 0;
}补充
- 先跑读端或写端都可,配对后继续。
- 非阻塞打开的行为要单独测试(
O_NONBLOCK)。
12. 文件用于进程间通信
概念
多个进程打开同一普通文件,通过读写文件通信。
原理
- 打开的文件是内核中的一块缓冲区。
- 有血缘进程
fork后文件描述符可能相同;无血缘进程 fd 可能不同,但只要打开同一文件就可通信。 - 文件内容持久,可重复读。
重点注意
- 普通文件没有管道式阻塞/EOF 通知,需文件锁、轮询或额外同步。
fork共享同一 fd 时文件偏移共享;各自open偏移独立。- 效率低,易竞争,不适合高实时通信。
flock是建议锁。
函数原型
int open(const char *path, int flags, mode_t mode);
off_t lseek(int fd, off_t offset, int whence);
int fsync(int fd);
int flock(int fd, int operation); // LOCK_SH / LOCK_EX / LOCK_UN / LOCK_NB案例:writer / reader 加锁读写(两个程序)
// writer.cpp:加排他锁,从文件末尾追加写
#include <cstdio>
#include <fcntl.h>
#include <unistd.h>
#include <sys/file.h>
int main() {
int fd = open("ipc.file", O_RDWR | O_CREAT, 0666);
if (fd < 0) {
std::perror("open");
return 1;
}
if (flock(fd, LOCK_EX) < 0) {
std::perror("flock");
close(fd);
return 1;
}
lseek(fd, 0, SEEK_END); // 追加写
write(fd, "data", 4);
fsync(fd); // 落盘(可选)
flock(fd, LOCK_UN);
close(fd);
return 0;
}// reader.cpp:加共享锁,从文件开头读
#include <cstdio>
#include <iostream>
#include <fcntl.h>
#include <unistd.h>
#include <sys/file.h>
int main() {
int fd = open("ipc.file", O_RDONLY);
if (fd < 0) {
std::perror("open");
return 1;
}
if (flock(fd, LOCK_SH) < 0) {
std::perror("flock");
close(fd);
return 1;
}
char buf[64];
ssize_t n = read(fd, buf, sizeof(buf) - 1);
if (n > 0) {
buf[n] = '\0';
std::cout << "read: " << buf << '\n';
}
flock(fd, LOCK_UN);
close(fd);
return 0;
}补充
- 需要强制同步可用信号量/POSIX 锁。
- 文件 IPC 可重复读,但无"新数据到达"通知。
13. mmap 函数原型
概念
存储映射 I/O:把磁盘文件映射到进程地址空间,读写内存即读写文件。
原理
mmap建立映射;munmap释放;msync落盘。- 映射建立时隐含一次对文件的读操作。
- 映射区与文件页缓存关联,
MAP_SHARED修改反映到文件。
重点注意
addr通常 NULL;length > 0;offset必须页对齐。- 失败返回
MAP_FAILED((void*)-1),不是 NULL。 MAP_SHARED修改反映到文件;MAP_PRIVATE写时复制不反映。- fd 在 mmap 创建完成后即可关闭。
函数原型
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
// 成功返回映射区首地址;失败返回 MAP_FAILED
int munmap(void *addr, size_t length);
// 成功 0;失败 -1
int msync(void *addr, size_t length, int flags);
// MS_SYNC / MS_ASYNC
int ftruncate(int fd, off_t length);
// 调整文件大小,mmap 前常用参数表
| 参数 | 说明 |
|---|---|
| addr | 通常 NULL,系统自动分配 |
| length | 映射区大小,应 ≤ 文件实际大小 |
| prot | PROT_READ / PROT_WRITE / PROT_READ | PROT_WRITE |
| flags | MAP_SHARED 共享写回;MAP_PRIVATE 写时复制 |
| fd | 文件描述符;匿名映射传 -1 |
| offset | 默认 0;必须 4K 整数倍 |
案例:完整的最小 mmap 程序
// mmap_basic.cpp
#include <cstdio>
#include <cstring>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
constexpr size_t kSize = 4096;
int main() {
int fd = open("mapfile", O_RDWR | O_CREAT | O_TRUNC, 0666);
if (fd < 0) {
std::perror("open");
return 1;
}
if (ftruncate(fd, kSize) < 0) {
std::perror("ftruncate");
close(fd);
return 1;
}
void *raw = mmap(nullptr, kSize, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
if (raw == MAP_FAILED) {
std::perror("mmap");
close(fd);
return 1;
}
close(fd); // 映射已建立,fd 可以关闭
auto *p = static_cast<char *>(raw);
std::strcpy(p, "hello mmap"); // 写内存 = 写文件
if (msync(p, kSize, MS_SYNC) < 0) {
std::perror("msync");
}
if (munmap(p, kSize) < 0) {
std::perror("munmap");
}
return 0;
}补充
- 匿名映射:
mmap(nullptr, kSize, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0)。 MAP_ANONYMOUS在 g++ 下可直接使用(自动定义_GNU_SOURCE);用 gcc 编译 C 时需_GNU_SOURCE或_DEFAULT_SOURCE。
14. 复习(mmap 前)
概念
复习管道/FIFO/文件 IPC 与 mmap 的定位差异。
重点注意
- 管道/FIFO 数据一次性;mmap 可重复读。
- 文件 IPC 无阻塞通知;mmap 也无同步,需额外锁。
- 管道亲缘;FIFO/mmap 文件映射可无血缘。
补充
- mmap 适合大块数据、可重复读、无血缘文件映射。
15. mmap 建立映射区
概念
打开文件 → 调整大小 → mmap 建立映射 → 访问内存 → munmap。
原理
- 映射建立时隐含一次对文件的读操作。
- 映射区与文件页缓存关联,
MAP_SHARED修改写回文件。
重点注意
- 文件必须有实际大小,否则可能 SIGBUS。
length不要超过文件大小;越界访问可能 SIGSEGV。- mmap 成功后 fd 可关闭。
- 失败检查
MAP_FAILED。
案例:完整流程(建立映射并验证内容已写回文件)
// mmap_flow.cpp:open -> ftruncate -> mmap -> 访问内存 -> munmap
#include <cstdio>
#include <cstring>
#include <iostream>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
constexpr size_t kSize = 4096;
int main() {
int fd = open("mapfile", O_RDWR | O_CREAT | O_TRUNC, 0666);
if (fd < 0) {
std::perror("open");
return 1;
}
// 1. 文件必须有实际大小,否则访问映射区会 SIGBUS
if (ftruncate(fd, kSize) < 0) {
std::perror("ftruncate");
close(fd);
return 1;
}
// 2. 建立映射(此处隐含一次对文件的读操作)
void *raw = mmap(nullptr, kSize, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
if (raw == MAP_FAILED) {
std::perror("mmap");
close(fd);
return 1;
}
auto *p = static_cast<char *>(raw);
// 3. 访问内存 = 访问文件
std::strcpy(p, "hello mmap");
// 4. 落盘后从 fd 读,验证内存内容确实写回了文件
if (msync(p, kSize, MS_SYNC) < 0) {
std::perror("msync");
}
char buf[32] = {};
ssize_t n = pread(fd, buf, sizeof(buf) - 1, 0);
std::cout << "pread from file: " << n
<< " bytes, content = \"" << buf << "\"\n";
// 5. 释放映射、关闭文件
if (munmap(p, kSize) < 0) {
std::perror("munmap");
}
close(fd);
return 0;
}补充
- 推荐保险调用:
mmap(nullptr, 有效文件大小, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)。 - 文件大小可用
ftruncate调整。
16. mmap 使用注意事项 1
重点注意(文档密集易错点)
- 文件大小为 0,却指定非 0 大小创建映射区 → 总线错误(SIGBUS)。
- 文件大小为 0,指定 0 大小 → 无效参数(EINVAL)。
- 文件只读,映射区却要读写 → 无效参数。
MAP_SHARED时,mmap 读写权限应 ≤ 文件 open 权限;只写打开文件不行。- fd 在 mmap 创建完成后即可关闭。
offset必须 4096 整数倍(MMU 最小映射单位 4K)。- 不能越界访问映射区。
案例:逐个复现上述错误
程序会自动创建一个 0 字节的 zero.file,然后按四种模式运行,观察各自的报错方式:
./mmap_err einval # length 为 0 -> mmap 返回 EINVAL
./mmap_err perm # 只读打开却要读写 -> mmap 返回 EINVAL
./mmap_err offset # offset 不是 4K 倍数 -> mmap 返回 EINVAL
./mmap_err bus # 文件 0 字节却访问 -> SIGBUS(进程被杀死)// mmap_err.cpp
#include <cstdio>
#include <iostream>
#include <string_view>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
constexpr size_t kPage = 4096;
int main(int argc, char *argv[]) {
if (argc < 2) {
std::cerr << "usage: " << argv[0] << " einval | perm | offset | bus\n";
return 1;
}
// 准备一个 0 字节文件
int zfd = open("zero.file", O_RDWR | O_CREAT | O_TRUNC, 0666);
if (zfd < 0) {
std::perror("open zero.file");
return 1;
}
close(zfd);
int fd = -1;
void *p = MAP_FAILED;
size_t len = kPage;
off_t offset = 0;
std::string_view mode = argv[1];
if (mode == "perm") {
// 只读打开,却要 PROT_READ | PROT_WRITE
fd = open("zero.file", O_RDONLY);
if (fd < 0) {
std::perror("open");
return 1;
}
p = mmap(nullptr, len, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
} else if (mode == "offset") {
// offset 不是 4096 的整数倍
fd = open("zero.file", O_RDWR);
if (fd < 0) {
std::perror("open");
return 1;
}
offset = 100;
p = mmap(nullptr, len, PROT_READ, MAP_SHARED, fd, offset);
} else if (mode == "einval") {
// length 为 0
fd = open("zero.file", O_RDWR);
if (fd < 0) {
std::perror("open");
return 1;
}
len = 0;
p = mmap(nullptr, len, PROT_READ, MAP_SHARED, fd, 0);
} else {
// 文件 0 字节,却映射 4096 字节并访问
fd = open("zero.file", O_RDWR);
if (fd < 0) {
std::perror("open");
return 1;
}
p = mmap(nullptr, kPage, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
if (p == MAP_FAILED) {
std::perror("mmap");
close(fd);
return 1;
}
std::cout << "mmap ok, touch memory beyond file size...\n";
// 访问超出文件大小的页 -> SIGBUS
*static_cast<volatile char *>(p) = 'x';
std::cout << "never reached\n";
}
if (p == MAP_FAILED) {
std::perror("mmap");
close(fd);
return 1;
}
std::cout << "mmap ok: " << p << '\n';
munmap(p, len);
close(fd);
return 0;
}补充
- 推荐保险调用:
mmap(nullptr, 有效文件大小, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)。 - 文件大小可用
ftruncate调整。
17. mmap 使用注意事项 2
重点注意
munmap传入地址必须是 mmap 返回的原地址,坚决杜绝p++后再munmap。MAP_PRIVATE修改只在内存有效,不反映到磁盘。MAP_PRIVATE只需 open 时有读权限即可创建映射区。- 映射区释放与文件关闭无关。
- mmap 出错概率高,必须检查
MAP_FAILED。
案例:munmap 传错地址 + MAP_PRIVATE 不写回
// munmap_private.cpp
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
constexpr size_t kSize = 4096;
int main() {
int fd = open("private.file", O_RDWR | O_CREAT | O_TRUNC, 0666);
if (fd < 0) {
std::perror("open");
return 1;
}
if (ftruncate(fd, kSize) < 0) {
std::perror("ftruncate");
close(fd);
return 1;
}
void *raw = mmap(nullptr, kSize, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
if (raw == MAP_FAILED) {
std::perror("mmap");
close(fd);
return 1;
}
auto *p = static_cast<char *>(raw);
// 1. munmap 传错地址:p + 1 不是页对齐地址 -> EINVAL
if (munmap(p + 1, kSize) < 0) {
std::cout << "munmap(p+1) failed: "
<< std::strerror(errno) << '\n';
}
// 必须传 mmap 返回的原地址
if (munmap(p, kSize) < 0) {
std::perror("munmap");
}
// 2. MAP_PRIVATE:修改只在内存有效
raw = mmap(nullptr, kSize, PROT_READ | PROT_WRITE,
MAP_PRIVATE, fd, 0);
if (raw == MAP_FAILED) {
std::perror("mmap");
close(fd);
return 1;
}
p = static_cast<char *>(raw);
std::strcpy(p, "private-data");
msync(p, kSize, MS_SYNC); // msync 也不会把它写回文件
std::cout << "in memory: " << p << '\n';
munmap(p, kSize);
char buf[32] = {};
if (pread(fd, buf, sizeof(buf) - 1, 0) > 0) {
std::cout << "file content: [" << buf << "]\n"; // 空:不写回
}
// 3. MAP_PRIVATE 只需 open 有读权限:O_RDONLY 的 fd 也能建可写映射
int fd2 = open("private.file", O_RDONLY);
if (fd2 < 0) {
std::perror("open");
return 1;
}
raw = mmap(nullptr, kSize, PROT_READ | PROT_WRITE,
MAP_PRIVATE, fd2, 0);
if (raw == MAP_FAILED) {
std::perror("mmap with O_RDONLY fd");
} else {
std::cout << "MAP_PRIVATE + O_RDONLY fd: mmap ok\n";
munmap(raw, kSize);
}
close(fd2);
close(fd);
return 0;
}补充
- 多进程
MAP_SHARED写同一映射区需同步,否则数据竞争。 munmap的length应与 mmap 时一致或按页取整。
18. mmap 总结
概念
mmap 是"内存即文件"的共享机制,适合大块数据、可重复读、无血缘文件映射。
重点注意
- 创建映射隐含读操作。
MAP_SHARED权限 ≤ 文件打开权限;MAP_PRIVATE无所谓。- 映射释放与 fd 关闭无关。
- 文件必须有实际大小。
munmap原地址。offset4K 对齐。- 检查
MAP_FAILED。
补充
- 与 FIFO 对比:mmap 可重复读;FIFO 读走即消失。
- mmap 本身无同步,多写要加锁/信号量/原子。
19. 父子进程间 mmap 通信
概念
父进程先建立 MAP_SHARED 映射,再 fork,父子共享映射区。
原理
fork继承映射;MAP_SHARED的修改互相可见。MAP_PRIVATE子写父看不到。
重点注意
- 必须
MAP_SHARED;MAP_PRIVATE不共享写。 - 映射要在
fork前建立,或父子各自映射同一文件。 - 父子都需
munmap。
案例:父子通过文件映射共享写
// mmap_fork.cpp
#include <cstdio>
#include <cstring>
#include <iostream>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/wait.h>
constexpr size_t kSize = 4096;
int main() {
int fd = open("share", O_RDWR | O_CREAT | O_TRUNC, 0666);
if (fd < 0) {
std::perror("open");
return 1;
}
if (ftruncate(fd, kSize) < 0) {
std::perror("ftruncate");
close(fd);
return 1;
}
// 映射必须在 fork 之前建立,MAP_SHARED 才能父子互相看见
void *raw = mmap(nullptr, kSize, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
if (raw == MAP_FAILED) {
std::perror("mmap");
close(fd);
return 1;
}
close(fd);
auto *p = static_cast<char *>(raw);
pid_t pid = fork();
if (pid < 0) {
std::perror("fork");
return 1;
}
if (pid == 0) {
// 子进程写
std::strcpy(p, "child-data");
_exit(0);
}
wait(nullptr); // 等子进程写完
std::cout << "parent sees: " << p << '\n'; // child-data
munmap(p, kSize);
return 0;
}补充
- 匿名映射也可:
MAP_SHARED | MAP_ANONYMOUS+fork。 - 父子共享写需同步,避免竞争。
20. 无血缘关系进程间 mmap 通信
概念
两个无血缘进程打开同一文件,各自 mmap(MAP_SHARED),一个写一个读。
原理
- 文件页缓存共享;
MAP_SHARED修改互相可见。 - 数据可重复读,读走不消失。
重点注意
- 不能用匿名映射;必须借助磁盘文件(或
/dev/shm文件)。 - mmap 可重复读;FIFO 读走就没了。
- 多读端频繁读会读到重复内容,因为写端未更新。
- 需同步机制(信号量/文件锁/原子标志)。
案例:writer / reader(两个独立进程,靠 ready 标志同步)
writer 创建 /tmp/demo_shm 文件并映射共享内存,写入 msg 后置 ready=1;reader 映射同一文件,轮询等 ready 变 1 后读出 msg,完成两个无血缘进程通过 MMAP 共享内存通信。
// shm.h
#pragma once
#include <atomic>
#include <cstddef>
struct Shared {
std::atomic<int> ready{0}; // 0=未就绪 1=已就绪
char msg[64];
};
inline constexpr const char *PATH = "/tmp/demo_shm";
inline constexpr std::size_t SIZE = sizeof(Shared);// writer.cpp
#include "shm.h"
#include <cstdio>
#include <cstring>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
int main() {
unlink(PATH);
int fd = open(PATH, O_RDWR | O_CREAT | O_TRUNC, 0666);
if (fd < 0) {
perror("open");
return 1;
}
if (ftruncate(fd, SIZE) != 0) {
perror("ftruncate");
return 1;
}
auto *s = static_cast<Shared *>(
mmap(nullptr, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0));
if (s == MAP_FAILED) {
perror("mmap");
return 1;
}
close(fd);
s->ready.store(0, std::memory_order_relaxed);
std::strncpy(s->msg, "hello from writer", sizeof(s->msg) - 1);
s->msg[sizeof(s->msg) - 1] = '\0';
s->ready.store(1, std::memory_order_release);
sleep(5);
munmap(s, SIZE);
unlink(PATH);
}// reader.cpp
#include "shm.h"
#include <cstdio>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
int main() {
int fd = open(PATH, O_RDWR);
if (fd < 0) {
perror("open");
return 1;
}
auto *s = static_cast<Shared *>(
mmap(nullptr, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0));
if (s == MAP_FAILED) {
perror("mmap");
return 1;
}
close(fd);
while (s->ready.load(std::memory_order_acquire) == 0) {
usleep(1000);
}
printf("reader got: %s\n", s->msg);
munmap(s, SIZE);
}补充
volatile不是同步;生产用信号量/文件锁/原子操作。msync是落盘,不是进程间可见性的唯一手段。
21. mmap 总结(二)
重点注意
- 与 FIFO 数据语义对比:mmap 可重复读,FIFO 一次性。
- 无血缘必须文件映射;匿名只亲缘。
- 共享写必须
MAP_SHARED+ 同步。
补充
- 多读端 mmap:每个读进程都能读到完整内容,可能重复读。
- 多读端 FIFO:并集才是写端写入的数据。
22. mmap 匿名映射区
概念
匿名映射:不关联文件,fd = -1,offset = 0。
原理
- 内核分配匿名内存页;
fork后子进程继承映射,可共享。 - 没有文件路径,无血缘进程无法打开同一对象。
重点注意
- 只能用于血缘关系进程间通信(文档原话)。
- 必须
MAP_SHARED才能父子共享写;MAP_PRIVATE不共享。 - 无血缘不能用匿名映射,需文件映射/FIFO/共享内存。
函数原型
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
// 匿名映射:addr 传 nullptr,fd 传 -1,offset 传 0
// void *p = mmap(nullptr, size, PROT_READ | PROT_WRITE,
// MAP_SHARED | MAP_ANONYMOUS, -1, 0);案例:匿名映射 + fork 父子共享
// mmap_anon.cpp
#include <cstdio>
#include <cstring>
#include <iostream>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/wait.h>
constexpr size_t kSize = 4096;
int main() {
// fd = -1:不关联任何文件,因此无法被无血缘进程打开
void *raw = mmap(nullptr, kSize, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if (raw == MAP_FAILED) {
std::perror("mmap");
return 1;
}
auto *p = static_cast<char *>(raw);
pid_t pid = fork();
if (pid < 0) {
std::perror("fork");
return 1;
}
if (pid == 0) {
// 子进程写
std::strcpy(p, "anon");
_exit(0);
}
wait(nullptr); // 等子进程写完
std::cout << "parent sees: " << p << '\n'; // anon(MAP_SHARED 才可见)
munmap(p, kSize);
return 0;
}补充
- g++ 自动定义
_GNU_SOURCE,MAP_ANONYMOUS可直接使用;用 gcc 编译 C 时需_GNU_SOURCE或_DEFAULT_SOURCE。 - 匿名映射也需
munmap。
23. 总结(全章)
概念回顾
- 管道:亲缘、半双工、一次性读、内核缓冲区。
- FIFO:无血缘、路径名、open 阻塞等对端、一次性读。
- 文件:无血缘、无阻塞通知、需锁/轮询、可重复读。
- mmap:亲缘/无血缘(文件映射)、可重复读、需同步、
MAP_SHARED才共享写。
重点注意总表
| 易错点 | 对应 |
|---|---|
| fd[0] 读、fd[1] 写 | pipe |
| fork 后必须关不用端 | pipe/FIFO |
| 读端全关写 → SIGPIPE | pipe |
| 写端全关 read 返回 0 | pipe |
| 管道容量 64KB,PIPE_BUF 4096B | pipe |
| FIFO open 阻塞等对端 | FIFO |
| 普通文件无阻塞通知 | 文件 IPC |
| mmap offset 4K 对齐、文件要有大小 | mmap |
| mmap 失败返回 MAP_FAILED | mmap |
| MAP_SHARED 才共享写 | mmap |
| munmap 传原地址 | mmap |
| 匿名映射只亲缘 | mmap |
| mmap 可重复读,FIFO 读走就没 | 对比 |
函数速查
int pipe(int fd[2]);
int pipe2(int fd[2], int flags);
pid_t fork(void);
int dup2(int oldfd, int newfd);
int execlp(const char *file, const char *arg, ...);
pid_t waitpid(pid_t pid, int *wstatus, int options);
int mkfifo(const char *pathname, mode_t mode);
int open(const char *pathname, int flags, ...);
int unlink(const char *pathname);
int flock(int fd, int operation);
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);
int msync(void *addr, size_t length, int flags);
int ftruncate(int fd, off_t length);
int fcntl(int fd, int cmd, ...);一句话复习路线
管道读写四条行为 → pipe+fork+close+dup2+exec+wait 骨架 → FIFO mkfifo+open 阻塞特性 → 文件 IPC 无通知需锁 → mmap 六坑:文件有大小、offset 4K、MAP_SHARED、MAP_FAILED、munmap 原地址、匿名只亲缘。
