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

一、共享内存的诞生:为何它是最快的 IPC?
在学习 System V 共享内存(后文简称 "共享内存")之前,我们先思考一个问题:为什么管道、消息队列的通信效率远不如共享内存? 答案藏在数据拷贝的次数 和内核参与度中。
1.1 传统 IPC 的性能瓶颈:两次拷贝 + 内核中转
无论是匿名管道、命名管道,还是消息队列,进程间的通信都离不开内核的中转 ,数据至少会经历两次内存拷贝:
- 进程→内核 :发送进程将数据从自己的用户态地址空间,通过**write()**等系统调用拷贝到内核维护的缓冲区(管道 / 消息队列缓冲区);
- 内核→进程 :接收进程通过**read()**等系统调用,将数据从内核缓冲区拷贝到自己的用户态地址空间。
这个过程中,每次数据传递都需要陷入内核态、执行系统调用、完成两次拷贝,而系统调用和内存拷贝本身就是性能开销的大头,当需要传输大量数据时,这种开销会被无限放大。
同时,内核会对管道 / 消息队列的读写做同步互斥控制,进一步增加了通信的耗时。
1.2 共享内存的性能突破:零内核中转 + 一次拷贝(甚至零拷贝)
共享内存的设计思想彻底颠覆了传统 IPC 的通信模式:内核在物理内存中开辟一块连续的内存区域,将这块内存映射到多个通信进程的用户态地址空间,让进程直接访问这块内存,就像访问自己的堆 / 栈内存一样。
整个通信过程中,内核只负责内存的创建和映射,不参与任何数据传递,数据传递完全在进程的用户态完成,数据拷贝次数被极致压缩:
- 理想情况(零拷贝):一个进程将数据写入共享内存,另一个进程直接读取,无需任何数据拷贝;
- 实际场景(一次拷贝):进程从磁盘读取数据后,直接写入共享内存,仅需一次磁盘到内存的拷贝。
没有系统调用的开销、没有内核中转的损耗、极少的内存拷贝,这就是共享内存成为最快 IPC 的核心原因。在大数据量传输场景(如音视频数据、大型文件、数据库缓存)中,共享内存的性能优势会体现得淋漓尽致。
1.3 System V 共享内存的核心特征
作为 System V IPC 家族的核心成员(另外两个是消息队列、信号量),共享内存还具备以下核心特征,这也是它与 POSIX 共享内存、内存映射的重要区别:
- 生命周期随内核 :共享内存创建后,会一直存在于内核中,直到被显式删除(
shmctl的IPC_RMID)或系统重启,进程退出不会自动释放; - 通过 Key 标识唯一性 :内核通过
key_t类型的键值标识不同的共享内存段,进程通过相同的 Key 找到同一块共享内存; - 独立的 IPC 资源 :共享内存是内核独立管理的 IPC 资源,可通过
ipcs/ipcrm命令查看和删除; - 无原生同步互斥:内核不提供任何同步互斥机制,多个进程同时访问共享内存会导致数据竞争,需要开发者手动实现(如信号量、管道);
- 页对齐分配 :共享内存的大小会被内核自动向上对齐到内存页大小(默认 4096 字节),分配时建议指定页对齐的大小,避免内存浪费。
二、共享内存的核心原理:物理内存映射与地址空间关联
要理解共享内存,必须先搞清楚内核如何创建共享内存 、进程如何访问共享内存 ,核心是理解物理内存开辟 和进程地址空间映射两个关键步骤。
2.1 共享内存的底层实现逻辑
共享内存的实现基于 Linux 的虚拟内存管理机制,核心分为三步:
- 内核开辟物理内存 :进程通过**
shmget函数向内核申请一块连续的物理内存区域,内核为其分配唯一的共享内存标识码(shmid)**,并维护对应的管理数据结构;- 内存映射到进程地址空间 :进程通过**
shmat**函数,将内核开辟的物理内存映射到自己的用户态虚拟地址空间(通常在堆和栈之间的共享内存区域);- 进程直接访问内存 :映射完成后,进程可以通过**
shmat**返回的指针,直接读写这块内存,就像操作普通的用户态内存一样,其他映射了该内存的进程能实时看到数据变化。
简单来说,共享内存就是多个进程的虚拟地址空间,映射到同一块物理内存,这是进程间能直接共享数据的根本原因。
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 内核):
cpp
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 函数原型
cpp
#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 值,原型如下:
cpp
// 功能:生成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 并创建共享内存
cpp
#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 函数原型
cpp
// 功能:将共享内存段映射到当前进程的用户态地址空间
// 参数:
// shmid:由shmget返回的共享内存标识码
// shmaddr:指定映射的虚拟地址,通常设为NULL,由内核自动分配合适的地址
// shmflg:映射标志位,核心取值为0或SHM_RDONLY
// 返回值:成功返回映射后的虚拟地址指针,失败返回(void*)-1,并设置errno
void *shmat(int shmid, const void *shmaddr, int shmflg);
3.2.2 关键参数详解
- shmaddr :几乎所有场景都设为NULL,让内核自动选择合适的虚拟地址进行映射,避免手动指定地址导致的地址冲突;
- shmflg :
- 0 :默认值,表示共享内存可读可写;
- SHM_RDONLY :表示共享内存只读,进程只能读取数据,无法写入,适用于只读场景,提高安全性。
3.2.3 核心注意点
- 映射成功后,返回的指针可以像普通的 malloc 指针一样使用,支持任意的内存操作(如赋值、拷贝、指针偏移);
- 进程可以多次调用 shmat映射同一块共享内存,会得到不同的虚拟地址指针,均指向同一块物理内存;
- 映射后,内核的 shmid_ds结构体中,
shm_nattch(附加进程数)会加 1。
3.3 第三步:解除共享内存的映射 ------shmdt ()
当进程不再需要访问共享内存时,需要通过shmdt函数解除虚拟地址空间与共享内存物理地址的映射关系,释放进程的虚拟地址资源。
3.3.1 函数原型
cpp
// 功能:解除共享内存与当前进程地址空间的映射
// 参数:
// 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 函数原型
cpp
// 功能:控制共享内存段的属性,核心用于删除共享内存
// 参数:
// 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 的公共函数,实现代码复用,避免重复编写:
cpp
#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 中声明的函数,封装共享内存的创建、获取、销毁逻辑:
cpp
#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
负责创建共享内存 、映射内存 、循环读取共享内存数据并打印 、最后销毁共享内存:
cpp
#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 、最后解除映射:
cpp
#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
一键编译服务端和客户端,简化编译操作:
bash
.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**可执行文件:
bash
make
4.6.2 运行
-
先启动服务端 :打开一个终端,执行
./server,服务端创建并映射共享内存,开始循环读取:bash./server 服务端:创建共享内存成功,shmid = 12345 服务端:共享内存映射成功,地址 = 0x7f8900000000 服务端读取到: 服务端读取到:A 服务端读取到:AB ... -
再启动客户端 :打开另一个终端,执行
./client,客户端获取并映射共享内存,开始写入 A-Z:bash./client 客户端:获取共享内存成功,shmid = 12345 客户端:共享内存映射成功,地址 = 0x7f8900000000 客户端:解除共享内存映射成功 -
运行结果:服务端会实时打印客户端写入的内容,从 A 逐步到 Z,写入完成后,服务端销毁共享内存,程序退出。

4.7 案例核心亮点与注意点
- 无亲缘进程通信:服务端和客户端是完全独立的进程,通过相同的 key 值找到同一块共享内存,实现数据共享;
- 直接内存操作:客户端通过指针直接向共享内存写入字符,服务端直接读取,无任何内核中转,效率极高;
- 页对齐分配:共享内存大小设为 4096 字节(页大小),避免内核向上对齐导致的内存浪费;
- 显式销毁:服务端作为发起者,负责销毁共享内存,避免内存泄漏;
- 同步问题 :本案例通过
sleep(1)实现简单的 "同步",让服务端和客户端的读写节奏一致,这是临时的解决方案,实际开发中需要使用专业的同步机制(如信号量)。
五、共享内存的核心问题:同步与互斥的实现
本案例中使用**sleep实现同步是极不推荐** 的,实际开发中,多个进程同时访问共享内存时,会出现数据竞争 问题,导致数据错乱,这是共享内存的核心痛点 ------内核不提供原生的同步与互斥机制。
5.1 共享内存的并发问题:数据竞争
当多个进程同时对共享内存进行写操作 ,或一读一写时,会出现数据竞争:
- 进程 A 正在向共享内存写入 "123456",写入到一半时,进程 B 开始读取,结果读取到 "123abc" 的乱码;
- 进程 A 和进程 B 同时向共享内存的同一地址写入数据,最终的结果可能是 A 或 B 的部分数据,导致数据覆盖。
出现这个问题的根本原因是:共享内存的读写操作是 "非原子的",内核不对进程的访问做任何限制。
5.2 解决思路:手动添加同步互斥机制
要解决共享内存的并发问题,需要手动为共享内存添加 "访问锁",保证同一时刻只有一个进程访问共享内存(互斥),或让进程按指定顺序访问(同步)。
Linux 中常用的同步互斥机制有:
- System V 信号量:与 System V 共享内存同属一个家族,是最搭配的同步方式,专门用于 System V IPC 的同步;
- 管道(匿名 / 命名):通过管道的阻塞特性,实现简单的同步(如 "生产者 - 消费者" 模型);
- POSIX 信号量:轻量级的同步机制,使用简单,适用于所有 IPC 方式;
- 互斥锁 / 条件变量:适用于线程间同步,进程间使用需要结合共享内存。
其中,管道实现同步 是最简单、最易上手的方式,适合入门学习;System V 信号量 是最专业的方式,适合生产环境。本文以命名管道为例,实现共享内存的访问控制,让进程的读写操作具有顺序性。
5.3 进阶实战:管道实现共享内存的同步控制
核心思路:通过命名管道的阻塞特性,实现 "客户端写入完成后,通知服务端读取" 的同步逻辑,即 "生产者 - 消费者" 模型:
- 服务端创建命名管道,以读方式打开,阻塞等待客户端的 "写入完成" 信号;
- 客户端以写方式打开命名管道,向共享内存写入数据后,向管道写入一个字节的信号;
- 服务端接收到管道的信号后,才开始读取共享内存的数据;
- 客户端写入 "quit" 后,服务端退出,同时删除共享内存和命名管道。
由于代码篇幅较长,在这列出核心实现要点如下:
- 新增命名管道相关封装:在 Comm.hpp 中封装命名管道的创建、打开、等待、通知函数;
- 服务端逻辑修改:映射共享内存后,打开命名管道,阻塞等待客户端的信号,接收到信号后再读取共享内存;
- 客户端逻辑修改:向共享内存写入数据后,向命名管道写入信号,通知服务端读取;
- 退出逻辑:客户端写入 "quit" 后,服务端检测到该字符串,销毁共享内存和命名管道,程序退出。
该实现完美解决了共享内存的同步问题,保证了读写操作的顺序性,避免了数据竞争。
六、共享内存的内核管理与命令行操作
System V 共享内存是内核独立管理的 IPC 资源,即使进程退出,共享内存仍会存在于内核中,因此需要掌握命令行查看和删除共享内存的方法,用于调试和解决内存泄漏问题。
6.1 查看共享内存:ipcs -m
ipcs是 Linux 中查看 System V IPC 资源的命令,**-m**参数表示仅查看共享内存(-q查看消息队列,-s查看信号量):
bash
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,表示删除指定的共享内存:
bash
# 删除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 核心特点
- 极致性能:最快的 IPC 方式,进程直接访问物理内存,无内核中转,数据拷贝次数最少;
- 生命周期随内核:创建后持续存在于内核,直到显式删除或系统重启,进程退出不释放;
- 无原生同步互斥:内核不做任何访问限制,需要开发者手动实现同步互斥,否则会出现数据竞争;
- 通过 Key 标识:由 ftok 生成唯一的 Key 值,不同进程通过 Key 找到同一块共享内存;
- 页对齐分配:大小自动向上对齐到内存页大小,建议手动指定页对齐大小,避免内存浪费;
- 独立的 IPC 资源:由内核独立管理,可通过 ipcs/ipcrm 命令查看和删除,易调试;
- 支持无亲缘进程通信:突破管道的亲缘限制,任意进程只要有相同的 Key 和访问权限,即可访问。
7.2 典型使用场景
共享内存适用于大数据量、高频率、对性能要求极高的进程间通信场景,典型应用包括:
- 音视频数据传输:播放器进程与解码进程之间传输音视频帧,对实时性和性能要求高;
- 大型文件共享:多个进程共享同一个大型文件的内存缓存,避免多次读取磁盘;
- 数据库缓存:数据库的多个子进程共享缓存数据,提高数据访问效率;
- 高性能计算:分布式计算中,多个计算进程共享中间结果,减少数据传输开销;
- 游戏开发:游戏的渲染进程、逻辑进程、资源加载进程之间共享数据,保证游戏的流畅性。
7.3 局限性
- 无原生同步互斥:使用门槛高,需要手动实现同步机制,若实现不当,容易出现数据竞争、死锁等问题;
- 生命周期随内核:易造成内存泄漏,程序异常退出时,若未显式删除共享内存,会一直占用内核内存;
- 内存大小限制:共享内存的大小受限于物理内存和内核参数,无法传输无限大的数据;
- 不支持跨主机通信:仅适用于同一台 Linux 主机上的进程间通信,无法实现跨主机的网络通信;
- 数据无保护:共享内存是裸内存,没有数据格式、校验等机制,数据的完整性需要开发者自己保证。
7.4 与其他 IPC 方式的性能对比
为了更直观地体现共享内存的性能优势,我们将其与管道、消息队列做核心性能对比(单位:MB/s,大数据量传输场景):
| IPC 方式 | 数据拷贝次数 | 内核参与度 | 同步机制 | 传输速率 | 适用场景 |
|---|---|---|---|---|---|
| 匿名 / 命名管道 | 2 次 | 高 | 原生支持 | 100~200 | 小数据量、低频率通信 |
| 消息队列 | 2 次 | 高 | 原生支持 | 80~150 | 有消息边界的小数据通信 |
| System V 共享内存 | 0~1 次 | 低 | 手动实现 | 1000+ | 大数据量、高性能通信 |
从对比可以看出,共享内存的传输速率是管道和消息队列的 5~10 倍,在大数据量场景下优势极其明显。
八、共享内存开发的避坑指南
System V 共享内存的使用门槛较高,新手开发时容易踩坑,导致程序崩溃、内存泄漏、数据错乱等问题。本节梳理开发中最常见的坑,并给出对应的解决方案,帮助你写出健壮的代码。
8.1 坑 1:未显式删除共享内存,导致内核内存泄漏
问题 :程序正常退出或异常退出时,未调用**shmctl(IPC_RMID)**删除共享内存,导致共享内存一直存在于内核中,占用内存资源;
解决方案:
- 由通信的发起进程(服务端)负责删除共享内存,在程序退出前(正常 / 异常)都要执行;
- 注册信号处理函数(如 SIGINT、SIGTERM),在进程接收到退出信号时,先删除共享内存再退出;
- 开发阶段通过ipcs -m查看共享内存,通过ipcrm -m手动删除残留的共享内存。
8.2 坑 2:未处理 ftok 函数的失败场景
问题 :ftok函数的文件路径不存在,或 proj_id为 0,导致 ftok失败返回 - 1,后续 shmget也会失败;
解决方案:
- 确保 ftok 的文件路径是已存在的有效路径 (如当前目录
./),且文件不会被随意删除重建;- proj_id 设置为非 0 的整型值(如 0x66、0x6666);
- 必须判断 ftok 的返回值,失败时及时报错并退出。
8.3 坑 3:共享内存大小未页对齐,导致内存浪费
问题:设置的共享内存大小不是 4096 字节(页大小)的整数倍,内核会自动向上对齐到页大小,导致多余的内存被浪费;
解决方案:
- 共享内存的大小始终设置为页大小的整数倍(如 4096、8192、16384);
- 通过**sysconf(_SC_PAGESIZE)**获取系统的页大小,实现跨平台的页对齐。
8.4 坑 4:多个进程同时读写,未实现同步机制,导致数据错乱
问题:多个进程同时对共享内存进行读写,未添加同步互斥机制,出现数据竞争、数据覆盖、乱码等问题;
解决方案:
- 简单场景使用管道实现同步,利用管道的阻塞特性控制读写顺序;
- 生产环境使用System V 信号量 或POSIX 信号量,实现专业的同步互斥;
- 遵循 "单写多读" 或 "单读单写" 的设计原则,减少并发冲突。
8.5 坑 5:解除映射后,继续使用原指针访问共享内存,导致段错误
问题 :调用**shmdt**解除映射后,进程仍通过原 shmat返回的指针访问共享内存,导致段错误(SIGSEGV);
解决方案:
- 解除映射后,立即将原指针置为 NULL,避免后续误操作;
- 封装共享内存的操作函数,在解除映射后,禁止对原指针的任何访问。
8.6 坑 6:使用 IPC_CREAT|IPC_EXCL 时,未处理共享内存已存在的场景
问题 :服务端使用**IPC_CREAT | IPC_EXCL**创建共享内存时,若共享内存已存在,shmget会失败,若未处理该错误,程序会直接退出;
解决方案:
- 检测 shmget的错误码,若为 EEXIST(文件已存在),先通过**
ipcrm**删除残留的共享内存,再重新创建;- 或在程序启动时,先检查是否存在指定 key 的共享内存,若存在则先删除。
8.7 坑 7:权限设置不当,导致其他进程无法访问共享内存
问题:创建共享内存时,权限位设置过小(如 0600),导致其他用户的进程无法获取或访问共享内存;
解决方案:
- 根据实际场景设置合理的权限,如开发阶段设置 0666,生产阶段设置 0644;
- 确保通信进程的所有者相同,或属于同一用户组。
总结
共享内存的学习,让我们理解了性能与复杂度的权衡------ 极致的性能背后,是更高的使用门槛和更复杂的代码实现。但这并不意味着共享内存难以掌握,只要理解其核心原理,掌握同步机制的实现,就能在实际开发中发挥其性能优势。
同时,共享内存也是 Linux 虚拟内存管理机制的典型应用,学习共享内存的过程,也是对 Linux 虚拟内存、物理内存、地址映射等核心概念的深化理解。
希望本文能帮助你彻底吃透 System V 共享内存,在高性能进程间通信的场景中,灵活运用这一强大的 IPC 工具,打造出高性能的 Linux 应用程序!
