一文吃透Linux System V共享内存:原理+实操+避坑指南
本文深入解析了SystemV共享内存机制,作为Linux高效IPC方式之一,它允许多进程直接访问同一物理内存区域,避免了数据拷贝开销。文章从原理、API调用到实战代码演示了完整使用流程:通过ftok生成唯一key,shmget创建/获取共享内存,shmat挂载到进程地址空间,shmdt脱离映射,shmctl控制生命周期。重点指出了使用中的常见问题:内存泄漏需手动删除、多进程竞争需同步机制、key一

🔥个人主页:Cx330🌸
❄️个人专栏:《C语言》《LeetCode刷题集》《数据结构-初阶》《C++知识分享》
《优选算法指南-必刷经典100题》《Linux操作系统》:从入门到入魔
🌟心向往之行必能至
🎥Cx330🌸的简介:

目录
2.3.1 ftok:生成唯一 Key(共享内存的 “身份证”)
3.2 Writer 进程:写入数据到共享内存(Writer.cc)
3.3 Reader 进程:从共享内存读取数据(Reader.cc)
前言:
在Linux进程间通信(IPC)中,共享内存是效率最高的方式之一——它直接让多个进程共享同一块物理内存区域,无需像管道、消息队列那样进行数据拷贝,省去了内核与用户空间之间的频繁数据交换开销。而System V共享内存(简称SysV共享内存),作为Linux早期就支持的经典IPC机制,至今仍在很多底层开发、高性能程序中广泛应用。
今天这篇博客,就带大家从“是什么、怎么工作、怎么用、踩过哪些坑”四个维度,彻底搞懂System V共享内存,全程附代码示例,新手也能跟着上手。
一、先搞懂:System V共享内存是什么?
System V共享内存,是System V系列IPC机制(包括共享内存、消息队列、信号量)中的一种,核心作用是“让多个进程访问同一块物理内存”,实现高效的数据共享。
我们可以用一个通俗的比喻理解:把物理内存想象成一个“公共仓库”,System V共享内存就相当于在这个仓库里划分出一块专属区域,然后给多个进程发放“访问权限”,这些进程可以直接往这块区域读写数据,不用再通过“中间人”(比如内核缓冲区)传递,速度自然更快。
1.1 通信流程与地址空间示意图

1.2 关键特点(必记)
-
高效性:无内核与用户空间的数据拷贝,直接操作物理内存,是所有IPC机制中速度最快的。
-
持续性:共享内存一旦创建,会一直存在于内核中,直到被显式删除(或系统重启),不受进程生命周期影响(即使创建它的进程退出,内存依然存在)。
-
非实时性:没有自带的同步机制,多个进程同时读写时,需要手动搭配信号量等同步手段,否则会出现数据混乱。
-
标识符(key):每个System V共享内存都有一个唯一的key值,进程通过key值找到对应的共享内存,实现跨进程关联。
1.3 核心理解与提出问题

问题:
-
整个过程是谁做的?操作系统 OS,那是谁让 OS 做的呢,操作系统必然会提供系统调用!我们程序员所写的就可以调用系统调用,所以是用户让操作系统做的。
-
共享区这个东西用户可以直接访问吗?共享区这个不属于内核空间,是属于用户空间的。所以我们用户随便拿个指针就可以直接访问了!意味着我们也可以不需要使用系统调用来读写 shm。我们之前使用动态库也可以没系统调用。
总结:创建和 “删除” shm 需要系统调用,使用 shm 不需要(类似 malloc ())。
我们创建管道的时候用了系统调用,使用管道的时候也用了,这也是和共享内存的差别。用户空间最有代表的就是用户可以直接访问。
二、底层原理:System V共享内存如何工作?
2.1 内核管理数据结构
内核通过struct shmid_ds管理共享内存的属性,是共享内存描述结构体 的子集,结合 Linux 2.6.18 内核源码,核心字段如下:
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 核心原理拆解
System V 共享内存的使用流程遵循 “生成 Key→创建 / 获取共享内存→挂载→读写→脱离→删除”,核心 API 包括ftok、shmget、shmat、shmdt、shmctl,逐一解析如下:
2.3 先熟悉5个核心系统调用
2.3.1 ftok:生成唯一 Key(共享内存的 “身份证”)
用于将 “文件路径 + 项目 ID” 转换为唯一的key_t类型值,作为共享内存的全局标识 —— 多个进程通过相同的key可获取同一块共享内存。
#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.3.2 shmget:创建 / 获取共享内存
用于创建新的共享内存或获取已存在的共享内存,返回共享内存标识符(shmid),后续操作均通过shmid关联共享内存。
#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值补充:
创建成功了就会返回一个共享内存的标识符,也可以叫句柄,但是跟文件描述符可没有关系联系。这也是这种技术会被边缘化的原因之一,要是能跟文件关联上多好
shmget 怎么知道 shm 存在还是不存在呢?所以共享内存一定要有一个标识 shm 唯一性的标识符!在哪里?在它的结构体里一定有一个唯一标识符的。需要用户设置唯一值,作为 shm 在内核中的唯一值,我们叫做 key
2.3.3 shmat:挂载共享内存
将共享内存映射到当前进程的虚拟地址空间,返回映射后的虚拟地址指针 —— 进程通过该指针读写共享内存。
#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.3.4 shmdt:脱离共享内存
将共享内存从当前进程的虚拟地址空间中脱离(解除映射关系),并非删除共享内存。
#include <sys/shm.h>
int shmdt(const void *shmaddr);
- 参数:
shmaddr:shmat返回的虚拟地址指针; - 关键注意:
- 脱离后,进程无法再访问该共享内存,但共享内存本身仍存在于内核中;
- 若进程未调用
shmdt就退出,内核会自动解除映射(避免内存泄漏);
- 返回值:成功返回 0,失败返回 - 1。
2.3.5 shmctl:控制共享内存(核心功能:删除)
用于获取共享内存属性、修改属性或删除共享内存,是共享内存生命周期管理的核心 API。
#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:标记共享内存为 “待删除”,后续新进程无法挂载,所有进程脱离后内核释放内存
- IPC_STAT:获取共享内存属性,存入buf指向的
buf:存储属性的结构体指针(IPC_RMID时可设为 NULL);
返回值:成功返回 0,失败返回 - 1。

三、实操实战:手把手用代码操作System V共享内存
提供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";
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)
// header only
#include "Shm.hpp"
#include <iostream>
#include <string>
int main()
{
Shm shm;
shm.Get();
shm.Debug();
return 0;
}
3.3 Reader 进程:从共享内存读取数据(Reader.cc)
// header only
#include "Shm.hpp"
#include <iostream>
#include <string>
#include <unistd.h>
// Writer -> shm -> Reader
int main()
{
// 1.在内核中创建共享内存
Shm shm;
shm.Create();
sleep(3);
shm.Attach();
shm.Debug();
shm.GetShmAttr();
sleep(5);
shm.Delete();
return 0;
}
3.4 编译与运行
3.4.1 Makefile
all:Writer Reader
Reader:Reader.cc
g++ -o $@ $^ -std=c++11
Writer:Writer.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f Writer Reader
3.4.2 运行步骤与输出结果展示
- 步骤一:先运行./Reader
- 步骤二:再运行./Writter
先补充一下之前共享内存代码这块:我们把之前的代码中的大小变成 4097 进行测试,之前我们说过大小必须是 4096 的整数倍,那会发生什么呢。
操作系统会给你申请 4096*2,这不就是浪费了 4095 嘛。
那这样难道就不可以嘛。当然不行,如果你实际是 4097,操作系统给你了 4096*2,那我们访问越界的时候,可能都不会有提示了。至于为什么会这样给,我们后面线程的时候来说
四. 内核管理 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→内核数据结构→物理内存” 的链路,实现对共享内存的创建、挂载、脱离、删除等操作的统一管理。
五、避坑指南:这些问题一定要注意!
System V共享内存虽然高效,但使用时容易踩坑,尤其是新手,以下几个问题必须重点关注:
1. 共享内存忘记删除,导致内存泄漏
坑点:共享内存的生命周期独立于进程,若进程异常退出(比如崩溃),没有执行shmctl(IPC_RMID),共享内存会一直占用物理内存,长期下来会导致内存泄漏。
解决方法:
-
在程序中添加信号处理(如捕获SIGINT信号),进程退出前强制删除共享内存。
-
手动查看/删除共享内存:
-
查看所有System V共享内存ipcs -m -
删除指定shmid的共享内存ipcrm -m 123456(123456是shmid)
-
2. 多个进程同时读写,导致数据混乱
坑点:System V共享内存没有自带同步机制,若多个进程同时写入,会出现数据覆盖、错乱的问题。
解决方法:搭配System V信号量(或互斥锁)使用,实现“互斥访问”——同一时间只有一个进程能读写共享内存。
3. key值不一致,导致进程找不到共享内存
坑点:不同进程使用ftok()生成key时,路径(KEY_PATH)或标识(KEY_ID)不一致,会导致生成的key不同,无法找到同一个共享内存。
解决方法:所有需要通信的进程,使用完全相同的KEY_PATH和KEY_ID生成key;或直接使用固定的key值(如0x123456),避免ftok()的潜在问题。
4. 共享内存大小设置不合理
坑点:设置的共享内存大小不足,导致写入数据被截断;或设置过大,浪费物理内存。
六、总结:System V共享内存的适用场景
System V共享内存的核心优势是“高效”,核心劣势是“需要手动管理同步和生命周期”,因此它适合以下场景:
-
高性能要求的进程间通信(如实时数据传输、高频数据共享)。
-
多个进程需要共享大量数据,且数据交换频繁(避免频繁拷贝)。
-
底层开发、嵌入式Linux开发(System V IPC兼容性好,占用资源少)。
最后提醒:虽然System V共享内存很经典,但在现代Linux开发中,也可以关注POSIX共享内存(如shm_open()、mmap()),它更灵活、更符合POSIX标准,不过System V共享内存的兼容性和底层可控性,依然是它不可替代的优势。
如果觉得这篇博客对你有帮助,欢迎点赞收藏,评论区留言讨论你在使用System V共享内存时遇到的问题~
更多推荐


所有评论(0)