system v 共享内存(shared memory)
system v 共享内存是内核在物理内存中划出的一块连续内存区域,允许多个进程将该区域映射到自身的虚拟地址空间中。进程对这块内存的读写操作完全等同于本地内存(无需系统调用 / 数据拷贝),是所有 ipc 机制中速度最快的(“零拷贝” 特性)。其核心价值是解决 “大量数据在进程间传输” 的性能问题,但本身无内置同步机制,需配合信号量等工具实现互斥 / 同步。

核心功能
- 最快的 ipc 机制:内核开辟一块连续的内存区域,映射到多个进程的虚拟地址空间(共享区),进程直接读写该内存(无需内核中转);
- 无数据拷贝(其他 ipc 需多次拷贝:用户态→内核态→用户态),性能极致;
- 无同步机制:需配合信号量 / 互斥锁防止 “读写冲突”(如进程 a 写时进程 b 读)。
核心特征
| 特性 | 说明 |
|---|---|
| 零拷贝 | 数据直接在进程虚拟地址空间读写,无read()/write()的拷贝开销(相比管道 / 消息队列的 2 次拷贝) |
| 内核持久化 | 内存段存在于内核中,进程退出后不消失,需显式删除 |
| 无内置同步 | 共享内存无锁 / 阻塞机制,需配合信号量 / 互斥锁避免 “竞态条件” |
| 地址独立 | 不同进程映射到的虚拟地址不同,但指向同一块物理内存 |
| 大小对齐 | 内存大小按系统页大小(通常 4kb)对齐,不足一页按一页分配 |
与其他 ipc 机制的性能对比
| ipc 机制 | 数据拷贝次数 | 核心开销 | 适用场景 | 性能排序 |
|---|---|---|---|---|
| system v 共享内存 | 0 次 | 仅内存映射 / 同步开销 | 大量数据、高性能需求的通信 | 1 |
| system v 消息队列 | 2 次(用户→内核→用户) | 系统调用 + 拷贝开销 | 结构化、带类型的小数据通信 | 2 |
| 命名管道(fifo) | 2 次 | 系统调用 + 拷贝开销 | 简单流式数据通信 | 3 |
| 匿名管道 | 2 次 | 系统调用 + 拷贝开销 | 亲缘进程临时通信 | 3 |
问题:同步依赖
共享内存的 “零拷贝” 优势也带来了风险:多个进程可同时读写同一块内存,若没有同步机制,会出现 “一个进程写了一半,另一个进程就读取” 的 “竞态条件”(数据错乱)。因此,共享内存必须配合信号量(system v/posix)或互斥锁使用,实现 “临界区独占访问”。
使用流程
- 用
ftok()生成唯一键值; - 创建内存段:进程调用
shmget(),内核在物理内存中分配一块连续区域,初始化shmid_ds; - 映射到进程空间:进程调用
shmat(),内核修改该进程的页表,将虚拟地址映射到共享物理内存; - 进程读写:进程直接通过虚拟地址读写物理内存,数据对所有映射的进程立即可见;
- 解除映射:进程调用
shmdt(),内核修改页表,解除虚拟地址与物理内存的关联; - 删除内存段:最后一个进程解除映射后,调用
shmctl(ipc_rmid),内核释放物理内存。
关键:多个进程的虚拟地址不同,但页表指向同一块物理内存,因此修改对所有进程可见。
函数解释
所有操作需包含以下头文件:
#include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h>
1. ftok ():生成 ipc 唯一键值
将「文件路径」和「项目 id」转换为key_t类型的整数键值,用于标识 system v ipc 对象(共享内存、消息队列、信号量)。不同进程使用相同的路径和项目 id,可生成相同的键值,从而访问同一个 ipc 对象。
key_t ftok(const char *pathname, int proj_id);
| 参数 | 说明 |
|---|---|
| pathname | 必须是存在且可访问的文件路径(如/tmp/test),文件仅作为标识,无需读写。 |
| proj_id | 项目标识(低 8 位有效,通常取 1-255),仅用于区分同一文件下的不同 ipc 对象。 |
返回值
- 成功:返回唯一的
key_t类型整数(通常是 int 别名); - 失败:返回
-1,并设置errno(如enoent路径不存在、eacces权限不足)。
注意事项
- 若文件被删除后重建,即使路径和 id 相同,生成的键值也会变化;
- 若无需跨进程共享,可直接用
ipc_private替代 ftok 生成的键值(仅当前进程 / 子进程可用)。
2. shmget ():创建 / 获取共享内存段
在内核中创建或获取共享内存段,返回唯一的shmid(共享内存标识符),内核会为每个共享内存段维护一个shmid_ds结构体(存储大小、权限、映射数等信息)。
int shmget(key_t key, size_t size, int shmflg);
| 参数 | 说明 |
|---|---|
| key | ftok 生成的键值,或ipc_private(创建私有共享内存)。 |
| size | 共享内存大小(字节):- 创建时:必须指定(按系统页大小 4k 对齐,不足则向上取整);- 获取时:设为 0。 |
| shmflg | 标志位(按位或组合):- ipc_creat:不存在则创建,存在则获取;- ipc_excl:与ipc_creat联用,若已存在则失败(确保创建新段);- 权限位:如0664(同文件权限,八进制)。 |
返回值
- 成功:返回非负整数
shmid(共享内存标识符); - 失败:返回
-1,设置errno(如eexist已存在、enomem内存不足、einvalsize 无效)。
注意事项
- 共享内存创建后,即使创建进程退出,也会一直存在于内核,直到被
shmctl删除或系统重启; - 权限位需与后续
shmat的读写权限匹配(如shm_rdonly需 shmget 设置读权限)。
3. shmat ():映射共享内存到进程地址空间
将内核中的共享内存段附加(映射) 到进程的虚拟地址空间,返回映射后的地址,进程可直接读写该地址(等同于操作普通内存)。
void *shmat(int shmid, const void *shmaddr, int shmflg);
| 参数 | 说明 |
|---|---|
| shmid | shmget 返回的共享内存标识符。 |
| shmaddr | 指定映射到进程的虚拟地址:- 设null(推荐):由系统自动分配;- 非 null:需对齐页大小,通常不推荐。 |
| shmflg | 映射标志:- 0:默认,读写权限;- shm_rdonly:只读权限(需 shmget 设置读权限)。 |
返回值
- 成功:返回映射后的虚拟地址(
void*); - 失败:返回
(void*)-1,设置errno(如einvalshmid 无效、eacces权限不足)。
注意事项
- 多个进程可映射同一个共享内存段,读写操作直接同步;
- 映射后进程退出,共享内存不会自动删除,仅解除映射。
4. shmdt ():解除共享内存映射
将共享内存段与进程的虚拟地址空间分离(解除映射),仅断开关联,不删除内核中的共享内存。
int shmdt(const void *shmaddr);
| 参数 | 说明 |
|---|---|
| shmaddr | shmat 返回的映射地址。 |
返回值
- 成功:返回
0; - 失败:返回
-1,设置errno(如einval地址不是映射地址)。
注意事项
- 解除映射后,进程不可再访问该地址,否则触发段错误;
- 若所有进程都解除映射,共享内存仍存在于内核,需
shmctl主动删除。
5. shmctl ():控制共享内存段(核心:删除)
system v 共享内存的控制接口,支持获取状态、设置属性、删除共享内存(最常用ipc_rmid命令)。
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
| 参数 | 说明 |
|---|---|
| shmid | shmget 返回的共享内存标识符。 |
| cmd | 操作命令(核心):- ipc_stat:读取共享内存状态,存入buf;- ipc_set:修改共享内存属性(从buf读取);- ipc_rmid:删除共享内存段(内核释放资源)。 |
| buf | struct shmid_ds结构体指针:- ipc_stat/ipc_set:存储 / 读取状态;- ipc_rmid:设null即可。 |
核心结构体(简化版)
struct shmid_ds {
struct ipc_perm shm_perm; // 权限结构体(含所有者、权限位等)
size_t shm_segsz; // 共享内存大小(字节)
pid_t shm_lpid; // 最后操作的进程id
pid_t shm_cpid; // 创建进程id
shmatt_t shm_nattch; // 当前映射的进程数
time_t shm_atime; // 最后映射时间
time_t shm_dtime; // 最后解除映射时间
};
返回值
- 成功:返回
0(ipc_stat/ipc_set/ipc_rmid); - 失败:返回
-1,设置errno(如einvalshmid 无效、eperm权限不足)。
注意事项
- 执行
ipc_rmid后,内核标记共享内存为「待删除」:- 若仍有进程映射,新进程无法再映射该段;- 当所有进程解除映射后,内核才真正释放资源; - 普通用户仅能删除自己创建的共享内存,root 可删除所有。
完整示例(创建→写入→读取→删除)
1. 写进程(shm_write.c)
#include <stdio.h> // 提供printf、perror等输入输出函数
#include <stdlib.h> // 提供exit()退出函数(进程出错时终止)
#include <string.h> // 提供strncpy字符串拷贝函数
#include <sys/ipc.h> // 提供ftok()函数(生成ipc键值)
#include <sys/shm.h> // 提供shmget/shmat/shmdt/shmctl等共享内存核心函数
#include <unistd.h> // 提供getchar()(等待用户输入)、系统调用基础功能
// 2. 宏定义:把固定值抽出来,方便修改和理解
#define pathname "/tmp/shm_test" // ftok需要的文件路径(必须存在!小白要先touch这个文件)
#define proj_id 100 // 项目id(仅低8位有效,随便设1-255之间的数即可)
#define shm_size 4096 // 共享内存大小(字节),4096是系统页大小(对齐要求,不能随便设小)
int main() {
// -------------------------- 步骤1:生成唯一的ipc键值 --------------------------
// key_t是专门存ipc键值的类型(本质是整数)
// ftok作用:把"文件路径+项目id"转换成唯一整数,让读写进程能找到同一个共享内存
key_t key = ftok(pathname, proj_id);
// 检查ftok是否失败(返回-1就是失败)
if (key == -1) {
// perror:自动打印"xxx failed: 具体错误原因"(比如文件不存在会提示no such file)
perror("ftok failed");
exit(1); // 1表示异常退出(0是正常退出),终止进程
}
// -------------------------- 步骤2:创建共享内存段 --------------------------
// shmget作用:向内核申请一块共享内存,返回"共享内存id(shmid)"(类似文件句柄)
// 参数1:ftok生成的键值(标识共享内存)
// 参数2:共享内存大小(必须是系统页大小的整数倍,4096是最常用的)
// 参数3:标志位组合(ipc_creat=创建新的 | ipc_excl=如果已存在则报错 | 0664=权限,和文件权限一样)
int shmid = shmget(key, shm_size, ipc_creat | ipc_excl | 0664);
// 检查shmget是否失败(比如内存不足、键值已存在)
if (shmid == -1) {
perror("shmget failed");
exit(1);
}
// 打印shmid,方便调试(比如用ipcs -m命令查看时能对应上)
printf("共享内存id(shmid):%d\n", shmid);
// -------------------------- 步骤3:把共享内存映射到进程地址空间 --------------------------
// shmat作用:把内核里的共享内存,"挂到"当前进程的内存地址上,进程才能直接读写
// 参数1:shmget返回的共享内存id
// 参数2:指定映射的地址(设null让系统自动分配,小白千万别改)
// 参数3:映射权限(0=读写,shm_rdonly=只读)
// 返回值:映射后的内存地址(进程直接操作这个地址就等于操作共享内存)
char *shm_addr = (char *)shmat(shmid, null, 0);
// 检查映射是否失败(返回(void*)-1就是失败,注意强制类型转换)
if (shm_addr == (void *)-1) {
perror("shmat failed");
exit(1);
}
// -------------------------- 步骤4:向共享内存写入数据 --------------------------
// 要写入的字符串(小白注意:c语言字符串末尾有个隐藏的'\0',表示结束)
const char *msg = "hello, shared memory!";
// strncpy:把msg拷贝到共享内存地址shm_addr
// 第三个参数:strlen(msg)+1 是为了把末尾的'\0'也拷贝过去(否则读的时候会乱码)
strncpy(shm_addr, msg, strlen(msg) + 1);
// 打印写入的内容,确认写成功
printf("已向共享内存写入:%s\n", shm_addr);
// -------------------------- 等待读进程读取数据 --------------------------
// 暂停进程,等用户按回车再继续(给读进程留时间读取,否则写进程直接删了共享内存,读进程就读不到了)
printf("请按回车键继续(此时可以启动读进程读取数据)...\n");
getchar(); // 阻塞等待用户输入回车
// -------------------------- 步骤5:解除共享内存映射 --------------------------
// shmdt作用:把共享内存和当前进程"解绑"(进程不再能访问这个地址,但共享内存还在内核里)
// 参数:shmat返回的映射地址
if (shmdt(shm_addr) == -1) {
perror("shmdt failed");
exit(1);
}
// -------------------------- 步骤6:删除共享内存(释放内核资源) --------------------------
// shmctl作用:控制共享内存(这里用ipc_rmid命令删除)
// 参数1:共享内存id
// 参数2:操作命令(ipc_rmid=删除共享内存)
// 参数3:共享内存的状态结构体(删除时设null即可)
if (shmctl(shmid, ipc_rmid, null) == -1) {
perror("shmctl failed");
exit(1);
}
// 正常退出进程(0表示无错误)
return 0;
}2. 读进程(shm_read.c)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define pathname "/tmp/shm_test"
#define proj_id 100
#define shm_size 4096
int main() {
// 1. 生成相同键值
key_t key = ftok(pathname, proj_id);
if (key == -1) {
perror("ftok failed");
exit(1);
}
// 2. 获取已存在的共享内存(size设0,仅获取)
int shmid = shmget(key, 0, 0);
if (shmid == -1) {
perror("shmget failed");
exit(1);
}
printf("shmid: %d\n", shmid);
// 3. 映射(只读权限)
char *shm_addr = (char *)shmat(shmid, null, shm_rdonly);
if (shm_addr == (void *)-1) {
perror("shmat failed");
exit(1);
}
// 4. 读取数据
printf("read from shm: %s\n", shm_addr);
// 5. 解除映射
if (shmdt(shm_addr) == -1) {
perror("shmdt failed");
exit(1);
}
return 0;
}
编译与运行
# 先创建标识文件 touch /tmp/shm_test # 编译 gcc shm_write.c -o shm_write gcc shm_read.c -o shm_read # 先运行写进程 ./shm_write # 再新开终端运行读进程 ./shm_read
常用辅助命令
| 命令 | 说明 |
|---|---|
| ipcs -m | 查看所有共享内存段 |
| ipcrm -m <shmid> | 命令行删除指定共享内存 |
| cat /proc/sys/kernel/shmmax | 查看共享内存最大限制 |
内核为每个共享内存段维护关键元数据结构 struct shmid_ds(类似文件的inode):
struct shmid_ds {
struct ipc_perm shm_perm; // 权限信息(uid/gid、访问权限)
size_t shm_segsz; // 共享内存段大小(字节,按页对齐)
pid_t shm_lpid; // 最后操作的进程id
pid_t shm_cpid; // 创建进程id
shmatt_t shm_nattch;// 当前映射该段的进程数
time_t shm_atime; // 最后映射时间
time_t shm_dtime; // 最后解除映射时间
time_t shm_ctime; // 最后修改时间
};关键注意事项
1 同步是必选项
共享内存无任何内置同步机制,多个进程同时读写会导致数据错乱(如写进程写了一半,读进程就读取)。必须配合:
- system v/posix 信号量(互斥 / 同步);
- 互斥锁(pthread_mutex_t);
- 自定义标志(如共享内存中加 “是否写入完成” 的标记)。
2 内存大小与对齐
shmget()的size参数会按系统页大小(通常 4kb)对齐,例如申请 100 字节,内核实际分配 4096 字节;- 建议按页大小申请(如 4096、8192),避免内存浪费。
3 资源泄漏问题
- 共享内存段是内核资源,进程退出(即使异常)不会自动删除,仅解除映射(
shmdt); - 未调用
shmctl(ipc_rmid)会导致内存段永久占用内核资源,直到系统重启; - 解决:确保至少一个进程执行
ipc_rmid,或通过ipcrm -m <shmid>手动删除。
4 权限与访问控制
shmget()的shmflg需设置正确权限(如0666),否则其他进程无法映射 / 读写;- 若进程 uid/gid 不匹配,会返回
eacces错误,需检查权限设置。
5 进程异常退出处理
- 进程异常退出时,内核会自动解除其共享内存映射(
shmdt),但内存段仍存在; - 信号量需设置
sem_undo,避免进程异常退出导致死锁。
system v 共享内存 vs posix 共享内存
| 特性 | system v 共享内存 | posix 共享内存(mmap+shm_open) |
|---|---|---|
| 标识方式 | 键值(key)+ 标识符(shmid) | 文件系统路径(/dev/shm/xxx) |
| 持久化 | 内核持久化(需显式删除) | 文件系统持久化(需 unlink) |
| 接口复杂度 | 较低(5 个核心函数) | 较高(mmap/shm_open/ftruncate) |
| 跨进程可见性 | 需 ftok 生成 key | 路径可见,更易管理 |
| 大小调整 | 创建时固定 | 可通过 ftruncate 动态调整 |
| 现代支持 | 传统接口,维护中 | 现代接口,推荐使用 |
总结
system v 共享内存是性能最高的 ipc 机制,核心是 “内核物理内存映射到进程虚拟地址空间”,实现零拷贝数据共享;
核心操作流程:ftok()生成键值 → shmget()创建 / 获取段 → shmat()映射到进程空间 → 读写数据 → shmdt()解除映射 → shmctl(ipc_rmid)删除段;
关键要点:
- 必须配合信号量 / 互斥锁实现同步,避免竞态条件;
- 内存大小按页对齐,按需申请避免浪费;
- 用完必须调用
shmctl(ipc_rmid),防止内核资源泄漏;
现代开发中,posix 共享内存(shm_open+mmap)更易管理,但 system v 共享内存仍是传统高性能 ipc 的核心选择。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。
发表评论