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

1818 lines
29 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.
高地址
┌──────────────────────────────┐
│ 内核空间 (kernel space) │ ← 所有进程共享
│ │
│ - 内核代码 (.text) │
│ - 内核数据 (.data/.bss) │
│ - task_struct / 内核栈 │
│ - 页表 / slab / kmalloc │
│ │
│ ⚠ 用户态不可访问 │
└──────────────────────────────┘
│ (用户/内核分界线典型0x00007fffffffffff
┌──────────────────────────────┐
│ 用户栈 (stack) ↑向下增长 │
├──────────────────────────────┤
│ mmap 区域(共享库 / 文件) │
│ - libc.so │
│ - ld.so │
│ - mmap文件 │
├──────────────────────────────┤
│ 堆 (heap) ↑向上增长 │
│ malloc/new │
├──────────────────────────────┤
│ .bss未初始化全局变量
│ .data已初始化全局变量
│ .rodata只读数据
│ .text代码
└──────────────────────────────┘
低地址
---
【内核空间(高地址)】(所有进程共享)
- 内核代码 / 数据
- task_struct在这里但你访问不到
-------------------------
【用户空间(每个进程独立)】
- stack向下
- mmap共享库 / 文件映射)
- heap向上
- .bss / .data / .rodata
- .text
---
好,这一问已经进入**操作系统理解的核心区了**——我给你讲清楚两件事:
1. 👉 **虚拟地址空间每一块到底在干嘛(彻底拆开)**
2. 👉 **用一个 Fibonacci 程序,从“加载 → 运行 → 调用函数 → 返回”完整走一遍**
我会尽量按你现在的知识体系(偏底层 + CPU视角来讲不搞虚的。
---
# 🧠 一、经典虚拟地址空间:逐块拆解
我们按从低地址 → 高地址来讲(更符合程序执行逻辑)
---
## 1⃣ `.text`(代码段)
👉 存什么?
* 机器指令(编译后的)
* 只读
比如:
```c
int fib(int n) { return n <= 1 ? n : fib(n-1)+fib(n-2); }
```
👉 编译后就在 `.text`
👉 CPU执行
```asm
call fib
```
👉 本质就是:
👉 **从 `.text` 里一条一条取指令执行**
---
## 2⃣ `.rodata`(只读数据)
👉 存什么?
* 字符串常量
* const 数据
比如:
```c
printf("Hello\n");
```
👉 `"Hello\n"` 在这里
---
## 3⃣ `.data`(已初始化全局变量)
```c
int a = 10;
```
👉 存:
* 有初值的全局变量
* 静态变量
👉 在 ELF 里有实际数据
---
## 4⃣ `.bss`(未初始化全局变量)
```c
int b;
```
👉 特点:
* **ELF文件里不占空间**
* 加载时全部置 0
👉 OS做的事情
```c
memset(bss, 0)
```
---
## 5⃣ Heap
👉 动态分配:
```c
malloc()
new
```
👉 特点:
* **向上增长**
* 本质是:
👉 `brk()``mmap()`
👉 glibc 会管理(不是内核直接管 malloc
---
## 6⃣ mmap 区域(重点!)
👉 这里非常关键(你前面问过)
存:
* 动态链接库libc.so
* 文件映射
* 大块 malloc
👉 比如:
```c
printf()
```
👉 实际函数在:
👉 `libc.so` → 映射到 mmap 区
---
## 7⃣ 用户栈stack
👉 你问的关键问题:
> 运行栈是不是用户栈?
**是的!!完全就是同一个东西**
👉 存:
* 函数调用帧stack frame
* 返回地址
* 局部变量
* 参数
👉 特点:
* **向下增长**
---
## 8⃣ 内核空间(高地址)⚠
👉 用户态不能访问
里面有:
* 内核代码
* 页表
* `task_struct`
* 内核栈
👉 每个进程:
✔ 用户空间不同
✔ 内核空间映射相同
---
# 🧠 二、用 Fibonacci 程序跑一遍(重点来了)
我们用这个:
```c
#include <stdio.h>
int fib(int n) {
if (n <= 1) return n;
return fib(n-1) + fib(n-2);
}
int main() {
int x = fib(3);
printf("%d\n", x);
}
```
---
# 🚀 阶段 1程序加载execve
你执行:
```bash
./a.out
```
---
## 🔹 内核做的事:
1. 创建进程fork/exec
2. 创建虚拟地址空间
3. 加载 ELF
| 段 | 放哪 |
| ------- | ---- |
| `.text` | 代码区 |
| `.data` | 数据区 |
| `.bss` | 清零 |
| libc.so | mmap |
👉 建立页表(虚拟 → 物理)
---
# 🚀 阶段 2进入 `main`
CPU开始执行
```asm
_start main
```
---
## 🔹 栈初始化:
用户栈里已经有:
```id="stack_init"
argc
argv
envp
```
---
# 🚀 阶段 3调用 fib(3)
执行:
```c
fib(3)
```
👉 CPU干
```asm
call fib
```
---
## 🔥 栈变化(关键!)
### 初始栈:
```id="stack0"
(main frame)
```
---
### 调用 fib(3)
```id="stack1"
fib(3)
main
```
---
### fib(3) → fib(2)
```id="stack2"
fib(2)
fib(3)
main
```
---
### fib(2) → fib(1)
```id="stack3"
fib(1)
fib(2)
fib(3)
main
```
---
### fib(1) 返回
👉 弹栈
```id="stack4"
fib(2)
fib(3)
main
```
---
👉 然后 fib(2) 调 fib(0)
---
👉 整个过程:
✔ **全部发生在“用户栈”**
---
# 🚀 阶段 4printf 执行
```c
printf("%d\n", x);
```
---
## 🔥 发生了什么?
### 1⃣ 调用 libc
👉 `.text` 没有 printf
👉 跳到:
👉 mmap 区 → `libc.so`
---
### 2⃣ 进入内核
printf 最终会调用:
👉 `write()` 系统调用
---
👉 CPU做
```asm
syscall
```
---
## 🔥 切换:
👉 用户态 → 内核态
---
## 🔹 内核做:
* 找 stdout
* 写入终端
---
👉 返回用户态
---
# 🚀 阶段 5程序结束
```c
return 0;
```
---
👉 调用:
```c
exit()
```
---
👉 内核:
* 回收内存
* 销毁页表
* 销毁 task_struct
---
# 🧠 三、把“空间”和“行为”对上号
| 行为 | 发生在哪 |
| ------- | -------------- |
| 执行代码 | `.text` |
| 常量 | `.rodata` |
| 全局变量 | `.data/.bss` |
| malloc | heap |
| 函数调用 | stack |
| printf | mmap (libc.so) |
| syscall | 内核空间 |
---
# 🧠 四、一个你必须建立的核心模型
👉 程序运行 = 三个东西在互动:
### 1⃣ `.text`
👉 CPU执行指令
### 2⃣ stack
👉 控制流程(函数调用)
### 3⃣ heap / mmap
👉 提供数据
---
# 🧠 五、再回答你一句核心问题(非常关键)
> 运行栈 = 用户栈?
✔ 完全正确
👉 更精确说:
👉 **运行栈 = 当前线程的栈(在用户空间)**
⚠ 注意:
* 每个线程都有自己的栈
* 不止一个栈(多线程)
---
# 🚀 六、如果你想再深入(我建议下一步)
你现在已经可以进阶到:
👉 **页表 + 缺页异常 + mmap 懒加载**
也就是:
👉 fib(3) 根本不会一开始就全部加载进内存
而是:
👉 **访问才加载page fault**
---
如果你愿意,我可以下一步给你讲一个非常爽的东西:
👉 **第一次执行 printf 时libc.so 是怎么被“动态加载 + 重定位 + PLT/GOT解析”的**
这个和你前面问的 GOT/PLT 是一整条线,会直接打通。
---
你这个想法**非常接近真实机制了**,而且已经踩到一个很关键的点:
👉 **“既然 libc.so 已经在内存里了,能不能直接复用?”**
答案是:
👉 **可以复用,但不是你想的那种“直接从某个进程的内存映射过来”**
👉 而是通过一个更底层的东西:**页缓存page cache**
我给你讲清楚这个“微妙但关键”的区别。
---
# 🧠 一句话先定性
👉 **mmap 永远是“映射文件”,不是“映射别的进程的内存”**
👉 但多个进程会**共享同一份物理页(来自 page cache**
---
# 🧠 一、你设想的模型 vs 真实模型
---
## ❌ 你想的模型(直觉版)
```text
进程A:
libc.so → 已经在物理内存
进程B:
mmap → 直接指向进程A的那块内存
```
👉 这其实是:
👉 ❌ **进程间直接共享内存(不是 mmap 的默认行为)**
---
## ✅ 真实模型Linux
```text
磁盘: libc.so
页缓存page cache内核
进程A 映射
进程B 映射
进程C 映射
```
---
👉 关键点:
✔ 所有进程都 mmap “同一个文件”
✔ 内核让它们**指向同一份物理页**
---
# 🧠 二、page cache 是核心
👉 当第一个进程加载 libc.so
```text
page fault
→ 内核从磁盘读 libc.so
→ 放入 page cache
→ 建立页表映射
```
---
👉 当第二个进程也用 libc.so
```text
mmap libc.so
→ 访问时 page fault
→ 发现 page cache 里已经有
→ 直接复用物理页
```
---
👉 结果:
✔ 没有重复读磁盘
✔ 没有重复占用内存
---
# 🧠 三、那你说的“直接从内存映射”本质上是什么?
👉 其实你描述的就是:
👉 **page cache + mmap 的效果**
只是:
| 你以为 | 实际 |
| -------------- | -- |
| 从别的进程内存拿 | ❌ |
| 从 page cache 拿 | ✔ |
---
# 🧠 四、MAP_SHARED vs MAP_PRIVATE 在这里的区别
---
## 🔹 对 `.text`(代码段)
```c
mmap(..., MAP_PRIVATE)
```
👉 但因为是只读:
✔ 实际效果 = 共享
✔ 不会 COW
---
## 🔹 对 `.data`
👉 初始:
✔ 共享 page cache
👉 写时:
✔ 触发 COW
✔ 复制新物理页
---
## 🔹 如果是 MAP_SHARED
👉 修改会:
✔ 写回文件
✔ 所有进程可见
---
# 🧠 五、为什么不能“直接映射别的进程内存”?
👉 因为:
### 1⃣ 安全性
* 进程隔离
* 不能随便访问别人的内存
---
### 2⃣ 生命周期问题
* 进程A退出怎么办
* 内存归谁?
---
### 3⃣ 内核统一管理
👉 page cache 是:
✔ 全局
✔ 可复用
✔ 可回收
---
# 🧠 六、但!你这个想法其实在某种机制里是对的
👉 有一种东西真的可以:
👉 **直接共享内存**
---
## 🔥 那就是:
### 1⃣ `mmap + MAP_SHARED + 同一个文件`
👉 标准共享内存
---
### 2⃣ `shm`(共享内存)
```c
shm_open()
mmap()
```
---
### 3⃣ `memfd_create`(高级玩法)
👉 直接在内存里创建“文件”
---
👉 这些才是:
✔ 真正的“进程间共享内存”
---
# 🧠 七、给你一个最终“通透模型”
---
## 🟢 mmap 的本质
👉 映射的是:
```text
虚拟地址 → 文件(或匿名内存)
```
---
## 🟡 page cache 的作用
👉 让:
```text
多个进程 → 共享同一份物理页
```
---
## 🔴 COW 的作用
👉 写的时候:
```text
共享 → 私有
```
---
# 🧠 八、用一句话总结你的问题(建议背)
👉 **mmap 不是从“已有内存”映射,而是从“文件”映射;多个进程之所以共享,是因为内核用 page cache 复用了同一份物理页**
---
# 🚀 给你一个你现在可以思考的进阶问题
你已经到这个层级了,可以思考这个:
👉 **如果 page cache 里的 libc.so 被回收了(内存紧张),会发生什么?**
提示你一下方向:
* 页被 reclaim
* 页表还在
* 再访问 → page fault
---
如果你想,我可以带你把:
👉 **page cache + LRU + 内存回收机制kswapd**
这一整套讲清楚,这就是 Linux 内存管理的“内功心法”了。
---
当运行./main的时候运行ELF文件查文件头的时候发现需要libc.so于是通过mmap在虚拟内存中圈地。加载剩余的.text.bss/.data到虚拟内存。然后执行代码段第一次执行到call printf的时候执行的其实是call printf@plt此时plt第一条指令是跳转到GOT[x]然后初始GOT[x]跳转到的是PLT的下一条指令而这条指令就是jmp到mmap开辟的虚拟内存映射到的物理内存中。PLT跳转发现内存不存在于是发送page fault到内核。内核加载页并且从磁盘加载libc.so到mmap映射到的那部分内存中。修改GOT[x]指向mmap映射的内存。第二次访问的时候直接跳转GOT即可。
对于已经在内存中加载了的运行库.so在上面发现内存不存在的时候page fault访问到页缓存发现存在缓存则直接调用缓存
你这版已经**80%正确了,而且思路是通的**👍——差的那 20% 主要集中在两个点:
1. **PLT/GOT 的跳转细节(你有一点“想象多了一步”)**
2. **page fault 触发的位置和时机(这个很关键)**
我帮你**逐句订正 + 给你一版“完全正确、可以当标准答案背”的流程**。
---
# 🔧 一、逐句帮你“debug”
我引用你原话,逐条改:
---
## ✅ 第一段(基本正确)
> 当运行 ./main 的时候,运行 ELF 文件,查文件头的时候发现需要 libc.so于是通过 mmap 在虚拟内存中圈地。
👉 ✔ 基本正确,但补充:
* 是内核加载 ELF 后
* **把控制权交给动态链接器ld.so**
* **ld.so 用 mmap 加载 libc.so**
---
## ⚠ 第二段(小问题)
> 加载剩余的 .text.bss/.data 到虚拟内存
👉 ✔ 对,但更精确:
* `.text/.data` 是 **映射mmap**
* `.bss` 是 **匿名页(清零)**
---
## ⚠ 第三段PLT部分有误
> plt第一条指令是跳转到 GOT[x],然后初始 GOT[x] 跳转到的是 PLT 的下一条指令,而这条指令就是 jmp 到 mmap 开辟的虚拟内存映射到的物理内存中
👉 ❌ 这里有一个关键错误:
👉 **PLT 并不会直接跳到 libc**
正确是:
```asm
printf@plt:
jmp *GOT[printf] ← 第一步
push reloc_index ← 第二步
jmp plt0 ← 第三步
```
---
👉 初始:
```text
GOT[printf] = printf@plt + 6
```
---
👉 所以第一次执行:
```text
jmp GOT → 回到 PLT 自己 → push → jmp plt0
```
👉 ❌ 并不会跳到 libc
👉 ✔ 而是跳到 **动态解析器**
---
## ❌ 第四段(最大误区)
> PLT跳转发现内存不存在于是发送page fault到内核
👉 ❌ 这里是**最大错误点**
👉 **PLT 跳转本身不会触发 page fault**
---
👉 page fault 发生在:
✔ 访问 libc 代码页时
✔ 或访问 GOT / 数据页时
👉 ❌ 不是因为“跳转失败”
---
## ⚠ 第五段(部分正确)
> 内核加载页,并且从磁盘加载 libc.so
👉 ✔ 条件成立时正确:
👉 如果 page cache 里没有:
✔ 从磁盘加载
👉 如果有:
✔ 直接用 page cache
---
## ❌ 第六段(小错)
> 修改 GOT[x] 指向 mmap 映射的内存
👉 ❌ 不是内核改的!
👉 ✔ 是:
👉 **动态链接器ld.so修改 GOT**
---
## ✅ 最后一段(很好)
> page fault访问到页缓存发现存在缓存则直接调用缓存
👉 ✔ 完全正确 👍
---
# 🚀 二、给你一版“标准正确流程”(可以当答案用)
我帮你整理成一条**完全正确的执行链**
---
# 🧠 【完整流程:从启动到 printf】
---
## 🟢 阶段 1程序启动
```text
./main
→ execve
```
---
### 内核做:
1. 加载 ELF
2. 发现需要动态链接
3. 启动动态链接器ld.so
---
## 🟢 阶段 2动态链接器初始化
ld.so 做:
* mmap 主程序
* mmap libc.so建立 VMA不一定加载物理页
* 建立 GOT / PLT 初始状态
---
## 🟢 阶段 3执行到 printf
```asm
call printf → call printf@plt
```
---
## 🟢 阶段 4第一次进入 PLT
```asm
printf@plt:
jmp *GOT[printf]
```
---
👉 此时:
```text
GOT[printf] → printf@plt + 6
```
---
👉 所以跳回:
```asm
push reloc_index
jmp plt0
```
---
## 🟢 阶段 5进入动态解析器
```text
plt0 → _dl_runtime_resolve
```
---
👉 动态链接器做:
1. 在 libc.so 中查找 printf
2. 得到地址
---
## 🟢 阶段 6可能发生 page fault关键点
👉 当访问 libc.so 的代码页:
```text
虚拟地址 → 没有物理页 → page fault
```
---
### 内核处理:
* 查 VMA来自 mmap
* 查 page cache
---
👉 分两种:
### ✔ 情况 A缓存存在
```text
直接映射物理页
```
---
### ✔ 情况 B缓存不存在
```text
从磁盘读取 libc.so → 放入 page cache → 映射
```
---
## 🟢 阶段 7修复 GOT关键
👉 动态链接器:
```c
GOT[printf] = libc中printf地址
```
---
## 🟢 阶段 8跳转到真正 printf
👉 开始执行 libc.so 中的代码
---
## 🟢 阶段 9第二次调用 printf
```asm
call printf@plt
→ jmp *GOT[printf]
→ 直接跳 libc
```
---
👉 ❌ 不再走解析器
👉 ❌ 不再走 PLT fallback
---
# 🧠 三、帮你总结“关键修正点”
---
## 🔥 你要记住的 5 个核心点
---
### 1⃣ libc.so 早就 mmap 了
👉 ❌ 不是第一次 printf 才加载
👉 ✔ 启动时就建立映射
---
### 2⃣ PLT 不负责找地址
👉 ✔ PLT = 跳板
👉 ✔ ld.so = 真正解析
---
### 3⃣ GOT 不是空
👉 ✔ 初始指向 PLT 自己
---
### 4⃣ page fault ≠ 跳转失败
👉 ✔ 是访问未加载页
👉 ❌ 不是因为“地址不存在”
---
### 5⃣ GOT 是用户态改的
👉 ✔ ld.so 修改
👉 ❌ 内核不管
---
# 🧠 四、你现在的理解水平(实话)
你现在已经:
✅ 理解虚拟内存
✅ 理解 mmap
✅ 理解 page cache
✅ 理解 PLT/GOT
👉 差的只是:
👉 **“谁在什么时候做什么”这个时间顺序的精度**
---
# 🚀 如果你再往前一步(我强烈建议)
下一步你可以直接进入:
👉 **fork() + COW + 页表复制**
或者更狠一点:
👉 **自己用 gdb + objdump 追踪一次 printf@plt 执行**
我可以带你一步一步把汇编跑出来,那会非常爽,直接“从理解变成掌控”。
---
malloc = 在用户空间的“堆heap或 mmap 区域”分配虚拟内存,
小块通过 brk 扩展 heap大块通过 mmap 创建匿名映射,
实际物理内存通过 page fault 按需分配
VMA → 删除
页表 → 删除
物理页:
匿名页 → 释放
文件页 → 留在 cache你这段总结已经**非常接近“内核真实行为”了**,可以说 90% 正确 👍
但有两个地方需要**精确修正一下**,不然以后在 page cache / 回收策略这里会卡住。
---
# ✅ 先给你一个“修正版结论”(可以直接记)
```text
进程退出时:
VMA → 删除
页表 → 删除
物理页:
匿名页heap/stack → 立即释放
文件页mmap 文件) → 留在 page cache可能被回收
```
👉 你写的整体结构 ✔ 完全正确
👉 下面我帮你把**两个关键细节补严谨**
---
# 🧠 一、你这句话的第一个问题
> 物理内存我不管,里面的东西保留,直到下一次重新加载文件到该物理内存区域
👉 ❌ 这里有个“时间理解错误”
---
## ❗ 正确是:
👉 **不是“保留到下一次加载”**
👉 而是:
👉 **保留在 page cache直到内核决定回收它**
---
## 🔥 内核不会保证:
```text
下一次一定还在同一个物理页
```
---
👉 实际是:
* 有内存 → 留着(加速)
* 内存紧张 → 被回收LRU
---
👉 所以更准确说法是:
```text
文件页可能继续留在内存中page cache
也可能被内核回收,并不是等“下一次加载”才处理
```
---
# 🧠 二、你这句话的第二个细节
> 删除 mmap 建立的虚拟 → 物理映射
👉 ✔ 对,但再精确一点:
---
## 🔹 删除的其实是:
```text
虚拟地址 → 物理页 的映射(页表项 PTE
```
---
👉 不是:
❌ “删除 mmap 本身”
✔ 而是:
```text
VMA 删除 + 页表清空
```
---
# 🧠 三、帮你把“文件页”的生命周期讲清楚(关键)
---
## 🟢 阶段 1程序运行中
```text
libc.so → mmap
→ page fault
→ 加载到 page cache
→ 建立页表映射
```
---
## 🟡 阶段 2进程退出
```text
页表删除
→ 引用计数 -1
```
---
👉 如果:
```text
没有进程再用
```
---
👉 页会:
```text
留在 page cache暂时
```
---
## 🔴 阶段 3内存紧张时
👉 内核kswapd
```text
回收 page cache
→ 丢弃干净页clean page
```
---
👉 如果是脏页:
```text
写回磁盘再释放
```
---
# 🧠 四、匿名页为什么直接释放?
👉 因为:
```text
heap / stack → 没有“文件来源”
```
---
👉 所以:
```text
只能:
→ 直接 free 物理页
```
---
👉 对比一下:
| 类型 | 来源 | 能否缓存 |
| --- | ---- | ---- |
| 文件页 | 磁盘文件 | ✔ |
| 匿名页 | 无 | ❌ |
---
# 🧠 五、你现在的理解可以升级成这个模型
---
## 🟢 文件映射libc.so
```text
磁盘 ↔ page cache ↔ 进程页表
```
---
👉 进程退出:
```text
页表断开
page cache 还在
```
---
---
## 🔴 匿名内存heap/stack
```text
进程页表 ↔ 物理页
```
---
👉 进程退出:
```text
直接释放
```
---
# 🧠 六、帮你把原话“升维改写”
你原话:
```text
删除虚拟→物理映射,物理内存保留直到下次加载
```
---
👉 更精确版本:
```text
进程退出时会删除所有虚拟地址到物理页的映射,
匿名页会立即释放,
文件页会留在 page cache 中,
由内核根据内存压力决定是否回收,
而不是等到下一次加载才处理
```
---
# 🧠 七、一个非常关键的直觉(建议你记住)
👉 **page cache 不是“为某个进程服务”,而是“为整个系统服务”**
---
👉 所以:
✔ 进程死了,缓存还在
✔ 其他进程可以复用
✔ 内核可以随时回收
---
# 🚀 你现在已经到了一个很高的层级了
你现在其实已经掌握了:
* mmap 本质
* page fault
* page cache
* 进程退出回收机制
---
👉 下一步你可以直接挑战一个“内核级核心问题”:
👉 **LRU 是怎么决定回收哪一页的active/inactive list 是怎么工作的?**
这个一通,你对 Linux 内存管理就基本“内功圆满”了。
---
你这个问题问到**本质核心了**,这一点一旦想通,前面所有 mmap / page fault / cache 全都串起来。
我给你用**最直白 + 严谨**的方式讲清楚👇
---
# 🧠 一、结论先说死
## ✅ 用户程序 **不能直接访问物理地址**
👉 必须经过:
```text
虚拟地址 → 页表 → 物理地址
```
---
👉 你写的代码:
```c
int *p = malloc(4);
*p = 123;
```
👉 CPU 实际干的是:
```text
虚拟地址 p
→ 查页表MMU做的
→ 找到物理页 + offset
→ 写入物理内存
```
---
❗ **用户态根本没有“访问物理地址”的能力**
否则会出现:
* 读别的进程数据
* 改内核数据
* 系统直接崩
👉 所以这是操作系统最核心的隔离机制
---
# 🧠 二、那“物理页”到底是什么?
你可以这样理解:
---
## 🔹 物理内存长这样(简化)
```text
RAM:
[4KB][4KB][4KB][4KB][4KB]...
↑ ↑ ↑
页0 页1 页2
```
---
👉 每一个 **4KB块**
```text
就叫物理页physical page
```
---
## 📌 定义一句话:
```text
物理页 = 物理内存中最小的分配单位通常4KB
```
---
👉 比如:
```text
物理地址 0x111111
```
其实是:
```text
页号 + 页内偏移
```
---
## 🔹 举个具体例子
```text
虚拟地址0x7fff12345678
```
CPU做的事
1. 拆分:
```text
页号 + offset
```
2. 查页表:
```text
页号 → 物理页(比如 0x111000
```
3. 拼起来:
```text
物理地址 = 0x111000 + offset
```
---
# 🧠 三、为什么必须走虚拟地址?
这是操作系统的“核心设计”:
---
## 🔒 1. 安全隔离
进程 A
```text
虚拟地址 0x1000 → 物理页 A
```
进程 B
```text
虚拟地址 0x1000 → 物理页 B
```
👉 地址一样,但物理完全不同
---
## 🔥 结果:
```text
两个进程互相看不到对方
```
---
## 🧠 2. 灵活管理
内核可以:
* 随便换物理页swap / compact
* 做写时复制COW
* 做 mmap
* 做共享库
👉 程序完全不知道
---
## ⚡ 3. 支持“看起来连续”的内存
你 malloc
```text
0x1000
0x2000
0x3000
```
👉 实际物理可能是:
```text
0x111000
0x9a3000
0x004000
```
👉 完全不连续,但你感觉是连续的
---
# 🧠 四、那我能不能访问物理地址?
👉 分情况:
---
## ❌ 用户态
```text
不行 ❌
```
* 没指令权限
* 没页表控制权
---
## ✅ 内核态Linux内核
可以:
* 直接操作物理页
* 管理页表
* 分配内存
---
## ⚠️ 特殊情况(你可能听过)
### 1. `/dev/mem`
👉 可以访问物理内存(但现在基本被禁)
---
### 2. 驱动 / mmap 设备
👉 内核帮你映射:
```text
物理地址 → 用户虚拟地址
```
👉 你还是用“虚拟地址访问”
---
# 🧠 五、把你刚才那个问题彻底收束
你问:
> 物理地址0x111111里面的数据还在吗
👉 正确答案:
---
## ✔ 数据“可能还在”
* 如果页没被覆盖
* 如果是 page cache
---
## ❌ 但你“永远不能直接访问它”
因为:
```text
你没有虚拟地址 → 映射不到它
```
---
👉 就像:
```text
银行金库里有钱(物理页)
但你没有账户(虚拟地址)
```
---
# 🧠 六、最终统一模型(非常重要)
```text
用户程序
虚拟地址
↓(页表 / MMU
物理页4KB块
RAM
```
---
👉 所有东西:
* malloc
* mmap
* libc.so
* stack
本质都是:
```text
虚拟地址 → 映射到某些物理页
```
---
# 🚀 你现在已经踩到“操作系统核心三件套”了
你已经在理解:
* 虚拟内存
* 页表
* 物理页
* page fault
* mmap
* COW
---
👉 下一步如果你继续深入,会自然进入:
👉 **TLB快表是怎么加速页表的**
👉 这个一通,你基本就是“系统级选手”了 😄
---
这个问题切中了 Linux 内核执行程序的“第一现场”。
当你在终端输入 `./main` 并按下回车时,内核并不是直接运行你的二进制代码,而是经历了一场从 **`execve` 系统调用** 开始的“虚拟内存大拆迁与大建设”。
---
### 1. 谁发起的 mmap
在 Linux 中,分配虚拟内存的动作主要发生在两个阶段:**内核态阶段**(映射程序本身)和**用户态阶段**(映射动态库)。
#### **第一阶段:内核的“强拆与重建”**
当你调用 `execve` 时,内核里的 `do_execve` 函数开始工作:
1. **清空旧领地**:它会把当前进程(之前是 Bash原来的虚拟内存布局全部释放掉Unmap
2. **映射 ELF 段**:内核读取 `main` 文件的程序头表Program Header Table
* 内核会针对 ELF 中的 `LOAD` 段(通常是 `.text` 和 `.data`)调用内部的 `mmap` 函数。
* **此时,虚拟内存开始分配。** 比如,它把文件偏移 0 处的内容映射到虚拟地址 `0x400000`(这只是个例子,现代系统有 ASLR 地址随机化)。
3. **映射堆栈**:内核还会自动 `mmap` 出一块区域作为**用户栈**。
#### **第二阶段:动态链接器的“圈地”**
如果你的程序是动态链接的(绝大多数情况),内核在映射完 `main` 之后,会发现需要一个叫 `ld-linux.so` 的解释器。
1. 内核把控制权交给**动态链接器**。
2. 链接器读取 `main` 的依赖列表(比如需要 `libc.so`)。
3. 链接器亲自调用 `mmap` 系统调用,在虚拟内存的中间区域(通常是栈和堆之间的空隙)给 `libc.so` 找个位置。
---
### 2. 内存分配的层级(从上往下看)
你可以把这个过程看作一个“俄罗斯套娃”:
1. **用户代码**:调用 `malloc(100)`。
2. **C 库 (Glibc)**:发现手里没余粮了,调用系统调用 `brk` 或 `mmap`。
3. **内核系统调用接口**:接收到 `mmap` 请求。
4. **内核内存管理 (VMA)**:在进程的虚拟地址空间里找一段空白区域,塞进去一个 `vm_area_struct` 结构体(这就算是“分配”了虚拟内存)。
5. **MMU/页表**:此时页表还是空的。直到你第一次读写这块内存,触发 **Page Fault**,内核才真正去翻“物理页”的牌子。
---
### 3. 总结mmap 的起点
如果你问的是**时间点**
* **最早起点**:在 `execve` 系统调用执行过程中,内核加载 ELF 文件头之后,就开始为程序的代码段进行第一次 `mmap` 映射了。
如果你问的是**空间位置**
* **代码段**通常在较低的地址。
* **共享库**(通过 `mmap` 显式调用的)通常从虚拟地址空间的中间某处开始向下或向上生长。
---
### 验证一下?
你可以写一个最简单的 `hello world`,然后在 Fedora 上运行:
```bash
strace ./main 2>&1 | grep mmap
```
你会惊奇地发现,即便你程序里一行 `mmap` 都没有写,输出里也会有一大堆 `mmap(...)`。这些全都是内核和动态链接器在帮你“打江山”。
**既然你对 `mmap` 分配内存的起始点这么感兴趣,想不想聊聊 `mmap` 分配出来的虚拟地址,是怎么通过“多级页表”这种像树一样的结构最终找到物理内存的?**
---