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

2026年AI Agent实战:从玩具到生产力的落地手册(附源码)

2026年AI Agent实战:从玩具到生产力的落地手册(附源码)

欢迎文末添加好友交流,共同进步! “ 俺はモンキー・D・ルフィ。海贼王になる男だ!” * 前言 * 目录 * 一、AI Agent 的核心架构 * 1.1 什么是AI Agent? * 1.2 2026年Agent技术栈全景 * 二、从零搭建生产级Agent框架 * 2.1 项目结构设计 * 2.2 核心代码:Agent基类 * 2.3 记忆管理系统 * 三、三大核心技术实现 * 3.1 ReAct框架:推理+行动协同 * 3.2 工具调用系统 * 3.3 任务规划器 * 四、实战案例:智能客服Agent * 4.1 场景分析

By Ne0inhk

OpenClaw(小龙虾)B 端企业级应用实战:CentOS 7 快速部署指南,拥有你的第一个 AI 运维员工

大家好,我是独孤风。 春节期间,OpenClaw(小龙虾)彻底火了,人人都在谈论如何“养一只自己的小龙虾”。 过去一年,我们见识了太多能言善辩的大模型,但它们大多停留在“动嘴”阶段。你问它怎么重启服务器,它给你列出 1234 步骤,最后还得你自己去敲键盘。而 OpenClaw 的爆火,是因为它彻底解决了 “执行” 的问题。 它不是一个只会聊天的对话框,而是一个住在你服务器里、拥有操作权限、能 7x24 小时不间断工作的 “数字员工”。  但是,目前的大部分应用还是停留在助手阶段,帮助我们做一些简单的事务性的工作。在 B 端企业级场景下,应用几乎是没有的。那么OpenClaw能不能在B端应用呢?它的出现能否直接改写了运维与开发的成本结构呢?这篇文章我们就来实战一下,实现一个最基本的OpenClaw小龙虾AI运维员工。 正文共:6013字 25图 预计阅读时间:16分钟 文末联系作者,加入AI学习交流群 一、

By Ne0inhk

拯救者Y7000p 2024款安装ubuntu20.04无wifi问题

目录 电脑型号 问题描述 解决方法 方法一:安装网卡驱动 一、确定是否为网卡驱动问题 二、查看网卡型号 三、安装依赖库 四、安装驱动 五、激活对应内核 六、如果还不成功(笔者是在到一步才真正解决) 七、参考链接 方法二:升级内核版本 电脑型号 拯救者Y7000P2024;网卡型号:瑞昱rlt8852ce;ubuntu:20.04;显卡:GTX4060;ubuntu内核:5.15.0-113 问题描述 在安装完ubuntu20.04后发现wifi图标不存在且无法连接无线网 解决方法 此时 ubuntu 系统没有网络,可以使用 USB 数据线连接手机,用手机的网络共享给电脑进行如下操作,或者给电脑插网线,或者插网卡。 确保已经关闭BIOS安全启动

By Ne0inhk
Linux的写作日记:Linux基础开发工具(一)

Linux的写作日记:Linux基础开发工具(一)

前言         经过了之前的Linux学习,相信各位读者已经知道了Linux是什么,并掌握了基本的指令操作。接下来,我们将开启新的航程,驶向Linux基础开发工具的广阔海洋!         在本章节中,你将学会强大的软件包管理器、高效的Vim编辑器、核心的编译器gcc/g++以及版本控制工具git等等。是不是已经干劲十足了?         各位船员请就位,我们的Linux探索之舟,现在扬帆起航! 1.之前权限留下的小尾巴 1.1.目录权限         下面我先给各位读者留下一个小问题:如果我想要进入一个目录,需要什么权限捏?这个问题我先不告诉各位答案,下面我们通过控制变量法来看看这个问题的答案到底是什么,首先我们要创建好一个目录,如下所示。         此时不难看出,这个目录的拥有者和所属组具有所有的权限,而其他用户没有写的权限,下面我们先把使用者的r权限去掉,关于权限如何去掉,我在上篇文章已经说明了,忘记的读者可以看看我上一篇的文章(阅读量这不就上来了(#^.^#)),此时权限就如下所示。         此时我们尝试进入目录,发现是可以进入目录的

By Ne0inhk