跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++

Linux System V 共享内存:原理、实操与避坑指南

Linux System V 共享内存是进程间通信的高效机制,允许直接访问物理内存区域。核心涉及 ftok 生成标识、shmget 创建获取、shmat 挂载、shmdt 脱离及 shmctl 控制。使用时需注意同步机制缺失导致的并发问题、资源未释放导致的泄漏以及 key 值匹配问题。适用于高性能数据传输场景,现代开发也可考虑 POSIX 共享内存替代方案。

星星泡饭发布于 2026/3/29更新于 2026/6/619 浏览
Linux System V 共享内存:原理、实操与避坑指南

Linux System V 共享内存:原理、实操与避坑指南

前言

在 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 核心理解与提出问题

文章配图

问题:

  1. **整个过程是谁做的?**操作系统 OS,那是谁让 OS 做的呢,操作系统必然会提供系统调用!我们程序员所写的就可以调用系统调用,所以是用户让操作系统做的。
  2. **共享区这个东西用户可以直接访问吗?**共享区这个不属于内核空间,是属于用户空间的。所以我们用户随便拿个指针就可以直接访问了!意味着我们也可以不需要使用系统调用来读写 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:**标记共享内存为'待删除',后续新进程无法挂载,所有进程脱离后内核释放内存
  • 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 共享内存的兼容性和底层可控性,依然是它不可替代的优势。

目录

  1. Linux System V 共享内存:原理、实操与避坑指南
  2. 前言
  3. 一、先搞懂:System V 共享内存是什么?
  4. 1.1 通信流程与地址空间示意图
  5. 1.2 关键特点(必记)
  6. 1.3 核心理解与提出问题
  7. 二、底层原理:System V 共享内存如何工作?
  8. 2.1 内核管理数据结构
  9. 2.2 核心原理拆解
  10. 2.3 先熟悉 5 个核心系统调用
  11. 2.3.1 ftok:生成唯一 Key(共享内存的“身份证”)
  12. 2.3.2 shmget:创建 / 获取共享内存
  13. 2.3.3 shmat:挂载共享内存
  14. 2.3.4 shmdt:脱离共享内存
  15. 2.3.5 shmctl:控制共享内存(核心功能:删除)
  16. 三、实操实战:手把手用代码操作 System V 共享内存
  17. 3.1 封装类核心逻辑解析(Shm.hpp)
  18. 3.2 Writer 进程:写入数据到共享内存(Writer.cc)
  19. 3.3 Reader 进程:从共享内存读取数据(Reader.cc)
  20. 3.4 编译与运行
  21. 3.4.1 Makefile
  22. 3.4.2 运行步骤与输出结果展示
  23. 四。内核管理 System V 共享内存
  24. 五、避坑指南:这些问题一定要注意!
  25. 1. 共享内存忘记删除,导致内存泄漏
  26. 2. 多个进程同时读写,导致数据混乱
  27. 3. key 值不一致,导致进程找不到共享内存
  28. 4. 共享内存大小设置不合理
  29. 六、总结:System V 共享内存的适用场景
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • 企业电子招投标采购系统功能简介
  • Gossip 协议解读
  • C/C++ 错误信息捕获与处理实战指南
  • llama-cpp-python 安装配置与性能优化指南
  • AI 产品经理的 5 点核心认知与实践指南
  • 网络安全核心基础知识详解
  • 第五届长城杯 2025 Web 初赛 Writeup
  • 五大生成模型全方位对比
  • 混沌工程开源平台解析与测试实践指南
  • 大型语言模型微调入门指南
  • Python 工厂模式封装 Webhook 群聊机器人
  • Python 全流程图文安装与配置指南
  • 50 道前端核心面试题:HTML/CSS/JS/Vue/React/TS/工程化/网络/跨端
  • 使用 cpolar 实现 Open-Lovable 远程访问与网页克隆
  • Android MVVM 架构实战:深入理解 LiveData 生命周期与数据观察
  • Python 爬虫入门指南:基础原理与实战
  • Python入门安装教程
  • 大模型如何赋能千行百业:现状、挑战与未来
  • Android 开发新趋势:车载 Framework 与智能座舱技术解析
  • Retinaface+CurricularFace 镜像内 Python 3.11.14 安全补丁升级方法

相关免费在线工具

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online

  • JSON 压缩

    通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online

  • JSON美化和格式化

    将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online