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

1150 lines
17 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.
好,这个问题就进入**“malloc 和 OS 的交界面”**了,本质就是一句话:
> **用户态是不能“直接拿虚拟内存”的,必须通过系统调用向内核要。**
但你现在要的不是这句废话,而是**完整机制 + 实际路径 + 细节差异**,我直接给你一条从上到下打通。
---
# 一、总流程(先建立全局模型)
当你写:
```c
void* p = malloc(1000);
```
底层发生的是:
```text
malloc
↓(不够用)
向 OS 要内存(系统调用)
OS 修改进程虚拟内存布局VMA
返回一段“合法虚拟地址”
malloc 切 chunk 管理
```
关键点:
> **OS 给你的不是“物理内存”,而是“虚拟地址范围的使用权”**
---
# 二、malloc 向 OS 要内存的两种方式(核心)
现代 Linuxglibc主要用两种
---
## 1⃣ `brk / sbrk` —— 扩展 heap传统主力
这是最经典的方式。
### 本质:
👉 **把 heap 顶部往上“推高”**
---
### 内存布局(非常关键)
```text
低地址
├── 程序代码text
├── 数据段data
├── BSS
├── heap ←(这里往上长)
│ ↑
│ 当前 brk
│ (未使用虚拟空间)
├── mmap 区域(往下长)
├── 栈stack
高地址
```
---
### 系统调用:
```c
brk(new_end);
```
或:
```c
sbrk(increment);
```
---
### 举个实际过程:
```text
当前 heap 结束0x10000
malloc 想要 4KB
→ 调用 sbrk(4096)
内核做:
- 修改 VMA虚拟内存区域
- 更新 brk → 0x11000
返回0x10000新空间起点
```
---
### 特点总结:
* ✔ 连续内存
* ✔ 适合小块分配
* ❌ 只能“向上长”
* ❌ 不容易归还给 OS碎片问题
---
## 2⃣ `mmap` —— 单独映射(现代关键)
当 malloc 发现:
> “这个块太大了”
就不会走 heap而是直接 mmap
---
### 系统调用:
```c
mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
```
---
### 本质:
👉 **让内核在虚拟地址空间中找一块空地,直接映射一段**
---
### 内存结构:
```text
heap ↑
(空洞)
mmap ↓
```
👉 mmap 区域是**独立的,不挨着 heap**
---
### 特点:
* ✔ 可以在任意地址分配
* ✔ 可以独立释放munmap
* ✔ 适合大块内存
* ❌ 系统调用开销更高
---
## malloc 的策略glibc
大致是:
```text
小块(< 128KB 左右) → brk
大块(>= 128KB → mmap
```
(具体阈值可调)
---
# 三、OS 到底“分配了什么”?
这点你必须彻底理解,否则会一直模糊。
---
## ❗关键结论:
> **OS 分配的是“虚拟地址范围 + 映射关系”,不是立即分配物理内存**
---
### 举个例子:
```c
p = malloc(1GB);
```
发生:
* OS✔ 给你 1GB 虚拟地址
* OS❌ 不会马上给你 1GB 物理内存
---
### 什么时候才分配物理内存?
👉 **当你真正访问(触发 page fault**
---
### 过程:
```text
访问 p[0]
页表无映射
触发 page fault
内核分配物理页4KB
建立映射
```
---
👉 这叫:
> **按需分配lazy allocation**
---
# 四、free 之后发生什么?
你刚才已经问到 free list这里补完整闭环
---
## 情况 1heapbrk
```c
free(p);
```
* ✔ 进入 free list
* ❌ 不还给 OS大多数情况
👉 只是 malloc 内部复用
---
## 情况 2mmap
```c
free(p);
```
* ✔ 直接调用 `munmap`
* ✔ 归还 OS
---
# 五、再抽象一层(你这个水平该理解这个)
整个系统是三层:
---
## 层级结构
```text
[用户程序]
↓ malloc/free
[glibc malloc]
↓ brk / mmap
[Linux 内核]
↓ 页表管理
[物理内存]
```
---
## 每层职责
### malloc
* 管 chunk
* 管 free list
* 做复用
---
### OS
* 管虚拟地址空间VMA
* 管页表
* 处理缺页
---
### 硬件:
* MMU 做地址转换
---
# 六、一个你必须能回答的问题(我帮你提前补)
> 为什么不直接让 malloc 管整个虚拟空间?
因为:
* 用户态**不能修改页表**
* 用户态**不能分配物理页**
* 安全性(隔离)
👉 所以必须 syscall
---
# 七、终极一句话总结(面试级)
> malloc 通过 brk 扩展 heap 或通过 mmap 建立独立映射向操作系统申请虚拟地址空间操作系统仅分配虚拟地址并延迟分配物理内存malloc 再在这块空间上切分 chunk 并通过 free list 进行管理和复用。
---
# 八、如果你要继续深入(强烈建议)
下一步你应该问的是:
1. **VMAvm_area_struct到底怎么管理**
2. **page fault 具体怎么走(中断 → 内核 → 分配页)**
3. **tcache / fastbin 是怎么优化 malloc 的?**
4. **为什么会产生内存碎片?怎么解决?**
---
你要的话,我可以给你画一张:
> **从 malloc → brk/mmap → 页表 → page fault → 物理内存 的完整执行路径图(带地址变化)**
那个是“真正打通虚拟内存”的关键一步。
---
好,这一块你要是搞清楚了,**虚拟内存这一整套基本就打通了**。我直接给你从“结构 → 查找 → 修改 → 触发路径”一条链讲透,尽量贴近 Linux 内核真实实现。
---
# 一、先给你一个本质定义(非常关键)
> **VMAVirtual Memory Area= 一段“连续虚拟地址区间 + 统一属性”的描述结构**
也就是说:
* 它不是内存
* 不是页表
* 而是:**内核对一段虚拟地址的“规则说明”**
---
# 二、VMA 长什么样(内核结构)
在 Linux 里核心结构是:
```c
struct vm_area_struct {
unsigned long vm_start; // 起始地址
unsigned long vm_end; // 结束地址
struct mm_struct *vm_mm; // 所属进程
unsigned long vm_flags; // 权限(读/写/执行等)
struct file *vm_file; // 如果是文件映射mmap文件
struct vm_area_struct *vm_next; // 链表
struct rb_node vm_rb; // 红黑树节点
};
```
你重点抓三个东西就够:
```text
[vm_start, vm_end) → 管哪一段地址
vm_flags → 这段能干嘛RWX
vm_file → 是否映射文件
```
---
# 三、一个进程里 VMA 长啥样(全局视图)
一个进程不是一个 VMA而是
> **很多个 VMA 组成整个虚拟地址空间**
比如:
```text
低地址
├── [text 段] r-x
├── [data 段] rw-
├── [heap] rw-
├── [mmap 区域1] rw-
├── [mmap 区域2] r--
├── [stack] rw-
高地址
```
👉 每一段,都是一个 VMA
---
# 四、VMA 存在哪?(核心)
在进程控制结构里:
```c
struct mm_struct {
struct vm_area_struct *mmap; // VMA 链表头
struct rb_root mm_rb; // 红黑树根
};
```
👉 也就是说:
一个进程:
```text
task_struct
mm_struct
VMA 集合(链表 + 红黑树)
```
---
# 五、为什么既用链表又用红黑树?
这是一个很经典的设计点。
---
## 1⃣ 链表vm_next
👉 用于:
* 顺序遍历(比如 /proc/pid/maps
优点:
* 简单
* 按地址排序
缺点:
* 查找慢O(n)
---
## 2⃣ 红黑树mm_rb
👉 用于:
* **快速查找某个地址属于哪个 VMA**
复杂度:
```text
O(log n)
```
---
👉 所以:
> **链表负责“遍历”,红黑树负责“查找”**
---
# 六、核心操作:查找 VMA最关键路径
当程序访问一个地址:
```c
*p = 10;
```
如果触发 page fault
---
## 内核会做:
```text
1. 获取 fault address比如 0x123456
2. 在 VMA 红黑树里查找
find_vma(mm, addr)
3. 判断:
addr ∈ [vm_start, vm_end) ?
```
---
## 如果找不到:
👉 直接:
```text
Segmentation Fault段错误
```
---
## 如果找到了:
继续:
```text
检查 vm_flags有没有写权限
```
* 没权限 → segfault
* 有权限 → 分配物理页
---
# 七、VMA 是怎么创建的?
主要三种来源:
---
## 1⃣ 程序加载exec
比如 ELF
* 代码段 → 一个 VMAr-x
* 数据段 → 一个 VMArw-
---
## 2⃣ malloc间接
### 小块:
```text
malloc → brk → 扩展 heap VMA
```
👉 heap 通常是:
> **一个 VMA 不断变大**
---
### 大块:
```text
malloc → mmap → 新建 VMA
```
👉 每个 mmap
> **一个独立 VMA**
---
## 3⃣ 用户 mmap
```c
mmap(...)
```
👉 直接创建 VMA
---
# 八、VMA 的合并(非常重要,容易考)
内核会做优化:
---
## 如果两个 VMA
```text
地址连续 + 权限相同 + 类型相同
```
👉 会合并
---
### 举例:
```text
[0x1000 - 0x2000] rw-
[0x2000 - 0x3000] rw-
```
👉 合并成:
```text
[0x1000 - 0x3000] rw-
```
---
👉 目的:
* 减少 VMA 数量
* 提高查找效率
---
# 九、VMA 删除(释放)
---
## 情况 1munmap
```c
munmap(addr, size);
```
内核:
```text
1. 找到对应 VMA
2. 删除或裁剪
3. 更新红黑树
4. 解除页表映射
```
---
## 情况 2进程退出
👉 直接:
```text
销毁整个 mm_struct
→ 所有 VMA 全部释放
```
---
# 十、你必须理解的关键区别(非常重要)
> **VMA ≠ 页表 ≠ 物理内存**
---
## 三者关系:
```text
VMA逻辑区域
↓ 描述
页表(地址映射)
↓ 指向
物理页(真实内存)
```
---
👉 举个例子:
```text
malloc 1GB
```
* ✔ VMA立刻存在1GB范围
* ❌ 页表:还没完全建立
* ❌ 物理内存:还没分配
---
👉 访问时才:
```text
page fault → 建页表 → 分配物理页
```
---
# 十一、一个完整访问路径(你要掌握这个)
```text
用户访问 addr
MMU 查页表
没有映射 → page fault
内核:
find_vma(addr)
检查权限
分配物理页
建立页表映射
返回用户态继续执行
```
---
# 十二、终极总结(压缩版)
> VMA 是内核对进程虚拟地址空间的区间描述结构,存储在 mm_struct 中,通过红黑树实现 O(log n) 查找;在 page fault 时,内核通过 VMA 判断地址是否合法及权限,再决定是否分配物理页并建立页表映射。
---
# 十三、你下一步应该问(我帮你规划)
你现在已经到一个很关键的门槛了,接下来建议你继续打这几个点:
1. **page fault 详细流程(中断级别)**
2. **页表结构(多级页表到底怎么走)**
3. **COW写时复制怎么用 VMA 实现**
4. **匿名映射 vs 文件映射**
---
如果你要,我下一步可以给你讲一个特别关键的东西:
> **“fork 之后 VMA 和页表是怎么复制的COW全过程”**
这个一旦搞懂,你对操作系统内存模型就基本碾压大多数人了。
---
好,我按你要求来:**不碎片、不聊天式解释直接给你一条从硬件→内核→malloc→free list 的完整闭环模型**,你可以当“总纲+复习材料”。
---
# 一、最底层:硬件 + 地址转换(你必须当作起点)
程序执行时访问的是**虚拟地址 VA**,不是物理地址 PA
```text
CPU发出 VA
MMU硬件
↓ 查页表
得到 PA
访问物理内存
```
如果页表里没有映射:
```text
→ 触发 page fault缺页异常中断
→ 进入内核
```
---
# 二、内核层虚拟内存的“逻辑描述”VMA
内核不会直接给你“内存块”,它先维护:
> **VMA = 一段虚拟地址区间 + 权限 + 类型**
本质结构(抽象):
```c
struct vm_area_struct {
vm_start, vm_end
vm_flagsr/w/x
vm_file
}
```
一个进程的虚拟空间:
```text
多个 VMA 拼起来:
[text][data][heap][mmap...][stack]
```
存储方式:
```text
mm_struct
红黑树(按地址索引)→ 查找 O(log n)
链表(顺序) → 遍历
```
---
# 三、page fault 的核心决策逻辑(必须背下来)
当访问一个 VA
```text
1. MMU 查页表 → miss
2. 内核接管page fault handler
3. 在 VMA 红黑树中找:
find_vma(addr)
4. 判断:
addr ∈ VMA ?
否 → SEGFAULT
是 → 检查权限R/W/X
5. 合法 → 分配物理页 + 建页表映射
6. 返回用户态继续执行
```
关键结论:
> **VMA 决定“这地址是否合法”,页表决定“映射到哪”**
---
# 四、OS 如何“分配内存”(你问的核心)
注意OS 从来不“直接给物理内存”,而是:
> **给虚拟地址范围VMA+ 延迟物理分配**
两种系统调用路径:
---
## 1⃣ brk / sbrkheap 扩展)
```text
作用:扩展 heap VMA 的末尾
```
过程:
```text
原 heap end = 0x10000
sbrk(4096)
→ 内核:
修改 VMAheap end → 0x11000
(不分配物理页)
```
特点:
* 连续
* 只能向上增长
* 小块分配主力
---
## 2⃣ mmap独立映射
```c
mmap(NULL, size, ...)
```
过程:
```text
内核:
在地址空间找一段空洞
创建新 VMA
```
特点:
* 任意位置
* 独立 VMA
* 可单独释放munmap
* 大块内存使用
---
## malloc 的策略(关键)
```text
小块 → brkheap
大块 → mmap独立 VMA
```
---
# 五、从 OS 到 malloc控制权转移
到这里:
```text
OS
管 VMA地址是否合法
管页表(地址映射)
```
但:
> **OS 不管“你怎么用这块内存”**
于是:
```text
malloc 接管 heap 内存
```
---
# 六、malloc 的世界heap → chunk
malloc 拿到一大块虚拟空间heap然后
```text
切成很多 chunk
```
每个 chunk
```c
[ header | payload | (footer) ]
header:
size
alloc bit
```
heap 结构:
```text
[A][F][A][F][F][A]
```
A = allocated
F = free
---
# 七、问题核心:如何找到空闲块?
这就是你看到的:
> 隐式 / 显式空闲链表
---
## 1⃣ 隐式空闲链表implicit
结构:
```text
所有 chunk 按顺序排列
```
查找:
```text
从头扫:
if free && size够 → 用
```
复杂度:
```text
O(n)
```
问题:
* heap 越大越慢
---
## 2⃣ 显式空闲链表explicit
结构:
```text
只把 free chunk 串起来:
heap:
[A][F][A][F][F]
free list:
F <-> F <-> F
```
free chunk 结构:
```c
[ header | prev | next | payload ]
```
查找:
```text
遍历 free list
```
复杂度:
```text
O(空闲块数量)
```
---
# 八、free 的完整行为(关键闭环)
```c
free(p);
```
发生:
```text
1. 标记 chunk = free
2. 向前/向后检查是否也是 free
3. 合并coalescing
4. 插入 free list
```
---
## 合并机制(核心)
```text
[ F ][ 当前 ][ F ]
→ 合并成一个大 F
```
依赖:
* header当前
* footer前一个
---
# 九、为什么 free 不还给 OS
因为:
```text
brk 区域:
只能整体收缩
中间空洞无法归还
→ 留在 free list 复用
```
但:
```text
mmap
free → munmap → 直接还 OS
```
---
# 十、现代 malloc真实情况
实际不会只用一个 free list而是
```text
tcache线程本地
fastbin小块
smallbin
largebin
unsorted bin
```
本质:
> **多组“显式空闲链表”,按大小分类**
优化目标:
```text
减少查找时间
减少锁竞争
提高局部性
```
---
# 十一、完整路径(你必须能在脑子里跑一遍)
```text
malloc(1000)
malloc 查 tcache / bin
↓ 没找到
向 OS 要:
brk 或 mmap
OS 创建/扩展 VMA
返回虚拟地址
malloc 切 chunk
返回 pointer
(访问时)
MMU 查页表
没有映射 → page fault
内核:
find_vma
检查权限
分配物理页
建页表
继续执行
```
---
# 十二、最核心的三层模型(压缩记忆版)
```text
【OS层】
VMA这段地址能不能用
页表:这段地址映射到哪
【malloc层】
heap一大块空间
chunk切出来的小块
【算法层】
free list
隐式(扫所有)
显式(只扫空闲)
```
---
# 十三、一句话终极总结(你要能秒说出来)
> 操作系统通过 brk 或 mmap 为进程分配虚拟地址区间VMA并在访问时按需建立页表与物理内存映射malloc 在这些区间内切分 chunk 并通过显式或隐式空闲链表管理空闲块,实现用户级内存分配与复用。
---
如果你下一步要继续压深度,最值得打的两个点是:
* **page fault 从中断入口到分配物理页的完整路径(含寄存器/内核栈)**
* **COWfork 时 VMA + 页表如何共享 + 写时复制触发机制)**
这两个一旦吃透,你对操作系统内存这一块就是“工程级理解”了。