进程间通信(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)来组织代码。

目录

  1. 进程间通信常见方式
  2. 管道的特质
  3. 管道的基本用法
  4. 管道读写行为(必背)
  5. 父子进程通信练习分析(ls | wc -l)
  6. 总结 / 复习
  7. 父子进程 lswc-l
  8. 兄弟进程间通信
  9. 多个读写端操作管道和管道缓冲区大小
  10. 命名管道 FIFO 的创建和原理
  11. FIFO 实现非血缘关系进程间通信
  12. 文件用于进程间通信
  13. mmap 函数原型
  14. 复习(mmap 前)
  15. mmap 建立映射区
  16. mmap 使用注意事项 1
  17. mmap 使用注意事项 2
  18. mmap 总结
  19. 父子进程间 mmap 通信
  20. 无血缘关系进程间 mmap 通信
  21. mmap 总结(二)
  22. mmap 匿名映射区
  23. 总结(全章)

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。

重点注意

  • open FIFO 默认阻塞等对端:只读打开等写端,只写打开等读端。
  • 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 原地址。
  • offset 4K 对齐。
  • 检查 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
读端全关写 → SIGPIPEpipe
写端全关 read 返回 0pipe
管道容量 64KB,PIPE_BUF 4096Bpipe
FIFO open 阻塞等对端FIFO
普通文件无阻塞通知文件 IPC
mmap offset 4K 对齐、文件要有大小mmap
mmap 失败返回 MAP_FAILEDmmap
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 原地址、匿名只亲缘。