跳到主要内容
Linux 进程间通信:System V 共享内存原理与实战 | 极客日志
C++
Linux 进程间通信:System V 共享内存原理与实战 综述由AI生成 System V 共享内存是 Linux 下速度最快的进程间通信方式,通过将物理内存映射到多进程虚拟地址空间,实现零内核中转的数据传递。详细解析了其核心设计思想、API 使用流程(ftok、shmget、shmat、shmdt、shmctl)、内核管理数据结构及实战代码示例。内容涵盖 C++ 封装类实现、编译运行步骤、残留内存清理方法,并重点讨论了同步问题、删除机制及常见错误排查,提供了与其他 IPC 方式的性能对比。
kaikai 发布于 2026/3/24 更新于 2026/5/25 18 浏览前言
在 Linux IPC 体系中,System V 共享内存是速度最快的进程间通信方式。与管道、命名管道需要通过内核缓冲区中转数据不同,共享内存直接将一块物理内存映射到多个进程的虚拟地址空间,进程间数据传递无需内核参与,仅需用户态的内存拷贝,效率远超其他 IPC 方式。
一。共享内存核心原理:为什么它最快?
1.1 核心设计思想
共享内存的本质是 内核维护的一块连续物理内存 ,内核通过特殊的内存管理机制(页表映射),将这块物理内存同时映射到多个进程的虚拟地址空间的'共享区'(虚拟地址通常在 0xC0000000 附近)。此时,多个进程访问自己虚拟地址空间中的这块区域,本质上是访问同一份物理内存 —— 数据传递无需经过内核转发,仅需一次用户态内存拷贝,这是其速度最快的核心原因。
1.2 通信流程与地址空间示意图
进程 A 虚拟地址空间 物理内存 进程 B 虚拟地址空间
+
| 0xC0000000 argv/environ |||| 0xC0000000 argv/environ ||
| 栈 |||| 栈 ||
| 堆 || 共享内存块 || 堆 ||
| 未初始化数据(bss) || (内核维护) || 未初始化数据(bss) ||
| 初始化数据 |<
| 0x08048000 文本段(代码)|||| 0x08048000 文本段(代码)|
+
1.3 核心特性
无内核中转 :进程间数据直接通过物理内存交互,无系统调用开销(管道需 read/write 系统调用);
生命周期随内核 :共享内存创建后,即使创建进程退出,内存块仍存在于内核中,需手动调用 shmctl(IPC_RMID) 删除;
无同步与互斥 :内核不提供数据访问的同步机制,多个进程同时写会导致数据混乱('临界区问题'),需配合信号量等工具实现同步;
跨进程通信 :支持任意进程间通信(无需亲缘关系),只要进程持有相同的 key 或 shmid;
大小建议 :共享内存大小最好是内存页(PAGE_SIZE,默认 4096 字节)的整数倍,避免内存碎片。
二。System V 共享内存核心 API 与内核数据结构
2.1 内核管理数据结构
内核通过 struct shmid_ds 管理共享内存的属性,是共享内存描述结构体的子集,结合 Linux 2.6.18 内核源码,核心字段如下:
struct shmid_ds {
struct ipc_perm shm_perm;
size_t shm_segsz;
pid_t shm_cpid;
shm_lpid;
shm_nattch;
shm_atime;
shm_dtime;
shm_ctime;
* shm_unused2;
};
pid_t
unsigned
short
time_t
time_t
time_t
void
struct ipc_perm 是 System V IPC(共享内存、消息队列、信号量)的通用权限结构体,内核通过该结构体的 key 字段唯一标识一个 IPC 资源。
2.2 核心 API 详解 System V 共享内存的使用流程 遵循 '生成 Key→创建 / 获取共享内存→挂载→读写→脱离→删除' ,核心 API 包括 ftok、shmget、shmat、shmdt、shmctl,逐一解析如下:
2.2.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.2.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。
2.2.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.2.4 shmdt:脱离共享内存 将共享内存从当前进程的虚拟地址空间中脱离(解除映射关系),并非删除共享内存 。
#include <sys/shm.h>
int shmdt (const void * shmaddr) ;
参数 :shmaddr:shmat 返回的虚拟地址指针;
关键注意 :
脱离后,进程无法再访问该共享内存,但共享内存本身仍存在于内核中;
若进程未调用 shmdt 就退出,内核会自动解除映射(避免内存泄漏);
返回值 :成功返回 0,失败返回 -1。
2.2.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);
三。实战案例:基于封装类的共享内存通信 提供 Shm.hpp 封装类对上述核心 API 进行完整封装,无需修改即可使用。结合 Writer.cc(写进程)和 Reader.cc(读进程),实现跨进程数据读写。
3.1 封装类核心逻辑解析(Shm.hpp) Shm.hpp 封装了'生成 Key→创建 / 获取→挂载→删除→属性查询'的全流程,核心接口与 API 映射关系如下:
函数名 调用示例 功能描述 Create()`shmget(key, size, IPC_CREAT IPC_EXCL 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) {
key_t key = GetKey ();
_shmid = shmget (key, _size, flags);
if (_shmid < 0 ) {
perror ("shmget" );
return false ;
}
return true ;
}
public :
bool Create () {
return CreateCoreHelper (IPC_CREAT | IPC_EXCL | 0666 );
}
bool Get () {
return CreateCoreHelper (IPC_CREAT);
}
bool Delete () {
int n = shmctl (_shmid, IPC_RMID, nullptr );
return n < 0 ? false : true ;
}
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;
}
void * Attch () {
_start = (char *)shmat (_shmid, nullptr , 0 );
return _start;
}
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)
#include "Shm.hpp"
#include <iostream>
#include <string>
#include <unistd.h>
Shm shm;
class Init {
public :
Init () {
shm.Get ();
addr = (char *)shm.Attch ();
std::cout << "addr: " << ToHex ((long long )addr) << std::endl;
}
~Init () {
shm.Detach ();
}
char * Addr () { return addr; }
public :
char * addr;
};
Init init;
int main () {
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 );
}
return 0 ;
}
3.3 Reader 进程:从共享内存读取数据(Reader.cc) #include "Shm.hpp"
#include <iostream>
#include <string>
#include <unistd.h>
int main () {
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.Detach ();
shm.Delete ();
return 0 ;
}
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
步骤二 :再运行 ./Writer
3.5 残留共享内存清理 若进程异常退出导致共享内存未删除,可通过以下命令手动清理:
四。内核如何管理 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→内核数据结构→物理内存'的链路,实现对共享内存的创建、挂载、脱离、删除等操作的统一管理。
五。关键问题与避坑指南
5.1 共享内存的同步问题(核心坑!) 共享内存本身无同步与互斥机制 ,若多个进程同时写入,会导致数据覆盖(如进程 A 写'hello',进程 B 同时写'world',最终可能得到'hwllo'等混乱数据)—— 这是重点问题。
配合 System V 信号量 :用信号量的 P/V 操作(申请 / 释放资源)保护临界区,确保同一时间仅一个进程访问共享内存;
管道通知机制 :如实例所示,用命名管道实现'信号唤醒'(Writer 写完成后向管道发信号,Reader 收到信号后再读);
文件锁 :通过 fcntl 函数给共享内存关联的文件加锁,实现简单的互斥访问。
5.2 共享内存的删除机制
shmctl(shmid, IPC_RMID, NULL) 的作用是'标记删除',而非'立即删除':
标记后,新进程调用 shmget 无法获取该共享内存;
已挂载的进程仍可正常读写,直到所有进程调用 shmdt 脱离;
最后一个进程脱离后,内核才会真正释放物理内存。
若未调用 IPC_RMID,共享内存会一直残留于内核中,直到系统重启(需手动清理)。
5.3 常见错误与排查 错误现象 原因分析 解决方案 shmget 报错'File exists'使用 `IPC_CREAT IPC_EXCL` 创建已存在的内存 shmat 返回(void*)-1权限不足(如创建时权限为 0600) 创建时指定 0666 权限 读取数据为空或乱码 1. Writer 未写入就读取;2. 无同步机制 增加 sleep 延迟或实现同步机制 进程退出后内存未释放 未调用 shmctl(IPC_RMID) ipcs -m 查询 + ipcrm -m shmid 手动删除
5.4 共享内存与其他 IPC 的性能对比与总结 IPC 方式 数据传递路径 核心开销 适用场景 速度排名 匿名管道 进程 A→内核缓冲区→进程 B 2 次系统调用 + 2 次内存拷贝 亲缘进程、简单数据流 3 命名管道 进程 A→内核缓冲区→进程 B 2 次系统调用 + 2 次内存拷贝 任意进程、简单数据流 2 System V 共享内存 进程 A→共享内存→进程 B 0 次系统调用 + 1 次内存拷贝 高频 / 大数据量跨进程通信 1
结尾 System V 共享内存是 Linux 中效率最高的 IPC 方式,核心优势在于'无内核中转、用户态直接通信'。共享内存适合高频、大数据量的跨进程通信场景(如服务器集群数据共享、高频交易系统、视频流传输)。若需实现安全的同步通信,可后续学习 System V 信号量的使用,将二者结合实现'高效 + 安全'的跨进程通信。
相关免费在线工具 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