Linux 进程间通信之 System V 共享内存:IPC 的原理与实战

Linux 进程间通信之 System V 共享内存:IPC 的原理与实战
在这里插入图片描述

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


🎬 博主简介:

在这里插入图片描述

文章目录


前言:

在 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)删除;
  • 无同步与互斥:内核不提供数据访问的同步机制,多个进程同时写会导致数据混乱(“临界区问题”),需配合信号量等工具实现同步;
  • 跨进程通信:支持任意进程间通信(无需亲缘关系),只要进程持有相同的keyshmid

大小建议:共享内存大小最好是内存页(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 包括ftokshmgetshmatshmdtshmctl,逐一解析如下:

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);
  • 参数shmaddrshmat返回的虚拟地址指针;
  • 关键注意
    • 脱离后,进程无法再访问该共享内存,但共享内存本身仍存在于内核中;
    • 若进程未调用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;#endif

3.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_idsstruct shmid_kernel管理所有共享内存资源,核心逻辑如下:

  • 全局管理结构:内核维护shm_ids全局变量(struct ipc_ids类型),记录系统中所有共享内存的元数据(如max_idin_useentries数组);
  • 索引机制struct ipc_id_aryentries数组存储struct kern_ipc_perm指针,内核通过shmid索引到对应的共享内存权限结构体;
  • 物理内存关联struct shmid_kernel包含struct file *shm_file字段,通过文件系统的inodevm_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_CREATipcrm -m shmid删除旧内存
shmat返回(void*)-1权限不足(如创建时权限为 0600)创建时指定0666权限
读取数据为空或乱码1. Writer 未写入就读取;2. 无同步机制增加sleep延迟或实现同步机制
进程退出后内存未释放未调用shmctl(IPC_RMID)ipcs -m查询 +ipcrm -m shmid手动删除

5.4 共享内存与其他 IPC 的性能对比与总结

IPC 方式数据传递路径核心开销适用场景速度排名
匿名管道进程 A→内核缓冲区→进程 B2 次系统调用 + 2 次内存拷贝亲缘进程、简单数据流3
命名管道进程 A→内核缓冲区→进程 B2 次系统调用 + 2 次内存拷贝任意进程、简单数据流2
System V 共享内存进程 A→共享内存→进程 B0 次系统调用 + 1 次内存拷贝高频 / 大数据量跨进程通信1

结尾:

🍓 我是草莓熊 Lotso!若这篇技术干货帮你打通了学习中的卡点: 👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长 ❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量 ⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用 💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑 🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解 技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标! 

结语:System V 共享内存是 Linux 中效率最高的 IPC 方式,核心优势在于 “无内核中转、用户态直接通信”。共享内存适合高频、大数据量的跨进程通信场景(如服务器集群数据共享、高频交易系统、视频流传输)。若需实现安全的同步通信,可后续学习 System V 信号量的使用,将二者结合实现 “高效 + 安全” 的跨进程通信。创作不易,觉得有帮助的话,欢迎点赞、收藏、关注三连~ 后续会持续更新 Linux IPC 系列(信号量、消息队列),带你从底层吃透进程间通信技术。

✨把这些内容吃透超牛的!放松下吧✨ʕ˘ᴥ˘ʔづきらど

Read more

算法王冠上的明珠——动态规划之路径问题(第一篇)

算法王冠上的明珠——动态规划之路径问题(第一篇)

目录 1. 什么叫路径类动态规划 一、核心定义(通俗理解) 二、核心特征(识别这类问题的关键) 2. 动态规划步骤 状态表示 状态转移方程 初始化 填表顺序 返回值 3. 例题讲解 3.1 LeetCode62. 不同路径 3.2 LeetCode63. 不同路径 II 3.3 LeetCodeLCR 166. 珠宝的最高价值 今天我们来聊一聊动态规划的路径类问题。 1. 什么叫路径类动态规划 路径类动态规划是 动态规划的一个重要分支,核心解决 “从起点到终点的路径相关问题”—— 比如 “路径总数”“最短路径长度”“路径上的最大 / 最小和” 等,其本质是通过 “状态递推” 避免重复计算,高效求解多阶段决策的路径问题。 一、

By Ne0inhk
【数据结构】常见时间复杂度以及空间复杂度

【数据结构】常见时间复杂度以及空间复杂度

时间复杂度与空间复杂度 * 一、复杂度的概念 * 二、时间复杂度 * 1、大O的渐进表示法 * 2、函数clock计算运算时间 * 3、常见复杂度对比 * 3.1常数项复杂度 * 3.2线性时间复杂度 * 案例1 * 案例2 * 3.3平方阶复杂度 * 3.4对数复杂度 * 3.5递归函数 * 单递归 * 双递归 * 三、空间复杂度 * 冒泡排序O(1) * 三个反置O(N) 一、复杂度的概念 * 一个算法的好坏,主要是对比两者的时间和空间两个维度,也就是时间和空间复杂度。 * 时间复杂度主要衡量一个算法运行的快慢,空间复杂度主要衡量一个算法运行需要的额外空间 二、时间复杂度 * 算法的时间复杂度是一个函数式T(N),算法中的基本操作的执行次数,为算法的时间复杂度。 * 注:编译器的不同,编译所需要的时间也不同。越新的编译器,编译的时间往往比旧的编译器快 * 当一个算法函数式为T(

By Ne0inhk
数据结构-单链表

数据结构-单链表

单链表 * 概念与结构 * 结点 * 链表的性质 * 链表的打印 * 实现单链表 * 头文件 * 源文件 * 单链表的打印 * 单链表申请新节点内存 * 尾插 * 头插 * 尾删 * 头删 * 查找 * 在指定位置之前插入数据 * 在指定位置之后插入数据 * 删除pos结点 * 删除pos之后的结点 * 销毁链表 * 链表的分类 * 代码地址 概念与结构 概念:链表是⼀种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 逻辑结构:线性 物理结构(存储结构):不一定是线性的 链表就类似一个火车,车头是哨兵位(可有可无),车厢是节点 * 将火车里的某节车厢去掉或加上,不会影响其他车厢,每节车厢都是独立存在的。 在链表⾥,每节“车厢”是什么样的呢? \color{red}{在链表⾥,每节“车厢”是什么样的呢?

By Ne0inhk
【数据结构】排序详解:从快速排序分区逻辑,到携手冒泡排序的算法效率深度评测

【数据结构】排序详解:从快速排序分区逻辑,到携手冒泡排序的算法效率深度评测

🔥@晨非辰Tong: 个人主页 👀专栏:《C语言》、《数据结构与算法入门指南》 💪学习阶段:C语言、数据结构与算法初学者 ⏳“人理解迭代,神理解递归。” 文章目录 * 引言 * 一、介绍交换排序 * 二、高效交换--快速排序“:递归版 * 2.1 介绍:创造背景以及基本思想 * 2.2 基于二叉树结构的主体框架 * 三、找基准值key的三种==递归版==实战方法 * 3.1 快排核心构成:寻找key的算法之"hoare"版本 * 3.3.1 画图理解算法 * 3.3.2 代码实战 * 3.1.3 ==**代码分析**== * 3.2

By Ne0inhk