在 Linux 系统开发中多进程协作是我们经常会遇到的需求。比如我们有一个控制进程需要精准控制多个工作进程的执行节奏让它们按照我们的指令来启动、暂停或者退出。这时候选择一个高效的进程间通信方式就变得尤为重要。传统的管道、消息队列虽然实现简单但是在高频交互或者大数据量的场景下性能往往不尽如人意。而基于 mmap 的共享内存作为 Linux 下最快的 IPC 方式之一正好可以解决这个问题。今天这篇文章我们就从原理到代码一步步实现一个可以精准控制多进程活动的 mmap 通信方案。一、mmap 为什么能做到这么快1.1 内存映射的本质mmap全称 Memory Mapping是 Linux 提供的一个核心系统调用。它的作用是把一个文件或者共享内存对象映射到调用进程的虚拟地址空间中。完成映射之后进程就可以像访问普通内存一样直接读写这块映射区域完全不需要调用read、write这类传统的文件操作函数。这就省去了用户态到内核态之间的数据拷贝大大降低了通信的开销。更关键的是当多个进程映射同一个共享内存对象的时候它们的虚拟地址会通过操作系统的页表机制最终映射到同一块物理内存区域。这样一来一个进程对这块内存的修改其他进程可以立刻看到这就是 mmap 实现进程间通信的核心原理。mmap 内存映射原理多个进程的虚拟地址映射到同一块物理内存1.2 为什么还需要同步机制看到这里你可能会问既然共享内存这么方便直接读写不就行了吗其实不然共享内存本身是没有任何同步机制的。如果多个进程同时读写这块内存很容易出现竞态条件 —— 比如一个进程还没把数据写完另一个进程就已经开始读了结果就会读到不完整的脏数据。所以我们必须配合同步机制来保证共享内存的安全访问。在这个方案里我们选择了互斥锁pthread_mutex和条件变量pthread_cond来实现同步互斥锁保证同一时间只有一个进程可以操作共享内存避免并发读写的冲突条件变量实现进程间的等待和唤醒让工作进程可以在没有指令的时候阻塞等待不消耗 CPU 资源这里有一个非常关键的点默认情况下互斥锁和条件变量是为线程间同步设计的只能在同一个进程的不同线程之间使用。要让它们在不同进程之间生效我们必须设置PTHREAD_PROCESS_SHARED属性把锁和条件变量标记为进程间共享的这样不同进程才能正确地识别和使用同一个锁。二、mmap与 IPC 性能对比为了让大家更直观地感受到 mmap 的性能优势我们参考了 Linux 下的实测性能数据对比了几种常见的 IPC 方式在传输 1GB 数据时的耗时情况不同 IPC 方式传输 1GB 数据的耗时对比单位ms从这张图里我们可以非常明显地看到mmap 共享内存的耗时只有 290ms比匿名管道快了将近 90 倍比 POSIX 消息队列也快了将近 27 倍这就是为什么在高性能场景下大家都优先选择共享内存的原因。详细的性能数据如下表所示IPC 机制传输 1GB 耗时 (ms)吞吐量 (GB/s)CPU 占用率匿名管道254300.03985%命名管道 (FIFO)261200.03882%System V 消息队列87600.11445%POSIX 消息队列79800.12542%System V 共享内存3203.12515%mmap 共享内存2903.44812%可以看到mmap 不仅是速度最快的CPU 占用率也是最低的因为它省去了大量的数据拷贝和系统调用的开销把资源真正用在了业务逻辑上。三、完整代码实现接下来我们来看完整的代码实现整个方案分为三个部分共享内存的封装头文件、服务端、客户端。3.1 SharedMem.hpp这个头文件是整个方案的核心我们在这里封装了两个核心类SafeObj封装了进程间共享的锁、条件变量和缓冲区保证共享内存的线程安全MmapMemory封装了 mmap 相关的系统调用比如shm_open、mmap、munmap等然后派生了MmapMemoryServer和MmapMemoryClient类分别对应服务端和客户端的逻辑#pragma once #include iostream #include sys/mman.h #include semaphore.h #include sys/types.h #include sys/stat.h #include fcntl.h #include unistd.h #include string.h #include pthread.h #include string #define SIZE 4096 class SafeObj { public: SafeObj() default; void InitObj() { // 初始化进程间共享的互斥锁 pthread_mutexattr_t mattr; pthread_mutexattr_init(mattr); pthread_mutexattr_setpshared(mattr, PTHREAD_PROCESS_SHARED); pthread_mutex_init(lock, mattr); // 初始化进程间共享的条件变量 pthread_condattr_t cattr; pthread_condattr_init(cattr); pthread_condattr_setpshared(cattr, PTHREAD_PROCESS_SHARED); pthread_cond_init(cond, cattr); // 清空共享缓冲区 memset(buffer, 0, sizeof(buffer)); } void CleanupObj() { // 销毁锁和条件变量 pthread_mutex_destroy(lock); pthread_cond_destroy(cond); } void LockObj() { int n ::pthread_mutex_lock(lock); (void)n; } void UnlockObj() { int n ::pthread_mutex_unlock(lock); (void)n; } void Wait() { int n ::pthread_cond_wait(cond, lock); (void)n; } void Signal() { int n ::pthread_cond_signal(cond); (void)n; } void BroadCast() { int n ::pthread_cond_broadcast(cond); if (n ! 0) std::cerr broadcast error std::endl; else std::cerr broadcast succcess std::endl; } void GetContent(std::string *out) { *out buffer; } void SetContent(const std::string in) { memset(buffer, 0, sizeof(buffer)); strncpy(buffer, in.c_str(), in.size()); } ~SafeObj() default; private: pthread_mutex_t lock; pthread_cond_t cond; char buffer[SIZE]; // 进程间共享的数据缓冲区 }; // 共享内存对象的名称必须以/开头这是POSIX共享内存的约定 #define SHARED_MEMORY_FILE /shm #define SHARED_MEMORY_SIZE sizeof(SafeObj) class MmapMemory { public: MmapMemory(const std::string file, int size) : _file(file), _size(size), _fd(-1), _mmap_addr(nullptr) {} void OpenFile() { // 创建/打开共享内存对象 _fd ::shm_open(_file.c_str(), O_CREAT | O_RDWR, 0666); if (_fd 0) { perror(shm_open); exit(1); } } void TruncSharedMemory() { // 设置共享内存的大小 int n ftruncate(_fd, _size); if (n 0) { perror(ftruncate); exit(2); } } void *Mmap() { // 映射共享内存到当前进程的虚拟地址空间 _mmap_addr ::mmap(nullptr, _size, PROT_READ | PROT_WRITE, MAP_SHARED, _fd, 0); if (_mmap_addr MAP_FAILED) { perror(mmap); exit(3); } return _mmap_addr; } void RemoveFile() { // 删除共享内存对象 int n ::shm_unlink(_file.c_str()); if (n 0) { perror(shm_unlink); exit(4); } } void *MmapAddr() { return _mmap_addr; } ~MmapMemory() { if (_fd 0) { close(_fd); std::cout 关闭mmap文件 std::endl; } // 解除内存映射 int n ::munmap(_mmap_addr, _size); if (n 0) { std::cout munmap 完成 std::endl; } } private: int _fd; int _size; std::string _file; void *_mmap_addr; }; class MmapMemoryServer : public MmapMemory { public: MmapMemoryServer() : MmapMemory(SHARED_MEMORY_FILE, SHARED_MEMORY_SIZE) { // 服务端初始化共享内存 MmapMemory::OpenFile(); MmapMemory::TruncSharedMemory(); MmapMemory::Mmap(); obj static_castSafeObj *(MmapMemory::MmapAddr()); obj-InitObj(); } // 接收客户端的控制指令 void RecvMessage(std::string *out) { obj-LockObj(); obj-Wait(); obj-GetContent(out); obj-UnlockObj(); } ~MmapMemoryServer() { obj-CleanupObj(); MmapMemory::RemoveFile(); } private: SafeObj *obj; }; class MmapMemoryClient : public MmapMemory { public: MmapMemoryClient() : MmapMemory(SHARED_MEMORY_FILE, SHARED_MEMORY_SIZE) { // 客户端打开已有的共享内存 MmapMemory::OpenFile(); MmapMemory::Mmap(); obj static_castSafeObj *(MmapMemory::MmapAddr()); } // 发送控制指令给服务端 void SendMessage(const std::string in) { obj-LockObj(); obj-SetContent(in); obj-BroadCast(); // 唤醒所有等待的工作进程 obj-UnlockObj(); } ~MmapMemoryClient() default; private: SafeObj *obj; };3.2 服务端[Server.cc]服务端的逻辑很简单首先创建共享内存然后 fork 出 10 个子进程加上主进程一共 11 个工作进程。每个工作进程都会循环等待客户端的指令收到指令之后判断是不是要唤醒自己如果是的话就打印激活信息如果是 end 指令就退出。#include SharedMem.hpp #include sys/wait.h #include sys/types.h #include string #include iostream void Active(MmapMemoryServer svr, std::string processname) { std::cout process is running: processname std::endl; std::string who; while (true) { // 阻塞等待客户端的指令 svr.RecvMessage(who); // 判断是不是要唤醒当前进程 if(who processname || who all) { std::cout processname is active! std::endl; } // 如果是结束指令退出进程 if(who end) { std::cout processname is quit! std::endl; break; } } } int main() { MmapMemoryServer svr; // 创建10个子工作进程 for (int i 0; i 10; i) { pid_t id fork(); if (id 0) { std::string name process- std::to_string(i); Active(svr, name); exit(0); } } // 主进程也作为一个工作进程 Active(svr, process-main); // 等待所有子进程退出 for(int i 0; i 10; i) { wait(nullptr); } return 0; }3.4 Makefile编译的时候我们需要链接 pthread 和 rt 库因为shm_open函数在 rt 库中同时我们使用了 C11 的标准。.PHONY:all all:server client server:Server.cc g -o $ $^ -lpthread -lrt -stdc11 -g client:Client.cc g -o $ $^ -lpthread -lrt -stdc11 -g .PHONY:clean clean: rm -f server client四、编译运行与效果展示接下来我们来实际运行一下这个程序看看效果首先编译代码make编译完成之后我们先启动服务端./server这时候服务端会启动 11 个工作进程并且打印出每个进程的启动信息process is running: process-0 process is running: process-1 process is running: process-2 process is running: process-3 process is running: process-main process is running: process-5 process is running: process-4 process is running: process-7 process is running: process-6 process is running: process-9 process is running: process-8然后我们打开另一个终端启动客户端./client客户端启动之后会等待我们输入控制指令Please Enter#我们先尝试唤醒单个进程输入process-1Please Enter# process-1 broadcast succcess Please Enter#这时候我们回到服务端的终端就可以看到 process-1 进程被唤醒了打印出了激活信息process-1 is active!接下来我们尝试唤醒所有进程输入allPlease Enter# all broadcast succcess Please Enter#这时候服务端的所有进程都会被唤醒打印出各自的激活信息process-2 is active! process-9 is active! process-5 is active! process-main is active! process-7 is active! process-3 is active! process-6 is active! process-4 is active! process-8 is active! process-0 is active!最后我们输入end让所有进程退出Please Enter# end broadcast succcess 关闭mmap文件 munmap完成这时候服务端的所有进程都会打印退出信息然后整个程序就正常结束了process-4 is quit! process-2 is quit! process-7 is quit! process-0 is quit! process-main is quit! process-9 is quit! process-5 is quit! process-6 is quit! process-8 is quit! process-1 is quit! process-3 is quit! 关闭mmap文件 munmap完成五、关键 API 详解这里我们来解释一下代码里用到的几个核心系统调用帮助大家更好地理解整个流程5.1 shm_open这个函数是用来创建或者打开一个 POSIX 共享内存对象的它的作用类似于我们熟悉的open函数只不过它打开的不是普通的磁盘文件而是 tmpfs 文件系统中的共享内存对象这样就完全不需要磁盘 IO效率非常高。函数原型int shm_open(const char *name, int oflag, mode_t mode);name共享内存对象的名字必须以/开头比如我们代码里的/shm这个名字是全局唯一的不同的进程就是通过这个名字来找到同一个共享内存对象的。oflag打开标志比如O_CREAT表示如果对象不存在就创建O_RDWR表示以读写模式打开。mode权限位和文件的权限一样比如 0666 表示所有用户都可以读写。注意使用这个函数的时候编译的时候需要链接-lrt库不然会出现函数未定义的错误。5.2 mmap这是整个方案的核心函数它的作用是把共享内存对象映射到当前进程的虚拟地址空间中。函数原型void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);addr我们想要映射到的虚拟地址一般传nullptr就可以让内核自己选择合适的地址。length映射的长度也就是我们共享内存的大小。prot内存的保护模式比如PROT_READ | PROT_WRITE表示这块内存可读可写。flags映射的标志做进程间通信的话必须用MAP_SHARED这样我们对内存的修改才会共享到其他进程如果用了MAP_PRIVATE那么修改就是进程私有的其他进程看不到就没法实现通信了。fd共享内存对象的文件描述符就是shm_open返回的那个。offset文件的偏移一般传 0 就可以从文件的开头开始映射。5.3 munmap 和 shm_unlinkmunmap解除内存映射当进程不需要使用共享内存的时候调用这个函数把虚拟地址和物理内存的映射关系解除。shm_unlink删除共享内存对象这个和删除普通文件的unlink函数很像。删除之后已经打开了这个对象的进程还可以继续使用直到所有进程都解除映射之后这块内存才会被真正释放所以不用担心调用了 unlink 之后其他进程就用不了了。六、注意事项最后给大家分享几个使用 mmap 做进程间通信的时候容易踩的坑共享内存的大小必须是页大小的整数倍Linux 的内存管理是以页为单位的我们代码里的缓冲区大小用了 4096正好是一个页的大小这样不会浪费内存。如果你的大小不是页的整数倍内核会自动向上对齐到页的整数倍。锁和条件变量必须设置 PTHREAD_PROCESS_SHARED这是很多新手最容易踩的坑默认的锁是线程间的不设置这个属性的话不同进程用同一个锁是不会生效的会导致同步完全失效出现各种奇怪的并发问题。不要用 MAP_PRIVATE 做 IPC这个标志会让映射的内存变成写时复制的你修改了内存之后只会修改你自己进程的副本其他进程根本看不到所以做进程间通信必须用MAP_SHARED。共享内存对象不会自动清理如果你的程序异常退出没有调用shm_unlink那么共享内存对象会一直留在系统里占用内存所以最好在程序退出的时候记得清理掉共享内存对象。七、总结这篇文章我们从原理到代码完整实现了一个基于 mmap 的进程间通信方案它可以实现多进程的可控协作而且性能非常高比传统的 IPC 方式快了几十倍。mmap 共享内存非常适合那些需要高频交互、大数据量传输的场景比如视频处理、高频交易、多进程控制等等掌握这个技术对你的 Linux 开发会有很大的帮助。