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