这一篇讲透 Linux 下 System V 共享内存:它为什么是"最快的 IPC"、底层凭什么能"让两个进程看到同一块内存"、以及 ftok / shmget / shmat / shmdt / shmctl 这套接口到底怎么用、怎么删、怎么保证两个进程挂到同一块上。代码沿用课件里的 comm.h / comm.c / server.c / client.c 多文件结构,方便你直接对照课程。
目录
目录
编辑
目录
一、一个问题:管道传大块数据,够快吗?
二、一句话结论:什么是 System V 共享内存
三、System V 是什么:标准还是 Linux 专属?
四、底层原理:两块虚拟地址,如何指向同一块物理内存
4.1 先回忆:进程之间是"隔离"的
4.2 共享内存的"魔法":让页表指向同一块物理内存
4.3 为什么快?"不经过内核"指的是什么
五、内核怎么管理共享内存:shmid_ds 结构
六、五个接口逐个拆:ftok / shmget / shmat / shmdt / shmctl
6.1 ftok —— 在用户层构造一个 key
6.2 shmget —— 创建或获取共享内存
6.3 shmat —— 挂接到进程地址空间
6.4 shmdt —— 与当前进程"去关联"
6.5 shmctl —— 控制 / 删除共享内存
七、IPC_CREAT 与 IPC_EXCL:到底几个意思
八、key 的本质:用户空间"约定"出来的名字
九、多文件版完整 demo
comm.h
comm.c
server.c —— 创建者,循环读出 client 写的内容
client.c —— 参与者,往共享内存里写 26 个字母
运行与排坑
十、共享内存的同步问题
十一、与前面文章的联系 / 面试常考串联
11.1 对比《通信(一):什么是进程间通信?》
11.2 对比《通信(二):匿名管道 vs 命名管道》
11.3 对比《进程(四):虚拟地址空间 / 页表 / 进程隔离》
十二、面试官追问
十三、小结
一、一个问题:管道传大块数据,够快吗?
在 《Linux系统篇24——通信(二):匿名管道 vs 命名管道》 里我们把"管道"讲透了:内核缓冲区 + 两个 fd,先进先出。但它有个绕不开的代价——
每一次通信都要"用户态 → 内核态 → 用户态"拷贝两次。
进程 A 把数据从自己的用户空间 write 进内核缓冲区,进程 B 再从内核缓冲区 read 到自己的用户空间。数据在用户态和内核态之间兜了一圈。传几 KB 没感觉,传几百 MB 的大块数据,这个拷贝开销就非常明显。
于是有了 IPC 家族里速度天花板——共享内存(Shared Memory)。它不走内核中转,而是让多个进程直接读写同一块物理内存。下面把它拆开。
二、一句话结论:什么是 System V 共享内存
共享内存 = 同一块物理内存,被映射到多个进程的虚拟地址空间;进程之间不再经过内核拷贝即可交换数据。但它的代价是"不带同步互斥",必须自己配信号量/管道来控制访问顺序。

三、System V 是什么:标准还是 Linux 专属?
System V 是一套标准,Linux 内核支持并实现了一套 IPC 通信模块。
展开说:
- System V(System Five)是早期的 Unix 标准之一,它定义了进程间通信的一组接口规范。
- Linux 内核实现了这套规范里的三种 IPC:消息队列、共享内存、信号量。所以你会看到它们都被命名为 msg* / shm* / sem*,API 形态高度一致(都要先用 key 去"找/建",再用一个 id 去"操作")。
- 它和 POSIX IPC(同样是消息队列、共享内存、信号量,但接口命名是 mq_* / shm_open 那种)是并列的两套体系。大厂面试常问两者区别,这里先记住:我们这篇讲的是 System V 这套。
四、底层原理:两块虚拟地址,如何指向同一块物理内存
4.1 先回忆:进程之间是"隔离"的
在 《Linux系统篇(十五)——进程(四):一文吃透 Linux 虚拟地址空间》 里我们建立了一个关键认知:
- 每个进程有独立的虚拟地址空间;
- CPU 访问变量时,靠 MMU + 页表 把虚拟地址翻译成物理地址;
- 页表里存的是"虚拟页号 → 物理页号"。
所以两个进程的 0x1234 虽然虚拟地址相同,但页表不同,最终落到两块不同的物理内存。这正是进程隔离的基石,也是"为什么全局变量在进程间是假共享"的根本原因。

4.2 共享内存的"魔法":让页表指向同一块物理内存
共享内存干的事,恰恰是把这套规则反过来用——

4.3 为什么快?"不经过内核"指的是什么
管道每次传递数据都要陷入内核("进入内核的系统调用来传递彼此的数据")。共享内存之所以快,是因为数据一旦映射好了,进程后续读写就只是读写自己的虚拟地址,不需要再调用 read/write 让内核搬运——内核只在"建立映射"那一刻介入,之后就是两个进程对一块内存的直接访问。这就是课件说的"数据传递不再涉及到内核"。
但同时要记住一句硬话:"共享内存没有进行同步与互斥!共享内存缺乏访问控制!会带来并发问题。" 所以它快,但"不安全",后面第十节点一下怎么补。
五、内核怎么管理共享内存:shmid_ds 结构
既然"多个进程可能使用不同的共享内存",内核必须有一套结构体来描述和组织每一个共享内存对象。
struct shmid_ds {
struct ipc_perm shm_perm; /* 操作权限 */
int shm_segsz; /* 共享内存段大小 (bytes) */
__kernel_time_t shm_atime; /* 最后 attach 时间 */
__kernel_time_t shm_dtime; /* 最后 detach 时间 */
__kernel_time_t shm_ctime; /* 最后改动时间 */
__kernel_ipc_pid_t shm_cpid; /* 创建者 pid */
__kernel_ipc_pid_t shm_lpid; /* 最后操作者 pid */
unsigned short shm_nattch; /* 当前挂接数 (attach 的进程数) */
unsigned short shm_unused; /* 兼容性字段 */
void *shm_unused2; /* 旧版 DIPC 用 */
void *shm_unused3; /* 未使用 */
};
几个面试常问的成员:
- shm_perm:权限,和文件权限类似,由 shmget 的 shmflg 里的 0666 这种位决定。
- shm_segsz:这段共享内存多大。
- shm_nattch:当前有几个进程挂接在上面。
六、五个接口逐个拆:ftok / shmget / shmat / shmdt / shmctl
6.1 ftok —— 在用户层构造一个 key
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
- 作用:把"一个真实存在的路径 + 一个项目 ID"映射成一个 key_t 整数。
- 关键点(图片里强调了):key 是由用户空间生成的,再传给 OS;它不是内核直接分配给你的。两个进程只要传相同的 pathname 和 proj_id,就能算出同一个 key,进而找到同一块共享内存。
- 坑:pathname 必须真实存在,它只是被拿来"取某些位"参与运算,路径指向的文件内容和大小都无所谓。
6.2 shmget —— 创建或获取共享内存
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
| key | 这个共享内存段的"名字",由 ftok 生成 |
| size | 共享内存大小(建议是页大小 4096 的整数倍) |
| shmflg | 由九个权限标志构成,用法和创建文件时的 mode 模式标志一样(如 0666) |
返回值:成功返回该共享内存段的标识码(非负整数,叫 shmid);失败返回 -1。
shmflg 最重要的两个位:
- IPC_CREAT:共享内存不存在 → 创建并返回;已存在 → 获取并返回。
- IPC_CREAT | IPC_EXCL:共享内存不存在 → 创建并返回;已存在 → 出错返回 -1(用来确保"我是创建者")。
6.3 shmat —— 挂接到进程地址空间
void *shmat(int shmid, const void *shmaddr, int shmflg);
- 作用:把标识为 shmid 的共享内存,映射到当前进程的虚拟地址空间。
- 参数:
- shmid:shmget 返回的标识码;
- shmaddr:指定连接地址,通常传 NULL 让内核自动选一个地址;
- shmflg:常用 SHM_RND(地址向下对齐)、SHM_RDONLY(只读)。
- 返回值:成功返回指向共享内存第一个字节的指针;失败返回 (void*)-1。
6.4 shmdt —— 与当前进程"去关联"
int shmdt(const void *shmaddr);
- 作用:把共享内存段与当前进程脱离(解除映射)。
- 参数:shmaddr 就是 shmat 返回的指针。
- 返回值:成功 0;失败 -1。
- ⚠️ 关键坑:"将共享内存段与当前进程脱离不等于删除共享内存段。" 它只是让当前进程不再能访问这段地址,物理内存和内核对象还在。
6.5 shmctl —— 控制 / 删除共享内存
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
- 作用:用于控制共享内存(查状态、设权限、删除)。
- cmd 的三个主要取值:
- IPC_RMID:标记删除。即便还有进程挂着,也照样删(内核会等最后一个进程 detach 后真正释放)。
- IPC_STAT:把当前状态拷进 buf。
- IPC_SET:用 buf 更新权限。
- 💡 生命周期提醒:System V 的 IPC 资源生命周期随内核——进程退出不会自动删,除非重启或手动 shmctl(IPC_RMID) / ipcrm -m <shmid>。所以"谁来删"必须是进程该做的事,别指望系统替你收尾
七、IPC_CREAT 与 IPC_EXCL:到底几个意思
| IPC_CREAT | 创建并返回 | 获取并返回 | 参与者(client) |
| IPC_CREAT | IPC_EXCL` | 创建并返回 | 失败返回 -1 |
| 0(只用权限位或 0) | 失败返回 -1 | 获取并返回 | 参与者另一种写法 |
为什么发起者要用 IPC_CREAT | IPC_EXCL? 它等价于"我要一个全新的、没人用过的共享内存",如果拿到的是别人已经建好的,说明 key 冲突或资源残留在内核里——直接报错更稳妥,避免偷偷挂到一个不对的内存上。 去获取。
八、key 的本质:用户空间"约定"出来的名字
key 由用户空间生成,传给 OS。很多人第一次用共享内存会懵:两个进程凭什么保证挂到同一块内存?
答案就在 key:
这跟命名管道"认名字"是一个思想:靠一个双方都认可的名字来接头。区别在于,命名管道的名字是文件系统里的 FIFO 文件,共享内存的"名字"是内存里的一个整数 key。
顺带区分两个容易混的 ID:
- key:跨进程"约定"出来的名字,用户态决定;
- shmid:内核返回给你的"本地句柄",只在你这个进程上下文里用,用来调用 shmat/shmdt/shmctl。
问题:System‑V共享内存为什么key需要用户生成?
shmid是进程私有句柄,别的进程拿不到。key相当于全局暗号,用来让多个独立进程找到同一块共享内存,内核无法自动生成并通知其他进程该标识,所以要用户约定生成;如果传入IPC_PRIVATE由内核自动生成key,则仅fork的父子进程可以使用,不适合无关进程通信。
九、多文件版完整 demo
comm.h
#ifndef _COMM_H_
#define _COMM_H_
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define PATHNAME "." // ftok 用到的真实路径,双方必须一致
#define PROJ_ID 0x6666 // 项目 ID,双方必须一致
int createShm(int size); // 创建者:IPC_CREAT|IPC_EXCL
int destroyShm(int shmid);// 删除
int getShm(int size); // 参与者:IPC_CREAT
#endif
comm.c
#include "comm.h"
static int commShm(int size, int flags)
{
key_t key = ftok(PATHNAME, PROJ_ID); // ① 用户态生成 key
if (key < 0) {
perror("ftok");
return -1;
}
int shmid = 0;
if ((shmid = shmget(key, size, flags)) < 0) { // ② 用 key 创建/获取
perror("shmget");
return -2;
}
return shmid;
}
int destroyShm(int shmid)
{
if (shmctl(shmid, IPC_RMID, NULL) < 0) { // ⑤ 标记删除
perror("shmctl");
return -1;
}
return 0;
}
int createShm(int size)
{
return commShm(size, IPC_CREAT | IPC_EXCL | 0666); // 发起者:必须全新
}
int getShm(int size)
{
return commShm(size, IPC_CREAT); // 参与者:获取即可
}
server.c —— 创建者,循环读出 client 写的内容
#include "comm.h"
int main()
{
int shmid = createShm(4096); // ① 创建全新共享内存
char *addr = shmat(shmid, NULL, 0); // ③ 挂接到自己地址空间
sleep(2);
int i = 0;
while (i++ < 26) {
printf("client# %s\\n", addr); // ④ 直接读,不经过内核
sleep(1);
}
shmdt(addr); // ⑥ 去关联(不是删除)
sleep(2);
destroyShm(shmid); // ⑦ 真正删除
return 0;
}
client.c —— 参与者,往共享内存里写 26 个字母
#include "comm.h"
int main()
{
int shmid = getShm(4096); // ① 获取已存在的共享内存
sleep(1);
char *addr = shmat(shmid, NULL, 0); // ③ 挂接
sleep(2);
int i = 0;
while (i < 26) {
addr[i] = 'A' + i; // ④ 直接写,不经过内核
i++;
addr[i] = 0; // 末尾置 0,方便 printf
sleep(1);
}
shmdt(addr); // ⑥ 去关联
sleep(2);
return 0;
}
运行与排坑
先跑 ./server,它会 IPC_CREAT|IPC_EXCL 建一块新的;此时若你 Ctrl+C 杀掉 server,共享内存不会被删除(生命周期随内核),重跑就会报错:
# ./server
shmget: File exists
# ipcs -m # 查看残留的共享内存
—— Shared Memory Segments ——–
key shmid owner perms bytes nattch status
0x66026a25 688145 root 666 4096 0
# ipcrm -m 688145 # 手动删掉残留(演示用,正常应由进程删)
删 IPC 资源是"进程该做的事",这里 ipcrm 只为演示命令,正常场景千万别依赖手动清理。
十、共享内存的同步问题
共享内存没有进行同步与互斥!共享内存缺乏访问控制!会带来并发问题。
也就是说,server 读的时候,client 可能还没写完——读到的就是半成品。怎么补?两条路:
记住主线结论就够了:共享内存管"数据",同步管"秩序",两者一定要搭配使用。 细节留到下一篇信号量。
十一、与前面文章的联系 / 面试常考串联
11.1 对比《通信(一):什么是进程间通信?》
那篇把 IPC 全家福摆过一遍,其中对共享内存的定位是:"把同一块物理内存映射到多个进程,大家直接读写,零拷贝、性能最高。但它不负责同步,必须配信号量使用。" 本篇就是把那一句"零拷贝"背后的页表映射原理和API 实战补齐。
11.2 对比《通信(二):匿名管道 vs 命名管道》
| 数据路径 | 用户 → 内核缓冲 → 用户(两次拷贝) | 进程直接读写物理内存(零拷贝) |
| 边界/同步 | 自带 FIFO、阻塞、EOF | 无边界、无同步,要自己配 |
| 生命周期 | 匿名随进程、命名随文件 | 随内核,必须手动删 |
一句话:管道是"内核帮你搬",共享内存是"大家直接改同一块",所以共享内存快,但麻烦也多。
11.3 对比《进程(四):虚拟地址空间 / 页表 / 进程隔离》
那篇的核心是"每个进程一套页表,虚拟地址互不干扰"。本篇恰恰是它的反向应用:
- 普通变量:父子进程靠 COW(写时复制)各自一份物理页;
- 共享内存:内核特意让两个进程的页表项指向同一片物理页,于是读写互相可见。
这个结论把"进程隔离"和"进程共享"统一在了同一个页表机制下——面试被问"共享内存为什么能让两个进程看到同一份数据",答"页表指向同一物理页"就点到根上了。
十二、面试官追问
| 共享内存为什么最快? | 映射建立后,进程读写只是访问自己的虚拟地址,不再调用 read/write 让内核搬运;数据传递不经过内核拷贝。 |
| shmdt 和 shmctl(IPC_RMID) 区别? | shmdt 只是"当前进程"解除映射,内核对象还在;shmctl(IPC_RMID) 才是标记删除(等所有进程 detach 后真正释放物理内存)。 |
| 两个进程怎么保证挂到同一块? | 用同一个 key,而 key 来自 ftok 的"相同 pathname + 相同 proj_id"。内核按 key 查找共享内存对象。 |
| IPC_EXCL 什么场景用? | 保证本进程新建 IPC 对象,对象已存在则报错,避免重复创建 |
| 生命周期随内核是什么意思? | 进程退出不会自动删,除非重启或手动 shmctl(IPC_RMID)/ipcrm;不删会内核资源泄漏。 |
| 共享内存没有同步怎么办? | 搭配信号量做互斥/秩序,或用管道做轻量通知;实际工程中两者组合最常见。 |
十三、小结
用三句话收尾:
💬 看完如果有一句没懂,或者你手上的课件图跟我这里解读对不上,评论区直接说,我补图补例子。也欢迎把本篇和你正在学的《通信(一)(二)》串起来复习。
网硕互联帮助中心




评论前必须登录!
注册