Skip to content

内存管理

概念

内存管理是操作系统的核心职责之一,负责协调进程对物理内存的使用。其目标是:让多个进程安全隔离地共享有限的物理内存,同时提供比物理内存更大的地址空间幻觉。

关键术语速览:

术语含义
虚拟地址进程看到的地址,由 OS 映射到物理地址
物理地址实际内存芯片上的地址
页(Page)虚拟地址空间的固定大小块(通常 4KB)
页框(Frame)物理内存的固定大小块,大小与页相同
MMU内存管理单元,硬件完成地址翻译
TLB快表,缓存最近使用的页表项
缺页中断访问未映射到物理内存的虚拟页时触发

核心原理

1. 虚拟内存

为什么需要虚拟内存

  • 进程隔离:每个进程拥有独立的虚拟地址空间,无法直接访问其他进程的内存,防止越界破坏。
  • 地址空间统一:每个进程都从地址 0x0 开始,编译器和链接器无需关心物理内存布局。
  • 按需加载:程序启动时不必全部装入内存,只在访问时才分配物理页,节省内存。
  • 内存超用(Overcommit):进程申请的虚拟内存总量可以超过物理内存,配合换页机制使用。

虚拟地址 → 物理地址翻译(MMU + 页表)

虚拟地址(VA)
    |
    v
+-------+--------+--------+
| VPN2  |  VPN1  | Offset |   (多级页表示例,VPN = 虚拟页号)
+-------+--------+--------+
    |        |
    v        v
  页全局目录 → 页中间目录 → 页表 → 页表项(PTE)
                                        |
                                        v
                                    物理页框号(PFN)
                                        |
                                        v
                               PFN << 12 | Offset = 物理地址(PA)

整个翻译由硬件 MMU 自动完成,OS 负责维护页表数据结构。

进程虚拟地址空间布局(以 64 位 Linux 为例)

高地址
+---------------------------+  0xFFFFFFFFFFFFFFFF
|       内核空间             |  (用户态不可访问)
+---------------------------+  0xFFFF800000000000
|                           |
|       (不可用间隙)        |
|                           |
+---------------------------+  0x00007FFFFFFFFFFF
|         栈(Stack)        |  向低地址增长 ↓
|       (局部变量、返回地址)  |
+---------------------------+
|           ...             |
+---------------------------+
|    内存映射区(mmap 区)    |  动态库、匿名映射
+---------------------------+
|           ...             |
+---------------------------+
|         堆(Heap)         |  向高地址增长 ↑
|       (malloc 分配)        |
+---------------------------+
|      BSS 段(未初始化数据) |
+---------------------------+
|      数据段(已初始化数据) |
+---------------------------+
|      代码段(Text)        |  只读,存放指令
+---------------------------+  0x0000000000400000
低地址

2. 分页(Paging)

页与页框

  • 虚拟地址空间被划分为固定大小的页(Page),物理内存被划分为同等大小的页框(Frame)
  • 典型大小:4KB 字节),地址低 12 位为页内偏移。
  • 虚拟地址 = 虚拟页号(VPN) + 页内偏移(Offset)

页表结构

  • 一级页表:一张大表,32 位地址空间需 个表项,占用 4MB(每进程),开销大。
  • 多级页表:将页表拆成树状结构,只为实际使用的地址范围分配页表节点,节省空间。Linux x86-64 默认使用 4 级页表(PGD → PUD → PMD → PTE)。
  • 每个页表项(PTE)存储:物理页框号 + 标志位(Present / Dirty / Accessed / User/Kernel 等)。

TLB(快表)加速地址翻译

多级页表每次翻译需多次内存访问,性能差。TLB 是 MMU 内部的硬件缓存,存储最近使用的 VPN → PFN 映射。

CPU 发出虚拟地址
       |
       v
   查 TLB ──命中──→ 直接得到物理地址(1 个时钟周期级别)
       |
     未命中(TLB Miss)
       |
       v
   查多级页表(走内存,慢)
       |
       v
   更新 TLB → 返回物理地址
  • TLB 刷新:进程切换时需刷新 TLB(或使用 ASID 标记避免完全刷新)。
  • 命中率通常 > 99%,得益于程序的局部性原理

缺页中断(Page Fault)处理流程

  1. CPU 访问虚拟地址,MMU 发现 PTE 的 Present 位为 0。
  2. CPU 触发缺页中断,陷入内核态。
  3. OS 缺页中断处理程序检查该地址是否合法(是否在 VMA 范围内)。
  4. 若合法:找一个空闲页框,将对应页从磁盘(swap 区或文件)读入。
  5. 更新页表 PTE,设置 Present 位,返回用户态重新执行触发中断的指令。
  6. 若非法(如访问 NULL):触发 Segmentation Fault,进程收到 SIGSEGV 信号。

3. 分段(Segmentation)

段 vs 页

对比项分段分页
划分依据逻辑含义(代码段/数据段/栈段)固定大小块
大小可变固定(4KB)
外碎片有(段间空隙)
内碎片有(页内未用空间)
共享与保护按段粒度,直观需要多个页对齐

段页式(先分段再分页)

将地址空间先按逻辑划分为段,每段再独立分页。地址翻译:段号 → 段基址 + 页表 → 物理地址。Intel x86 保护模式即采用段页式,但现代 OS(Linux/Windows)将所有段基址设为 0,实际退化为纯分页

现代 OS 为什么主要用分页

  • 页大小固定,管理简单,无外碎片。
  • 硬件(MMU/TLB)天然为分页设计优化。
  • 便于换页(swap)和内存映射。
  • 段页式复杂度高,收益有限。

4. 页面置换算法

当物理内存不足、需要换入新页时,OS 必须选择一个页换出到磁盘。

FIFO(先进先出)

按页进入内存的顺序淘汰最早进入的页。实现简单,但性能差。

Belady 异常:分配的物理页框增多,缺页次数反而增加。FIFO 是唯一会出现此异常的常见算法。

LRU(最近最少使用)

淘汰最长时间未被访问的页,基于时间局部性假设。理论上接近最优,但精确实现开销大(需维护访问时间戳或链表)。

实现方式:

  • 双向链表 + 哈希表:每次访问将页移到链表头,淘汰链表尾部。时间复杂度 O(1)。
  • 计数器:每个页记录最后访问时间戳,淘汰时扫描最小值,O(n)。

Clock(时钟算法,近似 LRU)

用一个循环链表和一个指针("时钟指针")模拟。每个页有一个 使用位(R bit)

  1. 访问页时置 R = 1。
  2. 需要换出时,时钟指针扫描:R = 1 则清零并跳过,R = 0 则换出该页。

优点:O(1) 近似 LRU,Linux 实际使用的策略基于此思想(active/inactive 链表)。

OPT(最优算法)

淘汰未来最长时间不会被访问的页。缺页率最低,是理论基准,但因无法预知未来,只用于评估其他算法。

对比总结

算法缺页率实现复杂度Belady 异常实用性
OPT最低不可实现理论基准
LRU接近最优中(需链表+哈希)高(近似实现)
Clock接近 LRU高(Linux 实际使用)
FIFO较高最低

5. 内存映射(mmap)

mmap 系统调用将文件或设备映射到进程的虚拟地址空间。

文件映射 vs 匿名映射

  • 文件映射:虚拟地址区域对应磁盘文件的某个区间,读写内存即读写文件(由 OS 在合适时机同步)。
  • 匿名映射:不对应任何文件,常用于分配大块内存(malloc 大对象底层即用此方式)。

共享映射 vs 私有映射(写时复制 COW)

  • 共享映射(MAP_SHARED):多个进程映射同一物理页,写入对所有进程可见,也写回文件。
  • 私有映射(MAP_PRIVATE):写时复制(Copy-On-Write)。初始时共享同一物理页,某进程写入时内核为其复制一份私有副本,其他进程不受影响。

fork() 就利用 COW:父子进程共享所有物理页,只有在写入时才复制,大幅降低 fork 开销。

主要用途

用途说明
文件 I/O 优化绕过 read/write 系统调用,减少数据拷贝次数
进程间共享内存MAP_SHARED 让多进程共享同一段物理内存
动态库加载.so / .dll 以文件映射方式加载,多进程共享代码段
大内存分配malloc 对 > 128KB 的请求使用匿名 mmap

6. 内存分配

malloc 底层:brk vs mmap

glibc 的 malloc 有两种向 OS 申请内存的方式:

方式触发条件系统调用特点
brk申请 < 128KBbrk / sbrk移动堆顶指针,适合小对象,不立即归还 OS
mmap申请 ≥ 128KBmmap + munmap独立映射,free 后立即归还 OS

128KB 阈值(MMAP_THRESHOLD)可动态调整。malloc 内部维护空闲链表,优先复用已释放的内存块,减少系统调用次数。

内存碎片

  • 内碎片(Internal Fragmentation):分配的块比请求的大,多余部分浪费。例如请求 10 字节,分配了 16 字节对齐块。
  • 外碎片(External Fragmentation):空闲块总量足够,但分散成小块,无法满足大的连续分配请求。

伙伴系统(Buddy System)

Linux 内核用于管理物理页框的分配算法:

  • 内存按 个页框为单位管理(order 0 = 1 页,order 11 = 2048 页)。
  • 分配时找最小满足需求的 order,若无则拆分更大的块(一分为二,两块互为"伙伴")。
  • 释放时检查伙伴是否空闲,若是则合并,反复向上合并,减少外碎片。

Slab 分配器

伙伴系统以页为粒度,对内核中大量小对象(如 task_structinode)效率低。Slab 在伙伴系统之上增加对象缓存层:

  • 为每种固定大小的对象维护一个缓存(slab cache)。
  • 预先分配一批对象,分配/释放时直接从缓存取回,避免反复初始化。
  • 减少内碎片,提升小对象分配速度。

面试常问 & 怎么答

Q1:什么是虚拟内存?为什么需要它?

答题思路:先定义,再说三个核心原因,可提 MMU + 页表机制。

虚拟内存是 OS 为每个进程提供的独立地址空间抽象,由 MMU 将虚拟地址动态映射到物理地址。

需要它的原因:

  1. 进程隔离:每个进程有独立的地址空间,无法随意访问其他进程内存,保证安全性。
  2. 地址空间统一:程序编译时使用固定的虚拟地址,无需关心实际物理内存位置。
  3. 按需加载 + 内存超用:物理内存不足时,将暂时不用的页换到磁盘(swap),进程申请的虚拟空间可超过物理内存总量。

Q2:页面置换算法有哪些?LRU 怎么实现?

答题思路:列出四种算法,重点介绍 LRU 的 O(1) 实现,提 Clock 是实际使用的近似方案。

常见算法:OPT(理论最优)、LRU(最近最少使用)、Clock(时钟,近似 LRU)、FIFO(先进先出,有 Belady 异常)。

LRU 精确实现:双向链表 + 哈希表。

  • 哈希表:页号 → 链表节点,O(1) 查找。
  • 双向链表:维护访问顺序,最近访问的节点移到头部,尾部节点是最久未用的。
  • 访问页:O(1) 查哈希 → 将节点移到头部。
  • 缺页换出:O(1) 移除并返回尾部节点。

实际系统:精确 LRU 需要在每次访问时修改链表,代价高。Linux 使用基于 Clock 思想的双链表(active list / inactive list)近似实现。


Q3:malloc 底层是怎么实现的?

答题思路:分两层回答——用户态 glibc 的空闲链表复用,以及向 OS 申请内存的两种方式。

malloc 分两层工作:

用户态(glibc ptmalloc)

  • 维护多个空闲链表(bins),按大小分类(fast bins、small bins、large bins 等)。
  • malloc 时优先从空闲链表中找合适块复用,避免频繁系统调用。
  • free 时将块放回对应链表,相邻空闲块会合并以减少碎片。

向 OS 申请内存

  • 小于 128KB:调用 brk/sbrk 扩展堆顶,内存释放后不立即归还 OS(留在堆中复用)。
  • 大于等于 128KB:调用 mmap 申请独立匿名映射,free 后调用 munmap 立即归还 OS。

这样设计是权衡系统调用开销与内存归还及时性的结果。


看到什么就先想到这类

关键词 / 场景联想到
进程间内存隔离虚拟内存、页表、MMU
地址翻译慢 / TLB missTLB、多级页表、页大小
程序访问 NULL 崩溃缺页中断 → Segmentation Fault
物理内存不足页面置换算法、swap
多进程共享库mmap 文件映射、共享页
fork 很快 / 写时复制COW(Copy-On-Write)、私有映射
malloc 大对象 vs 小对象brk vs mmap,128KB 阈值
内核分配小对象高效Slab 分配器
物理页框分配伙伴系统(Buddy System)
内存泄漏 / 碎片内碎片 vs 外碎片,堆管理