最近在学习 Linux 系统编程进程间通信IPC是绕不开的核心重点。从匿名管道到命名管道我一步步走过来直到接触到共享内存 —— 这个所有 IPC 方式中速度最快的 王者。学习过程中我踩了不少坑也产生了很多疑问为什么 key 不能让内核自动生成shmget 报错 File exists 到底是哪里出了问题key 和 shmid 到底有什么区别今天我就把自己的完整学习笔记、代码实战和踩坑经验整理出来从底层原理到代码实现带你彻底搞懂共享内存。一、为什么需要共享内存—— 从管道的局限性说起在学习共享内存之前我已经掌握了管道通信匿名管道只能用于有亲缘关系的进程父子、兄弟之间命名管道可以用于无关进程但本质还是基于文件但管道有一个致命的性能问题数据需要两次拷贝写进程用户缓冲区 → 内核管道缓冲区读进程内核管道缓冲区 → 用户缓冲区这就像两个人传递东西必须经过一个中间人效率自然不高。我当时就在想有没有一种方式能让两个进程直接读写同一块内存不需要内核中转答案就是共享内存。二、共享内存的核心原理让不同进程看到同一份资源1. 本质到底是什么共享内存的核心思想其实非常朴素操作系统在物理内存中开辟一块独立的区域让多个进程通过页表映射将这块物理内存映射到自己的虚拟地址空间中。这样一来我往这块内存写一个字符另一个进程立刻就能看到不需要任何系统调用直接用指针读写零拷贝这就是它成为最快 IPC 方式的根本原因2. 内核如何管理共享内存Linux 内核有一个非常经典的设计原则先描述再组织。对于共享内存也不例外每一块共享内存都对应一个struct shmid_ds结构体共享内存描述符内核用一个数组管理所有的共享内存描述符这个结构体包含了共享内存的所有元数据struct shmid_ds { struct ipc_perm shm_perm; // 权限信息包含key、uid、gid、mode size_t shm_segsz; // 共享内存大小字节 time_t shm_atime; // 最后一次挂接时间 time_t shm_dtime; // 最后一次脱接时间 time_t shm_ctime; // 最后一次修改时间 pid_t shm_cpid; // 创建进程PID pid_t shm_lpid; // 最后操作进程PID shmatt_t shm_nattch;// 当前挂接的进程数量 };3. 我最困惑的问题key vs shmid这是我学习过程中最容易混淆的两个概念也是我第一个问老师的问题。标识所属层面作用谁生成key_t key内核层面在内核中唯一标识一块共享内存用户通过ftok生成int shmid用户层面用户进程操作共享内存的句柄内核通过shmget返回为什么 key 不能由内核自动生成这是我当时最想不通的问题。既然 key 是用来唯一标识的让内核随机生成一个不就行了吗后来我才明白内核可以生成唯一 ID但无法帮两个毫无关系的进程 提前约定 同一个 ID。如果 key 由内核随机生成进程 A 创建共享内存得到一个随机 key进程 B 根本不知道这个 key 是什么永远找不到这块共享内存而用户通过ftok(固定路径, 固定ID)生成的 key 是稳定、可复现的。两个进程只要提前约定好路径和 ID就能生成同一个 key找到同一块共享内存。三、5 个核心函数详解从入门到踩坑1. ftok生成约定好的 keykey_t ftok(const char *pathname, int proj_id);作用将一个存在的文件路径和一个 0~255 的数字转换成唯一的key_t值原理提取文件的inode号与proj_id组合生成 key我踩过的坑pathname必须真实存在否则返回 - 1文件不能被删除重建否则inode会变key 也会变proj_id只用低 8 位超过 255 的部分会被截断2. shmget创建 / 获取共享内存int shmget(key_t key, size_t size, int shmflg);作用根据 key 创建新的共享内存或获取已存在的共享内存参数keyftok生成的唯一标识size共享内存大小必须是 4096页大小的整数倍shmflg操作标志IPC_CREAT不存在则创建存在则获取IPC_EXCL与IPC_CREAT一起使用存在则报错0666设置读写权限我踩过的最大的坑shmget: File exists我第一次运行代码的时候就遇到了这个错误当时还以为是 shmid 冲突了后来才搞清楚这是因为我用了IPC_CREAT | IPC_EXCL而系统中已经存在相同 key 的共享内存了。这是IPC_EXCL的保护机制确保你拿到的是全新的共享内存。解决方法就是用ipcrm命令删除旧的共享内存。3. shmat挂接共享内存void *shmat(int shmid, const void *shmaddr, int shmflg);作用将共享内存映射到当前进程的虚拟地址空间最佳实践shmaddr填nullptr让内核自动选择地址shmflg填0表示可读写注意失败判断必须强转成long long因为 64 位地址直接和 - 1 比较会出错if ((long long int)_start_addr -1) exit(3);4. shmdt脱接共享内存int shmdt(const void *shmaddr);作用取消共享内存与当前进程的映射关系注意这只是断开连接并不会删除共享内存5. shmctl控制共享内存int shmctl(int shmid, int cmd, struct shmid_ds *buf);作用对共享内存进行各种管理操作我最常用的两个命令IPC_STAT获取共享内存属性方便调试IPC_RMID标记共享内存为删除状态等所有进程脱接后真正删除四、我的完整代码实现客户端 - 服务端通信我把共享内存的所有操作封装成了一个Shm类这样用起来更方便也更符合面向对象的设计思想。1. 文件结构Shm.hpp共享内存核心封装类Server.cc服务端创建共享内存 读数据Client.cc客户端获取共享内存 写数据Makefile一键编译脚本2. 核心封装Shm.hpp#pragma once #include iostream #include cstdio #include unistd.h #include cstdlib #include sys/shm.h const int gsize 128; // 4096的整数倍 #define PATHNAME /tmp #define PROJ_ID 0x66 class Shm { public: Shm() : _shmid(-1), _size(gsize), _start_addr(nullptr) { } void Delete() { int n shmctl(_shmid, IPC_RMID, nullptr); (void)n; } void Attach() { _start_addr shmat(_shmid, nullptr, 0); if ((long long int)_start_addr -1) exit(3); } void PrintAttr() { struct shmid_ds ds; int n shmctl(_shmid, IPC_STAT, ds); if(n 0) { perror(shmctl); exit(4); } printf(key: 0x%x\n, ds.shm_perm.__key); printf(shm_nattch: %ld\n, ds.shm_nattch); printf(shm_segsz: 0x%lx\n, ds.shm_segsz); } void Detach() { int n shmdt(_start_addr); (void)n; } void Get() { GetHelper(IPC_CREAT); } void Create() { GetHelper(IPC_CREAT | IPC_EXCL | 0666); } void *Addr() { return _start_addr; } int Size() { return _size; } ~Shm() {} private: key_t GetKey() { return ftok(PATHNAME, PROJ_ID); } void GetHelper(int shmflg) { key_t k GetKey(); if (k 0) { std::cerr GetKey error; exit(1); } _shmid shmget(k, _size, shmflg); if (_shmid 0) { perror(shmget); exit(2); } printf(key0x%x, _shmid %d\n, k, _shmid); } private: int _shmid; int _size; void *_start_addr; };我觉得设计得比较好的地方面向对象封装屏蔽了底层系统调用的复杂性单一职责Create()负责创建新共享内存Get()负责获取已存在的代码复用GetHelper()统一处理shmget的逻辑调试友好PrintAttr()可以打印共享内存的所有内核信息3. 服务端实现Server.cc服务端是共享内存的创建者和管理者#include Shm.hpp int main() { Shm sharedmem; // 创建新的共享内存确保唯一 sharedmem.Create(); // 挂接到进程地址空间 sharedmem.Attach(); sleep(2); // 打印共享内存属性 sharedmem.PrintAttr(); char *shm_start (char *)sharedmem.Addr(); int size sharedmem.Size(); // 循环读取共享内存内容 while (true) { for (int i 0; i size; i) { std::cout shm_start[i] ; } std::cout std::endl; sleep(1); } // 注意死循环导致这里永远执行不到 sharedmem.Detach(); sharedmem.Delete(); return 0; }4. 客户端实现Client.cc客户端是共享内存的使用者#include Shm.hpp int main() { Shm sharedmem; // 获取已存在的共享内存 sharedmem.Get(); // 挂接到进程地址空间 sharedmem.Attach(); sleep(2); // 打印共享内存属性 sharedmem.PrintAttr(); char *shm_start (char *)sharedmem.Addr(); int size sharedmem.Size(); int index 0; // 从键盘输入写入共享内存 while (true) { std::cout Please Enter ; std::cin *shm_start; shm_start; index % size; // 防止越界 } sharedmem.Detach(); return 0; }5. 运行效果编译make先运行服务端./Server再运行客户端./Client在客户端输入字符服务端会立刻打印出来那种 我写什么另一个进程立刻就能看到 的感觉真的很神奇五、共享内存的特点与常见问题1. 核心特点优点速度最快直接内存访问零拷贝无需系统调用映射后直接用指针读写支持大量数据传输缺点没有同步互斥机制多个进程同时读写会导致数据混乱生命周期随内核用户不主动删除会一直存在直到系统重启没有访问控制任何进程只要知道 key 就能访问2. 最常见的坑共享内存残留这是我踩过最多的坑。程序异常退出比如 CtrlC没有调用shmctl(IPC_RMID)导致共享内存一直残留在系统中。解决方法查看系统中的共享内存ipcs -m删除残留的共享内存ipcrm -m shmid或ipcrm -M key优化代码在析构函数中自动释放共享内存~Shm() { if (_shmid ! -1) { Detach(); Delete(); } }3. 下一个要解决的问题同步机制现在的代码中客户端写和服务端读是完全异步的会导致数据混乱。接下来我打算学习信号量给共享内存加上同步互斥机制实现更可靠的进程间通信。六、学习总结通过这次学习我不仅掌握了共享内存的使用方法更理解了 Linux 内核 先描述再组织 的设计哲学。回顾整个学习过程我觉得最重要的是搞清楚三个问题是什么共享内存是让多个进程映射同一块物理内存为什么为了实现最快的进程间通信怎么用ftok 生成 key → shmget 创建 / 获取 → shmat 挂接 → 读写 → shmdt 脱接 → shmctl 删除共享内存是 Linux 系统编程中非常重要的一部分它的高性能特点使其在高并发、大数据传输的场景中得到广泛应用。希望我的这篇学习笔记能帮到同样在学习的你。
从零吃透 Linux System V 共享内存:从原理到代码实战全记录
最近在学习 Linux 系统编程进程间通信IPC是绕不开的核心重点。从匿名管道到命名管道我一步步走过来直到接触到共享内存 —— 这个所有 IPC 方式中速度最快的 王者。学习过程中我踩了不少坑也产生了很多疑问为什么 key 不能让内核自动生成shmget 报错 File exists 到底是哪里出了问题key 和 shmid 到底有什么区别今天我就把自己的完整学习笔记、代码实战和踩坑经验整理出来从底层原理到代码实现带你彻底搞懂共享内存。一、为什么需要共享内存—— 从管道的局限性说起在学习共享内存之前我已经掌握了管道通信匿名管道只能用于有亲缘关系的进程父子、兄弟之间命名管道可以用于无关进程但本质还是基于文件但管道有一个致命的性能问题数据需要两次拷贝写进程用户缓冲区 → 内核管道缓冲区读进程内核管道缓冲区 → 用户缓冲区这就像两个人传递东西必须经过一个中间人效率自然不高。我当时就在想有没有一种方式能让两个进程直接读写同一块内存不需要内核中转答案就是共享内存。二、共享内存的核心原理让不同进程看到同一份资源1. 本质到底是什么共享内存的核心思想其实非常朴素操作系统在物理内存中开辟一块独立的区域让多个进程通过页表映射将这块物理内存映射到自己的虚拟地址空间中。这样一来我往这块内存写一个字符另一个进程立刻就能看到不需要任何系统调用直接用指针读写零拷贝这就是它成为最快 IPC 方式的根本原因2. 内核如何管理共享内存Linux 内核有一个非常经典的设计原则先描述再组织。对于共享内存也不例外每一块共享内存都对应一个struct shmid_ds结构体共享内存描述符内核用一个数组管理所有的共享内存描述符这个结构体包含了共享内存的所有元数据struct shmid_ds { struct ipc_perm shm_perm; // 权限信息包含key、uid、gid、mode size_t shm_segsz; // 共享内存大小字节 time_t shm_atime; // 最后一次挂接时间 time_t shm_dtime; // 最后一次脱接时间 time_t shm_ctime; // 最后一次修改时间 pid_t shm_cpid; // 创建进程PID pid_t shm_lpid; // 最后操作进程PID shmatt_t shm_nattch;// 当前挂接的进程数量 };3. 我最困惑的问题key vs shmid这是我学习过程中最容易混淆的两个概念也是我第一个问老师的问题。标识所属层面作用谁生成key_t key内核层面在内核中唯一标识一块共享内存用户通过ftok生成int shmid用户层面用户进程操作共享内存的句柄内核通过shmget返回为什么 key 不能由内核自动生成这是我当时最想不通的问题。既然 key 是用来唯一标识的让内核随机生成一个不就行了吗后来我才明白内核可以生成唯一 ID但无法帮两个毫无关系的进程 提前约定 同一个 ID。如果 key 由内核随机生成进程 A 创建共享内存得到一个随机 key进程 B 根本不知道这个 key 是什么永远找不到这块共享内存而用户通过ftok(固定路径, 固定ID)生成的 key 是稳定、可复现的。两个进程只要提前约定好路径和 ID就能生成同一个 key找到同一块共享内存。三、5 个核心函数详解从入门到踩坑1. ftok生成约定好的 keykey_t ftok(const char *pathname, int proj_id);作用将一个存在的文件路径和一个 0~255 的数字转换成唯一的key_t值原理提取文件的inode号与proj_id组合生成 key我踩过的坑pathname必须真实存在否则返回 - 1文件不能被删除重建否则inode会变key 也会变proj_id只用低 8 位超过 255 的部分会被截断2. shmget创建 / 获取共享内存int shmget(key_t key, size_t size, int shmflg);作用根据 key 创建新的共享内存或获取已存在的共享内存参数keyftok生成的唯一标识size共享内存大小必须是 4096页大小的整数倍shmflg操作标志IPC_CREAT不存在则创建存在则获取IPC_EXCL与IPC_CREAT一起使用存在则报错0666设置读写权限我踩过的最大的坑shmget: File exists我第一次运行代码的时候就遇到了这个错误当时还以为是 shmid 冲突了后来才搞清楚这是因为我用了IPC_CREAT | IPC_EXCL而系统中已经存在相同 key 的共享内存了。这是IPC_EXCL的保护机制确保你拿到的是全新的共享内存。解决方法就是用ipcrm命令删除旧的共享内存。3. shmat挂接共享内存void *shmat(int shmid, const void *shmaddr, int shmflg);作用将共享内存映射到当前进程的虚拟地址空间最佳实践shmaddr填nullptr让内核自动选择地址shmflg填0表示可读写注意失败判断必须强转成long long因为 64 位地址直接和 - 1 比较会出错if ((long long int)_start_addr -1) exit(3);4. shmdt脱接共享内存int shmdt(const void *shmaddr);作用取消共享内存与当前进程的映射关系注意这只是断开连接并不会删除共享内存5. shmctl控制共享内存int shmctl(int shmid, int cmd, struct shmid_ds *buf);作用对共享内存进行各种管理操作我最常用的两个命令IPC_STAT获取共享内存属性方便调试IPC_RMID标记共享内存为删除状态等所有进程脱接后真正删除四、我的完整代码实现客户端 - 服务端通信我把共享内存的所有操作封装成了一个Shm类这样用起来更方便也更符合面向对象的设计思想。1. 文件结构Shm.hpp共享内存核心封装类Server.cc服务端创建共享内存 读数据Client.cc客户端获取共享内存 写数据Makefile一键编译脚本2. 核心封装Shm.hpp#pragma once #include iostream #include cstdio #include unistd.h #include cstdlib #include sys/shm.h const int gsize 128; // 4096的整数倍 #define PATHNAME /tmp #define PROJ_ID 0x66 class Shm { public: Shm() : _shmid(-1), _size(gsize), _start_addr(nullptr) { } void Delete() { int n shmctl(_shmid, IPC_RMID, nullptr); (void)n; } void Attach() { _start_addr shmat(_shmid, nullptr, 0); if ((long long int)_start_addr -1) exit(3); } void PrintAttr() { struct shmid_ds ds; int n shmctl(_shmid, IPC_STAT, ds); if(n 0) { perror(shmctl); exit(4); } printf(key: 0x%x\n, ds.shm_perm.__key); printf(shm_nattch: %ld\n, ds.shm_nattch); printf(shm_segsz: 0x%lx\n, ds.shm_segsz); } void Detach() { int n shmdt(_start_addr); (void)n; } void Get() { GetHelper(IPC_CREAT); } void Create() { GetHelper(IPC_CREAT | IPC_EXCL | 0666); } void *Addr() { return _start_addr; } int Size() { return _size; } ~Shm() {} private: key_t GetKey() { return ftok(PATHNAME, PROJ_ID); } void GetHelper(int shmflg) { key_t k GetKey(); if (k 0) { std::cerr GetKey error; exit(1); } _shmid shmget(k, _size, shmflg); if (_shmid 0) { perror(shmget); exit(2); } printf(key0x%x, _shmid %d\n, k, _shmid); } private: int _shmid; int _size; void *_start_addr; };我觉得设计得比较好的地方面向对象封装屏蔽了底层系统调用的复杂性单一职责Create()负责创建新共享内存Get()负责获取已存在的代码复用GetHelper()统一处理shmget的逻辑调试友好PrintAttr()可以打印共享内存的所有内核信息3. 服务端实现Server.cc服务端是共享内存的创建者和管理者#include Shm.hpp int main() { Shm sharedmem; // 创建新的共享内存确保唯一 sharedmem.Create(); // 挂接到进程地址空间 sharedmem.Attach(); sleep(2); // 打印共享内存属性 sharedmem.PrintAttr(); char *shm_start (char *)sharedmem.Addr(); int size sharedmem.Size(); // 循环读取共享内存内容 while (true) { for (int i 0; i size; i) { std::cout shm_start[i] ; } std::cout std::endl; sleep(1); } // 注意死循环导致这里永远执行不到 sharedmem.Detach(); sharedmem.Delete(); return 0; }4. 客户端实现Client.cc客户端是共享内存的使用者#include Shm.hpp int main() { Shm sharedmem; // 获取已存在的共享内存 sharedmem.Get(); // 挂接到进程地址空间 sharedmem.Attach(); sleep(2); // 打印共享内存属性 sharedmem.PrintAttr(); char *shm_start (char *)sharedmem.Addr(); int size sharedmem.Size(); int index 0; // 从键盘输入写入共享内存 while (true) { std::cout Please Enter ; std::cin *shm_start; shm_start; index % size; // 防止越界 } sharedmem.Detach(); return 0; }5. 运行效果编译make先运行服务端./Server再运行客户端./Client在客户端输入字符服务端会立刻打印出来那种 我写什么另一个进程立刻就能看到 的感觉真的很神奇五、共享内存的特点与常见问题1. 核心特点优点速度最快直接内存访问零拷贝无需系统调用映射后直接用指针读写支持大量数据传输缺点没有同步互斥机制多个进程同时读写会导致数据混乱生命周期随内核用户不主动删除会一直存在直到系统重启没有访问控制任何进程只要知道 key 就能访问2. 最常见的坑共享内存残留这是我踩过最多的坑。程序异常退出比如 CtrlC没有调用shmctl(IPC_RMID)导致共享内存一直残留在系统中。解决方法查看系统中的共享内存ipcs -m删除残留的共享内存ipcrm -m shmid或ipcrm -M key优化代码在析构函数中自动释放共享内存~Shm() { if (_shmid ! -1) { Detach(); Delete(); } }3. 下一个要解决的问题同步机制现在的代码中客户端写和服务端读是完全异步的会导致数据混乱。接下来我打算学习信号量给共享内存加上同步互斥机制实现更可靠的进程间通信。六、学习总结通过这次学习我不仅掌握了共享内存的使用方法更理解了 Linux 内核 先描述再组织 的设计哲学。回顾整个学习过程我觉得最重要的是搞清楚三个问题是什么共享内存是让多个进程映射同一块物理内存为什么为了实现最快的进程间通信怎么用ftok 生成 key → shmget 创建 / 获取 → shmat 挂接 → 读写 → shmdt 脱接 → shmctl 删除共享内存是 Linux 系统编程中非常重要的一部分它的高性能特点使其在高并发、大数据传输的场景中得到广泛应用。希望我的这篇学习笔记能帮到同样在学习的你。