04

内存抽象

每个进程都觉得自己拥有全部内存

阅读量:3 · 预计 16 分钟读完

虚拟内存分页
阅读进度5%

第四章 内存抽象

导读

内存是计算机中最快的存储介质,但也是最昂贵和最有限的资源。如何高效、安全地管理内存,是操作系统面临的核心挑战之一。内存抽象(Memory Abstraction)是操作系统虚拟化的重要组成部分——它通过一系列硬件和软件机制,将有限的物理内存转化为每个进程独享的、巨大的虚拟地址空间。

本章将系统介绍内存抽象的演进历程。我们将从最简单的基址寄存器和界限寄存器开始,逐步深入到分段(Segmentation)、分页(Paging)、TLB(Translation Lookaside Buffer)等现代内存管理的核心技术。理解这些技术的工作原理,不仅有助于深入理解操作系统的内存管理机制,也能为编写高性能程序提供重要指导。


4.1 地址空间的概念

4.1.1 什么是地址空间

地址空间(Address Space)是进程可以访问的所有内存地址的有序集合。在32位系统中,地址空间的大小为 2^32 = 4GB,地址范围从 0x00000000 到 0xFFFFFFFF。在64位系统中,理论地址空间为 2^64 = 16EB。

操作系统为每个进程创建独立的地址空间,使得每个进程都认为自己独占了整个内存。这种抽象带来了两个关键好处:

简化编程:程序员不需要关心物理内存的实际布局,可以使用从0开始的连续地址。

进程隔离:每个进程有独立的地址空间,不能访问其他进程的内存,保证了安全性和稳定性。

4.1.2 虚拟地址与物理地址

在支持内存虚拟化的系统中,程序使用的地址是虚拟地址(Virtual Address),而实际访问内存时需要转换为物理地址(Physical Address)。这种转换由硬件的内存管理单元(MMU, Memory Management Unit)完成。

  • 虚拟地址:程序看到的地址,由CPU生成。
  • 物理地址:实际的内存地址,用于访问物理内存。
  • 地址转换:虚拟地址 → 物理地址的映射过程。

地址转换对程序是透明的——程序不需要知道地址转换的存在,它仍然使用虚拟地址编程。地址转换由硬件自动完成,速度极快(通常只需几个时钟周期)。


4.2 基址寄存器和界限寄存器

4.2.1 最简单的内存虚拟化

最早期的内存虚拟化方案使用两个硬件寄存器:基址寄存器(Base Register)和界限寄存器(Bounds/Limit Register)。

  • 基址寄存器:记录进程地址空间在物理内存中的起始位置。
  • 界限寄存器:记录进程地址空间的大小(或结束位置)。

4.2.2 地址转换过程

当程序访问虚拟地址 VA 时,MMU 执行以下操作:

检查 VA 是否在合法范围内:0 ≤ VA < 界限寄存器的值。

如果合法,计算物理地址:PA = VA + 基址寄存器的值。

如果不合法,触发异常(地址越界)。

4.2.3 优缺点分析

优点

  • 实现简单,硬件开销小。
  • 提供了基本的内存保护(通过界限寄存器)。

缺点

  • 要求每个进程的地址空间在物理内存中连续存放。
  • 容易产生外部碎片(物理内存中的空闲空间分散,无法满足大进程的需求)。
  • 无法共享内存(多个进程不能共享同一物理内存区域)。

4.3 分段(Segmentation)

4.3.1 分段的动机

基址/界限方案的主要问题是要求地址空间连续。分段(Segmentation)通过将地址空间划分为多个逻辑段(Segment),解决了这个问题。每个段对应地址空间的一个逻辑部分,如代码段、数据段、堆段、栈段等。

4.3.2 分段的工作原理

在分段方案中,虚拟地址由两部分组成:

  • 段号(Segment Number):标识访问哪个段。
  • 段内偏移(Offset):标识段内的具体位置。

每个段有一个段表项(Segment Table Entry),包含:

  • 基址:该段在物理内存中的起始地址。
  • 长度:该段的大小。
  • 保护位:读/写/执行权限。
  • 有效位:该段是否驻留在物理内存中。

地址转换过程:

从虚拟地址中提取段号和偏移。

用段号查找段表,获取该段的基址和长度。

检查偏移是否小于长度(越界检查)。

检查保护位(权限检查)。

计算物理地址:PA = 基址 + 偏移。

4.3.3 分段的优点

  • 消除内部碎片:每个段的大小可以根据需要灵活设置。
  • 支持共享:多个进程的段可以映射到同一物理内存区域(如共享库)。
  • 支持稀疏地址空间:未使用的段不需要分配物理内存。
  • 逻辑组织:段对应程序的逻辑结构(代码、数据、栈等),便于保护和共享。

4.3.4 分段的问题

  • 外部碎片:段的长度不固定,频繁的创建和销毁会导致物理内存中出现大量小的空闲区域。
  • 段表管理:段表可能很大,需要额外的内存存储。
  • 管理复杂度:需要处理段的分配、回收、合并等问题。

4.4 分页(Paging)

4.4.1 分页的基本思想

分页(Paging)是现代操作系统最广泛使用的内存管理方案。它将地址空间划分为固定大小的小块,称为页(Page);将物理内存划分为同样大小的小块,称为页帧(Page Frame)。页和页帧的大小相同,通常为4KB(也有2MB、1GB的大页)。

分页的核心思想是:将进程的虚拟页分散存储在不连续的物理页帧中,通过页表(Page Table) 记录虚拟页到物理页帧的映射关系。

4.4.2 虚拟地址的结构

在分页方案中,虚拟地址被分为两部分:

  • 虚拟页号(Virtual Page Number, VPN):高位部分,标识虚拟页。
  • 页内偏移(Offset):低位部分,标识页内的具体位置。

例如,在4KB页大小的32位系统中:

  • 虚拟地址 = 32位
  • 页大小 = 4KB = 2^12 字节
  • 偏移 = 12位(低12位)
  • VPN = 20位(高20位)
  • 虚拟页数量 = 2^20 = 1,048,576 页

4.4.3 页表

页表(Page Table)是操作系统为每个进程维护的数据结构,记录虚拟页到物理页帧的映射。每个虚拟页对应一个页表项(Page Table Entry, PTE),PTE通常包含以下信息:

  • 物理页帧号(PFN):映射的物理页帧编号。
  • 有效位(Valid Bit):该页是否驻留在物理内存中。
  • 保护位(Protection Bits):读/写/执行权限。
  • 存在位(Present Bit):该页是否在内存中(与有效位类似)。
  • 脏位(Dirty Bit):该页是否被修改过。
  • 引用位(Reference Bit):该页最近是否被访问过。

4.4.4 地址转换过程

从虚拟地址中提取VPN和偏移。

用VPN查找页表,获取对应的PTE。

检查有效位:如果为0,触发缺页异常(Page Fault)。

检查保护位:如果访问权限不满足,触发保护异常。

从PTE中提取PFN。

计算物理地址:PA = PFN × 页大小 + 偏移。

4.4.5 页表的实现

线性页表(Linear Page Table)

  • 最简单的实现:一个数组,以VPN为索引。
  • 优点:查找速度快(O(1))。
  • 缺点:占用内存大。32位系统、4KB页大小时,页表有 2^20 个条目,每个条目4字节,共4MB。

多级页表(Multi-level Page Table)

  • 将页表分层组织,只分配实际使用的页表页面。
  • 例如,两级页表将VPN分为两部分:页目录索引和页表索引。
  • 优点:大幅减少页表的内存占用(稀疏地址空间只占用少量页表页面)。
  • 缺点:地址转换需要多次内存访问(每级一次)。

反向页表(Inverted Page Table)

  • 以物理页帧为索引,记录哪个进程的哪个虚拟页映射到该页帧。
  • 优点:页表大小固定(等于物理页帧数),不受虚拟地址空间大小影响。
  • 缺点:查找速度慢(需要遍历或使用哈希表)。

4.5 TLB(Translation Lookaside Buffer)

4.5.1 TLB的动机

分页方案的一个主要问题是地址转换需要访问页表,而页表存储在内存中。这意味着每次内存访问都需要额外的内存访问来获取物理地址,性能损失严重。

TLB(Translation Lookaside Buffer,地址转换后备缓冲器)是一种硬件缓存,用于加速地址转换。TLB缓存了最近使用的虚拟页到物理页帧的映射,大多数地址转换可以直接在TLB中完成,无需访问内存中的页表。

4.5.2 TLB的工作原理

TLB是一个小型、高速的缓存,通常包含32-128个条目。每个TLB条目包含:

  • 虚拟页号(VPN):标签(Tag)。
  • 物理页帧号(PFN):数据(Data)。
  • 有效位、保护位等

地址转换过程:

CPU生成虚拟地址,提取VPN。

并行查找TLB(硬件自动完成)。

如果TLB命中(TLB Hit),直接获取PFN,计算物理地址。

如果TLB未命中(TLB Miss),访问内存中的页表获取映射,更新TLB,然后计算物理地址。

4.5.3 TLB的组织方式

全相联TLB(Fully Associative)

  • 任何条目可以放在TLB的任何位置。
  • 查找时需要并行比较所有条目。
  • 优点:命中率高。
  • 缺点:硬件复杂度高。

组相联TLB(Set Associative)

  • TLB分为若干组,每个VPN映射到固定的组。
  • 查找时只需比较一组内的条目。
  • 在命中率和硬件复杂度之间取得平衡。

4.5.4 TLB的替换策略

当TLB满且发生TLB Miss时,需要替换一个旧条目。常见的替换策略包括:

  • LRU(Least Recently Used):替换最久未使用的条目。
  • 随机替换:随机选择一个条目替换。
  • FIFO:替换最早进入的条目。

4.5.5 TLB的性能影响

TLB的命中率对系统性能影响巨大。假设TLB命中时间为1个时钟周期,TLB Miss时需要100个时钟周期访问页表:

  • TLB命中率99%:平均访问时间 = 0.99 × 1 + 0.01 × 100 = 1.99 周期
  • TLB命中率95%:平均访问时间 = 0.95 × 1 + 0.05 × 100 = 5.95 周期
  • TLB命中率90%:平均访问时间 = 0.90 × 1 + 0.10 × 100 = 10.9 周期

可以看到,即使TLB命中率从99%降到90%,平均访问时间也增加了约5倍。因此,保持高的TLB命中率对性能至关重要。


4.6 缺页异常与页面置换

4.6.1 缺页异常(Page Fault)

当程序访问的虚拟页不在物理内存中时(PTE的有效位为0),硬件触发缺页异常。操作系统需要介入处理:

确认缺页地址:从硬件寄存器获取触发缺页的虚拟地址。

检查合法性:如果地址非法(如访问未分配的内存),终止进程。

查找空闲页帧:在物理内存中找到一个空闲页帧。

读入页面:从磁盘(交换区或文件)将所需页面读入空闲页帧。

更新页表:设置PTE的有效位和PFN。

重新执行指令:返回用户态,重新执行触发缺页的指令。

4.6.2 页面置换算法

当没有空闲页帧时,操作系统需要选择一个已在内存中的页面换出(Victim Page),腾出空间。页面置换算法的目标是最小化缺页次数。

最优置换(OPT, Optimal)

  • 换出将来最长时间不会被访问的页面。
  • 理论上最优,但无法实现(需要预知未来)。
  • 用作性能比较的基准。

FIFO置换

  • 换出最早进入内存的页面。
  • 实现简单,但可能换出频繁使用的页面。
  • 可能出现Belady异常(增加物理页帧数反而增加缺页次数)。

LRU置换(Least Recently Used)

  • 换出最久未被访问的页面。
  • 基于"局部性原理":最近被访问的页面很可能再次被访问。
  • 性能接近OPT,但实现开销较大(需要记录访问时间或维护栈)。

时钟算法(Clock Algorithm)

  • LRU的近似实现,开销更低。
  • 每个页面有一个引用位,页面排列成环形缓冲区(时钟)。
  • 指针扫描时,如果引用位为1,清零并继续;如果为0,换出该页面。
  • 也称为"二次机会算法"。

4.6.3 抖动(Thrashing)

当系统分配给进程的物理页帧数不足以容纳其工作集(Working Set)时,进程会频繁发生缺页异常,大量时间花在页面换入换出上,CPU利用率急剧下降。这种现象称为抖动

抖动的根本原因是多道程序度过高——系统中同时运行的进程太多,每个进程分到的物理页帧不足。解决方法包括:

  • 降低多道程序度(减少同时运行的进程数)。
  • 使用工作集模型,为每个进程分配足够的页帧。
  • 使用页面故障频率(PFF)策略动态调整页帧分配。

4.7 内存映射文件

4.7.1 mmap() 系统调用

内存映射文件(Memory-Mapped File)允许将文件直接映射到进程的地址空间,使得文件的读写可以像内存访问一样进行。在Linux中,通过 mmap() 系统调用实现:

c
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

4.7.2 内存映射的优势

  • 简化I/O操作:文件读写变成内存读写,无需 read()/write() 系统调用。
  • 减少数据拷贝:文件数据直接从磁盘读入内存映射区域,无需拷贝到用户缓冲区。
  • 共享内存:多个进程可以映射同一文件,实现进程间共享。
  • 按需加载:只有实际访问的页面才会从磁盘加载(利用缺页机制)。

4.8 常见误区

误区一:虚拟内存就是"用磁盘代替内存"

虚拟内存不仅仅是交换(Swap)。它的核心是地址空间的抽象——为每个进程提供独立、连续的地址空间。交换只是在物理内存不足时的一种补充手段。即使系统有足够的物理内存,虚拟内存的地址抽象和保护功能仍然不可或缺。

误区二:分页会浪费大量内存

分页确实存在内部碎片(每个页的最后一部分可能未使用),但内部碎片的平均浪费只有半页(约2KB)。相比之下,分段的外部碎片可能更严重。现代系统还支持大页(2MB或1GB),进一步减少了页表的开销和TLB Miss率。

误区三:TLB Miss的开销可以忽略

TLB Miss的开销远大于普通内存访问。一次TLB Miss可能需要访问多级页表,导致多次内存访问(每次100+时钟周期)。在性能敏感的代码中,应注意数据的局部性,减少TLB Miss。

误区四:64位系统不需要担心内存管理

虽然64位系统的虚拟地址空间非常大(256TB),但物理内存仍然有限。页表的管理、TLB的维护、缺页处理等开销在64位系统中同样重要。此外,大地址空间意味着多级页表的级数可能增加,地址转换的开销也可能增大。

误区五:所有进程的页表都存储在内存中

实际上,只有当前运行进程的页表部分需要常驻内存。其他进程的页表可以部分换出到磁盘。此外,现代系统使用多级页表和按需分配,只有实际使用的虚拟页范围才会分配页表页面。


4.9 实践应用

4.9.1 查看内存信息

bash
# 查看系统内存使用情况
free -h

# 查看详细的内存信息
cat /proc/meminfo

# 查看进程的内存映射
cat /proc/[pid]/maps

# 查看进程的内存使用详情
cat /proc/[pid]/status | grep -i vm

4.9.2 使用大页内存

大页(Huge Pages)可以减少TLB Miss率,提升性能:

bash
# 查看大页配置
cat /proc/meminfo | grep -i huge

# 分配大页(需要root权限)
echo 1024 > /proc/sys/vm/nr_hugepages

# 在程序中使用大页
#include <sys/mman.h>
void *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE,
                 MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB, -1, 0);

4.9.3 内存映射文件示例

c
#include <stdio.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
    int fd = open("data.txt", O_RDONLY);
    struct stat sb;
    fstat(fd, &sb);
    
    // 将文件映射到内存
    void *mapped = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
    
    // 像访问内存一样访问文件
    char *data = (char *)mapped;
    for (int i = 0; i < sb.st_size; i++) {
        putchar(data[i]);
    }
    
    munmap(mapped, sb.st_size);
    close(fd);
    return 0;
}

4.9.4 观察缺页异常

bash
# 查看进程的缺页统计
cat /proc/[pid]/stat | awk '{print "Minor faults: "$10, "Major faults: "$12}'
  • Minor faults(次要缺页):页面在内存中但不在TLB中,或页面在内存中但未映射。
  • Major faults(主要缺页):页面不在内存中,需要从磁盘读入。

4.10 本章小结

本章系统介绍了内存抽象的核心技术,主要内容包括:

地址空间:操作系统为每个进程提供独立的虚拟地址空间,通过地址转换映射到物理内存。

基址/界限寄存器:最简单的内存虚拟化方案,要求地址空间连续,容易产生外部碎片。

分段:将地址空间划分为逻辑段,支持非连续分配和共享,但存在外部碎片问题。

分页:将地址空间划分为固定大小的页,通过页表映射到物理页帧。分页是现代内存管理的基础。

TLB:硬件缓存,加速地址转换。TLB命中率对性能影响巨大。

缺页异常与页面置换:当物理内存不足时,通过缺页机制按需加载页面。页面置换算法(LRU、Clock等)影响系统性能。

内存映射文件:将文件映射到地址空间,简化I/O操作,支持共享。

内存抽象是操作系统虚拟化的核心组成部分。通过分页、TLB、缺页处理等机制,操作系统在提供强大抽象的同时,保持了接近裸机的性能。