Files
Operating-System/Memory/conclude.md
2026-03-22 17:40:44 +08:00

548 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
好,我给你把**你前面问的所有内容**,从 `程序运行 → 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可被复用或回收
```
### killSIGKILL
* 直接杀掉进程
* 不执行用户态清理
* 内核同样回收虚拟地址空间
---
## 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 做了什么?
不是直接找物理内存!
👉 实际上:
* 小内存:从 **heapbrk**
* 大内存:用 **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-writefork**
4. **Linux内存管理buddy + slab**
5. **用户态 vs 内核态完整切换流程**
---
如果你愿意,我可以给你画一张:
👉 “一次 malloc + 访问 + page fault + free 的完整时序图”
那个会直接把你这一块彻底打通。