☰
Linux系统篇25——通信(三):System V 共享内存:从页表映射到 shmget/shmat 一文搞懂
2026/10/3 22:23:59 网站建设 项目流程

这一篇讲透 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_CREATIPC_EXCL`创建并返回失败返回 -1
0(只用权限位或 0)失败返回 -1获取并返回参与者另一种写法

为什么发起者要用IPC_CREAT | IPC_EXCL?它等价于"我要一个全新的、没人用过的共享内存",如果拿到的是别人已经建好的,说明 key 冲突或资源残留在内核里——直接报错更稳妥,避免偷偷挂到一个不对的内存上。 去获取。

八、key 的本质:用户空间"约定"出来的名字

key 由用户空间生成,传给 OS。很多人第一次用共享内存会懵:两个进程凭什么保证挂到同一块内存?

答案就在key:

  1. shmget(key, ...)在内核里是用 key 作为查找主键的——同一个 key 找到的就是同一块共享内存对象。
  2. 而 key 来自ftok(pathname, proj_id),它本质是"路径 + 项目ID"的哈希值。
  3. 所以双方只要约定好同一个路径、同一个 proj_id,各自ftok出来的 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 可能还没写完——读到的就是半成品。怎么补?两条路:

  1. 配信号量(Semaphore):最正统的"共享内存 + 信号量"组合,下一篇会展开。
  2. 借一根管道做"通知":课件实例2 给了思路——用命名管道充当"轮次信号",client 写完发一个字节唤醒 server。本质是用管道的阻塞特性,给共享内存加一层简单的顺序控制(课件也称它"仅仅是让同学们理解顺序性",有 bug 仅作演示)。

记住主线结论就够了:共享内存管"数据",同步管"秩序",两者一定要搭配使用。细节留到下一篇信号量。

十一、与前面文章的联系 / 面试常考串联

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;不删会内核资源泄漏。
共享内存没有同步怎么办?搭配信号量做互斥/秩序,或用管道做轻量通知;实际工程中两者组合最常见。

十三、小结

用三句话收尾:

  1. 原理:共享内存让多个进程的页表指向同一块物理内存,读写零拷贝、最快;
  2. 接口:ftok 建 key → shmget 建/取 → shmat 挂 → shmdt 摘 → shmctl(IPC_RMID) 删;
  3. 坑:shmdt ≠ 删除、生命周期随内核、没有同步互斥、大小最好按页对齐。

💬 看完如果有一句没懂,或者你手上的课件图跟我这里解读对不上,评论区直接说,我补图补例子。也欢迎把本篇和你正在学的《通信(一)(二)》串起来复习。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询