
🔥草莓熊Lotso: 个人主页
❄️个人专栏: 《C++知识分享》 《Linux 入门到实践:零基础也能懂》
✨生活是默默的坚持,毅力是永久的享受!
🎬 博主简介:

文章目录
- 前言:
- [一. 共享内存核心原理:为什么它最快?](#一. 共享内存核心原理:为什么它最快?)
-
- [1.1 核心设计思想](#1.1 核心设计思想)
- [1.2 通信流程与地址空间示意图](#1.2 通信流程与地址空间示意图)
- [1.3 核心特性](#1.3 核心特性)
- [二. System V 共享内存核心 API 与内核数据结构](#二. System V 共享内存核心 API 与内核数据结构)
-
- [2.1 内核管理数据结构](#2.1 内核管理数据结构)
- [2.2 核心 API 详解](#2.2 核心 API 详解)
-
- [2.2.1 ftok:生成唯一 Key(共享内存的 "身份证")](#2.2.1 ftok:生成唯一 Key(共享内存的 “身份证”))
- [2.2.2 shmget:创建 / 获取共享内存](#2.2.2 shmget:创建 / 获取共享内存)
- [2.2.3 shmat:挂载共享内存](#2.2.3 shmat:挂载共享内存)
- [2.2.4 shmdt:脱离共享内存](#2.2.4 shmdt:脱离共享内存)
- [2.2.5 shmctl:控制共享内存(核心功能:删除)](#2.2.5 shmctl:控制共享内存(核心功能:删除))
- [三. 实战案例:基于封装类的共享内存通信](#三. 实战案例:基于封装类的共享内存通信)
-
- [3.1 封装类核心逻辑解析(Shm.hpp)](#3.1 封装类核心逻辑解析(Shm.hpp))
- [3.2 Writer 进程:写入数据到共享内存(Writer.cc)](#3.2 Writer 进程:写入数据到共享内存(Writer.cc))
- [3.3 Reader 进程:从共享内存读取数据(Reader.cc)](#3.3 Reader 进程:从共享内存读取数据(Reader.cc))
- [3.4 编译与运行](#3.4 编译与运行)
-
- [3.4.1 Makefile](#3.4.1 Makefile)
- [3.4.2 运行步骤与输出结果展示](#3.4.2 运行步骤与输出结果展示)
- [3.5 残留共享内存清理(上面图中其实也有写)](#3.5 残留共享内存清理(上面图中其实也有写))
- [四. 内核如何管理 System V 共享内存](#四. 内核如何管理 System V 共享内存)
- [五. 关键问题与避坑指南](#五. 关键问题与避坑指南)
-
- [5.1 共享内存的同步问题(核心坑!)](#5.1 共享内存的同步问题(核心坑!))
- [5.2 共享内存的删除机制](#5.2 共享内存的删除机制)
- [5.3 常见错误与排查](#5.3 常见错误与排查)
- [5.4 共享内存与其他 IPC 的性能对比与总结](#5.4 共享内存与其他 IPC 的性能对比与总结)
- 结尾:
前言:
在 Linux IPC 体系中,System V 共享内存是速度最快的进程间通信方式。与管道、命名管道需要通过内核缓冲区中转数据不同,共享内存直接将一块物理内存映射到多个进程的虚拟地址空间,进程间数据传递无需内核参与,仅需用户态的内存拷贝,效率远超其他 IPC 方式。本文将从原理、API、实战、内核管理四个维度,全面解析 System V 共享内存的底层逻辑与使用技巧。
一. 共享内存核心原理:为什么它最快?
1.1 核心设计思想
共享内存的本质是 内核维护的一块连续物理内存 ,内核通过特殊的内存管理机制(页表映射),将这块物理内存同时映射到多个进程的虚拟地址空间的 "共享区"(虚拟地址通常在 0xC0000000 附近)。此时,多个进程访问自己虚拟地址空间中的这块区域,本质上是访问同一份物理内存 ------ 数据传递无需经过内核转发,仅需一次用户态内存拷贝,这是其速度最快的核心原因。

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

bash
进程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 内核源码,核心字段如下:
cpp
struct shmid_ds {
struct ipc_perm shm_perm; // 权限控制结构体(包含key、uid、gid、mode等)
size_t shm_segsz; // 共享内存大小(字节)
pid_t shm_cpid; // 创建进程PID
pid_t shm_lpid; // 最后一次操作该内存的进程PID
unsigned short 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可获取同一块共享内存。
cpp
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
- 参数细节 :
pathname:必须是系统中已存在的文件路径(如"/home"),且调用进程对该文件有访问权限proj_id:非 0 的 8 位整数(如0x6666),不同的proj_id会生成不同的key(即使路径相同);
- 返回值 :成功返回唯一
key,失败返回 - 1(errno会标识错误原因,如文件不存在、权限不足)。

2.2.2 shmget:创建 / 获取共享内存
用于创建新的共享内存或获取已存在的共享内存,返回共享内存标识符(shmid),后续操作均通过shmid关联共享内存。
cpp
#include <sys/shm.h>
int shmget(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:挂载共享内存
将共享内存映射到当前进程的虚拟地址空间,返回映射后的虚拟地址指针 ------ 进程通过该指针读写共享内存。
cpp
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数细节:
shmid:shmget返回的共享内存标识符;shmaddr:指定挂载的虚拟地址(NULL 表示由内核自动分配,推荐使用);shmflg:挂载标志:0:可读可写挂载;SHM_RDONLY:只读挂载(进程无写权限);SHM_RND:若shmaddr非 NULL,将挂载地址向下调整为SHMLBA(内存页边界)的整数倍;
返回值 :成功返回虚拟地址指针,失败返回(void*)-1。

2.2.4 shmdt:脱离共享内存
将共享内存从当前进程的虚拟地址空间中脱离(解除映射关系),并非删除共享内存。
cpp
#include <sys/shm.h>
int shmdt(const void *shmaddr);
- 参数 :
shmaddr:shmat返回的虚拟地址指针; - 关键注意 :
- 脱离后,进程无法再访问该共享内存,但共享内存本身仍存在于内核中;
- 若进程未调用
shmdt就退出,内核会自动解除映射(避免内存泄漏);
- 返回值:成功返回 0,失败返回 - 1。
2.2.5 shmctl:控制共享内存(核心功能:删除)
用于获取共享内存属性、修改属性或删除共享内存,是共享内存生命周期管理的核心 API。
cpp
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_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 |
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(调试用) |
cpp
#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";
const int proj_id = 0x6666;
const int g_size = 4096;
static std::string ToHex(long long data)
{
char buf[16];
snprintf(buf, sizeof(buf), "0x%llx", data);
return buf;
}
class Shm
{
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;
}
bool CreateCoreHelper(int flags)
{
// 1. 获取key值
key_t key = GetKey();
// 2. 创建共享内存
_shmid = shmget(key, _size, flags);
if(_shmid < 0)
{
perror("shmget");
return false;
}
return true;
}
public:
// 1.创建共享内存
bool Create()
{
return CreateCoreHelper(IPC_CREAT | IPC_EXCL | 0666);
}
// 2.获取共享内存
bool Get()
{
return CreateCoreHelper(IPC_CREAT);
}
// 3. 删除共享内存
bool Delete()
{
int n = shmctl(_shmid, IPC_RMID, nullptr);
return n < 0 ? false : true;
}
// 4. 获取共享内存属性
void GetShmAttr()
{
struct shmid_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. 共享内存去关联
void Detach()
{
int n = shmdt(_start);
(void)n;
}
void Debug()
{
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;
};
typedef struct data
{
int count;
char buf[26 * 2];
}buffer_t;
#endif
3.2 Writer 进程:写入数据到共享内存(Writer.cc)
cpp
// header only
#include "Shm.hpp"
#include <iostream>
#include <string>
#include <unistd.h>
Shm shm;
class Init
{
public:
Init()
{
shm.Get();
addr = (char*)shm.Attch();
std::cout << "addr: " << ToHex((long long)addr) << std::endl;
}
~Init()
{
shm.Detach();
}
char *Addr()
{
return addr;
}
public:
char* addr;
};
Init init;
// Write.
int main()
{
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);
}
return 0;
}
3.3 Reader 进程:从共享内存读取数据(Reader.cc)
cpp
#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 -> Reader
int main()
{
// 在内核中创建共享内存
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();
return 0;
}
3.4 编译与运行
3.4.1 Makefile
bash
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 残留共享内存清理(上面图中其实也有写)
若进程异常退出导致共享内存未删除,可通过以下命令手动清理:
bash
# 查看所有System V共享内存
ipcs -m
# 删除指定shmid的共享内存(如shmid=688145)
ipcrm -m 688145

四. 内核如何管理 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` 创建已存在的内存 |
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 |
结尾:
html
🍓 我是草莓熊 Lotso!若这篇技术干货帮你打通了学习中的卡点:
👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长
❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量
⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用
💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑
🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解
技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标!
结语:System V 共享内存是 Linux 中效率最高的 IPC 方式,核心优势在于 "无内核中转、用户态直接通信"。共享内存适合高频、大数据量的跨进程通信场景(如服务器集群数据共享、高频交易系统、视频流传输)。若需实现安全的同步通信,可后续学习 System V 信号量的使用,将二者结合实现 "高效 + 安全" 的跨进程通信。创作不易,觉得有帮助的话,欢迎点赞、收藏、关注三连~ 后续会持续更新 Linux IPC 系列(信号量、消息队列),带你从底层吃透进程间通信技术。
✨把这些内容吃透超牛的!放松下吧✨ ʕ˘ᴥ˘ʔ づきらど
