虚拟内存
一、 虚拟内存 (VM) 概述
-
核心思想: 为每个进程提供一个私有的、连续的、巨大的地址空间,称为虚拟地址空间 (Virtual Address Space),它与物理内存(DRAM)的大小和组织方式解耦。
-
关键特性:
- 抽象 (Abstraction): 提供了一个比物理内存更易于使用的内存模型。
- 透明 (Transparent): 用户程序通常感知不到虚拟内存的存在,像直接操作真实内存一样。
- 高效 (Efficient): 性能是 VM 机制被广泛接受的前提。
- 保护/隔离 (Protection / Isolation): 进程的虚拟地址空间是私有的,不能访问其他进程或内核的地址空间,提供了内存保护。
-
地址类型:
- 虚拟地址 (Virtual Address, VA): CPU 生成的、程序使用的地址。
- 物理地址 (Physical Address, PA): 硬件内存(DRAM)中的实际地址。
-
地址翻译 (Address Translation):
-
定义: 将虚拟地址 (VA) 转换为物理地址 (PA) 的过程。
-
**机制:**需要硬件 (HW)和操作系统 (OS)紧密协作。
- 硬件: 内存管理单元 (Memory Management Unit, MMU),CPU 芯片上的专用硬件,负责动态、快速地翻译地址。
- 软件 (OS): 维护页表 (Page Table) 等查找结构(存储在主存中,MMU 可以访问),记录 VA 到 PA 的映射关系。
-
二、 物理寻址 vs. 虚拟寻址
- 物理寻址 (Physical Addressing):
- CPU 直接使用物理地址 (PA) 访问主存。
- 主存被看作一个连续的字节数组,每个字节有唯一 PA。
- 应用于早期 PC、嵌入式系统、某些超级计算机等相对简单的系统。
- 虚拟寻址 (Virtual Addressing):
- CPU 生成虚拟地址 (VA)。
- VA 必须先通过 MMU 和页表翻译成 PA,然后才能访问主存。
- 应用于所有现代服务器、桌面和笔记本电脑。是计算机科学中的伟大思想之一。
三、 地址空间 (Address Space)
- 虚拟地址空间 (Virtual Address Space):
- 大小由地址总线位数
n
决定,包含 N = 2 n N=2^n N=2n 个虚拟地址,范围从0
到 N − 1 N−1 N−1。 - 每个进程拥有独立的虚拟地址空间。
- 大小由地址总线位数
- 物理地址空间 (Physical Address Space):
- 大小由物理内存容量决定,设地址线有
m
位,则包含 M = 2 m M=2^m M=2m 个物理地址,范围从0
到 M − 1 M−1 M−1。 - 所有进程共享物理地址空间。
- 大小由物理内存容量决定,设地址线有
- 多对一映射: 同一个物理内存单元可以被不同进程的不同虚拟地址映射(用于共享),或者在不同时间被同一进程的不同虚拟地址映射。
四、 用户视角:内存 API (Memory API)
-
栈 (Stack): 用于存储局部变量、函数参数、返回地址等。通常由编译器自动管理,向下增长。
-
堆 (Heap): 用于动态内存分配。由程序员通过库函数管理,向上增长。
malloc()
: 分配指定大小的未初始化内存。free()
: 释放之前malloc
分配的内存。calloc()
: 分配内存并初始化为 0。realloc()
: 调整已分配内存的大小(可能涉及数据拷贝)。
-
malloc
/free
与系统调用:malloc
和free
是 C 库函数,不是系统调用。- 库函数内部维护数据结构来管理可用内存块。
- 当库函数发现现有空闲空间不足以满足
malloc
请求时,会调用brk
或mmap
系统调用向操作系统申请更多内存(扩大堆区域)。 free
通常只是将内存标记为可用,并放回库函数的内部空闲列表,不一定立即归还给操作系统。
-
brk
系统调用: 用于改变堆顶指针 (program break) 的位置,从而扩大或缩小堆区。一般由内存分配库函数调用,不建议用户程序直接调用。
五、 虚拟内存的核心动机 (Why VM?)
- 高效利用 DRAM (VM as a Cache):
- 将 DRAM 视为磁盘上更大虚拟地址空间的一个缓存 (Cache)。
- 只将程序当前需要的活动部分保留在 DRAM 中,其余部分放在磁盘上。
- 克服物理内存大小的限制,支持运行比物理内存更大的程序。
- 简化内存管理 (VM as a Management Tool):
- 统一的地址空间: 每个进程都看到一个相同的、线性的、从 0 开始的虚拟地址空间布局(代码段、数据段、堆、共享库、栈等总在相似的虚拟地址范围)。这极大地简化了链接 (Linking) 和加载 (Loading) 过程。
- 简化内存分配: OS 为进程分配物理内存页,并将它们映射到进程的虚拟地址空间。虚拟地址空间中连续的页面在物理内存中不必连续,便于 OS 寻找空闲物理页。
- 简化共享: 通过让不同进程的虚拟页面映射到同一个物理页面,可以轻松实现代码(如共享库)和数据(如 IPC 机制)的共享。
- 提供内存保护 (VM as a Protection Tool):
- 地址空间隔离: 每个进程的页表是独立的,确保一个进程不能访问其他进程的物理内存。
- 权限控制: 页表项 (PTE) 中可以包含权限位 (如读
READ
, 写WRITE
, 执行EXECUTE
, 内核态访问SUPERVISOR
)。 - 硬件检查: MMU 在每次地址翻译时都会检查访问权限。
- 违例处理: 如果发生权限冲突(如用户态代码试图写入只读页面,或访问内核空间),MMU 会触发保护故障 (Protection Fault) (一种页错误),内核的故障处理程序会检查原因,若确实违规,则向进程发送
SIGSEGV
(Segmentation Fault) 信号,通常导致进程终止。
六、 VM 作为 DRAM 缓存的机制
- 分页 (Paging):
- 虚拟内存和物理内存都被划分为固定大小的块,称为页 (Page)。
- 虚拟内存中的页称为虚拟页 (Virtual Page, VP)。
- 物理内存中的页称为物理页 (Physical Page, PP) 或页帧 (Page Frame)。
- 页是数据传输的基本单位(在磁盘和内存之间,或内存和缓存之间)。页大小通常是 4KB 到 2MB。
- 页表 (Page Table):
- 每个进程一个页表,存储在内核管理的内存中。
- 是一个页表条目 (Page Table Entry, PTE) 的数组。
- 映射: 页表建立了虚拟页号 (VPN) 到物理页号 (PPN) 或磁盘地址的映射。
- PTE 结构 (简化):
- 有效位 (Valid Bit): 指示该虚拟页当前是否缓存在物理内存 (DRAM) 中。
1
表示有效(在内存中),0
表示无效(不在内存中,可能在磁盘上或未分配)。 - 物理页号 (PPN): 如果有效位为 1,这里存储对应的物理页号。
- 磁盘地址: 如果有效位为 0 且页面已被分配并换出,这里可能存储页面在磁盘上的位置信息。
- 权限位: R, W, X, S 等。
- 有效位 (Valid Bit): 指示该虚拟页当前是否缓存在物理内存 (DRAM) 中。
- 页面状态 (Page States):
- 未分配 (Unallocated): VM 系统尚未创建或分配的 VP,不占用磁盘或内存空间,对应 PTE 通常为 null 或标记为无效。
- 未缓存 (Uncached): 已分配的 VP,但当前不在物理内存中,其内容在磁盘上,对应 PTE 的有效位为 0。
- 已缓存 (Cached): 已分配的 VP,当前在物理内存中,对应 PTE 的有效位为 1,并包含有效的 PPN。
- 页命中 (Page Hit):
- CPU 访问一个 VA,对应的 VP 恰好在物理内存中 (PTE 有效位为 1)。
- MMU 利用 PTE 中的 PPN 和 VA 中的页内偏移量计算出 PA,访问物理内存。
- 缺页故障 (Page Fault / Page Miss):
- CPU 访问一个 VA,对应的 VP 不在物理内存中 (PTE 有效位为 0)。
- MMU 无法完成地址翻译,触发一个异常 (Exception),称为缺页故障,将控制权转移给内核的缺页处理程序。
- 处理流程:
- 选择牺牲页 (Victim Page): 如果物理内存已满,内核需要选择一个当前在内存中的物理页作为牺牲品(基于某种页面替换算法,如 LRU)。
- 写回 (Write Back): 如果牺牲页被修改过(“脏页”),需要先将其内容写回到磁盘。
- 加载 (Load): 内核从磁盘读取所需的虚拟页内容到被选中的物理页帧中。
- 更新页表: 修改磁盘换入页的 PTE(设置有效位为 1,填入 PPN)和(如果发生替换)牺牲页的 PTE(设置有效位为 0)。
- 重启指令: 内核处理完成后,返回到用户进程,重新执行导致缺页的指令。此时,由于页面已在内存中,会发生页命中。
- 分配新页 (Allocating Pages):
- 当进程需要新的内存页时(如
malloc
首次访问新申请的堆空间),OS 需要在虚拟地址空间中分配 VP,可能需要在磁盘上为其预留空间,并更新页表(初始时标记为未缓存,有效位为 0)。首次访问该 VP 会触发缺页故障,将其加载到内存。
- 当进程需要新的内存页时(如
- 局部性原理 (Locality):
- 虚拟内存之所以高效,是因为程序通常表现出时间局部性(刚访问过的内存很可能再次访问)和空间局部性(访问某内存位置后很可能访问其附近位置)。
- 这使得程序在任何时刻实际需要的活动页面集合(工作集, Working Set)通常远小于其总的虚拟地址空间,并且常常能装入物理内存。
- 颠簸 (Thrashing): 如果系统中所有进程的工作集之和远大于物理内存,会导致页面频繁地换入换出,系统大部分时间在进行磁盘 I/O 而不是计算,性能急剧下降。