(三)内存那些事儿:内存分配管理

内存分配管理

引言:从单任务到多任务的内存管理挑战

在早期的单任务计算机系统中,内存管理相对简单:整个内存空间被单一程序占用,程序启动时将代码和数据从存储设备装入内存即可。然而,随着计算机应用复杂性的增加,出现了两个核心挑战:

  1. 多任务并发需求:如何让多个程序同时在内存中运行?
  2. 动态内存需求:程序运行时如何动态申请和释放内存?

为解决这些问题,现代操作系统发展出了复杂而精巧的内存分配与管理机制

内存装入 vs 内存分配:概念辨析

在深入讨论之前,需要明确两个相关但不同的概念:

内存装入(Loading)

  • 核心任务:将程序从存储设备转移到内存中
  • 发生时机:程序启动时
  • 管理范围:整个程序的内存布局(代码段、数据段、栈、堆的初始分配)
  • 典型操作:建立虚拟地址空间、分配初始内存段

内存分配(Allocation)

  • 核心任务:程序运行时动态申请和释放内存
  • 发生时机:程序执行过程中
  • 管理范围:主要是堆内存的动态管理
  • 典型操作:malloc/new 申请、free/delete 释放

关键理解:栈上变量的分配不属于动态内存分配范畴,因为栈空间在进程创建时已预分配(Windows 默认 1MB,Linux 默认 8MB),局部变量通过栈指针移动完成分配和释放。


核心知识点体系

1. 内存布局基础:系统的内存视图

1.1 系统区与用户区的分离

┌─────────────────────────────────────────────┐
│                系统区                        │  ← 操作系统内核、驱动程序
├─────────────────────────────────────────────┤
│        栈(向下增长)                        │  ← 函数调用、局部变量
├─────────────────────────────────────────────┤
│      内存映射区                             │  ← 共享库、文件映射
├─────────────────────────────────────────────┤
│        堆(向上增长)                        │  ← 动态分配(malloc/new)
├─────────────────────────────────────────────┤
│         BSS段                              │  ← 未初始化全局变量
├─────────────────────────────────────────────┤
│        数据段                               │  ← 已初始化全局变量
├─────────────────────────────────────────────┤
│        代码段                               │  ← 可执行指令、常量
└─────────────────────────────────────────────┘

系统区存在的意义

  • 操作系统本身也是程序,需要常驻内存管理系统资源
  • 进程切换时系统区内容保持不变,确保操作系统连续运行
  • 提供进程间的共享基础设施

1.2 用户区内存段的功能分工

  • 代码段:存储可执行指令,通常只读且可共享
  • 数据段/BSS 段:存储全局变量,程序启动时分配
  • :动态内存分配区域,向高地址增长
  • :函数调用管理,向低地址增长
  • 内存映射区:文件映射、共享内存、动态库

2. 连续分配:多任务的初步解决方案

2.1 分配方式的演进

单程序分配 → 固定分区分配 → 动态分区分配 → 非连续分配
     ↓             ↓              ↓            ↓
  简单直接      支持多任务      减少浪费      解决碎片

2.2 连续分配的局限性

分配方式 优点 缺点 适用场景
单程序分配 实现简单,无碎片 不支持多任务 嵌入式系统
固定分区分配 支持多任务,管理简单 内存浪费,分区固定 早期多任务系统
动态分区分配 按需分配,减少浪费 外部碎片,扩展困难 资源受限环境

2.3 碎片问题的产生

  • 内部碎片:分配块内部的未使用空间
  • 外部碎片:分配块之间的空闲空间过小,无法利用
  • 碎片化后果:即使总空闲空间足够,也无法满足大块内存需求

关键认识:连续分配的根本问题是将逻辑连续性与物理连续性绑定,限制了内存使用的灵活性。


3. 非连续分配:现代内存管理的核心

3.1 设计思想的转变

核心洞察:程序需要的是逻辑上连续的地址空间,而非物理上连续的内存。

程序视角:连续的逻辑地址空间
    ↓
地址转换机制(MMU)
    ↓
物理内存:可以非连续存储

3.2 地址映射机制的两种思路

  1. 纯映射规则:制定通用转换公式

    • 问题:不同程序的地址布局差异巨大,无法用统一规则处理
    • 结论:理论上不可行
  2. 纯映射表:建立地址对应关系表

    • 问题:32 位系统需要 4GB 映射表,内存开销巨大
    • 结论:实践中不可行
  3. 混合方案:映射表 + 映射规则

    • 核心思想:将地址空间划分为固定大小的单元,用映射表记录单元映射,用规则计算单元内偏移
    • 实现:分页管理和分段管理

4. 分段管理:基于程序逻辑结构

4.1 分段的自然性

程序天然具有段结构:代码段、数据段、栈段、堆段,各段功能不同,不要求连续存储。

4.2 分段地址转换

逻辑地址:(段号, 段内偏移)
    ↓
查段表:段号 → 段基址 + 段长度
    ↓
物理地址:段基址 + 段内偏移

4.3 分段的优势与局限

优势

  • 符合程序逻辑结构
  • 支持段级权限控制(代码段只读、数据段可写)
  • 便于段间共享

局限

  • 段内仍需连续内存
  • 动态扩展受限(如堆空间增长)
  • 外部碎片问题依然存在

5. 分页管理:现代内存管理的基石

5.1 分页的核心机制

设计理念:将地址空间划分为固定大小的页面(通常 4KB),实现细粒度的非连续分配。

逻辑地址拆分:
逻辑地址 A = 页号 × 页大小 + 页内偏移
页号 p = ⌊A / 2^n⌋
页内偏移 d = A mod 2^n

地址转换:
物理地址 P = 物理页框号 × 页大小 + 页内偏移
         = f × 2^n + d

5.2 页表机制

页表的作用:建立逻辑页号到物理页框号的映射关系

MMU 硬件支持

  • 快速地址转换,避免软件开销
  • 管理页表状态位(存在位、访问位、修改位、权限位)
  • 触发缺页中断

MMU(内存管理单元)是一个硬件组件,其主要功能是实现逻辑地址和物理地址的转换,并管理页表的状态位。

在地址转换过程中,MMU 依据预先设定的规则和存储在内存中的页表等数据结构,将 CPU 发出的逻辑地址快速准确地转换为物理地址,使得程序能够正确访问内存中的数据。

同时,MMU 管理的页表状态位包含多种重要信息。例如,可读可写标志位决定了 CPU 对特定内存区域的访问权限。如果代码段对应的页表项设置为只读,当程序试图对该区域进行写操作时,MMU 会检测到并产生一个异常,通知操作系统进行处理,从而防止程序对关键代码的误修改,保证系统的稳定性和安全性。

又如,“是否还没有加载到实际内存中”标志位,MMU 通过该标志位可以快速判断数据当前的存储位置。当 CPU 访问一个逻辑地址,MMU 从页表项获取该标识位后,如果发现数据在硬盘而非内存中,就会触发相应机制,通知操作系统将数据从硬盘交换到内存,更新标识位,然后再进行地址转换和访问,确保程序能够顺利获取所需数据。总之,MMU 在操作系统的内存管理中起着至关重要的作用,是实现高效、安全内存访问的关键硬件支持。

那么这个翻译是怎么实现的?查页表,对于每个程序,内存管理单元 MMU 都为其保存一个页表,该页表中存放的是虚拟页面到物理页面的映射。每当为一个虚拟页面 📃 寻找到一个物理页面之后,就在页表里增加一条记录来保留该映射关系。当然,随着虚拟页面进出物理内存,页表的内容也会不断更新变化。(逻辑页号,页内偏移地址->查进程页表,得物理页号->物理地址)。这个过程通常由处理器的硬件直接完成,不需要软件参与。通常,操作系统只需在进程切换时,把进程页表的首地址装入处理器特定的寄存器中即可。一般来说,页表存储在主存之中。这样处理器每访问一个在内存中的操作数,就要访问两次内存 1.用来查找页表将操作数的逻辑地址变换为物理地址;2.完成真正的读写操作 这样做时间上耗费严重。为缩短查找时间,可以将页表从内存装入 CPU 内部的 关联存储器中,实现按内容查找(hash 表的方法)。(任务都在 CPU 中完成,缩短时间)

5.3 多级页表:解决页表空间问题

问题:32 位系统单级页表需要 4MB,100 个进程需要 400MB 存储页表

解决方案:二级页表

一级页表:1024个页表项,每项指向一个二级页表
二级页表:1024个页表项,每项指向一个物理页框

空间节省原理:程序通常只使用部分地址空间,对应的二级页表可以不存在。


6. 虚拟内存:突破物理限制

6.1 交换空间机制

核心思想:利用存储设备扩展内存容量

物理内存 + 交换空间 = 虚拟内存容量

使用内存的逻辑地址还有一个好处,即逻辑地址不受实际硬件限制,可以有无限大。基于这个特点,可以解决物理内存地址不够用的情况。直观来看,即便多核心 CPU 一次也仅使用几片内存空间,不会同时占用整个内存空间。若存在一种机制,能将内存空间与硬盘空间进行交换,即把当前不用的内存数据存储到硬盘空间,就可为真正需要使用的内存腾出空间,待需要时再交换回来。这就是交换空间的概念,交换空间是硬盘上的一块区域,用于临时存储内存中暂时不用的数据。

操作系统为每个逻辑地址维护一个标识位,用于标记该逻辑地址对应的内存数据当前是在内存还是硬盘上。通常,这个标识位存储在内存管理单元(MMU)相关的数据结构中。MMU 负责处理地址转换,它需要快速获取这个标识位信息以决定如何处理地址访问请求。例如,在分页系统中,标识位可以与页表项关联存储。当 CPU 访问一个逻辑地址时,MMU 首先从页表项中获取该标识位。若标识位表明数据在内存,直接通过上述映射机制转换为物理地址进行访问;若标识位表明数据在硬盘上,操作系统会先将数据从硬盘交换到内存,更新标识位,然后再进行地址转换和访问。

实现机制

  1. 页表项标识页面位置(内存/磁盘)
  2. 访问不在内存的页面触发缺页中断
  3. 操作系统将页面从磁盘调入内存
  4. 必要时将其他页面置换到磁盘

6.2 页面置换算法

在分页存储管理系统中,由于内存空间有限,而进程运行时可能需要访问大量页面,当内存中没有空闲物理页框,且又需要调入新页面时,就需要将内存中已有的某些页面替换出去,这一过程称为页面置换。

页面置换的目标是尽可能准确地选择那些未来一段时间内不会被访问,或者在近期内不会再被频繁访问的页面进行替换,以此来减少因页面频繁调入调出(抖动)对系统性能造成的负面影响,保证系统高效稳定运行。

为了实现这一目标,人们设计了多种页面置换算法。例如先进先出(FIFO)算法,它按照页面进入内存的先后顺序进行置换,即最早进入内存的页面会最先被换出。虽然该算法实现简单,但它没有考虑到页面的实际使用情况,可能会把一些经常被访问的页面换出,导致缺页率较高。

最近最久未使用(LRU)算法则是基于这样一种假设:如果一个页面在过去很长时间内都没有被访问,那么在未来它被访问的可能性也较小。因此,LRU 算法会选择将最近最久未使用的页面换出。然而,实现 LRU 算法通常需要额外的硬件支持或复杂的软件机制来记录页面的访问时间。

还有时钟(Clock)算法,它是对 FIFO 算法的一种改进。该算法为每个页面设置一个访问位,通过循环扫描页面,将访问位为 0 的页面换出,同时在扫描过程中对访问位为 1 的页面将其访问位清 0 。这种算法相对简单且开销较小,在实际应用中较为常见。

算法 策略 优点 缺点
FIFO 最先进入的页面最先置换 实现简单 可能置换频繁使用页面
LRU 最近最久未使用页面置换 符合程序局部性 实现复杂,开销大
Clock 循环扫描,置换访问位为 0 的页面 开销适中,效果较好 近似 LRU,有一定误差

6.3 缺页中断处理流程

当进程试图访问的页面不在内存中时,便会引发缺页中断。缺页中断是一种特殊的中断,与其他硬件中断不同,它是在执行指令过程中由软件引发的。

具体来说,当处理器根据逻辑地址访问内存时,首先会通过页表查找对应的物理页框号。如果在页表中发现该页表项的存在位为 0,表示该页面不在内存中,此时就会触发缺页中断。

缺页中断发生后,操作系统会暂停当前进程的执行,转而执行缺页中断处理程序。缺页中断处理程序的主要任务是将所需页面从外存调入内存。这一过程可能涉及到查找页面在磁盘等外存设备中的位置、申请空闲物理页框、将页面数据读入内存等一系列操作。

在页面成功调入内存后,操作系统会更新页表,将该页面的存在位设为 1,并恢复被中断进程的执行,使得进程能够继续访问所需页面。

缺页中断频繁发生会严重影响系统性能,因为每次缺页中断都伴随着磁盘 I/O 操作,而磁盘 I/O 的速度相比内存访问速度要慢得多。因此,合理的页面置换算法以及适当的内存分配策略对于减少缺页中断次数,提升系统整体性能至关重要。

访问页面 → 检查页表 → 页面不在内存 → 缺页中断
    ↓
保存进程状态 → 查找页面位置 → 选择置换页面(如需要)
    ↓
磁盘I/O读取页面 → 更新页表 → 恢复进程执行

7. 段页式存储:结合两者优势

7.1 组合设计思路

程序逻辑结构 → 分段管理 → 段级权限控制
     ↓              ↓
   段内细分      分页管理 → 减少碎片,提高利用率

7.2 三级地址转换

逻辑地址:(段号, 页号, 页内偏移)
    ↓
段表查找:段号 → 页表基址
    ↓
页表查找:页号 → 物理页框号
    ↓
物理地址:物理页框号 × 页大小 + 页内偏移

8. 堆内存分配:动态内存管理

8.1 堆管理器的两级结构

程序申请内存 → 堆管理器 → 操作系统内核
     ↓              ↓            ↓
  字节级请求     页内分配      页级分配

8.2 分配过程

  1. 程序请求:调用 malloc/new 申请 N 字节
  2. 堆查找:在堆管理器维护的空闲链表中查找合适块
  3. 空间不足:向操作系统申请新的页面
  4. 页面分配:操作系统分配物理页框,更新页表
  5. 块划分:堆管理器从新页面中划分出请求大小的块

8.3 堆管理算法

  • 首次适应:找到第一个足够大的空闲块
  • 最佳适应:找到最小的满足需求的空闲块
  • 最差适应:找到最大的空闲块进行分割
  • 伙伴系统:按 2 的幂次大小管理内存块

知识点关联关系

1. 技术演进的逻辑脉络

多任务需求 → 连续分配方案 → 碎片问题 → 非连续分配
     ↓              ↓            ↓           ↓
内存效率   →   分段管理   →   分页管理  →  段页结合
     ↓              ↓            ↓           ↓
容量限制   →   虚拟内存   →   页面置换  →  现代内存管理

2. 核心概念的相互依赖

  • 虚拟地址空间非连续分配 的基础抽象
  • 页表机制 支撑 地址转换虚拟内存
  • MMU 硬件 加速 地址转换 并触发 缺页中断
  • 页面置换 基于 页表状态位 实现 虚拟内存
  • 堆管理器分页机制 之上实现 动态分配

3. 性能与复杂度的权衡

简单性 ←→ 功能性
连续分配     非连续分配

硬件成本 ←→ 性能提升
软件实现     MMU硬件

内存占用 ←→ 管理效率
直接映射     多级页表

实际应用与性能考量

1. 现代系统的内存管理策略

  • Linux:伙伴系统 + slab 分配器
  • Windows:堆管理器 + 虚拟内存管理器
  • 移动系统:低内存优化 + 压缩技术

2. 性能优化原则

  • 局部性原理:时间局部性和空间局部性指导页面置换
  • 预取策略:预测程序行为,提前加载页面
  • 内存压缩:压缩不常用页面而非置换到磁盘
  • 大页支持:减少页表开销,提高 TLB 命中率

3. 内存碎片的现代解决方案

  • 内存紧凑:移动进程合并空闲空间
  • 垃圾回收:自动释放不再使用的内存
  • 内存池技术:预分配固定大小的内存块

总结:现代内存管理的核心思想

  1. 抽象与映射:通过虚拟地址空间抽象物理内存,用地址转换实现灵活映射
  2. 分层管理:操作系统管理页面,堆管理器管理应用内存,各司其职
  3. 硬件协同:MMU 硬件加速地址转换,软硬结合提升性能
  4. 按需分配:基于程序行为模式,实现内存的按需加载和释放
  5. 局部性优化:利用程序访问的时空局部性,优化缓存和置换策略

现代内存管理通过这些机制的有机结合,实现了高效、安全、灵活的内存使用,是操作系统核心功能的重要体现。




    Enjoy Reading This Article?

    Here are some more articles you might like to read next:

  • (七)内核那些事儿:操作系统对网络包的处理
  • (六)内核那些事儿:文件系统
  • (五)内核那些事儿:系统和程序的交互
  • (四)内核那些事儿:设备管理与驱动开发
  • (三)内核那些事儿:CPU中断和信号