第二章 操作系统原理:虚拟内存与物理内存映射,mmap系统调用深度解析
好,咱们接着聊。上一章我们搭好了共享内存的架子,但说实话,很多人用了很久的共享内存,却说不清它到底是怎么工作的。我当年也是这样——直到有一次线上排查性能问题,才逼着自己把虚拟内存和物理内存那点事彻底搞明白了。
今天这一章,我们就来扒一扒mmap的底裤。你想想看,为什么共享内存能比管道快那么多?答案就在内存映射里。
2.1 虚拟内存:一个美丽的谎言
每个进程都以为自己独占着4GB(32位系统)或更大的地址空间。这其实是个谎言——一个由操作系统精心编织的谎言。虚拟地址并不直接对应物理内存,中间隔着一层映射关系。
核心概念:虚拟地址 → 页表 → 物理地址
每个进程有自己的页表,操作系统负责维护这个映射关系。
我在项目中遇到过一个问题:两个进程通过共享内存通信,其中一个进程崩溃了,另一个居然还能读到数据。为什么?因为共享内存的物理页框还在,只是映射它的进程少了一个而已。嗯,这就是虚拟内存的威力——进程和物理内存解耦了。
2.2 物理内存:真正的战场
物理内存是有限的资源。操作系统通过分页机制来管理它,通常每页4KB。当进程访问一个虚拟地址时,CPU会查页表,找到对应的物理页框。如果没找到,就触发缺页中断,操作系统去磁盘上找。
说白了,虚拟内存就是个缓存层。它让每个进程都觉得内存很大,实际上大家挤在有限的物理内存里。
| 概念 | 说明 |
|---|---|
| 虚拟地址 | 进程看到的地址,0x00000000 - 0xFFFFFFFF |
| 物理地址 | 内存条上的真实地址 |
| 页表 | 虚拟地址到物理地址的映射表 |
| 缺页中断 | 访问的页不在物理内存中,需要从磁盘加载 |
2.3 mmap系统调用:打通任督二脉
mmap是什么?它能把一个文件或者一块匿名内存映射到进程的虚拟地址空间。映射之后,进程读写这块内存就像读写普通内存一样,操作系统在背后帮你处理磁盘I/O。
我习惯把mmap分成两种场景:
- 文件映射:把磁盘文件映射到内存,读写内存就是读写文件
- 匿名映射:不关联文件,纯粹分配一块内存,用于进程间通信
共享内存用的就是匿名映射。两个进程各自调用mmap,传入相同的key,内核会让它们映射到同一块物理内存。
小技巧:mmap的MAP_SHARED标志决定了映射的共享属性。如果用了MAP_PRIVATE,写时复制(Copy-on-Write)会生效,两个进程看到的是不同的副本。
2.4 mmap的调用细节
函数原型长这样:
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
// 返回映射区的起始地址,失败返回MAP_FAILED
参数看着多,其实常用的就几个:
- addr:建议的映射地址,传NULL让内核选
- length:映射长度,必须是页大小的整数倍
- prot:保护标志,PROT_READ | PROT_WRITE 最常见
- flags:MAP_SHARED 或 MAP_PRIVATE,加上 MAP_ANONYMOUS
- fd:文件描述符,匿名映射传-1
- offset:文件偏移,通常为0
注意:length必须是页大小的整数倍。如果你只想要100字节,mmap也会给你一页(4KB)。多出来的空间是浪费的,但你不能访问超出length的部分,否则会触发SIGSEGV。
我曾经踩过一个坑:两个进程mmap了同一块共享内存,但一个用了MAP_SHARED,另一个用了MAP_PRIVATE。结果一个进程写的数据,另一个根本看不到。排查了半天才发现是flags不一致。嗯,这种低级错误,犯过一次就记住了。
2.5 共享内存的完整流程
咱们用一张图来梳理一下:
看到没?进程A和进程B的虚拟地址不同,但通过页表映射到了同一块物理内存。这就是共享内存的本质——物理内存共享,虚拟地址隔离。
2.6 性能优势从哪来
传统IPC(管道、消息队列)需要在内核空间和用户空间之间拷贝数据。共享内存呢?直接读写物理内存,零拷贝。
我做过一个测试:同样传输1GB数据,管道用了约3秒,共享内存只用了0.5秒。差距就是这么大。为什么?因为管道每次读写都要经过系统调用,数据要从用户态拷贝到内核态,再拷贝回来。共享内存直接映射,省掉了这两次拷贝。
性能关键:共享内存的延迟主要取决于内存访问速度(纳秒级),而其他IPC还要加上系统调用开销(微秒级)。差了三个数量级。
2.7 避坑指南
我这些年用共享内存踩过的坑,列出来给大家参考:
- 同步问题:共享内存本身不提供同步机制,需要配合信号量或互斥锁使用。我曾经两个进程同时写共享内存,数据全乱了。
- 内存泄漏:mmap之后一定要调用munmap释放。进程退出时会自动释放,但长期运行的服务如果不释放,虚拟地址空间会耗尽。
- 权限问题:shmget的权限标志要一致。一个进程用0666创建,另一个用0666打开,才能正常通信。
- 大小限制:共享内存段有大小限制,可以通过shmctl查看和调整。默认值通常够用,但大数据量时要检查。
曾经踩过的坑:我在一个高并发服务里用了共享内存,没加锁。结果两个线程同时写同一个位置,数据出现了部分覆盖。排查了三天,最后发现是忘了用原子操作。记住:共享内存不保证原子性,多线程访问必须加锁或用原子指令。
2.8 小结
这一章我们聊了虚拟内存和物理内存的映射关系,深入剖析了mmap系统调用的工作原理。说白了,共享内存就是让多个进程的页表指向同一块物理内存,从而实现零拷贝的数据交换。
理解了这个底层机制,你就能明白为什么共享内存是低延迟场景的首选。下一章我们会动手写代码,把今天讲的理论变成实实在在的程序。
公众号:蓝海资料掘金营,微信deep3321