编程进阶网 编程进阶网
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机的原理
  • 操作系统原理
  • 网络协议原理
  • 数据库的原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 技能之旅
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Kotlin精通
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机的原理
  • 操作系统原理
  • 网络协议原理
  • 数据库的原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 技能之旅
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Kotlin精通
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • Android提升进阶

  • iOS开发和进阶

  • Web开发和进阶

  • Linux应用开发

    • Linux应用开发
    • QML基础入门

    • QT核心库实践

    • Linux系统编程

      • Linux系统编程基础
      • 系统调用原理精讲
      • 文件IO深度解析
      • 进程管理深度解析
      • 线程并发深度解读
      • 信号处理深度解读
      • 进程间通信IPC
        • 网络IO多路复用
        • 内存管理深度解读
        • 动态链接与共享库
        • Linux与定时器
      • 综合项目实战

    • IoT智能硬件开发

    • Apps
    • Linux应用开发
    • Linux系统编程
    杨充
    2025-07-02
    目录

    进程间通信IPC

    # 06.进程间通信IPC

    嵌入式 Qt 应用很少有单进程架构——UI 进程 + 后台服务进程 + 守护进程 构成了典型的进程树。本节覆盖 pipe/FIFO/SysV IPC/Unix Domain Socket/mmap 五种 IPC 机制,给出嵌入式场景的选型矩阵。

    # 目录介绍

    • 6.1 案例引入
      • 6.1.1 UI 进程和后台服务通信延迟超过 50ms
      • 6.1.2 核心问题
    • 6.2 管道 pipe 与 FIFO
      • 6.2.1 匿名管道的局限性
      • 6.2.2 命名管道 FIFO 的创建与使用
      • 6.2.3 管道缓冲区大小与写阻塞
    • 6.3 System V IPC
      • 6.3.1 消息队列 msgget/msgsnd/msgrcv
      • 6.3.2 共享内存 shmget/shmat/shmdt
      • 6.3.3 信号量 semget/semop 同步保护
      • 6.3.4 System V IPC 的残留清理陷阱
    • 6.4 Unix Domain Socket
      • 6.4.1 与 TCP Socket 的差异
      • 6.4.2 SOCK_STREAM vs SOCK_DGRAM
      • 6.4.3 抽象命名空间 abstract namespace
      • 6.4.4 传递文件描述符 SCM_RIGHTS
    • 6.5 mmap 共享内存
      • 6.5.1 MAP_SHARED vs MAP_PRIVATE
      • 6.5.2 匿名映射与文件映射
      • 6.5.3 多进程之间的同步协议
    • 6.6 嵌入式 IPC 选型矩阵
      • 6.6.1 五种 IPC 的性能对比表
      • 6.6.2 不同场景的推荐方案
      • 6.6.3 Qt 跨进程方案 (QSharedMemory/QDBus) 与原生 Linux IPC 对比
    • 6.7 关键结论与速查表

    # 6.1 案例引入

    # 6.1.1 UI 进程和后台服务通信延迟超过 50ms

    某智能家居中控,QML UI 进程需要实时读取后台 Zigbee 网关服务的设备状态。最初使用 TCP localhost socket,发现状态变化的平均延迟 5-15ms,偶发 50ms+。切换到 Unix Domain Socket + SCM_RIGHTS 传递共享内存 fd 后,稳定在 0.5ms 以内。

    同一台设备上的跨进程通信,为什么 TCP 比 Unix Domain Socket 慢 30 倍? 答案在内核的网络协议栈 vs 本地文件系统路径上。

    TCP localhost 的完整数据路径:

    UI 进程                 内核                            后台进程
      │                     │                                │
      │ send(buf, 128)      │                                │
      ├────────────────────→│                                │
      │                     │ ① TCP 分段 + 封包              │
      │                     │ ② IP 路由(lo 接口)           │
      │                     │ ③ 网络协议栈(netfilter/iptables hook,即使 localhost 也要过)
      │                     │ ④ TCP 校验和 + 序列号管理      │
      │                     │ ⑤ sk_buff 分配 + 拷贝(至少 2 次)
      │                     │ ⑥ 拥塞控制 / 流量控制检查       │
      │                     │                                │
      │                     │                    recv() ←────┤
      │                     │←───────────────────────────────┤
    

    Unix Domain Socket 的数据路径:

    UI 进程                 内核                            后台进程
      │                     │                                │
      │ send(buf, 128)      │                                │
      ├────────────────────→│                                │
      │                     │ ① 直接把 sk_buff 挂到对端接收队列
      │                     │ ② 唤醒对端(阻塞在 recv 的进程)
      │                     │                                │
      │                     │                    recv() ←────┤
      │                     │←───────────────────────────────┤
    

    核心差异:UDS 跳过整个 TCP/IP 协议栈——不封包、不校验、不路由、不拥塞控制。数据在内核内存中直接传递(零拷贝)。

    # 6.1.2 核心问题

    1. 管道 vs 消息队列 vs 共享内存——各自适用什么场景?
    2. System V IPC 资源泄露如何预防(ipcs/ipcrm 不是长久之计)?
    3. 如何在多进程之间安全地共享一块 mmap 内存?
    4. SCM_RIGHTS 传递文件描述符的机制是什么?能否用这种方法实现"不经过磁盘文件的 mmap 共享"?

    # 6.2 管道 pipe 与 FIFO

    # 6.2.1 匿名管道的局限性

    匿名管道(pipe)——最古老的 IPC,单向、只限父子进程:

    int fd[2];
    if (pipe(fd) < 0) { perror("pipe"); exit(1); }
    
    pid_t pid = fork();
    if (pid == 0) {
        close(fd[0]);          // 子:只写
        write(fd[1], "hello", 5);
        close(fd[1]);
    } else {
        close(fd[1]);          // 父:只读
        char buf[16];
        read(fd[0], buf, sizeof(buf));
        printf("received: %.5s\n", buf);
        close(fd[0]);
    }
    

    pipe 的五大局限:

    局限 说明 后果
    单向 只能一端写一端读 双向需两个 pipe
    仅限父子 pipe() 返回的 fd 只能由 fork 继承 无亲缘进程无法用
    无名字 无文件系统实体 不能用 open 打开
    无消息边界 字节流,无边界 需自定协议分帧
    缓冲区固定 默认 16 页(64 KB) 写超缓冲则阻塞

    pipe2——原子设置 O_NONBLOCK 和 O_CLOEXEC:

    int fd[2];
    pipe2(fd, O_NONBLOCK | O_CLOEXEC);  // 一次 syscall 设两个 flag
    

    pipe() 在 Shell 中的经典应用:

    # ls 的输出通过管道给 wc,wc 必须等 ls 完成才收到 EOF
    ls -l / | wc -l
    
    # 内核实现:
    # 1. shell 调 pipe() 创建一对 fd
    # 2. shell fork,子进程 dup2(fd[1], STDOUT),exec ls
    # 3. shell fork,子进程 dup2(fd[0], STDIN),exec wc
    # 4. shell close 两个 fd
    

    # 6.2.2 命名管道 FIFO 的创建与使用

    FIFO 是有名字的管道——创建文件系统节点,任意两个进程都可以用 open 连接:

    // 进程 A:创建 FIFO 并写入
    mkfifo("/tmp/myfifo", 0666);
    int fd = open("/tmp/myfifo", O_WRONLY);
    write(fd, "hello from A", 12);
    close(fd);
    
    // 进程 B:打开 FIFO 并读取(可完全独立运行,无需亲缘关系)
    int fd = open("/tmp/myfifo", O_RDONLY);
    char buf[128];
    int n = read(fd, buf, sizeof(buf));
    printf("B got: %.*s\n", n, buf);
    close(fd);
    

    FIFO 的 open 阻塞规则:

    打开模式 行为
    O_RDONLY 阻塞,直到另一个进程以写方式打开(O_WRONLY / O_RDWR)
    O_WRONLY 阻塞,直到另一个进程以读方式打开
    O_RDWR 不阻塞
    O_RDONLY \| O_NONBLOCK 立即返回(即使无写者)
    O_WRONLY \| O_NONBLOCK 失败返回 -1,errno = ENXIO(无读者)

    典型模式——FIFO 作为服务端的多客户端入口:

    // 服务端:为每个客户端创建独立 FIFO 回复
    #define REQUEST_FIFO  "/tmp/server_req"
    #define RESPONSE_FIFO "/tmp/client_%d_resp"
    
    // 服务端:循环读请求,为每个客户写回复
    int req_fd = open(REQUEST_FIFO, O_RDONLY);
    while (1) {
        struct request req;
        read(req_fd, &req, sizeof(req));
    
        char resp_path[64];
        snprintf(resp_path, sizeof(resp_path), RESPONSE_FIFO, req.client_id);
        int resp_fd = open(resp_path, O_WRONLY);
        write(resp_fd, &response, sizeof(response));
        close(resp_fd);
    }
    

    # 6.2.3 管道缓冲区大小与写阻塞

    管道容量由内核参数决定:

    # Linux 默认管道大小(从 2.6.11 起可改)
    cat /proc/sys/fs/pipe-max-size      # 最大可设值(通常 1 MB)
    

    fcntl F_SETPIPE_SZ 动态修改单管道容量:

    int pipe_sz = fcntl(fd[1], F_GETPIPE_SZ);  // 读取当前大小(默认 64 KB)
    fcntl(fd[1], F_SETPIPE_SZ, 1024 * 1024);    // 设为 1 MB(须 root 或有 CAP_SYS_RESOURCE)
    

    写阻塞的三个触发条件:

    条件 行为 示例
    write 数据 ≤ PIPE_BUF(4096)且管道有空间 原子写入,不阻塞 小写入不交叉
    write 数据 > PIPE_BUF 且管道空间不够 阻塞直到有足够空间 大写入可能阻塞
    设置了 O_NONBLOCK 立即返回 -1,errno = EAGAIN 非阻塞模式

    PIPE_BUF 的原子性保证——小于等于 PIPE_BUF 的写入不会被其他写入者的数据交叉。这是多进程同时写 FIFO 的基础:

    // 多个进程同时向同一 FIFO 写日志——PIPE_BUF 保证每行完整
    // 进程 A: write(fd, "[A] log message\n", 15);  // < PIPE_BUF → 原子
    // 进程 B: write(fd, "[B] log message\n", 15);  // 同上
    // FIFO 内容: "[A] log message\n[B] log message\n" 或相反顺序
    // 不会出现: "[A] log [B] log message\nmessage\n"
    

    # 6.3 System V IPC

    # 6.3.1 消息队列 msgget/msgsnd/msgrcv

    System V 消息队列是内核中的链表结构——每个消息有类型(mtype),接收方可按类型选择接收:

    #include <sys/msg.h>
    
    struct my_msg {
        long mtype;           // 必须:消息类型(> 0)
        char text[256];       // 消息体
    };
    
    // 创建/获取消息队列
    key_t key = ftok("/tmp/msq_key", 'A');  // 生成 IPC 键
    int msqid = msgget(key, IPC_CREAT | 0666);
    
    // 发送
    struct my_msg msg = {.mtype = 1};
    strcpy(msg.text, "hello");
    msgsnd(msqid, &msg, strlen(msg.text) + 1, 0);
    
    // 接收(按类型接收)
    struct my_msg rcv;
    ssize_t n = msgrcv(msqid, &rcv, sizeof(rcv.text), 1, 0);  // 只收 mtype=1
    printf("got: %s\n", rcv.text);
    

    msgrcv 的类型匹配规则:

    msgtyp 值 行为
    0 接收队列中第一条消息(无视类型)
    > 0 接收第一条指定类型的消息
    < 0 接收队列中类型 ≤ |msgtyp| 的最小类型消息

    消息队列 vs FIFO:

    特性 FIFO SysV 消息队列
    消息边界 无(字节流) 有(天然按消息分界)
    优先级/类型 无 有(mtype 多路分发)
    持久性 进程退出自动清理 内核级持久(如不显式删除,重启才清)
    容量 管道缓冲(64 KB) 可配(msg_qbytes)

    # 6.3.2 共享内存 shmget/shmat/shmdt

    共享内存是速度最快的 IPC——数据不经过内核中转,双方直接读写同一物理页:

    #include <sys/shm.h>
    
    // 创建共享内存段(4 KB)
    key_t key = ftok("/tmp/shm_key", 'B');
    int shmid = shmget(key, 4096, IPC_CREAT | 0666);
    
    // 附加到进程地址空间
    char *shared = (char *)shmat(shmid, NULL, 0);
    
    // 进程 A 写入
    strcpy(shared, "shared data");
    shared[0] = 'X';              // 直接改写——无系统调用!
    
    // 进程 B 读取(另一进程,同一 shmid)
    printf("B sees: %s\n", shared);   // "Xhared data"
    
    // 解除附加
    shmdt(shared);
    
    // 标记删除(最后一个 detach 的进程真正释放)
    shmctl(shmid, IPC_RMID, NULL);
    

    共享内存为什么最快?

    管道:    进程A → write (syscall) → 内核缓冲区 → read (syscall) → 进程B
    消息队列: 进程A → msgsnd (syscall) → 内核链表 → msgrcv (syscall) → 进程B
    共享内存: 进程A → 直接写 → 同一物理页 ← 直接读 ← 进程B
               无 syscall!无拷贝!
    

    共享内存的致命缺陷——无同步机制。进程 A 写了一半被进程 B 读到的是不完整数据。需要信号量(semaphore)或 futex 配合使用。

    # 6.3.3 信号量 semget/semop 同步保护

    SysV 信号量不是简单的 int——是一个"信号量集合",每个集合可含多个信号量:

    #include <sys/sem.h>
    
    // 创建含 1 个信号量的集合(初值 = 1 → 二元信号量/互斥锁)
    key_t key = ftok("/tmp/sem_key", 'C');
    int semid = semget(key, 1, IPC_CREAT | 0666);
    semctl(semid, 0, SETVAL, 1);               // 第 0 个信号量初值为 1
    
    // P 操作(wait / down)——减 1,如果结果 < 0 则阻塞
    struct sembuf op = {.sem_num = 0, .sem_op = -1, .sem_flg = 0};
    semop(semid, &op, 1);                      // wait
    
    // 临界区
    shared->counter++;
    
    // V 操作(post / up)——加 1,如有等待者唤醒之
    op.sem_op = 1;
    semop(semid, &op, 1);                      // post
    
    // 删除集合
    semctl(semid, 0, IPC_RMID);
    

    sembuf 的 sem_op 三种取值:

    sem_op 操作 条件
    > 0 加 sem_op 到当前值(V 操作) 不阻塞
    < 0 当前值 + sem_op ≥ 0 才通过;否则阻塞(P 操作) 阻塞直到满足
    = 0 等待信号量值为 0 阻塞直到值为 0

    sem_flg 标志:

    标志 含义
    0 不满足条件则阻塞
    IPC_NOWAIT 不满足立即返回 EAGAIN
    SEM_UNDO 进程退出时自动撤销操作(防止死锁)

    SEM_UNDO 的防死锁价值——如果进程在持锁期间崩溃,内核在清理进程资源时自动做逆向操作(+1 恢复信号量),防止其他进程永久阻塞。

    # 6.3.4 System V IPC 的残留清理陷阱

    System V IPC 资源是内核级持久对象——不随进程退出而自动释放(共享内存在有 attach 者时不释放,消息队列和信号量集永不自动释放)。

    典型事故:

    进程 A 创建消息队列,msgsnd 一条消息,异常崩溃
    → 消息队列仍在内核中 → msgrcv 的人永远等不到下一条
    → 后续重启的进程 A 又创建一个新的消息队列
    → 旧队列永远残留 → 占用内核内存
    → 长期运行后 → ipcs 显示 500 条废弃消息队列
    

    查看和清理:

    # 查看所有 SysV IPC 资源
    ipcs            # 全部
    ipcs -q         # 消息队列
    ipcs -m         # 共享内存
    ipcs -s         # 信号量
    
    # 删除指定资源
    ipcrm -q <msqid>
    ipcrm -m <shmid>
    ipcrm -s <semid>
    

    预防措施——进程退出前显式销毁:

    // 1. 用 atexit 注册清理函数
    void cleanup_ipc() {
        msgctl(msqid, IPC_RMID, NULL);
        shmctl(shmid, IPC_RMID, NULL);
        semctl(semid, 0, IPC_RMID);
    }
    atexit(cleanup_ipc);
    
    // 2. shmctl IPC_RMID 后立即 shmdt——最后一个 detach 的进程触发真正释放
    shmctl(shmid, IPC_RMID, NULL);
    shmdt(shared);
    
    // 3. Systemd 服务中,用 IPCPrivateNamespace=yes 隔离 IPC 命名空间
    // 服务停止时整个命名空间的 IPC 资源自动清理
    

    # 6.4 Unix Domain Socket

    # 6.4.1 与 TCP Socket 的差异

    Unix Domain Socket(UDS)是本地进程通信的事实标准——拥有和 TCP 相同的 API,但没有网络协议栈开销:

    // 服务端
    int srv = socket(AF_UNIX, SOCK_STREAM, 0);
    
    struct sockaddr_un addr = {.sun_family = AF_UNIX};
    strcpy(addr.sun_path, "/tmp/my.sock");
    unlink(addr.sun_path);                      // 清理上次残留
    bind(srv, (struct sockaddr *)&addr, sizeof(addr));
    listen(srv, 5);
    
    int client = accept(srv, NULL, NULL);
    char buf[256];
    read(client, buf, sizeof(buf));
    close(client);
    close(srv);
    unlink(addr.sun_path);
    

    TCP vs UDS 性能根源:

    维度 TCP localhost Unix Domain Socket
    网络协议栈 完整经过(IP/TCP 层) 完全绕过
    数据拷贝次数 2-3 次(user↔kernel↔sk_buff) 1 次(user↔kernel)
    校验和 计算 不计算
    iptables/netfilter 经过 不经过
    吞吐量(同机) ~10-30 Gbps ~40-80 Gbps
    延迟(同机) 5-15 μs 2-5 μs
    SCM_RIGHTS ❌ ✅
    SO_PEERCRED ❌ ✅(获取对端 PID/UID/GID)

    获取对端进程凭证:

    struct ucred cred;
    socklen_t len = sizeof(cred);
    getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cred, &len);
    printf("peer: pid=%d uid=%d gid=%d\n", cred.pid, cred.uid, cred.gid);
    

    这是实现权限校验的利器——不同于 TCP 连接中完全无法验证对端是谁。

    # 6.4.2 SOCK_STREAM vs SOCK_DGRAM

    SOCK_STREAM(字节流):面向连接,保证顺序,无消息边界。

    SOCK_DGRAM(数据报):无连接,保留消息边界,但受限于 SO_SNDBUF 大小:

    // SOCK_DGRAM 服务端(无需 listen/accept)
    int fd = socket(AF_UNIX, SOCK_DGRAM, 0);
    bind(fd, (struct sockaddr *)&addr, sizeof(addr));
    struct sockaddr_un client_addr;
    socklen_t len = sizeof(client_addr);
    char buf[4096];
    ssize_t n = recvfrom(fd, buf, sizeof(buf), 0,
                         (struct sockaddr *)&client_addr, &len);
    

    选择规则:

    场景 SOCK_STREAM SOCK_DGRAM
    数据量 > 64 KB ✅ ❌(超出缓冲区会截断)
    需要消息边界 ❌ ✅
    顺序有要求 ✅ ⚠️(不保证,但 UDS 下极少乱序)
    多客户端服务 ✅(accept 天然多对多) ⚠️(需一个 socket 绑定不同路径)

    # 6.4.3 抽象命名空间 abstract namespace

    UDS 路径存在文件系统中——进程退出后残留的 socket 文件没人清理。抽象命名空间解决了这个问题:

    // 普通命名空间:sun_path = "/tmp/my.sock" → 文件系统中可见
    // 抽象命名空间:sun_path[0] = '\0' → 无文件系统实体!
    struct sockaddr_un addr = {.sun_family = AF_UNIX};
    addr.sun_path[0] = '\0';
    strcpy(addr.sun_path + 1, "my_abstract_sock");   // 名字不占文件系统
    socklen_t addr_len = offsetof(struct sockaddr_un, sun_path)
                         + 1 + strlen("my_abstract_sock");
    bind(fd, (struct sockaddr *)&addr, addr_len);
    

    抽象命名空间的好处:

    • 进程退出后自动消失(无残留文件)
    • 不占文件系统 inode
    • 不受文件系统权限控制(全靠 socket 层权限)
    • 适合嵌入式系统中 /tmp 是 tmpfs(重启清零)的场景

    # 6.4.4 传递文件描述符 SCM_RIGHTS

    SCM_RIGHTS 是 UDS 的杀手级功能——通过 socket 在两个进程间传递文件描述符:

    // 发送方:把共享内存 fd 发给对端
    void send_fd(int sock, int fd_to_send) {
        struct msghdr msg = {0};
        char buf[CMSG_SPACE(sizeof(int))];
        memset(buf, 0, sizeof(buf));
    
        struct iovec io = {.iov_base = "F", .iov_len = 1};  // 最少 1 字节数据
        msg.msg_iov = &io;
        msg.msg_iovlen = 1;
    
        msg.msg_control = buf;
        msg.msg_controllen = sizeof(buf);
    
        struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
        cmsg->cmsg_level = SOL_SOCKET;
        cmsg->cmsg_type = SCM_RIGHTS;
        cmsg->cmsg_len = CMSG_LEN(sizeof(int));
        memcpy(CMSG_DATA(cmsg), &fd_to_send, sizeof(int));
    
        if (sendmsg(sock, &msg, 0) < 0) perror("sendmsg");
    }
    
    // 接收方:拿到发送方传来的 fd
    int recv_fd(int sock) {
        struct msghdr msg = {0};
        char data;
        struct iovec io = {.iov_base = &data, .iov_len = 1};
        msg.msg_iov = &io;
        msg.msg_iovlen = 1;
    
        char cbuf[CMSG_SPACE(sizeof(int))];
        msg.msg_control = cbuf;
        msg.msg_controllen = sizeof(cbuf);
    
        if (recvmsg(sock, &msg, 0) < 0) { perror("recvmsg"); return -1; }
    
        struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
        int fd;
        memcpy(&fd, CMSG_DATA(cmsg), sizeof(int));
        return fd;
    }
    

    SCM_RIGHTS 在本章案例中的应用:

    1. 后台服务进程创建共享内存(shm_open + ftruncate + mmap)
    2. 后台服务把共享内存 fd 通过 UDS + SCM_RIGHTS 发给 UI 进程
    3. UI 进程接收 fd → mmap 同一 shm → 双方读写同一物理内存
    4. 通知机制:后台服务写完后通过 UDS 发一个通知(1 字节即可)
       UI 进程在 epoll 中同时监听 UDS 可读 + shared memory 用信号量同步
    

    # 6.5 mmap 共享内存

    # 6.5.1 MAP_SHARED vs MAP_PRIVATE

    mmap 可以创建进程间的共享内存映射:

    // 共享映射:修改对其它映射者可见,写入回磁盘
    void *shared = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
                        MAP_SHARED, fd, 0);
    
    // 私有映射(COW):修改私有化,不影响其它进程/磁盘
    void *priv = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
                      MAP_PRIVATE, fd, 0);
    
    特性 MAP_SHARED MAP_PRIVATE
    修改对其他进程可见 ✅ ❌(COW 后各自独立)
    修改写入磁盘 ✅(msync 确保) ❌
    父子进程 fork 后 双方看到同一物理页 fork 时为 SHARED,写时 COW
    用途 进程间共享数据 加载只读数据 + 局部修改

    # 6.5.2 匿名映射与文件映射

    文件映射(有 fd):mmap 文件的某段内容到内存——数据持久化在磁盘中。

    匿名映射(无 fd):MAP_ANONYMOUS 映射的是"虚无"——等价于 malloc 但可共享给子进程:

    // 匿名共享映射——父子进程共享同一块内存(无文件)
    void *anon = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
                      MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    
    // fork 后——子进程的 anon 指针指向同一物理页
    pid_t pid = fork();
    if (pid == 0) {
        strcpy((char *)anon, "child wrote this");
    } else {
        wait(NULL);
        printf("parent sees: %s\n", (char *)anon);  // "child wrote this"
    }
    

    shm_open——POSIX 共享内存(比 SysV 更现代):

    #include <sys/mman.h>
    #include <fcntl.h>
    
    // POSIX 共享内存 = /dev/shm 下的文件 + mmap
    int fd = shm_open("/my_shm", O_RDWR | O_CREAT, 0666);
    ftruncate(fd, 4096);
    void *ptr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    // 多进程间通过同一 shm name 共享 ptr
    

    shm_open 底层是 /dev/shm/ tmpfs 上的文件——天然享受内核的 page cache 和回写机制。

    # 6.5.3 多进程之间的同步协议

    共享内存 + 信号量 = 完整的 IPC 解决方案。以下是"单生产者单消费者"的标准协议:

    // 协议:
    //   write_pos:生产者写入的位置(被写入的数据对消费者可见)
    //   read_pos: 消费者读取的位置(缓冲区空间对生产者可用)
    //   信号量:s_empty(空间数)和 s_full(数据项数)
    
    struct shm_ring {
        volatile uint32_t write_pos;
        volatile uint32_t read_pos;
        sem_t s_empty;      // 初始值为 BUFFER_SIZE
        sem_t s_full;       // 初始值为 0
        char data[BUFFER_SIZE];
    };
    
    void producer(struct shm_ring *ring, char c) {
        sem_wait(&ring->s_empty);                // 等空间
        ring->data[ring->write_pos % BUFFER_SIZE] = c;
        __sync_synchronize();                    // 内存屏障(ARM 必加!)
        ring->write_pos++;
        sem_post(&ring->s_full);                 // 通知消费者
    }
    
    char consumer(struct shm_ring *ring) {
        sem_wait(&ring->s_full);                 // 等数据
        char c = ring->data[ring->read_pos % BUFFER_SIZE];
        __sync_synchronize();
        ring->read_pos++;
        sem_post(&ring->s_empty);               // 通知生产者(空间释放)
        return c;
    }
    

    关键——内存屏障:ARM 是弱内存模型——write_pos++ 在 ARM 上可能被重排到 ring->data[...] = c 之前!消费者读到 write_pos 已增加但 data 还是旧值。__sync_synchronize()(等价 dmb ish)阻止这种重排。

    性能优化的进一步方案——把信号量换成 futex:sem_wait/sem_post 每次都要系统调用。用 futex 实现用户态快速路径 + 竞争时陷入内核:

    // 用户态自旋等待(无锁快速路径)
    while (__sync_sub_and_fetch(&ring->write_pos, 0) == ring->read_pos) {
        // 空,可选短暂的 cpu_relax()
    }
    // 拿到数据后:
    __sync_add_and_fetch(&ring->read_pos, 1);
    

    # 6.6 嵌入式 IPC 选型矩阵

    # 6.6.1 五种 IPC 的性能对比表

    实测数据(ARM Cortex-A7 @ 1 GHz,传输 1 KB 消息的端到端延迟):

    IPC 机制 延迟 吞吐 (MB/s) 数据拷贝次数 适用数据量
    匿名管道 (pipe) ~8 μs ~120 2(user↔kernel) 小~中
    FIFO (命名管道) ~8 μs ~120 2 小~中
    SysV 消息队列 ~12 μs ~80 2 小(<= 8 KB/条)
    Unix Domain Socket ~5 μs ~200 1 小~大
    mmap 共享内存 + 信号量 ~2 μs ~2000+ 0 任意

    mmap 的吞吐极限——不经过系统调用,性能接近 CPU 内存带宽(ARM Cortex-A7 DDR3 ~2 GB/s)。唯一瓶颈是同步信号量的开销(每次 ~500 ns)。

    # 6.6.2 不同场景的推荐方案

    场景 推荐方案 原因
    父子进程简单通信 pipe / pipe2 零配置,fork 自动继承
    无亲缘进程,小数据 UDS (SOCK_STREAM) 高吞吐,API 统一,支持 SCM_RIGHTS
    无亲缘进程,广播 UDS (SOCK_DGRAM) + 抽象命名空间 消息边界保留 + 无文件残留
    高频大数据(视频帧/传感器原始数据) mmap 共享内存 + futex/POSIX 信号量 零拷贝,直通内存带宽
    可恢复的消息队列 SysV 消息队列 / POSIX mq 内核持久,崩溃后可恢复
    进程基础架构(DBus/服务总线) UDS (SOCK_STREAM) 标准化 + 多客户端 + 权限校验
    只读共享配置 mmap (MAP_SHARED) 文件 多进程自动同步(Page Cache 一致)
    日志多进程写 FIFO + O_APPEND 等价的多进程写管道 PIPE_BUF 原子性保证

    # 6.6.3 Qt 跨进程方案与原生 Linux IPC 对比

    方案 底层 适用 开发复杂度 性能
    QSharedMemory SysV shmget 或 shm_open(平台相关) 同一机器的 Qt 进程间 低 最高
    QDBus UDS 桌面 Linux 标准 IPC 中 中
    QLocalSocket / QLocalServer UDS 同一机器 低 高
    QProcess + 标准 I/O 匿名管道 父子进程 低 中
    原生 POSIX shm + futex mmap + futex 性能极致需求 高 最高

    Qt 嵌入式推荐:

    • 通用跨进程数据共享 → QLocalSocket(UDS 的高层封装)
    • 高频传感器数据 → QSharedMemory + QSystemSemaphore
    • Qt 与第三方非 Qt 进程通信 → 原生 shm_open + mmap + POSIX 信号量

    # 6.7 关键结论与速查表

    核心结论五条:

    1. mmap 共享内存是最快的 IPC——数据不经过内核中转,零拷贝;代价是必须自行实现同步协议
    2. UDS 是同机 IPC 的最佳通用方案——性能仅次于共享内存,API 兼容 TCP,天然支持 SCM_RIGHTS 和 SO_PEERCRED
    3. SysV IPC 是遗留技术——资源不随进程退出自动清理,容易残留;POSIX shm_open + mmap 是更现代的选择
    4. 管道适用于父子进程——pipe 是最简单的 IPC,但受限于单向、无消息边界、仅限亲缘
    5. 嵌入式 IPC 优先 UDS + mmap——UDS 做控制通道(通知、fd 传递),mmap 做数据通道(零拷贝大数据传输)

    五种 IPC 速查表:

    机制 亲缘关系 消息边界 持久性 双向 最适数据量
    pipe 仅父子 无 进程级 否 < 64 KB
    FIFO 任意 无 进程级(文件残留) 否 < 64 KB
    SysV 消息队列 任意 有 内核级 可以(mtype 多路复用) < 8 KB/条
    UDS 任意 无/有(DGRAM) 进程级(可文件残留) ✅ 任意
    mmap 共享内存 任意 无(自行实现) 文件级或进程级 ✅ 任意

    IPC 选型决策树:

    需要持久化(崩溃恢复)?
      ├─ 是 → SysV 消息队列 / POSIX mq
      └─ 否 → 数据量?
               ├─ < 4 KB → UDS (简单,性能够)
               ├─ 4 KB - 1 MB → UDS (依然是最好的)
               └─ > 1 MB → mmap 共享内存 + UDS(控制通道)
                   高频小数据?
                     └─ 是 → mmap 环形缓冲区 + futex 通知
    

    常见陷阱速查:

    陷阱 根因 解决
    SysV IPC 资源泄漏 进程异常退出未调 IPC_RMID atexit 清理 / IPC 命名空间隔离
    FIFO 残留 inode 进程退出前未 unlink 用抽象命名空间 UDS 替代
    共享内存数据竞争 无同步机制 信号量 / futex + 内存屏障
    SCM_RIGHTS fd 泄露 接收方忘记 close 接收后立即 dup 再关原始 fd
    管道写阻塞 管道缓冲区满 设 O_NONBLOCK 或消费端加速
    TCP localhost 延迟高 经过完整网络协议栈 换成 UDS

    下一步:把你的智能家居中控的 TCP localhost 通信替换为 UDS + mmap 环形缓冲区——用 SCM_RIGHTS 传递共享内存 fd,验证延迟是否从 15ms 降到 0.5ms 以内。


    延伸阅读:

    • man 7 unix —— Unix Domain Socket 详解
    • man 2 shm_open —— POSIX 共享内存
    • man 7 mq_overview —— POSIX 消息队列
    • man 3 sem_overview —— POSIX 信号量
    • 《The Linux Programming Interface》(Kerrisk) —— 第 43-48、54-57 章
    上次更新: 2026/07/12, 19:39:51
    信号处理深度解读
    网络IO多路复用

    ← 信号处理深度解读 网络IO多路复用→

    最近更新
    01
    audit
    07-27
    02
    C++入门教程全章思考题汇编
    07-24
    03
    12.技术团队建设能力
    07-21
    更多文章>
    Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 鄂ICP备2024073355号-1 | 鄂ICP备2024073355号
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式