------ 小dora 操作系统学习笔记Vol.3 · 内存管理特辑
"小dora 看着
0x7ffdec42
一脸疑惑:'这玩意儿真的是内存地址?怎么和隔壁老李的进程一样?'"操作系统默默笑了: "你看到的地址,全是假的。"
🎯 前情提要:为什么操作系统要骗人?
因为在没有虚拟内存之前,程序员写程序像在抢地盘------
- 谁先上,谁先占;
- 想多用点内存?抱歉没了;
- 想多个程序同时跑?彼此踩脚。
于是,操作系统说: "别打了,我给你们每人发一个'虚拟王国'。"
🧱 一、虚拟内存是啥?------ 一场优雅的集体幻觉
📌 巧记口诀:
"地址是假的,运行是真的;彼此隔离,安全高效。"
虚拟内存(Virtual Memory)是一套机制,让每个进程:
- 拥有独立的地址空间
- 访问受保护、不会互相干扰
- 感觉"内存无限大"
- 实际背后是 OS + 硬件 + 魔法三连
🧭 二、地址空间结构:每个进程都住在「豪华假别墅」
📦 Linux 的典型地址空间长这样:
diff
高地址
↓
+----------------------+
| 栈 Stack(向下增长)|
+----------------------+
| mmap 动态映射区 |
+----------------------+
| 堆 Heap(向上增长) |
+----------------------+
| 数据段(静态变量) |
+----------------------+
| 代码段(.text) |
+----------------------+
低地址
📌 巧记口诀:
"代码底、数据中,堆向上、栈向下。"
🧮 三、分页机制:把内存切片成「页」方便管理
📌 巧记口诀:
"虚拟地址分两段,页号偏移要记全。"
-
虚拟地址被切成:
- 页号:决定查哪张页表
- 页内偏移:定位页内具体位置
-
每一页常见大小为 4KB(2¹²)
-
每个页映射一个物理页框
🧱 四、多级页表:为啥不能只用一张页表?
📌 巧记口诀:
"页表太大放不下,分级索引省空间。"
以 x86-64 为例(48 位虚拟地址):
sql
9 + 9 + 9 + 9 + 12
PML4 | PDPT | PD | PT | offset
- 每级页表存 512 项(2⁹)
- 每次内存访问需要 最多查 4 次页表
🧠 为什么这样设计?
- 懒加载:只建访问过的页表路径
- 节省空间:没必要为 256TB 的地址建全表
🏎️ 五、TLB:翻表太慢?操作系统说"记下来!"
📌 巧记口诀:
"翻表慢,TLB 缓;缓存页号,快如闪电。"
TLB(Translation Lookaside Buffer)是MMU 内建的小型高速缓存,缓存最近访问的「虚拟页号 ➝ 物理页号」映射。
- 命中:一步搞定
- 未命中:回页表查 ➝ 填 TLB
📌 问题来了:
- TLB 有限(几十到上千条)
- 上下文切换后,TLB 清空 ➝ 重新填充
- 高并发 / 大数据结构访问 ➝ 容易 TLB miss
🧠 六、页表项详解:那些神秘的控制位到底干嘛的?
以 x86 为例,每个页表项(PTE)结构如下:
css
| P | R/W | U/S | A | D | COW | Frame Addr |
位 | 含义 |
---|---|
P | Present(页是否存在) |
R/W | 可读写权限 |
U/S | 用户 or 内核访问 |
A | 是否访问过(用于 LRU) |
D | 是否修改过(dirty) |
COW | 是否延迟复制标记 |
Frame Addr | 高位表示物理页框地址 |
🧨 七、Copy-On-Write:fork 不是真复制!
📌 巧记口诀:
"fork 是复制影分身,写时再动真格。"
fork()
时父子共享页面,均设为只读、打上 COW 标记- 任意一方试图写入 ➝ Page Fault
- OS 拷贝页面 ➝ 修改页表映射 ➝ 解除只读限制
优点:
- 快速 fork
- 减少内存浪费
- Linux 的
vfork()
、Python 的多进程都靠这招
🧬 八、缺页异常与页面置换:你访问的页我还没准备好!
📌 缺页异常(Page Fault)触发流程:
- 访问页未在内存
- CPU 抛出异常 ➝ 进入 OS
- 查磁盘 ➝ 找到页 ➝ 加载到物理内存
- 更新页表 ➝ 恢复进程
📌 页面置换算法:
"LRU 常用不换,FIFO 谁先来谁走。"
算法 | 描述 |
---|---|
FIFO | 先进先换 |
LRU | 最近最少使用换出 |
CLOCK | 模拟时钟 + 引用位,效率高 |
🔍 九、堆 & mmap 的真正分配机制
malloc()
小分配 ➝ brk 系统调用(调整 heap)- 大对象 ➝
mmap()
分配匿名页 - 栈空间按需增长(page fault + guard page)
🔐 十、内核空间映射与安全隔离
📌 Linux 使用高半区映射策略
- 低地址空间(0x000... 到 0xC000...):用户态
- 高地址空间:内核页表映射(用户不可访问)
📌 KASLR(内核地址空间布局随机化)防止攻击者利用固定偏移
🧪 面试级练习题
Q1:fork 后父子进程共享内存吗?为什么?
✅ 共享物理内存,但页表不同,写操作会触发 COW
Q2:TLB miss 会引发 Page Fault 吗?
✅ 不会,TLB miss ➝ 查页表;只有页表项无效时才会 Page Fault
Q3:为什么进程间的地址空间都从 0 开始?这不会冲突吗?
✅ 虚拟地址是每个进程独立的一套,物理地址由页表映射,不会冲突
Q4:访问一个未被 mmap 的地址会怎样?
✅ 会引发 Page Fault,系统发现非法访问,抛出 SIGSEGV
🧠 一句话总结
"你看到的内存地址都是假的,但操作系统却把它们变成了每个程序的真世界。"