参考: 中国科学院大学 2025年秋《高级操作系统教程》课件
历史
最早期的计算机系统
- 硬件: 物理内存容量小
- 软件
- 单个应用程序 + (简单)操作系统
- 直接面对物理内存编程
- 各自使用物理内存的一部分
多道编程时代
- 多用户多程序: 计算机很昂贵,多人同时使用(远程连接)
- 分时复用 CPU 资源: 保存恢复寄存器速度很快
- 分时复用物理内存资源: 将全部内存写入磁盘开销太高
- 多个进程同时使用、各占一部分物理内存: 没有安全性(隔离性)
IBM 360 内存隔离: Protection Key 机制
- 内存被划分为一个个大小为 2KB 的内存块(Block)
- 每个内存块有一个 4-bit 的 key,保存在寄存器中
- 1MB 内存需要 256 个保存 key 的寄存器,占 256-Byte
- 内存变大,则需要改 CPU 以增加 key 寄存器
- 每个进程对应一个 key
- CPU 用另一个专门的寄存器,保存当前运行进程的 key
- 不同进程的 key 不同
- 当一个进程访问一块内存时
- CPU 检查进程的 key 与内存的 key 是否匹配
- 不匹配则拒绝内存访问
缺点:
- 不具有可扩展性,随着内存扩大,需要的 key 寄存器太多,无法实现
- 可以将 key 放到内存中实现
- 加载内存的定位问题: 绝对内存寻址在不同进程上的行为不同
- 这个问题本质上来源于直接使用物理地址寻址
直接使用物理地址寻址的问题:
- 干扰性:一个应用会因其他应用的加载而受到影响
- 扩展性:一旦物理地址范围确定,则很难使用更大范围的内存
- 安全性:一个应用可通过自身的内存地址,猜测其他应用的位置
硬件虚拟地址寻址机制
分段
分段机制常见于 x86 平台: 现代操作系统通常不再依赖分段
- x86 ISA 中 GDT 和 LDT
虚拟地址
- 虚拟地址分为:段号 + 段内地址
- 段号:由操作系统决定
- 段内地址:由编译器生成
虚拟地址转换
维护一个段表(由操作系统配置)
翻译时由 CPU 负责查表并转换
分段机制的问题:
- 对物理内存连续性的要求: 物理内存也必须以段为单位进行分配
- 存在问题:内存利用率
- 外部碎片:段与段之间留下碎片空间
- 内部碎片:段内部预留未使用的碎片空间
分页
虚拟地址
- 虚拟地址空间划分成连续的、等长的虚拟页
- 物理内存也被划分成连续的、等长的物理页
- 虚拟页和物理页的页长相等
- 虚拟地址分为:虚拟页号 + 页内偏移
- 使用页表记录虚拟页号到物理页号的映射
不同 ISA 的页表基地址:
- RISC-V: satp 寄存器
- x86-64: CR3 寄存器
- AARCH64(Arm): 两个寄存器:TTBR0_EL1 & TTBR1_EL1
- 根据虚拟地址第63位选择
- 以Linux为例
- 应用程序(地址首位为0):使用 TTBR0_EL1
- 操作系统(地址首位为1):使用 TTBR1_EL1
- 系统调用中不需要切换页表
- RISC-V 和 x86 都使用单个页表基地址寄存器,在系统调用过程需要切换页表基地址寄存器
- 将内核映射到虚拟地址高位可以避免 TLB flush
分页机制的特点
- 物理内存离散分配
- 任意虚拟页可以映射到任意物理页
- 大大降低对物理内存连续性的要求
- 主存资源易于管理,利用率更高
- 按照固定页大小分配物理内存
- 能大大降低外部碎片和内部碎片
- 被现代处理器和操作系统广泛采用
TLB 优化
降低 TLB flush 开销
新的硬件特性 ASID(RISC-V): Address Space ID
- OS 为不同进程分配8位或16位 ASID (SATP 的第 44-59 位)
- TLB 的每一项也会缓存 ASID
- 地址翻译时,硬件会将 TLB entry 的 ASID 与 SATP 的 ASID 对比
- 若不匹配,则 TLB miss
使用了 ASID 之后:
- 切换页表(即切换进程)后,不再需要刷掉 TLB,提高性能
- 但修改页表映射后,仍需刷掉 TLB
多核场景下的 TLB 实现
- OS 修改页表后,需要清空其它核的 TLB 吗?
- 需要,因为一个进程可能在多个核上运行
- OS 如何知道需要清空哪些核的 TLB?
- 操作系统知道进程调度信息
- OS如何清空其它核的TLB?
- x86_64: 发送 IPI 中断某个核,通知它主动清空
- AARCH64: 可在 local CPU 上清空其它核 TLB
- 调用的 ARM 指令: TLBI ASIDE1IS
- RISC-V:SFENCE.VMA
- rs1 针对页表,指示了页表哪个虚拟地址对应的转换被修改了
- rs2 给出了被修改页表的进程的地址空间标识符 (ASID)
- • SBI call + IPI 实现 remote sfence
操作系统对虚拟内存的管理
段
OS 采用段来管理虚拟地址
- 段内连续,段与段之间非连续
- 合法虚拟地址段:代码、数据、堆、栈
- 非法虚拟地址段:未映射
一旦访问,则触发 segfault
1 | +---------+ |
VMA
在 Linux 中通过 vm_area_struct(VMA)结构体管理记录进程的合法虚拟地址空间
1 | +---------+ +--------------------- -+ |
VMA 的添加:
- OS 在创建应用程序时分配
- 数据(对应 ELF 段)
- 代码(对应 ELF 段)
- 栈(初始无内容)
- 应用程序主动向 OS 发出请求
- brk()(扩大、缩小堆区域)
- 可选策略: OS 也可以在创建应用时分配初始的堆 VMA
- mmap()
- 申请空的虚拟内存区域 (匿名映射)
- 申请映射文件数据的虚拟内存区域
- brk()(扩大、缩小堆区域)
VMA 和作用:便于操作系统更快速高效的完成地址的检查(使用页表则慢的多)
- 不落在 VMA 区域的访存,则为非法
物理页面的分配
操作系统何时分配物理页:
- 立即映射:每个虚拟页都对应了一个物理内存页
- 延迟映射(延迟映射/按需调页, DEMAND PAGING):有些虚拟页不对应任何物理内存页
- 对应的数据在磁盘上
- 没有对应的数据(初始化为0)
- 通过缺页异常处理函数映射
- 优势:节约内存资源
- 劣势:缺页异常导致访问延迟增加
- 权衡优化(预取 Prefault): 每次 page fault 载入连续的多个页,减少 fault 次数
OS 内存管理中的其他机制
- 共享内存: 多个进程共享一段物理内存
- 写时拷贝(copy-on-write): 共享读,写触发 page fault 处理时拷贝
- 内存去重(memory deduplication)
- 基于 copy-on-write 在内存中扫描发现具有相同内容的物理页面时执行去重
- 操作系统发起,对用户态透明
- 例:Linux KSM, kernel same-page merging
- 内存压缩
- 当内存资源不充足的时候, 选择将一些“最近不太会使用”的内存页进行数据压缩,从而释放出空闲内存
- Windows 10
- 压缩后的数据仍然存放在内存中
- 当访问被压缩的数据时,操作系统将其解压即可
- Linux
- zswap:换页过程中磁盘的缓存
- 将准备换出的数据压缩并先写入 zswap 区域 (内存)
- 好处:减少甚至避免磁盘I/O;增加设备寿命
- 大页的使用
- 好处
- 减少TLB缓存项的使用,提高 TLB 命中率
- 减少页表的级数,提升遍历页表的效率
- 案例
- 提供API允许应用程序进行显示的大页分配
- 透明大页(Transparent Huge Pages) 机制
- 弊端
- 未使用整个大页而造成物理内存资源浪费
- 增加管理内存的复杂度
- 不同 ISA 对页面大小的支持不同
- RISC-V: 1G, 2M, 4K
- x86: 4K
- AARCH64
- TCR_EL1 可以配置 3 种:4K, 16K, 64K
- 4K + 大页:2M/1G
- 16K + 大页:32M
- 64K + 大页:512M
- 好处