上一篇《通信(三):System V 共享内存:从页表映射到 shmget/shmat 一文搞懂》把接口、生命周期、key/shmid 都过了一遍。但有个问题一直没正面答:共享内存映射建好之后,进程读写那段内存,到底要不要进内核、走不走系统调用? 这篇把它讲透,顺带补三个篇25 没细说的隐藏细节。
一、先给结论:访问共享内存没有系统调用
直接回答:映射建好之后,进程对共享内存的每一次读写,都不会触发系统调用。
反直觉点:很多人默认"IPC 嘛,肯定要经过内核",共享内存恰恰是反例。它快不是"快一点",是根本不进内核。
为什么这个问题关键:弄懂"不进内核",就弄懂了它为什么是 IPC 速度天花板,也弄懂了"零拷贝"到底省掉了哪一步。
二、为什么没系统调用:地址在用户空间
进程虚拟地址空间布局大致是 :代码段、数据段、堆、栈......共享内存的映射区域通常落在堆和栈之间的用户空间区域。
管道不一样 :管道是文件,数据存在内核的文件缓冲区。用户要读写必须调 read / write,这俩本身就是系统调用,会切到内核态把数据从内核缓冲区搬到用户空间(或反过来)。

共享内存相反 :shmat 把那段物理内存映射进本进程的虚拟地址空间后,你拿到的 shmaddr 就是一个普通用户态虚拟地址 。之后 addr[i] = 'A' 这种操作,和写普通数组没区别------CPU 经 MMU 直接访问页表指向的物理页,全程在用户态,内核不参与。

澄清一个误区(回指篇25):不是"建立映射不进内核"。shmget / shmat / shmdt / shmctl 都是系统调用,页表改写由内核完成。但映射建好之后的那无数次读写,不再进内核。一次建、永远用,这就是它比管道省掉的两趟拷贝。
三、shmat 返回的是什么:虚拟地址,和 malloc 同理
shmat 返回 void*,本质是一个虚拟地址,不是物理地址,也不是"文件句柄"。
类比 malloc:malloc 返回的也是虚拟地址,物理页是内核按需分配的;shmat 同理,区别只在于它映射到的物理页是多进程共享的那一块 ,而 malloc 拿到的是本进程私有物理页。
推论:你在共享内存上做的所有操作------指针偏移、memcpy、下标访问------都和本地内存一样,零额外成本。这也是"零拷贝"的真正含义:没有内核搬运这一环,数据是"大家直接改同一块物理页"。
四、大小为什么必须按 4KB 对齐
物理内存按页 管理,Linux 通常一页 4096 字节。shmget 申请共享内存时,内核按页向上取整分配。
你传 size = 4097 会怎样?内核实际给你两页 = 8192 字节 ,但 shmid_ds.shm_segsz 显示的还是你申请的 4097。多出来的部分被浪费,且你不能越界使用。
验证方法:
bash
ipcs -m
用 ipcs -m 看对应段的 size 列,或在代码里打印 shm_segsz。一般直接传 4096 的整数倍(4096、8192......)最干净。
划重点:这不是"建议",是内核分配粒度的硬约束。不对齐不会报错,但会多占物理页、造成浪费。
五、没有同步,现场能乱成什么样
篇25 说过"共享内存不带同步互斥",这里给个直观现场。
基于篇25 的 server / client demo:如果 server 先启动,sleep 2 后开始循环读;而 client 还在 sleep 1 + sleep 2,还没往里写。
server 第一次 printf 读到的就是空白或上一次的旧数据------不是"等 client 写完再读",而是直接读、读到啥算啥。
根因 :共享内存只负责"大家看得到同一块",不负责"谁先写谁后读"的顺序,也没有读写保护。多个进程并发写还会互相覆盖。
结论:实际项目里共享内存必须配信号量(下一篇《通信(四)》)或用管道做通知。本篇只点现象,方案留到下篇。
六、回指篇25:接口 / 生命周期 / key-shmid 不重讲
这篇是篇25 的补遗,下面这些篇25 已经讲透,这里不重复:
- 五个接口
ftok / shmget / shmat / shmdt / shmctl的用法; - 生命周期随内核、
ipcs -m / ipcrm -m清理残留; - key 与 shmid 的区别;
- 多文件 demo 与
IPC_CREAT | IPC_EXCL。
没看过篇25 的,先去把《通信(三):System V 共享内存:从页表映射到 shmget/shmat 一文搞懂》看了,再回来啃这篇细节。
七、共享内存的优缺点
前面几节把机制拆开了,这里收一张总表,方便面试时一口报全。
优点:
- 速度最快:映射建好后读写不进内核、零拷贝,是 IPC 里的性能天花板。
- 用法简单:shmat 拿到指针就能当本地内存用,和 malloc 一个道理,没有 read/write 那套。
- 扛大块数据:管道搬大块数据开销明显,共享内存无内核搬运,量越大优势越突出。
缺点:
- 无同步互斥:多进程并发读写会乱,必须自己配信号量或用管道通知(本篇五已给现场)。
- 无访问控制:任何挂接进程都能读能写,没有读写权限隔离。
- 生命周期要手动管:随内核,不删就泄漏,得靠 ipcrm 或 shmctl(IPC_RMID) 清理。
- 大小受页对齐约束:不对齐浪费物理页(本篇四已讲)。
一句话:共享内存是"把性能给你,把秩序留给你自己"的 IPC。
八、面试官追问(本篇新增)
| 问题 | 答案 |
|---|---|
| 访问共享内存期间会发生系统调用吗? | 映射建好之后不会。shmat 返回的是用户态虚拟地址,后续读写只是访问自己的页表映射,全程用户态;只有建 / 挂 / 摘 / 删那几次系统调用才进内核。 |
| 共享内存大小为什么要按页对齐? | 内核按 4KB 物理页分配,传 4097 实际拿 8192,多出的浪费;传整数倍最干净。 |
| shmat 返回的和 malloc 返回的本质区别? | 都是虚拟地址; 区别在映射到的物理页:malloc 是进程私有,shmat 是多进程共享同一块。 |
| 为什么共享内存比管道快? | 管道数据走内核文件缓冲区,每次读写都系统调用搬数据; 共享内存映射后直接用户态访问物理页,无内核搬运。 |
九、小结
一句话:共享内存映射建好后,读写不进内核、无系统调用,因为它是用户空间的虚拟地址;shmat 和 malloc 同理,只是物理页被多个进程共享。
三个隐藏细节:
- 没系统调用,是它最快的根本原因;
- 大小按 4KB 页对齐,不对齐白白浪费物理页;
- 无同步会导致读到空白 / 旧数据,必须配信号量。
回指篇25 看接口与生命周期;下一篇《通信(四)》用信号量把"同步"这个坑正式填上。
