14、进程间通信(IPC):共享内存、命名管道、消息队列的跨平台封装

进程间通信,简称 IPC,说白了就是让两个或多个进程能互相“说上话”。

我刚开始做跨平台开发时,最头疼的就是这个。Windows 上有自己的那一套,Linux 上又是另一套。你想想看,同一个功能,在 Windows 下用 CreateFileMapping,到了 Linux 就得换成 shmget,代码写得跟两门语言似的。

所以这一章,我们就来聊聊怎么把这些底层接口封装成统一的 C++ 接口。我会重点讲三种最常用的 IPC 方式:共享内存、命名管道、消息队列。

14.1 共享内存的跨平台封装

共享内存,说白了就是让两个进程直接读写同一块物理内存。这是最快的 IPC 方式,没有之一。我在项目中遇到过需要实时传输视频帧的场景,用共享内存延迟能控制在微秒级,换成别的方案根本扛不住。

但跨平台封装时,有几个坑你得注意:

  • 创建方式不同:Windows 用 CreateFileMapping + MapViewOfFile,Linux 用 shmget + shmat 或 mmap
  • 命名规则不同:Windows 的命名空间是全局的,Linux 的共享内存对象在 /dev/shm 下
  • 权限控制不同:Windows 用安全描述符,Linux 用文件权限位

核心思路:定义一个抽象基类 SharedMemory,把创建、映射、解除映射、销毁四个操作抽象成纯虚函数。然后分别实现 WindowsSharedMemory 和 LinuxSharedMemory。

下面是我个人习惯用的接口设计:

class SharedMemory {
public:
    virtual ~SharedMemory() = default;
    
    // 创建或打开共享内存
    virtual bool create(const std::string& name, size_t size) = 0;
    virtual bool open(const std::string& name) = 0;
    
    // 映射到进程地址空间
    virtual void* map() = 0;
    virtual void unmap() = 0;
    
    // 销毁
    virtual void destroy() = 0;
    
    // 获取大小
    virtual size_t size() const = 0;
};

嗯,这里要注意一点:Windows 的命名需要加上 "Global\" 前缀才能跨会话访问。我曾经因为这个坑,在远程桌面环境下调试了整整一个下午。

小技巧:在 Linux 下,我建议用 mmap + shm_open 的组合,而不是传统的 System V 接口(shmget)。因为 POSIX 接口更现代,而且支持文件描述符操作,方便集成到事件循环中。

14.2 命名管道的跨平台封装

命名管道,也叫 FIFO,是一种半双工的通信方式。它适合做“流式”数据传输,比如日志收集、命令下发等场景。

Windows 和 Linux 的命名管道差异非常大:

特性 Windows Linux
创建方式 CreateNamedPipe mkfifo
连接方式 ConnectNamedPipe / CreateFile open(阻塞或非阻塞)
读写模式 支持重叠 I/O 标准文件 I/O
命名格式 \\.\pipe\name /tmp/name 或任意路径

封装时,我建议把命名管道抽象成“服务端”和“客户端”两个角色:

class NamedPipeServer {
public:
    virtual bool create(const std::string& name) = 0;
    virtual bool waitForConnection() = 0;
    virtual size_t read(void* buffer, size_t size) = 0;
    virtual size_t write(const void* data, size_t size) = 0;
    virtual void close() = 0;
};

class NamedPipeClient {
public:
    virtual bool connect(const std::string& name) = 0;
    virtual size_t read(void* buffer, size_t size) = 0;
    virtual size_t write(const void* data, size_t size) = 0;
    virtual void close() = 0;
};

避坑指南:我曾经在 Linux 下用命名管道做双向通信,结果发现 mkfifo 创建的管道是单向的。如果你需要双向通信,要么创建两个管道,要么改用 Unix Domain Socket。Windows 的命名管道默认就是双向的,这点很容易让人产生误解。

14.3 消息队列的跨平台封装

消息队列适合做“异步”通信。发送方把消息丢进队列,接收方按需取走。这种模式在微服务架构中很常见。

跨平台封装时,你会发现:

  • Windows 的消息队列(MSMQ)太重,而且不是所有 Windows 版本都默认安装
  • Linux 的 POSIX 消息队列(mq_open)功能有限,消息大小和队列长度都有上限
  • System V 消息队列(msgget)虽然功能强,但接口老旧

我个人建议:不要直接封装系统原生的消息队列。更好的做法是在共享内存或命名管道之上,自己实现一个轻量级的消息队列。

下面是我在项目中用过的方案:

class MessageQueue {
public:
    // 初始化队列(基于共享内存)
    bool init(const std::string& name, size_t maxMessages, size_t maxMessageSize);
    
    // 发送消息(非阻塞)
    bool send(const void* data, size_t size, int priority = 0);
    
    // 接收消息(阻塞)
    bool receive(void* buffer, size_t& size, int& priority);
    
    // 尝试接收(非阻塞)
    bool tryReceive(void* buffer, size_t& size, int& priority);
    
    // 清理
    void destroy();
    
private:
    // 内部使用共享内存 + 信号量实现
    SharedMemory* shm_;
    Semaphore* sem_;
};

为什么这么做?因为系统原生的消息队列在跨平台时,行为差异太大。比如 POSIX 消息队列不支持优先级反转处理,而 System V 的消息队列在进程崩溃后可能留下垃圾数据。自己封装一层,反而更可控。

14.4 整体架构图

下面这张图展示了三种 IPC 方式在跨平台封装中的位置关系:

IPC 跨平台封装架构 应用层(业务代码) IPC 抽象接口层(统一 API) 共享内存 SharedMemory 命名管道 NamedPipe 消息队列 MessageQueue 平台适配层(条件编译 + 策略模式) Windows API POSIX API System V API

14.5 封装策略总结

做跨平台 IPC 封装,我总结了三条原则:

  1. 抽象优先:先定义好纯虚接口,再考虑平台实现。不要反过来。
  2. 条件编译要克制:只在平台适配层用 #ifdef,上层代码不要出现任何平台相关的宏。
  3. 测试要覆盖边界:进程崩溃、资源泄漏、并发读写,这些场景在跨平台下表现可能完全不同。

最后说一句:如果你只是做 Linux 下的开发,那直接用 Unix Domain Socket 就好了,它比命名管道和消息队列都灵活。但如果你必须支持 Windows,那还是老老实实按我上面说的方式封装吧。


公众号:蓝海资料掘金营,微信deep3321