在 Linux 进程间通信的大家族中,System V 共享内存是性能天花板般的存在 —— 它是最快的 IPC 实现方式,没有之一。不同于管道(匿名 / 命名)的内核缓冲区中转、消息队列的内核封装,共享内存让多个进程直接访问同一块物理内存,进程间数据传递彻底摆脱内核系统调用的开销,实现了真正的 “内存级” 数据交互。 但天下没有免费的午餐,极致的性能背后是缺乏原生的同步与互斥机制,这也让共享内存的使用门槛远高于管道和消息队列。本文将从 System V 共享内存的核心原理出发,一步步拆解其设计思想、数据结构、核心函数、实战开发,还会讲解如何解决其同步问题,让你彻底吃透这一高性能 IPC 方式,从原理到实战拿捏到位。下面就让我们正式开始吧!

在学习 System V 共享内存(后文简称 “共享内存”)之前,我们先思考一个问题:为什么管道、消息队列的通信效率远不如共享内存? 答案藏在数据拷贝的次数和内核参与度中。
无论是匿名管道、命名管道,还是消息队列,进程间的通信都离不开内核的中转,数据至少会经历两次内存拷贝:
write()等系统调用拷贝到内核维护的缓冲区(管道 / 消息队列缓冲区);read()等系统调用,将数据从内核缓冲区拷贝到自己的用户态地址空间。这个过程中,每次数据传递都需要陷入内核态、执行系统调用、完成两次拷贝,而系统调用和内存拷贝本身就是性能开销的大头,当需要传输大量数据时,这种开销会被无限放大。
同时,内核会对管道 / 消息队列的读写做同步互斥控制,进一步增加了通信的耗时。
共享内存的设计思想彻底颠覆了传统 IPC 的通信模式:内核在物理内存中开辟一块连续的内存区域,将这块内存映射到多个通信进程的用户态地址空间,让进程直接访问这块内存,就像访问自己的堆 / 栈内存一样。
整个通信过程中,内核只负责内存的创建和映射,不参与任何数据传递,数据传递完全在进程的用户态完成,数据拷贝次数被极致压缩:
没有系统调用的开销、没有内核中转的损耗、极少的内存拷贝,这就是共享内存成为最快 IPC 的核心原因。在大数据量传输场景(如音视频数据、大型文件、数据库缓存)中,共享内存的性能优势会体现得淋漓尽致。
作为 System V IPC 家族的核心成员(另外两个是消息队列、信号量),共享内存还具备以下核心特征,这也是它与 POSIX 共享内存、内存映射的重要区别:
shmctl的IPC_RMID)或系统重启,进程退出不会自动释放;key_t类型的键值标识不同的共享内存段,进程通过相同的 Key 找到同一块共享内存;ipcs/ipcrm命令查看和删除;要理解共享内存,必须先搞清楚内核如何创建共享内存、进程如何访问共享内存,核心是理解物理内存开辟和进程地址空间映射两个关键步骤。
共享内存的实现基于 Linux 的虚拟内存管理机制,核心分为三步:
shmget函数向内核申请一块连续的物理内存区域,内核为其分配唯一的共享内存标识码(shmid),并维护对应的管理数据结构;shmat函数,将内核开辟的物理内存映射到自己的用户态虚拟地址空间(通常在堆和栈之间的共享内存区域);shmat返回的指针,直接读写这块内存,就像操作普通的用户态内存一样,其他映射了该内存的进程能实时看到数据变化。简单来说,共享内存就是多个进程的虚拟地址空间,映射到同一块物理内存,这是进程间能直接共享数据的根本原因。
在 Linux 进程的 32 位虚拟地址空间中,共享内存、内存映射、共享库都位于0x40000000~0xC0000000的区域(堆和栈之间),这块区域专门用于映射内核管理的共享资源。
addr_A指向一块虚拟内存,该虚拟内存映射到物理内存地址0x12340000;addr_B指向另一块虚拟内存,该虚拟内存同样映射到物理内存地址0x12340000;addr_A写入数据,进程 B 通过addr_B能立即读取到,实现数据共享。这种映射关系由内核的页表维护,进程对虚拟地址的访问会被硬件 MMU(内存管理单元)转换为对物理地址的访问,整个过程对进程透明。

内核为每一块共享内存维护一个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>。
shmget函数的作用是向内核申请创建一块新的共享内存,或获取已存在的共享内存,返回一个唯一的共享内存标识码shmid,后续操作均通过该shmid进行。
#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); 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 值也会不同。
shmflg是标志位的组合,核心取值有三个,可与权限位(如 0666)按位或:
IPC_CREAT | IPC_EXCL),表示创建全新的共享内存;若 key 值对应的共享内存已存在,则直接失败(返回 - 1),避免覆盖已有资源;常用组合:
IPC_CREAT | 0666:创建或获取共享内存,适用于接收进程;IPC_CREAT | IPC_EXCL | 0666:创建全新的共享内存,适用于通信的发起进程(如服务端)。#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;
} shmget仅创建 / 获取了共享内存的内核资源,进程还无法访问,需要通过shmat函数将共享内存的物理地址映射到进程的虚拟地址空间,映射成功后返回一个指针,进程通过该指针访问共享内存。
// 功能:将共享内存段映射到当前进程的用户态地址空间
// 参数:
// shmid:由shmget返回的共享内存标识码
// shmaddr:指定映射的虚拟地址,通常设为NULL,由内核自动分配合适的地址
// shmflg:映射标志位,核心取值为0或SHM_RDONLY
// 返回值:成功返回映射后的虚拟地址指针,失败返回(void*)-1,并设置errno
void *shmat(int shmid, const void *shmaddr, int shmflg);shm_nattch(附加进程数)会加 1。 当进程不再需要访问共享内存时,需要通过shmdt函数解除虚拟地址空间与共享内存物理地址的映射关系,释放进程的虚拟地址资源。
// 功能:解除共享内存与当前进程地址空间的映射
// 参数:
// shmaddr:由shmat返回的映射地址指针
// 返回值:成功返回0,失败返回-1,并设置errno
int shmdt(const void *shmaddr);shm_nattch(附加进程数)会减 1; shmctl是共享内存的控制函数,支持获取共享内存的属性、修改属性、删除共享内存等操作,是管理共享内存的核心函数,其中删除共享内存(IPC_RMID) 是最常用的功能。
// 功能:控制共享内存段的属性,核心用于删除共享内存
// 参数:
// 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);命令 | 功能说明 |
|---|---|
IPC_STAT | 将内核中该共享内存的 shmid_ds 结构体数据,拷贝到用户态的 buf 中,用于获取共享内存属性 |
IPC_SET | 在进程有足够权限的前提下,将用户态 buf 中的 shmid_ds 数据,设置到内核的共享内存管理结构中,用于修改共享内存属性(如权限) |
IPC_RMID | 删除共享内存段,此时 buf 设为 NULL 即可;即使有进程仍映射了该共享内存,内核也会立即标记其为待删除,当最后一个进程解除映射后,释放物理内存 |
shmctl(shmid, IPC_RMID, NULL)后,共享内存的 key 值会失效,新进程无法通过该 key 获取共享内存,但已有映射的进程仍可继续访问,直到解除映射;结合四个核心函数,共享内存的完整使用流程可总结为:
发起进程(服务端):
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,代码可直接编译运行,覆盖共享内存的全生命周期操作。
封装 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实现 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;
}负责创建共享内存、映射内存、循环读取共享内存数据并打印、最后销毁共享内存:
#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;
}负责获取共享内存、映射内存、向共享内存写入 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;
}一键编译服务端和客户端,简化编译操作:
.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 在终端进入代码目录,执行make命令,生成server和client可执行文件:
make先启动服务端:打开一个终端,执行./server,服务端创建并映射共享内存,开始循环读取:
./server
服务端:创建共享内存成功,shmid = 12345
服务端:共享内存映射成功,地址 = 0x7f8900000000
服务端读取到:
服务端读取到:A
服务端读取到:AB
...再启动客户端:打开另一个终端,执行./client,客户端获取并映射共享内存,开始写入 A-Z:
./client
客户端:获取共享内存成功,shmid = 12345
客户端:共享内存映射成功,地址 = 0x7f8900000000
客户端:解除共享内存映射成功运行结果:服务端会实时打印客户端写入的内容,从 A 逐步到 Z,写入完成后,服务端销毁共享内存,程序退出。

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