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

10 KiB
Raw Permalink Blame History

好,我给你把你前面问的所有内容,从 程序运行 → 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
call printf  call printf@plt
  1. PLT 机制:
printf@plt:
    jmp *GOT[printf]     ; 初始指向自己PLT下一条
    push reloc_index
    jmp plt0             ; 调用动态解析器
  1. 动态解析器 _dl_runtime_resolve
  • 查找 libc.so 中 printf 的真实地址
  • 修改 GOT[printf] → 指向真实函数
  1. 第一次访问 libc.so 的页:
  • 如果虚拟页不存在物理页 → page fault
  • 内核分配物理页
  • 文件页 → 从磁盘读入 page cache
  • 匿名页 → 分配物理内存zero-initialized
  1. 第二次调用 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可被复用或回收

killSIGKILL

  • 直接杀掉进程
  • 不执行用户态清理
  • 内核同样回收虚拟地址空间

5 物理页的数据状态

类型 退出后物理页 用户是否能访问
匿名页stack / heap 立即释放 → 可能被覆盖 不可访问
文件映射页libc.so 留在 page cache → 数据还在 无原虚拟地址不可访问

直觉:匿名页像自己的沙堡,文件页像公共沙堆

  • 匿名页没了就没了
  • 文件页数据还在,但你已经没法直接操作

6 page fault & page cache 统一模型

  • 文件映射或 malloc 匿名页访问不存在物理页 → page fault
  • 内核分配物理页或加载 page cache
  • page cache 为系统共享,多个进程可以复用
  • 懒加载 + 按需分配 → 内存利用效率高

7 高阶总结(统一模型)

  1. 进程运行:虚拟地址 → 页表 → 物理页

  2. PLT/GOTPLT 跳转 → 动态解析器 → 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 映射到真实物理内存

并且:

  • 用户程序 永远不能直接访问物理地址
  • 一切访问都必须走: 虚拟地址 → 页表 → 物理地址

二、程序运行时的完整内存过程(你之前问的重点)

我们用你举的例子:

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缺页异常

流程:

  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

必须:

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 的完整时序图”

那个会直接把你这一块彻底打通。