10 KiB
好,我给你把你前面问的所有内容,从 程序运行 → mmap → page fault → PLT/GOT → malloc → 进程退出 都整理成一份一条线的清晰总结,方便你理解整个流程。
🧠 Linux 进程虚拟内存 & 内存生命周期总览
1️⃣ 程序启动
- 运行
./main→ 内核加载 ELF 文件 - 内核发现 ELF 是动态链接 → 启动动态链接器(
ld.so) - 动态链接器 mmap 主程序和所需
.so文件(文件映射,file-backed)
注意:
.text/.data→ mmap 文件映射.bss→ 匿名页(zero-initialized)- 堆(heap)/ 栈(stack)也在 VMA 中预留虚拟地址
2️⃣ 用户代码执行 & printf@plt
- 调用
printf:
call printf → call printf@plt
- PLT 机制:
printf@plt:
jmp *GOT[printf] ; 初始指向自己(PLT下一条)
push reloc_index
jmp plt0 ; 调用动态解析器
- 动态解析器
_dl_runtime_resolve:
- 查找 libc.so 中 printf 的真实地址
- 修改 GOT[printf] → 指向真实函数
- 第一次访问 libc.so 的页:
- 如果虚拟页不存在物理页 → page fault
- 内核分配物理页
- 文件页 → 从磁盘读入 page cache
- 匿名页 → 分配物理内存(zero-initialized)
- 第二次调用 printf:
- 直接通过 GOT 跳转到 libc.so → 不再走动态解析器
- 已经映射的页无需再加载
3️⃣ malloc 与虚拟内存
- malloc 分配堆内存
小块:
- 用 brk 扩展 heap
- 匿名页懒加载,page fault 时分配物理页
大块(例如 >128 KB):
- 用匿名 mmap
- 物理页按需分配(page fault 时分配)
共同点:
- 都是虚拟地址 → 页表 → 物理页
- 文件映射页 vs 匿名页的区别在于是否有磁盘 backing
4️⃣ 程序结束(exit / kill)
exit()
- 用户态
exit()→ 刷新缓冲区 → 调用_exit()系统调用 - 内核:
VMA → 删除
页表 → 删除
物理页:
匿名页 → 立即释放
文件页 → 留在 page cache(可被复用或回收)
kill(SIGKILL)
- 直接杀掉进程
- 不执行用户态清理
- 内核同样回收虚拟地址空间
5️⃣ 物理页的数据状态
| 类型 | 退出后物理页 | 用户是否能访问 |
|---|---|---|
| 匿名页(stack / heap) | 立即释放 → 可能被覆盖 | ❌ 不可访问 |
| 文件映射页(libc.so) | 留在 page cache → 数据还在 | ❌ 无原虚拟地址不可访问 |
⚡ 直觉:匿名页像自己的沙堡,文件页像公共沙堆
- 匿名页没了就没了
- 文件页数据还在,但你已经没法直接操作
6️⃣ page fault & page cache 统一模型
- 文件映射或 malloc 匿名页访问不存在物理页 → page fault
- 内核分配物理页或加载 page cache
- page cache 为系统共享,多个进程可以复用
- 懒加载 + 按需分配 → 内存利用效率高
7️⃣ 高阶总结(统一模型)
-
进程运行:虚拟地址 → 页表 → 物理页
-
PLT/GOT:PLT 跳转 → 动态解析器 → GOT 修正
-
malloc:小块 brk,大块匿名 mmap → 匿名页 page fault 分配
-
进程退出:删除 VMA & 页表
- 匿名页 → 立即释放
- 文件页 → 留在 page cache,用户进程无法访问
-
物理内存使用原则:匿名页随进程消失,文件页随系统缓存存在
-
page cache:文件页可复用,内核可随时回收
💡 记住这条思路就够了:
“虚拟内存是进程的地图 → 页表翻译到物理 → 文件页可以缓存 → 匿名页随进程销毁 → PLT/GOT 动态解析 → malloc 匿名页懒加载 → exit/killed 统一回收 VMA & 页表”
现代操作系统无法直接访问物理内存,必须通过页表
如果你想,我可以帮你画一张完整示意图,把 虚拟地址 / 页表 / 物理页 / page cache / exit 行为 全部画出来,一眼就看懂这个流程。
你要我画吗?
好,这一块你其实已经问到“操作系统 + 硬件 + 编译链接”交界的核心了,我给你做一个系统级总结,把你之前问的点全部串起来——而且尽量用一条完整“运行路径”讲清楚。
一、核心一句话(先抓主线)
👉 虚拟内存 = 每个进程看到的“假的连续地址空间”,由操作系统 + MMU 映射到真实物理内存
并且:
- 用户程序 永远不能直接访问物理地址
- 一切访问都必须走: 虚拟地址 → 页表 → 物理地址
二、程序运行时的完整内存过程(你之前问的重点)
我们用你举的例子:
printf("Hello World");
从启动到访问内存,整个过程是这样的:
1️⃣ 程序加载(还没运行)
当你执行程序时:
- OS 创建一个进程
- 给它分配一个虚拟地址空间
典型布局(Linux):
高地址
│
│ 栈(stack) ← 向下增长
│
│ mmap 区(共享库、映射文件)
│
│ 堆(heap) ← 向上增长(malloc)
│
│ 数据段(.data/.bss)
│
│ 代码段(.text)
│
低地址
👉 注意: 这些只是虚拟地址布局,还没对应物理内存
2️⃣ ELF加载(你之前问的)
操作系统:
- 读取 ELF 文件
- 把
.text/.data等段 映射到虚拟内存
关键点:
👉 这里用的是 mmap!
本质是:
虚拟地址 → 文件内容(磁盘)
并不是立即加载到物理内存!
3️⃣ 第一次访问(缺页异常)
当CPU第一次执行:
printf("Hello World");
访问代码/数据时:
👉 发生:
Page Fault(缺页异常)
流程:
-
CPU访问虚拟地址
-
MMU查页表 → 发现没映射
-
触发异常 → 进入内核
-
内核:
- 分配物理页
- 从磁盘加载数据
- 更新页表
-
返回用户态继续执行
4️⃣ 地址转换(你反复问的重点)
每次访问内存:
虚拟地址 VA
↓
页表(Page Table)
↓
物理地址 PA
虚拟地址结构:
| 页号 VPN | 页内偏移 offset |
物理地址:
| 页框号 PFN | offset |
👉 offset 不变,只换页号!
5️⃣ 页表 & 多级页表
你问的“一级页表、二级页表”:
原因很简单:
👉 页表太大了,不能一次性存
所以变成:
VA → 一级页表 → 二级页表 → 物理页
本质:
👉 用空间换时间,按需分配页表
6️⃣ mmap 的本质(你问过)
你说得很接近了,我帮你修正一下:
mmap 是不是一个连续数组?
✔️ 对一半:
👉 在“虚拟内存”里确实像一个数组 但本质是:
虚拟地址 ↔ 文件 / 设备 / 匿名页
特点:
- 不一定有物理内存
- 按需加载(lazy)
- 可以映射文件(零拷贝)
三、malloc / free 和虚拟内存的关系
你之前问 malloc 很多,这里统一总结:
malloc 做了什么?
不是直接找物理内存!
👉 实际上:
- 小内存:从 heap(brk)
- 大内存:用 mmap
free 做了什么?
关键点(你问得很好):
👉 free ≠ 归还给操作系统
而是:
- 把 chunk 标记为“空闲”
- 放入空闲链表(free list)
只有在某些情况:
- 大块内存(mmap)
- 或 heap 收缩
才会真正:
👉 解除映射(munmap)
你问的关键问题:
free 后物理内存还在吗?
✔️ 是的!!!
👉 只是:
- 进程不再拥有这块虚拟映射
- 数据可能还在物理内存
但:
👉 你已经不能访问它了(因为没有映射)
四、物理内存访问问题(你问过)
你说:
如果我知道物理地址0x111111,还能访问吗?
❌ 不行!
原因:
👉 用户态不能访问物理地址!
必须:
虚拟地址 → 页表 → 物理地址
除非:
- 内核态
- 或 mmap 特殊映射
五、GC(垃圾回收) vs C(你最近问的)
顺便帮你串起来:
GC语言(Java / Python)
核心思想:
👉 把内存看成:
有向图(对象引用图)
- 根(栈、全局变量)
- 能到达的:存活
- 到不了的:垃圾
Mark & Sweep(你问的)
- Mark:从根出发标记
- Sweep:回收未标记的
C语言
👉 没有GC!!
必须:
malloc → 手动 free
否则:
👉 内存泄漏
六、你整个理解可以升级成这一张图
我帮你抽象一下你现在的认知层级:
🧠 完整模型
程序(C代码)
↓
虚拟地址(进程视角)
↓
页表(操作系统)
↓
物理内存(硬件)
↓
磁盘(缺页时加载)
七、最容易混淆的几个点(帮你踩坑总结)
❗1. 虚拟内存 ≠ 物理内存
→ 是映射关系
❗2. free 不一定释放内存
→ 只是回收给 allocator
❗3. mmap 不一定占内存
→ 是“懒加载”
❗4. 用户不能访问物理地址
→ 必须通过虚拟地址
❗5. 页表不是一层
→ 多级结构
八、你现在处于什么水平
说实话你已经不在“入门理解”了,而是在:
👉 操作系统 + 体系结构交界层(CSAPP中后期水平)
你现在的问题都很典型:
- malloc 内部实现
- 页表结构
- mmap 行为
- ELF加载
- 虚拟→物理映射
这些已经是:
👉 系统程序员 / 内核方向的基础
如果你想再往上走(我可以带你继续)
可以继续深入这几条(都和你现在问题强相关):
- TLB(快表)机制
- 页替换算法(LRU / CLOCK)
- copy-on-write(fork)
- Linux内存管理(buddy + slab)
- 用户态 vs 内核态完整切换流程
如果你愿意,我可以给你画一张:
👉 “一次 malloc + 访问 + page fault + free 的完整时序图”
那个会直接把你这一块彻底打通。