为什么?

你有没有想过,为什么一个几十GB的大型游戏,可以在只有16GB内存的电脑上流畅运行?按照最原始的想法,程序必须全部装进内存才能执行,那GTA5岂不是需要几十GB的内存?

答案藏在操作系统内存管理的进化史中——从固定分区到虚拟内存,人类花了半个多世纪解决了这个难题。

本文将从最基础的内存管理方法开始,一步步剖析现代操作系统如何利用请求分页虚拟内存技术,让有限的内存运行看似无限的软件。


1. 早期内存管理:连续分配时代

在计算机诞生初期,内存容量极小(以KB计),程序也相对简单。操作系统需要管理这宝贵的内存资源,让多个程序能够并发运行。最初的方法是连续分配,即每个程序必须占用一段连续的内存空间。

1.1 固定分区(Fixed Partitioning)

基本思想

系统启动时,管理员将内存预先划分成若干个大小固定的分区。这些分区可以大小相等,也可以大小不等(如一个大的给大型程序,几个小的给小型程序)。每个分区最多装入一道程序,程序必须整体装入某个分区。

地址转换与保护

固定分区需要硬件支持:每个分区有一对基址寄存器界限寄存器。程序装入分区后,基址寄存器存放该分区的起始地址,界限寄存器存放分区大小。CPU产生的逻辑地址与界限比较,若越界则产生中断;否则加上基址得到物理地址。

优缺点
  • 优点:实现简单,分配和回收算法直接,支持多道程序。
  • 缺点
    • 内部碎片:如果程序小于所在分区,分区内剩余空间无法被其他程序使用,造成浪费。例如分区大小为4MB,程序只有2MB,则浪费2MB。
    • 程序大小受限:即使整个内存空闲总量足够,只要程序超过最大分区的大小,就无法运行。例如内存共64MB,分为4个16MB分区,空闲3个共48MB,但程序大小为20MB,无法装入任何单个分区。
    • 多道程序度固定:最大并发程序数等于分区数,灵活性差。
历史注记

固定分区在IBM OS/360等早期大型机操作系统中广泛应用。当时的管理员需要在系统启动前手工规划分区大小,如果某天突然来了一个大程序,只能重启系统重新分区——这在现代看来几乎是不可思议的。

1.2 可变分区(Variable Partitioning / Dynamic Partitioning)

基本思想

不再预先划分固定分区,而是根据程序的实际大小动态地从空闲内存中划分出一块连续区域分配给它。内存中分区的大小和数量随程序装入和退出而动态变化。

分配算法

操作系统维护一个空闲分区表或链表,采用以下常见算法选择空闲区:

  • 首次适应(First Fit):从低地址开始找到第一个足够大的空闲区。实现简单,但可能把大空闲区切小,导致后续大程序难以找到合适空间。
  • 最佳适应(Best Fit):找到大小最接近程序需求的空闲区。优点是可以留下大的空闲区,但容易产生大量难以利用的小碎片。
  • 最坏适应(Worst Fit):找到最大的空闲区。优点是剩余的空闲区可能还可以再分配,但大空闲区被快速消耗,不利于大程序。
  • 循环首次适应(Next Fit):从上次分配位置继续搜索,避免低地址区频繁被切割。
碎片问题:外部碎片

可变分区没有内部碎片,因为分配大小正好等于程序大小(或稍加对齐)。但会产生外部碎片:随着程序装入和退出,内存中形成许多零散的空闲区,它们总和可能很大,但每一块都不足以装入一个新程序。例如内存空闲总空间为10MB,但分散成5个2MB的小块,而新程序需要5MB连续空间,无法分配。

解决外部碎片:紧凑(Compaction)

操作系统可以定期将内存中的程序移动,使所有空闲区合并成一大块连续区域。但紧凑需要动态重定位技术支持,且移动程序会消耗CPU时间,通常成本较高,只在必要时进行。

一个生动的类比

固定分区和可变分区都要求程序占用连续内存一次性全部装入。这就像去图书馆借书,必须找到一整排连续的空书架,而且要把整套书一次性全部摆上去。当程序大小超过物理内存或找不到足够大的连续空间时,程序就无法运行。


2. 分页技术的引入:打破连续性的限制

为了摆脱“连续内存”的束缚,计算机科学家发明了分页(Paging)。分页的核心思想是:将内存和程序都划分为固定大小的块,程序不必连续存放,可以分散到内存的各个角落。

2.1 分页的基本概念

  • 页框(Page Frame):将物理内存划分为大小相等的块,称为页框(或物理块、帧)。
  • 页(Page):将程序的逻辑地址空间划分为同样大小的块,称为页(或页面)。
  • 页和页框大小相同,通常为4KB(也可以为2MB、1GB等大页)。
  • 程序按照页为单位装入内存的页框中,一个页可以装入任意一个空闲页框

2.2 地址结构

程序的逻辑地址由两部分组成:页号P页内偏移量W。例如32位地址,若页面大小为4KB(2^12),则低12位为偏移量,高20位为页号。

物理地址同样由页框号F偏移量W组成。页内偏移量在逻辑地址和物理地址中完全相同,因此地址转换的关键是找到页号对应的页框号。

2.3 页表(Page Table)

操作系统为每个进程维护一张页表,记录该进程每一页对应的页框号。页表存储在内存中,进程运行时,硬件通过页表进行地址转换。

地址转换过程(以单级页表为例):

  1. 从CPU取出逻辑地址,分离出页号P和偏移量W。
  2. 以页号P为索引,查找页表,得到对应的页框号F。
  3. 物理地址 = F × 页面大小 + W。

页表还包含一些控制位:

  • 有效位(Present bit):表示该页是否在内存中(对于请求分页)。
  • 访问位(Accessed bit):用于页面置换算法。
  • 修改位(Dirty bit):表示该页是否被修改过,决定换出时是否需要写回磁盘。
  • 保护位:只读、读写、执行等权限。

2.4 快表(TLB)加速

页表存储在内存中,每次访问数据需要两次内存访问:一次查页表,一次访问数据。为了加速,CPU内部引入了快表(Translation Lookaside Buffer, TLB),缓存最近使用的页表项。命中时只需一次内存访问,大幅提高性能。

TLB的局限性:TLB容量很小(典型值为64~1024个条目),因此对于工作集较大的程序(如数据库、大型游戏),TLB缺失率可能很高。这正是大页(Huge Pages)发挥作用的地方——用2MB或1GB的页面代替4KB页面,同样的TLB条目数可以覆盖更大的地址空间。

2.5 多级页表解决页表过大

对于64位系统,单级页表会占用巨大空间。例如48位逻辑地址,4KB页面,页号有2^36个,每个页表项4字节,单级页表需要2^38字节=256GB,不现实。因此现代系统采用多级页表,将页表本身也分页存储,只将用到的部分调入内存。

x86-64的四级页表

  • 第0级(PTE):指向4KB页框
  • 第1级(PMD):指向一组PTE
  • 第2级(PUD):指向一组PMD
  • 第3级(PGD):指向一组PUD

每次地址转换需要4次内存访问(查4级页表),加上TLB加速,实际开销可控。Intel和AMD后来还引入了五级页表(57位虚拟地址),以支持更大地址空间。

2.6 静态分页:一次性全部装入,但不要求连续

在早期的分页系统中(称为静态分页),程序运行前,所有的页必须全部装入内存。虽然不要求连续,但总量必须装得下。

  • 好处
    • 解决了外部碎片,内存利用率高。
    • 程序可以离散存放,不再受连续空间限制。
  • 代价
    • 有少量内部碎片:程序的最后一页可能填不满一个页框,造成浪费(平均半个页框)。
    • 程序仍然不能超过物理内存总量,而且必须一次性全部装入。如果内存不够装下整个程序,程序就不能运行。
    • 需要页表存储开销和地址转换硬件支持。

静态分页相比固定/可变分区已经进步巨大,但它依然无法运行比物理内存更大的程序。它打破了“连续”限制,但没有打破“全部装入”的限制。


3. 虚拟内存与请求分页:现代操作系统的基石

现代操作系统(Windows、Linux、macOS等)采用请求分页(Demand Paging)技术,实现了虚拟内存(Virtual Memory)。其核心思想是:程序不需要一次性全部装入内存,只需装入当前要用的部分即可

3.1 局部性原理

虚拟内存得以实现的基础是程序运行的局部性原理

  • 时间局部性:程序一旦访问了某个地址,不久后很可能再次访问该地址(如循环)。
  • 空间局部性:程序一旦访问了某个地址,不久后很可能访问其附近的地址(如顺序执行、数组遍历)。

由于局部性,程序在任意一段时间内只需要访问其地址空间的一小部分。因此,我们可以只将这部分装入内存,其余部分留在磁盘,程序就能正常运行。

3.2 请求分页机制

  • 程序开始时,只装入少量必要页面(甚至只有第一页),就可以启动。
  • 当程序访问到一个不在内存中的页时(页表项的有效位为0),硬件会产生缺页中断(Page Fault)
  • 操作系统接管中断,从磁盘(通常是交换区或文件系统)中读取缺失的页,装入一个空闲页框,更新页表,然后重新执行引发缺页的指令。
  • 如果内存中没有空闲页框,操作系统必须置换出一个页面,将其写回磁盘(如果被修改过),腾出空间。

这样,程序感觉自己拥有一个巨大且连续的地址空间(虚拟地址空间),实际上它的数据分散在物理内存和磁盘之间,由操作系统和硬件协同管理。

3.3 缺页中断处理详细流程

当CPU访问一个页面时,硬件首先查询TLB。如果TLB未命中,则查询页表。若页表项显示该页不在内存(有效位=0),则触发缺页中断。缺页中断处理步骤如下:

  1. 陷入内核:保存现场,切换到内核态。
  2. 检查地址合法性:确认该逻辑地址属于进程的地址空间(否则为非法访问,终止进程)。
  3. 查找空闲页框:从空闲页框链表中获取一个空闲页框。如果没有空闲页框,则执行页面置换算法,选择一个牺牲页。
  4. 若牺牲页被修改过,则将其内容写回磁盘(交换区)。
  5. 从磁盘读入缺失页:根据页表项中记录的磁盘地址(或交换区位置),将页面内容读入选定的页框。
  6. 更新页表:将该页表项设置为有效,记录页框号,清除修改位等。
  7. 恢复现场:重新执行引发缺页的指令(因为该指令之前没有完成)。

缺页中断处理可能涉及磁盘I/O,耗时较长(毫秒级),是影响性能的关键因素。

缺页中断的类型

实际上,缺页中断不止一种:

  • 主要缺页(Major Page Fault):需要从磁盘读取数据,耗时最长。
  • 次要缺页(Minor Page Fault):页面已在内存中(如共享库、页面缓存),只需建立页表映射,无需磁盘I/O。
  • 无效缺页(Invalid Page Fault):访问了非法地址,导致段错误(Segmentation Fault)。

3.4 页面置换算法

当内存已满,需要选出一个页面换出。不同的置换算法对性能影响很大。常见算法:

  • 先进先出(FIFO):选择最早装入内存的页面换出。实现简单,但可能换出频繁访问的页面,性能差。存在Belady异常(增加页框数反而缺页率上升)。
  • 最优置换(OPT):选择未来最长时间不会被访问的页面换出。这是理论最优,但无法实现(因为无法预知未来)。用作衡量其他算法的标准。
  • 最近最久未使用(LRU):选择最长时间没有被访问的页面换出。基于局部性原理,性能好,但实现成本高(需要硬件支持记录访问时间或维护栈)。
  • 时钟算法(Clock):近似LRU。为每个页框设置访问位,页面被访问时置1。置换时循环扫描,若访问位为1则置0并跳过,若为0则换出。这是实际系统常用的折中方案。
  • 改进型时钟算法:同时考虑访问位和修改位,优先换出未修改且未被访问的页面,减少写回磁盘次数。
Linux内核实际使用的算法

Linux内核并没有直接使用经典的LRU或时钟算法,而是采用了一种双链表LRU近似算法

  • 活跃链表(active list):存放最近被访问过的页面
  • 非活跃链表(inactive list):存放可能被换出的页面
  • 页面在两条链表之间动态移动,内核通过kswapd守护进程在后台进行页面回收

这种设计兼顾了性能和实现复杂度,也是为什么Linux在各种负载下都有良好表现的原因之一。

3.5 页框分配策略

操作系统需要决定给每个进程分配多少物理页框:

  • 固定分配:每个进程固定分配一定数量的页框。
  • 可变分配:根据进程运行情况和系统负载动态调整。
  • 全局置换:置换时可以抢占其他进程的页框。
  • 局部置换:只能在进程自己的页框内置换。

现代操作系统通常采用可变分配 + 全局置换可变分配 + 局部置换,以平衡系统整体性能。

3.6 抖动与工作集

当系统同时运行太多进程,或某些进程频繁访问大量页面,导致内存中所有页面都被频繁置换,系统的大部分时间花在页面调入调出上,CPU利用率急剧下降,这种现象称为抖动(Thrashing)

防止抖动的有效方法是使用工作集模型(Working Set Model)

  • 工作集:一个进程在某个时间窗口内实际访问的页面集合。
  • 操作系统监控每个进程的工作集大小,确保分配足够的页框容纳其工作集。若进程工作集总和超过物理内存,则暂停部分进程或减少并发度。
如何检测抖动?

在实际系统中,可以通过以下指标判断是否发生抖动:

  • CPU利用率骤降:大量时间花在等待磁盘I/O而非计算
  • 缺页率飙升:每秒缺页中断次数远超正常值
  • 磁盘队列长度增加:swap设备的I/O请求堆积

Linux的vmstat命令可以直观地观察这些指标:si(swap in)和so(swap out)列持续非零,往往意味着内存紧张甚至抖动。


4. 现代操作系统实例:Windows与Linux

4.1 Windows的虚拟内存

  • Windows使用页面文件(pagefile.sys)作为磁盘交换区。当物理内存不足时,系统将部分页面换出到页面文件。
  • 页面文件大小可以自动管理或手动设置。如果页面文件太小,可能导致系统提示“内存不足”。
  • Windows还使用内存压缩(Memory Compression):将不常用的页面压缩后保留在内存中,减少磁盘I/O,提高性能(Windows 10/11)。
  • 任务管理器中的“提交大小”(Commit Size)显示所有进程虚拟内存的总和,“工作集”显示物理内存使用量。
Windows特有的内存管理特性
  • 超级抓取(Superfetch/Prefetch):分析用户使用习惯,预加载常用应用程序和数据到内存,减少启动时间。
  • 工作集修剪(Working Set Trimming):当内存压力增大时,内存管理器会主动修剪进程的工作集,将不活跃页面移出。
  • 软页面错误(Soft Page Fault):当页面在备用列表或修改列表中时,只需更改页表映射,无需磁盘I/O。

4.2 Linux的虚拟内存

  • Linux使用交换分区(swap partition)交换文件(swap file)作为磁盘交换区。
  • Linux内核的页面回收机制会根据内存压力选择匿名页或文件页进行回收。
  • Linux也支持内存压缩(zram/zswap)和透明大页(THP)等优化。
  • 通过free -h命令可以查看物理内存和交换空间的使用情况。
Linux内存管理的实用命令
# 查看内存使用概览
free -h

# 查看详细内存统计(包括slab、page cache等)
cat /proc/meminfo

# 查看进程内存映射
cat /proc/<PID>/maps

# 查看swap使用情况
swapon --show

# 查看缺页中断统计
ps -o minflt,majflt <PID>

4.3 内存映射与文件

现代操作系统还利用内存映射(mmap)将文件直接映射到进程的虚拟地址空间。访问文件就像访问内存一样,缺页时从文件读取,修改后可写回。许多大型游戏使用内存映射来快速加载资源。

mmap vs read/write的性能差异

  • read/write:需要在内核空间和用户空间之间拷贝数据(两次拷贝)
  • mmap:直接将文件页面映射到用户空间,缺页时由内核自动加载(零拷贝,仅一次缺页中断)

对于大型文件的随机访问,mmap通常比传统的read/write快得多。这也是数据库系统(如PostgreSQL、MongoDB)大量使用mmap的原因。


5. 实战:GTA5是怎么运行起来的?

现在我们回到最初的问题:你的电脑只有16GB内存,GTA5安装包可能超过100GB,游戏运行时占用的数据量也远超16GB。它是怎么跑起来的?

5.1 游戏资源加载策略

GTA5等开放世界游戏采用流式加载(Streaming)技术。游戏世界被划分成无数小块(chunks),只有玩家附近区域的模型、纹理、碰撞数据、NPC等需要加载到内存。远处的物体可能只是低精度模型或根本不加载。当玩家移动时,游戏引擎会预判需要哪些资源,提前从磁盘读取,并淘汰不再需要的资源。

流式加载的工程实现
  • 资源分片:游戏世界被划分为网格(如每128m×128m一个区块),每个区块包含独立的模型、纹理、AI数据。
  • LOD(Level of Detail):远处物体使用低多边形模型和低分辨率纹理,近处才加载高精度版本。
  • 预取(Prefetching):根据玩家移动方向和速度,提前预测下一个需要加载的区域,在玩家到达前完成加载。
  • 优先级队列:距离玩家最近的区块具有最高加载优先级,远处区块可以被延迟甚至丢弃。

5.2 操作系统层面的支持

游戏进程启动时,操作系统只加载游戏引擎的核心代码、初始菜单、必要配置文件等少量页面。当游戏开始运行时:

  1. 玩家进入某个区域,游戏引擎请求访问该区域的地图数据,这些数据可能位于硬盘上的.rpf资源包中。
  2. CPU访问到对应的虚拟地址,发现该页不在物理内存中(页表项有效位=0),触发缺页中断
  3. 操作系统从硬盘/SSD读取缺失的页面(可能是从资源包文件中读取并映射),装入空闲页框。如果物理内存已满,则根据置换算法选择一个不常用的页面换出到页面文件或直接丢弃(如果是文件映射且未被修改)。
  4. 游戏继续运行,玩家几乎感觉不到(除非频繁发生,导致卡顿)。
文件映射的优化

GTA5的资源包(.rpf文件)通常通过内存映射(mmap)加载。这意味着:

  • 游戏引擎只需告诉操作系统“我要访问这个文件的这部分”
  • 操作系统自动处理页面调度,无需游戏引擎手动管理内存
  • 当玩家离开某个区域,对应的页面会自动被淘汰(如果内存压力大)或保留在页面缓存中(以备返回)

5.3 为什么大内存和高速SSD很重要?

  • 大内存:可以让更多游戏数据常驻内存,减少缺页中断次数,游戏更流畅。例如16GB内存可以缓存更多纹理和地图数据,减少从SSD读取的频率。
  • 高速SSD:加快缺页处理速度。当缺页发生时,从SSD读取页面只需几十微秒到几百微秒,而机械硬盘需要几毫秒到十几毫秒。高速SSD能显著缩短加载时间,减少“加载中”或卡顿现象。
  • 如果内存太小或硬盘太慢,系统会频繁在内存和磁盘之间倒腾数据,导致抖动,游戏帧率暴跌。
实测数据

以GTA5为例,在不同硬件配置下的体验差异:

配置

内存

硬盘

加载时间

城内帧率稳定性

低配

8GB

HDD

3-5分钟

开车时频繁卡顿

中配

16GB

SATA SSD

1-2分钟

偶尔轻微卡顿

高配

32GB

NVMe SSD

30秒

几乎无卡顿

5.4 扩展:显存与虚拟内存

GTA5还大量使用显存(VRAM)存储纹理、几何数据等。显卡也有自己的内存管理机制,类似地使用虚拟内存技术,当显存不足时,会将部分纹理换出到系统内存甚至磁盘,导致性能下降。因此,大显存显卡对于高分辨率、高画质游戏至关重要。

GPU虚拟内存的演进

现代GPU(NVIDIA、AMD)也实现了完整的虚拟内存系统:

  • GPU页表:类似于CPU页表,将GPU虚拟地址映射到显存或系统内存
  • 统一内存(Unified Memory):NVIDIA Pascal架构及以后,CPU和GPU共享同一套虚拟地址空间,数据在两者之间自动迁移
  • 显存压缩:GPU也使用无损压缩技术(如NVIDIA的Delta Color Compression)来节省显存带宽

对于GTA5而言,如果显存不足(如4GB显存开4K分辨率),GPU会频繁地将纹理换入换出,导致帧率剧烈波动。这也是为什么高端显卡不仅需要强大的计算单元,还需要充足的显存。


6. 总结:内存管理的进化史

阶段

核心技术

是否要求连续内存

程序能否超过物理内存

主要问题

固定分区

预先划分固定大小分区

内部碎片,灵活性差,程序受最大分区限制

可变分区

按程序大小动态划分连续区域

外部碎片,需紧凑,分配算法复杂

静态分页

分页,一次性全部装入

少量内部碎片,仍需全部装入,程序不能超过物理内存

请求分页(现代)

分页 + 按需调页 + 缺页中断 + 页面置换

缺页开销,可能抖动,需要复杂置换算法

未来的发展方向

内存管理技术仍在不断演进,以下是几个值得关注的方向:

  1. 持久内存(Persistent Memory):Intel Optane等新型存储介质,介于DRAM和SSD之间,有望重塑内存层级结构。
  2. 内存分解(Memory Disaggregation):通过RDMA/CXL等技术,将内存池化,让多台机器共享物理内存。
  3. 异构内存管理:CPU、GPU、NPU等不同计算单元共享统一内存视图,减少数据搬运开销。
  4. 机器学习驱动的页面预取:利用ML模型预测程序的访存模式,提前加载所需页面,减少缺页延迟。

从固定分区到虚拟内存,操作系统的内存管理经历了从“简单粗暴”到“精细智能”的演变。今天,我们能够同时运行浏览器、音乐播放器、游戏和编程工具,正是得益于这些看似枯燥的底层机制在默默工作。

下次当你流畅地驰骋在洛圣都的街头时,不妨想想背后那些忙碌的页表、缺页中断和页面置换算法——它们才是真正的“无名英雄”。


如果你觉得这篇文章有帮助,欢迎点赞、评论和分享!

有任何关于操作系统或计算机基础的问题,也欢迎在评论区留言讨论。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐