Memory Management
内存管理要解决什么
- 抽象:给进程一个“看起来连续、很大、独占”的地址空间(虚拟内存)。
- 隔离与保护:进程互不干扰(权限位、用户态/内核态、地址空间隔离)。
- 效率:利用局部性,减少慢速存储访问(缓存、页缓存、TLB)。
- 共享:共享库、共享内存、文件映射、COW。
虚拟地址
进程看到和使用的地址。它属于每个进程私有的虚拟地址空间。
物理地址
物理地址(Physical Address):真实的硬件内存地址,指向 DRAM 里的某个单元,是全局唯一的。
Page
页(Page):虚拟内存空间(进程看到的地址空间)被等长切成的块,比如 4 KB。
Frame
物理内存(DRAM)被等长切成的块,大小与“页”相同。
核心思想:把“虚拟页”按需映射到“物理页框”,由操作系统维护“页表”告诉硬件该怎么翻译。
分段Segmentation
按逻辑单元/可变大小划分地址空间(如代码段、数据段、栈段);
为什么要使用Page
- 解决外碎片:等长分配,避免分段式那种“洞洞相连”的外碎片。
-
分页=等长小块:虚拟内存按固定大小(如 4KB)切成页,物理内存按同样大小切成页框。映射时不要求连续,随便哪块空闲页框都能用。
-
支持虚拟内存:只把活跃页留在内存,不常用的页换到磁盘(swap)。
-
提供隔离与保护:页表权限(R/W/X、U/S)让进程互不干扰。
-
支持共享:多进程可把各自的虚拟页映射到同一物理页(如共享库、只读代码)。
内存碎片
内碎片(Internal fragmentation)
已分配的块内部有用不到的空隙——给多了。 典型成因:按固定粒度分配,请求大小不是粒度整数倍,需要向上取整。
外碎片(External fragmentation)
还没分配出去的空闲内存被切成很多不相邻的小洞,总量够但拼不出一块足够大的连续空间——给不了。
虚拟内存转换
虚拟地址 VA → 物理地址 PA:MMU 通过页表完成转换。
页表
- 页表是操作系统为每个进程(或地址空间)维护的地址映射数据结构
- 在物理内存的专用页里(一页通常 4KB),每一级页表都是若干“页表页”。这些页由内核内存分配器(如 Linux 的 buddy)分配,并标记为仅内核可访问。
- CPU 怎么找到它:
- 查 TLB(iTLB/dTLB):命中则直接得到物理页框号(PFN),否则启动页表遍历(page walk)
- x86-64:
CR3寄存器保存PML4 表的物理地址
页表(Page Table / PTE)关键字段:
- PFN:物理页框号
- Present:是否在内存、R/W/X、U/S:用户态/内核态访问权限
- A / D:Accessed/Dirty
- copy-on-write 标记(OS 逻辑)
TLB
TLB:页表结果的缓存;TLB miss 会触发查页表(甚至 page walk)。
多级页表
- 每一层是一个表页(4KB),表项(如 512 项 × 8B)指向下一层表的物理页或直接指向大页的物理页框
减少稀疏地址空间的页表占用(x86-64 常见 4/5 级)。
给page size=512B,页表映射,求物理地址(0x457)
0x457/0x200 = 2 .. 0x57, 找映射 page_table(2) = 4, 然后 physical = 4*0x200 + 0x57
Page Fault流程
若 PTE 不在或权限不符,触发 page-fault 异常
- CPU 陷入内核
- 保存现场(PC/寄存器/标志)。把故障地址(如 x86 的
CR2)与错误码交给内核的缺页处理入口 - 在进程的VMA/内存映射里找这段地址是否合法映射
- 不合法 → 保护异常/越界:发送信号(如
SIGSEGV)或杀进程;到此结束。 - 合法 → 进入“如何把这页准备好”的分支
- 按需分配/零页(demand-zero):匿名内存首次触达 → 分配空闲页、清零
- 换入(swap-in):该页被换出过 → 从交换区把数据读回
- 文件映射(page cache):文件
mmap或缺页读 → 从存储把相应页读入页缓存。 - 写时复制(COW):对只读共享页执行写 → 分配新页、复制旧内容、将新页映射为可写。
- 若无空闲物理页:触发页面置换:依据 A/D 位与策略(近似 LRU 等)选牺牲页
- 填写 PTE:PFN、权限(R/W/X、U/S、NX)
- 返回用户态并重试
系统抖动
系统抖动在操作系统里通常指 内存抖动/颠簸(thrashing):系统把大量时间花在 换页/换入换出(paging/swapping) 上,而不是在真正执行进程的计算,导致整体性能反而急剧下降。
最典型的场景是分页虚拟内存:
- 进程需要访问某个页,但该页不在内存 → 缺页中断
- OS 从磁盘把页换入(可能还要换出别的页)→ I/O 很慢
核心原因:内存不够,工作集装不下。
常见诱因:
- 多道程序度太高:同时跑的进程太多,每个都分到很少页框
- 局部性差/访问模式变化:程序突然跳来跳去,缓存不住
- 不合适的页面置换策略:比如某些情况下会频繁把马上又要用的页换出去
CPU 利用率反而下降(CPU 大量时间在等待 I/O)
磁盘 I/O 飙高(频繁读写 swap / pagefile)
用户态内存分配(malloc 背后)
malloc常用两路:- 小块:从 heap/arena 切分
- 大块:
mmap直接映射 - 关注点:碎片(fragmentation)、线程缓存、arena、
free不一定立刻归还 OS
内核态物理内存管理
- Buddy system:按 2^k 页块分配/合并(解决外部碎片的一种方式)。
- Slab/Slub:面向对象的缓存分配(inode、dentry、task_struct 等)。
Buddy System
做什么:Linux 的底层物理页分配器,按2 的幂大小管理连续的页框怎么工作(核心流程)
- 空闲链表分层:每个内存域(zone)维护多条空闲链表
free_area[order]。 - 分配:
- 需求向上取整到最小 2 的幂(比如要 3 页 → 取 4 页,order=2)。
- 若该 order 没空闲块,就到更大 order 找,一路“劈开”成两个 buddy,直到得到目标大小。
- 释放与合并:
- 释放块后,检查它的伙伴块是否也空闲且同阶;
- 若是,合并成更大一阶,继续向上合并,尽量恢复大块连续空间。
- 快路径:为减少锁竞争,小分配会先走Per-CPU page list 缓存。
slub allocator
内核里大量分配的是小而高频的内核对象(dentry、inode、task_struct…),直接用 buddy 按页分配会浪费和慢。 于是有一层对象缓存分配器,把“页”切成“对象”,按类型复用。
为每类对象建 kmem_cache(同大小/同构造函数)。
预初始化、对象复用,命中缓存即可用,延迟稳定、CPU cache 友好,减少频繁的构造/析构。
SLUB(现代默认)
- 设计更简化:每 CPU 保持一个活动 slab 和简洁的空闲链;
- 锁更少、NUMA 友好、吞吐和内存效率更好;
- 调试能力完善(
slub_debug)。→ 大多数发行版默认。
磁盘调度
磁盘的移臂调度(也叫磁盘调度、寻道调度)指的是:当磁盘上同时有很多 I/O 请求(读/写不同磁道上的块)时,操作系统决定按什么顺序让磁头去服务这些请求,从而尽量减少磁头来回移动的时间。
FCFS(先来先服务)
- 按到达顺序处理
- 优点:公平、实现简单
- 缺点:寻道距离可能很大,性能差
SSTF(最短寻道时间优先)
- 每次选“离当前磁头位置最近”的请求
- 优点:平均寻道时间小,性能好
- 缺点:可能饥饿(远处请求一直轮不到)
SCAN(电梯算法)
- 磁头像电梯一样:沿一个方向扫,遇到请求就服务;到头再反向
- 优点:比 SSTF 更公平,吞吐稳定
- 缺点:边缘请求等待可能更久
LOOK / C-LOOK
- SCAN/C-SCAN 的改进:不必真的扫到磁盘最外/最内磁道,只扫到“当前方向上最后一个请求”为止
- 优点:少走空路,性能更好
说一下逻辑地址、线性地址、物理地址的区别和关系。
什么是虚拟内存?为什么需要虚拟内存?
分段(segmentation)和分页(paging)的区别?各自优缺点?
什么是页表?多级页表为什么可以节省内存?
TLB(快表)是什么?为什么能提高性能?
页命中率、缺页中断是什么?缺页中断发生时操作系统要做什么?
常见页面置换算法:FIFO、LRU、OPT、CLOCK,大致原理和优缺点?
什么是抖动(thrashing)?为什么会出现?如何缓解?
堆和栈的区别?(从管理方式、空间大小、分配效率、生命周期等方面)
内存碎片是什么?如何产生?怎么减少?
什么是内存对齐?为什么需要对齐?
用户态与内核态的内存空间一般如何划分?
解释一下:malloc / free 在底层是如何向操作系统申请 / 归还内存的?(brk、mmap 等)