
🔥小叶-duck:个人主页
❄️个人专栏:《Data-Structure-Learning》《C++入门到进阶&自我学习过程记录》
✨未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游
目录
[一. 共享内存核心原理:为什么它最快?](#一. 共享内存核心原理:为什么它最快?)
[1.1 核心设计思想](#1.1 核心设计思想)
[1.2 通信流程与地址空间示意图](#1.2 通信流程与地址空间示意图)
[1.3 共享内存核心特性](#1.3 共享内存核心特性)
[2.1 内核管理共享内存的数据结构](#2.1 内核管理共享内存的数据结构)
[3.1 ftok:生成唯一 Key](#3.1 ftok:生成唯一 Key)
[3.2 shmget:创建 / 获取共享内存](#3.2 shmget:创建 / 获取共享内存)
[3.3 shmat:挂载共享内存](#3.3 shmat:挂载共享内存)
[3.4 shmdt:脱离共享内存](#3.4 shmdt:脱离共享内存)
[3.5 shmctl:控制共享内存(核心功能:删除)](#3.5 shmctl:控制共享内存(核心功能:删除))
[四. 实战案例:基于封装类的共享内存通信](#四. 实战案例:基于封装类的共享内存通信)
[4.1 封装类核心逻辑解析(Shm.hpp)](#4.1 封装类核心逻辑解析(Shm.hpp))
[4.2 客户端进程:写入数据到共享内存(client.cc)](#4.2 客户端进程:写入数据到共享内存(client.cc))
[4.3 服务端进程:创建共享内存并从共享内存读取数据(server.cc)](#4.3 服务端进程:创建共享内存并从共享内存读取数据(server.cc))
[4.4 编译与运行](#4.4 编译与运行)
[4.5 残留共享内存清理](#4.5 残留共享内存清理)
[五. 内核如何管理 System V 共享内存](#五. 内核如何管理 System V 共享内存)
[六. 关键问题分析](#六. 关键问题分析)
[6.1 共享内存的同步问题](#6.1 共享内存的同步问题)
[6.2 共享内存的删除机制](#6.2 共享内存的删除机制)
[6.3 常见错误与排查](#6.3 常见错误与排查)
一. 共享内存核心原理:为什么它最快?
1.1 核心设计思想
共享内存的本质是内核维护的一块连续物理内存 ,内核通过特殊的内存管理机制(页表映射 ),将这块物理内存同时映射到多个进程的虚拟地址空间的 "共享区" (虚拟地址通常在 0xC0000000 附近)。此时,多个进程访问自己虚拟地址空间中的这块区域,本质上就是让多个进程看到并且访问同一份物理内存 ------ 数据传递无需经过内核转发,仅需一次用户态内存拷贝,这是其速度最快的核心原因。
简单理解:
倘若在物理内存中申请一段连续空间,记录好该物理内存的起始地址 与空间大小 ,再将这块物理内存映射至进程 A 的用户共享区 ,进程 A 便会得到一段对应的虚拟起始地址,后续依托该虚拟地址即可直接完成内存读写。同理,进程 B 也能在自身地址空间内划分一块区域,建立同一片物理共享内存的映射关系,获取属于自己的虚拟地址;两个进程只需借助各自虚拟地址搭配偏移 ,就能访问同一块物理内存的数据。动态链接库的加载 本质就依托这类映射思想 ,依靠页表映射机制,让多个进程的虚拟地址指向同一片物理内存,该实现进程数据互通的技术,便是共享内存。
1.2 通信流程与地址空间示意图

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


二、共享内存数据结构
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 资源。
三、共享内存核心API详解
System V 共享内存的使用流程遵循 "生成 Key→创建 / 获取共享内存→挂载→读写→脱离→删除",核心 API 包括 ftok、shmget、shmat、shmdt、shmctl,逐一解析如下:
3.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 会标识错误原因,如文件不存在、权限不足)。

3.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。

3.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**。

3.4 shmdt:脱离共享内存
将共享内存从当前进程的虚拟地址空间中脱离(解除映射关系 ),并非删除共享内存。
cpp
#include <sys/shm.h>
int shmdt(const void *shmaddr);
参数:
- **shmaddr:**shmat返回的虚拟地址指针;
- 关键注意 :
- 脱离后,进程无法再访问该共享内存,但共享内存本身仍存在于内核中;
- 若进程未调用 shmdt 就退出,内核会自动解除映射(避免内存泄漏);
- 返回值:成功返回 0,失败返回 - 1。
3.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进行完整封装,无需修改即可使用。结合client.cc(写进程) 和server.cc(读进程),实现跨进程数据读写。
4.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) | 挂载共享内存,返回虚拟地址指针 |
| Destroy()**** | shmctl(shmid, IPC_RMID, NULL) | 删除共享内存 |
| GetShmAttr() | shmctl(shmid, IPC_STAT, &ds) | 获取共享内存属性(PID、大小、Key) |
cpp
#pragma once
#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/ipc.h>
#include <unistd.h>
#include <sys/shm.h>
// 错误处理宏
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while (0)
// key_t ftok(const char *pathname, int proj_id);
const int projid = 0x66; // int proj_id
const std::string pathname = "."; // const char *pathname
const int gmode = 0666; // 权限位
#define CREATER "creater"
#define USER "user"
class Shm
{
public:
Shm(const std::string &pathname, int projid, const std::string &usertype)
: _shmid(-1), _size(4096), _start_mem(nullptr), _usertype(usertype)
{
// 获取唯一 Key
_key = ftok(pathname.c_str(), projid);
if (_key == -1)
{
ERR_EXIT("ftok");
}
printf("key:0x%x\n", _key);
// 创建/获取共享内存
if (usertype == USER)
{
Get();
}
else if (usertype == CREATER)
{
Creat();
}
// 进程连接共享内存
Attach();
}
void *GetVirtualAddr()
{
printf("VirtualAddr:%p\n", _start_mem);
return _start_mem;
}
// 获取指定共享内存的相关属性
void Attr()
{
struct shmid_ds ds; // 描述共享内存的数据结构
int n = shmctl(_shmid, IPC_STAT, &ds); // ds:输出型参数
printf("shsm_segsz: %ld\n", ds.shm_segsz);
printf("key: 0x%x\n", ds.shm_perm.__key);
printf("uid: %d\n", ds.shm_perm.uid);
}
// 析构函数
~Shm()
{
// 不管是什么进程结束调用析构函数
// 都需要进行去关联,最后再判断是否需要释放共享内存
Detach();
// 调用Destroy释放共享内存(由creater释放)
if (_usertype == CREATER)
{
Destroy();
}
}
private:
// 接口私有化:将所有接口在构造函数中调用,避免接口面向用户
// 创建共享内存
void Creat()
{
// IPC_CREAT | IPC_EXCL:若共享内存已存在则报错(确保创建全新内存,避免覆盖)
// 一起使用一般为创建共享内存的进程
CreatHelper(IPC_CREAT | IPC_EXCL | gmode);
// 创建共享内存时如果不加权限位gmode则默认全0,则无法进行后续进程与共享内存的连接(Permission denied)
}
// 获取共享内存
void Get()
{
// IPC_CREAT:若共享内存不存在则创建,存在则直接获取
// 单独使用一般为直接获取shmid的进程
CreatHelper(IPC_CREAT);
}
void CreatHelper(int flg)
{
// 共享内存的生命周期->随内核,不随进程
// 创建/获取共享内存
_shmid = shmget(_key, _size, flg);
if (_shmid == -1)
{
ERR_EXIT("shmget");
}
printf("shmid:%d\n", _shmid);
}
// 共享内存映射挂载
void Attach()
{
_start_mem = shmat(_shmid, nullptr, 0);
if (_start_mem == (void *)(-1))
{
ERR_EXIT("shmat");
}
printf("attach success\n");
}
// 去关联
void Detach()
{
int n = shmdt(_start_mem);
if (n == 0)
{
printf("detach success\n");
}
else
{
ERR_EXIT("shmdt");
}
}
// 删除共享内存
void Destroy()
{
if (_shmid == -1)
return;
int n = shmctl(_shmid, IPC_RMID, nullptr);
if (n < 0)
{
ERR_EXIT("shmctl");
}
else
{
printf("shmctl delete shm:%d success\n", _shmid);
}
}
private:
int _shmid;
int _size;
void *_start_mem; // 共享内存在地址空间的起始地址
key_t _key;
std::string _usertype; // 判断是使用者还是创建者,用于区分调用不同的函数
};
struct buffer_t
{
int count;
char buf[26 * 2];
};
4.2 客户端进程:写入数据到共享内存(client.cc)
cpp
#include "Shm.hpp"
int main()
{
// Shm shm;
// //通过同一个唯一key获取同一个共享内存
// shm.Get();
// sleep(5);
// //映射到自己的地址空间中
// shm.Attach();
// shm.GetVirtualAddr();
// sleep(5);
Shm shm(pathname, projid, USER);
buffer_t *shm_buffer = (buffer_t *)shm.GetVirtualAddr();
memset(shm_buffer->buf, 0, 4096);
char ch = 'A';
for(int i = 0; i < 5 * 2; i += 2, ch++)
{
shm_buffer->buf[i] = ch;
usleep(234219);
shm_buffer->buf[i + 1] = ch;
usleep(734217);
shm_buffer->count++;
usleep(734217);
sleep(1);
}
return 0;
}
4.3 服务端进程:创建共享内存并从共享内存读取数据(server.cc)
cpp
#include "Shm.hpp"
int main()
{
// Shm shm;
// shm.Creat();
// sleep(5);
// shm.Attach();
// shm.GetVirtualAddr();
// sleep(5);
// shm.Destroy();
Shm shm(pathname, projid, CREATER);
buffer_t *shm_buffer = (buffer_t *)shm.GetVirtualAddr();
shm_buffer->count = 0;
int old_count = shm_buffer->count; // 模拟一个简单的保护和同步机制
while(true)
{
if(old_count != shm_buffer->count)
{
std::cout << "count: " << shm_buffer->count << std::endl;
std::cout << "data: " << shm_buffer->buf << std::endl;
old_count = shm_buffer->count;
}
usleep(74329);
if(shm_buffer->count >= 4)
{
break;
}
}
return 0;
}
4.4 编译与运行
Makefile:
bash
.PHONY:all
all:client server
client:client.cc
g++ -o $@ $^ -std=c++11
server:server.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f client server
运行步骤与输出结果展示:
- 步骤一:先运行./server
- 步骤二:再运行./client


4.5 残留共享内存清理
若进程异常退出导致共享内存未删除,可通过以下命令手动清理:
bash
# 查看所有System V共享内存
ipcs -m
# 删除指定shmid的共享内存(如shmid=19)
ipcrm -m 19

五. 内核如何管理 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→内核数据结构→物理内存"**的链路,实现对共享内存的创建、挂载、脱离、删除等操作的统一管理。
六. 关键问题分析
6.1 共享内存的同步问题
共享内存本身无同步与互斥机制,若多个进程同时写入,会导致数据覆盖(如进程 A 写 "hello",进程 B 同时写 "world",最终可能得到 "hwllo" 等混乱数据)
解决方案:
- 配合 System V 信号量 :用信号量的 P/V 操作(申请 / 释放资源)保护临界区,确保同一时间仅一个进程访问共享内存;
- 管道通知机制 :用命名管道 实现**"信号唤醒"**(Writer 写完成后向管道发信号,Reader 收到信号后再读);
- 文件锁 :通过fcntl函数 给共享内存关联的文件加锁,实现简单的互斥访问。
6.2 共享内存的删除机制
- shmctl(shmid, IPC_RMID, NULL)的作用是**"标记删除"** ,而非 "立即删除" :
- 标记后,新进程调用shmget无法获取该共享内存;
- 已挂载的进程仍可正常读写,直到所有进程调用shmdt脱离;
- 最后一个进程脱离 后,内核才会真正释放物理内存。
- 若未调用IPC_RMID,共享内存会一直残留于内核中,直到系统重启(需手动清理)。
6.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 手动删除 |
结束语
System V 共享内存是 Linux 中效率最高的 IPC 方式,核心优势在于 "无内核中转、用户态直接通信"。共享内存适合高频、大数据量的跨进程通信场景(如服务器集群数据共享、高频交易系统、视频流传输)。若需实现安全的同步通信,可后续学习 System V 信号量的使用,将二者结合实现 "高效 + 安全" 的跨进程通信。