内存管理(一)—— 内存管理的概念

17 阅读8分钟

1. 什么是内存?

1.1 内存的本质

内存,通常指 Random Access Memory (RAM) ,即随机存取存储器。它是计算机中用于临时存放正在运行的程序和数据的硬件设备。我们购买手机、电脑时常见的参数——8GB、16GB、32GB 内存——指的正是 RAM 的容量。

一般(冯·诺依曼体系结构的典型实现中),程序在执行之前,其指令和数据必须先从外存加载到内存中,随后 CPU 才能从内存中取出指令并执行。

CPU 通常不能直接从外存执行程序,根本原因在于两者之间存在巨大的速度差异:CPU 访问寄存器和高速缓存的延迟通常只有纳秒(ns)级,而内存访问约为几十纳秒,SSD约为几十微秒(μs),机械硬盘则约为几毫秒(ms)。这种访问延迟相差可达数千倍至数百万倍。

如果 CPU 每执行一条指令都需要等待外存完成读写,大部分时间都将处于等待状态,计算性能将无法发挥。因此,内存作为计算机的主存储器,承担着保存正在运行程序和数据的任务,在 CPU 与外存之间建立了一层高速存储,使程序能够高效运行。

不同存储层采用的物理介质、组织方式和访问机制不同,导致“信号传播时间、并行访问能力、以及完成一次访问所需的控制步骤”差异巨大。在实际开发中,大量性能调优本质上都是为了尽量减少对慢速外存的访问,将热点数据留在内存中。

1.2 内存编址与寻址能力

存储单元:为了使 CPU 能够准确定位并访问其中的每一份数据,内存被划分为一个个小房间,每个小房间就是一个存储单元(用于存放数据的最小单元)。

地址编号:为了区分数据存放的位置,需要给每个存储单元进行地址编号。这个编号称为内存地址,每个地址对应一个存储单元。

一个存储单元能存多少数据取决于计算机的编址方式:

编址方式一个存储单元的大小说明
按字节编址1 字节(Byte)= 8 位(bit)现代计算机最常见
按字编址1 个字(Word)字长由体系结构决定,如 16 位、32 位或 64 位

为什么早期的 32 位系统最多只能识别 4GB 内存?

假设一台计算机按字节编址,其内存地址本身是用二进制表示的。如果 CPU 的地址总线只有 3232 位,那么它最多能表示 2322^{32} 个不同的地址编号。因此,受限于寻址能力,即使你插上 8GB8\text{GB} 的内存条,操作系统也只能使用前 4GB4\text{GB} 的空间。

指针大小与寻址空间

在 C/C++ 中,sizeof(void*) 的返回值直接反映了系统的寻址能力:32 位系统下为 4 字节,64 位系统下为 8 字节。这就是为什么 32 位程序无法使用超过 4GB 的内存——因为指针本身就没有足够的位数来表示更大的地址。

1.3 结合内存理解指令的工作原理

CPU 只认识二进制机器指令,每条指令完成一个具体操作。高级语言经编译后变成等价的机器指令序列。

一条机器指令 = 操作码(做什么)+ 操作数(对谁做)。程序运行时,指令存放在代码段,变量存放在数据段,CPU 从代码段取指、访问数据段执行。

例如 x = x + 1; 编译后可能生成:

步骤指令执行过程
MOV从内存地址 79 读取 x 到寄存器 R3
ADDR3 中的值 +1
MOV将 R3 结果写回内存地址 79

即:内存(79) → R3 → 运算 → R3 → 内存(79)

CPU 不能直接在内存中运算,必须先把数据搬到寄存器,算完再搬回去。这是由硬件设计决定的——运算器(ALU)只与寄存器相连,不与内存直接相连。

寄存器分配与性能

寄存器数量极少(x86-64 仅 16 个),但访问速度是内存的 ~100 倍。编译器的核心优化策略就是寄存器分配——决定哪些变量常驻寄存器,哪些放在内存:

for (int i = 0; i < 1000000; i++) {
    sum += arr[i];
}

sum 和 i 被分配到寄存器,100 万次循环中几乎不碰内存;arr[i] 每次从内存读取。这就是为什么编译器的图着色/线性扫描算法比手动指定 register 关键字更优——它能综合变量生命周期和使用频率做出更全局的决策。

2. 内存管理要做什么?

操作系统作为系统资源的管理者,需要对包括内存在内的各种软硬件资源进行管理。那么操作系统在管理内存时需要做哪些事情呢?

操作系统在内存管理层面,本质上是在解决四个核心工程问题:分配与回收地址扩充地址转换内存保护

2.1 内存空间的分配与回收

各种进程想要投入运行,需要先把进程相关的数据放入内存。内存中有的区域已经被分配出去,有的区域还在空闲,操作系统需要回答以下问题:

  • 怎么管理这些空闲或非空闲的区域?
  • 新进程的数据应该放在内存的什么位置?
  • 进程运行结束后,之前占有的内存空间怎么回收?

这些都是操作系统需要负责的问题,因此内存管理的第一件事就是内存空间的分配与回收。后续详细介绍

2.2 内存空间的扩充(实现虚拟性)

一个有趣的问题:如果电脑内存只有 4GB,但有一个超过 60GB 的游戏要运行,理论上需要把 60GB 数据全部放入内存,这怎么可能呢?

答案是采用了虚拟技术,这是操作系统虚拟性的体现。操作系统对内存进行管理时,需要提供某种技术,从逻辑上对内存空间进行扩充,把物理上很小的内存拓展为逻辑上很大的内存。

2.3 地址转换

为了让编程人员更方便,程序员写程序时应该只关注指令数据的逻辑地址,而逻辑地址到物理地址的转换应该由操作系统来负责。

历史上演进过三种装入与转换方式:

装入方式转换时机使用场景与特点
绝对装入编译时单道程序阶段(无操作系统),直接写死物理地址,毫无灵活性。
可重定位装入(静态重定位)装入时早期多道批处理系统。程序装入内存时一次性转换完毕,运行期间不能移动。
动态运行时装入(动态重定位)运行时现代操作系统采用的方式。程序在内存中可以移动,需要硬件机制支持。

2.4 内存保护

内存保护是指操作系统要保证各个进程在各自的存储空间内运行,互不干扰。

在内存中,空间通常被严格划分为:

  • 内核空间(Kernel Space) :操作系统核心使用的内存区域,拥有最高特权。
  • 用户空间(User Space) :普通用户程序使用的内存区域。

如果进程一可以随意访问操作系统的内存,或篡改其他进程的数据,系统将面临极大的安全崩溃风险。因此,硬件层面提供了两种经典的保护机制:

方法一:上下限寄存器

在 CPU 中设置一对上限寄存器和下限寄存器,分别存储该进程被允许访问的物理地址上限和下限。CPU 寻址时会进行比对,只有在两者之间才允许访问。

方法二:重定位寄存器 + 界地址寄存器

  • 重定位寄存器(基址寄存器) :存放进程的起始物理地址。
  • 界地址寄存器(限长寄存器) :存放该进程最大的逻辑长度。

判断流程:

  1. 访问时,先将逻辑地址与界地址寄存器的值对比,若超出最大长度则抛出越界异常。
  2. 若合法,再将逻辑地址与重定位寄存器的值相加,得到实际的物理地址去访问。

本节内容是为了让大家对内存管理形成一个整体框架,后续章节会详细介绍内存空间的分配与回收、内存空间扩充等相关策略。