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

17 KiB
Raw Permalink Blame History

好,这个问题就进入**“malloc 和 OS 的交界面”**了,本质就是一句话:

用户态是不能“直接拿虚拟内存”的,必须通过系统调用向内核要。

但你现在要的不是这句废话,而是完整机制 + 实际路径 + 细节差异,我直接给你一条从上到下打通。


一、总流程(先建立全局模型)

当你写:

void* p = malloc(1000);

底层发生的是:

malloc
  ↓(不够用)
向 OS 要内存(系统调用)
  ↓
OS 修改进程虚拟内存布局VMA
  ↓
返回一段“合法虚拟地址”
  ↓
malloc 切 chunk 管理

关键点:

OS 给你的不是“物理内存”,而是“虚拟地址范围的使用权”


二、malloc 向 OS 要内存的两种方式(核心)

现代 Linuxglibc主要用两种


1 brk / sbrk —— 扩展 heap传统主力

这是最经典的方式。

本质:

👉 把 heap 顶部往上“推高”


内存布局(非常关键)

低地址
│
├── 程序代码text
├── 数据段data
├── BSS
├── heap  ←(这里往上长)
│     ↑
│     当前 brk
│
│   (未使用虚拟空间)
│
├── mmap 区域(往下长)
│
├── 栈stack
│
高地址

系统调用:

brk(new_end);

或:

sbrk(increment);

举个实际过程:

当前 heap 结束0x10000
malloc 想要 4KB

→ 调用 sbrk(4096)

内核做:
  - 修改 VMA虚拟内存区域
  - 更新 brk → 0x11000

返回0x10000新空间起点

特点总结:

  • ✔ 连续内存
  • ✔ 适合小块分配
  • 只能“向上长”
  • 不容易归还给 OS碎片问题

2 mmap —— 单独映射(现代关键)

当 malloc 发现:

“这个块太大了”

就不会走 heap而是直接 mmap


系统调用:

mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);

本质:

👉 让内核在虚拟地址空间中找一块空地,直接映射一段


内存结构:

heap ↑
        (空洞)
mmap ↓

👉 mmap 区域是独立的,不挨着 heap


特点:

  • ✔ 可以在任意地址分配
  • ✔ 可以独立释放munmap
  • ✔ 适合大块内存
  • 系统调用开销更高

malloc 的策略glibc

大致是:

小块(< 128KB 左右) → brk
大块(>= 128KB     → mmap

(具体阈值可调)


三、OS 到底“分配了什么”?

这点你必须彻底理解,否则会一直模糊。


关键结论:

OS 分配的是“虚拟地址范围 + 映射关系”,不是立即分配物理内存


举个例子:

p = malloc(1GB);

发生:

  • OS✔ 给你 1GB 虚拟地址
  • OS 不会马上给你 1GB 物理内存

什么时候才分配物理内存?

👉 当你真正访问(触发 page fault


过程:

访问 p[0]
  ↓
页表无映射
  ↓
触发 page fault
  ↓
内核分配物理页4KB
  ↓
建立映射

👉 这叫:

按需分配lazy allocation


四、free 之后发生什么?

你刚才已经问到 free list这里补完整闭环


情况 1heapbrk

free(p);
  • ✔ 进入 free list
  • 不还给 OS大多数情况

👉 只是 malloc 内部复用


情况 2mmap

free(p);
  • ✔ 直接调用 munmap
  • ✔ 归还 OS

五、再抽象一层(你这个水平该理解这个)

整个系统是三层:


层级结构

[用户程序]
    ↓ 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 里核心结构是:

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;           // 红黑树节点
};

你重点抓三个东西就够:

[vm_start, vm_end)  → 管哪一段地址
vm_flags           → 这段能干嘛RWX
vm_file            → 是否映射文件

三、一个进程里 VMA 长啥样(全局视图)

一个进程不是一个 VMA而是

很多个 VMA 组成整个虚拟地址空间

比如:

低地址
│
├── [text 段]        r-x
├── [data 段]        rw-
├── [heap]           rw-
├── [mmap 区域1]     rw-
├── [mmap 区域2]     r--
│
├── [stack]          rw-
│
高地址

👉 每一段,都是一个 VMA


四、VMA 存在哪?(核心)

在进程控制结构里:

struct mm_struct {
    struct vm_area_struct *mmap; // VMA 链表头
    struct rb_root mm_rb;        // 红黑树根
};

👉 也就是说:

一个进程:

task_struct
   ↓
mm_struct
   ↓
VMA 集合(链表 + 红黑树)

五、为什么既用链表又用红黑树?

这是一个很经典的设计点。


1 链表vm_next

👉 用于:

  • 顺序遍历(比如 /proc/pid/maps

优点:

  • 简单
  • 按地址排序

缺点:

  • 查找慢O(n)

2 红黑树mm_rb

👉 用于:

  • 快速查找某个地址属于哪个 VMA

复杂度:

O(log n)

👉 所以:

链表负责“遍历”,红黑树负责“查找”


六、核心操作:查找 VMA最关键路径

当程序访问一个地址:

*p = 10;

如果触发 page fault


内核会做:

1. 获取 fault address比如 0x123456
2. 在 VMA 红黑树里查找
   ↓
   find_vma(mm, addr)
3. 判断:
   addr ∈ [vm_start, vm_end) ?

如果找不到:

👉 直接:

Segmentation Fault段错误

如果找到了:

继续:

检查 vm_flags有没有写权限
  • 没权限 → segfault
  • 有权限 → 分配物理页

七、VMA 是怎么创建的?

主要三种来源:


1 程序加载exec

比如 ELF

  • 代码段 → 一个 VMAr-x
  • 数据段 → 一个 VMArw-

2 malloc间接

小块:

malloc → brk → 扩展 heap VMA

👉 heap 通常是:

一个 VMA 不断变大


大块:

malloc → mmap → 新建 VMA

👉 每个 mmap

一个独立 VMA


3 用户 mmap

mmap(...)

👉 直接创建 VMA


八、VMA 的合并(非常重要,容易考)

内核会做优化:


如果两个 VMA

地址连续 + 权限相同 + 类型相同

👉 会合并


举例:

[0x1000 - 0x2000] rw-
[0x2000 - 0x3000] rw-

👉 合并成:

[0x1000 - 0x3000] rw-

👉 目的:

  • 减少 VMA 数量
  • 提高查找效率

九、VMA 删除(释放)


情况 1munmap

munmap(addr, size);

内核:

1. 找到对应 VMA
2. 删除或裁剪
3. 更新红黑树
4. 解除页表映射

情况 2进程退出

👉 直接:

销毁整个 mm_struct
→ 所有 VMA 全部释放

十、你必须理解的关键区别(非常重要)

VMA ≠ 页表 ≠ 物理内存


三者关系:

VMA逻辑区域
   ↓ 描述
页表(地址映射)
   ↓ 指向
物理页(真实内存)

👉 举个例子:

malloc 1GB
  • ✔ VMA立刻存在1GB范围
  • 页表:还没完全建立
  • 物理内存:还没分配

👉 访问时才:

page fault → 建页表 → 分配物理页

十一、一个完整访问路径(你要掌握这个)

用户访问 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

CPU发出 VA
   ↓
MMU硬件
   ↓ 查页表
得到 PA
   ↓
访问物理内存

如果页表里没有映射:

→ 触发 page fault缺页异常中断
→ 进入内核

二、内核层虚拟内存的“逻辑描述”VMA

内核不会直接给你“内存块”,它先维护:

VMA = 一段虚拟地址区间 + 权限 + 类型

本质结构(抽象):

struct vm_area_struct {
    vm_start, vm_end
    vm_flagsr/w/x
    vm_file(是否文件映射)
}

一个进程的虚拟空间:

多个 VMA 拼起来:
[text][data][heap][mmap...][stack]

存储方式:

mm_struct
    红黑树(按地址索引)→ 查找 O(log n)
    链表(顺序)        → 遍历

三、page fault 的核心决策逻辑(必须背下来)

当访问一个 VA

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 扩展)

作用:扩展 heap VMA 的末尾

过程:

原 heap end = 0x10000
sbrk(4096)
→ 内核:
    修改 VMAheap end → 0x11000
    (不分配物理页)

特点:

  • 连续
  • 只能向上增长
  • 小块分配主力

2 mmap独立映射

mmap(NULL, size, ...)

过程:

内核:
    在地址空间找一段空洞
    创建新 VMA

特点:

  • 任意位置
  • 独立 VMA
  • 可单独释放munmap
  • 大块内存使用

malloc 的策略(关键)

小块 → brkheap
大块 → mmap独立 VMA

五、从 OS 到 malloc控制权转移

到这里:

OS
    管 VMA地址是否合法
    管页表(地址映射)

但:

OS 不管“你怎么用这块内存”

于是:

malloc 接管 heap 内存

六、malloc 的世界heap → chunk

malloc 拿到一大块虚拟空间heap然后

切成很多 chunk

每个 chunk

[ header | payload | (footer) ]

header:
    size
    alloc bit

heap 结构:

[A][F][A][F][F][A]

A = allocated F = free


七、问题核心:如何找到空闲块?

这就是你看到的:

隐式 / 显式空闲链表


1 隐式空闲链表implicit

结构:

所有 chunk 按顺序排列

查找:

从头扫:
    if free && size够 → 用

复杂度:

O(n)

问题:

  • heap 越大越慢

2 显式空闲链表explicit

结构:

只把 free chunk 串起来:

heap:
[A][F][A][F][F]

free list:
F <-> F <-> F

free chunk 结构:

[ header | prev | next | payload ]

查找:

遍历 free list

复杂度:

O(空闲块数量)

八、free 的完整行为(关键闭环)

free(p);

发生:

1. 标记 chunk = free
2. 向前/向后检查是否也是 free
3. 合并coalescing
4. 插入 free list

合并机制(核心)

[ F ][ 当前 ][ F ]
→ 合并成一个大 F

依赖:

  • header当前
  • footer前一个

九、为什么 free 不还给 OS

因为:

brk 区域:
    只能整体收缩
    中间空洞无法归还

→ 留在 free list 复用

但:

mmap
    free → munmap → 直接还 OS

十、现代 malloc真实情况

实际不会只用一个 free list而是

tcache线程本地
fastbin小块
smallbin
largebin
unsorted bin

本质:

多组“显式空闲链表”,按大小分类

优化目标:

减少查找时间
减少锁竞争
提高局部性

十一、完整路径(你必须能在脑子里跑一遍)

malloc(1000)
   ↓
malloc 查 tcache / bin
   ↓ 没找到
向 OS 要:
    brk 或 mmap
   ↓
OS 创建/扩展 VMA
   ↓
返回虚拟地址
   ↓
malloc 切 chunk
   ↓
返回 pointer

(访问时)
   ↓
MMU 查页表
   ↓
没有映射 → page fault
   ↓
内核:
    find_vma
    检查权限
    分配物理页
    建页表
   ↓
继续执行

十二、最核心的三层模型(压缩记忆版)

【OS层】
VMA这段地址能不能用
页表:这段地址映射到哪

【malloc层】
heap一大块空间
chunk切出来的小块

【算法层】
free list
    隐式(扫所有)
    显式(只扫空闲)

十三、一句话终极总结(你要能秒说出来)

操作系统通过 brk 或 mmap 为进程分配虚拟地址区间VMA并在访问时按需建立页表与物理内存映射malloc 在这些区间内切分 chunk 并通过显式或隐式空闲链表管理空闲块,实现用户级内存分配与复用。


如果你下一步要继续压深度,最值得打的两个点是:

  • page fault 从中断入口到分配物理页的完整路径(含寄存器/内核栈)
  • COWfork 时 VMA + 页表如何共享 + 写时复制触发机制)

这两个一旦吃透,你对操作系统内存这一块就是“工程级理解”了。