Linux 进程间通信之 System V 共享内存:IPC 的原理与实战
🔥草莓熊Lotso:个人主页
❄️个人专栏: 《C++知识分享》《Linux 入门到实践:零基础也能懂》
✨生活是默默的坚持,毅力是永久的享受!
🎬 博主简介:
文章目录
- 前言:
- 一. 共享内存核心原理:为什么它最快?
- 二. System V 共享内存核心 API 与内核数据结构
- 三. 实战案例:基于封装类的共享内存通信
- 四. 内核如何管理 System V 共享内存
- 五. 关键问题与避坑指南
- 结尾:
前言:
在 Linux IPC 体系中,System V 共享内存是速度最快的进程间通信方式。与管道、命名管道需要通过内核缓冲区中转数据不同,共享内存直接将一块物理内存映射到多个进程的虚拟地址空间,进程间数据传递无需内核参与,仅需用户态的内存拷贝,效率远超其他 IPC 方式。本文将从原理、API、实战、内核管理四个维度,全面解析 System V 共享内存的底层逻辑与使用技巧。
一. 共享内存核心原理:为什么它最快?
1.1 核心设计思想
共享内存的本质是 内核维护的一块连续物理内存 ,内核通过特殊的内存管理机制(页表映射),将这块物理内存同时映射到多个进程的虚拟地址空间的 “共享区”(虚拟地址通常在 0xC0000000 附近)。此时,多个进程访问自己虚拟地址空间中的这块区域,本质上是访问同一份物理内存 —— 数据传递无需经过内核转发,仅需一次用户态内存拷贝,这是其速度最快的核心原因。

1.2 通信流程与地址空间示意图

进程A虚拟地址空间 物理内存 进程B虚拟地址空间 +------------------------+ +----------------+ +------------------------+ | 0xC0000000 argv/environ |||| 0xC0000000 argv/environ || 栈 |||| 栈 || 堆 || 共享内存块 || 堆 || 未初始化数据(bss) || (内核维护) || 未初始化数据(bss) || 初始化数据 |<----->|4096字节(页) |<----->| 初始化数据 || 0x08048000 文本段(代码)|||| 0x08048000 文本段(代码)| +------------------------+ +----------------+ +------------------------+ 1.3 核心特性
- 无内核中转:进程间数据直接通过物理内存交互,无系统调用开销(管道需
read/write系统调用); - 生命周期随内核:共享内存创建后,即使创建进程退出,内存块仍存在于内核中,需手动调用
shmctl(IPC_RMID)删除; - 无同步与互斥:内核不提供数据访问的同步机制,多个进程同时写会导致数据混乱(“临界区问题”),需配合信号量等工具实现同步;
- 跨进程通信:支持任意进程间通信(无需亲缘关系),只要进程持有相同的
key或shmid;
大小建议:共享内存大小最好是内存页(PAGE_SIZE,默认 4096 字节)的整数倍,避免内存碎片。

二. System V 共享内存核心 API 与内核数据结构
2.1 内核管理数据结构
内核通过struct shmid_ds管理共享内存的属性,是共享内存描述结构体的子集,结合 Linux 2.6.18 内核源码,核心字段如下:
structshmid_ds{structipc_perm shm_perm;// 权限控制结构体(包含key、uid、gid、mode等) size_t shm_segsz;// 共享内存大小(字节) pid_t shm_cpid;// 创建进程PID pid_t shm_lpid;// 最后一次操作该内存的进程PIDunsignedshort shm_nattch;// 当前挂载到该内存的进程数 time_t shm_atime;// 最后一次挂载时间(shmat调用时间) time_t shm_dtime;// 最后一次脱离时间(shmdt调用时间) time_t shm_ctime;// 最后一次属性修改时间void*shm_unused2;// 预留字段(内核内部使用)};
struct ipc_perm是 System V IPC(共享内存、消息队列、信号量)的通用权限结构体,内核通过该结构体的key字段唯一标识一个 IPC 资源。
2.2 核心 API 详解
System V 共享内存的使用流程遵循 “生成 Key→创建 / 获取共享内存→挂载→读写→脱离→删除”,核心 API 包括ftok、shmget、shmat、shmdt、shmctl,逐一解析如下:
2.2.1 ftok:生成唯一 Key(共享内存的 “身份证”)
用于将 “文件路径 + 项目 ID” 转换为唯一的key_t类型值,作为共享内存的全局标识 —— 多个进程通过相同的key可获取同一块共享内存。
#include<sys/ipc.h> key_t ftok(constchar*pathname,int proj_id);- 参数细节:
pathname:必须是系统中已存在的文件路径(如"/home"),且调用进程对该文件有访问权限proj_id:非 0 的 8 位整数(如0x6666),不同的proj_id会生成不同的key(即使路径相同);
返回值:成功返回唯一key,失败返回 - 1(errno会标识错误原因,如文件不存在、权限不足)。

2.2.2 shmget:创建 / 获取共享内存
用于创建新的共享内存或获取已存在的共享内存,返回共享内存标识符(shmid),后续操作均通过shmid关联共享内存。
#include<sys/shm.h>intshmget(key_t key, size_t size,int shmflg);参数深度解析:
key:ftok生成的唯一 Key;size:共享内存大小(建议为 4096 的整数倍),创建时需指定,获取时可设为 0;shmflg:权限标志组合,核心组合:IPC_CREAT:若共享内存不存在则创建,存在则直接获取(常用)IPC_CREAT | IPC_EXCL:若共享内存已存在则报错(确保创建全新内存,避免覆盖);- 权限位(如
0666):控制进程对共享内存的访问权限(与文件权限规则一致);
返回值:成功返回shmid(非负整数),失败返回 - 1。

关于key值是什么的补充

2.2.3 shmat:挂载共享内存
将共享内存映射到当前进程的虚拟地址空间,返回映射后的虚拟地址指针 —— 进程通过该指针读写共享内存。
#include<sys/shm.h>void*shmat(int shmid,constvoid*shmaddr,int shmflg);参数细节:
shmid:shmget返回的共享内存标识符;shmaddr:指定挂载的虚拟地址(NULL 表示由内核自动分配,推荐使用);shmflg:挂载标志:0:可读可写挂载;SHM_RDONLY:只读挂载(进程无写权限);SHM_RND:若shmaddr非 NULL,将挂载地址向下调整为SHMLBA(内存页边界)的整数倍;
返回值:成功返回虚拟地址指针,失败返回(void*)-1。

2.2.4 shmdt:脱离共享内存
将共享内存从当前进程的虚拟地址空间中脱离(解除映射关系),并非删除共享内存。
#include<sys/shm.h>intshmdt(constvoid*shmaddr);- 参数:
shmaddr:shmat返回的虚拟地址指针; - 关键注意:
- 脱离后,进程无法再访问该共享内存,但共享内存本身仍存在于内核中;
- 若进程未调用
shmdt就退出,内核会自动解除映射(避免内存泄漏);
- 返回值:成功返回 0,失败返回 - 1。
2.2.5 shmctl:控制共享内存(核心功能:删除)
用于获取共享内存属性、修改属性或删除共享内存,是共享内存生命周期管理的核心 API。
#include<sys/shm.h>intshmctl(int shmid,int cmd,structshmid_ds*buf);参数深度解析:
shmid:共享内存标识符;cmd:控制命令(核心 3 种):
| 命令 | 功能描述 |
|---|---|
IPC_STAT | 获取共享内存属性,存入buf指向的shmid_ds结构体(如查询挂载进程数、大小) |
IPC_SET | 修改共享内存属性(需进程有CAP_SYS_ADMIN权限),属性值从buf读取 |
IPC_RMID | 标记共享内存为 “待删除”,后续新进程无法挂载,所有进程脱离后内核释放内存 |

buf:存储属性的结构体指针(IPC_RMID时可设为 NULL);
返回值:成功返回 0,失败返回 - 1。

三. 实战案例:基于封装类的共享内存通信
提供Shm.hpp封装类对上述核心 API进行完整封装,无需修改即可使用。结合Writer.cc(写进程)和Reader.cc(读进程),实现跨进程数据读写。
3.1 封装类核心逻辑解析(Shm.hpp)
Shm.hpp封装了 “生成 Key→创建 / 获取→挂载→删除→属性查询” 的全流程,核心接口与 API 映射关系如下:
| 函数名 | 调用示例 | 功能描述 |
|---|---|---|
Create() | shmget(key, size, IPC_CREAT|IPC_EXCL|0666) | 创建全新共享内存 |
Get() | shmget(key, size, IPC_CREAT) | 获取已存在的共享内存 |
Attch() | shmat(shmid, NULL, 0) | 挂载共享内存,返回虚拟地址指针 |
Delete() | shmctl(shmid, IPC_RMID, NULL) | 删除共享内存 |
GetShmAttr() | shmctl(shmid, IPC_STAT, &ds) | 获取共享内存属性(PID、大小、Key) |
Debug() | - | 打印shmid、size、key(调试用) |
#ifndef__SHM_HPP__#define__SHM_HPP__#include<iostream>#include<cstdio>#include<sys/shm.h>#include<string.h>#include<unistd.h>const std::string proj_name ="/home";constint proj_id =0x6666;constint g_size =4096;static std::string ToHex(longlong data){char buf[16];snprintf(buf,sizeof(buf),"0x%llx", data);return buf;}classShm{public:Shm(int size = g_size):_shmid(-1),_size(size),_key(0){}~Shm(){}private: key_t GetKey(){ _key =ftok(proj_name.c_str(), proj_id);if(_key <0){perror("ftok");}return _key;}boolCreateCoreHelper(int flags){// 1. 获取key值 key_t key =GetKey();// 2. 创建共享内存 _shmid =shmget(key, _size, flags);if(_shmid <0){perror("shmget");returnfalse;}returntrue;}public:// 1.创建共享内存boolCreate(){returnCreateCoreHelper(IPC_CREAT | IPC_EXCL |0666);}// 2.获取共享内存boolGet(){returnCreateCoreHelper(IPC_CREAT);}// 3. 删除共享内存boolDelete(){int n =shmctl(_shmid, IPC_RMID,nullptr);return n <0?false:true;}// 4. 获取共享内存属性voidGetShmAttr(){structshmid_ds ds;int n =shmctl(_shmid, IPC_STAT,&ds);if(n <0){perror("shmctl");return;} std::cout << ds.shm_cpid << std::endl; std::cout << ds.shm_segsz << std::endl; std::cout <<ToHex(_key)<< std::endl;}// 5. 共享内存映射挂载void*Attch(){ _start =(char*)shmat(_shmid,nullptr,0);return _start;}// 6. 共享内存去关联voidDetach(){int n =shmdt(_start);(void)n;}voidDebug(){ std::cout <<"shmid: "<< _shmid << std::endl; std::cout <<"size: "<< _size << std::endl; std::cout <<"key: "<<ToHex(_key)<< std::endl;}private:int _shmid;int _size; key_t _key;char*_start;};typedefstructdata{int count;char buf[26*2];}buffer_t;#endif3.2 Writer 进程:写入数据到共享内存(Writer.cc)
// header only#include"Shm.hpp"#include<iostream>#include<string>#include<unistd.h> Shm shm;classInit{public:Init(){ shm.Get(); addr =(char*)shm.Attch(); std::cout <<"addr: "<<ToHex((longlong)addr)<< std::endl;}~Init(){ shm.Detach();}char*Addr(){return addr;}public:char* addr;}; Init init;// Write.intmain(){ std::cout <<"test begin..."<< std::endl; buffer_t *shmbuf =(buffer_t*)init.Addr(); shmbuf->count =0;memset(shmbuf->buf,0,4096);char ch ='A';for(int i =0; i <26*2; i +=2, ch++){ shmbuf->buf[i]= ch;usleep(234219); shmbuf->buf[i +1]= ch;usleep(734217); shmbuf->count++;usleep(734217);sleep(1);}return0;}3.3 Reader 进程:从共享内存读取数据(Reader.cc)
#include"Shm.hpp"#include<iostream>#include<string>#include<unistd.h>// // RAII// Shm shm;// class Init// {// public:// Init()// {// }// ~Init()// {// std::cout << "~Init()" << std::endl;// }// };// Writer -> shm -> Readerintmain(){// 在内核中创建共享内存 Shm shm; shm.Create();char*addr =(char*)shm.Attch(); buffer_t *shmbuf =(buffer_t*)addr;int old = shmbuf->count;// 实现一个简单的保护和同步机制while(true){if(old != shmbuf->count){ std::cout <<"count: "<< shmbuf->count << std::endl; std::cout <<"data: "<< shmbuf->buf << std::endl; old = shmbuf->count;}usleep(74329);if(shmbuf->count >=26){break;}}// shm.Debug();// shm.GetShmAttr();// 删除共享内存 shm.Detach(); shm.Delete();return0;}3.4 编译与运行
3.4.1 Makefile
all:Reader Writer Reader:Reader.cc g++ -o$@ $^ -std=c++11 Writer:Writer.cc g++ -o$@ $^ -std=c++11 .Phony:clean clean: rm-f Reader Writer 3.4.2 运行步骤与输出结果展示
- 步骤一:先运行./Reader
- 步骤二:再运行./Writter

重要知识点图示理解:

3.5 残留共享内存清理(上面图中其实也有写)
若进程异常退出导致共享内存未删除,可通过以下命令手动清理:
# 查看所有System V共享内存 ipcs -m# 删除指定shmid的共享内存(如shmid=688145) ipcrm -m688145
四. 内核如何管理 System V 共享内存
根据附录的内核源码解析,内核通过struct ipc_ids和struct shmid_kernel管理所有共享内存资源,核心逻辑如下:
- 全局管理结构:内核维护
shm_ids全局变量(struct ipc_ids类型),记录系统中所有共享内存的元数据(如max_id、in_use、entries数组); - 索引机制:
struct ipc_id_ary的entries数组存储struct kern_ipc_perm指针,内核通过shmid索引到对应的共享内存权限结构体; - 物理内存关联:
struct shmid_kernel包含struct file *shm_file字段,通过文件系统的inode和vm_area_struct实现物理内存与进程虚拟地址的映射。
简单来说:内核将共享内存抽象为一种特殊的 IPC 资源,通过 “Key→shmid→内核数据结构→物理内存” 的链路,实现对共享内存的创建、挂载、脱离、删除等操作的统一管理。
五. 关键问题与避坑指南
5.1 共享内存的同步问题(核心坑!)
共享内存本身无同步与互斥机制,若多个进程同时写入,会导致数据覆盖(如进程 A 写 “hello”,进程 B 同时写 “world”,最终可能得到 “hwllo” 等混乱数据)—— 这是 PPT 反复强调的重点问题。
解决方案:
- 配合 System V 信号量:用信号量的 P/V 操作(申请 / 释放资源)保护临界区,确保同一时间仅一个进程访问共享内存;
- 管道通知机制:如 PPT 实例 2 所示,用命名管道实现 “信号唤醒”(Writer 写完成后向管道发信号,Reader 收到信号后再读);
- 文件锁:通过fcntl函数给共享内存关联的文件加锁,实现简单的互斥访问。
5.2 共享内存的删除机制
shmctl(shmid, IPC_RMID, NULL)的作用是 “标记删除”,而非 “立即删除”:- 标记后,新进程调用
shmget无法获取该共享内存; - 已挂载的进程仍可正常读写,直到所有进程调用
shmdt脱离; - 最后一个进程脱离后,内核才会真正释放物理内存。
- 标记后,新进程调用
- 若未调用
IPC_RMID,共享内存会一直残留于内核中,直到系统重启(需手动清理)。
5.3 常见错误与排查
| 错误现象 | 原因分析 | 解决方案 |
|---|---|---|
shmget报错 “File exists” | 使用 IPC_CREAT|IPC_EXCL 创建已存在的内存 | 改用IPC_CREAT或ipcrm -m shmid删除旧内存 |
shmat返回(void*)-1 | 权限不足(如创建时权限为 0600) | 创建时指定0666权限 |
| 读取数据为空或乱码 | 1. Writer 未写入就读取;2. 无同步机制 | 增加sleep延迟或实现同步机制 |
| 进程退出后内存未释放 | 未调用shmctl(IPC_RMID) | ipcs -m查询 +ipcrm -m shmid手动删除 |
5.4 共享内存与其他 IPC 的性能对比与总结
| IPC 方式 | 数据传递路径 | 核心开销 | 适用场景 | 速度排名 |
|---|---|---|---|---|
| 匿名管道 | 进程 A→内核缓冲区→进程 B | 2 次系统调用 + 2 次内存拷贝 | 亲缘进程、简单数据流 | 3 |
| 命名管道 | 进程 A→内核缓冲区→进程 B | 2 次系统调用 + 2 次内存拷贝 | 任意进程、简单数据流 | 2 |
| System V 共享内存 | 进程 A→共享内存→进程 B | 0 次系统调用 + 1 次内存拷贝 | 高频 / 大数据量跨进程通信 | 1 |
结尾:
🍓 我是草莓熊 Lotso!若这篇技术干货帮你打通了学习中的卡点: 👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长 ❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量 ⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用 💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑 🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解 技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标! 结语:System V 共享内存是 Linux 中效率最高的 IPC 方式,核心优势在于 “无内核中转、用户态直接通信”。共享内存适合高频、大数据量的跨进程通信场景(如服务器集群数据共享、高频交易系统、视频流传输)。若需实现安全的同步通信,可后续学习 System V 信号量的使用,将二者结合实现 “高效 + 安全” 的跨进程通信。创作不易,觉得有帮助的话,欢迎点赞、收藏、关注三连~ 后续会持续更新 Linux IPC 系列(信号量、消息队列),带你从底层吃透进程间通信技术。
✨把这些内容吃透超牛的!放松下吧✨ʕ˘ᴥ˘ʔづきらど