共享内存、文件映射、写时复制、Swap,四件事看起来毫不相干,本质都是【改页表】。页表不只是翻译表,还是【资源分配的账本】。
5.4 到 5.10 都在讲【怎么把内存分出去】,这一节换个角度:这张表还能被用来做别的事。
一、共享:两个虚拟地址,一份物理内存
进程 A 把某一页映射进来,进程 B 也把【同一页物理内存】映射进来。 两张页表里填的是同一个物理页号。 于是 A 写一个字节,B 立刻看得见——中间没有任何拷贝。 这就是共享内存。它比【发消息】快,因为省掉了两次拷贝;但它也比【发消息】危险——没有边界,谁都能改。 所以共享内存必须配一把锁或者一个信号量,而这把锁又是另一个麻烦(进程崩了锁怎么办)。
二、保护:只读就是真的只读
页表项里有 R/W 位和 U/S 位。 把代码段的页设成【只读】,往里写一个字节,CPU 立刻抛异常——这不是软件检查,是硬件拦的。 把内核的页设成【用户态不可访问】,用户程序就翻不到内核内存。 这带来一个重要认识:权限不是【约定】,是【翻译表的一部分】。 所以绕过权限的唯一办法是【改页表】,而改页表需要特权指令。
三、四件事,一个机制
共享内存:两张表填同一个物理页号。 文件映射(5.12):页表直接指向页缓存里的页,读写文件等于读写内存。 写时复制(5.14):先让两边指向同一页并设成只读,谁写谁触发异常,这时候才复制。 Swap(5.15):把页表项的 P 位改成 0,内容挪到磁盘;再访问就缺页,从磁盘换回来。 四件事全是【改页表项】的三种字段组合出来——物理页号、权限位、P 位。
四、这个认识的用处
它把一堆【听起来不同的功能】收成一个【机制】,记起来省力,出问题时也知道该往哪看。 比如【内存明明够但申请失败】——可能是虚拟地址空间不够,不是物理内存不够。 比如【共享内存里的数据没同步】——因为它是同一块物理内存,两个进程的写没有先后保证。 比如【文件改了但程序读到旧的】——因为那一页还在页缓存里,没重新映射。
五、动手 Demo:虚拟内存的应用——共享、保护、按需
这一节真跑两件事:同一个文件映射两次,写一处看另一处;以只读方式映射,往里写看报不报错。
import mmap
with open("shared.bin", "w+b") as f:
f.write(b"\x00" * 65536)
with open("shared.bin", "r+b") as f:
m1 = mmap.mmap(f.fileno(), 0)
m2 = mmap.mmap(f.fileno(), 0)
m1[0:4] = b"\xDE\xAD\xBE\xEF"
print("从映射二读:", m2[0:4])
with open("shared.bin", "rb") as f:
m = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)
try:
m[0] = 1
except TypeError as e:
print("写只读映射 ->", type(e).__name__)
实跑输出:
--------------------------------------------------------------------
虚拟内存的应用——共享、保护、按需
--------------------------------------------------------------------
① 同一块内存,两个地址:一个改了,另一个立刻看见
动作 结果
-------------------------------------------
通过映射一写入 DE AD BE EF —
从映射二读出 DE AD BE EF
两者是同一份物理内存吗 是
**这就是 5.11 的核心**:**两个映射的虚拟地址不同,页表指向同一批物理页。**
**所以写一个等于写另一个——共享内存不需要拷贝。**
② 只读映射:想写就报错
动作 结果
-----------------------------------------
以只读方式映射 成功
往里写一个字节 被挡住了(TypeError)
**保护位是页表项里的 W 那一位**:**写只读页,CPU 直接抛异常。**
**这不是【检查了一下】,是硬件拦的,绕不过去。**
③ 虚拟内存顺手支撑的四件事
用途 靠哪条机制
--------------------------------------------------------
共享内存 两个页表填同一个物理页号
文件映射(5.12) 页表直接指向页缓存里的页
写时复制(5.14) 先共享,写的时候才复制
Swap(5.15) 页表项改成【不在了】,内容挪到盘上
第一个结果:通过映射一写进去的四个字节,从映射二读出来了。两者是同一份物理内存,所以不需要任何同步或拷贝。 第二个结果:写只读映射抛了 TypeError。在 Linux 上是总线错误(SIGBUS)或段错误(SIGSEGV),性质一样——硬件拦的。 注意这两个实验都不用任何特权:共享与保护是【普通程序也能用】的机制。 所以【进程隔离】不是绝对安全——它只保证【不通过页表就看不见】。
📋 Linux 上看共享内存段:ipcs -m 列出所有 System V 共享内存,ls -l /dev/shm 看 POSIX 共享内存(其实就是 tmpfs 里的文件)。
30 秒实验
ls -l /dev/shm,看有没有程序在用 POSIX 共享内存。ipcs -m,看 System V 共享内存段的字节数与连接进程数。- 用 Python 的
multiprocessing.shared_memory建一块共享内存,两个进程各写一角,验证互相可见。
常见误区
- 【共享内存要拷贝数据】。不拷贝——两个页表填的是同一个物理页号。
- 【只读是软件检查】。是页表项里的权限位,CPU 直接抛异常,绕不过去。
- 【虚拟内存只有【扩大内存】一个用途】。共享、保护、映射、COW、Swap 全是它的用途。
自测三问(点开对答案)
1. 共享内存为什么不需要拷贝? —— **两个进程的页表里填了同一个物理页号**,**所以它们的虚拟地址指向同一块物理内存。** 2. 只读保护是软件做的还是硬件做的? —— **硬件**——页表项里的 R/W 位,**写只读页 CPU 直接抛异常。** 3. 共享、映射、COW、Swap 共同的本质是什么? —— **都是【改页表项】**:**改物理页号、改权限位、改 P 位。**- ☐ 5.12 文件映射
相关笔记
- 虚拟内存 — 机制本身
- 文件映射 — 把文件挂上来
- 写时复制 — 先共享再复制
- 🐧 Linux是怎样工作的 · 索引