目录
前言
一、共享内存的诞生:为何它是最快的 IPC?
1.1 传统 IPC 的性能瓶颈:两次拷贝 + 内核中转
1.2 共享内存的性能突破:零内核中转 + 一次拷贝(甚至零拷贝)
1.3 System V 共享内存的核心特征
二、共享内存的核心原理:物理内存映射与地址空间关联
2.1 共享内存的底层实现逻辑
2.2 共享内存的地址空间示意图
2.3 System V 共享内存的核心数据结构:shmid_ds
三、共享内存的核心函数:从创建到释放的完整流程
3.1 第一步:创建 / 获取共享内存 ——shmget ()
3.1.1 函数原型
3.1.2 关键参数详解
(1)key 值:共享内存的 “唯一标识”
(2)shmflg:创建规则 + 权限
3.1.3 简单示例:生成 key 并创建共享内存
3.2 第二步:映射共享内存到进程地址空间 ——shmat ()
3.2.1 函数原型
3.2.2 关键参数详解
3.2.3 核心注意点
3.3 第三步:解除共享内存的映射 ——shmdt ()
3.3.1 函数原型
3.3.2 核心注意点
3.4 第四步:控制 / 删除共享内存 ——shmctl ()
3.4.1 函数原型
3.4.2 核心控制命令(cmd)
3.4.3 核心注意点
3.5 共享内存的全生命周期函数调用流程
四、共享内存的基础实战:实现简单的进程间通信
4.1 公共头文件:comm.h
4.2 公共实现文件:comm.c
4.3 服务端程序:server.c
4.4 客户端程序:client.c
4.5 编译脚本:Makefile
4.6 编译与运行
4.6.1 编译
4.6.2 运行
4.7 案例核心亮点与注意点
五、共享内存的核心问题:同步与互斥的实现
5.1 共享内存的并发问题:数据竞争
5.2 解决思路:手动添加同步互斥机制
5.3 进阶实战:管道实现共享内存的同步控制
六、共享内存的内核管理与命令行操作
6.1 查看共享内存:ipcs -m
6.2 删除共享内存:ipcrm -m shmid
6.3 内核管理 IPC 资源的核心结构
七、System V 共享内存的核心特点、使用场景与局限性
7.1 核心特点
7.2 典型使用场景
7.3 局限性
7.4 与其他 IPC 方式的性能对比
八、共享内存开发的避坑指南
8.1 坑 1:未显式删除共享内存,导致内核内存泄漏
8.2 坑 2:未处理 ftok 函数的失败场景
8.3 坑 3:共享内存大小未页对齐,导致内存浪费
8.4 坑 4:多个进程同时读写,未实现同步机制,导致数据错乱
8.5 坑 5:解除映射后,继续使用原指针访问共享内存,导致段错误
8.6 坑 6:使用 IPC_CREAT|IPC_EXCL 时,未处理共享内存已存在的场景
8.7 坑 7:权限设置不当,导致其他进程无法访问共享内存
总结
前言
在 Linux 进程间通信的大家族中,System V 共享内存是性能天花板般的存在 —— 它是最快的 IPC 实现方式,没有之一。不同于管道(匿名 / 命名)的内核缓冲区中转、消息队列的内核封装,共享内存让多个进程直接访问同一块物理内存,进程间数据传递彻底摆脱内核系统调用的开销,实现了真正的 “内存级” 数据交互。
但天下没有免费的午餐,极致的性能背后是缺乏原生的同步与互斥机制,这也让共享内存的使用门槛远高于管道和消息队列。本文将从 System V 共享内存的核心原理出发,一步步拆解其设计思想、数据结构、核心函数、实战开发,还会讲解如何解决其同步问题,让你彻底吃透这一高性能 IPC 方式,从原理到实战拿捏到位。下面就让我们正式开始吧!

一、共享内存的诞生:为何它是最快的 IPC?
在学习 System V 共享内存(后文简称 “共享内存”)之前,我们先思考一个问题:为什么管道、消息队列的通信效率远不如共享内存? 答案藏在数据拷贝的次数和内核参与度中。
1.1 传统 IPC 的性能瓶颈:两次拷贝 + 内核中转
无论是匿名管道、命名管道,还是消息队列,进程间的通信都离不开内核的中转,数据至少会经历两次内存拷贝:
这个过程中,每次数据传递都需要陷入内核态、执行系统调用、完成两次拷贝,而系统调用和内存拷贝本身就是性能开销的大头,当需要传输大量数据时,这种开销会被无限放大。
同时,内核会对管道 / 消息队列的读写做同步互斥控制,进一步增加了通信的耗时。
1.2 共享内存的性能突破:零内核中转 + 一次拷贝(甚至零拷贝)
共享内存的设计思想彻底颠覆了传统 IPC 的通信模式:内核在物理内存中开辟一块连续的内存区域,将这块内存映射到多个通信进程的用户态地址空间,让进程直接访问这块内存,就像访问自己的堆 / 栈内存一样。
整个通信过程中,内核只负责内存的创建和映射,不参与任何数据传递,数据传递完全在进程的用户态完成,数据拷贝次数被极致压缩:
- 理想情况(零拷贝):一个进程将数据写入共享内存,另一个进程直接读取,无需任何数据拷贝;
- 实际场景(一次拷贝):进程从磁盘读取数据后,直接写入共享内存,仅需一次磁盘到内存的拷贝。
没有系统调用的开销、没有内核中转的损耗、极少的内存拷贝,这就是共享内存成为最快 IPC 的核心原因。在大数据量传输场景(如音视频数据、大型文件、数据库缓存)中,共享内存的性能优势会体现得淋漓尽致。
1.3 System V 共享内存的核心特征
作为 System V IPC 家族的核心成员(另外两个是消息队列、信号量),共享内存还具备以下核心特征,这也是它与 POSIX 共享内存、内存映射的重要区别:
二、共享内存的核心原理:物理内存映射与地址空间关联
要理解共享内存,必须先搞清楚内核如何创建共享内存、进程如何访问共享内存,核心是理解物理内存开辟和进程地址空间映射两个关键步骤。
2.1 共享内存的底层实现逻辑
共享内存的实现基于 Linux 的虚拟内存管理机制,核心分为三步:
简单来说,共享内存就是多个进程的虚拟地址空间,映射到同一块物理内存,这是进程间能直接共享数据的根本原因。
2.2 共享内存的地址空间示意图
在 Linux 进程的 32 位虚拟地址空间中,共享内存、内存映射、共享库都位于0x40000000~0xC0000000的区域(堆和栈之间),这块区域专门用于映射内核管理的共享资源。
- 进程 A 的虚拟地址空间中,指针addr_A指向一块虚拟内存,该虚拟内存映射到物理内存地址0x12340000;
- 进程 B 的虚拟地址空间中,指针addr_B指向另一块虚拟内存,该虚拟内存同样映射到物理内存地址0x12340000;
- 进程 A 通过addr_A写入数据,进程 B 通过addr_B能立即读取到,实现数据共享。
这种映射关系由内核的页表维护,进程对虚拟地址的访问会被硬件 MMU(内存管理单元)转换为对物理地址的访问,整个过程对进程透明。

2.3 System V 共享内存的核心数据结构:shmid_ds
内核为每一块共享内存维护一个shmid_ds结构体,用于管理共享内存的属性、权限、关联进程等信息,这是共享内存的 “管理档案”。其核心定义如下(基于 Linux 2.6 内核):
struct shmid_ds {
struct ipc_perm shm_perm; // IPC资源的通用权限结构
size_t shm_segsz; // 共享内存段的大小(字节)
__kernel_time_t shm_atime; // 最后一次附加(shmat)的时间
__kernel_time_t shm_dtime; // 最后一次分离(shmdt)的时间
__kernel_time_t shm_ctime; // 最后一次修改的时间
__kernel_ipc_pid_t shm_cpid; // 创建共享内存的进程PID
__kernel_ipc_pid_t shm_lpid; // 最后一次操作共享内存的进程PID
unsigned short shm_nattch; // 当前附加到该共享内存的进程数
unsigned short shm_unused; // 兼容字段
void *shm_unused2; // 兼容字段
void *shm_unused3; // 兼容字段
};
其中,ipc_perm是所有 System V IPC 资源(共享内存、消息队列、信号量)的通用权限结构,包含了 Key 值、所有者 UID/GID、访问权限等核心信息,内核通过该结构保证 IPC 资源的访问安全。
三、共享内存的核心函数:从创建到释放的完整流程
System V 共享内存的操作围绕四个核心函数展开,这四个函数完成了创建 / 获取、映射、分离、控制 / 删除共享内存的全生命周期管理,所有函数的头文件均为<sys/ipc.h>和<sys/shm.h>。
3.1 第一步:创建 / 获取共享内存 ——shmget ()
shmget函数的作用是向内核申请创建一块新的共享内存,或获取已存在的共享内存,返回一个唯一的共享内存标识码shmid,后续操作均通过该shmid进行。
3.1.1 函数原型
#include <sys/ipc.h>
#include <sys/shm.h>
// 功能:创建/获取共享内存段
// 参数:
// key:共享内存的键值,用于标识唯一的共享内存段,由ftok函数生成
// size:共享内存的大小(字节),建议为页大小(4096)的整数倍
// shmflg:标志位,由IPC_CREAT、IPC_EXCL和权限位(如0666)组合而成
// 返回值:成功返回shmid(非负整数),失败返回-1,并设置errno
int shmget(key_t key, size_t size, int shmflg);
3.1.2 关键参数详解
(1)key 值:共享内存的 “唯一标识”
key是一个key_t类型的整数(本质是int),是共享内存的全局唯一标识,不同进程通过相同的 key 值,才能找到同一块共享内存。
key 值通常由ftok() 函数生成,该函数通过一个已存在的文件路径和一个整型项目 ID,生成唯一的 key 值,原型如下:
// 功能:生成System V IPC的键值
// 参数:
// pathname:已存在的文件路径(如"./")
// proj_id:整型项目ID(非0,通常取0x66、0x6666等)
// 返回值:成功返回key值,失败返回-1
key_t ftok(const char *pathname, int proj_id);
注意:ftok 生成 key 值的依据是文件的 inode 号和 proj_id,若文件被删除重建(inode 号改变),即使路径和 proj_id 相同,生成的 key 值也会不同。
(2)shmflg:创建规则 + 权限
shmflg是标志位的组合,核心取值有三个,可与权限位(如 0666)按位或:
- IPC_CREAT:如果 key 值对应的共享内存不存在,则创建;若已存在,则直接获取并返回 shmid;
- IPC_EXCL:必须与 IPC_CREAT 配合使用(IPC_CREAT | IPC_EXCL),表示创建全新的共享内存;若 key 值对应的共享内存已存在,则直接失败(返回 – 1),避免覆盖已有资源;
- 权限位:如 0644、0666,与文件权限一致,用于控制不同用户对共享内存的访问权限。
常用组合:
- IPC_CREAT | 0666:创建或获取共享内存,适用于接收进程;
- IPC_CREAT | IPC_EXCL | 0666:创建全新的共享内存,适用于通信的发起进程(如服务端)。
3.1.3 简单示例:生成 key 并创建共享内存
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <perror.h>
#define PATHNAME "./" // 已存在的文件路径
#define PROJ_ID 0x6666 // 项目ID
#define SHM_SIZE 4096 // 共享内存大小,页对齐
int main()
{
// 生成key值
key_t key = ftok(PATHNAME, PROJ_ID);
if (key == -1)
{
perror("ftok error");
return -1;
}
printf("生成key值:0x%x\\n", key);
// 创建全新的共享内存
int shmid = shmget(key, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);
if (shmid == -1)
{
perror("shmget error");
return -1;
}
printf("创建共享内存成功,shmid:%d\\n", shmid);
return 0;
}
3.2 第二步:映射共享内存到进程地址空间 ——shmat ()
shmget仅创建 / 获取了共享内存的内核资源,进程还无法访问,需要通过shmat函数将共享内存的物理地址映射到进程的虚拟地址空间,映射成功后返回一个指针,进程通过该指针访问共享内存。
3.2.1 函数原型
// 功能:将共享内存段映射到当前进程的用户态地址空间
// 参数:
// shmid:由shmget返回的共享内存标识码
// shmaddr:指定映射的虚拟地址,通常设为NULL,由内核自动分配合适的地址
// shmflg:映射标志位,核心取值为0或SHM_RDONLY
// 返回值:成功返回映射后的虚拟地址指针,失败返回(void*)-1,并设置errno
void *shmat(int shmid, const void *shmaddr, int shmflg);
3.2.2 关键参数详解
- 0:默认值,表示共享内存可读可写;
- SHM_RDONLY:表示共享内存只读,进程只能读取数据,无法写入,适用于只读场景,提高安全性。
3.2.3 核心注意点
- 映射成功后,返回的指针可以像普通的 malloc 指针一样使用,支持任意的内存操作(如赋值、拷贝、指针偏移);
- 进程可以多次调用 shmat 映射同一块共享内存,会得到不同的虚拟地址指针,均指向同一块物理内存;
- 映射后,内核的 shmid_ds 结构体中,shm_nattch(附加进程数)会加 1。
3.3 第三步:解除共享内存的映射 ——shmdt ()
当进程不再需要访问共享内存时,需要通过shmdt函数解除虚拟地址空间与共享内存物理地址的映射关系,释放进程的虚拟地址资源。
3.3.1 函数原型
// 功能:解除共享内存与当前进程地址空间的映射
// 参数:
// shmaddr:由shmat返回的映射地址指针
// 返回值:成功返回0,失败返回-1,并设置errno
int shmdt(const void *shmaddr);
3.3.2 核心注意点
- shmdt 不是删除共享内存,只是解除进程与共享内存的映射关系,共享内存仍存在于内核中;
- 解除映射后,进程不能再通过原指针访问共享内存,否则会导致段错误(SIGSEGV);
- 解除映射后,内核的 shmid_ds 结构体中,shm_nattch(附加进程数)会减 1;
- 进程退出时,内核会自动解除该进程对所有共享内存的映射,但若未显式删除共享内存,共享内存仍会存在于内核中。
3.4 第四步:控制 / 删除共享内存 ——shmctl ()
shmctl是共享内存的控制函数,支持获取共享内存的属性、修改属性、删除共享内存等操作,是管理共享内存的核心函数,其中删除共享内存(IPC_RMID) 是最常用的功能。
3.4.1 函数原型
// 功能:控制共享内存段的属性,核心用于删除共享内存
// 参数:
// shmid:由shmget返回的共享内存标识码
// cmd:控制命令,核心取值为IPC_STAT、IPC_SET、IPC_RMID
// buf:指向shmid_ds结构体的指针,用于获取/设置共享内存属性,删除时设为NULL
// 返回值:成功返回0,失败返回-1,并设置errno
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
3.4.2 核心控制命令(cmd)
| IPC_STAT | 将内核中该共享内存的 shmid_ds 结构体数据,拷贝到用户态的 buf 中,用于获取共享内存属性 |
| IPC_SET | 在进程有足够权限的前提下,将用户态 buf 中的 shmid_ds 数据,设置到内核的共享内存管理结构中,用于修改共享内存属性(如权限) |
| IPC_RMID | 删除共享内存段,此时 buf 设为 NULL 即可;即使有进程仍映射了该共享内存,内核也会立即标记其为待删除,当最后一个进程解除映射后,释放物理内存 |
3.4.3 核心注意点
- IPC_RMID 是唯一能删除共享内存的方式,必须显式调用,否则共享内存会一直存在于内核中,直到系统重启,造成内存泄漏;
- 调用shmctl(shmid, IPC_RMID, NULL)后,共享内存的 key 值会失效,新进程无法通过该 key 获取共享内存,但已有映射的进程仍可继续访问,直到解除映射;
- 删除共享内存的操作,通常由通信的发起进程(服务端) 执行。
3.5 共享内存的全生命周期函数调用流程
结合四个核心函数,共享内存的完整使用流程可总结为:
发起进程(服务端):
ftok()生成 key → shmget(IPC_CREAT|IPC_EXCL|0666)创建共享内存 → shmat()映射内存 → 读写共享内存 → shmdt()解除映射 → shmctl(IPC_RMID)删除共享内存。
接收进程(客户端):
ftok()生成相同 key → shmget(IPC_CREAT|0666)获取共享内存 → shmat()映射内存 → 读写共享内存 → shmdt()解除映射(无需删除)。
四、共享内存的基础实战:实现简单的进程间通信
理论学习后,我们通过一个基础的实战案例,实现两个无亲缘进程间的共享内存通信:服务端创建共享内存,客户端向共享内存写入字母 A-Z,服务端实时读取并打印。
案例包含公共头文件 comm.h、服务端程序 server.c、客户端程序 client.c、Makefile,代码可直接编译运行,覆盖共享内存的全生命周期操作。
4.1 公共头文件:comm.h
封装 ftok、shmget、shmdt、shmctl 的公共函数,实现代码复用,避免重复编写:
#ifndef _COMM_H_
#define _COMM_H_
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <perror.h>
#include <stdlib.h>
// 定义常量
#define PATHNAME "./" // ftok的文件路径,当前目录
#define PROJ_ID 0x6666 // ftok的项目ID
#define SHM_SIZE 4096 // 共享内存大小,4096字节(1页)
// 错误处理宏
#define ERR_EXIT(m) \\
do\\
{\\
perror(m);\\
exit(EXIT_FAILURE);\\
}while(0)
// 创建共享内存(服务端使用)
int createShm(int size);
// 获取共享内存(客户端使用)
int getShm(int size);
// 销毁共享内存
int destroyShm(int shmid);
#endif
4.2 公共实现文件:comm.c
实现 comm.h 中声明的函数,封装共享内存的创建、获取、销毁逻辑:
#include "comm.h"
// 内部公共函数,封装shmget的核心逻辑
static int commShm(int size, int flags)
{
// 1. 生成key值
key_t key = ftok(PATHNAME, PROJ_ID);
if (key == -1)
ERR_EXIT("ftok error");
// 2. 创建/获取共享内存
int shmid = shmget(key, size, flags);
if (shmid == -1)
ERR_EXIT("shmget error");
return shmid;
}
// 创建全新的共享内存
int createShm(int size)
{
return commShm(size, IPC_CREAT | IPC_EXCL | 0666);
}
// 获取已存在的共享内存
int getShm(int size)
{
return commShm(size, IPC_CREAT);
}
// 销毁共享内存
int destroyShm(int shmid)
{
if (shmctl(shmid, IPC_RMID, NULL) == -1)
{
perror("shmctl error");
return -1;
}
return 0;
}
4.3 服务端程序:server.c
负责创建共享内存、映射内存、循环读取共享内存数据并打印、最后销毁共享内存:
#include "comm.h"
#include <unistd.h>
int main()
{
// 1. 创建共享内存
int shmid = createShm(SHM_SIZE);
printf("服务端:创建共享内存成功,shmid = %d\\n", shmid);
// 2. 映射共享内存到地址空间
char *shmaddr = (char*)shmat(shmid, NULL, 0);
if (shmaddr == (void*)-1)
ERR_EXIT("shmat error");
printf("服务端:共享内存映射成功,地址 = %p\\n", shmaddr);
// 3. 循环读取共享内存数据
int i = 0;
while (i++ < 26) // 读取26次,对应客户端写入的A-Z
{
printf("服务端读取到:%s\\n", shmaddr);
sleep(1); // 每秒读取一次,模拟业务逻辑
}
// 4. 解除共享内存映射
if (shmdt(shmaddr) == -1)
ERR_EXIT("shmdt error");
printf("服务端:解除共享内存映射成功\\n");
// 5. 销毁共享内存
if (destroyShm(shmid) == 0)
printf("服务端:销毁共享内存成功\\n");
return 0;
}
4.4 客户端程序:client.c
负责获取共享内存、映射内存、向共享内存写入 A-Z、最后解除映射:
#include "comm.h"
#include <unistd.h>
#include <string.h>
int main()
{
// 1. 获取已存在的共享内存
int shmid = getShm(SHM_SIZE);
printf("客户端:获取共享内存成功,shmid = %d\\n", shmid);
// 2. 映射共享内存到地址空间
char *shmaddr = (char*)shmat(shmid, NULL, 0);
if (shmaddr == (void*)-1)
ERR_EXIT("shmat error");
printf("客户端:共享内存映射成功,地址 = %p\\n", shmaddr);
// 3. 向共享内存写入A-Z
int i = 0;
while (i < 26)
{
shmaddr[i] = 'A' + i; // 依次写入A、B、C…Z
i++;
shmaddr[i] = '\\0'; // 字符串结束符,保证打印正常
sleep(1); // 每秒写入一个字符,模拟业务逻辑
}
// 4. 解除共享内存映射
if (shmdt(shmaddr) == -1)
ERR_EXIT("shmdt error");
printf("客户端:解除共享内存映射成功\\n");
return 0;
}
4.5 编译脚本:Makefile
一键编译服务端和客户端,简化编译操作:
.PHONY: all
all: server client
# 编译服务端
server: server.c comm.c
gcc -o $@ $^
# 编译客户端
client: client.c comm.c
gcc -o $@ $^
# 清理可执行文件
.PHONY: clean
clean:
rm -f server client
4.6 编译与运行
4.6.1 编译
在终端进入代码目录,执行make命令,生成server和client可执行文件:
make
4.6.2 运行
服务端:创建共享内存成功,shmid = 12345
服务端:共享内存映射成功,地址 = 0x7f8900000000
服务端读取到:
服务端读取到:A
服务端读取到:AB
…
客户端:获取共享内存成功,shmid = 12345
客户端:共享内存映射成功,地址 = 0x7f8900000000
客户端:解除共享内存映射成功

4.7 案例核心亮点与注意点
五、共享内存的核心问题:同步与互斥的实现
本案例中使用sleep实现同步是极不推荐的,实际开发中,多个进程同时访问共享内存时,会出现数据竞争问题,导致数据错乱,这是共享内存的核心痛点 ——内核不提供原生的同步与互斥机制。
5.1 共享内存的并发问题:数据竞争
当多个进程同时对共享内存进行写操作,或一读一写时,会出现数据竞争:
- 进程 A 正在向共享内存写入 “123456”,写入到一半时,进程 B 开始读取,结果读取到 “123abc” 的乱码;
- 进程 A 和进程 B 同时向共享内存的同一地址写入数据,最终的结果可能是 A 或 B 的部分数据,导致数据覆盖。
出现这个问题的根本原因是:共享内存的读写操作是 “非原子的”,内核不对进程的访问做任何限制。
5.2 解决思路:手动添加同步互斥机制
要解决共享内存的并发问题,需要手动为共享内存添加 “访问锁”,保证同一时刻只有一个进程访问共享内存(互斥),或让进程按指定顺序访问(同步)。
Linux 中常用的同步互斥机制有:
其中,管道实现同步是最简单、最易上手的方式,适合入门学习;System V 信号量是最专业的方式,适合生产环境。本文以命名管道为例,实现共享内存的访问控制,让进程的读写操作具有顺序性。
5.3 进阶实战:管道实现共享内存的同步控制
核心思路:通过命名管道的阻塞特性,实现 “客户端写入完成后,通知服务端读取” 的同步逻辑,即 “生产者 – 消费者” 模型:
由于代码篇幅较长,在这列出核心实现要点如下:
该实现完美解决了共享内存的同步问题,保证了读写操作的顺序性,避免了数据竞争。
六、共享内存的内核管理与命令行操作
System V 共享内存是内核独立管理的 IPC 资源,即使进程退出,共享内存仍会存在于内核中,因此需要掌握命令行查看和删除共享内存的方法,用于调试和解决内存泄漏问题。
6.1 查看共享内存:ipcs -m
ipcs是 Linux 中查看 System V IPC 资源的命令,-m参数表示仅查看共享内存(-q查看消息队列,-s查看信号量):
ipcs -m
输出结果示例:
—— Shared Memory Segments ——–
key shmid owner perms bytes nattch status
0x66662a25 12345 root 666 4096 2 dest
字段说明:
- key:共享内存的键值,由 ftok 生成;
- shmid:共享内存标识码,shmget 返回的值;
- owner:共享内存的创建者;
- perms:共享内存的访问权限;
- bytes:共享内存的大小(字节);
- nattch:当前附加到该共享内存的进程数;
- status:状态,dest表示该共享内存已被标记为待删除(调用了 IPC_RMID),最后一个进程解除映射后会被销毁。
6.2 删除共享内存:ipcrm -m shmid
ipcrm是 Linux 中删除 System V IPC 资源的命令,-m参数后接 shmid,表示删除指定的共享内存:
# 删除shmid为12345的共享内存
ipcrm -m 12345
适用场景:程序异常退出,未显式调用 shmctl 删除共享内存,导致共享内存残留于内核中,通过该命令手动删除,避免内存泄漏。
6.3 内核管理 IPC 资源的核心结构
内核通过ipc_ids结构体管理所有的 System V IPC 资源(共享内存、消息队列、信号量),该结构体维护了 IPC 资源的数组、使用计数、最大 ID 等信息;每个 IPC 资源都有一个kern_ipc_perm结构体,用于存储通用的权限和标识信息。
共享内存、消息队列、信号量的管理结构都挂载在 ipc_ids 下,内核通过这种方式实现对 IPC 资源的统一管理,这也是ipcs/ipcrm命令能查看和删除所有 System V IPC 资源的原因。
七、System V 共享内存的核心特点、使用场景与局限性
结合前面的原理和实战,我们总结 System V 共享内存的核心特点、典型使用场景和局限性,帮助你在实际开发中快速判断是否适合使用共享内存。
7.1 核心特点
7.2 典型使用场景
共享内存适用于大数据量、高频率、对性能要求极高的进程间通信场景,典型应用包括:
7.3 局限性
7.4 与其他 IPC 方式的性能对比
为了更直观地体现共享内存的性能优势,我们将其与管道、消息队列做核心性能对比(单位:MB/s,大数据量传输场景):
| 匿名 / 命名管道 | 2 次 | 高 | 原生支持 | 100~200 | 小数据量、低频率通信 |
| 消息队列 | 2 次 | 高 | 原生支持 | 80~150 | 有消息边界的小数据通信 |
| System V 共享内存 | 0~1 次 | 低 | 手动实现 | 1000+ | 大数据量、高性能通信 |
从对比可以看出,共享内存的传输速率是管道和消息队列的 5~10 倍,在大数据量场景下优势极其明显。
八、共享内存开发的避坑指南
System V 共享内存的使用门槛较高,新手开发时容易踩坑,导致程序崩溃、内存泄漏、数据错乱等问题。本节梳理开发中最常见的坑,并给出对应的解决方案,帮助你写出健壮的代码。
8.1 坑 1:未显式删除共享内存,导致内核内存泄漏
问题:程序正常退出或异常退出时,未调用shmctl(IPC_RMID)删除共享内存,导致共享内存一直存在于内核中,占用内存资源;
解决方案:
8.2 坑 2:未处理 ftok 函数的失败场景
问题:ftok 函数的文件路径不存在,或 proj_id 为 0,导致 ftok 失败返回 – 1,后续 shmget 也会失败;
解决方案:
8.3 坑 3:共享内存大小未页对齐,导致内存浪费
问题:设置的共享内存大小不是 4096 字节(页大小)的整数倍,内核会自动向上对齐到页大小,导致多余的内存被浪费;
解决方案:
8.4 坑 4:多个进程同时读写,未实现同步机制,导致数据错乱
问题:多个进程同时对共享内存进行读写,未添加同步互斥机制,出现数据竞争、数据覆盖、乱码等问题;
解决方案:
8.5 坑 5:解除映射后,继续使用原指针访问共享内存,导致段错误
问题:调用shmdt解除映射后,进程仍通过原 shmat 返回的指针访问共享内存,导致段错误(SIGSEGV);
解决方案:
8.6 坑 6:使用 IPC_CREAT|IPC_EXCL 时,未处理共享内存已存在的场景
问题:服务端使用IPC_CREAT | IPC_EXCL创建共享内存时,若共享内存已存在,shmget 会失败,若未处理该错误,程序会直接退出;
解决方案:
8.7 坑 7:权限设置不当,导致其他进程无法访问共享内存
问题:创建共享内存时,权限位设置过小(如 0600),导致其他用户的进程无法获取或访问共享内存;
解决方案:
总结
共享内存的学习,让我们理解了性能与复杂度的权衡—— 极致的性能背后,是更高的使用门槛和更复杂的代码实现。但这并不意味着共享内存难以掌握,只要理解其核心原理,掌握同步机制的实现,就能在实际开发中发挥其性能优势。
同时,共享内存也是 Linux 虚拟内存管理机制的典型应用,学习共享内存的过程,也是对 Linux 虚拟内存、物理内存、地址映射等核心概念的深化理解。
希望本文能帮助你彻底吃透 System V 共享内存,在高性能进程间通信的场景中,灵活运用这一强大的 IPC 工具,打造出高性能的 Linux 应用程序!





