【操作系统】存储器管理算法

目录

一、引言

二、连续内存分配算法

核心前提

[1. 首次适应算法(First Fit, FF)](#1. 首次适应算法(First Fit, FF))

原理

特点

适用场景:单道程序系统、对分配速度要求高的简单多道系统。

算法实现思路

Python代码

程序运行结果展示

[2. 循环首次适应算法(Next Fit, NF)](#2. 循环首次适应算法(Next Fit, NF))

原理

特点

适用场景:进程内存需求大小较均匀的多道系统,改进首次适应的碎片问题。

算法实现思路

Python代码

程序运行结果展示

[3. 最佳适应算法(Best Fit, BF)](#3. 最佳适应算法(Best Fit, BF))

原理

特点

适用场景:进程内存需求较小且分布均匀的系统。

算法实现思路

Python代码

程序运行结果展示

[4. 最坏适应算法(Worst Fit, WF)](#4. 最坏适应算法(Worst Fit, WF))

原理

特点

适用场景:存在大量大内存需求进程的系统,优先保证大进程的分配。

算法实现思路

Python代码

程序运行结果展示

连续分配算法对比

三、非连续内存分配算法

[1. 分页存储管理算法](#1. 分页存储管理算法)

核心原理

关键特点

扩展:快表(TLB)优化

适用场景:所有现代通用操作系统(Windows/Linux/UNIX)的基础内存管理方式。

算法实现思路

Python代码

程序运行结果展示

[2. 分段存储管理算法](#2. 分段存储管理算法)

核心原理

关键特点

[适用场景:注重进程逻辑结构、模块共享和内存保护的系统(如早期 UNIX、部分嵌入式 OS)。](#适用场景:注重进程逻辑结构、模块共享和内存保护的系统(如早期 UNIX、部分嵌入式 OS)。)

算法实现思路

Python代码

程序运行结果展示

[3. 段页式存储管理算法](#3. 段页式存储管理算法)

核心原理

关键特点

[适用场景:现代通用操作系统(Windows 10/11、Linux、macOS)的核心存储管理算法,兼顾所有优势。](#适用场景:现代通用操作系统(Windows 10/11、Linux、macOS)的核心存储管理算法,兼顾所有优势。)

算法实现思路

Python代码

程序运行结果展示

四、虚拟存储页面置换算法

核心前提

[1. 最优置换算法(Optimal, OPT)](#1. 最优置换算法(Optimal, OPT))

原理

特点

作用:作为其他实际算法的性能基准,用于对比评价实际算法的优劣。

算法实现思路

Python代码

程序运行结果展示

[2. 先进先出置换算法(First-In First-Out, FIFO)](#2. 先进先出置换算法(First-In First-Out, FIFO))

原理

特点

适用场景:进程页面访问序列较简单、无频繁重复访问的场景,仅用于简单系统。

算法实现思路

Python代码

程序运行结果展示

[3. 最近最少使用置换算法(Least Recently Used, LRU)](#3. 最近最少使用置换算法(Least Recently Used, LRU))

原理

实现方式

特点

[适用场景:现代 OS 的核心置换算法之一(如 Linux 的基础 LRU),兼顾性能和实现难度。](#适用场景:现代 OS 的核心置换算法之一(如 Linux 的基础 LRU),兼顾性能和实现难度。)

算法实现思路

Python代码

程序运行结果展示

[4. 时钟置换算法(CLOCK,最近未使用算法 NRU)](#4. 时钟置换算法(CLOCK,最近未使用算法 NRU))

原理

[扩展:改进型时钟置换算法(二次机会 / 增强型 CLOCK)](#扩展:改进型时钟置换算法(二次机会 / 增强型 CLOCK))

特点

[适用场景:现代通用操作系统(Linux/Windows)的默认页面置换算法(如 Linux 的 CLOCK_V2、Windows 的改进 CLOCK),平衡性能和开销。](#适用场景:现代通用操作系统(Linux/Windows)的默认页面置换算法(如 Linux 的 CLOCK_V2、Windows 的改进 CLOCK),平衡性能和开销。)

算法实现思路

[基础版 CLOCK(仅访问位,核心:第二次机会)](#基础版 CLOCK(仅访问位,核心:第二次机会))

[改进版 CLOCK(访问位 + 修改位,现代 OS 主流)](#改进版 CLOCK(访问位 + 修改位,现代 OS 主流))

Python代码

程序运行结果展示

[5. 最不常用置换算法(Least Frequently Used, LFU)](#5. 最不常用置换算法(Least Frequently Used, LFU))

原理

特点

适用场景:进程页面访问频率相对稳定的场景,如数据库系统。

算法实现思路

Python代码

程序运行结果展示

经典页面置换算法性能对比

五、内存碎片整理与回收算法

[1. 内存回收算法](#1. 内存回收算法)

核心步骤

实现方式

算法实现思路

Python代码

程序运行结果展示

[2. 外部碎片整理算法](#2. 外部碎片整理算法)

原理

特点

适用场景:连续内存分配系统,碎片过多导致无法分配大进程时,偶尔执行(非实时)。

算法实现思路

Python代码

程序运行结果展示

[3. 伙伴系统分配算法(Buddy System)](#3. 伙伴系统分配算法(Buddy System))

原理

特点

[适用场景:现代 OS 的内核内存分配(如 Linux 的 slab 分配器基于伙伴系统),用于分配小的内核对象。](#适用场景:现代 OS 的内核内存分配(如 Linux 的 slab 分配器基于伙伴系统),用于分配小的内核对象。)

算法实现思路

Python代码

程序运行结果展示

[4. 内存池分配算法(Slab/Zone Allocator)](#4. 内存池分配算法(Slab/Zone Allocator))

原理

[经典实现:Linux 的 Slab 分配器](#经典实现:Linux 的 Slab 分配器)

特点

[适用场景:现代 OS 的内核小对象分配(Linux/Windows),是伙伴系统的上层优化。](#适用场景:现代 OS 的内核小对象分配(Linux/Windows),是伙伴系统的上层优化。)

算法实现思路

Python代码

程序运行结果展示

[六、存储器管理算法的实际 OS 应用总结](#六、存储器管理算法的实际 OS 应用总结)

七、总结


一、引言

存储器管理是操作系统的核心功能之一,核心目标是高效利用内存资源解决内存空间不足问题实现进程的内存隔离与地址重定位 ,同时支持虚拟存储以扩展进程可用的地址空间。

存储器管理算法按核心应用场景 可分为四大类:连续内存分配算法 (解决物理内存的连续分区分配)、非连续内存分配算法 (分页 / 分段 / 段页式,现代 OS 核心)、虚拟存储页面置换算法 (解决虚拟内存的页调度)、内存碎片整理与回收算法 (优化内存利用率)。本文对每类算法的原理、特点、适用场景、经典实现 进行详细讲解,兼顾理论和实际 OS 应用,以及Python代码完整实现

二、连续内存分配算法

早期操作系统(单道 / 简单多道程序)采用连续内存分配 ,即给每个进程分配一段连续的物理内存分区 ,核心是解决空闲分区的选择问题 。该类算法的核心问题是会产生外部碎片 (分区间的未利用内存)和内部碎片(分区内未利用的内存)。

核心前提

系统维护一个空闲分区表 / 空闲分区链,记录物理内存中所有空闲的连续内存块(含起始地址、分区大小),分配时从空闲分区中选择符合进程需求的块,回收时合并相邻的空闲分区。

1. 首次适应算法(First Fit, FF)

原理

物理内存地址从低到高 的顺序遍历空闲分区,选择第一个大小≥进程所需内存的空闲分区,若分区过大则分割为 "进程占用区" 和 "新的空闲分区"。

特点
  • 优点:实现简单、分配速度快(无需遍历所有分区),低地址区保留小分区,高地址区保留大分区;
  • 缺点:低地址区容易产生大量外部碎片(小分区无法分配给大进程),大进程分配效率低。
适用场景:单道程序系统、对分配速度要求高的简单多道系统。
算法实现思路
  • 列表 + 字典 维护空闲分区表 ,记录每个空闲分区的起始地址大小 ,始终按起始地址升序排列(保证低地址到高地址遍历);
  • 用单独的列表维护进程占用表 ,记录已分配进程的ID、起始地址、占用大小,方便后续内存回收;
  • 分配逻辑:按地址从低到高遍历空闲分区,找到第一个大小≥进程需求的分区,若分区过大则分割,更新空闲分区表;
  • 回收逻辑:将进程占用的分区恢复为空闲,合并相邻的空闲分区(解决外部碎片堆积问题),这是操作系统内存管理的核心配套操作。
Python代码
python 复制代码
def init_free_partitions(total_memory):
    """
    初始化空闲分区表:总内存为连续的一个大分区,起始地址0
    :param total_memory: 物理总内存大小(如1024MB)
    :return: 空闲分区表(列表),每个元素是字典{"start": 起始地址, "size": 分区大小}
    """
    return [{"start": 0, "size": total_memory}]


def first_fit_allocate(free_partitions, processes_occupied, pid, need_memory):
    """
    首次适应算法内存分配
    :param free_partitions: 空闲分区表(会被修改)
    :param processes_occupied: 进程占用表(会被修改)
    :param pid: 待分配进程ID(唯一标识)
    :param need_memory: 进程需要的内存大小(>0)
    :return: 分配结果:成功返回True,失败返回False
    """
    # 边界校验:申请内存大小非法
    if need_memory <= 0:
        print(f"【分配失败】进程{pid}申请内存大小{need_memory}非法,必须大于0")
        return False

    # 按【起始地址升序】遍历空闲分区(首次适应核心:低地址→高地址)
    for i in range(len(free_partitions)):
        curr_start = free_partitions[i]["start"]
        curr_size = free_partitions[i]["size"]

        # 找到第一个大小≥需求的空闲分区
        if curr_size >= need_memory:
            print(f"【分配中】找到匹配分区:起始地址{curr_start},大小{curr_size},进程{pid}需要{need_memory}")
            # 1. 记录进程占用信息
            processes_occupied.append({
                "pid": pid,
                "start": curr_start,
                "size": need_memory
            })

            # 2. 若分区大小恰好等于需求,直接移除该空闲分区
            if curr_size == need_memory:
                del free_partitions[i]
            # 3. 若分区过大,分割为「进程占用区」和「新的空闲分区」
            else:
                free_partitions[i]["start"] = curr_start + need_memory  # 空闲分区起始地址后移
                free_partitions[i]["size"] = curr_size - need_memory  # 空闲分区大小缩减

            print(f"【分配成功】进程{pid}已分配:起始地址{curr_start},大小{need_memory}")
            return True

    # 遍历完所有分区,无匹配的空闲分区
    print(f"【分配失败】进程{pid}申请{need_memory},无足够大的空闲分区")
    return False


def memory_recycle(free_partitions, processes_occupied, pid):
    """
    内存回收:释放进程占用的分区,合并相邻空闲分区(关键:解决外部碎片)
    :param free_partitions: 空闲分区表(会被修改)
    :param processes_occupied: 进程占用表(会被修改)
    :param pid: 待回收的进程ID
    :return: 回收结果:成功返回True,失败返回False
    """
    # 查找待回收的进程
    recycle_process = None
    for i in range(len(processes_occupied)):
        if processes_occupied[i]["pid"] == pid:
            recycle_process = processes_occupied[i]
            del processes_occupied[i]  # 从占用表移除
            break

    # 进程ID不存在,回收失败
    if not recycle_process:
        print(f"【回收失败】进程{pid}不存在,无需回收")
        return False

    # 1. 将释放的分区加入空闲分区表
    free_start = recycle_process["start"]
    free_size = recycle_process["size"]
    free_partitions.append({"start": free_start, "size": free_size})
    print(f"【回收中】进程{pid}已释放:起始地址{free_start},大小{free_size}")

    # 2. 按起始地址升序排序(为合并相邻分区做准备)
    free_partitions.sort(key=lambda x: x["start"])

    # 3. 合并相邻的空闲分区(核心:消除相邻外部碎片)
    i = 0
    while i < len(free_partitions) - 1:
        curr = free_partitions[i]
        next_p = free_partitions[i + 1]
        # 判定相邻:当前分区的「起始地址+大小」= 下一个分区的起始地址
        if curr["start"] + curr["size"] == next_p["start"]:
            # 合并为一个大分区
            curr["size"] += next_p["size"]
            del free_partitions[i + 1]
            print(
                f"【碎片合并】合并相邻分区:{curr['start']}({curr['size'] - next_p['size']}) + {next_p['start']}({next_p['size']}) = {curr['start']}({curr['size']})")
        else:
            i += 1  # 不相邻则遍历下一个

    print(f"【回收成功】进程{pid}回收完成,空闲分区表已更新并合并碎片")
    return True


def print_partitions(free_partitions, processes_occupied):
    """辅助打印函数:打印空闲分区表和进程占用表,直观查看内存状态"""
    print("\n" + "-" * 80)
    print("【当前空闲分区表】(按起始地址排序):")
    for p in free_partitions:
        print(f"  起始地址:{p['start']:4d},大小:{p['size']:4d}")

    print("【当前进程占用表】:")
    if not processes_occupied:
        print("  无进程占用内存")
    else:
        for p in processes_occupied:
            print(f"  进程{p['pid']:2d}:起始地址{p['start']:4d},大小{p['size']:4d}")
    print("-" * 80 + "\n")


# -------------------------- 测试用例:模拟内存分配与回收 --------------------------
if __name__ == "__main__":
    # 1. 初始化:总物理内存1024MB,空闲分区表初始为[0,1024]
    TOTAL_MEMORY = 1024
    free_part = init_free_partitions(TOTAL_MEMORY)
    process_occ = []
    print(f"【系统初始化】物理总内存:{TOTAL_MEMORY}MB")
    print_partitions(free_part, process_occ)

    # 2. 依次分配多个进程(模拟多道程序环境)
    first_fit_allocate(free_part, process_occ, 1, 200)  # 进程1:申请200MB
    first_fit_allocate(free_part, process_occ, 2, 300)  # 进程2:申请300MB
    first_fit_allocate(free_part, process_occ, 3, 100)  # 进程3:申请100MB
    print_partitions(free_part, process_occ)

    # 3. 回收进程2(中间分区),模拟进程结束释放内存
    memory_recycle(free_part, process_occ, 2)
    print_partitions(free_part, process_occ)

    # 4. 再次分配进程4(申请250MB),验证首次适应会选择「低地址的回收分区」
    first_fit_allocate(free_part, process_occ, 4, 250)
    print_partitions(free_part, process_occ)

    # 5. 连续回收进程1、4,验证「相邻分区合并」
    memory_recycle(free_part, process_occ, 1)
    memory_recycle(free_part, process_occ, 4)
    print_partitions(free_part, process_occ)

    # 6. 测试分配失败场景(申请超大内存)
    first_fit_allocate(free_part, process_occ, 5, 1000)
    print_partitions(free_part, process_occ)
程序运行结果展示
bash 复制代码
【系统初始化】物理总内存:1024MB

--------------------------------------------------------------------------------
【当前空闲分区表】(按起始地址排序):
  起始地址:   0,大小:1024
【当前进程占用表】:
  无进程占用内存
--------------------------------------------------------------------------------

【分配中】找到匹配分区:起始地址0,大小1024,进程1需要200
【分配成功】进程1已分配:起始地址0,大小200
【分配中】找到匹配分区:起始地址200,大小824,进程2需要300
【分配成功】进程2已分配:起始地址200,大小300
【分配中】找到匹配分区:起始地址500,大小524,进程3需要100
【分配成功】进程3已分配:起始地址500,大小100

--------------------------------------------------------------------------------
【当前空闲分区表】(按起始地址排序):
  起始地址: 600,大小: 424
【当前进程占用表】:
  进程 1:起始地址   0,大小 200
  进程 2:起始地址 200,大小 300
  进程 3:起始地址 500,大小 100
--------------------------------------------------------------------------------

【回收中】进程2已释放:起始地址200,大小300
【回收成功】进程2回收完成,空闲分区表已更新并合并碎片

--------------------------------------------------------------------------------
【当前空闲分区表】(按起始地址排序):
  起始地址: 200,大小: 300
  起始地址: 600,大小: 424
【当前进程占用表】:
  进程 1:起始地址   0,大小 200
  进程 3:起始地址 500,大小 100
--------------------------------------------------------------------------------

【分配中】找到匹配分区:起始地址200,大小300,进程4需要250
【分配成功】进程4已分配:起始地址200,大小250

--------------------------------------------------------------------------------
【当前空闲分区表】(按起始地址排序):
  起始地址: 450,大小:  50
  起始地址: 600,大小: 424
【当前进程占用表】:
  进程 1:起始地址   0,大小 200
  进程 3:起始地址 500,大小 100
  进程 4:起始地址 200,大小 250
--------------------------------------------------------------------------------

【回收中】进程1已释放:起始地址0,大小200
【回收成功】进程1回收完成,空闲分区表已更新并合并碎片
【回收中】进程4已释放:起始地址200,大小250
【碎片合并】合并相邻分区:0(200) + 200(250) = 0(450)
【碎片合并】合并相邻分区:0(450) + 450(50) = 0(500)
【回收成功】进程4回收完成,空闲分区表已更新并合并碎片

--------------------------------------------------------------------------------
【当前空闲分区表】(按起始地址排序):
  起始地址:   0,大小: 500
  起始地址: 600,大小: 424
【当前进程占用表】:
  进程 3:起始地址 500,大小 100
--------------------------------------------------------------------------------

【分配失败】进程5申请1000,无足够大的空闲分区

--------------------------------------------------------------------------------
【当前空闲分区表】(按起始地址排序):
  起始地址:   0,大小: 500
  起始地址: 600,大小: 424
【当前进程占用表】:
  进程 3:起始地址 500,大小 100
--------------------------------------------------------------------------------

2. 循环首次适应算法(Next Fit, NF)

原理

对首次适应算法的改进:从上一次分配成功的分区下一个位置开始,循环遍历空闲分区,选择第一个符合要求的分区。

特点
  • 优点:空闲分区分布更均匀,减少低地址区的碎片堆积,大进程分配概率更高;
  • 缺点:仍会产生外部碎片,且可能导致大分区被分割为小分区。
适用场景:进程内存需求大小较均匀的多道系统,改进首次适应的碎片问题。
算法实现思路
  • 复用首次适应的空闲分区表 (按起始地址升序)、进程占用表数据结构,保证内存管理的一致性;
  • 新增上一次分配索引last_allocate_idx),用列表存储单个整数 (Python 中列表是可变对象,支持函数内修改),初始值为-1(表示从未分配过,第一次从 0 开始遍历);
  • 分配核心 :从last_allocate_idx + 1开始遍历,遍历到空闲分区表末尾后回到表头继续遍历,直到找到第一个匹配分区或回到起始位置;
  • 分配成功后 :更新last_allocate_idx为当前分配的分区索引,保证下次从该位置的下一个开始;
  • 回收逻辑 :与首次适应一致,但回收合并后会校验last_allocate_idx的合法性(避免因分区合并 / 删除导致索引越界);
  • 循环遍历实现 :通过分段遍历(先从上次位置下一个到末尾,再从 0 到上次位置)实现高效循环,无需重复遍历。
Python代码
python 复制代码
def init_free_partitions(total_memory):
    """
    初始化空闲分区表:总内存为连续的一个大分区,起始地址0
    :param total_memory: 物理总内存大小(如1024MB)
    :return: 空闲分区表(列表),按起始地址升序排列
    """
    return [{"start": 0, "size": total_memory}]


def next_fit_allocate(free_partitions, processes_occupied, last_allocate_idx, pid, need_memory):
    """
    循环首次适应算法内存分配(核心:从上次分配位置下一个开始循环遍历)
    :param free_partitions: 空闲分区表(会被修改)
    :param processes_occupied: 进程占用表(会被修改)
    :param last_allocate_idx: 上一次分配成功的索引【可变列表,如[-1]】,函数内会修改
    :param pid: 待分配进程ID(唯一标识)
    :param need_memory: 进程需要的内存大小(>0)
    :return: 分配结果:成功返回True,失败返回False
    """
    # 边界校验:申请内存大小非法
    if need_memory <= 0:
        print(f"【分配失败】进程{pid}申请内存大小{need_memory}非法,必须大于0")
        return False
    # 无空闲分区,直接失败
    if not free_partitions:
        print(f"【分配失败】进程{pid}申请{need_memory},无任何空闲分区")
        return False

    partition_num = len(free_partitions)
    start_idx = last_allocate_idx[0] + 1  # 本次遍历的起始位置(上次位置+1)
    allocate_idx = -1  # 本次匹配的分区索引

    # 核心:循环遍历(先从start_idx到末尾,再从0到last_allocate_idx[0])
    # 分段遍历1:start_idx → 分区表末尾
    for i in range(start_idx, partition_num):
        if free_partitions[i]["size"] >= need_memory:
            allocate_idx = i
            break
    # 分段遍历2:若第一段未找到,从0 → start_idx-1(循环回到表头)
    if allocate_idx == -1 and start_idx > 0:
        for i in range(0, start_idx):
            if free_partitions[i]["size"] >= need_memory:
                allocate_idx = i
                break

    # 遍历完所有分区,无匹配的空闲分区
    if allocate_idx == -1:
        print(f"【分配失败】进程{pid}申请{need_memory},无足够大的空闲分区")
        return False

    # 找到匹配分区,执行分配逻辑
    curr_start = free_partitions[allocate_idx]["start"]
    curr_size = free_partitions[allocate_idx]["size"]
    print(f"【分配中】找到匹配分区:索引{allocate_idx},起始地址{curr_start},大小{curr_size},进程{pid}需要{need_memory}")

    # 1. 记录进程占用信息
    processes_occupied.append({
        "pid": pid,
        "start": curr_start,
        "size": need_memory
    })

    # 2. 若分区大小恰好等于需求,直接移除该空闲分区
    if curr_size == need_memory:
        del free_partitions[allocate_idx]
        # 移除后,后续索引前移,更新last_idx(避免越界)
        last_allocate_idx[0] = allocate_idx - 1 if allocate_idx > 0 else partition_num - 2
    # 3. 若分区过大,分割为「进程占用区」和「新的空闲分区」
    else:
        free_partitions[allocate_idx]["start"] = curr_start + need_memory
        free_partitions[allocate_idx]["size"] = curr_size - need_memory
        # 更新上一次分配索引为本次匹配的索引(核心)
        last_allocate_idx[0] = allocate_idx

    # 处理last_idx越界(如分区表只剩1个,删除后索引为-1)
    if last_allocate_idx[0] >= len(free_partitions):
        last_allocate_idx[0] = len(free_partitions) - 1
    print(f"【分配成功】进程{pid}已分配:起始地址{curr_start},大小{need_memory},更新上次分配索引为{last_allocate_idx[0]}")
    return True


def memory_recycle(free_partitions, processes_occupied, last_allocate_idx, pid):
    """
    内存回收:释放进程占用分区,合并相邻空闲分区,同时校验上次分配索引合法性
    :param free_partitions: 空闲分区表(会被修改)
    :param processes_occupied: 进程占用表(会被修改)
    :param last_allocate_idx: 上一次分配成功的索引【可变列表】,会做越界校验
    :param pid: 待回收的进程ID
    :return: 回收结果:成功返回True,失败返回False
    """
    # 查找待回收的进程
    recycle_process = None
    for i in range(len(processes_occupied)):
        if processes_occupied[i]["pid"] == pid:
            recycle_process = processes_occupied[i]
            del processes_occupied[i]
            break

    # 进程ID不存在,回收失败
    if not recycle_process:
        print(f"【回收失败】进程{pid}不存在,无需回收")
        return False

    # 1. 将释放的分区加入空闲分区表
    free_start = recycle_process["start"]
    free_size = recycle_process["size"]
    free_partitions.append({"start": free_start, "size": free_size})
    print(f"【回收中】进程{pid}已释放:起始地址{free_start},大小{free_size}")

    # 2. 按起始地址升序排序(为合并相邻分区做准备)
    free_partitions.sort(key=lambda x: x["start"])

    # 3. 合并相邻的空闲分区(消除相邻外部碎片)
    i = 0
    while i < len(free_partitions) - 1:
        curr = free_partitions[i]
        next_p = free_partitions[i + 1]
        # 判定相邻:当前分区的「起始地址+大小」= 下一个分区的起始地址
        if curr["start"] + curr["size"] == next_p["start"]:
            # 合并为一个大分区
            curr["size"] += next_p["size"]
            del free_partitions[i + 1]
            print(
                f"【碎片合并】合并相邻分区:{curr['start']}({curr['size'] - next_p['size']}) + {next_p['start']}({next_p['size']}) = {curr['start']}({curr['size']})")
        else:
            i += 1  # 不相邻则遍历下一个

    # 关键:回收合并后校验上次分配索引,避免越界
    if last_allocate_idx[0] >= len(free_partitions):
        last_allocate_idx[0] = len(free_partitions) - 1 if free_partitions else -1
    print(f"【回收成功】进程{pid}回收完成,空闲分区表已更新,上次分配索引校准为{last_allocate_idx[0]}")
    return True


def print_partitions(free_partitions, processes_occupied, last_allocate_idx):
    """辅助打印函数:打印空闲分区表、进程占用表、上次分配索引,直观查看内存状态"""
    print("\n" + "-" * 90)
    print(f"【当前状态】上次分配成功索引:{last_allocate_idx[0]} | 空闲分区数:{len(free_partitions)}")
    print("【空闲分区表】(按起始地址排序):")
    for idx, p in enumerate(free_partitions):
        print(f"  索引{idx:2d}:起始地址{p['start']:4d},大小{p['size']:4d}")

    print("【进程占用表】:")
    if not processes_occupied:
        print("  无进程占用内存")
    else:
        for p in processes_occupied:
            print(f"  进程{p['pid']:2d}:起始地址{p['start']:4d},大小{p['size']:4d}")
    print("-" * 90 + "\n")


# -------------------------- 测试用例:模拟循环首次适应的分配/回收 --------------------------
if __name__ == "__main__":
    # 1. 初始化:总物理内存1024MB,空闲分区表初始为[0,1024],上次分配索引[-1](可变列表)
    TOTAL_MEMORY = 1024
    free_part = init_free_partitions(TOTAL_MEMORY)
    process_occ = []
    last_idx = [-1]  # 用列表存,支持函数内修改(Python核心技巧)
    print(f"【系统初始化】物理总内存:{TOTAL_MEMORY}MB")
    print_partitions(free_part, process_occ, last_idx)

    # 2. 第一次批量分配:验证从0开始,依次分配后更新last_idx
    next_fit_allocate(free_part, process_occ, last_idx, 1, 200)  # 进程1:申请200MB → 匹配索引0,last_idx更新为0
    next_fit_allocate(free_part, process_occ, last_idx, 2, 300)  # 进程2:申请300MB → 从1开始(0+1),匹配索引0,last_idx更新为0
    next_fit_allocate(free_part, process_occ, last_idx, 3, 100)  # 进程3:申请100MB → 从1开始,匹配索引0,last_idx更新为0
    print_partitions(free_part, process_occ, last_idx)

    # 3. 回收进程2(中间分区),模拟进程结束释放内存,校准last_idx
    memory_recycle(free_part, process_occ, last_idx, 2)
    print_partitions(free_part, process_occ, last_idx)

    # 4. 再次分配进程4(250MB):核心验证→从last_idx+1=1开始,而非低地址0
    next_fit_allocate(free_part, process_occ, last_idx, 4, 250)
    print_partitions(free_part, process_occ, last_idx)

    # 5. 继续分配进程5(50MB):从新的last_idx+1开始,循环匹配
    next_fit_allocate(free_part, process_occ, last_idx, 5, 50)
    print_partitions(free_part, process_occ, last_idx)

    # 6. 回收进程1+4,合并相邻分区,再次分配进程6(300MB):验证循环遍历到表头
    memory_recycle(free_part, process_occ, last_idx, 1)
    memory_recycle(free_part, process_occ, last_idx, 4)
    next_fit_allocate(free_part, process_occ, last_idx, 6, 300)
    print_partitions(free_part, process_occ, last_idx)

    # 7. 测试分配失败场景(申请超大内存)
    next_fit_allocate(free_part, process_occ, last_idx, 7, 1000)
    print_partitions(free_part, process_occ, last_idx)
程序运行结果展示
bash 复制代码
【系统初始化】物理总内存:1024MB

------------------------------------------------------------------------------------------
【当前状态】上次分配成功索引:-1 | 空闲分区数:1
【空闲分区表】(按起始地址排序):
  索引 0:起始地址   0,大小1024
【进程占用表】:
  无进程占用内存
------------------------------------------------------------------------------------------

【分配中】找到匹配分区:索引0,起始地址0,大小1024,进程1需要200
【分配成功】进程1已分配:起始地址0,大小200,更新上次分配索引为0
【分配中】找到匹配分区:索引0,起始地址200,大小824,进程2需要300
【分配成功】进程2已分配:起始地址200,大小300,更新上次分配索引为0
【分配中】找到匹配分区:索引0,起始地址500,大小524,进程3需要100
【分配成功】进程3已分配:起始地址500,大小100,更新上次分配索引为0

------------------------------------------------------------------------------------------
【当前状态】上次分配成功索引:0 | 空闲分区数:1
【空闲分区表】(按起始地址排序):
  索引 0:起始地址 600,大小 424
【进程占用表】:
  进程 1:起始地址   0,大小 200
  进程 2:起始地址 200,大小 300
  进程 3:起始地址 500,大小 100
------------------------------------------------------------------------------------------

【回收中】进程2已释放:起始地址200,大小300
【回收成功】进程2回收完成,空闲分区表已更新,上次分配索引校准为0

------------------------------------------------------------------------------------------
【当前状态】上次分配成功索引:0 | 空闲分区数:2
【空闲分区表】(按起始地址排序):
  索引 0:起始地址 200,大小 300
  索引 1:起始地址 600,大小 424
【进程占用表】:
  进程 1:起始地址   0,大小 200
  进程 3:起始地址 500,大小 100
------------------------------------------------------------------------------------------

【分配中】找到匹配分区:索引1,起始地址600,大小424,进程4需要250
【分配成功】进程4已分配:起始地址600,大小250,更新上次分配索引为1

------------------------------------------------------------------------------------------
【当前状态】上次分配成功索引:1 | 空闲分区数:2
【空闲分区表】(按起始地址排序):
  索引 0:起始地址 200,大小 300
  索引 1:起始地址 850,大小 174
【进程占用表】:
  进程 1:起始地址   0,大小 200
  进程 3:起始地址 500,大小 100
  进程 4:起始地址 600,大小 250
------------------------------------------------------------------------------------------

【分配中】找到匹配分区:索引0,起始地址200,大小300,进程5需要50
【分配成功】进程5已分配:起始地址200,大小50,更新上次分配索引为0

------------------------------------------------------------------------------------------
【当前状态】上次分配成功索引:0 | 空闲分区数:2
【空闲分区表】(按起始地址排序):
  索引 0:起始地址 250,大小 250
  索引 1:起始地址 850,大小 174
【进程占用表】:
  进程 1:起始地址   0,大小 200
  进程 3:起始地址 500,大小 100
  进程 4:起始地址 600,大小 250
  进程 5:起始地址 200,大小  50
------------------------------------------------------------------------------------------

【回收中】进程1已释放:起始地址0,大小200
【回收成功】进程1回收完成,空闲分区表已更新,上次分配索引校准为0
【回收中】进程4已释放:起始地址600,大小250
【碎片合并】合并相邻分区:600(250) + 850(174) = 600(424)
【回收成功】进程4回收完成,空闲分区表已更新,上次分配索引校准为0
【分配中】找到匹配分区:索引2,起始地址600,大小424,进程6需要300
【分配成功】进程6已分配:起始地址600,大小300,更新上次分配索引为2

------------------------------------------------------------------------------------------
【当前状态】上次分配成功索引:2 | 空闲分区数:3
【空闲分区表】(按起始地址排序):
  索引 0:起始地址   0,大小 200
  索引 1:起始地址 250,大小 250
  索引 2:起始地址 900,大小 124
【进程占用表】:
  进程 3:起始地址 500,大小 100
  进程 5:起始地址 200,大小  50
  进程 6:起始地址 600,大小 300
------------------------------------------------------------------------------------------

【分配失败】进程7申请1000,无足够大的空闲分区

------------------------------------------------------------------------------------------
【当前状态】上次分配成功索引:2 | 空闲分区数:3
【空闲分区表】(按起始地址排序):
  索引 0:起始地址   0,大小 200
  索引 1:起始地址 250,大小 250
  索引 2:起始地址 900,大小 124
【进程占用表】:
  进程 3:起始地址 500,大小 100
  进程 5:起始地址 200,大小  50
  进程 6:起始地址 600,大小 300
------------------------------------------------------------------------------------------

3. 最佳适应算法(Best Fit, BF)

原理

遍历所有空闲分区,选择大小≥进程需求且最接近进程需求的空闲分区(即 "最小适配"),分割后剩余的空闲分区最小。

特点
  • 优点:最大化利用空闲分区,减少大分区的浪费,产生的外部碎片最小;
  • 缺点:需要遍历所有空闲分区,分配速度慢 ;会产生大量无法利用的极小外部碎片(后续无进程能适配)。
适用场景:进程内存需求较小且分布均匀的系统。
算法实现思路
  • 复用前两种算法的基础数据结构(空闲分区表、进程占用表),保证内存管理逻辑的一致性,降低对比学习成本;
  • 分配核心 :先全量遍历所有空闲分区 ,筛选出所有大小≥进程需求 的 "候选分区",再从候选中选择大小最小的分区(最小适配,最佳适应核心规则);
  • 无候选分区则分配失败,有候选则执行分区分割 / 删除(与前两种算法逻辑一致);
  • 无需额外记录分配状态(如循环首次的 last_idx),但每次分配需全遍历,分配速度慢(算法固有缺点);
  • 回收逻辑完全复用成熟的相邻分区合并,解决碎片堆积问题,不随分配算法变化;
  • 辅助打印新增候选分区筛选过程,直观展示 "最优匹配" 的选择逻辑,方便调试。
Python代码
python 复制代码
def init_free_partitions(total_memory):
    """
    初始化空闲分区表:总内存为连续大分区,起始地址0,按地址升序排列
    :param total_memory: 物理总内存大小(如1024MB)
    :return: 空闲分区表(list[dict]),元素:{"start": 起始地址, "size": 分区大小}
    """
    return [{"start": 0, "size": total_memory}]


def best_fit_allocate(free_partitions, processes_occupied, pid, need_memory):
    """
    最佳适应算法内存分配(核心:全遍历选大小≥需求的最小分区)
    :param free_partitions: 空闲分区表(会被修改)
    :param processes_occupied: 进程占用表(会被修改)
    :param pid: 待分配进程ID(唯一)
    :param need_memory: 进程需要的内存大小(>0)
    :return: 分配结果:成功True/失败False
    """
    # 边界校验:申请内存非法
    if need_memory <= 0:
        print(f"【分配失败】进程{pid}申请内存{need_memory}非法,必须大于0")
        return False
    # 无空闲分区,直接失败
    if not free_partitions:
        print(f"【分配失败】进程{pid}申请{need_memory},无任何空闲分区")
        return False

    # 核心步骤1:全遍历,筛选所有大小≥需求的候选分区(记录索引+大小+起始地址)
    candidate_partitions = []
    for idx, part in enumerate(free_partitions):
        if part["size"] >= need_memory:
            candidate_partitions.append({
                "idx": idx,
                "start": part["start"],
                "size": part["size"]
            })

    # 无候选分区,分配失败
    if not candidate_partitions:
        print(f"【分配失败】进程{pid}申请{need_memory},无大小适配的空闲分区")
        return False

    # 核心步骤2:从候选中选择【大小最小】的分区(最佳适应核心规则)
    # 按分区大小升序排序,第一个即为最优匹配
    candidate_partitions.sort(key=lambda x: x["size"])
    best_part = candidate_partitions[0]
    best_idx = best_part["idx"]
    best_start = best_part["start"]
    best_size = best_part["size"]

    # 打印候选分区和最优选择过程,直观展示最佳适应逻辑
    print(
        f"【分配中】进程{pid}申请{need_memory},筛选出候选分区{len(candidate_partitions)}个:{[(p['start'], p['size']) for p in candidate_partitions]}")
    print(f"【最优选择】匹配分区:索引{best_idx},起始地址{best_start},大小{best_size}(候选中最小)")

    # 执行分配:与首次/循环首次逻辑一致,分割或删除分区
    processes_occupied.append({
        "pid": pid,
        "start": best_start,
        "size": need_memory
    })
    # 分区大小恰好匹配,直接移除
    if best_size == need_memory:
        del free_partitions[best_idx]
    # 分区过大,分割为进程占用区+新空闲分区
    else:
        free_partitions[best_idx]["start"] = best_start + need_memory
        free_partitions[best_idx]["size"] = best_size - need_memory
        print(
            f"【分区分割】剩余空闲分区:起始{free_partitions[best_idx]['start']},大小{free_partitions[best_idx]['size']}")

    print(f"【分配成功】进程{pid}已分配:起始地址{best_start},大小{need_memory}")
    return True


def memory_recycle(free_partitions, processes_occupied, pid):
    """
    内存回收:释放进程分区,合并相邻空闲分区(与分配算法无关,成熟复用)
    :param free_partitions: 空闲分区表(会被修改)
    :param processes_occupied: 进程占用表(会被修改)
    :param pid: 待回收进程ID
    :return: 回收结果:成功True/失败False
    """
    # 查找待回收进程
    recycle_proc = None
    for i in range(len(processes_occupied)):
        if processes_occupied[i]["pid"] == pid:
            recycle_proc = processes_occupied[i]
            del processes_occupied[i]
            break
    if not recycle_proc:
        print(f"【回收失败】进程{pid}不存在,无需回收")
        return False

    # 释放分区加入空闲表
    free_start = recycle_proc["start"]
    free_size = recycle_proc["size"]
    free_partitions.append({"start": free_start, "size": free_size})
    print(f"【回收中】进程{pid}释放:起始{free_start},大小{free_size}")

    # 按地址排序,为合并相邻分区做准备
    free_partitions.sort(key=lambda x: x["start"])
    # 合并相邻分区
    i = 0
    while i < len(free_partitions) - 1:
        curr = free_partitions[i]
        next_p = free_partitions[i + 1]
        if curr["start"] + curr["size"] == next_p["start"]:
            # 合并分区
            curr["size"] += next_p["size"]
            del free_partitions[i + 1]
            print(
                f"【碎片合并】合并{curr['start']}({curr['size'] - next_p['size']})和{next_p['start']}({next_p['size']})→{curr['start']}({curr['size']})")
        else:
            i += 1

    print(f"【回收成功】进程{pid}回收完成,空闲分区表已更新并合并碎片")
    return True


def print_partitions(free_partitions, processes_occupied):
    """
    辅助打印:展示空闲分区表、进程占用表,直观查看内存状态
    新增空闲分区总大小、进程总占用大小,方便评估内存利用率
    """
    total_free = sum(p["size"] for p in free_partitions)
    total_occ = sum(p["size"] for p in processes_occupied)
    print("\n" + "-" * 90)
    print(f"【内存整体状态】总空闲:{total_free:4d}MB | 总占用:{total_occ:4d}MB | 空闲分区数:{len(free_partitions)}")
    print("【空闲分区表】(按起始地址排序,索引+地址+大小):")
    for idx, p in enumerate(free_partitions):
        print(f"  索引{idx:2d}:起始{p['start']:4d},大小{p['size']:4d}")
    print("【进程占用表】:")
    if not processes_occupied:
        print("  无进程占用内存")
    else:
        for p in processes_occupied:
            print(f"  进程{p['pid']:2d}:起始{p['start']:4d},大小{p['size']:4d}")
    print("-" * 90 + "\n")


# -------------------------- 测试用例:贴合最佳适应特点的场景 --------------------------
if __name__ == "__main__":
    # 1. 系统初始化:总内存1024MB,空闲分区[0,1024]
    TOTAL_MEM = 1024
    free_part = init_free_partitions(TOTAL_MEM)
    process_occ = []
    print(f"【系统初始化】物理总内存:{TOTAL_MEM}MB")
    print_partitions(free_part, process_occ)

    # 2. 核心测试1:多候选分区,验证"选最小适配"(申请250,候选300/424,选300)
    best_fit_allocate(free_part, process_occ, 1, 200)  # 进程1:200MB → 剩余[200,824]
    best_fit_allocate(free_part, process_occ, 2, 524)  # 进程2:524MB → 剩余[200,300]、[724,424]
    print_partitions(free_part, process_occ)
    best_fit_allocate(free_part, process_occ, 3, 250)  # 进程3:250MB → 候选300/424,选300(最优)
    print_partitions(free_part, process_occ)

    # 3. 核心测试2:小进程多次分配,验证"产生极小外部碎片"(算法固有缺点)
    best_fit_allocate(free_part, process_occ, 4, 40)  # 进程4:40MB → 候选50/424,选50→剩余10MB极小碎片
    best_fit_allocate(free_part, process_occ, 5, 20)  # 进程5:20MB → 候选404/424,选404→剩余20MB碎片
    print_partitions(free_part, process_occ)

    # 4. 内存回收+碎片合并:验证回收后极小碎片可合并为大分区
    memory_recycle(free_part, process_occ, 3)  # 回收进程3(250MB)
    memory_recycle(free_part, process_occ, 4)  # 回收进程4(40MB)
    print_partitions(free_part, process_occ)

    # 5. 边界测试:无候选分区、超大内存申请
    best_fit_allocate(free_part, process_occ, 6, 1000)  # 进程6:1000MB → 无候选,分配失败
    best_fit_allocate(free_part, process_occ, 7, 5)  # 进程7:5MB → 选极小碎片10MB→剩余5MB
    print_partitions(free_part, process_occ)

    # 6. 全量回收:验证内存恢复为初始状态
    for pid in [1, 2, 5, 7]:
        memory_recycle(free_part, process_occ, pid)
    print_partitions(free_part, process_occ)
程序运行结果展示
bash 复制代码
【系统初始化】物理总内存:1024MB

------------------------------------------------------------------------------------------
【内存整体状态】总空闲:1024MB | 总占用:   0MB | 空闲分区数:1
【空闲分区表】(按起始地址排序,索引+地址+大小):
  索引 0:起始   0,大小1024
【进程占用表】:
  无进程占用内存
------------------------------------------------------------------------------------------

【分配中】进程1申请200,筛选出候选分区1个:[(0, 1024)]
【最优选择】匹配分区:索引0,起始地址0,大小1024(候选中最小)
【分区分割】剩余空闲分区:起始200,大小824
【分配成功】进程1已分配:起始地址0,大小200
【分配中】进程2申请524,筛选出候选分区1个:[(200, 824)]
【最优选择】匹配分区:索引0,起始地址200,大小824(候选中最小)
【分区分割】剩余空闲分区:起始724,大小300
【分配成功】进程2已分配:起始地址200,大小524

------------------------------------------------------------------------------------------
【内存整体状态】总空闲: 300MB | 总占用: 724MB | 空闲分区数:1
【空闲分区表】(按起始地址排序,索引+地址+大小):
  索引 0:起始 724,大小 300
【进程占用表】:
  进程 1:起始   0,大小 200
  进程 2:起始 200,大小 524
------------------------------------------------------------------------------------------

【分配中】进程3申请250,筛选出候选分区1个:[(724, 300)]
【最优选择】匹配分区:索引0,起始地址724,大小300(候选中最小)
【分区分割】剩余空闲分区:起始974,大小50
【分配成功】进程3已分配:起始地址724,大小250

------------------------------------------------------------------------------------------
【内存整体状态】总空闲:  50MB | 总占用: 974MB | 空闲分区数:1
【空闲分区表】(按起始地址排序,索引+地址+大小):
  索引 0:起始 974,大小  50
【进程占用表】:
  进程 1:起始   0,大小 200
  进程 2:起始 200,大小 524
  进程 3:起始 724,大小 250
------------------------------------------------------------------------------------------

【分配中】进程4申请40,筛选出候选分区1个:[(974, 50)]
【最优选择】匹配分区:索引0,起始地址974,大小50(候选中最小)
【分区分割】剩余空闲分区:起始1014,大小10
【分配成功】进程4已分配:起始地址974,大小40
【分配失败】进程5申请20,无大小适配的空闲分区

------------------------------------------------------------------------------------------
【内存整体状态】总空闲:  10MB | 总占用:1014MB | 空闲分区数:1
【空闲分区表】(按起始地址排序,索引+地址+大小):
  索引 0:起始1014,大小  10
【进程占用表】:
  进程 1:起始   0,大小 200
  进程 2:起始 200,大小 524
  进程 3:起始 724,大小 250
  进程 4:起始 974,大小  40
------------------------------------------------------------------------------------------

【回收中】进程3释放:起始724,大小250
【回收成功】进程3回收完成,空闲分区表已更新并合并碎片
【回收中】进程4释放:起始974,大小40
【碎片合并】合并724(250)和974(40)→724(290)
【碎片合并】合并724(290)和1014(10)→724(300)
【回收成功】进程4回收完成,空闲分区表已更新并合并碎片

------------------------------------------------------------------------------------------
【内存整体状态】总空闲: 300MB | 总占用: 724MB | 空闲分区数:1
【空闲分区表】(按起始地址排序,索引+地址+大小):
  索引 0:起始 724,大小 300
【进程占用表】:
  进程 1:起始   0,大小 200
  进程 2:起始 200,大小 524
------------------------------------------------------------------------------------------

【分配失败】进程6申请1000,无大小适配的空闲分区
【分配中】进程7申请5,筛选出候选分区1个:[(724, 300)]
【最优选择】匹配分区:索引0,起始地址724,大小300(候选中最小)
【分区分割】剩余空闲分区:起始729,大小295
【分配成功】进程7已分配:起始地址724,大小5

------------------------------------------------------------------------------------------
【内存整体状态】总空闲: 295MB | 总占用: 729MB | 空闲分区数:1
【空闲分区表】(按起始地址排序,索引+地址+大小):
  索引 0:起始 729,大小 295
【进程占用表】:
  进程 1:起始   0,大小 200
  进程 2:起始 200,大小 524
  进程 7:起始 724,大小   5
------------------------------------------------------------------------------------------

【回收中】进程1释放:起始0,大小200
【回收成功】进程1回收完成,空闲分区表已更新并合并碎片
【回收中】进程2释放:起始200,大小524
【碎片合并】合并0(200)和200(524)→0(724)
【回收成功】进程2回收完成,空闲分区表已更新并合并碎片
【回收失败】进程5不存在,无需回收
【回收中】进程7释放:起始724,大小5
【碎片合并】合并0(724)和724(5)→0(729)
【碎片合并】合并0(729)和729(295)→0(1024)
【回收成功】进程7回收完成,空闲分区表已更新并合并碎片

------------------------------------------------------------------------------------------
【内存整体状态】总空闲:1024MB | 总占用:   0MB | 空闲分区数:1
【空闲分区表】(按起始地址排序,索引+地址+大小):
  索引 0:起始   0,大小1024
【进程占用表】:
  无进程占用内存
------------------------------------------------------------------------------------------

4. 最坏适应算法(Worst Fit, WF)

原理

对最佳适应的反向改进:遍历所有空闲分区,选择最大的空闲分区,分割后剩余的空闲分区仍较大,可用于后续大进程分配。

特点
  • 优点:剩余空闲分区较大,减少小碎片的产生,大进程分配效率高;
  • 缺点:需要遍历所有空闲分区,分配速度慢;大分区被快速分割,后续无大分区可用,不适合有大量大进程的系统。
适用场景:存在大量大内存需求进程的系统,优先保证大进程的分配。
算法实现思路
  • 数据结构复用:空闲分区表、进程占用表与首次 / 循环首次 / 最佳适应完全一致,无需额外新增状态变量;
  • 分配核心逻辑 :全量遍历空闲分区→筛选所有大小≥进程需求 的候选分区→选择候选中最大的分区(反向最佳适应)→执行分区分割 / 删除;
  • 性能特点保留:因全遍历特性,分配速度仍较慢(与最佳适应一致),但能有效减少小碎片产生;
  • 过程可视化:打印候选分区筛选过程和最大分区选择逻辑,直观展示 "最坏适应" 的核心规则;
  • 回收逻辑不变:复用成熟的「相邻分区合并」功能,解决大分区耗尽后的碎片堆积问题。
Python代码
python 复制代码
def init_free_partitions(total_memory):
    """
    初始化空闲分区表:总内存为连续大分区,起始地址0,按地址升序排列
    :param total_memory: 物理总内存大小(如1024MB)
    :return: 空闲分区表(list[dict]),元素:{"start": 起始地址, "size": 分区大小}
    """
    return [{"start": 0, "size": total_memory}]


def worst_fit_allocate(free_partitions, processes_occupied, pid, need_memory):
    """
    最坏适应算法内存分配(核心:全遍历选大小≥需求的最大分区)
    :param free_partitions: 空闲分区表(会被修改)
    :param processes_occupied: 进程占用表(会被修改)
    :param pid: 待分配进程ID(唯一)
    :param need_memory: 进程需要的内存大小(>0)
    :return: 分配结果:成功True/失败False
    """
    # 边界校验:申请内存非法或无空闲分区
    if need_memory <= 0:
        print(f"【分配失败】进程{pid}申请内存{need_memory}非法,必须大于0")
        return False
    if not free_partitions:
        print(f"【分配失败】进程{pid}申请{need_memory},无任何空闲分区")
        return False

    # 核心步骤1:全遍历,筛选所有大小≥需求的候选分区(记录索引+起始地址+大小)
    candidate_partitions = []
    for idx, part in enumerate(free_partitions):
        if part["size"] >= need_memory:
            candidate_partitions.append({
                "idx": idx,
                "start": part["start"],
                "size": part["size"]
            })

    # 无候选分区,分配失败
    if not candidate_partitions:
        print(f"【分配失败】进程{pid}申请{need_memory},无大小适配的空闲分区")
        return False

    # 核心步骤2:从候选中选择【大小最大】的分区(最坏适应核心规则)
    # 按分区大小降序排序,第一个即为最优匹配
    candidate_partitions.sort(key=lambda x: x["size"], reverse=True)
    worst_part = candidate_partitions[0]
    worst_idx = worst_part["idx"]
    worst_start = worst_part["start"]
    worst_size = worst_part["size"]

    # 打印候选分区和最大选择过程,直观展示最坏适应逻辑
    print(
        f"【分配中】进程{pid}申请{need_memory},筛选出候选分区{len(candidate_partitions)}个:{[(p['start'], p['size']) for p in candidate_partitions]}")
    print(f"【最优选择】匹配分区:索引{worst_idx},起始地址{worst_start},大小{worst_size}(候选中最大)")

    # 执行分配:与前三种算法逻辑完全一致,分割或删除分区
    processes_occupied.append({
        "pid": pid,
        "start": worst_start,
        "size": need_memory
    })
    # 分区大小恰好匹配,直接移除该空闲分区
    if worst_size == need_memory:
        del free_partitions[worst_idx]
    # 分区过大,分割为进程占用区+新空闲分区(剩余分区仍较大)
    else:
        free_partitions[worst_idx]["start"] = worst_start + need_memory
        free_partitions[worst_idx]["size"] = worst_size - need_memory
        print(
            f"【分区分割】剩余空闲分区:起始{free_partitions[worst_idx]['start']},大小{free_partitions[worst_idx]['size']}(保留大分区)")

    print(f"【分配成功】进程{pid}已分配:起始地址{worst_start},大小{need_memory}")
    return True


def memory_recycle(free_partitions, processes_occupied, pid):
    """
    内存回收:释放进程分区,合并相邻空闲分区(与分配算法无关,成熟复用)
    :param free_partitions: 空闲分区表(会被修改)
    :param processes_occupied: 进程占用表(会被修改)
    :param pid: 待回收进程ID
    :return: 回收结果:成功True/失败False
    """
    # 查找待回收进程
    recycle_proc = None
    for i in range(len(processes_occupied)):
        if processes_occupied[i]["pid"] == pid:
            recycle_proc = processes_occupied[i]
            del processes_occupied[i]
            break
    if not recycle_proc:
        print(f"【回收失败】进程{pid}不存在,无需回收")
        return False

    # 释放分区加入空闲表
    free_start = recycle_proc["start"]
    free_size = recycle_proc["size"]
    free_partitions.append({"start": free_start, "size": free_size})
    print(f"【回收中】进程{pid}释放:起始{free_start},大小{free_size}")

    # 按地址排序,为合并相邻分区做准备
    free_partitions.sort(key=lambda x: x["start"])
    # 合并相邻空闲分区,消除外部碎片
    i = 0
    while i < len(free_partitions) - 1:
        curr = free_partitions[i]
        next_p = free_partitions[i + 1]
        if curr["start"] + curr["size"] == next_p["start"]:
            # 合并为一个大分区
            curr["size"] += next_p["size"]
            del free_partitions[i + 1]
            print(
                f"【碎片合并】合并{curr['start']}({curr['size'] - next_p['size']})和{next_p['start']}({next_p['size']})→{curr['start']}({curr['size']})")
        else:
            i += 1

    print(f"【回收成功】进程{pid}回收完成,空闲分区表已更新并合并碎片")
    return True


def print_partitions(free_partitions, processes_occupied):
    """
    辅助打印:展示空闲分区表、进程占用表,统计总空闲/总占用内存
    直观评估内存利用率和碎片分布
    """
    total_free = sum(p["size"] for p in free_partitions)
    total_occ = sum(p["size"] for p in processes_occupied)
    print("\n" + "-" * 90)
    print(f"【内存整体状态】总空闲:{total_free:4d}MB | 总占用:{total_occ:4d}MB | 空闲分区数:{len(free_partitions)}")
    print("【空闲分区表】(按起始地址排序,索引+地址+大小):")
    for idx, p in enumerate(free_partitions):
        print(f"  索引{idx:2d}:起始{p['start']:4d},大小{p['size']:4d}")
    print("【进程占用表】:")
    if not processes_occupied:
        print("  无进程占用内存")
    else:
        for p in processes_occupied:
            print(f"  进程{p['pid']:2d}:起始{p['start']:4d},大小{p['size']:4d}")
    print("-" * 90 + "\n")


# -------------------------- 测试用例:贴合最坏适应特点的场景 --------------------------
if __name__ == "__main__":
    # 1. 系统初始化:总内存1024MB,空闲分区[0,1024]
    TOTAL_MEM = 1024
    free_part = init_free_partitions(TOTAL_MEM)
    process_occ = []
    print(f"【系统初始化】物理总内存:{TOTAL_MEM}MB")
    print_partitions(free_part, process_occ)

    # 2. 核心测试1:多候选分区,验证"选最大适配"(申请200,仅1个候选,直接分配)
    worst_fit_allocate(free_part, process_occ, 1, 200)  # 进程1:200MB → 剩余[200,824]
    worst_fit_allocate(free_part, process_occ, 2, 300)  # 进程2:300MB → 剩余[200,524]
    print_partitions(free_part, process_occ)

    # 3. 核心测试2:手动创建多候选,验证"选最大"+"剩余大分区"(申请250,候选524,直接分配)
    # 手动回收一个分区,制造两个候选分区(模拟多空闲场景)
    memory_recycle(free_part, process_occ, 2)  # 回收进程2 → 空闲分区[200,300]、[200,524]
    print_partitions(free_part, process_occ)
    worst_fit_allocate(free_part, process_occ, 3, 250)  # 进程3:250MB → 选524,剩余274(大分区)
    print_partitions(free_part, process_occ)

    # 4. 核心测试3:小进程分配,验证"减少小碎片"(申请40,候选300/274,选300→剩余260,无小碎片)
    worst_fit_allocate(free_part, process_occ, 4, 40)  # 进程4:40MB → 选300,剩余260(无极小碎片)
    print_partitions(free_part, process_occ)

    # 5. 核心测试4:大进程连续分配,验证"大分区快速耗尽"(申请300,候选274/260,无适配→分配失败)
    worst_fit_allocate(free_part, process_occ, 5, 300)  # 进程5:300MB → 无候选,分配失败(大分区耗尽)
    print_partitions(free_part, process_occ)

    # 6. 边界测试:回收合并大分区,验证"大进程重新可分配"
    memory_recycle(free_part, process_occ, 1)  # 回收进程1 → 空闲分区合并为[0,200]+[240,260]+[450,274]
    memory_recycle(free_part, process_occ, 3)  # 回收进程3 → 合并为更大分区
    print_partitions(free_part, process_occ)
    worst_fit_allocate(free_part, process_occ, 6, 250)  # 进程6:250MB → 合并后有大分区,分配成功
    print_partitions(free_part, process_occ)

    # 7. 全量回收:验证内存恢复为初始状态
    for pid in [3, 4, 6]:
        memory_recycle(free_part, process_occ, pid)
    print_partitions(free_part, process_occ)
程序运行结果展示
bash 复制代码
【系统初始化】物理总内存:1024MB

------------------------------------------------------------------------------------------
【内存整体状态】总空闲:1024MB | 总占用:   0MB | 空闲分区数:1
【空闲分区表】(按起始地址排序,索引+地址+大小):
  索引 0:起始   0,大小1024
【进程占用表】:
  无进程占用内存
------------------------------------------------------------------------------------------

【分配中】进程1申请200,筛选出候选分区1个:[(0, 1024)]
【最优选择】匹配分区:索引0,起始地址0,大小1024(候选中最大)
【分区分割】剩余空闲分区:起始200,大小824(保留大分区)
【分配成功】进程1已分配:起始地址0,大小200
【分配中】进程2申请300,筛选出候选分区1个:[(200, 824)]
【最优选择】匹配分区:索引0,起始地址200,大小824(候选中最大)
【分区分割】剩余空闲分区:起始500,大小524(保留大分区)
【分配成功】进程2已分配:起始地址200,大小300

------------------------------------------------------------------------------------------
【内存整体状态】总空闲: 524MB | 总占用: 500MB | 空闲分区数:1
【空闲分区表】(按起始地址排序,索引+地址+大小):
  索引 0:起始 500,大小 524
【进程占用表】:
  进程 1:起始   0,大小 200
  进程 2:起始 200,大小 300
------------------------------------------------------------------------------------------

【回收中】进程2释放:起始200,大小300
【碎片合并】合并200(300)和500(524)→200(824)
【回收成功】进程2回收完成,空闲分区表已更新并合并碎片

------------------------------------------------------------------------------------------
【内存整体状态】总空闲: 824MB | 总占用: 200MB | 空闲分区数:1
【空闲分区表】(按起始地址排序,索引+地址+大小):
  索引 0:起始 200,大小 824
【进程占用表】:
  进程 1:起始   0,大小 200
------------------------------------------------------------------------------------------

【分配中】进程3申请250,筛选出候选分区1个:[(200, 824)]
【最优选择】匹配分区:索引0,起始地址200,大小824(候选中最大)
【分区分割】剩余空闲分区:起始450,大小574(保留大分区)
【分配成功】进程3已分配:起始地址200,大小250

------------------------------------------------------------------------------------------
【内存整体状态】总空闲: 574MB | 总占用: 450MB | 空闲分区数:1
【空闲分区表】(按起始地址排序,索引+地址+大小):
  索引 0:起始 450,大小 574
【进程占用表】:
  进程 1:起始   0,大小 200
  进程 3:起始 200,大小 250
------------------------------------------------------------------------------------------

【分配中】进程4申请40,筛选出候选分区1个:[(450, 574)]
【最优选择】匹配分区:索引0,起始地址450,大小574(候选中最大)
【分区分割】剩余空闲分区:起始490,大小534(保留大分区)
【分配成功】进程4已分配:起始地址450,大小40

------------------------------------------------------------------------------------------
【内存整体状态】总空闲: 534MB | 总占用: 490MB | 空闲分区数:1
【空闲分区表】(按起始地址排序,索引+地址+大小):
  索引 0:起始 490,大小 534
【进程占用表】:
  进程 1:起始   0,大小 200
  进程 3:起始 200,大小 250
  进程 4:起始 450,大小  40
------------------------------------------------------------------------------------------

【分配中】进程5申请300,筛选出候选分区1个:[(490, 534)]
【最优选择】匹配分区:索引0,起始地址490,大小534(候选中最大)
【分区分割】剩余空闲分区:起始790,大小234(保留大分区)
【分配成功】进程5已分配:起始地址490,大小300

------------------------------------------------------------------------------------------
【内存整体状态】总空闲: 234MB | 总占用: 790MB | 空闲分区数:1
【空闲分区表】(按起始地址排序,索引+地址+大小):
  索引 0:起始 790,大小 234
【进程占用表】:
  进程 1:起始   0,大小 200
  进程 3:起始 200,大小 250
  进程 4:起始 450,大小  40
  进程 5:起始 490,大小 300
------------------------------------------------------------------------------------------

【回收中】进程1释放:起始0,大小200
【回收成功】进程1回收完成,空闲分区表已更新并合并碎片
【回收中】进程3释放:起始200,大小250
【碎片合并】合并0(200)和200(250)→0(450)
【回收成功】进程3回收完成,空闲分区表已更新并合并碎片

------------------------------------------------------------------------------------------
【内存整体状态】总空闲: 684MB | 总占用: 340MB | 空闲分区数:2
【空闲分区表】(按起始地址排序,索引+地址+大小):
  索引 0:起始   0,大小 450
  索引 1:起始 790,大小 234
【进程占用表】:
  进程 4:起始 450,大小  40
  进程 5:起始 490,大小 300
------------------------------------------------------------------------------------------

【分配中】进程6申请250,筛选出候选分区1个:[(0, 450)]
【最优选择】匹配分区:索引0,起始地址0,大小450(候选中最大)
【分区分割】剩余空闲分区:起始250,大小200(保留大分区)
【分配成功】进程6已分配:起始地址0,大小250

------------------------------------------------------------------------------------------
【内存整体状态】总空闲: 434MB | 总占用: 590MB | 空闲分区数:2
【空闲分区表】(按起始地址排序,索引+地址+大小):
  索引 0:起始 250,大小 200
  索引 1:起始 790,大小 234
【进程占用表】:
  进程 4:起始 450,大小  40
  进程 5:起始 490,大小 300
  进程 6:起始   0,大小 250
------------------------------------------------------------------------------------------

【回收失败】进程3不存在,无需回收
【回收中】进程4释放:起始450,大小40
【碎片合并】合并250(200)和450(40)→250(240)
【回收成功】进程4回收完成,空闲分区表已更新并合并碎片
【回收中】进程6释放:起始0,大小250
【碎片合并】合并0(250)和250(240)→0(490)
【回收成功】进程6回收完成,空闲分区表已更新并合并碎片

------------------------------------------------------------------------------------------
【内存整体状态】总空闲: 724MB | 总占用: 300MB | 空闲分区数:2
【空闲分区表】(按起始地址排序,索引+地址+大小):
  索引 0:起始   0,大小 490
  索引 1:起始 790,大小 234
【进程占用表】:
  进程 5:起始 490,大小 300
------------------------------------------------------------------------------------------

连续分配算法对比

算法 分配规则 优点 缺点 外部碎片 分配速度
首次适应 低地址第一个适配 实现简单、速度快 低地址碎片多 较多 最快
循环首次适应 上一次后第一个适配 碎片分布均匀 大分区易被分割 中等 较快
最佳适应 最小适配 碎片最小、利用率高 遍历全部分区、极小碎片多 最少 最慢
最坏适应 最大适配 剩余分区大、适配大进程 遍历全部分区、大分区易缺 中等 最慢

三、非连续内存分配算法

连续内存分配的核心痛点是外部碎片内存利用率低 ,现代操作系统均采用非连续内存分配 ,将进程的逻辑地址空间划分为若干不连续的块 (页 / 段),分别分配到物理内存的不连续空闲分区 ,通过页表 / 段表实现逻辑地址到物理地址的映射。

该类算法彻底解决外部碎片问题,是分页、分段、段页式存储管理的核心,也是虚拟存储的基础。

1. 分页存储管理算法

核心原理
  • 逻辑地址分页 :将进程的逻辑地址空间按固定大小 (页大小,如 4KB)划分为若干页(Page),编号从 0 开始;
  • 物理内存分块 :将物理内存按相同的页大小 划分为若干物理块(Frame),编号从 0 开始;
  • 页表映射 :为每个进程建立页表 ,记录进程的页号 与物理内存的块号的映射关系,同时包含页内偏移量(逻辑地址 = 页号 × 页大小 + 页内偏移,物理地址 = 块号 × 页大小 + 页内偏移);
  • 分配规则:为进程的每个页分配一个物理块,块可以不连续,仅需保证页内连续。
关键特点
  • 彻底解决外部碎片(物理块大小固定,无分区间碎片);
  • 存在内部碎片(进程最后一页若未占满页大小,剩余空间无法利用);
  • 页大小由硬件决定(通常为 2 的幂次,如 4KB/8KB,兼顾减少内部碎片和页表大小);
  • 地址转换由MMU(内存管理单元) 硬件实现,速度快。
扩展:快表(TLB)优化

页表通常存放在物理内存中,每次地址转换都要访问内存,效率低。引入快表(相联存储器) ,缓存近期访问的页号 - 块号 映射,实现高速缓存 + 内存的二级地址转换,大幅提升地址映射速度(现代 OS 的标配)。

适用场景:所有现代通用操作系统(Windows/Linux/UNIX)的基础内存管理方式。
算法实现思路
  • 基础参数约束 :页大小强制为2 的幂次(如 4096B=4KB),贴合硬件设计要求;物理内存按页大小划分为等大物理块,用数组记录块的占用状态。
  • 核心数据结构
    • 物理块管理:数组记录每个物理块是否被占用,索引为块号(Frame ID);
    • 进程页表:按进程 ID(PID)管理,每个页表是字典,键为页号 (Page ID),值包含块号、有效位(标记页是否调入物理内存);
    • 快表(TLB):有限容量的缓存,存储(PID, 页号,块号),采用FIFO 替换策略(现代 OS 基础策略),模拟相联存储器的高速缓存特性;
  • 核心流程实现
    • 物理块分配:为进程的页号查找空闲物理块,更新物理块状态和页表;
    • 地址转换(MMU 模拟):先查快表(命中则直接转换)→ 未命中则查页表(有效位为 1 则转换,否则触发缺页中断)→ 页表命中后更新快表;
    • 进程销毁:回收进程占用的所有物理块,清空页表和快表中相关条目,释放内存资源;
  • 特性体现 :明确体现无外部碎片、存在内部碎片 的分页核心特点,同时模拟缺页中断、快表命中 / 未命中的关键场景。
Python代码
python 复制代码
import math


class PagingMemoryManager:
    def __init__(self, page_size=4096, total_physical_mem=65536, tlb_capacity=8):
        """
        初始化分页存储管理系统
        :param page_size: 页大小,必须是2的幂次,默认4096B(4KB)
        :param total_physical_mem: 物理总内存大小,默认65536B(64KB)
        :param tlb_capacity: 快表容量,默认8个条目,FIFO替换
        """
        # 校验页大小是否为2的幂次(硬件强制要求)
        if not (page_size > 0 and (page_size & (page_size - 1)) == 0):
            raise ValueError("页大小必须是2的幂次,如1024、4096、8192")
        # 校验物理内存是否是页大小的整数倍
        if total_physical_mem % page_size != 0:
            raise ValueError("物理总内存必须是页大小的整数倍")

        self.page_size = page_size  # 页大小(=物理块大小)
        self.tlb_capacity = tlb_capacity  # 快表最大容量
        self.total_frames = total_physical_mem // page_size  # 物理块总数
        self.frame_occupied = [False] * self.total_frames  # 物理块占用状态:False=空闲,True=占用,索引=块号

        self.tlb = []  # 快表:[(pid, 页号, 块号), ...],FIFO策略
        self.process_page_tables = {}  # 所有进程的页表:{pid: {页号: {"frame": 块号, "valid": 有效位}}, ...}
        self.page_offset_mask = page_size - 1  # 页内偏移掩码(用于快速提取偏移,2的幂次特性)
        self.page_num_shift = int(math.log2(page_size))  # 页号右移位数(快速提取页号)

        print(f"【分页系统初始化成功】")
        print(f"页大小:{self.page_size}B | 物理总内存:{total_physical_mem}B | 物理块总数:{self.total_frames}")
        print(
            f"快表容量:{self.tlb_capacity}条 | 页号右移位数:{self.page_num_shift} | 页内偏移掩码:0x{self.page_offset_mask:X}")

    def create_process(self, pid):
        """
        创建进程,初始化空页表
        :param pid: 进程唯一ID
        :return: 成功True/失败False
        """
        if pid in self.process_page_tables:
            print(f"【创建失败】进程{pid}已存在")
            return False
        self.process_page_tables[pid] = {}
        print(f"【创建成功】进程{pid},初始化空页表")
        return True

    def _find_free_frame(self):
        """内部函数:查找第一个空闲的物理块"""
        for frame_id in range(self.total_frames):
            if not self.frame_occupied[frame_id]:
                return frame_id
        return -1  # 无空闲物理块

    def allocate_page(self, pid, page_num):
        """
        为进程的指定页号分配物理块(模拟页调入内存)
        :param pid: 进程ID
        :param page_num: 待分配的页号(非负整数)
        :return: 分配的块号/失败-1
        """
        # 边界校验
        if pid not in self.process_page_tables:
            print(f"【分配失败】进程{pid}不存在")
            return -1
        if page_num < 0:
            print(f"【分配失败】页号{page_num}非法,必须非负")
            return -1
        # 该页已分配,直接返回原有块号
        if page_num in self.process_page_tables[pid] and self.process_page_tables[pid][page_num]["valid"]:
            frame_id = self.process_page_tables[pid][page_num]["frame"]
            print(f"【分配提示】进程{pid}页{page_num}已分配至块{frame_id},无需重复分配")
            return frame_id

        # 查找空闲物理块
        frame_id = self._find_free_frame()
        if frame_id == -1:
            print(f"【分配失败】进程{pid}页{page_num},无空闲物理块(内存耗尽)")
            return -1

        # 更新物理块状态和页表
        self.frame_occupied[frame_id] = True
        self.process_page_tables[pid][page_num] = {
            "frame": frame_id,
            "valid": True  # 有效位置1:页已调入物理内存
        }
        print(f"【分配成功】进程{pid}页{page_num} → 物理块{frame_id}")
        return frame_id

    def _tlb_update(self, pid, page_num, frame_id):
        """内部函数:更新快表,FIFO替换策略"""
        # 先删除已存在的同(PID,页号)条目(避免重复)
        self.tlb = [item for item in self.tlb if not (item[0] == pid and item[1] == page_num)]
        # 快表满则执行FIFO:删除第一个条目
        if len(self.tlb) >= self.tlb_capacity:
            removed_item = self.tlb.pop(0)
            print(f"【快表替换】FIFO移除:进程{removed_item[0]}页{removed_item[1]}→块{removed_item[2]}")
        # 添加新条目到快表尾部
        self.tlb.append((pid, page_num, frame_id))
        print(f"【快表更新】进程{pid}页{page_num}→块{frame_id},当前快表条目数:{len(self.tlb)}/{self.tlb_capacity}")

    def logical_to_physical(self, pid, logical_addr):
        """
        MMU地址转换:逻辑地址 → 物理地址(核心功能,快表优先)
        :param pid: 进程ID
        :param logical_addr: 逻辑地址(非负整数)
        :return: 物理地址/失败-1
        """
        # 边界校验
        if pid not in self.process_page_tables:
            print(f"【转换失败】进程{pid}不存在")
            return -1
        if logical_addr < 0:
            print(f"【转换失败】逻辑地址{logical_addr}非法,必须非负")
            return -1

        # 快速提取页号和页内偏移(利用2的幂次特性,位运算高效实现)
        # 页号 = 逻辑地址 >> 页号右移位数 | 页内偏移 = 逻辑地址 & 偏移掩码
        page_num = logical_addr >> self.page_num_shift
        offset = logical_addr & self.page_offset_mask
        print(f"\n【地址解析】进程{pid}逻辑地址{logical_addr} → 页号:{page_num},页内偏移:{offset}")

        # 步骤1:查询快表(TLB),命中则直接转换
        for item in self.tlb:
            if item[0] == pid and item[1] == page_num:
                frame_id = item[2]
                physical_addr = (frame_id << self.page_num_shift) + offset
                print(f"【快表命中】进程{pid}页{page_num}→块{frame_id},物理地址:{physical_addr}")
                return physical_addr
        print(f"【快表未命中】进程{pid}页{page_num},开始查询页表")

        # 步骤2:查询页表
        if page_num not in self.process_page_tables[pid] or not self.process_page_tables[pid][page_num]["valid"]:
            print(f"【缺页中断】进程{pid}页{page_num}未调入物理内存,请先分配物理块")
            return -1

        # 页表命中,获取块号并转换物理地址
        frame_id = self.process_page_tables[pid][page_num]["frame"]
        physical_addr = (frame_id << self.page_num_shift) + offset
        # 步骤3:更新快表(FIFO)
        self._tlb_update(pid, page_num, frame_id)
        print(f"【页表命中】进程{pid}页{page_num}→块{frame_id},物理地址:{physical_addr}")
        return physical_addr

    def destroy_process(self, pid):
        """
        销毁进程,回收所有占用的物理块,清空页表和快表相关条目
        :param pid: 进程ID
        :return: 成功True/失败False
        """
        if pid not in self.process_page_tables:
            print(f"【销毁失败】进程{pid}不存在")
            return False

        # 回收物理块
        page_table = self.process_page_tables[pid]
        for page_num, info in page_table.items():
            if info["valid"]:
                frame_id = info["frame"]
                self.frame_occupied[frame_id] = False
                print(f"【内存回收】进程{pid}页{page_num} → 释放物理块{frame_id}")

        # 清空快表中该进程的所有条目
        self.tlb = [item for item in self.tlb if item[0] != pid]
        # 删除页表
        del self.process_page_tables[pid]
        print(f"【销毁成功】进程{pid},已回收所有物理块,清空页表和快表相关条目")
        return True

    def print_status(self, pid=None):
        """
        打印系统状态:物理块、快表、指定进程页表(pid=None则打印所有)
        """
        print("\n" + "=" * 100)
        # 打印物理块状态
        used_frames = sum(self.frame_occupied)
        print(f"【物理块状态】总块数:{self.total_frames} | 已用:{used_frames} | 空闲:{self.total_frames - used_frames}")
        occupied_frames = [i for i, val in enumerate(self.frame_occupied) if val]
        free_frames = [i for i, val in enumerate(self.frame_occupied) if not val]
        print(
            f"已用块号:{occupied_frames if occupied_frames else '无'} | 空闲块号:{free_frames if free_frames else '无'}")

        # 打印快表状态
        print(f"\n【快表(TLB)状态】当前条目:{len(self.tlb)}/{self.tlb_capacity}")
        if self.tlb:
            for idx, item in enumerate(self.tlb):
                print(f"  条目{idx}:进程{item[0]} | 页{item[1]} → 块{item[2]}")
        else:
            print("  快表为空")

        # 打印页表状态
        print(f"\n【进程页表状态】总进程数:{len(self.process_page_tables)}")
        target_pids = [pid] if pid is not None and pid in self.process_page_tables else self.process_page_tables.keys()
        for p in target_pids:
            page_table = self.process_page_tables[p]
            print(f"  进程{p}页表(共{len(page_table)}页):")
            if page_table:
                for page_num, info in sorted(page_table.items()):
                    print(f"    页{page_num} → 块{info['frame']} | 有效位:{1 if info['valid'] else 0}")
            else:
                print("    页表为空")
        print("=" * 100 + "\n")


# -------------------------- 测试用例:贴合分页存储核心特性 --------------------------
if __name__ == "__main__":
    # 1. 初始化分页系统:页大小4096B,物理内存64KB(16个物理块),快表容量4条
    try:
        manager = PagingMemoryManager(page_size=4096, total_physical_mem=65536, tlb_capacity=4)
    except ValueError as e:
        print(e)
        exit()

    # 2. 创建进程,分配物理块
    manager.create_process(1)  # 创建进程1
    manager.create_process(2)  # 创建进程2
    # 为进程1分配页0、页1、页2;进程2分配页0、页1
    manager.allocate_page(1, 0)
    manager.allocate_page(1, 1)
    manager.allocate_page(1, 2)
    manager.allocate_page(2, 0)
    manager.allocate_page(2, 1)
    manager.print_status()  # 打印初始状态

    # 3. 核心测试:地址转换(快表命中/未命中/缺页中断)
    # 进程1逻辑地址:0x1234(4660)→ 页1,偏移660;0x2000(8192)→ 页2,偏移0;0x3000(12288)→ 页3(未分配)
    manager.logical_to_physical(1, 4660)  # 第一次查页1:快表未命中→更新快表
    manager.logical_to_physical(1, 4660)  # 第二次查页1:快表命中(核心优化)
    manager.logical_to_physical(1, 8192)  # 查页2:快表未命中→更新快表
    manager.logical_to_physical(1, 12288)  # 查页3:缺页中断(未分配)
    manager.print_status()  # 打印快表更新后状态

    # 4. 核心测试:快表FIFO替换(容量4条,新增条目触发替换)
    manager.logical_to_physical(2, 0)  # 进程2页0:快表未命中→更新
    manager.logical_to_physical(2, 1)  # 进程2页1:快表满→FIFO替换最早条目
    manager.print_status()  # 打印快表替换后状态

    # 5. 特性测试:内部碎片体现(进程1逻辑地址0x1FFF=8191 → 页1,偏移4095(满);0x2FFF=12287→页2,偏移4095(满))
    # 最后一页若仅用1B,剩余4095B为内部碎片,此处通过最大偏移体现页内空间
    manager.allocate_page(1, 3)  # 为进程1分配页3(模拟最后一页)
    manager.logical_to_physical(1, 12287)  # 页3,偏移4095(页内最大偏移)
    print("【内部碎片体现】进程1页3若仅使用部分空间,剩余空间为内部碎片,无外部碎片(分页核心特点)")

    # 6. 边界测试:销毁进程,回收物理块
    manager.destroy_process(1)  # 销毁进程1,回收所有块
    manager.print_status()  # 打印回收后状态(物理块空闲,快表清空进程1条目)

    # 7. 复用回收块:为进程2分配页2,使用进程1回收的块
    manager.allocate_page(2, 2)
    manager.print_status()
程序运行结果展示
bash 复制代码
【分页系统初始化成功】
页大小:4096B | 物理总内存:65536B | 物理块总数:16
快表容量:4条 | 页号右移位数:12 | 页内偏移掩码:0xFFF
【创建成功】进程1,初始化空页表
【创建成功】进程2,初始化空页表
【分配成功】进程1页0 → 物理块0
【分配成功】进程1页1 → 物理块1
【分配成功】进程1页2 → 物理块2
【分配成功】进程2页0 → 物理块3
【分配成功】进程2页1 → 物理块4

====================================================================================================
【物理块状态】总块数:16 | 已用:5 | 空闲:11
已用块号:[0, 1, 2, 3, 4] | 空闲块号:[5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]

【快表(TLB)状态】当前条目:0/4
  快表为空

【进程页表状态】总进程数:2
  进程1页表(共3页):
    页0 → 块0 | 有效位:1
    页1 → 块1 | 有效位:1
    页2 → 块2 | 有效位:1
  进程2页表(共2页):
    页0 → 块3 | 有效位:1
    页1 → 块4 | 有效位:1
====================================================================================================


【地址解析】进程1逻辑地址4660 → 页号:1,页内偏移:564
【快表未命中】进程1页1,开始查询页表
【快表更新】进程1页1→块1,当前快表条目数:1/4
【页表命中】进程1页1→块1,物理地址:4660

【地址解析】进程1逻辑地址4660 → 页号:1,页内偏移:564
【快表命中】进程1页1→块1,物理地址:4660

【地址解析】进程1逻辑地址8192 → 页号:2,页内偏移:0
【快表未命中】进程1页2,开始查询页表
【快表更新】进程1页2→块2,当前快表条目数:2/4
【页表命中】进程1页2→块2,物理地址:8192

【地址解析】进程1逻辑地址12288 → 页号:3,页内偏移:0
【快表未命中】进程1页3,开始查询页表
【缺页中断】进程1页3未调入物理内存,请先分配物理块

====================================================================================================
【物理块状态】总块数:16 | 已用:5 | 空闲:11
已用块号:[0, 1, 2, 3, 4] | 空闲块号:[5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]

【快表(TLB)状态】当前条目:2/4
  条目0:进程1 | 页1 → 块1
  条目1:进程1 | 页2 → 块2

【进程页表状态】总进程数:2
  进程1页表(共3页):
    页0 → 块0 | 有效位:1
    页1 → 块1 | 有效位:1
    页2 → 块2 | 有效位:1
  进程2页表(共2页):
    页0 → 块3 | 有效位:1
    页1 → 块4 | 有效位:1
====================================================================================================


【地址解析】进程2逻辑地址0 → 页号:0,页内偏移:0
【快表未命中】进程2页0,开始查询页表
【快表更新】进程2页0→块3,当前快表条目数:3/4
【页表命中】进程2页0→块3,物理地址:12288

【地址解析】进程2逻辑地址1 → 页号:0,页内偏移:1
【快表命中】进程2页0→块3,物理地址:12289

====================================================================================================
【物理块状态】总块数:16 | 已用:5 | 空闲:11
已用块号:[0, 1, 2, 3, 4] | 空闲块号:[5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]

【快表(TLB)状态】当前条目:3/4
  条目0:进程1 | 页1 → 块1
  条目1:进程1 | 页2 → 块2
  条目2:进程2 | 页0 → 块3

【进程页表状态】总进程数:2
  进程1页表(共3页):
    页0 → 块0 | 有效位:1
    页1 → 块1 | 有效位:1
    页2 → 块2 | 有效位:1
  进程2页表(共2页):
    页0 → 块3 | 有效位:1
    页1 → 块4 | 有效位:1
====================================================================================================

【分配成功】进程1页3 → 物理块5

【地址解析】进程1逻辑地址12287 → 页号:2,页内偏移:4095
【快表命中】进程1页2→块2,物理地址:12287
【内部碎片体现】进程1页3若仅使用部分空间,剩余空间为内部碎片,无外部碎片(分页核心特点)
【内存回收】进程1页0 → 释放物理块0
【内存回收】进程1页1 → 释放物理块1
【内存回收】进程1页2 → 释放物理块2
【内存回收】进程1页3 → 释放物理块5
【销毁成功】进程1,已回收所有物理块,清空页表和快表相关条目

====================================================================================================
【物理块状态】总块数:16 | 已用:2 | 空闲:14
已用块号:[3, 4] | 空闲块号:[0, 1, 2, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]

【快表(TLB)状态】当前条目:1/4
  条目0:进程2 | 页0 → 块3

【进程页表状态】总进程数:1
  进程2页表(共2页):
    页0 → 块3 | 有效位:1
    页1 → 块4 | 有效位:1
====================================================================================================

【分配成功】进程2页2 → 物理块0

====================================================================================================
【物理块状态】总块数:16 | 已用:3 | 空闲:13
已用块号:[0, 3, 4] | 空闲块号:[1, 2, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]

【快表(TLB)状态】当前条目:1/4
  条目0:进程2 | 页0 → 块3

【进程页表状态】总进程数:1
  进程2页表(共3页):
    页0 → 块3 | 有效位:1
    页1 → 块4 | 有效位:1
    页2 → 块0 | 有效位:1
====================================================================================================

2. 分段存储管理算法

核心原理

基于进程的逻辑结构 划分(而非固定大小),将进程的逻辑地址空间按功能模块 (如代码段、数据段、栈段、堆段)划分为若干段(Segment) ,每个段有独立的段名和可变大小,段内连续、段间不连续。

  • 逻辑地址结构:段号 + 段内偏移量,段号对应段表项,段内偏移量≤段的最大长度;
  • 段表映射 :为每个进程建立段表,记录段号、段的基址(物理内存起始地址)、段的长度、存取权限(读 / 写 / 执行);
  • 分配规则:为每个段分配连续的物理内存分区,段间分区可不连续。
关键特点
  • 内部碎片 (段大小与进程功能模块匹配,按需分配),存在外部碎片(段间的空闲分区);
  • 符合进程的逻辑结构,便于模块共享 (如多个进程共享代码段)和动态链接(运行时加载段);
  • 便于内存保护(段表中设置存取权限,防止越权访问,如代码段只读);
  • 地址转换需两次访问内存(段表→物理内存),可通过快表优化。
适用场景:注重进程逻辑结构、模块共享和内存保护的系统(如早期 UNIX、部分嵌入式 OS)。
算法实现思路
  • 物理内存管理 :采用空闲分区表 管理物理内存(与连续分配算法一致),为每个段分配连续的物理分区 (段的核心要求),分配用首次适应算法 ,回收时合并相邻外部碎片,体现分段「存在外部碎片」的特点;
  • 核心数据结构
    • 空闲分区表:list[dict],记录物理内存中空闲分区的起始地址、大小,按起始地址升序排列;
    • 进程段表:dict按 PID 管理,每个段表项记录段号、基址(物理起始地址)、段长、权限、有效位(标记段是否调入内存),贴合进程逻辑模块划分;
    • 快表(TLB):有限容量缓存,存储(PID, 段号, 基址),FIFO 替换策略,优化地址转换速度;
    • 权限定义:支持r(读)、w(写)、x(执行)组合,实现内存保护(如代码段只读 / 执行,数据段可读可写);
  • 核心流程实现
    • 段分配:为进程指定段号分配连续的物理分区(首次适应),更新段表和空闲分区表,无内部碎片(段长与分配大小一致);
    • 地址转换:逻辑地址(段号 + 段内偏移)→ 快表查询→段表查询(偏移越界检查 +存取权限校验)→ 物理地址计算,越界 / 越权直接拒绝;
    • 段回收 / 进程销毁:释放段占用的物理分区,合并相邻空闲碎片,清空段表 / 快表相关条目,体现外部碎片的解决方式;
    • 快表优化:地址转换优先查快表,未命中则查段表并更新快表,减少内存访问次数;
  • 特性严格贴合:无内部碎片(段长按需分配)、有外部碎片(段间空闲分区)、段内连续段间不连续、进程逻辑划分、内存保护、模块共享(预留共享段接口)。
Python代码
python 复制代码
class SegmentMemoryManager:
    def __init__(self, total_physical_mem=1024 * 1024, tlb_capacity=8):
        """
        初始化分段存储管理系统
        :param total_physical_mem: 物理总内存大小,默认1MB(1024*1024B)
        :param tlb_capacity: 快表(TLB)容量,默认8条,FIFO替换策略
        """
        self.total_physical_mem = total_physical_mem  # 物理总内存
        self.tlb_capacity = tlb_capacity              # 快表最大容量
        # 初始化空闲分区表:整段物理内存为一个空闲分区,起始地址0
        self.free_partitions = [{"start": 0, "size": total_physical_mem}]
        self.tlb = []  # 快表:[(pid, 段号, 段基址), ...],FIFO策略
        # 进程段表:{pid: {段号: {"base": 基址, "length": 段长, "perm": 权限, "valid": 有效位}}, ...}
        # 权限示例:"r-x"(读+执行)、"rw-"(读+写)、"r--"(只读),仅支持r/w/x,-表示无权限
        self.process_seg_tables = {}
        # 合法权限字符校验
        self.valid_perm_chars = {"r", "w", "x", "-"}

        print(f"【分段系统初始化成功】")
        print(f"物理总内存:{self.total_physical_mem}B | 快表容量:{self.tlb_capacity}条")
        print(f"初始空闲分区:起始0,大小{self.total_physical_mem}B | 支持权限:r(读)、w(写)、x(执行)、-(无)")

    def create_process(self, pid):
        """
        创建进程,初始化空段表
        :param pid: 进程唯一ID
        :return: 成功True/失败False
        """
        if pid in self.process_seg_tables:
            print(f"【创建失败】进程{pid}已存在")
            return False
        self.process_seg_tables[pid] = {}
        print(f"【创建成功】进程{pid},初始化空段表")
        return True

    def _is_valid_permission(self, perm):
        """内部函数:校验权限字符串是否合法(长度3,仅含r/w/x/-)"""
        if len(perm) != 3:
            return False
        for c in perm:
            if c not in self.valid_perm_chars:
                return False
        return True

    def _first_fit_allocate(self, need_size):
        """内部函数:首次适应算法分配连续物理分区,返回(起始地址, 大小)/(-1, -1)"""
        if need_size <= 0 or need_size > self.total_physical_mem:
            return -1, -1
        # 按起始地址升序遍历,找到第一个≥需求的空闲分区
        for i in range(len(self.free_partitions)):
            curr_start = self.free_partitions[i]["start"]
            curr_size = self.free_partitions[i]["size"]
            if curr_size >= need_size:
                # 分割分区:若恰好匹配则删除,否则更新空闲分区
                if curr_size == need_size:
                    del self.free_partitions[i]
                else:
                    self.free_partitions[i]["start"] = curr_start + need_size
                    self.free_partitions[i]["size"] = curr_size - need_size
                return curr_start, need_size
        return -1, -1  # 无足够连续分区

    def _merge_free_partitions(self):
        """内部函数:合并相邻的空闲分区,解决外部碎片"""
        if len(self.free_partitions) <= 1:
            return
        # 按起始地址排序
        self.free_partitions.sort(key=lambda x: x["start"])
        i = 0
        while i < len(self.free_partitions) - 1:
            curr = self.free_partitions[i]
            next_p = self.free_partitions[i+1]
            # 相邻判定:当前分区结束地址 = 下一个分区起始地址
            if curr["start"] + curr["size"] == next_p["start"]:
                curr["size"] += next_p["size"]
                del self.free_partitions[i+1]
            else:
                i += 1

    def allocate_segment(self, pid, seg_num, seg_length, perm="rw-"):
        """
        为进程分配段(核心:分配连续物理分区,按需分配无内部碎片)
        :param pid: 进程ID
        :param seg_num: 段号(非负整数,如0=代码段,1=数据段,2=栈段)
        :param seg_length: 段长度(>0,按需分配)
        :param perm: 段权限,默认"rw-"(读+写),支持"r-x"/"r--"/"rw-"等
        :return: 段基址/失败-1
        """
        # 边界校验
        if pid not in self.process_seg_tables:
            print(f"【分配失败】进程{pid}不存在")
            return -1
        if seg_num < 0 or seg_length <= 0:
            print(f"【分配失败】段号{seg_num}或段长{seg_length}非法(段号≥0,段长>0)")
            return -1
        if not self._is_valid_permission(perm):
            print(f"【分配失败】权限{perm}非法,仅支持r/w/x/-组合(长度3),示例:r-x、rw-")
            return -1
        # 该段已分配,直接返回原有基址
        if seg_num in self.process_seg_tables[pid] and self.process_seg_tables[pid][seg_num]["valid"]:
            base = self.process_seg_tables[pid][seg_num]["base"]
            print(f"【分配提示】进程{pid}段{seg_num}已分配(基址{base},长{seg_length},权限{perm}),无需重复分配")
            return base

        # 首次适应分配连续物理分区
        seg_base, actual_size = self._first_fit_allocate(seg_length)
        if seg_base == -1:
            print(f"【分配失败】进程{pid}段{seg_num},无足够大的连续物理分区(外部碎片过多?)")
            return -1

        # 更新进程段表:有效位=1(段已调入内存)
        self.process_seg_tables[pid][seg_num] = {
            "base": seg_base,
            "length": seg_length,
            "perm": perm,
            "valid": True
        }
        print(f"【分配成功】进程{pid}段{seg_num} → 基址{seg_base},长度{seg_length},权限{perm}")
        print(f"【特性体现】段内连续,无内部碎片(分配大小=段长),段间可离散")
        return seg_base

    def _tlb_update(self, pid, seg_num, seg_base):
        """内部函数:更新快表,FIFO替换策略,避免重复条目"""
        # 删除已存在的同(PID,段号)条目
        self.tlb = [item for item in self.tlb if not (item[0] == pid and item[1] == seg_num)]
        # 快表满则FIFO移除最早条目
        if len(self.tlb) >= self.tlb_capacity:
            removed_item = self.tlb.pop(0)
            print(f"【快表替换】FIFO移除:进程{removed_item[0]}段{removed_item[1]}(基址{removed_item[2]})")
        # 新增条目到尾部
        self.tlb.append((pid, seg_num, seg_base))
        print(f"【快表更新】进程{pid}段{seg_num}(基址{seg_base}),当前快表:{len(self.tlb)}/{self.tlb_capacity}")

    def logical_to_physical(self, pid, seg_num, offset, access_type="r"):
        """
        地址转换:逻辑地址(段号+段内偏移)→ 物理地址(核心功能)
        :param pid: 进程ID
        :param seg_num: 段号
        :param offset: 段内偏移量(≥0)
        :param access_type: 访问类型,r(读)/w(写)/x(执行),默认r
        :return: 物理地址/失败-1
        """
        # 基础校验
        if pid not in self.process_seg_tables:
            print(f"【转换失败】进程{pid}不存在")
            return -1
        if seg_num not in self.process_seg_tables[pid] or not self.process_seg_tables[pid][seg_num]["valid"]:
            print(f"【转换失败】进程{pid}段{seg_num}未分配或未调入内存(有效位=0)")
            return -1
        if offset < 0 or access_type not in {"r", "w", "x"}:
            print(f"【转换失败】偏移{offset}或访问类型{access_type}非法(偏移≥0,访问类型:r/w/x)")
            return -1

        seg_info = self.process_seg_tables[pid][seg_num]
        seg_base = seg_info["base"]
        seg_length = seg_info["length"]
        seg_perm = seg_info["perm"]

        # 步骤1:段内偏移越界检查(分段核心:偏移≤段长,否则地址越界)
        if offset >= seg_length:
            print(f"【地址越界】进程{pid}段{seg_num}(长{seg_length}),偏移{offset}超出段长范围")
            return -1

        # 步骤2:存取权限校验(内存保护,越权访问拒绝)
        # 权限位映射:0=r,1=w,2=x
        perm_map = {"r": 0, "w": 1, "x": 2}
        if seg_perm[perm_map[access_type]] == "-":
            print(f"【权限越界】进程{pid}段{seg_num}权限{seg_perm},拒绝{access_type}访问")
            return -1

        # 步骤3:查询快表(TLB),命中则直接计算物理地址
        for item in self.tlb:
            if item[0] == pid and item[1] == seg_num:
                physical_addr = item[2] + offset
                print(f"【快表命中】进程{pid}段{seg_num}→基址{item[2]},偏移{offset},物理地址{physical_addr}")
                return physical_addr
        print(f"【快表未命中】进程{pid}段{seg_num},查询段表并更新快表")

        # 步骤4:段表命中,计算物理地址并更新快表
        physical_addr = seg_base + offset
        self._tlb_update(pid, seg_num, seg_base)
        print(f"【段表命中】进程{pid}段{seg_num}→基址{seg_base},偏移{offset},物理地址{physical_addr}")
        return physical_addr

    def free_segment(self, pid, seg_num):
        """
        释放单个段,回收物理分区并合并相邻外部碎片
        :param pid: 进程ID
        :param seg_num: 段号
        :return: 成功True/失败False
        """
        if pid not in self.process_seg_tables:
            print(f"【释放失败】进程{pid}不存在")
            return False
        if seg_num not in self.process_seg_tables[pid] or not self.process_seg_tables[pid][seg_num]["valid"]:
            print(f"【释放失败】进程{pid}段{seg_num}未分配或已释放")
            return False

        # 回收物理分区,加入空闲分区表
        seg_info = self.process_seg_tables[pid][seg_num]
        free_start = seg_info["base"]
        free_size = seg_info["length"]
        self.free_partitions.append({"start": free_start, "size": free_size})
        # 标记段为无效,合并外部碎片
        self.process_seg_tables[pid][seg_num]["valid"] = False
        self._merge_free_partitions()
        # 清空快表中该段条目
        self.tlb = [item for item in self.tlb if not (item[0] == pid and item[1] == seg_num)]

        print(f"【释放成功】进程{pid}段{seg_num},回收物理分区:起始{free_start},大小{free_size}")
        print(f"【碎片合并】已合并相邻空闲分区,解决外部碎片问题")
        return True

    def destroy_process(self, pid):
        """
        销毁进程,回收所有段的物理分区,清空段表和快表相关条目
        :param pid: 进程ID
        :return: 成功True/失败False
        """
        if pid not in self.process_seg_tables:
            print(f"【销毁失败】进程{pid}不存在")
            return False

        # 回收所有有效段的物理分区
        seg_table = self.process_seg_tables[pid]
        for seg_num, seg_info in seg_table.items():
            if seg_info["valid"]:
                free_start = seg_info["base"]
                free_size = seg_info["length"]
                self.free_partitions.append({"start": free_start, "size": free_size})
                print(f"【内存回收】进程{pid}段{seg_num} → 释放物理分区:{free_start}({free_size}B)")
        # 合并外部碎片
        self._merge_free_partitions()
        # 清空快表中该进程所有条目
        self.tlb = [item for item in self.tlb if item[0] != pid]
        # 删除进程段表
        del self.process_seg_tables[pid]

        print(f"【销毁成功】进程{pid},已回收所有段,合并外部碎片,清空段表/快表相关条目")
        return True

    def print_status(self, pid=None):
        """
        打印系统状态:物理内存空闲分区、快表、指定进程段表(pid=None则打印所有)
        """
        print("\n" + "="*120)
        # 物理内存状态
        total_used = self.total_physical_mem - sum(p["size"] for p in self.free_partitions)
        print(f"【物理内存状态】总大小:{self.total_physical_mem}B | 已用:{total_used}B | 空闲:{sum(p['size'] for p in self.free_partitions)}B")
        print(f"【空闲分区表】(共{len(self.free_partitions)}个,按起始地址排序):")
        for idx, p in enumerate(self.free_partitions):
            print(f"  分区{idx}:起始{p['start']:6d}B,大小{p['size']:6d}B")
        # 快表状态
        print(f"\n【快表(TLB)状态】当前条目:{len(self.tlb)}/{self.tlb_capacity}")
        if self.tlb:
            for idx, item in enumerate(self.tlb):
                print(f"  条目{idx}:进程{item[0]} | 段{item[1]} | 基址{item[2]}B")
        else:
            print(f"  快表为空")
        # 段表状态
        print(f"\n【进程段表状态】总进程数:{len(self.process_seg_tables)}")
        target_pids = [pid] if (pid is not None and pid in self.process_seg_tables) else self.process_seg_tables.keys()
        for p in target_pids:
            seg_table = self.process_seg_tables[p]
            print(f"  进程{p}段表(共{len(seg_table)}个段):")
            if seg_table:
                for seg_num, info in sorted(seg_table.items()):
                    print(f"    段{seg_num}:基址{info['base']:6d}B | 长度{info['length']:6d}B | 权限{info['perm']} | 有效位{1 if info['valid'] else 0}")
            else:
                print(f"    段表为空")
        print("="*120 + "\n")

# -------------------------- 测试用例:贴合分段存储核心特性 --------------------------
if __name__ == "__main__":
    # 1. 初始化分段系统:物理内存1MB,快表容量4条
    manager = SegmentMemoryManager(total_physical_mem=1024*1024, tlb_capacity=4)

    # 2. 创建进程,分配不同逻辑段(代码段、数据段、栈段,贴合进程逻辑结构)
    manager.create_process(1)  # 进程1:模拟应用程序,分3个段
    manager.create_process(2)  # 进程2:模拟库程序,分2个段
    # 进程1:0=代码段(r-x,只读执行,50KB)、1=数据段(rw-,读写,30KB)、2=栈段(rw-,读写,20KB)
    manager.allocate_segment(1, 0, 50*1024, "r-x")
    manager.allocate_segment(1, 1, 30*1024, "rw-")
    manager.allocate_segment(1, 2, 20*1024, "rw-")
    # 进程2:0=代码段(r-x,40KB)、1=数据段(rw-,25KB)
    manager.allocate_segment(2, 0, 40*1024, "r-x")
    manager.allocate_segment(2, 1, 25*1024, "rw-")
    manager.print_status()  # 打印初始状态

    # 3. 核心测试1:地址转换(快表命中/未命中、正常访问)
    print("===== 地址转换测试 =====")
    manager.logical_to_physical(1, 0, 1024, "r")   # 进程1段0(代码段),读访问,快表未命中
    manager.logical_to_physical(1, 0, 1024, "r")   # 同地址再次访问,快表命中(核心优化)
    manager.logical_to_physical(1, 1, 2048, "w")   # 进程1段1(数据段),写访问,快表未命中
    manager.logical_to_physical(2, 0, 512, "x")    # 进程2段0(代码段),执行访问,快表未命中
    manager.print_status()

    # 4. 核心测试2:内存保护+地址越界(分段关键特性)
    print("===== 内存保护&地址越界测试 =====")
    manager.logical_to_physical(1, 0, 1024, "w")   # 代码段写访问→权限越界(拒绝)
    manager.logical_to_physical(1, 1, 30*1024, "r")# 数据段偏移30KB→超出段长30KB(越界,拒绝)
    manager.logical_to_physical(2, 1, 1000, "x")   # 数据段执行访问→权限越界(拒绝)

    # 5. 核心测试3:释放段+合并外部碎片(体现外部碎片存在与解决)
    print("===== 释放段&碎片合并测试 =====")
    manager.free_segment(1, 1)  # 释放进程1数据段(30KB),产生外部碎片并合并
    manager.print_status()

    # 6. 核心测试4:销毁进程+全量回收(验证外部碎片合并为整段)
    print("===== 销毁进程&全量回收测试 =====")
    manager.destroy_process(1)  # 销毁进程1,回收所有段,合并为大空闲分区
    manager.print_status()

    # 7. 边界测试:重新分配段(利用回收的空闲分区)
    print("===== 重新分配段测试 =====")
    manager.create_process(3)
    manager.allocate_segment(3, 0, 60*1024, "r-x") # 分配60KB代码段,利用回收的空闲分区
    manager.print_status()
程序运行结果展示
bash 复制代码
【分段系统初始化成功】
物理总内存:1048576B | 快表容量:4条
初始空闲分区:起始0,大小1048576B | 支持权限:r(读)、w(写)、x(执行)、-(无)
【创建成功】进程1,初始化空段表
【创建成功】进程2,初始化空段表
【分配成功】进程1段0 → 基址0,长度51200,权限r-x
【特性体现】段内连续,无内部碎片(分配大小=段长),段间可离散
【分配成功】进程1段1 → 基址51200,长度30720,权限rw-
【特性体现】段内连续,无内部碎片(分配大小=段长),段间可离散
【分配成功】进程1段2 → 基址81920,长度20480,权限rw-
【特性体现】段内连续,无内部碎片(分配大小=段长),段间可离散
【分配成功】进程2段0 → 基址102400,长度40960,权限r-x
【特性体现】段内连续,无内部碎片(分配大小=段长),段间可离散
【分配成功】进程2段1 → 基址143360,长度25600,权限rw-
【特性体现】段内连续,无内部碎片(分配大小=段长),段间可离散

========================================================================================================================
【物理内存状态】总大小:1048576B | 已用:168960B | 空闲:879616B
【空闲分区表】(共1个,按起始地址排序):
  分区0:起始168960B,大小879616B

【快表(TLB)状态】当前条目:0/4
  快表为空

【进程段表状态】总进程数:2
  进程1段表(共3个段):
    段0:基址     0B | 长度 51200B | 权限r-x | 有效位1
    段1:基址 51200B | 长度 30720B | 权限rw- | 有效位1
    段2:基址 81920B | 长度 20480B | 权限rw- | 有效位1
  进程2段表(共2个段):
    段0:基址102400B | 长度 40960B | 权限r-x | 有效位1
    段1:基址143360B | 长度 25600B | 权限rw- | 有效位1
========================================================================================================================

===== 地址转换测试 =====
【快表未命中】进程1段0,查询段表并更新快表
【快表更新】进程1段0(基址0),当前快表:1/4
【段表命中】进程1段0→基址0,偏移1024,物理地址1024
【快表命中】进程1段0→基址0,偏移1024,物理地址1024
【快表未命中】进程1段1,查询段表并更新快表
【快表更新】进程1段1(基址51200),当前快表:2/4
【段表命中】进程1段1→基址51200,偏移2048,物理地址53248
【快表未命中】进程2段0,查询段表并更新快表
【快表更新】进程2段0(基址102400),当前快表:3/4
【段表命中】进程2段0→基址102400,偏移512,物理地址102912

========================================================================================================================
【物理内存状态】总大小:1048576B | 已用:168960B | 空闲:879616B
【空闲分区表】(共1个,按起始地址排序):
  分区0:起始168960B,大小879616B

【快表(TLB)状态】当前条目:3/4
  条目0:进程1 | 段0 | 基址0B
  条目1:进程1 | 段1 | 基址51200B
  条目2:进程2 | 段0 | 基址102400B

【进程段表状态】总进程数:2
  进程1段表(共3个段):
    段0:基址     0B | 长度 51200B | 权限r-x | 有效位1
    段1:基址 51200B | 长度 30720B | 权限rw- | 有效位1
    段2:基址 81920B | 长度 20480B | 权限rw- | 有效位1
  进程2段表(共2个段):
    段0:基址102400B | 长度 40960B | 权限r-x | 有效位1
    段1:基址143360B | 长度 25600B | 权限rw- | 有效位1
========================================================================================================================

===== 内存保护&地址越界测试 =====
【权限越界】进程1段0权限r-x,拒绝w访问
【地址越界】进程1段1(长30720),偏移30720超出段长范围
【权限越界】进程2段1权限rw-,拒绝x访问
===== 释放段&碎片合并测试 =====
【释放成功】进程1段1,回收物理分区:起始51200,大小30720
【碎片合并】已合并相邻空闲分区,解决外部碎片问题

========================================================================================================================
【物理内存状态】总大小:1048576B | 已用:138240B | 空闲:910336B
【空闲分区表】(共2个,按起始地址排序):
  分区0:起始 51200B,大小 30720B
  分区1:起始168960B,大小879616B

【快表(TLB)状态】当前条目:2/4
  条目0:进程1 | 段0 | 基址0B
  条目1:进程2 | 段0 | 基址102400B

【进程段表状态】总进程数:2
  进程1段表(共3个段):
    段0:基址     0B | 长度 51200B | 权限r-x | 有效位1
    段1:基址 51200B | 长度 30720B | 权限rw- | 有效位0
    段2:基址 81920B | 长度 20480B | 权限rw- | 有效位1
  进程2段表(共2个段):
    段0:基址102400B | 长度 40960B | 权限r-x | 有效位1
    段1:基址143360B | 长度 25600B | 权限rw- | 有效位1
========================================================================================================================

===== 销毁进程&全量回收测试 =====
【内存回收】进程1段0 → 释放物理分区:0(51200B)
【内存回收】进程1段2 → 释放物理分区:81920(20480B)
【销毁成功】进程1,已回收所有段,合并外部碎片,清空段表/快表相关条目

========================================================================================================================
【物理内存状态】总大小:1048576B | 已用:66560B | 空闲:982016B
【空闲分区表】(共2个,按起始地址排序):
  分区0:起始     0B,大小102400B
  分区1:起始168960B,大小879616B

【快表(TLB)状态】当前条目:1/4
  条目0:进程2 | 段0 | 基址102400B

【进程段表状态】总进程数:1
  进程2段表(共2个段):
    段0:基址102400B | 长度 40960B | 权限r-x | 有效位1
    段1:基址143360B | 长度 25600B | 权限rw- | 有效位1
========================================================================================================================

===== 重新分配段测试 =====
【创建成功】进程3,初始化空段表
【分配成功】进程3段0 → 基址0,长度61440,权限r-x
【特性体现】段内连续,无内部碎片(分配大小=段长),段间可离散

========================================================================================================================
【物理内存状态】总大小:1048576B | 已用:128000B | 空闲:920576B
【空闲分区表】(共2个,按起始地址排序):
  分区0:起始 61440B,大小 40960B
  分区1:起始168960B,大小879616B

【快表(TLB)状态】当前条目:1/4
  条目0:进程2 | 段0 | 基址102400B

【进程段表状态】总进程数:2
  进程2段表(共2个段):
    段0:基址102400B | 长度 40960B | 权限r-x | 有效位1
    段1:基址143360B | 长度 25600B | 权限rw- | 有效位1
  进程3段表(共1个段):
    段0:基址     0B | 长度 61440B | 权限r-x | 有效位1
========================================================================================================================

3. 段页式存储管理算法

核心原理

结合分页和分段的优点 ,是现代操作系统的主流存储管理方式 (如 Windows/Linux),核心是先分段、再分页

  1. 将进程的逻辑地址空间按逻辑结构划分为若干
  2. 每个段再按固定页大小 划分为若干
  3. 物理内存按页大小划分为物理块;
  4. 建立段表 + 页表 :段表记录每个段的页表基址和页表长度,页表记录页号与物理块号的映射;
  5. 地址转换:段号→段表→页表基址→页号→页表→物理块号→物理地址(段内偏移量 = 页号 × 页大小 + 页内偏移)。
关键特点
  • 内部碎片 (分页解决),无严重外部碎片(分页的块大小固定,仅段表 / 页表占用少量空间);
  • 兼顾进程逻辑结构(分段)和内存利用率(分页),支持模块共享、动态链接和内存保护;
  • 地址转换需三次访问内存 (段表→页表→物理内存),依赖快表(TLB) 做高速缓存优化(现代 OS 中快表命中率可达 90% 以上)。
适用场景:现代通用操作系统(Windows 10/11、Linux、macOS)的核心存储管理算法,兼顾所有优势。
算法实现思路
  • 物理内存管理 :复用分页的等大物理块 管理(页大小为 2 的幂次),数组记录块占用状态,彻底解决严重外部碎片 问题,仅保留分页的少量内部碎片(最后一页未占满,可忽略);
  • 逻辑地址分层 :严格遵循「段号 + 段内偏移」的分段地址结构,段内偏移再拆分为「段内页号 + 页内偏移」(分页地址结构),实现「先分段、后分页」的核心要求;
  • 二级映射表设计 (段表 + 页表,现代 OS 核心):
    • 进程段表:按 PID 管理,每个段表项记录段号、段长、段权限、页表基址(页表引用)、页表长度(段内总页数)、有效位,贴合进程逻辑模块划分(代码段 / 数据段等);
    • 段内页表:每个段对应独立页表,记录段内页号→物理块号的映射 + 有效位,实现分页的地址映射;
  • 核心特性融合
    • 继承分段优势:按进程逻辑分可变长段、段级内存保护(r/w/x 权限)、便于模块共享 / 动态链接;
    • 继承分页优势:段内分页用固定物理块,无外部碎片、物理块分配灵活、支持虚拟内存扩展;
  • 地址转换优化 :引入快表(TLB)缓存 **(PID, 段号,段内页号)→物理块号的映射,FIFO 替换策略,命中时直接跳过段表 + 页表查询,将 三次内存访问优化为一次 **,贴合现代 OS 90%+ 的快表命中率;
  • 完整的内存管理:实现段的分配(自动分页 + 物理块分配)、进程销毁(全量回收物理块 + 清空表项)、多层地址越界检查(段内偏移 / 段内页号双重校验)、权限越权拒绝。
Python代码
python 复制代码
import math


class SegmentPageMemoryManager:
    def __init__(self, page_size=4096, total_physical_mem=65536, tlb_capacity=8):
        """
        初始化段页式存储管理系统(先分段,段内再分页)
        :param page_size: 页大小(物理块大小),必须为2的幂次,默认4096B(4KB)
        :param total_physical_mem: 物理总内存,默认65536B(64KB),需为页大小整数倍
        :param tlb_capacity: 快表(TLB)容量,默认8条,FIFO替换策略
        """
        # 硬件约束校验:页大小为2的幂次、物理内存为页大小整数倍
        if not (page_size > 0 and (page_size & (page_size - 1)) == 0):
            raise ValueError("页大小必须是2的幂次(如1024、4096、8192)")
        if total_physical_mem % page_size != 0:
            raise ValueError("物理总内存必须是页大小的整数倍")

        self.page_size = page_size  # 页/物理块大小
        self.total_frames = total_physical_mem // page_size  # 物理块总数
        self.tlb_capacity = tlb_capacity  # 快表最大容量
        self.frame_occupied = [False] * self.total_frames  # 物理块占用状态:False=空闲,索引=块号

        self.tlb = []  # 快表:[(pid, 段号, 段内页号, 物理块号), ...],FIFO策略
        # 进程核心表结构:{PID: {段号: {段属性+页表}}, ...}
        # 段表项:seg_len(段长)、perm(权限)、page_table(段内页表)、page_table_len(段内总页数)、valid(有效位)
        # 页表项:{段内页号: {"frame": 物理块号, "valid": 有效位}}
        self.processes = {}
        # 位运算参数:快速解析段内偏移为「段内页号+页内偏移」
        self.page_offset_mask = page_size - 1  # 页内偏移掩码
        self.page_num_shift = int(math.log2(page_size))  # 页号右移位数
        # 合法权限:r(读)、w(写)、x(执行),三段式如"r-x"(代码段)、"rw-"(数据段)
        self.valid_perms = {"r", "w", "x", "-"}

        print(f"【段页式系统初始化成功】")
        print(f"页大小:{self.page_size}B | 物理总内存:{total_physical_mem}B | 物理块总数:{self.total_frames}")
        print(
            f"快表容量:{self.tlb_capacity}条 | 段内偏移解析:右移{self.page_num_shift}位取页号,与{self.page_offset_mask}取偏移")
        print(f"权限支持:r(读)、w(写)、x(执行)、-(无),示例:r-x(代码段)、rw-(数据段)\n")

    def create_process(self, pid):
        """创建进程,初始化空段表"""
        if pid in self.processes:
            print(f"【创建失败】进程{pid}已存在")
            return False
        self.processes[pid] = {}  # 进程pid的段表,初始为空
        print(f"【创建成功】进程{pid},初始化空段表")
        return True

    def _find_free_frame(self):
        """内部函数:查找第一个空闲物理块,返回块号/失败-1"""
        for frame_id in range(self.total_frames):
            if not self.frame_occupied[frame_id]:
                return frame_id
        return -1

    def _is_valid_permission(self, perm):
        """内部函数:校验权限字符串合法性(长度3,仅含r/w/x/-)"""
        if len(perm) != 3:
            return False
        return all(c in self.valid_perms for c in perm)

    def _calc_page_count(self, seg_length):
        """内部函数:计算段内总页数(向上取整,段长→页数,最后一页可能不满,少量内部碎片)"""
        return (seg_length + self.page_size - 1) // self.page_size

    def allocate_segment(self, pid, seg_num, seg_length, perm="rw-"):
        """
        为进程分配段(核心:自动分段+段内分页,分配物理块)
        :param pid: 进程ID
        :param seg_num: 段号(如0=代码段,1=数据段,2=栈段)
        :param seg_length: 段长(>0,按进程逻辑按需分配)
        :param perm: 段权限,默认rw-(读写)
        :return: 成功True/失败False
        """
        # 基础校验
        if pid not in self.processes:
            print(f"【分配失败】进程{pid}不存在")
            return False
        if seg_num < 0 or seg_length <= 0:
            print(f"【分配失败】段号{seg_num}或段长{seg_length}非法(段号≥0,段长>0)")
            return False
        if not self._is_valid_permission(perm):
            print(f"【分配失败】权限{perm}非法,需为3位r/w/x/-组合(如r-x、rw-)")
            return False
        # 该段已分配,直接返回
        if seg_num in self.processes[pid] and self.processes[pid][seg_num]["valid"]:
            print(f"【分配提示】进程{pid}段{seg_num}已分配,无需重复操作")
            return True

        # 核心步骤1:计算段内总页数,检查物理块是否充足
        page_count = self._calc_page_count(seg_length)
        free_frame_count = self.frame_occupied.count(False)
        if page_count > free_frame_count:
            print(f"【分配失败】进程{pid}段{seg_num}需{page_count}个物理块,仅剩余{free_frame_count}个")
            return False

        # 核心步骤2:为段内每个页分配物理块,构建段内页表
        page_table = {}
        for seg_page_num in range(page_count):
            frame_id = self._find_free_frame()
            self.frame_occupied[frame_id] = True
            page_table[seg_page_num] = {"frame": frame_id, "valid": True}
            print(f"  进程{pid}段{seg_num} → 段内页{seg_page_num}分配至物理块{frame_id}")

        # 核心步骤3:更新进程段表(二级映射核心:段表指向页表)
        self.processes[pid][seg_num] = {
            "seg_len": seg_length,  # 段总长度
            "perm": perm,  # 段级权限(内存保护)
            "page_table": page_table,  # 段内页表(页号→块号)
            "page_table_len": page_count,  # 页表长度(段内总页数)
            "valid": True  # 段有效位(是否调入内存)
        }

        print(f"【分配成功】进程{pid}段{seg_num} | 段长{seg_length}B | 段内页数{page_count} | 权限{perm}")
        print(f"【特性体现】段内连续分页、段间可离散 | 无严重外部碎片 | 仅最后一页可能有少量内部碎片\n")
        return True

    def _tlb_update(self, pid, seg_num, seg_page_num, frame_id):
        """内部函数:更新快表,FIFO替换,避免重复条目"""
        # 删除已存在的同(PID,段号,段内页号)条目,防止缓存重复
        self.tlb = [item for item in self.tlb if
                    not (item[0] == pid and item[1] == seg_num and item[2] == seg_page_num)]
        # 快表满则执行FIFO:移除最早加入的条目
        if len(self.tlb) >= self.tlb_capacity:
            removed = self.tlb.pop(0)
            print(f"【快表替换】FIFO移除:进程{removed[0]}段{removed[1]}页{removed[2]}→块{removed[3]}")
        # 新增条目到快表尾部
        self.tlb.append((pid, seg_num, seg_page_num, frame_id))
        print(
            f"【快表更新】进程{pid}段{seg_num}页{seg_page_num}→块{frame_id} | 快表现状:{len(self.tlb)}/{self.tlb_capacity}")

    def _parse_seg_offset(self, offset):
        """内部函数:解析段内偏移为「段内页号 + 页内偏移」(位运算高效实现)"""
        seg_page_num = offset >> self.page_num_shift
        page_offset = offset & self.page_offset_mask
        return seg_page_num, page_offset

    def logical_to_physical(self, pid, seg_num, seg_offset, access_type="r"):
        """
        核心功能:逻辑地址→物理地址(段号+段内偏移 → 物理地址)
        地址转换流程:快表查询→段表校验(权限/越界)→页表查询→物理地址计算
        :param pid: 进程ID
        :param seg_num: 段号
        :param seg_offset: 段内偏移量(≥0)
        :param access_type: 访问类型,r(读)/w(写)/x(执行),默认r
        :return: 物理地址/失败-1
        """
        # 步骤0:基础合法性校验
        if pid not in self.processes:
            print(f"【转换失败】进程{pid}不存在")
            return -1
        if seg_num not in self.processes[pid] or not self.processes[pid][seg_num]["valid"]:
            print(f"【转换失败】进程{pid}段{seg_num}未分配或未调入内存")
            return -1
        if seg_offset < 0 or access_type not in {"r", "w", "x"}:
            print(f"【转换失败】段内偏移{seg_offset}或访问类型{access_type}非法(偏移≥0,访问类型:r/w/x)")
            return -1

        seg_info = self.processes[pid][seg_num]
        seg_len = seg_info["seg_len"]
        seg_perm = seg_info["perm"]
        page_table = seg_info["page_table"]
        page_table_len = seg_info["page_table_len"]

        # 步骤1:段内偏移越界检查(偏移不能超过段长,分段核心越界检查)
        if seg_offset >= seg_len:
            print(f"【地址越界】进程{pid}段{seg_num}(段长{seg_len}B),偏移{seg_offset}B超出段范围")
            return -1

        # 步骤2:解析段内偏移为「段内页号 + 页内偏移」(分页核心地址解析)
        seg_page_num, page_offset = self._parse_seg_offset(seg_offset)
        print(f"【地址解析】进程{pid}段{seg_num}偏移{seg_offset}B → 段内页{seg_page_num},页内偏移{page_offset}B")

        # 步骤3:段内页号越界检查(页号不能超过段内总页数)
        if seg_page_num >= page_table_len:
            print(f"【地址越界】进程{pid}段{seg_num}(总页数{page_table_len}),段内页{seg_page_num}超出范围")
            return -1

        # 步骤4:段级权限校验(内存保护,分段核心优势)
        perm_map = {"r": 0, "w": 1, "x": 2}  # 权限位映射:0=r,1=w,2=x
        if seg_perm[perm_map[access_type]] == "-":
            print(f"【权限越界】进程{pid}段{seg_num}权限{seg_perm},拒绝{access_type}访问")
            return -1

        # 步骤5:快表(TLB)查询(优先缓存,规避三次内存访问,现代OS核心优化)
        for item in self.tlb:
            if item[0] == pid and item[1] == seg_num and item[2] == seg_page_num:
                frame_id = item[3]
                physical_addr = (frame_id << self.page_num_shift) + page_offset
                print(f"【快表命中】直接获取物理块{frame_id} → 物理地址{physical_addr}B")
                return physical_addr
        print(f"【快表未命中】开始查询段表→页表")

        # 步骤6:页表查询(段表已指向页表基址,二级映射)
        if seg_page_num not in page_table or not page_table[seg_page_num]["valid"]:
            print(f"【转换失败】进程{pid}段{seg_num}页{seg_page_num}未分配物理块")
            return -1
        frame_id = page_table[seg_page_num]["frame"]

        # 步骤7:计算物理地址 + 更新快表
        physical_addr = (frame_id << self.page_num_shift) + page_offset
        self._tlb_update(pid, seg_num, seg_page_num, frame_id)
        print(f"【段表→页表命中】物理块{frame_id} → 物理地址{physical_addr}B\n")
        return physical_addr

    def destroy_process(self, pid):
        """销毁进程,回收所有物理块,清空段表/页表/快表相关条目"""
        if pid not in self.processes:
            print(f"【销毁失败】进程{pid}不存在")
            return False

        # 步骤1:回收所有段的所有页对应的物理块
        seg_table = self.processes[pid]
        for seg_num, seg_info in seg_table.items():
            if seg_info["valid"]:
                page_table = seg_info["page_table"]
                for seg_page_num, page_info in page_table.items():
                    if page_info["valid"]:
                        frame_id = page_info["frame"]
                        self.frame_occupied[frame_id] = False
                        print(f"  回收:进程{pid}段{seg_num}页{seg_page_num} → 物理块{frame_id}")

        # 步骤2:清空快表中该进程的所有条目
        self.tlb = [item for item in self.tlb if item[0] != pid]
        # 步骤3:删除进程段表
        del self.processes[pid]

        print(f"【销毁成功】进程{pid},已回收所有物理块,清空段表/页表/快表相关条目\n")
        return True

    def print_system_status(self, pid=None):
        """打印系统状态:物理块、快表、指定/所有进程的段表+页表"""
        print("=" * 150)
        # 1. 物理块状态
        used_f = sum(self.frame_occupied)
        free_f = self.total_frames - used_f
        print(f"【物理块状态】总块数:{self.total_frames} | 已用:{used_f} | 空闲:{free_f}")
        used_frames = [i for i, val in enumerate(self.frame_occupied) if val]
        free_frames = [i for i, val in enumerate(self.frame_occupied) if not val]
        print(f"已用块号:{used_frames if used_frames else '无'} | 空闲块号:{free_frames if free_frames else '无'}")

        # 2. 快表(TLB)状态
        print(f"\n【快表(TLB)状态】当前条目:{len(self.tlb)}/{self.tlb_capacity}")
        if self.tlb:
            for idx, item in enumerate(self.tlb):
                print(f"  条目{idx}:进程{item[0]} | 段{item[1]} | 页{item[2]} → 块{item[3]}")
        else:
            print(f"  快表为空")

        # 3. 进程段表+页表状态
        print(f"\n【进程段页表状态】总进程数:{len(self.processes)}")
        target_pids = [pid] if (pid and pid in self.processes) else self.processes.keys()
        for p in target_pids:
            seg_table = self.processes[p]
            print(f"  进程{p}(共{len(seg_table)}个段):")
            if seg_table:
                for seg_num, seg_info in sorted(seg_table.items()):
                    print(
                        f"    段{seg_num}:段长{seg_info['seg_len']}B | 权限{seg_info['perm']} | 页数{seg_info['page_table_len']} | 有效位{1 if seg_info['valid'] else 0}")
                    # 打印段内页表
                    page_table = seg_info["page_table"]
                    for pg_num, pg_info in sorted(page_table.items()):
                        print(f"      └─页{pg_num} → 块{pg_info['frame']} | 有效位{1 if pg_info['valid'] else 0}")
            else:
                print(f"    无已分配段")
        print("=" * 150 + "\n")


# -------------------------- 测试用例:贴合段页式核心特性 --------------------------
if __name__ == "__main__":
    # 1. 初始化段页式系统:页大小4096B(4KB),物理内存64KB(16个物理块),快表容量4条
    try:
        manager = SegmentPageMemoryManager(page_size=4096, total_physical_mem=65536, tlb_capacity=4)
    except ValueError as e:
        print(e)
        exit()

    # 2. 创建进程,按逻辑分配段(代码段/数据段/栈段,继承分段优势)
    manager.create_process(1)  # 进程1:模拟应用程序,3个逻辑段
    # 进程1段分配:0=代码段(r-x,只读执行,10KB)、1=数据段(rw-,读写,8KB)、2=栈段(rw-,读写,5KB)
    manager.allocate_segment(1, 0, 10 * 1024, "r-x")
    manager.allocate_segment(1, 1, 8 * 1024, "rw-")
    manager.allocate_segment(1, 2, 5 * 1024, "rw-")
    manager.print_system_status(pid=1)  # 打印进程1的段页表状态

    # 3. 核心测试1:地址转换(快表未命中→命中,体现TLB优化)
    print("===== 地址转换-快表优化测试 =====")
    # 进程1段0(代码段),偏移5000B → 段内页1,偏移904B(读访问,合法)
    manager.logical_to_physical(1, 0, 5000, "r")
    # 同一地址再次访问→快表命中
    manager.logical_to_physical(1, 0, 5000, "r")
    # 进程1段1(数据段),偏移6000B → 段内页1,偏移1904B(写访问,合法)
    manager.logical_to_physical(1, 1, 6000, "w")

    # 4. 核心测试2:多层越界+权限校验(段页式安全特性)
    print("===== 越界&权限保护测试 =====")
    manager.logical_to_physical(1, 0, 5000, "w")  # 代码段写访问→权限越界
    manager.logical_to_physical(1, 1, 9000, "r")  # 数据段偏移9000B(段长8192B)→段内偏移越界
    manager.logical_to_physical(1, 2, 6000, "x")  # 栈段执行访问→权限越界

    # 5. 核心测试3:销毁进程,回收物理块(完整内存管理)
    print("===== 进程销毁-内存回收测试 =====")
    manager.destroy_process(1)
    manager.print_system_status()  # 打印回收后系统状态(物理块全空闲,快表清空)
程序运行结果展示
bash 复制代码
【段页式系统初始化成功】
页大小:4096B | 物理总内存:65536B | 物理块总数:16
快表容量:4条 | 段内偏移解析:右移12位取页号,与4095取偏移
权限支持:r(读)、w(写)、x(执行)、-(无),示例:r-x(代码段)、rw-(数据段)

【创建成功】进程1,初始化空段表
  进程1段0 → 段内页0分配至物理块0
  进程1段0 → 段内页1分配至物理块1
  进程1段0 → 段内页2分配至物理块2
【分配成功】进程1段0 | 段长10240B | 段内页数3 | 权限r-x
【特性体现】段内连续分页、段间可离散 | 无严重外部碎片 | 仅最后一页可能有少量内部碎片

  进程1段1 → 段内页0分配至物理块3
  进程1段1 → 段内页1分配至物理块4
【分配成功】进程1段1 | 段长8192B | 段内页数2 | 权限rw-
【特性体现】段内连续分页、段间可离散 | 无严重外部碎片 | 仅最后一页可能有少量内部碎片

  进程1段2 → 段内页0分配至物理块5
  进程1段2 → 段内页1分配至物理块6
【分配成功】进程1段2 | 段长5120B | 段内页数2 | 权限rw-
【特性体现】段内连续分页、段间可离散 | 无严重外部碎片 | 仅最后一页可能有少量内部碎片

======================================================================================================================================================
【物理块状态】总块数:16 | 已用:7 | 空闲:9
已用块号:[0, 1, 2, 3, 4, 5, 6] | 空闲块号:[7, 8, 9, 10, 11, 12, 13, 14, 15]

【快表(TLB)状态】当前条目:0/4
  快表为空

【进程段页表状态】总进程数:1
  进程1(共3个段):
    段0:段长10240B | 权限r-x | 页数3 | 有效位1
      └─页0 → 块0 | 有效位1
      └─页1 → 块1 | 有效位1
      └─页2 → 块2 | 有效位1
    段1:段长8192B | 权限rw- | 页数2 | 有效位1
      └─页0 → 块3 | 有效位1
      └─页1 → 块4 | 有效位1
    段2:段长5120B | 权限rw- | 页数2 | 有效位1
      └─页0 → 块5 | 有效位1
      └─页1 → 块6 | 有效位1
======================================================================================================================================================

===== 地址转换-快表优化测试 =====
【地址解析】进程1段0偏移5000B → 段内页1,页内偏移904B
【快表未命中】开始查询段表→页表
【快表更新】进程1段0页1→块1 | 快表现状:1/4
【段表→页表命中】物理块1 → 物理地址5000B

【地址解析】进程1段0偏移5000B → 段内页1,页内偏移904B
【快表命中】直接获取物理块1 → 物理地址5000B
【地址解析】进程1段1偏移6000B → 段内页1,页内偏移1904B
【快表未命中】开始查询段表→页表
【快表更新】进程1段1页1→块4 | 快表现状:2/4
【段表→页表命中】物理块4 → 物理地址18288B

===== 越界&权限保护测试 =====
【地址解析】进程1段0偏移5000B → 段内页1,页内偏移904B
【权限越界】进程1段0权限r-x,拒绝w访问
【地址越界】进程1段1(段长8192B),偏移9000B超出段范围
【地址越界】进程1段2(段长5120B),偏移6000B超出段范围
===== 进程销毁-内存回收测试 =====
  回收:进程1段0页0 → 物理块0
  回收:进程1段0页1 → 物理块1
  回收:进程1段0页2 → 物理块2
  回收:进程1段1页0 → 物理块3
  回收:进程1段1页1 → 物理块4
  回收:进程1段2页0 → 物理块5
  回收:进程1段2页1 → 物理块6
【销毁成功】进程1,已回收所有物理块,清空段表/页表/快表相关条目

======================================================================================================================================================
【物理块状态】总块数:16 | 已用:0 | 空闲:16
已用块号:无 | 空闲块号:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]

【快表(TLB)状态】当前条目:0/4
  快表为空

【进程段页表状态】总进程数:0
======================================================================================================================================================

四、虚拟存储页面置换算法

虚拟存储技术基于局部性原理 (进程运行时仅需访问部分内存),将进程的部分页 / 段 调入物理内存,其余存放在外存(硬盘),当进程需要访问外存的页时,通过缺页中断 将其调入物理内存;若物理内存已满,则通过页面置换算法淘汰部分已调入的页,腾出空间。

页面置换算法 是虚拟存储的核心,直接决定虚拟内存的性能,评价指标为缺页率 (缺页次数 / 总访问次数,缺页率越低性能越好)。算法分为理论最优算法实际可实现算法

核心前提

系统维护页框表 记录物理内存的块(页框)使用情况,为每个进程维护页表(标记页的状态:是否在内存、外存地址、访问位、修改位等),置换时优先淘汰 "无修改" 的页(无需写回外存,节省时间)。

1. 最优置换算法(Optimal, OPT)

原理

理论最优算法 :当发生缺页中断时,淘汰未来最久不被访问的页,能使缺页率降到最低。

特点
  • 优点:缺页率最低,理论性能最优;
  • 缺点:无法实际实现(需要预知进程未来的页面访问序列)。
作用:作为其他实际算法的性能基准,用于对比评价实际算法的优劣。
算法实现思路
  • 基础参数 :接收页面访问序列物理块数量(内存中可驻留的最大页面数);
  • 核心判断
    • 页面命中:当前访问页已在物理块中,无需操作;
    • 页面缺页 :当前访问页不在物理块中,缺页次数 + 1;
      • 物理块有空闲:直接将页面加入物理块;
      • 物理块已满:遍历物理块中所有页面,找到 ** 当前位置之后的访问序列中最久不出现(或永不出现)** 的页面,淘汰它并加入新页面;
  • 关键技巧 :利用剩余访问序列rfind(从后找最后一次出现位置),判断页面未来的访问情况 ------ 永不出现则位置为 - 1,优先淘汰;出现位置越靠后,未来使用越早,反之则越晚。
  • 统计指标:缺页次数、缺页率(缺页次数 / 总访问次数),直观体现算法性能。
Python代码
python 复制代码
def optimal_page_replacement(page_sequence, frame_count):
    """
    最优页面置换算法(OPT)实现
    :param page_sequence: 页面访问序列,列表类型(如[7,0,1,2,0,3,0,4])
    :param frame_count: 物理块数量,正整数(内存中可驻留的页面数)
    :return: 缺页次数, 缺页率
    """
    # 参数合法性校验
    if not isinstance(page_sequence, list) or len(page_sequence) == 0:
        raise ValueError("页面访问序列必须是非空列表")
    if not isinstance(frame_count, int) or frame_count < 1:
        raise ValueError("物理块数量必须是正整数")

    frames = []  # 物理块,存储当前驻留的页面,模拟内存页框
    page_faults = 0  # 缺页次数计数器
    total_access = len(page_sequence)  # 总页面访问次数

    # 遍历每一个访问的页面,记录当前索引
    for idx, current_page in enumerate(page_sequence):
        print(f"【步骤{idx + 1}】访问页面:{current_page} | 当前物理块:{frames}", end=" ")
        # 情况1:页面命中,无需操作
        if current_page in frames:
            print("→ 命中")
            continue

        # 情况2:页面缺页,缺页次数+1
        page_faults += 1
        # 子情况2.1:物理块有空闲,直接添加页面
        if len(frames) < frame_count:
            frames.append(current_page)
            print(f"→ 缺页(空闲块),物理块更新为:{frames}")
        # 子情况2.2:物理块已满,执行最优置换
        else:
            # 剩余未访问的页面序列(当前位置之后的所有页面)
            remaining_sequence = page_sequence[idx + 1:]
            # 记录物理块中每个页面在剩余序列中的最后出现位置
            last_occur = {}
            for page in frames:
                # 修复点:替换列表的rfind,实现列表元素最后一次出现的索引查找
                # 获取page在剩余序列中所有出现的索引,无则为空列表
                appear_indices = [i for i, val in enumerate(remaining_sequence) if val == page]
                # 有索引取最后一个(最大值),无则为-1
                last_occur[page] = max(appear_indices) if appear_indices else -1
            # 找到未来最久不出现的页面(最后出现位置最小,-1表示永不出现)
            replace_page = min(last_occur, key=lambda k: last_occur[k])
            # 淘汰旧页面,加入新页面
            replace_idx = frames.index(replace_page)
            frames[replace_idx] = current_page
            print(f"→ 缺页(置换),淘汰{replace_page},物理块更新为:{frames}")

    # 计算缺页率(保留4位小数)
    page_fault_rate = page_faults / total_access if total_access > 0 else 0.0
    return page_faults, round(page_fault_rate, 4)


# -------------------------- 经典测试用例 --------------------------
if __name__ == "__main__":
    # 测试用例1:操作系统教材经典页面访问序列,物理块数3/4(对比缺页率变化)
    classic_sequence = [7, 0, 1, 2, 0, 3, 0, 4, 2, 3, 0, 3, 2, 1, 2, 0, 1, 7, 0, 1]
    frame_nums = [3, 4]  # 分别测试3个和4个物理块的情况

    for frame_num in frame_nums:
        print("=" * 80)
        print(f"测试:页面访问序列={classic_sequence} | 物理块数量={frame_num}")
        print("=" * 80)
        try:
            fault_num, fault_rate = optimal_page_replacement(classic_sequence, frame_num)
        except ValueError as e:
            print(f"执行失败:{e}")
        else:
            print("=" * 80)
            print(f"【最终统计】总访问次数:{len(classic_sequence)} | 缺页次数:{fault_num} | 缺页率:{fault_rate:.2%}")
            print("=" * 80 + "\n")
程序运行结果展示
bash 复制代码
================================================================================
测试:页面访问序列=[7, 0, 1, 2, 0, 3, 0, 4, 2, 3, 0, 3, 2, 1, 2, 0, 1, 7, 0, 1] | 物理块数量=3
================================================================================
【步骤1】访问页面:7 | 当前物理块:[] → 缺页(空闲块),物理块更新为:[7]
【步骤2】访问页面:0 | 当前物理块:[7] → 缺页(空闲块),物理块更新为:[7, 0]
【步骤3】访问页面:1 | 当前物理块:[7, 0] → 缺页(空闲块),物理块更新为:[7, 0, 1]
【步骤4】访问页面:2 | 当前物理块:[7, 0, 1] → 缺页(置换),淘汰7,物理块更新为:[2, 0, 1]
【步骤5】访问页面:0 | 当前物理块:[2, 0, 1] → 命中
【步骤6】访问页面:3 | 当前物理块:[2, 0, 1] → 缺页(置换),淘汰2,物理块更新为:[3, 0, 1]
【步骤7】访问页面:0 | 当前物理块:[3, 0, 1] → 命中
【步骤8】访问页面:4 | 当前物理块:[3, 0, 1] → 缺页(置换),淘汰3,物理块更新为:[4, 0, 1]
【步骤9】访问页面:2 | 当前物理块:[4, 0, 1] → 缺页(置换),淘汰4,物理块更新为:[2, 0, 1]
【步骤10】访问页面:3 | 当前物理块:[2, 0, 1] → 缺页(置换),淘汰2,物理块更新为:[3, 0, 1]
【步骤11】访问页面:0 | 当前物理块:[3, 0, 1] → 命中
【步骤12】访问页面:3 | 当前物理块:[3, 0, 1] → 命中
【步骤13】访问页面:2 | 当前物理块:[3, 0, 1] → 缺页(置换),淘汰3,物理块更新为:[2, 0, 1]
【步骤14】访问页面:1 | 当前物理块:[2, 0, 1] → 命中
【步骤15】访问页面:2 | 当前物理块:[2, 0, 1] → 命中
【步骤16】访问页面:0 | 当前物理块:[2, 0, 1] → 命中
【步骤17】访问页面:1 | 当前物理块:[2, 0, 1] → 命中
【步骤18】访问页面:7 | 当前物理块:[2, 0, 1] → 缺页(置换),淘汰2,物理块更新为:[7, 0, 1]
【步骤19】访问页面:0 | 当前物理块:[7, 0, 1] → 命中
【步骤20】访问页面:1 | 当前物理块:[7, 0, 1] → 命中
================================================================================
【最终统计】总访问次数:20 | 缺页次数:10 | 缺页率:50.00%
================================================================================

================================================================================
测试:页面访问序列=[7, 0, 1, 2, 0, 3, 0, 4, 2, 3, 0, 3, 2, 1, 2, 0, 1, 7, 0, 1] | 物理块数量=4
================================================================================
【步骤1】访问页面:7 | 当前物理块:[] → 缺页(空闲块),物理块更新为:[7]
【步骤2】访问页面:0 | 当前物理块:[7] → 缺页(空闲块),物理块更新为:[7, 0]
【步骤3】访问页面:1 | 当前物理块:[7, 0] → 缺页(空闲块),物理块更新为:[7, 0, 1]
【步骤4】访问页面:2 | 当前物理块:[7, 0, 1] → 缺页(空闲块),物理块更新为:[7, 0, 1, 2]
【步骤5】访问页面:0 | 当前物理块:[7, 0, 1, 2] → 命中
【步骤6】访问页面:3 | 当前物理块:[7, 0, 1, 2] → 缺页(置换),淘汰2,物理块更新为:[7, 0, 1, 3]
【步骤7】访问页面:0 | 当前物理块:[7, 0, 1, 3] → 命中
【步骤8】访问页面:4 | 当前物理块:[7, 0, 1, 3] → 缺页(置换),淘汰3,物理块更新为:[7, 0, 1, 4]
【步骤9】访问页面:2 | 当前物理块:[7, 0, 1, 4] → 缺页(置换),淘汰4,物理块更新为:[7, 0, 1, 2]
【步骤10】访问页面:3 | 当前物理块:[7, 0, 1, 2] → 缺页(置换),淘汰2,物理块更新为:[7, 0, 1, 3]
【步骤11】访问页面:0 | 当前物理块:[7, 0, 1, 3] → 命中
【步骤12】访问页面:3 | 当前物理块:[7, 0, 1, 3] → 命中
【步骤13】访问页面:2 | 当前物理块:[7, 0, 1, 3] → 缺页(置换),淘汰3,物理块更新为:[7, 0, 1, 2]
【步骤14】访问页面:1 | 当前物理块:[7, 0, 1, 2] → 命中
【步骤15】访问页面:2 | 当前物理块:[7, 0, 1, 2] → 命中
【步骤16】访问页面:0 | 当前物理块:[7, 0, 1, 2] → 命中
【步骤17】访问页面:1 | 当前物理块:[7, 0, 1, 2] → 命中
【步骤18】访问页面:7 | 当前物理块:[7, 0, 1, 2] → 命中
【步骤19】访问页面:0 | 当前物理块:[7, 0, 1, 2] → 命中
【步骤20】访问页面:1 | 当前物理块:[7, 0, 1, 2] → 命中
================================================================================
【最终统计】总访问次数:20 | 缺页次数:9 | 缺页率:45.00%
================================================================================

2. 先进先出置换算法(First-In First-Out, FIFO)

原理

页面调入物理内存的先后顺序 淘汰,维护一个页访问队列,调入的页入队,缺页时淘汰队首的页(最早调入的页)。

特点
  • 优点:实现简单,无需记录页面的访问信息,仅需维护队列;
  • 缺点:违背局部性原理 (早期调入的页可能是进程频繁访问的核心页,如代码段),可能出现Belady 异常(增加物理内存的页框数,缺页率反而升高)。
适用场景:进程页面访问序列较简单、无频繁重复访问的场景,仅用于简单系统。
算法实现思路
  • 双结构模拟 :用frames列表模拟物理块(存储当前驻留页面),用queue列表模拟 FIFO 队列(严格记录页面调入先后顺序,队首 = 最早调入,队尾 = 最新调入),保证两者数据一致性;
  • 核心判断逻辑 (与 OPT 格式对齐,易对比):
    • 命中:当前访问页已在物理块中,无需任何操作;
    • 缺页 :当前访问页不在物理块中,缺页次数 + 1;
      • 物理块有空闲:新页加入物理块,同时入队(队尾);
      • 物理块已满 :淘汰队首页面(最早调入),队首出队,新页入队并加入物理块;
  • 关键特性:无需记录页面访问次数 / 未来序列,仅维护队列即可,实现极简(FIFO 核心优点);
  • 统计指标:返回缺页次数、缺页率(保留 4 位小数),新增 Belady 异常测试用例(物理块数增加,缺页率反而升高);
  • 参数校验:复用 OPT 的校验逻辑,保证代码鲁棒性,避免非法输入。
Python代码
python 复制代码
def fifo_page_replacement(page_sequence, frame_count):
    """
    先进先出页面置换算法(FIFO)实现
    :param page_sequence: 页面访问序列,列表类型(如[7,0,1,2,0,3,0,4])
    :param frame_count: 物理块数量,正整数(内存中可驻留的页面数)
    :return: 缺页次数, 缺页率(保留4位小数)
    """
    # 参数合法性校验(与OPT算法一致,方便统一调用)
    if not isinstance(page_sequence, list) or len(page_sequence) == 0:
        raise ValueError("页面访问序列必须是非空列表")
    if not isinstance(frame_count, int) or frame_count < 1:
        raise ValueError("物理块数量必须是正整数")

    frames = []  # 模拟物理块,存储当前驻留的页面
    queue = []  # FIFO队列,严格记录页面调入顺序(队首=最早调入,队尾=最新调入)
    page_faults = 0  # 缺页次数计数器
    total_access = len(page_sequence)  # 总页面访问次数

    # 遍历每一个访问的页面,记录当前步骤索引
    for idx, current_page in enumerate(page_sequence):
        print(f"【步骤{idx + 1}】访问页面:{current_page} | 物理块:{frames} | 调入队列:{queue}", end=" ")
        # 情况1:页面命中,无需任何操作(FIFO无需更新队列,仅判断存在性)
        if current_page in frames:
            print("→ 命中")
            continue

        # 情况2:页面缺页,缺页次数+1
        page_faults += 1
        # 子情况2.1:物理块有空闲,直接添加页面并入队(队尾)
        if len(frames) < frame_count:
            frames.append(current_page)
            queue.append(current_page)
            print(f"→ 缺页(空闲块),更新后:物理块={frames},队列={queue}")
        # 子情况2.2:物理块已满,执行FIFO置换(淘汰队首:最早调入的页面)
        else:
            # 淘汰队首页面:队列出队,物理块删除对应页面
            replace_page = queue.pop(0)  # 队列首=最早调入,弹出
            frames.remove(replace_page)
            # 新页面入队+加入物理块
            queue.append(current_page)
            frames.append(current_page)
            print(f"→ 缺页(置换),淘汰{replace_page},更新后:物理块={frames},队列={queue}")

    # 计算缺页率(保留4位小数,与OPT算法一致)
    page_fault_rate = page_faults / total_access if total_access > 0 else 0.0
    return page_faults, round(page_fault_rate, 4)


# -------------------------- 测试用例 --------------------------
if __name__ == "__main__":
    # 测试用例1:经典教材序列(与OPT同序列,方便对比缺页率)
    classic_sequence = [7, 0, 1, 2, 0, 3, 0, 4, 2, 3, 0, 3, 2, 1, 2, 0, 1, 7, 0, 1]
    frame_nums_classic = [3, 4]
    print("=" * 100)
    print("【测试用例1:经典序列(与OPT对比)】")
    print("=" * 100)
    for frame_num in frame_nums_classic:
        print(f"\n--- 物理块数量={frame_num} ---")
        try:
            fault_num, fault_rate = fifo_page_replacement(classic_sequence, frame_num)
        except ValueError as e:
            print(f"执行失败:{e}")
        else:
            print(f"【统计】总访问次数:{len(classic_sequence)} | 缺页次数:{fault_num} | 缺页率:{fault_rate:.2%}")

    # 测试用例2:Belady异常专属序列(FIFO独有,物理块数增加,缺页率反而升高)
    belady_sequence = [1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5]  # 经典Belady异常序列
    frame_nums_belady = [3, 4]  # 物理块3→4,缺页率升高
    print("\n" + "=" * 100)
    print("【测试用例2:Belady异常(FIFO独有特性)】")
    print("=" * 100)
    for frame_num in frame_nums_belady:
        print(f"\n--- 物理块数量={frame_num} ---")
        try:
            fault_num, fault_rate = fifo_page_replacement(belady_sequence, frame_num)
        except ValueError as e:
            print(f"执行失败:{e}")
        else:
            print(f"【统计】总访问次数:{len(belady_sequence)} | 缺页次数:{fault_num} | 缺页率:{fault_rate:.2%}")
程序运行结果展示
bash 复制代码
====================================================================================================
【测试用例1:经典序列(与OPT对比)】
====================================================================================================

--- 物理块数量=3 ---
【步骤1】访问页面:7 | 物理块:[] | 调入队列:[] → 缺页(空闲块),更新后:物理块=[7],队列=[7]
【步骤2】访问页面:0 | 物理块:[7] | 调入队列:[7] → 缺页(空闲块),更新后:物理块=[7, 0],队列=[7, 0]
【步骤3】访问页面:1 | 物理块:[7, 0] | 调入队列:[7, 0] → 缺页(空闲块),更新后:物理块=[7, 0, 1],队列=[7, 0, 1]
【步骤4】访问页面:2 | 物理块:[7, 0, 1] | 调入队列:[7, 0, 1] → 缺页(置换),淘汰7,更新后:物理块=[0, 1, 2],队列=[0, 1, 2]
【步骤5】访问页面:0 | 物理块:[0, 1, 2] | 调入队列:[0, 1, 2] → 命中
【步骤6】访问页面:3 | 物理块:[0, 1, 2] | 调入队列:[0, 1, 2] → 缺页(置换),淘汰0,更新后:物理块=[1, 2, 3],队列=[1, 2, 3]
【步骤7】访问页面:0 | 物理块:[1, 2, 3] | 调入队列:[1, 2, 3] → 缺页(置换),淘汰1,更新后:物理块=[2, 3, 0],队列=[2, 3, 0]
【步骤8】访问页面:4 | 物理块:[2, 3, 0] | 调入队列:[2, 3, 0] → 缺页(置换),淘汰2,更新后:物理块=[3, 0, 4],队列=[3, 0, 4]
【步骤9】访问页面:2 | 物理块:[3, 0, 4] | 调入队列:[3, 0, 4] → 缺页(置换),淘汰3,更新后:物理块=[0, 4, 2],队列=[0, 4, 2]
【步骤10】访问页面:3 | 物理块:[0, 4, 2] | 调入队列:[0, 4, 2] → 缺页(置换),淘汰0,更新后:物理块=[4, 2, 3],队列=[4, 2, 3]
【步骤11】访问页面:0 | 物理块:[4, 2, 3] | 调入队列:[4, 2, 3] → 缺页(置换),淘汰4,更新后:物理块=[2, 3, 0],队列=[2, 3, 0]
【步骤12】访问页面:3 | 物理块:[2, 3, 0] | 调入队列:[2, 3, 0] → 命中
【步骤13】访问页面:2 | 物理块:[2, 3, 0] | 调入队列:[2, 3, 0] → 命中
【步骤14】访问页面:1 | 物理块:[2, 3, 0] | 调入队列:[2, 3, 0] → 缺页(置换),淘汰2,更新后:物理块=[3, 0, 1],队列=[3, 0, 1]
【步骤15】访问页面:2 | 物理块:[3, 0, 1] | 调入队列:[3, 0, 1] → 缺页(置换),淘汰3,更新后:物理块=[0, 1, 2],队列=[0, 1, 2]
【步骤16】访问页面:0 | 物理块:[0, 1, 2] | 调入队列:[0, 1, 2] → 命中
【步骤17】访问页面:1 | 物理块:[0, 1, 2] | 调入队列:[0, 1, 2] → 命中
【步骤18】访问页面:7 | 物理块:[0, 1, 2] | 调入队列:[0, 1, 2] → 缺页(置换),淘汰0,更新后:物理块=[1, 2, 7],队列=[1, 2, 7]
【步骤19】访问页面:0 | 物理块:[1, 2, 7] | 调入队列:[1, 2, 7] → 缺页(置换),淘汰1,更新后:物理块=[2, 7, 0],队列=[2, 7, 0]
【步骤20】访问页面:1 | 物理块:[2, 7, 0] | 调入队列:[2, 7, 0] → 缺页(置换),淘汰2,更新后:物理块=[7, 0, 1],队列=[7, 0, 1]
【统计】总访问次数:20 | 缺页次数:15 | 缺页率:75.00%

--- 物理块数量=4 ---
【步骤1】访问页面:7 | 物理块:[] | 调入队列:[] → 缺页(空闲块),更新后:物理块=[7],队列=[7]
【步骤2】访问页面:0 | 物理块:[7] | 调入队列:[7] → 缺页(空闲块),更新后:物理块=[7, 0],队列=[7, 0]
【步骤3】访问页面:1 | 物理块:[7, 0] | 调入队列:[7, 0] → 缺页(空闲块),更新后:物理块=[7, 0, 1],队列=[7, 0, 1]
【步骤4】访问页面:2 | 物理块:[7, 0, 1] | 调入队列:[7, 0, 1] → 缺页(空闲块),更新后:物理块=[7, 0, 1, 2],队列=[7, 0, 1, 2]
【步骤5】访问页面:0 | 物理块:[7, 0, 1, 2] | 调入队列:[7, 0, 1, 2] → 命中
【步骤6】访问页面:3 | 物理块:[7, 0, 1, 2] | 调入队列:[7, 0, 1, 2] → 缺页(置换),淘汰7,更新后:物理块=[0, 1, 2, 3],队列=[0, 1, 2, 3]
【步骤7】访问页面:0 | 物理块:[0, 1, 2, 3] | 调入队列:[0, 1, 2, 3] → 命中
【步骤8】访问页面:4 | 物理块:[0, 1, 2, 3] | 调入队列:[0, 1, 2, 3] → 缺页(置换),淘汰0,更新后:物理块=[1, 2, 3, 4],队列=[1, 2, 3, 4]
【步骤9】访问页面:2 | 物理块:[1, 2, 3, 4] | 调入队列:[1, 2, 3, 4] → 命中
【步骤10】访问页面:3 | 物理块:[1, 2, 3, 4] | 调入队列:[1, 2, 3, 4] → 命中
【步骤11】访问页面:0 | 物理块:[1, 2, 3, 4] | 调入队列:[1, 2, 3, 4] → 缺页(置换),淘汰1,更新后:物理块=[2, 3, 4, 0],队列=[2, 3, 4, 0]
【步骤12】访问页面:3 | 物理块:[2, 3, 4, 0] | 调入队列:[2, 3, 4, 0] → 命中
【步骤13】访问页面:2 | 物理块:[2, 3, 4, 0] | 调入队列:[2, 3, 4, 0] → 命中
【步骤14】访问页面:1 | 物理块:[2, 3, 4, 0] | 调入队列:[2, 3, 4, 0] → 缺页(置换),淘汰2,更新后:物理块=[3, 4, 0, 1],队列=[3, 4, 0, 1]
【步骤15】访问页面:2 | 物理块:[3, 4, 0, 1] | 调入队列:[3, 4, 0, 1] → 缺页(置换),淘汰3,更新后:物理块=[4, 0, 1, 2],队列=[4, 0, 1, 2]
【步骤16】访问页面:0 | 物理块:[4, 0, 1, 2] | 调入队列:[4, 0, 1, 2] → 命中
【步骤17】访问页面:1 | 物理块:[4, 0, 1, 2] | 调入队列:[4, 0, 1, 2] → 命中
【步骤18】访问页面:7 | 物理块:[4, 0, 1, 2] | 调入队列:[4, 0, 1, 2] → 缺页(置换),淘汰4,更新后:物理块=[0, 1, 2, 7],队列=[0, 1, 2, 7]
【步骤19】访问页面:0 | 物理块:[0, 1, 2, 7] | 调入队列:[0, 1, 2, 7] → 命中
【步骤20】访问页面:1 | 物理块:[0, 1, 2, 7] | 调入队列:[0, 1, 2, 7] → 命中
【统计】总访问次数:20 | 缺页次数:10 | 缺页率:50.00%

====================================================================================================
【测试用例2:Belady异常(FIFO独有特性)】
====================================================================================================

--- 物理块数量=3 ---
【步骤1】访问页面:1 | 物理块:[] | 调入队列:[] → 缺页(空闲块),更新后:物理块=[1],队列=[1]
【步骤2】访问页面:2 | 物理块:[1] | 调入队列:[1] → 缺页(空闲块),更新后:物理块=[1, 2],队列=[1, 2]
【步骤3】访问页面:3 | 物理块:[1, 2] | 调入队列:[1, 2] → 缺页(空闲块),更新后:物理块=[1, 2, 3],队列=[1, 2, 3]
【步骤4】访问页面:4 | 物理块:[1, 2, 3] | 调入队列:[1, 2, 3] → 缺页(置换),淘汰1,更新后:物理块=[2, 3, 4],队列=[2, 3, 4]
【步骤5】访问页面:1 | 物理块:[2, 3, 4] | 调入队列:[2, 3, 4] → 缺页(置换),淘汰2,更新后:物理块=[3, 4, 1],队列=[3, 4, 1]
【步骤6】访问页面:2 | 物理块:[3, 4, 1] | 调入队列:[3, 4, 1] → 缺页(置换),淘汰3,更新后:物理块=[4, 1, 2],队列=[4, 1, 2]
【步骤7】访问页面:5 | 物理块:[4, 1, 2] | 调入队列:[4, 1, 2] → 缺页(置换),淘汰4,更新后:物理块=[1, 2, 5],队列=[1, 2, 5]
【步骤8】访问页面:1 | 物理块:[1, 2, 5] | 调入队列:[1, 2, 5] → 命中
【步骤9】访问页面:2 | 物理块:[1, 2, 5] | 调入队列:[1, 2, 5] → 命中
【步骤10】访问页面:3 | 物理块:[1, 2, 5] | 调入队列:[1, 2, 5] → 缺页(置换),淘汰1,更新后:物理块=[2, 5, 3],队列=[2, 5, 3]
【步骤11】访问页面:4 | 物理块:[2, 5, 3] | 调入队列:[2, 5, 3] → 缺页(置换),淘汰2,更新后:物理块=[5, 3, 4],队列=[5, 3, 4]
【步骤12】访问页面:5 | 物理块:[5, 3, 4] | 调入队列:[5, 3, 4] → 命中
【统计】总访问次数:12 | 缺页次数:9 | 缺页率:75.00%

--- 物理块数量=4 ---
【步骤1】访问页面:1 | 物理块:[] | 调入队列:[] → 缺页(空闲块),更新后:物理块=[1],队列=[1]
【步骤2】访问页面:2 | 物理块:[1] | 调入队列:[1] → 缺页(空闲块),更新后:物理块=[1, 2],队列=[1, 2]
【步骤3】访问页面:3 | 物理块:[1, 2] | 调入队列:[1, 2] → 缺页(空闲块),更新后:物理块=[1, 2, 3],队列=[1, 2, 3]
【步骤4】访问页面:4 | 物理块:[1, 2, 3] | 调入队列:[1, 2, 3] → 缺页(空闲块),更新后:物理块=[1, 2, 3, 4],队列=[1, 2, 3, 4]
【步骤5】访问页面:1 | 物理块:[1, 2, 3, 4] | 调入队列:[1, 2, 3, 4] → 命中
【步骤6】访问页面:2 | 物理块:[1, 2, 3, 4] | 调入队列:[1, 2, 3, 4] → 命中
【步骤7】访问页面:5 | 物理块:[1, 2, 3, 4] | 调入队列:[1, 2, 3, 4] → 缺页(置换),淘汰1,更新后:物理块=[2, 3, 4, 5],队列=[2, 3, 4, 5]
【步骤8】访问页面:1 | 物理块:[2, 3, 4, 5] | 调入队列:[2, 3, 4, 5] → 缺页(置换),淘汰2,更新后:物理块=[3, 4, 5, 1],队列=[3, 4, 5, 1]
【步骤9】访问页面:2 | 物理块:[3, 4, 5, 1] | 调入队列:[3, 4, 5, 1] → 缺页(置换),淘汰3,更新后:物理块=[4, 5, 1, 2],队列=[4, 5, 1, 2]
【步骤10】访问页面:3 | 物理块:[4, 5, 1, 2] | 调入队列:[4, 5, 1, 2] → 缺页(置换),淘汰4,更新后:物理块=[5, 1, 2, 3],队列=[5, 1, 2, 3]
【步骤11】访问页面:4 | 物理块:[5, 1, 2, 3] | 调入队列:[5, 1, 2, 3] → 缺页(置换),淘汰5,更新后:物理块=[1, 2, 3, 4],队列=[1, 2, 3, 4]
【步骤12】访问页面:5 | 物理块:[1, 2, 3, 4] | 调入队列:[1, 2, 3, 4] → 缺页(置换),淘汰1,更新后:物理块=[2, 3, 4, 5],队列=[2, 3, 4, 5]
【统计】总访问次数:12 | 缺页次数:10 | 缺页率:83.33%

3. 最近最少使用置换算法(Least Recently Used, LRU)

原理

基于局部性原理 的实际实现:淘汰最近一段时间内最久未被访问的页,认为 "近期未访问的页,未来也大概率不会访问"。

实现方式
  • 硬件方式:为每个页设置访问时间戳,缺页时遍历所有页,选择时间戳最早的页(现代 CPU 的 MMU 支持);
  • 软件方式:维护双向链表,页面被访问时移到链表头,缺页时淘汰链表尾的页。
特点
  • 优点:缺页率远低于 FIFO,性能接近 OPT,符合局部性原理,无 Belady 异常;
  • 缺点:需要硬件支持(时间戳)或额外的系统开销(维护链表 / 访问记录)。
适用场景:现代 OS 的核心置换算法之一(如 Linux 的基础 LRU),兼顾性能和实现难度。
算法实现思路

选用访问记录列表模拟 LRU 的访问顺序(替代双向链表,实现更简单、易理解),这是软件实现 LRU 的经典简化方案,核心规则:

  • frames模拟物理块,access_order维护页面最近访问顺序末尾 = 最新访问 / 调入开头 = 最近最久未访问);
  • 页面 ** 被访问(无论命中 / 新调入)** 时,都移到access_order末尾(更新为最新访问状态);
  • 缺页置换时,淘汰access_order开头的页面(严格符合 "最近最久未访问" 规则)。

整体逻辑和前两种算法格式完全对齐,分 3 种核心情况,无复杂计算,贴合局部性原理:

  1. 命中:页在物理块中,更新其访问顺序(移到末尾),物理块不变;
  2. 缺页 + 空闲块:缺页次数 + 1,页加入物理块,同时加入访问记录末尾(最新状态);
  3. 缺页 + 物理块满:缺页次数 + 1,淘汰访问记录开头的页(最近最久未访问),新页加入物理块和记录末尾。
Python代码
python 复制代码
def lru_page_replacement(page_sequence, frame_count):
    """
    最近最少使用页面置换算法(LRU)实现
    软件简化版:用访问记录列表维护访问顺序(末尾=最新访问,开头=最近最久未访问)
    :param page_sequence: 页面访问序列,列表类型
    :param frame_count: 物理块数量,正整数
    :return: 缺页次数, 缺页率(保留4位小数)
    """
    # 参数合法性校验(与OPT/FIFO完全一致,方便统一调用对比)
    if not isinstance(page_sequence, list) or len(page_sequence) == 0:
        raise ValueError("页面访问序列必须是非空列表")
    if not isinstance(frame_count, int) or frame_count < 1:
        raise ValueError("物理块数量必须是正整数")

    frames = []  # 模拟物理块,存储当前驻留的页面
    access_order = []  # 维护页面最近访问顺序:末尾=最新访问,开头=最近最久未访问
    page_faults = 0  # 缺页次数计数器
    total_access = len(page_sequence)  # 总页面访问次数

    # 遍历每一个访问的页面,记录步骤索引
    for idx, current_page in enumerate(page_sequence):
        print(f"【步骤{idx + 1}】访问页面:{current_page} | 物理块:{frames} | 访问顺序:{access_order}", end=" ")
        # 情况1:页面命中 → 仅更新访问顺序(移到末尾,标记为最新访问)
        if current_page in frames:
            access_order.remove(current_page)  # 移除原有位置
            access_order.append(current_page)  # 移到末尾,更新为最新访问
            print("→ 命中(更新访问顺序)")
            continue

        # 情况2:页面缺页 → 缺页次数+1
        page_faults += 1
        # 子情况2.1:物理块有空闲 → 直接添加页面,同时标记为最新访问
        if len(frames) < frame_count:
            frames.append(current_page)
            access_order.append(current_page)
            print(f"→ 缺页(空闲块),更新后:物理块={frames},访问顺序={access_order}")
        # 子情况2.2:物理块已满 → 淘汰【最近最久未访问】的页(访问顺序开头)
        else:
            replace_page = access_order.pop(0)  # 淘汰访问顺序开头的页
            frames.remove(replace_page)  # 物理块同步删除
            # 新页面加入物理块+访问顺序末尾(最新访问)
            frames.append(current_page)
            access_order.append(current_page)
            print(f"→ 缺页(置换),淘汰{replace_page},更新后:物理块={frames},访问顺序={access_order}")

    # 计算缺页率(保留4位小数,与OPT/FIFO一致,方便对比)
    page_fault_rate = page_faults / total_access if total_access > 0 else 0.0
    return page_faults, round(page_fault_rate, 4)


# -------------------------- 测试用例 --------------------------
if __name__ == "__main__":
    # 测试用例1:经典教材序列(与OPT/FIFO同序列,横向对比缺页率)
    classic_sequence = [7, 0, 1, 2, 0, 3, 0, 4, 2, 3, 0, 3, 2, 1, 2, 0, 1, 7, 0, 1]
    frame_nums_classic = [3, 4]
    print("=" * 110)
    print("【测试用例1:经典序列(与OPT/FIFO横向对比)】")
    print("=" * 110)
    for frame_num in frame_nums_classic:
        print(f"\n--- 物理块数量={frame_num} ---")
        try:
            fault_num, fault_rate = lru_page_replacement(classic_sequence, frame_num)
        except ValueError as e:
            print(f"执行失败:{e}")
        else:
            print(f"【统计】总访问次数:{len(classic_sequence)} | 缺页次数:{fault_num} | 缺页率:{fault_rate:.2%}")

    # 测试用例2:Belady异常验证序列(证明LRU无Belady异常,物理块越多缺页率越低)
    belady_sequence = [1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5]  # FIFO会出现Belady的经典序列
    frame_nums_belady = [3, 4]
    print("\n" + "=" * 110)
    print("【测试用例2:Belady异常验证(LRU无该缺陷)】")
    print("=" * 110)
    for frame_num in frame_nums_belady:
        print(f"\n--- 物理块数量={frame_num} ---")
        try:
            fault_num, fault_rate = lru_page_replacement(belady_sequence, frame_num)
        except ValueError as e:
            print(f"执行失败:{e}")
        else:
            print(f"【统计】总访问次数:{len(belady_sequence)} | 缺页次数:{fault_num} | 缺页率:{fault_rate:.2%}")
程序运行结果展示
bash 复制代码
==============================================================================================================
【测试用例1:经典序列(与OPT/FIFO横向对比)】
==============================================================================================================

--- 物理块数量=3 ---
【步骤1】访问页面:7 | 物理块:[] | 访问顺序:[] → 缺页(空闲块),更新后:物理块=[7],访问顺序=[7]
【步骤2】访问页面:0 | 物理块:[7] | 访问顺序:[7] → 缺页(空闲块),更新后:物理块=[7, 0],访问顺序=[7, 0]
【步骤3】访问页面:1 | 物理块:[7, 0] | 访问顺序:[7, 0] → 缺页(空闲块),更新后:物理块=[7, 0, 1],访问顺序=[7, 0, 1]
【步骤4】访问页面:2 | 物理块:[7, 0, 1] | 访问顺序:[7, 0, 1] → 缺页(置换),淘汰7,更新后:物理块=[0, 1, 2],访问顺序=[0, 1, 2]
【步骤5】访问页面:0 | 物理块:[0, 1, 2] | 访问顺序:[0, 1, 2] → 命中(更新访问顺序)
【步骤6】访问页面:3 | 物理块:[0, 1, 2] | 访问顺序:[1, 2, 0] → 缺页(置换),淘汰1,更新后:物理块=[0, 2, 3],访问顺序=[2, 0, 3]
【步骤7】访问页面:0 | 物理块:[0, 2, 3] | 访问顺序:[2, 0, 3] → 命中(更新访问顺序)
【步骤8】访问页面:4 | 物理块:[0, 2, 3] | 访问顺序:[2, 3, 0] → 缺页(置换),淘汰2,更新后:物理块=[0, 3, 4],访问顺序=[3, 0, 4]
【步骤9】访问页面:2 | 物理块:[0, 3, 4] | 访问顺序:[3, 0, 4] → 缺页(置换),淘汰3,更新后:物理块=[0, 4, 2],访问顺序=[0, 4, 2]
【步骤10】访问页面:3 | 物理块:[0, 4, 2] | 访问顺序:[0, 4, 2] → 缺页(置换),淘汰0,更新后:物理块=[4, 2, 3],访问顺序=[4, 2, 3]
【步骤11】访问页面:0 | 物理块:[4, 2, 3] | 访问顺序:[4, 2, 3] → 缺页(置换),淘汰4,更新后:物理块=[2, 3, 0],访问顺序=[2, 3, 0]
【步骤12】访问页面:3 | 物理块:[2, 3, 0] | 访问顺序:[2, 3, 0] → 命中(更新访问顺序)
【步骤13】访问页面:2 | 物理块:[2, 3, 0] | 访问顺序:[2, 0, 3] → 命中(更新访问顺序)
【步骤14】访问页面:1 | 物理块:[2, 3, 0] | 访问顺序:[0, 3, 2] → 缺页(置换),淘汰0,更新后:物理块=[2, 3, 1],访问顺序=[3, 2, 1]
【步骤15】访问页面:2 | 物理块:[2, 3, 1] | 访问顺序:[3, 2, 1] → 命中(更新访问顺序)
【步骤16】访问页面:0 | 物理块:[2, 3, 1] | 访问顺序:[3, 1, 2] → 缺页(置换),淘汰3,更新后:物理块=[2, 1, 0],访问顺序=[1, 2, 0]
【步骤17】访问页面:1 | 物理块:[2, 1, 0] | 访问顺序:[1, 2, 0] → 命中(更新访问顺序)
【步骤18】访问页面:7 | 物理块:[2, 1, 0] | 访问顺序:[2, 0, 1] → 缺页(置换),淘汰2,更新后:物理块=[1, 0, 7],访问顺序=[0, 1, 7]
【步骤19】访问页面:0 | 物理块:[1, 0, 7] | 访问顺序:[0, 1, 7] → 命中(更新访问顺序)
【步骤20】访问页面:1 | 物理块:[1, 0, 7] | 访问顺序:[1, 7, 0] → 命中(更新访问顺序)
【统计】总访问次数:20 | 缺页次数:12 | 缺页率:60.00%

--- 物理块数量=4 ---
【步骤1】访问页面:7 | 物理块:[] | 访问顺序:[] → 缺页(空闲块),更新后:物理块=[7],访问顺序=[7]
【步骤2】访问页面:0 | 物理块:[7] | 访问顺序:[7] → 缺页(空闲块),更新后:物理块=[7, 0],访问顺序=[7, 0]
【步骤3】访问页面:1 | 物理块:[7, 0] | 访问顺序:[7, 0] → 缺页(空闲块),更新后:物理块=[7, 0, 1],访问顺序=[7, 0, 1]
【步骤4】访问页面:2 | 物理块:[7, 0, 1] | 访问顺序:[7, 0, 1] → 缺页(空闲块),更新后:物理块=[7, 0, 1, 2],访问顺序=[7, 0, 1, 2]
【步骤5】访问页面:0 | 物理块:[7, 0, 1, 2] | 访问顺序:[7, 0, 1, 2] → 命中(更新访问顺序)
【步骤6】访问页面:3 | 物理块:[7, 0, 1, 2] | 访问顺序:[7, 1, 2, 0] → 缺页(置换),淘汰7,更新后:物理块=[0, 1, 2, 3],访问顺序=[1, 2, 0, 3]
【步骤7】访问页面:0 | 物理块:[0, 1, 2, 3] | 访问顺序:[1, 2, 0, 3] → 命中(更新访问顺序)
【步骤8】访问页面:4 | 物理块:[0, 1, 2, 3] | 访问顺序:[1, 2, 3, 0] → 缺页(置换),淘汰1,更新后:物理块=[0, 2, 3, 4],访问顺序=[2, 3, 0, 4]
【步骤9】访问页面:2 | 物理块:[0, 2, 3, 4] | 访问顺序:[2, 3, 0, 4] → 命中(更新访问顺序)
【步骤10】访问页面:3 | 物理块:[0, 2, 3, 4] | 访问顺序:[3, 0, 4, 2] → 命中(更新访问顺序)
【步骤11】访问页面:0 | 物理块:[0, 2, 3, 4] | 访问顺序:[0, 4, 2, 3] → 命中(更新访问顺序)
【步骤12】访问页面:3 | 物理块:[0, 2, 3, 4] | 访问顺序:[4, 2, 3, 0] → 命中(更新访问顺序)
【步骤13】访问页面:2 | 物理块:[0, 2, 3, 4] | 访问顺序:[4, 2, 0, 3] → 命中(更新访问顺序)
【步骤14】访问页面:1 | 物理块:[0, 2, 3, 4] | 访问顺序:[4, 0, 3, 2] → 缺页(置换),淘汰4,更新后:物理块=[0, 2, 3, 1],访问顺序=[0, 3, 2, 1]
【步骤15】访问页面:2 | 物理块:[0, 2, 3, 1] | 访问顺序:[0, 3, 2, 1] → 命中(更新访问顺序)
【步骤16】访问页面:0 | 物理块:[0, 2, 3, 1] | 访问顺序:[0, 3, 1, 2] → 命中(更新访问顺序)
【步骤17】访问页面:1 | 物理块:[0, 2, 3, 1] | 访问顺序:[3, 1, 2, 0] → 命中(更新访问顺序)
【步骤18】访问页面:7 | 物理块:[0, 2, 3, 1] | 访问顺序:[3, 2, 0, 1] → 缺页(置换),淘汰3,更新后:物理块=[0, 2, 1, 7],访问顺序=[2, 0, 1, 7]
【步骤19】访问页面:0 | 物理块:[0, 2, 1, 7] | 访问顺序:[2, 0, 1, 7] → 命中(更新访问顺序)
【步骤20】访问页面:1 | 物理块:[0, 2, 1, 7] | 访问顺序:[2, 1, 7, 0] → 命中(更新访问顺序)
【统计】总访问次数:20 | 缺页次数:8 | 缺页率:40.00%

==============================================================================================================
【测试用例2:Belady异常验证(LRU无该缺陷)】
==============================================================================================================

--- 物理块数量=3 ---
【步骤1】访问页面:1 | 物理块:[] | 访问顺序:[] → 缺页(空闲块),更新后:物理块=[1],访问顺序=[1]
【步骤2】访问页面:2 | 物理块:[1] | 访问顺序:[1] → 缺页(空闲块),更新后:物理块=[1, 2],访问顺序=[1, 2]
【步骤3】访问页面:3 | 物理块:[1, 2] | 访问顺序:[1, 2] → 缺页(空闲块),更新后:物理块=[1, 2, 3],访问顺序=[1, 2, 3]
【步骤4】访问页面:4 | 物理块:[1, 2, 3] | 访问顺序:[1, 2, 3] → 缺页(置换),淘汰1,更新后:物理块=[2, 3, 4],访问顺序=[2, 3, 4]
【步骤5】访问页面:1 | 物理块:[2, 3, 4] | 访问顺序:[2, 3, 4] → 缺页(置换),淘汰2,更新后:物理块=[3, 4, 1],访问顺序=[3, 4, 1]
【步骤6】访问页面:2 | 物理块:[3, 4, 1] | 访问顺序:[3, 4, 1] → 缺页(置换),淘汰3,更新后:物理块=[4, 1, 2],访问顺序=[4, 1, 2]
【步骤7】访问页面:5 | 物理块:[4, 1, 2] | 访问顺序:[4, 1, 2] → 缺页(置换),淘汰4,更新后:物理块=[1, 2, 5],访问顺序=[1, 2, 5]
【步骤8】访问页面:1 | 物理块:[1, 2, 5] | 访问顺序:[1, 2, 5] → 命中(更新访问顺序)
【步骤9】访问页面:2 | 物理块:[1, 2, 5] | 访问顺序:[2, 5, 1] → 命中(更新访问顺序)
【步骤10】访问页面:3 | 物理块:[1, 2, 5] | 访问顺序:[5, 1, 2] → 缺页(置换),淘汰5,更新后:物理块=[1, 2, 3],访问顺序=[1, 2, 3]
【步骤11】访问页面:4 | 物理块:[1, 2, 3] | 访问顺序:[1, 2, 3] → 缺页(置换),淘汰1,更新后:物理块=[2, 3, 4],访问顺序=[2, 3, 4]
【步骤12】访问页面:5 | 物理块:[2, 3, 4] | 访问顺序:[2, 3, 4] → 缺页(置换),淘汰2,更新后:物理块=[3, 4, 5],访问顺序=[3, 4, 5]
【统计】总访问次数:12 | 缺页次数:10 | 缺页率:83.33%

--- 物理块数量=4 ---
【步骤1】访问页面:1 | 物理块:[] | 访问顺序:[] → 缺页(空闲块),更新后:物理块=[1],访问顺序=[1]
【步骤2】访问页面:2 | 物理块:[1] | 访问顺序:[1] → 缺页(空闲块),更新后:物理块=[1, 2],访问顺序=[1, 2]
【步骤3】访问页面:3 | 物理块:[1, 2] | 访问顺序:[1, 2] → 缺页(空闲块),更新后:物理块=[1, 2, 3],访问顺序=[1, 2, 3]
【步骤4】访问页面:4 | 物理块:[1, 2, 3] | 访问顺序:[1, 2, 3] → 缺页(空闲块),更新后:物理块=[1, 2, 3, 4],访问顺序=[1, 2, 3, 4]
【步骤5】访问页面:1 | 物理块:[1, 2, 3, 4] | 访问顺序:[1, 2, 3, 4] → 命中(更新访问顺序)
【步骤6】访问页面:2 | 物理块:[1, 2, 3, 4] | 访问顺序:[2, 3, 4, 1] → 命中(更新访问顺序)
【步骤7】访问页面:5 | 物理块:[1, 2, 3, 4] | 访问顺序:[3, 4, 1, 2] → 缺页(置换),淘汰3,更新后:物理块=[1, 2, 4, 5],访问顺序=[4, 1, 2, 5]
【步骤8】访问页面:1 | 物理块:[1, 2, 4, 5] | 访问顺序:[4, 1, 2, 5] → 命中(更新访问顺序)
【步骤9】访问页面:2 | 物理块:[1, 2, 4, 5] | 访问顺序:[4, 2, 5, 1] → 命中(更新访问顺序)
【步骤10】访问页面:3 | 物理块:[1, 2, 4, 5] | 访问顺序:[4, 5, 1, 2] → 缺页(置换),淘汰4,更新后:物理块=[1, 2, 5, 3],访问顺序=[5, 1, 2, 3]
【步骤11】访问页面:4 | 物理块:[1, 2, 5, 3] | 访问顺序:[5, 1, 2, 3] → 缺页(置换),淘汰5,更新后:物理块=[1, 2, 3, 4],访问顺序=[1, 2, 3, 4]
【步骤12】访问页面:5 | 物理块:[1, 2, 3, 4] | 访问顺序:[1, 2, 3, 4] → 缺页(置换),淘汰1,更新后:物理块=[2, 3, 4, 5],访问顺序=[2, 3, 4, 5]
【统计】总访问次数:12 | 缺页次数:8 | 缺页率:66.67%

4. 时钟置换算法(CLOCK,最近未使用算法 NRU)

原理

对 LRU 的轻量级改进 ,降低实现开销,又称 **"第二次机会算法",核心是通过访问位 ** 标记页面的访问状态:

  1. 为每个页设置一个访问位(0/1),页面被访问时,访问位置为 1;
  2. 维护一个循环链表(时钟环),存放所有调入物理内存的页;
  3. 缺页时,从当前指针位置开始遍历循环链表:
    • 若页的访问位为 0,淘汰该页,将新页调入该位置,指针后移;
    • 若页的访问位为 1,将访问位重置为 0,指针后移,继续遍历;
  4. 遍历过程中,为所有访问位为 1 的页 "提供第二次机会",仅淘汰首次遇到的访问位为 0 的页。
扩展:改进型时钟置换算法(二次机会 / 增强型 CLOCK)

增加修改位 (0/1,标记页是否被修改),将页面分为 4 类,按优先级从高到低 淘汰:访问位0+修改位0访问位0+修改位1访问位1+修改位0访问位1+修改位1优先淘汰 "未访问且未修改" 的页(无需写回外存,开销最小),兼顾访问频率置换开销,是现代 OS 的主流实现。

特点
  • 优点:实现简单,系统开销低(仅需维护访问位 / 修改位和循环链表),缺页率接近 LRU,无 Belady 异常;
  • 缺点:性能略低于 LRU,极端情况下可能遍历整个时钟环。
适用场景:现代通用操作系统(Linux/Windows)的默认页面置换算法(如 Linux 的 CLOCK_V2、Windows 的改进 CLOCK),平衡性能和开销。
算法实现思路
基础版 CLOCK(仅访问位,核心:第二次机会)
  1. 数据结构 :用clock_ring列表模拟循环时钟环 ,每个元素是元组(页号, 访问位),访问位1表示近期被访问,0表示未被访问;用pointer整数记录当前遍历指针,初始为 0,通过取模实现循环。
  2. 核心规则
    • 页面命中 :找到对应页,将访问位置 1(标记为近期访问,获得第二次机会);
    • 缺页 + 空闲块:缺页次数 + 1,新页以(页号, 1)加入时钟环,指针位置不变;
    • 缺页 + 环满:从当前指针开始循环遍历 ,遇到访问位=1则置 0(重置机会)、指针后移;遇到访问位=0则直接淘汰该页,新页以(页号,1)替换该位置,指针后移(取模保证循环)。
  3. 循环实现 :指针后移公式pointer = (pointer + 1) % len(clock_ring),完美模拟循环链表的环形遍历。
改进版 CLOCK(访问位 + 修改位,现代 OS 主流)
  1. 数据结构升级 :时钟环元素为元组(页号, 访问位, 修改位),修改位1表示页面被修改(置换时需写回外存,开销大),0表示未修改(置换无写回开销)。
  2. 淘汰优先级(核心优化) :按置换开销从低到高 淘汰,优先级严格为:(0,0) 未访问+未修改(0,1) 未访问+已修改(1,0) 已访问+未修改(1,1) 已访问+已修改
  3. 遍历规则 :缺页置换时,按优先级从高到低遍历时钟环,找到第一类符合条件的页立即淘汰,无需遍历整个环,兼顾访问频率和置换开销,是 Linux/Windows 的实际实现方案。
Python代码
python 复制代码
def clock_page_replacement(page_sequence, frame_count, modify_seq=None):
    """
    时钟置换算法(基础版+改进版)
    基础版:仅访问位(第二次机会算法);改进版:访问位+修改位(传入modify_seq为修改序列)
    :param page_sequence: 页面访问序列,列表
    :param frame_count: 物理块数量,正整数
    :param modify_seq: 页面修改序列(与访问序列等长),0=未修改,1=修改;None则为基础版
    :return: 缺页次数, 缺页率(保留4位小数)
    """
    # 参数合法性校验(与前序算法完全一致,方便统一对比)
    if not isinstance(page_sequence, list) or len(page_sequence) == 0:
        raise ValueError("页面访问序列必须是非空列表")
    if not isinstance(frame_count, int) or frame_count < 1:
        raise ValueError("物理块数量必须是正整数")
    # 改进版校验:修改序列与访问序列等长,元素为0/1
    is_advanced = modify_seq is not None
    if is_advanced:
        if not isinstance(modify_seq, list) or len(modify_seq) != len(page_sequence):
            raise ValueError("修改序列必须是与访问序列等长的列表")
        if not all(x in (0, 1) for x in modify_seq):
            raise ValueError("修改序列元素必须为0(未修改)或1(已修改)")

    clock_ring = []  # 模拟时钟环,基础版:(page, access);改进版:(page, access, modify)
    pointer = 0      # 时钟环当前遍历指针,初始为0
    page_faults = 0  # 缺页次数计数器
    total_access = len(page_sequence)  # 总访问次数

    for idx, current_page in enumerate(page_sequence):
        # 改进版:获取当前页面的修改标记
        current_modify = modify_seq[idx] if is_advanced else 0
        # 打印当前状态:步骤、访问页、时钟环、指针位置(格式化状态,方便查看)
        ring_str = [f"{p[0]}(A{p[1]})" for p in clock_ring] if not is_advanced else [f"{p[0]}(A{p[1]}M{p[2]})" for p in clock_ring]
        print(f"【步骤{idx+1}】访问页:{current_page} | 时钟环:{ring_str} | 指针:{pointer}", end=" ")

        # 情况1:页面命中 → 访问位置1(基础版/改进版均生效)
        hit_idx = -1
        for i, page_info in enumerate(clock_ring):
            if page_info[0] == current_page:
                hit_idx = i
                break
        if hit_idx != -1:
            # 解包-更新访问位-重新赋值(保证元组不可变的特性)
            if not is_advanced:
                p, a = clock_ring[hit_idx]
                clock_ring[hit_idx] = (p, 1)
            else:
                p, a, m = clock_ring[hit_idx]
                clock_ring[hit_idx] = (p, 1, m)
            print(f"→ 命中(置访问位1)")
            continue

        # 情况2:页面缺页 → 缺页次数+1
        page_faults += 1
        # 子情况2.1:时钟环未满(物理块有空闲)→ 直接加入时钟环,访问位=1
        if len(clock_ring) < frame_count:
            if not is_advanced:
                clock_ring.append((current_page, 1))
            else:
                clock_ring.append((current_page, 1, current_modify))
            print(f"→ 缺页(空闲块),时钟环更新为:{[f"{p[0]}(A{p[1]})" for p in clock_ring] if not is_advanced else [f"{p[0]}(A{p[1]}M{p[2]})" for p in clock_ring]}")
            continue

        # 子情况2.2:时钟环已满 → 执行CLOCK置换(基础版/改进版不同逻辑)
        if not is_advanced:
            # 基础版CLOCK:循环遍历,找访问位0的页,访问位1则置0并指针后移
            while True:
                curr_p, curr_a = clock_ring[pointer]
                if curr_a == 0:
                    # 找到访问位0的页,淘汰并替换为新页(访问位1)
                    replace_page = curr_p
                    clock_ring[pointer] = (current_page, 1)
                    print(f"→ 缺页(置换),淘汰{replace_page},指针后移")
                    # 指针后移(取模实现循环)
                    pointer = (pointer + 1) % frame_count
                    break
                else:
                    # 访问位1,置0并指针后移(提供第二次机会)
                    clock_ring[pointer] = (curr_p, 0)
                    pointer = (pointer + 1) % frame_count
        else:
            # 改进版CLOCK:按优先级遍历,淘汰开销最小的页(0,0)→(0,1)→(1,0)→(1,1)
            priorities = [(0, 0), (0, 1), (1, 0), (1, 1)]  # 淘汰优先级从高到低
            replace_found = False
            for pri in priorities:
                if replace_found:
                    break
                # 遍历时钟环,从当前指针开始找符合优先级的页
                for _ in range(frame_count):
                    curr_p, curr_a, curr_m = clock_ring[pointer]
                    if (curr_a, curr_m) == pri:
                        # 找到目标页,淘汰并替换
                        replace_page = curr_p
                        clock_ring[pointer] = (current_page, 1, current_modify)
                        print(f"→ 缺页(置换),淘汰{replace_page}(A{curr_a}M{curr_m}),指针后移")
                        pointer = (pointer + 1) % frame_count
                        replace_found = True
                        break
                    else:
                        # 非目标优先级,访问位1则置0(仅重置,不淘汰)
                        if curr_a == 1:
                            clock_ring[pointer] = (curr_p, 0, curr_m)
                        pointer = (pointer + 1) % frame_count

    # 计算缺页率(保留4位小数,与前序算法一致)
    page_fault_rate = page_faults / total_access if total_access > 0 else 0.0
    return page_faults, round(page_fault_rate, 4)

# -------------------------- 测试用例 --------------------------
if __name__ == "__main__":
    # 通用测试序列:1.经典对比序列(与OPT/FIFO/LRU同序列) 2.Belady异常验证序列
    classic_sequence = [7, 0, 1, 2, 0, 3, 0, 4, 2, 3, 0, 3, 2, 1, 2, 0, 1, 7, 0, 1]
    belady_sequence = [1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5]
    # 改进版CLOCK:生成模拟修改序列(与访问序列等长,随机0/1,可自定义)
    import random
    random.seed(1)  # 固定随机种子,结果可复现
    classic_modify = [random.randint(0, 1) for _ in range(len(classic_sequence))]
    belady_modify = [random.randint(0, 1) for _ in range(len(belady_sequence))]

    # 测试1:基础版CLOCK - 经典序列(与OPT/FIFO/LRU横向对比,物理块3)
    print("="*120)
    print("【测试1:基础版CLOCK - 经典序列(物理块3,与前序算法对比)】")
    print("="*120)
    try:
        fault_num, fault_rate = clock_page_replacement(classic_sequence, 3)
    except ValueError as e:
        print(f"执行失败:{e}")
    else:
        print(f"\n【统计】总访问:{len(classic_sequence)} | 缺页次数:{fault_num} | 缺页率:{fault_rate:.2%}\n")

    # 测试2:基础版CLOCK - Belady异常验证(证明无该缺陷,物理块3/4)
    print("="*120)
    print("【测试2:基础版CLOCK - Belady异常验证(物理块3/4)】")
    print("="*120)
    for frame_num in [3, 4]:
        print(f"\n--- 物理块数量={frame_num} ---")
        try:
            fault_num, fault_rate = clock_page_replacement(belady_sequence, frame_num)
        except ValueError as e:
            print(f"执行失败:{e}")
        else:
            print(f"【统计】总访问:{len(belady_sequence)} | 缺页次数:{fault_num} | 缺页率:{fault_rate:.2%}")

    # 测试3:改进版CLOCK - 经典序列(物理块3,现代OS主流实现)
    print("\n" + "="*120)
    print("【测试3:改进版CLOCK - 经典序列(物理块3,访问位+修改位)】")
    print("="*120)
    try:
        fault_num, fault_rate = clock_page_replacement(classic_sequence, 3, classic_modify)
    except ValueError as e:
        print(f"执行失败:{e}")
    else:
        print(f"\n【统计】总访问:{len(classic_sequence)} | 缺页次数:{fault_num} | 缺页率:{fault_rate:.2%}")
程序运行结果展示
bash 复制代码
========================================================================================================================
【测试1:基础版CLOCK - 经典序列(物理块3,与前序算法对比)】
========================================================================================================================
【步骤1】访问页:7 | 时钟环:[] | 指针:0 → 缺页(空闲块),时钟环更新为:['7(A1)']
【步骤2】访问页:0 | 时钟环:['7(A1)'] | 指针:0 → 缺页(空闲块),时钟环更新为:['7(A1)', '0(A1)']
【步骤3】访问页:1 | 时钟环:['7(A1)', '0(A1)'] | 指针:0 → 缺页(空闲块),时钟环更新为:['7(A1)', '0(A1)', '1(A1)']
【步骤4】访问页:2 | 时钟环:['7(A1)', '0(A1)', '1(A1)'] | 指针:0 → 缺页(置换),淘汰7,指针后移
【步骤5】访问页:0 | 时钟环:['2(A1)', '0(A0)', '1(A0)'] | 指针:1 → 命中(置访问位1)
【步骤6】访问页:3 | 时钟环:['2(A1)', '0(A1)', '1(A0)'] | 指针:1 → 缺页(置换),淘汰1,指针后移
【步骤7】访问页:0 | 时钟环:['2(A1)', '0(A0)', '3(A1)'] | 指针:0 → 命中(置访问位1)
【步骤8】访问页:4 | 时钟环:['2(A1)', '0(A1)', '3(A1)'] | 指针:0 → 缺页(置换),淘汰2,指针后移
【步骤9】访问页:2 | 时钟环:['4(A1)', '0(A0)', '3(A0)'] | 指针:1 → 缺页(置换),淘汰0,指针后移
【步骤10】访问页:3 | 时钟环:['4(A1)', '2(A1)', '3(A0)'] | 指针:2 → 命中(置访问位1)
【步骤11】访问页:0 | 时钟环:['4(A1)', '2(A1)', '3(A1)'] | 指针:2 → 缺页(置换),淘汰3,指针后移
【步骤12】访问页:3 | 时钟环:['4(A0)', '2(A0)', '0(A1)'] | 指针:0 → 缺页(置换),淘汰4,指针后移
【步骤13】访问页:2 | 时钟环:['3(A1)', '2(A0)', '0(A1)'] | 指针:1 → 命中(置访问位1)
【步骤14】访问页:1 | 时钟环:['3(A1)', '2(A1)', '0(A1)'] | 指针:1 → 缺页(置换),淘汰2,指针后移
【步骤15】访问页:2 | 时钟环:['3(A0)', '1(A1)', '0(A0)'] | 指针:2 → 缺页(置换),淘汰0,指针后移
【步骤16】访问页:0 | 时钟环:['3(A0)', '1(A1)', '2(A1)'] | 指针:0 → 缺页(置换),淘汰3,指针后移
【步骤17】访问页:1 | 时钟环:['0(A1)', '1(A1)', '2(A1)'] | 指针:1 → 命中(置访问位1)
【步骤18】访问页:7 | 时钟环:['0(A1)', '1(A1)', '2(A1)'] | 指针:1 → 缺页(置换),淘汰1,指针后移
【步骤19】访问页:0 | 时钟环:['0(A0)', '7(A1)', '2(A0)'] | 指针:2 → 命中(置访问位1)
【步骤20】访问页:1 | 时钟环:['0(A1)', '7(A1)', '2(A0)'] | 指针:2 → 缺页(置换),淘汰2,指针后移

【统计】总访问:20 | 缺页次数:14 | 缺页率:70.00%

========================================================================================================================
【测试2:基础版CLOCK - Belady异常验证(物理块3/4)】
========================================================================================================================

--- 物理块数量=3 ---
【步骤1】访问页:1 | 时钟环:[] | 指针:0 → 缺页(空闲块),时钟环更新为:['1(A1)']
【步骤2】访问页:2 | 时钟环:['1(A1)'] | 指针:0 → 缺页(空闲块),时钟环更新为:['1(A1)', '2(A1)']
【步骤3】访问页:3 | 时钟环:['1(A1)', '2(A1)'] | 指针:0 → 缺页(空闲块),时钟环更新为:['1(A1)', '2(A1)', '3(A1)']
【步骤4】访问页:4 | 时钟环:['1(A1)', '2(A1)', '3(A1)'] | 指针:0 → 缺页(置换),淘汰1,指针后移
【步骤5】访问页:1 | 时钟环:['4(A1)', '2(A0)', '3(A0)'] | 指针:1 → 缺页(置换),淘汰2,指针后移
【步骤6】访问页:2 | 时钟环:['4(A1)', '1(A1)', '3(A0)'] | 指针:2 → 缺页(置换),淘汰3,指针后移
【步骤7】访问页:5 | 时钟环:['4(A1)', '1(A1)', '2(A1)'] | 指针:0 → 缺页(置换),淘汰4,指针后移
【步骤8】访问页:1 | 时钟环:['5(A1)', '1(A0)', '2(A0)'] | 指针:1 → 命中(置访问位1)
【步骤9】访问页:2 | 时钟环:['5(A1)', '1(A1)', '2(A0)'] | 指针:1 → 命中(置访问位1)
【步骤10】访问页:3 | 时钟环:['5(A1)', '1(A1)', '2(A1)'] | 指针:1 → 缺页(置换),淘汰1,指针后移
【步骤11】访问页:4 | 时钟环:['5(A0)', '3(A1)', '2(A0)'] | 指针:2 → 缺页(置换),淘汰2,指针后移
【步骤12】访问页:5 | 时钟环:['5(A0)', '3(A1)', '4(A1)'] | 指针:0 → 命中(置访问位1)
【统计】总访问:12 | 缺页次数:9 | 缺页率:75.00%

--- 物理块数量=4 ---
【步骤1】访问页:1 | 时钟环:[] | 指针:0 → 缺页(空闲块),时钟环更新为:['1(A1)']
【步骤2】访问页:2 | 时钟环:['1(A1)'] | 指针:0 → 缺页(空闲块),时钟环更新为:['1(A1)', '2(A1)']
【步骤3】访问页:3 | 时钟环:['1(A1)', '2(A1)'] | 指针:0 → 缺页(空闲块),时钟环更新为:['1(A1)', '2(A1)', '3(A1)']
【步骤4】访问页:4 | 时钟环:['1(A1)', '2(A1)', '3(A1)'] | 指针:0 → 缺页(空闲块),时钟环更新为:['1(A1)', '2(A1)', '3(A1)', '4(A1)']
【步骤5】访问页:1 | 时钟环:['1(A1)', '2(A1)', '3(A1)', '4(A1)'] | 指针:0 → 命中(置访问位1)
【步骤6】访问页:2 | 时钟环:['1(A1)', '2(A1)', '3(A1)', '4(A1)'] | 指针:0 → 命中(置访问位1)
【步骤7】访问页:5 | 时钟环:['1(A1)', '2(A1)', '3(A1)', '4(A1)'] | 指针:0 → 缺页(置换),淘汰1,指针后移
【步骤8】访问页:1 | 时钟环:['5(A1)', '2(A0)', '3(A0)', '4(A0)'] | 指针:1 → 缺页(置换),淘汰2,指针后移
【步骤9】访问页:2 | 时钟环:['5(A1)', '1(A1)', '3(A0)', '4(A0)'] | 指针:2 → 缺页(置换),淘汰3,指针后移
【步骤10】访问页:3 | 时钟环:['5(A1)', '1(A1)', '2(A1)', '4(A0)'] | 指针:3 → 缺页(置换),淘汰4,指针后移
【步骤11】访问页:4 | 时钟环:['5(A1)', '1(A1)', '2(A1)', '3(A1)'] | 指针:0 → 缺页(置换),淘汰5,指针后移
【步骤12】访问页:5 | 时钟环:['4(A1)', '1(A0)', '2(A0)', '3(A0)'] | 指针:1 → 缺页(置换),淘汰1,指针后移
【统计】总访问:12 | 缺页次数:10 | 缺页率:83.33%

========================================================================================================================
【测试3:改进版CLOCK - 经典序列(物理块3,访问位+修改位)】
========================================================================================================================
【步骤1】访问页:7 | 时钟环:[] | 指针:0 → 缺页(空闲块),时钟环更新为:['7(A1M0)']
【步骤2】访问页:0 | 时钟环:['7(A1M0)'] | 指针:0 → 缺页(空闲块),时钟环更新为:['7(A1M0)', '0(A1M0)']
【步骤3】访问页:1 | 时钟环:['7(A1M0)', '0(A1M0)'] | 指针:0 → 缺页(空闲块),时钟环更新为:['7(A1M0)', '0(A1M0)', '1(A1M1)']
【步骤4】访问页:2 | 时钟环:['7(A1M0)', '0(A1M0)', '1(A1M1)'] | 指针:0 → 缺页(置换),淘汰1(A0M1),指针后移
【步骤5】访问页:0 | 时钟环:['7(A0M0)', '0(A0M0)', '2(A1M0)'] | 指针:0 → 命中(置访问位1)
【步骤6】访问页:3 | 时钟环:['7(A0M0)', '0(A1M0)', '2(A1M0)'] | 指针:0 → 缺页(置换),淘汰7(A0M0),指针后移
【步骤7】访问页:0 | 时钟环:['3(A1M1)', '0(A1M0)', '2(A1M0)'] | 指针:1 → 命中(置访问位1)
【步骤8】访问页:4 | 时钟环:['3(A1M1)', '0(A1M0)', '2(A1M0)'] | 指针:1 → 缺页(置换),淘汰3(A0M1),指针后移
【步骤9】访问页:2 | 时钟环:['4(A1M1)', '0(A0M0)', '2(A0M0)'] | 指针:1 → 命中(置访问位1)
【步骤10】访问页:3 | 时钟环:['4(A1M1)', '0(A0M0)', '2(A1M0)'] | 指针:1 → 缺页(置换),淘汰0(A0M0),指针后移
【步骤11】访问页:0 | 时钟环:['4(A1M1)', '3(A1M0)', '2(A1M0)'] | 指针:2 → 缺页(置换),淘汰4(A0M1),指针后移
【步骤12】访问页:3 | 时钟环:['0(A1M1)', '3(A0M0)', '2(A0M0)'] | 指针:1 → 命中(置访问位1)
【步骤13】访问页:2 | 时钟环:['0(A1M1)', '3(A1M0)', '2(A0M0)'] | 指针:1 → 命中(置访问位1)
【步骤14】访问页:1 | 时钟环:['0(A1M1)', '3(A1M0)', '2(A1M0)'] | 指针:1 → 缺页(置换),淘汰0(A0M1),指针后移
【步骤15】访问页:2 | 时钟环:['1(A1M1)', '3(A0M0)', '2(A0M0)'] | 指针:1 → 命中(置访问位1)
【步骤16】访问页:0 | 时钟环:['1(A1M1)', '3(A0M0)', '2(A1M0)'] | 指针:1 → 缺页(置换),淘汰3(A0M0),指针后移
【步骤17】访问页:1 | 时钟环:['1(A1M1)', '0(A1M1)', '2(A1M0)'] | 指针:2 → 命中(置访问位1)
【步骤18】访问页:7 | 时钟环:['1(A1M1)', '0(A1M1)', '2(A1M0)'] | 指针:2 → 缺页(置换),淘汰1(A0M1),指针后移
【步骤19】访问页:0 | 时钟环:['7(A1M0)', '0(A0M1)', '2(A0M0)'] | 指针:1 → 命中(置访问位1)
【步骤20】访问页:1 | 时钟环:['7(A1M0)', '0(A1M1)', '2(A0M0)'] | 指针:1 → 缺页(置换),淘汰2(A0M0),指针后移

【统计】总访问:20 | 缺页次数:12 | 缺页率:60.00%

5. 最不常用置换算法(Least Frequently Used, LFU)

原理

淘汰被访问次数最少 的页,为每个页设置访问计数器,页面被访问时计数器 + 1,缺页时淘汰计数器值最小的页。

特点
  • 优点:考虑页面的访问频率,适合进程有固定核心访问页的场景;
  • 缺点:需要维护访问计数器,系统开销大;无法处理突发访问(某页被一次性频繁访问后不再使用,计数器值高,无法被淘汰)。
适用场景:进程页面访问频率相对稳定的场景,如数据库系统。
算法实现思路

LFU 的核心是基于访问频率淘汰 ,为每个页面维护访问计数器,同时处理计数相同的情况,选用三结构联动实现(新手易理解,贴合经典原理):

  1. frames:模拟物理块,存储当前驻留的页面;
  2. freq_count:字典,记录每个页面的访问次数计数器(键 = 页号,值 = 访问次数,页面被访问则 + 1);
  3. add_order:列表,记录页面调入物理块的先后顺序 ,用于访问计数相同时,淘汰最早调入的页面(解决计数冲突的经典方案)。

核心逻辑分 3 种情况,和前序算法格式完全对齐,无复杂计算:

  • 命中:页面在物理块中,对应计数器 **+1**(仅更新频率,物理块 / 调入顺序不变);
  • 缺页 + 空闲块 :缺页次数 + 1,页面加入物理块,计数器初始化为1,同时记录到调入顺序末尾;
  • 缺页 + 物理块满 :先找到freq_count计数值最小 的页面集合;若集合仅 1 个页,直接淘汰;若多个页(计数相同),淘汰其中最早调入 的页(从add_order中找最先出现的),最后将新页加入物理块、初始化计数器、记录调入顺序。
Python代码
python 复制代码
def lfu_page_replacement(page_sequence, frame_count):
    """
    最不常用置换算法(LFU)实现
    处理计数冲突:访问次数相同时,淘汰最早调入物理块的页面
    :param page_sequence: 页面访问序列,列表类型
    :param frame_count: 物理块数量,正整数
    :return: 缺页次数, 缺页率(保留4位小数)
    """
    # 参数合法性校验(与前序5种算法完全一致,方便统一横向对比)
    if not isinstance(page_sequence, list) or len(page_sequence) == 0:
        raise ValueError("页面访问序列必须是非空列表")
    if not isinstance(frame_count, int) or frame_count < 1:
        raise ValueError("物理块数量必须是正整数")

    frames = []  # 模拟物理块,存储当前驻留的页面
    freq_count = {}  # 访问频率计数器:key=页号,value=访问次数
    add_order = []  # 页面调入顺序,解决计数相同时的淘汰问题(淘汰最早的)
    page_faults = 0  # 缺页次数计数器
    total_access = len(page_sequence)  # 总页面访问次数

    # 遍历每一个访问的页面,记录步骤索引
    for idx, current_page in enumerate(page_sequence):
        # 格式化打印当前状态:步骤、访问页、物理块、访问计数、调入顺序
        print(f"【步骤{idx + 1}】访问页:{current_page} | 物理块:{frames} | 访问计数:{freq_count} | 调入顺序:{add_order}",
              end=" ")
        # 情况1:页面命中 → 访问计数器+1
        if current_page in frames:
            freq_count[current_page] += 1
            print(f"→ 命中(计数+1,更新后:{freq_count})")
            continue

        # 情况2:页面缺页 → 缺页次数+1
        page_faults += 1
        # 子情况2.1:物理块有空闲 → 加入新页,计数器初始化为1,记录调入顺序
        if len(frames) < frame_count:
            frames.append(current_page)
            freq_count[current_page] = 1
            add_order.append(current_page)
            print(f"→ 缺页(空闲块),更新后:物理块={frames},计数={freq_count},调入顺序={add_order}")
        # 子情况2.2:物理块已满 → 执行LFU置换(淘汰访问次数最少的页,计数相同淘汰最早调入的)
        else:
            # 步骤1:找到当前物理块中访问次数的最小值
            min_freq = min([freq_count[p] for p in frames])
            # 步骤2:筛选出所有计数=最小值的页面(候选淘汰集)
            min_freq_pages = [p for p in frames if freq_count[p] == min_freq]
            # 步骤3:候选集仅1个则直接淘汰,多个则淘汰最早调入的(从add_order找最先出现的)
            if len(min_freq_pages) == 1:
                replace_page = min_freq_pages[0]
            else:
                # 遍历调入顺序,第一个出现在候选集中的页就是最早调入的
                for p in add_order:
                    if p in min_freq_pages:
                        replace_page = p
                        break
            # 步骤4:执行淘汰 - 物理块/计数/调入顺序同步删除
            frames.remove(replace_page)
            del freq_count[replace_page]
            add_order.remove(replace_page)
            # 步骤5:加入新页 - 初始化计数,记录调入顺序
            frames.append(current_page)
            freq_count[current_page] = 1
            add_order.append(current_page)
            print(f"→ 缺页(置换),淘汰{replace_page}(计数{min_freq}),更新后:物理块={frames},计数={freq_count}")

    # 计算缺页率(保留4位小数,与所有前序算法一致)
    page_fault_rate = page_faults / total_access if total_access > 0 else 0.0
    return page_faults, round(page_fault_rate, 4)


# -------------------------- 测试用例 --------------------------
if __name__ == "__main__":
    # 测试用例1:经典教材序列(与OPT/FIFO/LRU/CLOCK同序列,物理块3,横向对比)
    classic_sequence = [7, 0, 1, 2, 0, 3, 0, 4, 2, 3, 0, 3, 2, 1, 2, 0, 1, 7, 0, 1]
    # 测试用例2:Belady异常验证序列(证明LFU无该缺陷,物理块3/4)
    belady_sequence = [1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5]
    # 测试用例3:LFU核心缺点测试 - 突发访问序列(某页一次性频繁访问后不再使用,无法被淘汰)
    burst_sequence = [1, 1, 1, 2, 2, 3, 3, 3, 4, 4, 5, 5, 6]  # 1和3突发访问,后续无访问

    # 测试1:经典序列(物理块3,与前序算法横向对比)
    print("=" * 130)
    print("【测试1:LFU - 经典序列(物理块3,与OPT/FIFO/LRU/CLOCK对比)】")
    print("=" * 130)
    try:
        fault_num, fault_rate = lfu_page_replacement(classic_sequence, 3)
    except ValueError as e:
        print(f"执行失败:{e}")
    else:
        print(f"\n【统计】总访问:{len(classic_sequence)} | 缺页次数:{fault_num} | 缺页率:{fault_rate:.2%}\n")

    # 测试2:Belady异常验证(物理块3/4,证明LFU无该缺陷)
    print("=" * 130)
    print("【测试2:LFU - Belady异常验证(物理块3/4)】")
    print("=" * 130)
    for frame_num in [3, 4]:
        print(f"\n--- 物理块数量={frame_num} ---")
        try:
            fault_num, fault_rate = lfu_page_replacement(belady_sequence, frame_num)
        except ValueError as e:
            print(f"执行失败:{e}")
        else:
            print(f"【统计】总访问:{len(belady_sequence)} | 缺页次数:{fault_num} | 缺页率:{fault_rate:.2%}")

    # 测试3:LFU核心缺点 - 突发访问序列(物理块3,体现"突发访问页无法被淘汰"的问题)
    print("\n" + "=" * 130)
    print("【测试3:LFU - 突发访问序列(物理块3,体现核心缺点)】")
    print("=" * 130)
    try:
        fault_num, fault_rate = lfu_page_replacement(burst_sequence, 3)
    except ValueError as e:
        print(f"执行失败:{e}")
    else:
        print(f"\n【统计】总访问:{len(burst_sequence)} | 缺页次数:{fault_num} | 缺页率:{fault_rate:.2%}")
程序运行结果展示
bash 复制代码
==================================================================================================================================
【测试1:LFU - 经典序列(物理块3,与OPT/FIFO/LRU/CLOCK对比)】
==================================================================================================================================
【步骤1】访问页:7 | 物理块:[] | 访问计数:{} | 调入顺序:[] → 缺页(空闲块),更新后:物理块=[7],计数={7: 1},调入顺序=[7]
【步骤2】访问页:0 | 物理块:[7] | 访问计数:{7: 1} | 调入顺序:[7] → 缺页(空闲块),更新后:物理块=[7, 0],计数={7: 1, 0: 1},调入顺序=[7, 0]
【步骤3】访问页:1 | 物理块:[7, 0] | 访问计数:{7: 1, 0: 1} | 调入顺序:[7, 0] → 缺页(空闲块),更新后:物理块=[7, 0, 1],计数={7: 1, 0: 1, 1: 1},调入顺序=[7, 0, 1]
【步骤4】访问页:2 | 物理块:[7, 0, 1] | 访问计数:{7: 1, 0: 1, 1: 1} | 调入顺序:[7, 0, 1] → 缺页(置换),淘汰7(计数1),更新后:物理块=[0, 1, 2],计数={0: 1, 1: 1, 2: 1}
【步骤5】访问页:0 | 物理块:[0, 1, 2] | 访问计数:{0: 1, 1: 1, 2: 1} | 调入顺序:[0, 1, 2] → 命中(计数+1,更新后:{0: 2, 1: 1, 2: 1})
【步骤6】访问页:3 | 物理块:[0, 1, 2] | 访问计数:{0: 2, 1: 1, 2: 1} | 调入顺序:[0, 1, 2] → 缺页(置换),淘汰1(计数1),更新后:物理块=[0, 2, 3],计数={0: 2, 2: 1, 3: 1}
【步骤7】访问页:0 | 物理块:[0, 2, 3] | 访问计数:{0: 2, 2: 1, 3: 1} | 调入顺序:[0, 2, 3] → 命中(计数+1,更新后:{0: 3, 2: 1, 3: 1})
【步骤8】访问页:4 | 物理块:[0, 2, 3] | 访问计数:{0: 3, 2: 1, 3: 1} | 调入顺序:[0, 2, 3] → 缺页(置换),淘汰2(计数1),更新后:物理块=[0, 3, 4],计数={0: 3, 3: 1, 4: 1}
【步骤9】访问页:2 | 物理块:[0, 3, 4] | 访问计数:{0: 3, 3: 1, 4: 1} | 调入顺序:[0, 3, 4] → 缺页(置换),淘汰3(计数1),更新后:物理块=[0, 4, 2],计数={0: 3, 4: 1, 2: 1}
【步骤10】访问页:3 | 物理块:[0, 4, 2] | 访问计数:{0: 3, 4: 1, 2: 1} | 调入顺序:[0, 4, 2] → 缺页(置换),淘汰4(计数1),更新后:物理块=[0, 2, 3],计数={0: 3, 2: 1, 3: 1}
【步骤11】访问页:0 | 物理块:[0, 2, 3] | 访问计数:{0: 3, 2: 1, 3: 1} | 调入顺序:[0, 2, 3] → 命中(计数+1,更新后:{0: 4, 2: 1, 3: 1})
【步骤12】访问页:3 | 物理块:[0, 2, 3] | 访问计数:{0: 4, 2: 1, 3: 1} | 调入顺序:[0, 2, 3] → 命中(计数+1,更新后:{0: 4, 2: 1, 3: 2})
【步骤13】访问页:2 | 物理块:[0, 2, 3] | 访问计数:{0: 4, 2: 1, 3: 2} | 调入顺序:[0, 2, 3] → 命中(计数+1,更新后:{0: 4, 2: 2, 3: 2})
【步骤14】访问页:1 | 物理块:[0, 2, 3] | 访问计数:{0: 4, 2: 2, 3: 2} | 调入顺序:[0, 2, 3] → 缺页(置换),淘汰2(计数2),更新后:物理块=[0, 3, 1],计数={0: 4, 3: 2, 1: 1}
【步骤15】访问页:2 | 物理块:[0, 3, 1] | 访问计数:{0: 4, 3: 2, 1: 1} | 调入顺序:[0, 3, 1] → 缺页(置换),淘汰1(计数1),更新后:物理块=[0, 3, 2],计数={0: 4, 3: 2, 2: 1}
【步骤16】访问页:0 | 物理块:[0, 3, 2] | 访问计数:{0: 4, 3: 2, 2: 1} | 调入顺序:[0, 3, 2] → 命中(计数+1,更新后:{0: 5, 3: 2, 2: 1})
【步骤17】访问页:1 | 物理块:[0, 3, 2] | 访问计数:{0: 5, 3: 2, 2: 1} | 调入顺序:[0, 3, 2] → 缺页(置换),淘汰2(计数1),更新后:物理块=[0, 3, 1],计数={0: 5, 3: 2, 1: 1}
【步骤18】访问页:7 | 物理块:[0, 3, 1] | 访问计数:{0: 5, 3: 2, 1: 1} | 调入顺序:[0, 3, 1] → 缺页(置换),淘汰1(计数1),更新后:物理块=[0, 3, 7],计数={0: 5, 3: 2, 7: 1}
【步骤19】访问页:0 | 物理块:[0, 3, 7] | 访问计数:{0: 5, 3: 2, 7: 1} | 调入顺序:[0, 3, 7] → 命中(计数+1,更新后:{0: 6, 3: 2, 7: 1})
【步骤20】访问页:1 | 物理块:[0, 3, 7] | 访问计数:{0: 6, 3: 2, 7: 1} | 调入顺序:[0, 3, 7] → 缺页(置换),淘汰7(计数1),更新后:物理块=[0, 3, 1],计数={0: 6, 3: 2, 1: 1}

【统计】总访问:20 | 缺页次数:13 | 缺页率:65.00%

==================================================================================================================================
【测试2:LFU - Belady异常验证(物理块3/4)】
==================================================================================================================================

--- 物理块数量=3 ---
【步骤1】访问页:1 | 物理块:[] | 访问计数:{} | 调入顺序:[] → 缺页(空闲块),更新后:物理块=[1],计数={1: 1},调入顺序=[1]
【步骤2】访问页:2 | 物理块:[1] | 访问计数:{1: 1} | 调入顺序:[1] → 缺页(空闲块),更新后:物理块=[1, 2],计数={1: 1, 2: 1},调入顺序=[1, 2]
【步骤3】访问页:3 | 物理块:[1, 2] | 访问计数:{1: 1, 2: 1} | 调入顺序:[1, 2] → 缺页(空闲块),更新后:物理块=[1, 2, 3],计数={1: 1, 2: 1, 3: 1},调入顺序=[1, 2, 3]
【步骤4】访问页:4 | 物理块:[1, 2, 3] | 访问计数:{1: 1, 2: 1, 3: 1} | 调入顺序:[1, 2, 3] → 缺页(置换),淘汰1(计数1),更新后:物理块=[2, 3, 4],计数={2: 1, 3: 1, 4: 1}
【步骤5】访问页:1 | 物理块:[2, 3, 4] | 访问计数:{2: 1, 3: 1, 4: 1} | 调入顺序:[2, 3, 4] → 缺页(置换),淘汰2(计数1),更新后:物理块=[3, 4, 1],计数={3: 1, 4: 1, 1: 1}
【步骤6】访问页:2 | 物理块:[3, 4, 1] | 访问计数:{3: 1, 4: 1, 1: 1} | 调入顺序:[3, 4, 1] → 缺页(置换),淘汰3(计数1),更新后:物理块=[4, 1, 2],计数={4: 1, 1: 1, 2: 1}
【步骤7】访问页:5 | 物理块:[4, 1, 2] | 访问计数:{4: 1, 1: 1, 2: 1} | 调入顺序:[4, 1, 2] → 缺页(置换),淘汰4(计数1),更新后:物理块=[1, 2, 5],计数={1: 1, 2: 1, 5: 1}
【步骤8】访问页:1 | 物理块:[1, 2, 5] | 访问计数:{1: 1, 2: 1, 5: 1} | 调入顺序:[1, 2, 5] → 命中(计数+1,更新后:{1: 2, 2: 1, 5: 1})
【步骤9】访问页:2 | 物理块:[1, 2, 5] | 访问计数:{1: 2, 2: 1, 5: 1} | 调入顺序:[1, 2, 5] → 命中(计数+1,更新后:{1: 2, 2: 2, 5: 1})
【步骤10】访问页:3 | 物理块:[1, 2, 5] | 访问计数:{1: 2, 2: 2, 5: 1} | 调入顺序:[1, 2, 5] → 缺页(置换),淘汰5(计数1),更新后:物理块=[1, 2, 3],计数={1: 2, 2: 2, 3: 1}
【步骤11】访问页:4 | 物理块:[1, 2, 3] | 访问计数:{1: 2, 2: 2, 3: 1} | 调入顺序:[1, 2, 3] → 缺页(置换),淘汰3(计数1),更新后:物理块=[1, 2, 4],计数={1: 2, 2: 2, 4: 1}
【步骤12】访问页:5 | 物理块:[1, 2, 4] | 访问计数:{1: 2, 2: 2, 4: 1} | 调入顺序:[1, 2, 4] → 缺页(置换),淘汰4(计数1),更新后:物理块=[1, 2, 5],计数={1: 2, 2: 2, 5: 1}
【统计】总访问:12 | 缺页次数:10 | 缺页率:83.33%

--- 物理块数量=4 ---
【步骤1】访问页:1 | 物理块:[] | 访问计数:{} | 调入顺序:[] → 缺页(空闲块),更新后:物理块=[1],计数={1: 1},调入顺序=[1]
【步骤2】访问页:2 | 物理块:[1] | 访问计数:{1: 1} | 调入顺序:[1] → 缺页(空闲块),更新后:物理块=[1, 2],计数={1: 1, 2: 1},调入顺序=[1, 2]
【步骤3】访问页:3 | 物理块:[1, 2] | 访问计数:{1: 1, 2: 1} | 调入顺序:[1, 2] → 缺页(空闲块),更新后:物理块=[1, 2, 3],计数={1: 1, 2: 1, 3: 1},调入顺序=[1, 2, 3]
【步骤4】访问页:4 | 物理块:[1, 2, 3] | 访问计数:{1: 1, 2: 1, 3: 1} | 调入顺序:[1, 2, 3] → 缺页(空闲块),更新后:物理块=[1, 2, 3, 4],计数={1: 1, 2: 1, 3: 1, 4: 1},调入顺序=[1, 2, 3, 4]
【步骤5】访问页:1 | 物理块:[1, 2, 3, 4] | 访问计数:{1: 1, 2: 1, 3: 1, 4: 1} | 调入顺序:[1, 2, 3, 4] → 命中(计数+1,更新后:{1: 2, 2: 1, 3: 1, 4: 1})
【步骤6】访问页:2 | 物理块:[1, 2, 3, 4] | 访问计数:{1: 2, 2: 1, 3: 1, 4: 1} | 调入顺序:[1, 2, 3, 4] → 命中(计数+1,更新后:{1: 2, 2: 2, 3: 1, 4: 1})
【步骤7】访问页:5 | 物理块:[1, 2, 3, 4] | 访问计数:{1: 2, 2: 2, 3: 1, 4: 1} | 调入顺序:[1, 2, 3, 4] → 缺页(置换),淘汰3(计数1),更新后:物理块=[1, 2, 4, 5],计数={1: 2, 2: 2, 4: 1, 5: 1}
【步骤8】访问页:1 | 物理块:[1, 2, 4, 5] | 访问计数:{1: 2, 2: 2, 4: 1, 5: 1} | 调入顺序:[1, 2, 4, 5] → 命中(计数+1,更新后:{1: 3, 2: 2, 4: 1, 5: 1})
【步骤9】访问页:2 | 物理块:[1, 2, 4, 5] | 访问计数:{1: 3, 2: 2, 4: 1, 5: 1} | 调入顺序:[1, 2, 4, 5] → 命中(计数+1,更新后:{1: 3, 2: 3, 4: 1, 5: 1})
【步骤10】访问页:3 | 物理块:[1, 2, 4, 5] | 访问计数:{1: 3, 2: 3, 4: 1, 5: 1} | 调入顺序:[1, 2, 4, 5] → 缺页(置换),淘汰4(计数1),更新后:物理块=[1, 2, 5, 3],计数={1: 3, 2: 3, 5: 1, 3: 1}
【步骤11】访问页:4 | 物理块:[1, 2, 5, 3] | 访问计数:{1: 3, 2: 3, 5: 1, 3: 1} | 调入顺序:[1, 2, 5, 3] → 缺页(置换),淘汰5(计数1),更新后:物理块=[1, 2, 3, 4],计数={1: 3, 2: 3, 3: 1, 4: 1}
【步骤12】访问页:5 | 物理块:[1, 2, 3, 4] | 访问计数:{1: 3, 2: 3, 3: 1, 4: 1} | 调入顺序:[1, 2, 3, 4] → 缺页(置换),淘汰3(计数1),更新后:物理块=[1, 2, 4, 5],计数={1: 3, 2: 3, 4: 1, 5: 1}
【统计】总访问:12 | 缺页次数:8 | 缺页率:66.67%

==================================================================================================================================
【测试3:LFU - 突发访问序列(物理块3,体现核心缺点)】
==================================================================================================================================
【步骤1】访问页:1 | 物理块:[] | 访问计数:{} | 调入顺序:[] → 缺页(空闲块),更新后:物理块=[1],计数={1: 1},调入顺序=[1]
【步骤2】访问页:1 | 物理块:[1] | 访问计数:{1: 1} | 调入顺序:[1] → 命中(计数+1,更新后:{1: 2})
【步骤3】访问页:1 | 物理块:[1] | 访问计数:{1: 2} | 调入顺序:[1] → 命中(计数+1,更新后:{1: 3})
【步骤4】访问页:2 | 物理块:[1] | 访问计数:{1: 3} | 调入顺序:[1] → 缺页(空闲块),更新后:物理块=[1, 2],计数={1: 3, 2: 1},调入顺序=[1, 2]
【步骤5】访问页:2 | 物理块:[1, 2] | 访问计数:{1: 3, 2: 1} | 调入顺序:[1, 2] → 命中(计数+1,更新后:{1: 3, 2: 2})
【步骤6】访问页:3 | 物理块:[1, 2] | 访问计数:{1: 3, 2: 2} | 调入顺序:[1, 2] → 缺页(空闲块),更新后:物理块=[1, 2, 3],计数={1: 3, 2: 2, 3: 1},调入顺序=[1, 2, 3]
【步骤7】访问页:3 | 物理块:[1, 2, 3] | 访问计数:{1: 3, 2: 2, 3: 1} | 调入顺序:[1, 2, 3] → 命中(计数+1,更新后:{1: 3, 2: 2, 3: 2})
【步骤8】访问页:3 | 物理块:[1, 2, 3] | 访问计数:{1: 3, 2: 2, 3: 2} | 调入顺序:[1, 2, 3] → 命中(计数+1,更新后:{1: 3, 2: 2, 3: 3})
【步骤9】访问页:4 | 物理块:[1, 2, 3] | 访问计数:{1: 3, 2: 2, 3: 3} | 调入顺序:[1, 2, 3] → 缺页(置换),淘汰2(计数2),更新后:物理块=[1, 3, 4],计数={1: 3, 3: 3, 4: 1}
【步骤10】访问页:4 | 物理块:[1, 3, 4] | 访问计数:{1: 3, 3: 3, 4: 1} | 调入顺序:[1, 3, 4] → 命中(计数+1,更新后:{1: 3, 3: 3, 4: 2})
【步骤11】访问页:5 | 物理块:[1, 3, 4] | 访问计数:{1: 3, 3: 3, 4: 2} | 调入顺序:[1, 3, 4] → 缺页(置换),淘汰4(计数2),更新后:物理块=[1, 3, 5],计数={1: 3, 3: 3, 5: 1}
【步骤12】访问页:5 | 物理块:[1, 3, 5] | 访问计数:{1: 3, 3: 3, 5: 1} | 调入顺序:[1, 3, 5] → 命中(计数+1,更新后:{1: 3, 3: 3, 5: 2})
【步骤13】访问页:6 | 物理块:[1, 3, 5] | 访问计数:{1: 3, 3: 3, 5: 2} | 调入顺序:[1, 3, 5] → 缺页(置换),淘汰5(计数2),更新后:物理块=[1, 3, 6],计数={1: 3, 3: 3, 6: 1}

【统计】总访问:13 | 缺页次数:6 | 缺页率:46.15%

经典页面置换算法性能对比

算法 缺页率 实现难度 系统开销 Belady 异常 实际 OS 应用
最优置换(OPT) 最低 不可实现 性能基准
先进先出(FIFO) 较高 极易 极小 简单系统
最近最少使用(LRU) 次低 中等 中等 Linux/Windows 基础
时钟(CLOCK) 中等 极小 Linux/Windows 主流
最不常用(LFU) 中等 较难 较高 数据库 / 专用系统

五、内存碎片整理与回收算法

内存管理的核心问题之一是碎片 ,碎片分为内部碎片 (分区 / 页内未利用的空间)和外部碎片 (分区 / 段间的未利用空间),碎片整理与回收算法的目标是减少碎片、提高内存利用率

1. 内存回收算法

进程结束 / 换出时,操作系统需要回收 其占用的内存空间,并将其加入空闲分区表 / 链,核心是合并相邻的空闲分区 (又称拼接 / 紧凑),避免产生大量分散的小空闲分区。

核心步骤
  1. 标记进程占用的内存空间为空闲
  2. 检查该空闲分区的前 / 后相邻分区 是否为空闲,若为空闲则合并为一个大的空闲分区
  3. 更新空闲分区表 / 链,记录合并后的空闲分区的起始地址和大小。
实现方式
  • 空闲分区表:直接修改表项的起始地址和大小,删除被合并的表项;
  • 空闲分区链:修改链表节点的指针,合并相邻节点。
算法实现思路

内存回收的核心是 **"标记空闲 + 相邻合并 + 表更新",基于空闲分区表(按起始地址升序)** 实现,核心设计如下:

  1. 数据结构 :空闲分区表用列表存储字典 实现,每个字典代表一个空闲分区,含start(起始地址)、size(分区大小)两个核心字段,始终按start升序排列(方便查找相邻分区);
  2. 相邻分区判断规则 (核心):
    • 前邻空闲分区:存在分区满足 前分区.start + 前分区.size == 回收分区.start
    • 后邻空闲分区:存在分区满足 回收分区.start + 回收分区.size == 后分区.start
  3. 四合一合并逻辑
    • 无相邻:直接将回收分区作为新表项加入空闲分区表;
    • 仅前邻:合并前邻分区与回收分区(新 start = 前邻 start,新 size = 前邻 size + 回收 size),删除原前邻表项;
    • 仅后邻:合并回收分区与后邻分区(新 start = 回收 start,新 size = 回收 size + 后邻 size),删除原后邻表项;
    • 前后均邻:合并前邻 + 回收 + 后邻三区(新 start = 前邻 start,新 size = 前邻 + 回收 + 后邻 size),删除原前、后邻表项;
  4. 完整流程 :模拟实际 OS 的初始内存划分→进程内存分配→进程结束回收→相邻分区合并→更新空闲表,贴合实际应用场景。
Python代码
python 复制代码
def print_free_table(free_table, desc="空闲分区表"):
    """
    格式化打印空闲分区表(按起始地址升序),方便查看分区状态
    :param free_table: 空闲分区表,列表[{'start': 起始地址, 'size': 大小}, ...]
    :param desc: 打印描述,区分不同阶段的分区表
    """
    # 先按起始地址升序排序,保证表的有序性
    free_table_sorted = sorted(free_table, key=lambda x: x['start'])
    print(f"\n{desc}(按起始地址升序):")
    if not free_table_sorted:
        print("  无空闲分区")
        return
    print(f"  序号 | 起始地址 | 分区大小")
    print(f"  ---- | -------- | --------")
    for idx, part in enumerate(free_table_sorted, 1):
        print(f"  {idx:4d} | {part['start']:8d} | {part['size']:8d}")
    # 返回排序后的表,保证后续操作的有序性
    return free_table_sorted

def allocate_memory(free_table, alloc_size):
    """
    模拟内存分配(首次适应算法,贴合OS实际),分配后更新空闲分区表
    :param free_table: 原空闲分区表
    :param alloc_size: 申请的内存大小
    :return: 分配成功返回起始地址,失败返回-1;同时更新空闲分区表
    """
    if alloc_size <= 0:
        print(f"分配失败:申请大小{alloc_size}非法(需大于0)")
        return -1
    # 按起始地址升序排序,首次适应算法:找到第一个大小足够的空闲分区
    free_table_sorted = sorted(free_table, key=lambda x: x['start'])
    for idx, part in enumerate(free_table_sorted):
        if part['size'] >= alloc_size:
            alloc_start = part['start']
            # 分区剩余空间:若剩余为0则删除表项,否则更新表项的起始地址和大小
            remaining_size = part['size'] - alloc_size
            if remaining_size == 0:
                del free_table_sorted[idx]
            else:
                free_table_sorted[idx]['start'] = alloc_start + alloc_size
                free_table_sorted[idx]['size'] = remaining_size
            print(f"内存分配成功:起始地址{alloc_start},大小{alloc_size}")
            # 更新原空闲表为排序后的新表
            free_table[:] = free_table_sorted
            return alloc_start
    # 无足够大的空闲分区,分配失败
    print(f"内存分配失败:无大小≥{alloc_size}的空闲分区")
    return -1

def memory_recycle(free_table, recycle_start, recycle_size):
    """
    内存回收核心算法:标记空闲+检查相邻分区+合并相邻分区+更新空闲分区表
    :param free_table: 空闲分区表(会被直接更新)
    :param recycle_start: 回收分区的起始地址
    :param recycle_size: 回收分区的大小
    :return: 更新后的空闲分区表
    """
    if recycle_start < 0 or recycle_size <= 0:
        raise ValueError("回收分区的起始地址≥0,大小必须>0")
    print(f"\n===== 开始内存回收 =====")
    print(f"回收分区信息:起始地址{recycle_start},大小{recycle_size}")
    # 按起始地址升序排序,方便查找相邻分区
    free_table = sorted(free_table, key=lambda x: x['start'])
    # 标记前邻、后邻分区(初始为None,无相邻)
    prev_part = None  # 前邻空闲分区
    next_part = None  # 后邻空闲分区

    # 步骤1:检查并找到前邻、后邻空闲分区
    for part in free_table:
        # 前邻判断:前分区.end == 回收分区.start (end=start+size)
        if part['start'] + part['size'] == recycle_start:
            prev_part = part
        # 后邻判断:回收分区.end == 后分区.start
        if recycle_start + recycle_size == part['start']:
            next_part = part

    # 步骤2:根据相邻情况执行合并(四合一核心逻辑)
    new_free_table = free_table.copy()
    if prev_part is None and next_part is None:
        # 情况1:无相邻分区,直接添加新的空闲分区
        new_free_table.append({'start': recycle_start, 'size': recycle_size})
        print("合并结果:无相邻空闲分区,直接添加为新空闲分区")
    elif prev_part is not None and next_part is None:
        # 情况2:仅前邻,合并前邻与回收分区
        new_free_table.remove(prev_part)
        merge_part = {
            'start': prev_part['start'],
            'size': prev_part['size'] + recycle_size
        }
        new_free_table.append(merge_part)
        print(f"合并结果:仅前邻(起始{prev_part['start']},大小{prev_part['size']}),合并为一个大分区")
    elif prev_part is None and next_part is not None:
        # 情况3:仅后邻,合并回收与后邻分区
        new_free_table.remove(next_part)
        merge_part = {
            'start': recycle_start,
            'size': recycle_size + next_part['size']
        }
        new_free_table.append(merge_part)
        print(f"合并结果:仅后邻(起始{next_part['start']},大小{next_part['size']}),合并为一个大分区")
    else:
        # 情况4:前后均邻,合并前邻+回收+后邻三区
        new_free_table.remove(prev_part)
        new_free_table.remove(next_part)
        merge_part = {
            'start': prev_part['start'],
            'size': prev_part['size'] + recycle_size + next_part['size']
        }
        new_free_table.append(merge_part)
        print(f"合并结果:前后均邻(前邻{prev_part['start']}/{prev_part['size']},后邻{next_part['start']}/{next_part['size']}),三区合并为一个大分区")

    # 步骤3:按起始地址升序更新空闲分区表,返回新表
    new_free_table_sorted = sorted(new_free_table, key=lambda x: x['start'])
    free_table[:] = new_free_table_sorted
    return free_table

# -------------------------- 完整场景测试用例 --------------------------
if __name__ == "__main__":
    # 模拟初始内存:总大小1000(地址0~999),初始为1个空闲分区(start=0,size=1000)
    TOTAL_MEM_SIZE = 1000
    free_table = [{'start': 0, 'size': TOTAL_MEM_SIZE}]
    print("===== 初始内存状态 =====")
    print_free_table(free_table, f"初始内存(总大小{TOTAL_MEM_SIZE})")

    # 阶段1:模拟3个进程依次申请内存(首次适应算法),模拟进程占用内存
    print("\n===== 阶段1:3个进程申请内存,模拟进程占用 =====")
    p1_size = 200  # 进程1申请200
    p2_size = 300  # 进程2申请300
    p3_size = 150  # 进程3申请150
    p1_start = allocate_memory(free_table, p1_size)
    p2_start = allocate_memory(free_table, p2_size)
    p3_start = allocate_memory(free_table, p3_size)
    print_free_table(free_table, "3个进程分配后")

    # 记录各进程的内存信息(结束时需回收)
    process_dict = {
        "进程1": {'start': p1_start, 'size': p1_size},
        "进程2": {'start': p2_start, 'size': p2_size},
        "进程3": {'start': p3_start, 'size': p3_size}
    }
    print("\n各进程占用内存信息:")
    for p_name, p_info in process_dict.items():
        print(f"  {p_name}:起始{p_info['start']},大小{p_info['size']}")

    # 阶段2:模拟进程依次结束,回收内存并合并相邻空闲分区(覆盖所有4种合并情况)
    print("\n===== 阶段2:进程依次结束,回收内存并合并相邻分区 =====")
    # 2.1 回收进程1(无相邻空闲分区,情况1)
    print(f"\n--- 第一步:进程1结束,回收内存 ---")
    memory_recycle(free_table, process_dict["进程1"]['start'], process_dict["进程1"]['size'])
    print_free_table(free_table, "回收进程1后")

    # 2.2 回收进程3(仅前邻空闲分区,情况2)
    print(f"\n--- 第二步:进程3结束,回收内存 ---")
    memory_recycle(free_table, process_dict["进程3"]['start'], process_dict["进程3"]['size'])
    print_free_table(free_table, "回收进程3后")

    # 2.3 先临时分配一个小分区(用于制造"前后均邻"场景,贴合实际)
    print(f"\n--- 临时操作:进程4申请50内存,制造后续合并场景 ---")
    p4_start = allocate_memory(free_table, 50)
    print_free_table(free_table, "进程4分配后")

    # 2.4 回收进程2(仅后邻空闲分区,情况3)
    print(f"\n--- 第三步:进程2结束,回收内存 ---")
    memory_recycle(free_table, process_dict["进程2"]['start'], process_dict["进程2"]['size'])
    print_free_table(free_table, "回收进程2后")

    # 2.5 回收进程4(前后均邻空闲分区,情况4,核心场景)
    print(f"\n--- 第四步:进程4结束,回收内存 ---")
    memory_recycle(free_table, p4_start, 50)
    print_free_table(free_table, "回收进程4后(最终内存状态)")

    # 验证最终合并结果:是否恢复为初始的1个大空闲分区(start=0,size=1000)
    print(f"\n===== 回收验证 =====")
    if len(free_table) == 1 and free_table[0]['start'] == 0 and free_table[0]['size'] == TOTAL_MEM_SIZE:
        print("验证通过:所有进程回收后,相邻分区完全合并,恢复为初始的1个大空闲分区!")
    else:
        print("验证失败:内存分区未完全合并")
程序运行结果展示
bash 复制代码
===== 初始内存状态 =====

初始内存(总大小1000)(按起始地址升序):
  序号 | 起始地址 | 分区大小
  ---- | -------- | --------
     1 |        0 |     1000

===== 阶段1:3个进程申请内存,模拟进程占用 =====
内存分配成功:起始地址0,大小200
内存分配成功:起始地址200,大小300
内存分配成功:起始地址500,大小150

3个进程分配后(按起始地址升序):
  序号 | 起始地址 | 分区大小
  ---- | -------- | --------
     1 |      650 |      350

各进程占用内存信息:
  进程1:起始0,大小200
  进程2:起始200,大小300
  进程3:起始500,大小150

===== 阶段2:进程依次结束,回收内存并合并相邻分区 =====

--- 第一步:进程1结束,回收内存 ---

===== 开始内存回收 =====
回收分区信息:起始地址0,大小200
合并结果:无相邻空闲分区,直接添加为新空闲分区

回收进程1后(按起始地址升序):
  序号 | 起始地址 | 分区大小
  ---- | -------- | --------
     1 |      650 |      350

--- 第二步:进程3结束,回收内存 ---

===== 开始内存回收 =====
回收分区信息:起始地址500,大小150
合并结果:仅后邻(起始650,大小350),合并为一个大分区

回收进程3后(按起始地址升序):
  序号 | 起始地址 | 分区大小
  ---- | -------- | --------
     1 |      650 |      350

--- 临时操作:进程4申请50内存,制造后续合并场景 ---
内存分配成功:起始地址650,大小50

进程4分配后(按起始地址升序):
  序号 | 起始地址 | 分区大小
  ---- | -------- | --------
     1 |      700 |      300

--- 第三步:进程2结束,回收内存 ---

===== 开始内存回收 =====
回收分区信息:起始地址200,大小300
合并结果:无相邻空闲分区,直接添加为新空闲分区

回收进程2后(按起始地址升序):
  序号 | 起始地址 | 分区大小
  ---- | -------- | --------
     1 |      700 |      300

--- 第四步:进程4结束,回收内存 ---

===== 开始内存回收 =====
回收分区信息:起始地址650,大小50
合并结果:仅后邻(起始700,大小300),合并为一个大分区

回收进程4后(最终内存状态)(按起始地址升序):
  序号 | 起始地址 | 分区大小
  ---- | -------- | --------
     1 |      700 |      300

===== 回收验证 =====
验证失败:内存分区未完全合并

2. 外部碎片整理算法

针对连续内存分配 / 分段存储的外部碎片 ,核心是紧凑(Compaction) 算法:

原理

将所有进程占用的连续内存分区 向内存的低地址端 / 高地址端移动 ,使所有进程的分区连续排列,将分散的外部碎片合并为一个大的连续空闲分区,位于内存的一端。

特点
  • 优点:彻底解决外部碎片,合并后的大空闲分区可分配给大进程;
  • 缺点:系统开销极大 (需要移动进程的内存数据,涉及地址重定位),会导致进程暂停运行(又称内存抖动)。
适用场景:连续内存分配系统,碎片过多导致无法分配大进程时,偶尔执行(非实时)。
算法实现思路

紧凑算法选向内存低地址端移动 (OS 最常用的实现方式),核心是进程连续重排 + 地址重定位 + 碎片彻底合并,完整步骤贴合操作系统经典原理,同时体现其核心特点:

  1. 数据结构设计 :分开维护两个核心表,避免混淆,贴合实际 OS 内存管理:
    • 进程分区表:列表存储字典,含pid(进程 ID)、start(物理起始地址)、size(分区大小),记录进程的内存占用;
    • 空闲分区表:延续之前的结构,start+size,记录分散的外部碎片;
  2. 紧凑核心步骤 (低地址端):
    • 暂停所有进程:打印提示,体现紧凑算法 "进程暂停运行" 的特点;
    • 进程排序:将所有进程分区按原物理起始地址升序排列,保证移动顺序的合理性;
    • 地址重定位:从内存低地址0开始,为每个进程重新分配连续的物理地址 (前一个进程的结束地址 = 后一个进程的起始地址),更新进程表的start字段,记录重定位信息;
    • 计算总占用:统计所有进程的总内存占用大小,确定空闲分区的起始地址(进程总占用结束地址);
    • 碎片合并:清空原有分散的空闲分区表,生成一个唯一的大空闲分区(起始 = 进程总占用结束地址,大小 = 内存总大小 - 进程总占用);
    • 进程恢复运行:打印提示,体现紧凑的完成;
  3. 开销量化 :统计紧凑过程中总移动数据量(所有进程的大小之和),体现其 "系统开销极大" 的特点;
  4. 完整场景模拟 :先通过多次分配 + 非连续回收制造明显的外部碎片,再执行紧凑整理,直观对比碎片前后的差异,验证算法效果。
Python代码
python 复制代码
def print_process_table(proc_table, desc="进程占用分区表"):
    """格式化打印进程分区表(按物理起始地址升序),记录进程地址重定位信息"""
    proc_sorted = sorted(proc_table, key=lambda x: x['start'])
    print(f"\n{desc}:")
    if not proc_sorted:
        print("  无进程占用内存")
        return
    print(f"  进程ID | 物理起始地址 | 分区大小")
    print(f"  ------ | ------------ | --------")
    for proc in proc_sorted:
        print(f"  {proc['pid']:6d} | {proc['start']:12d} | {proc['size']:8d}")
    return proc_sorted

def print_free_table(free_table, desc="空闲分区表(外部碎片)"):
    """格式化打印空闲分区表(按起始地址升序),量化外部碎片情况"""
    free_sorted = sorted(free_table, key=lambda x: x['start'])
    print(f"\n{desc}:")
    if not free_sorted:
        print("  无空闲分区(内存占满)")
        return
    print(f"  序号 | 起始地址 | 分区大小")
    print(f"  ---- | -------- | --------")
    total_free = 0
    for idx, part in enumerate(free_sorted, 1):
        print(f"  {idx:4d} | {part['start']:8d} | {part['size']:8d}")
        total_free += part['size']
    print(f"  合计 |        - | {total_free:8d} (外部碎片总大小)")
    return free_sorted, total_free

def allocate_memory(proc_table, free_table, total_mem, pid, alloc_size):
    """
    首次适应内存分配算法,分配后更新进程表和空闲表
    :param proc_table: 进程分区表 | free_table: 空闲分区表 | total_mem: 内存总大小
    :param pid: 申请进程ID | alloc_size: 申请大小
    :return: 分配成功返回新进程字典,失败返回None
    """
    if alloc_size <= 0 or alloc_size > total_mem:
        print(f"进程{pid}分配失败:申请大小{alloc_size}非法(需>0且≤{total_mem})")
        return None
    free_sorted = sorted(free_table, key=lambda x: x['start'])
    for idx, part in enumerate(free_sorted):
        if part['size'] >= alloc_size:
            # 分配地址:当前空闲分区起始地址
            alloc_start = part['start']
            # 更新空闲分区:剩余空间为0则删除,否则更新起始和大小
            remaining = part['size'] - alloc_size
            if remaining == 0:
                del free_sorted[idx]
            else:
                free_sorted[idx]['start'] = alloc_start + alloc_size
                free_sorted[idx]['size'] = remaining
            # 更新进程表:添加新进程
            new_proc = {'pid': pid, 'start': alloc_start, 'size': alloc_size}
            proc_table.append(new_proc)
            # 同步更新原空闲表
            free_table[:] = free_sorted
            print(f"进程{pid}分配成功:起始{alloc_start},大小{alloc_size}")
            return new_proc
    print(f"进程{pid}分配失败:无大小≥{alloc_size}的连续空闲分区(外部碎片过多)")
    return None

def memory_recycle(proc_table, free_table, pid):
    """
    内存回收+相邻合并,回收后更新进程表和空闲表(延续之前的合并逻辑)
    :param proc_table: 进程分区表 | free_table: 空闲分区表 | pid: 结束进程ID
    :return: 回收成功返回回收分区,失败返回None
    """
    # 查找要回收的进程
    recycle_proc = None
    for idx, proc in enumerate(proc_table):
        if proc['pid'] == pid:
            recycle_proc = proc_table.pop(idx)
            break
    if not recycle_proc:
        print(f"进程{pid}回收失败:未找到该进程")
        return None
    recycle_start = recycle_proc['start']
    recycle_size = recycle_proc['size']
    print(f"\n进程{pid}结束,回收内存:起始{recycle_start},大小{recycle_size}")

    # 相邻合并逻辑(延续之前的四合一合并)
    free_sorted = sorted(free_table, key=lambda x: x['start'])
    prev_part, next_part = None, None
    for part in free_sorted:
        if part['start'] + part['size'] == recycle_start:
            prev_part = part
        if recycle_start + recycle_size == part['start']:
            next_part = part

    new_free = free_sorted.copy()
    if not prev_part and not next_part:
        new_free.append({'start': recycle_start, 'size': recycle_size})
        print("  回收结果:无相邻空闲分区,新增外部碎片")
    elif prev_part and not next_part:
        new_free.remove(prev_part)
        new_free.append({'start': prev_part['start'], 'size': prev_part['size'] + recycle_size})
        print("  回收结果:与前邻空闲分区合并,减少外部碎片")
    elif not prev_part and next_part:
        new_free.remove(next_part)
        new_free.append({'start': recycle_start, 'size': recycle_size + next_part['size']})
        print("  回收结果:与后邻空闲分区合并,减少外部碎片")
    else:
        new_free.remove(prev_part)
        new_free.remove(next_part)
        new_free.append({'start': prev_part['start'], 'size': prev_part['size'] + recycle_size + next_part['size']})
        print("  回收结果:与前后邻空闲分区合并,显著减少外部碎片")

    # 同步更新空闲表
    free_table[:] = sorted(new_free, key=lambda x: x['start'])
    return recycle_proc

def memory_compaction(proc_table, free_table, total_mem):
    """
    外部碎片整理核心:紧凑(Compaction)算法(向低地址端移动)
    实现:进程地址重定位 + 彻底合并外部碎片 + 量化系统开销
    :param proc_table: 进程分区表(会更新重定位后的地址)
    :param free_table: 空闲分区表(会替换为合并后的大分区)
    :param total_mem: 内存总大小
    :return: 重定位后的进程表 + 合并后的空闲表 + 总移动开销
    """
    print("\n" + "="*80)
    print("===== 开始执行紧凑(Compaction)算法:外部碎片整理 =====")
    print("="*80)
    if not proc_table:
        print("  无进程占用内存,无需紧凑,空闲分区已为连续大分区")
        free_table[:] = [{'start': 0, 'size': total_mem}]
        return proc_table, free_table, 0
    if not free_table:
        print("  内存无外部碎片,无需紧凑")
        return proc_table, free_table, 0

    # 步骤1:暂停所有进程(紧凑算法核心特点:进程暂停,产生内存抖动)
    print("  【紧凑步骤1】暂停所有进程运行,开始内存数据移动...")

    # 步骤2:按原起始地址升序排列进程,确定移动顺序
    proc_sorted = sorted(proc_table, key=lambda x: x['start'])
    # 步骤3:地址重定位 - 从低地址0开始,为进程分配连续物理地址
    current_addr = 0  # 重定位的当前起始地址,从0开始
    relocate_info = []  # 记录进程重定位信息
    total_move_size = 0  # 总移动数据量,量化系统开销
    for proc in proc_sorted:
        old_start = proc['start']
        proc['start'] = current_addr  # 更新为新的物理起始地址
        relocate_info.append(f"进程{proc['pid']}:{old_start} → {current_addr}")
        total_move_size += proc['size']  # 累加移动数据量
        current_addr += proc['size']     # 下一个进程紧跟当前进程结束地址

    # 步骤4:彻底合并外部碎片 - 生成唯一的大空闲分区(高地址端)
    total_proc_size = current_addr  # 所有进程的总占用大小
    if total_proc_size > total_mem:
        raise ValueError("进程总占用超过内存总大小,内存越界!")
    free_compacted = [{'start': total_proc_size, 'size': total_mem - total_proc_size}]
    free_table[:] = free_compacted  # 更新空闲表为合并后的大分区

    # 步骤5:恢复所有进程运行,打印重定位和开销信息
    print("  【紧凑步骤2】所有进程地址重定位完成,恢复进程运行!")
    print("  【进程地址重定位信息】:")
    for info in relocate_info:
        print(f"    {info}")
    print(f"  【紧凑系统开销】:总移动内存数据量 = {total_move_size} 字节(所有进程数据均需移动)")
    print(f"  【碎片整理结果】:分散的外部碎片合并为1个连续大空闲分区(起始{total_proc_size},大小{total_mem - total_proc_size})")

    return proc_table, free_table, total_move_size

# -------------------------- 完整场景测试用例 --------------------------
if __name__ == "__main__":
    # 模拟系统内存配置:总大小2000字节(物理地址0~1999)
    TOTAL_MEM_SIZE = 2000
    # 初始化:进程表空,空闲表为1个连续大分区
    proc_table = []
    free_table = [{'start': 0, 'size': TOTAL_MEM_SIZE}]

    print("===== 初始内存状态:无进程,1个连续大空闲分区 =====")
    print_process_table(proc_table)
    print_free_table(free_table)

    # 阶段1:多次分配内存 - 5个进程依次申请不同大小,制造初始占用
    print("\n" + "="*80)
    print("===== 阶段1:5个进程依次分配内存,初始占用 =====")
    print("="*80)
    alloc_tasks = [(1, 300), (2, 400), (3, 200), (4, 500), (5, 100)]  # (pid, size)
    for pid, size in alloc_tasks:
        allocate_memory(proc_table, free_table, TOTAL_MEM_SIZE, pid, size)
    print_process_table(proc_table, "5个进程分配后 - 进程表")
    print_free_table(free_table, "5个进程分配后 - 空闲表")

    # 阶段2:非连续回收进程 - 回收1/3/5号进程(间隔回收),产生大量分散的外部碎片
    print("\n" + "="*80)
    print("===== 阶段2:间隔回收进程(1/3/5号),产生外部碎片 =====")
    print("="*80)
    for pid in [1, 3, 5]:
        memory_recycle(proc_table, free_table, pid)
    print_process_table(proc_table, "间隔回收后 - 进程表(剩余2/4号进程)")
    free_table, total_free = print_free_table(free_table, "间隔回收后 - 空闲表(大量外部碎片)")

    # 阶段3:尝试分配大进程 - 申请600字节,因外部碎片分散无法分配(体现碎片问题)
    print("\n" + "="*80)
    print("===== 阶段3:尝试分配大进程(6号,600字节),验证外部碎片问题 =====")
    print("="*80)
    allocate_memory(proc_table, free_table, TOTAL_MEM_SIZE, 6, 600)

    # 阶段4:执行紧凑算法 - 整理外部碎片,重定位进程,合并为大空闲分区
    print("\n" + "="*80)
    print("===== 阶段4:执行紧凑算法,彻底解决外部碎片 =====")
    print("="*80)
    memory_compaction(proc_table, free_table, TOTAL_MEM_SIZE)
    print_process_table(proc_table, "紧凑后 - 进程表(地址重定位,连续排列)")
    print_free_table(free_table, "紧凑后 - 空闲表(碎片彻底合并为大分区)")

    # 阶段5:再次分配大进程 - 申请600字节,紧凑后可成功分配(验证紧凑效果)
    print("\n" + "="*80)
    print("===== 阶段5:再次分配大进程(6号,600字节),验证紧凑效果 =====")
    print("="*80)
    allocate_memory(proc_table, free_table, TOTAL_MEM_SIZE, 6, 600)
    print_process_table(proc_table, "大进程分配后 - 进程表")
    print_free_table(free_table, "大进程分配后 - 空闲表")

    print("\n" + "="*80)
    print("===== 外部碎片整理(紧凑算法)全流程验证完成 =====")
    print("="*80)
程序运行结果展示
bash 复制代码
===== 初始内存状态:无进程,1个连续大空闲分区 =====

进程占用分区表:
  无进程占用内存

空闲分区表(外部碎片):
  序号 | 起始地址 | 分区大小
  ---- | -------- | --------
     1 |        0 |     2000
  合计 |        - |     2000 (外部碎片总大小)

================================================================================
===== 阶段1:5个进程依次分配内存,初始占用 =====
================================================================================
进程1分配成功:起始0,大小300
进程2分配成功:起始300,大小400
进程3分配成功:起始700,大小200
进程4分配成功:起始900,大小500
进程5分配成功:起始1400,大小100

5个进程分配后 - 进程表:
  进程ID | 物理起始地址 | 分区大小
  ------ | ------------ | --------
       1 |            0 |      300
       2 |          300 |      400
       3 |          700 |      200
       4 |          900 |      500
       5 |         1400 |      100

5个进程分配后 - 空闲表:
  序号 | 起始地址 | 分区大小
  ---- | -------- | --------
     1 |     1500 |      500
  合计 |        - |      500 (外部碎片总大小)

================================================================================
===== 阶段2:间隔回收进程(1/3/5号),产生外部碎片 =====
================================================================================

进程1结束,回收内存:起始0,大小300
  回收结果:无相邻空闲分区,新增外部碎片

进程3结束,回收内存:起始700,大小200
  回收结果:无相邻空闲分区,新增外部碎片

进程5结束,回收内存:起始1400,大小100
  回收结果:与后邻空闲分区合并,减少外部碎片

间隔回收后 - 进程表(剩余2/4号进程):
  进程ID | 物理起始地址 | 分区大小
  ------ | ------------ | --------
       2 |          300 |      400
       4 |          900 |      500

间隔回收后 - 空闲表(大量外部碎片):
  序号 | 起始地址 | 分区大小
  ---- | -------- | --------
     1 |        0 |      300
     2 |      700 |      200
     3 |     1400 |      600
  合计 |        - |     1100 (外部碎片总大小)

================================================================================
===== 阶段3:尝试分配大进程(6号,600字节),验证外部碎片问题 =====
================================================================================
进程6分配成功:起始1400,大小600

================================================================================
===== 阶段4:执行紧凑算法,彻底解决外部碎片 =====
================================================================================

================================================================================
===== 开始执行紧凑(Compaction)算法:外部碎片整理 =====
================================================================================
  【紧凑步骤1】暂停所有进程运行,开始内存数据移动...
  【紧凑步骤2】所有进程地址重定位完成,恢复进程运行!
  【进程地址重定位信息】:
    进程2:300 → 0
    进程4:900 → 400
    进程6:1400 → 900
  【紧凑系统开销】:总移动内存数据量 = 1500 字节(所有进程数据均需移动)
  【碎片整理结果】:分散的外部碎片合并为1个连续大空闲分区(起始1500,大小500)

紧凑后 - 进程表(地址重定位,连续排列):
  进程ID | 物理起始地址 | 分区大小
  ------ | ------------ | --------
       2 |            0 |      400
       4 |          400 |      500
       6 |          900 |      600

紧凑后 - 空闲表(碎片彻底合并为大分区):
  序号 | 起始地址 | 分区大小
  ---- | -------- | --------
     1 |     1500 |      500
  合计 |        - |      500 (外部碎片总大小)

================================================================================
===== 阶段5:再次分配大进程(6号,600字节),验证紧凑效果 =====
================================================================================
进程6分配失败:无大小≥600的连续空闲分区(外部碎片过多)

大进程分配后 - 进程表:
  进程ID | 物理起始地址 | 分区大小
  ------ | ------------ | --------
       2 |            0 |      400
       4 |          400 |      500
       6 |          900 |      600

大进程分配后 - 空闲表:
  序号 | 起始地址 | 分区大小
  ---- | -------- | --------
     1 |     1500 |      500
  合计 |        - |      500 (外部碎片总大小)

================================================================================
===== 外部碎片整理(紧凑算法)全流程验证完成 =====
================================================================================

3. 伙伴系统分配算法(Buddy System)

原理

针对连续内存分配 的外部碎片问题的改进算法 ,是 Linux 内核物理内存分配的核心算法之一,核心是按 2 的幂次划分空闲分区

  1. 将物理内存划分为一个大小为 2^n的初始空闲分区(如 2^30 字节 = 1GB);
  2. 分配时,若当前分区大小 > 2× 进程需求,则将分区对半分割为两个大小相等的 "伙伴分区",重复分割直到分区大小≥进程需求且为 2 的幂次;
  3. 回收时,检查被回收的分区的伙伴分区 是否为空闲,若为空闲则合并为一个大的伙伴分区,重复合并直到无法合并。
特点
  • 优点:分配 / 回收速度快(按 2 的幂次分割 / 合并,无需遍历),减少外部碎片,支持高效的内存分配;
  • 缺点:存在内部碎片(分区大小为 2 的幂次,可能大于进程需求)。
适用场景:现代 OS 的内核内存分配(如 Linux 的 slab 分配器基于伙伴系统),用于分配小的内核对象。
算法实现思路

伙伴系统的核心是基于 2 的幂次的分区管理,解决连续内存分配的外部碎片问题,同时保证分配 / 回收的高效性,核心设计严格贴合 Linux 内核原理:

  1. 核心数据结构buddy_free 字典,键为 2 的幂次的空闲分区大小 (如 8、16、32),值为对应大小的空闲分区起始地址列表(地址按对应大小对齐,保证伙伴关系),这是伙伴系统高效的关键(无需遍历,直接按大小索引)。
  2. 前置条件 :物理内存总大小必须是2 的整数次幂 ,初始时buddy_free中仅包含一个初始大小的键,值为[0](内存起始地址为 0)。
  3. 关键辅助计算
    • 2 的幂次向上取整 :将进程的内存需求,转换为大于等于需求的最小 2 的幂次(如需求 5→8,需求 10→16),保证分区大小为 2 的幂次;
    • 伙伴地址计算 :通过异或运算 快速计算(Linux 内核原生实现),对于大小2^k、起始地址s的分区,伙伴地址 =s ^ (2^k),比加减运算更高效,且能统一处理 "前伙伴 / 后伙伴";
    • 地址对齐检查 :大小2^k的分区,起始地址必须是2^k的整数倍,保证伙伴分区的合法性。
  4. 分配核心步骤
    • 计算需求的最小 2 的幂次alloc_size
    • alloc_size开始,按 2 的幂次递增查找首个有空闲分区的大小
    • 若找到的大小等于alloc_size,直接分配该大小的首个空闲地址;
    • 若找到的大小大于alloc_size递归对半分割 该分区为两个伙伴分区,直到分割为alloc_size,分配其中一个,另一个加入对应大小的空闲列表;
    • 记录分配信息,计算内部碎片(分区大小 - 需求大小)。
  5. 回收核心步骤
    • 校验回收分区的大小(2 的幂次)、地址(对齐)、是否已分配;
    • 计算当前分区的伙伴地址,检查伙伴是否为空闲(在对应大小的空闲列表中);
    • 若伙伴空闲,删除伙伴地址 ,将当前分区与伙伴分区合并为更大的 2 的幂次分区(大小 ×2);
    • 对合并后的大分区递归执行回收逻辑,直到伙伴不空闲,将最终的分区地址加入对应大小的空闲列表。
  6. 核心特点体现 :分配 / 回收时打印分割 / 合并过程 ,分配后打印内部碎片 ,回收后打印合并过程,直观体现伙伴系统的优缺点。
Python代码
python 复制代码
import math

def next_power_of_2(x):
    """
    计算大于等于x的最小2的幂次(伙伴系统核心计算)
    如x=5→8(2^3),x=8→8,x=10→16(2^4)
    """
    if x <= 0:
        return 1
    if (x & (x - 1)) == 0:  # x本身是2的幂次
        return x
    return 1 << (x - 1).bit_length()  # 左移实现2的幂次

def get_buddy_addr(s, size):
    """
    计算伙伴分区的起始地址(Linux内核原生实现:异或运算)
    :param s: 当前分区起始地址 | size: 当前分区大小(2的幂次)
    :return: 伙伴分区的起始地址
    """
    return s ^ size

def is_addr_aligned(s, size):
    """检查地址s是否按size(2的幂次)对齐,伙伴系统的必要条件"""
    return (s & (size - 1)) == 0

def print_buddy_status(buddy_free, total_mem, desc="伙伴系统空闲状态"):
    """
    格式化打印伙伴系统的空闲分区状态
    按2的幂次升序排列,展示各大小的空闲地址列表
    """
    print(f"\n{desc}(总内存:{total_mem} 字节,2的幂次分区):")
    print(f"  分区大小(2^k) | 空闲起始地址列表")
    print(f"  ------------- | ----------------")
    # 按2的幂次升序遍历键
    for size in sorted(buddy_free.keys()):
        addrs = sorted(buddy_free[size])
        if addrs:
            print(f"  {size:13d} | {addrs}")
        else:
            del buddy_free[size]  # 删除无空闲的大小,保持字典整洁
    # 统计总空闲内存
    total_free = sum(size * len(addrs) for size, addrs in buddy_free.items())
    print(f"  ------------- | ----------------")
    print(f"  总空闲内存     | {total_free} 字节")
    return total_free

def buddy_allocate(buddy_free, total_mem, pid, req_size):
    """
    伙伴系统内存分配核心函数
    :param buddy_free: 伙伴空闲字典 | total_mem: 总内存(2的幂次) | pid: 进程ID | req_size: 申请大小
    :return: 分配成功返回字典{'pid':pid, 'start':起始地址, 'alloc_size':分区大小, 'req_size':需求大小},失败返回None
    """
    if req_size <= 0 or req_size > total_mem:
        print(f"进程{pid}分配失败:申请大小{req_size}非法(需>0且≤{total_mem})")
        return None
    # 步骤1:计算需要的最小2的幂次分区大小
    alloc_size = next_power_of_2(req_size)
    current_size = alloc_size
    print(f"进程{pid}申请{req_size}字节,向上取整为2的幂次分区{alloc_size}字节")

    # 步骤2:按2的幂次递增,查找首个有空闲分区的大小
    while current_size <= total_mem:
        if current_size in buddy_free and buddy_free[current_size]:
            break
        current_size <<= 1  # 未找到,大小×2(左移1位)
    if current_size > total_mem:
        print(f"进程{pid}分配失败:无足够空闲内存(总空闲不足{alloc_size}字节)")
        return None

    # 步骤3:若找到的大小大于需要的大小,递归对半分割为伙伴分区
    while current_size > alloc_size:
        split_size = current_size >> 1  # 分割为原大小的1/2
        # 取出当前大小的首个空闲地址
        s = buddy_free[current_size].pop(0)
        if not buddy_free[current_size]:  # 该大小无空闲,删除键
            del buddy_free[current_size]
        # 分割为两个伙伴分区:s 和 s+split_size
        buddy1 = s
        buddy2 = s + split_size
        # 将两个伙伴分区加入分割后大小的空闲列表
        if split_size not in buddy_free:
            buddy_free[split_size] = []
        buddy_free[split_size].extend([buddy1, buddy2])
        buddy_free[split_size].sort()  # 排序,保证地址有序
        print(f"  分割{current_size}字节分区(地址{s})为两个{split_size}字节伙伴分区:{buddy1}, {buddy2}")
        current_size = split_size  # 继续分割,直到等于需要的大小

    # 步骤4:分配当前大小的首个空闲地址
    alloc_start = buddy_free[current_size].pop(0)
    if not buddy_free[current_size]:
        del buddy_free[current_size]
    # 计算内部碎片
    internal_fragment = alloc_size - req_size
    print(f"进程{pid}分配成功:地址{alloc_start},分区{alloc_size}字节,内部碎片{internal_fragment}字节")
    # 返回分配信息
    return {
        'pid': pid,
        'start': alloc_start,
        'alloc_size': alloc_size,
        'req_size': req_size
    }

def buddy_free_mem(buddy_free, total_mem, alloc_info):
    """
    伙伴系统内存回收核心函数(递归合并伙伴分区)
    :param buddy_free: 伙伴空闲字典 | total_mem: 总内存(2的幂次) | alloc_info: 分配信息(buddy_allocate的返回值)
    :return: 回收成功返回True,失败返回False
    """
    if not alloc_info:
        print("回收失败:无效的分配信息")
        return False
    pid = alloc_info['pid']
    s = alloc_info['start']
    size = alloc_info['alloc_size']
    print(f"\n开始回收进程{pid}:地址{s},分区大小{size}字节")

    # 步骤1:基础校验(大小为2的幂次、地址对齐、大小不超过总内存)
    if not is_addr_aligned(s, size) or (size & (size - 1)) != 0 or size > total_mem:
        print(f"进程{pid}回收失败:分区信息非法(地址未对齐/大小非2的幂次)")
        return False

    # 步骤2:递归合并伙伴分区的核心函数
    def merge(s, size):
        if size >= total_mem:  # 已到最大分区大小,无法再合并
            if size not in buddy_free:
                buddy_free[size] = []
            buddy_free[size].append(s)
            buddy_free[size].sort()
            print(f"  无法合并,将{size}字节分区(地址{s})加入空闲列表")
            return
        # 计算伙伴地址
        buddy_s = get_buddy_addr(s, size)
        print(f"  查找{size}字节分区(地址{s})的伙伴分区:地址{buddy_s}")
        # 检查伙伴是否在当前大小的空闲列表中
        if size in buddy_free and buddy_s in buddy_free[size]:
            # 伙伴空闲,删除伙伴地址,准备合并为更大的分区(size×2)
            buddy_free[size].remove(buddy_s)
            if not buddy_free[size]:
                del buddy_free[size]
            merge_addr = min(s, buddy_s)  # 合并后的起始地址为两个伙伴的较小地址
            merge_size = size << 1       # 合并后的大小为原大小×2
            print(f"  伙伴分区(地址{buddy_s})空闲,合并为{merge_size}字节分区(地址{merge_addr}),继续尝试合并")
            # 递归合并更大的伙伴分区
            merge(merge_addr, merge_size)
        else:
            # 伙伴不空闲,将当前分区加入空闲列表
            if size not in buddy_free:
                buddy_free[size] = []
            buddy_free[size].append(s)
            buddy_free[size].sort()
            print(f"  伙伴分区(地址{buddy_s})非空闲,将{size}字节分区(地址{s})加入空闲列表")

    # 执行递归合并
    merge(s, size)
    print(f"进程{pid}回收完成,已完成伙伴分区合并")
    return True

# -------------------------- 完整场景测试用例 --------------------------
if __name__ == "__main__":
    # 步骤1:初始化伙伴系统(总内存设为64字节,2^6,方便测试分割/合并;实际Linux为2^30/2^31)
    TOTAL_MEM = 64  # 总内存必须是2的幂次
    # 初始空闲字典:仅64字节大小,空闲地址[0]
    buddy_free = {TOTAL_MEM: [0]}
    allocated_procs = []  # 记录已分配的进程信息,用于回收
    print("===== 伙伴系统初始化 =====")
    print_buddy_status(buddy_free, TOTAL_MEM, "初始空闲状态")

    # 步骤2:多进程分配内存(触发多次分区分割,体现分配逻辑,统计内部碎片)
    print("\n" + "="*80)
    print("===== 阶段1:多进程分配内存,触发分区分割 =====")
    print("="*80)
    alloc_tasks = [(1, 5), (2, 10), (3, 3), (4, 15)]  # (pid, 需求大小)
    for pid, req_size in alloc_tasks:
        proc_info = buddy_allocate(buddy_free, TOTAL_MEM, pid, req_size)
        if proc_info:
            allocated_procs.append(proc_info)
        print_buddy_status(buddy_free, TOTAL_MEM, f"进程{pid}分配后")
        print("-"*60)

    # 统计总内部碎片
    total_internal_fragment = sum(p['alloc_size'] - p['req_size'] for p in allocated_procs)
    print(f"\n阶段1分配完成,总内部碎片:{total_internal_fragment} 字节")
    print("已分配进程信息:")
    for p in allocated_procs:
        print(f"  进程{p['pid']}:地址{p['start']},分区{p['alloc_size']},需求{p['req_size']},碎片{p['alloc_size']-p['req_size']}")

    # 步骤3:多进程回收内存(触发递归伙伴合并,体现回收逻辑)
    print("\n" + "="*80)
    print("===== 阶段2:多进程回收内存,触发伙伴分区合并 =====")
    print("="*80)
    # 按进程1→3→2→4的顺序回收,触发不同的合并场景(单次合并、递归合并)
    for pid in [1, 3, 2, 4]:
        # 查找对应进程的分配信息
        proc_info = next((p for p in allocated_procs if p['pid'] == pid), None)
        if proc_info:
            buddy_free_mem(buddy_free, TOTAL_MEM, proc_info)
            allocated_procs.remove(proc_info)
        print_buddy_status(buddy_free, TOTAL_MEM, f"进程{pid}回收后")
        print("-"*60)

    # 步骤4:验证最终状态(所有进程回收后,恢复为初始的单个大分区)
    print("\n" + "="*80)
    print("===== 伙伴系统全流程验证 =====")
    print("="*80)
    final_free = print_buddy_status(buddy_free, TOTAL_MEM, "所有进程回收后-最终空闲状态")
    if TOTAL_MEM in buddy_free and buddy_free[TOTAL_MEM] == [0] and final_free == TOTAL_MEM:
        print("✅ 验证通过:所有进程回收后,伙伴分区完全合并,恢复为初始的单个大分区!")
    else:
        print("❌ 验证失败:分区未完全合并")

    print("\n===== 伙伴系统分配/回收算法演示完成 =====")
程序运行结果展示
bash 复制代码
===== 伙伴系统初始化 =====

初始空闲状态(总内存:64 字节,2的幂次分区):
  分区大小(2^k) | 空闲起始地址列表
  ------------- | ----------------
             64 | [0]
  ------------- | ----------------
  总空闲内存     | 64 字节

================================================================================
===== 阶段1:多进程分配内存,触发分区分割 =====
================================================================================
进程1申请5字节,向上取整为2的幂次分区8字节
  分割64字节分区(地址0)为两个32字节伙伴分区:0, 32
  分割32字节分区(地址0)为两个16字节伙伴分区:0, 16
  分割16字节分区(地址0)为两个8字节伙伴分区:0, 8
进程1分配成功:地址0,分区8字节,内部碎片3字节

进程1分配后(总内存:64 字节,2的幂次分区):
  分区大小(2^k) | 空闲起始地址列表
  ------------- | ----------------
              8 | [8]
             16 | [16]
             32 | [32]
  ------------- | ----------------
  总空闲内存     | 56 字节
------------------------------------------------------------
进程2申请10字节,向上取整为2的幂次分区16字节
进程2分配成功:地址16,分区16字节,内部碎片6字节

进程2分配后(总内存:64 字节,2的幂次分区):
  分区大小(2^k) | 空闲起始地址列表
  ------------- | ----------------
              8 | [8]
             32 | [32]
  ------------- | ----------------
  总空闲内存     | 40 字节
------------------------------------------------------------
进程3申请3字节,向上取整为2的幂次分区4字节
  分割8字节分区(地址8)为两个4字节伙伴分区:8, 12
进程3分配成功:地址8,分区4字节,内部碎片1字节

进程3分配后(总内存:64 字节,2的幂次分区):
  分区大小(2^k) | 空闲起始地址列表
  ------------- | ----------------
              4 | [12]
             32 | [32]
  ------------- | ----------------
  总空闲内存     | 36 字节
------------------------------------------------------------
进程4申请15字节,向上取整为2的幂次分区16字节
  分割32字节分区(地址32)为两个16字节伙伴分区:32, 48
进程4分配成功:地址32,分区16字节,内部碎片1字节

进程4分配后(总内存:64 字节,2的幂次分区):
  分区大小(2^k) | 空闲起始地址列表
  ------------- | ----------------
              4 | [12]
             16 | [48]
  ------------- | ----------------
  总空闲内存     | 20 字节
------------------------------------------------------------

阶段1分配完成,总内部碎片:11 字节
已分配进程信息:
  进程1:地址0,分区8,需求5,碎片3
  进程2:地址16,分区16,需求10,碎片6
  进程3:地址8,分区4,需求3,碎片1
  进程4:地址32,分区16,需求15,碎片1

================================================================================
===== 阶段2:多进程回收内存,触发伙伴分区合并 =====
================================================================================

开始回收进程1:地址0,分区大小8字节
  查找8字节分区(地址0)的伙伴分区:地址8
  伙伴分区(地址8)非空闲,将8字节分区(地址0)加入空闲列表
进程1回收完成,已完成伙伴分区合并

进程1回收后(总内存:64 字节,2的幂次分区):
  分区大小(2^k) | 空闲起始地址列表
  ------------- | ----------------
              4 | [12]
              8 | [0]
             16 | [48]
  ------------- | ----------------
  总空闲内存     | 28 字节
------------------------------------------------------------

开始回收进程3:地址8,分区大小4字节
  查找4字节分区(地址8)的伙伴分区:地址12
  伙伴分区(地址12)空闲,合并为8字节分区(地址8),继续尝试合并
  查找8字节分区(地址8)的伙伴分区:地址0
  伙伴分区(地址0)空闲,合并为16字节分区(地址0),继续尝试合并
  查找16字节分区(地址0)的伙伴分区:地址16
  伙伴分区(地址16)非空闲,将16字节分区(地址0)加入空闲列表
进程3回收完成,已完成伙伴分区合并

进程3回收后(总内存:64 字节,2的幂次分区):
  分区大小(2^k) | 空闲起始地址列表
  ------------- | ----------------
             16 | [0, 48]
  ------------- | ----------------
  总空闲内存     | 32 字节
------------------------------------------------------------

开始回收进程2:地址16,分区大小16字节
  查找16字节分区(地址16)的伙伴分区:地址0
  伙伴分区(地址0)空闲,合并为32字节分区(地址0),继续尝试合并
  查找32字节分区(地址0)的伙伴分区:地址32
  伙伴分区(地址32)非空闲,将32字节分区(地址0)加入空闲列表
进程2回收完成,已完成伙伴分区合并

进程2回收后(总内存:64 字节,2的幂次分区):
  分区大小(2^k) | 空闲起始地址列表
  ------------- | ----------------
             16 | [48]
             32 | [0]
  ------------- | ----------------
  总空闲内存     | 48 字节
------------------------------------------------------------

开始回收进程4:地址32,分区大小16字节
  查找16字节分区(地址32)的伙伴分区:地址48
  伙伴分区(地址48)空闲,合并为32字节分区(地址32),继续尝试合并
  查找32字节分区(地址32)的伙伴分区:地址0
  伙伴分区(地址0)空闲,合并为64字节分区(地址0),继续尝试合并
  无法合并,将64字节分区(地址0)加入空闲列表
进程4回收完成,已完成伙伴分区合并

进程4回收后(总内存:64 字节,2的幂次分区):
  分区大小(2^k) | 空闲起始地址列表
  ------------- | ----------------
             64 | [0]
  ------------- | ----------------
  总空闲内存     | 64 字节
------------------------------------------------------------

================================================================================
===== 伙伴系统全流程验证 =====
================================================================================

所有进程回收后-最终空闲状态(总内存:64 字节,2的幂次分区):
  分区大小(2^k) | 空闲起始地址列表
  ------------- | ----------------
             64 | [0]
  ------------- | ----------------
  总空闲内存     | 64 字节
✅ 验证通过:所有进程回收后,伙伴分区完全合并,恢复为初始的单个大分区!

===== 伙伴系统分配/回收算法演示完成 =====

4. 内存池分配算法(Slab/Zone Allocator)

原理

针对频繁分配 / 释放小内存对象 (如进程控制块 PCB、文件描述符)的优化算法,核心是预先分配一组固定大小的内存池,每个内存池对应一种类型的小对象,分配时直接从内存池中取,释放时放回内存池,无需频繁的碎片整理。

经典实现:Linux 的 Slab 分配器

将内存池分为slab(包含若干相同的小对象),slab 分为空闲 slab、部分空闲 slab、满 slab,分配时从部分空闲 slab 中取对象,释放时将对象放回 slab,当 slab 无空闲对象时再从伙伴系统分配内存。

特点
  • 优点:分配 / 释放速度极快,无碎片问题,支持对象缓存;
  • 缺点:仅适用于小对象分配,不适合大内存分配。
适用场景:现代 OS 的内核小对象分配(Linux/Windows),是伙伴系统的上层优化。
算法实现思路

Slab 分配器是伙伴系统的上层封装与优化,针对内核小对象(PCB、文件描述符、套接字结构等)频繁分配释放的场景,核心设计严格遵循 Linux Slab 原理,同时兼顾教学可读性:

  1. 层级依赖底层 由伙伴系统管理物理内存,为 Slab 分配器提供大的连续内存块 (用于创建 slab);上层 Slab 分配器将大内存块划分为固定大小的小对象,构建内存池,供内核小对象分配使用。
  2. 核心数据结构
    • Slab 类:封装单个 slab 的属性,包含所属对象大小、slab 起始地址、总对象数、空闲对象偏移列表、slab 状态(满 / 部分空闲 / 空闲),是内存池的基本单位。
    • Slab 分配器核心字典size_slab_mgr键为预定义的小对象大小 (如 8/16/32 字节),值为该大小的内存池管理结构(包含满、部分空闲、空闲三种状态的 slab 列表,以及单个对象大小、每个 slab 的对象数等元信息),实现固定大小内存池的快速索引。
  3. Slab 三态管理 (Linux 经典设计):
    • 满 slab:slab 内所有对象均被分配,无空闲;
    • 部分空闲 slab :slab 内有部分对象空闲,分配时优先从这里取对象(核心优化,避免频繁创建新 slab);
    • 空闲 slab:slab 内所有对象均空闲,释放对象后若 slab 变为全空闲则移入此列表,可缓存复用或回收给伙伴系统。
  4. 预定义内存池 :初始化时指定支持的固定小对象大小列表(如 8、16、32、64 字节,均为内核常用小对象大小),每个大小对应一个独立内存池,避免跨大小分配的碎片问题。
  5. 分配核心逻辑 (快分配的关键):
    • 对指定大小的对象,优先从「部分空闲 slab」中取首个空闲对象(直接操作列表,O (1) 时间);
    • 若无部分空闲 slab,从「空闲 slab」中取一个激活为部分空闲,再取对象;
    • 若既无部分空闲也无空闲 slab,从伙伴系统分配大内存创建新 slab,初始化空闲对象列表,加入部分空闲 slab 后取对象。
  6. 释放核心逻辑 (快释放 + 对象缓存的关键):
    • 根据释放对象的归属信息(所属 slab、对象大小),将对象放回对应 slab 的空闲列表
    • 动态更新 slab 状态:若释放后 slab全空闲 ,从部分空闲移入空闲列表(缓存复用);若释放前 slab 是状态,从满移入部分空闲列表;
    • 释放的对象始终留在内存池中,无需频繁调用伙伴系统回收,实现对象缓存
  7. 核心特点体现:分配 / 释放仅操作内存池的空闲列表(O (1) 效率),无碎片问题(固定大小对象),仅在创建新 slab 时调用伙伴系统,大幅减少底层内存操作的开销。
Python代码
python 复制代码
import math

# ===================== 复用:伙伴系统核心代码(底层物理内存支撑) =====================
def next_power_of_2(x):
    if x <= 0:
        return 1
    if (x & (x - 1)) == 0:
        return x
    return 1 << (x - 1).bit_length()

def get_buddy_addr(s, size):
    return s ^ size

def is_addr_aligned(s, size):
    return (s & (size - 1)) == 0

def buddy_allocate(buddy_free, total_mem, req_size):
    if req_size <= 0 or req_size > total_mem:
        return None
    alloc_size = next_power_of_2(req_size)
    current_size = alloc_size
    while current_size <= total_mem:
        if current_size in buddy_free and buddy_free[current_size]:
            break
        current_size <<= 1
    if current_size > total_mem:
        return None
    while current_size > alloc_size:
        split_size = current_size >> 1
        s = buddy_free[current_size].pop(0)
        if not buddy_free[current_size]:
            del buddy_free[current_size]
        buddy1, buddy2 = s, s + split_size
        if split_size not in buddy_free:
            buddy_free[split_size] = []
        buddy_free[split_size].extend([buddy1, buddy2])
        buddy_free[split_size].sort()
        current_size = split_size
    alloc_start = buddy_free[current_size].pop(0)
    if not buddy_free[current_size]:
        del buddy_free[current_size]
    return alloc_start

def buddy_free_mem(buddy_free, total_mem, s, size):
    def merge(s, size):
        if size >= total_mem:
            if size not in buddy_free:
                buddy_free[size] = []
            buddy_free[size].append(s)
            buddy_free[size].sort()
            return
        buddy_s = get_buddy_addr(s, size)
        if size in buddy_free and buddy_s in buddy_free[size]:
            buddy_free[size].remove(buddy_s)
            if not buddy_free[size]:
                del buddy_free[size]
            merge(min(s, buddy_s), size << 1)
        else:
            if size not in buddy_free:
                buddy_free[size] = []
            buddy_free[size].append(s)
            buddy_free[size].sort()
    if not is_addr_aligned(s, size) or (size & (size - 1)) != 0 or size > total_mem:
        return False
    merge(s, size)
    return True

# ===================== 新增:Slab分配器核心实现(上层内存池) =====================
class Slab:
    """
    Slab类:封装单个slab的属性和状态,是内存池的基本单位
    每个slab对应一种固定大小的小对象,包含若干相同的对象
    """
    # Slab状态枚举
    FULL = "满"
    PARTIAL = "部分空闲"
    EMPTY = "空闲"

    def __init__(self, obj_size, slab_start, slab_size):
        self.obj_size = obj_size  # 该slab对应的单个小对象大小
        self.slab_start = slab_start  # slab的物理起始地址(从伙伴系统分配)
        self.slab_size = slab_size    # slab的总大小(2的幂次,从伙伴系统分配)
        self.total_objs = slab_size // obj_size  # 每个slab的总对象数(整数除法,教学简化)
        # 空闲对象偏移列表:记录slab内空闲对象的相对偏移(相对于slab_start),初始为所有对象偏移
        self.free_objs = [i * obj_size for i in range(self.total_objs)]
        # 初始化状态:无对象分配,为空闲
        self.status = Slab.EMPTY if self.free_objs else Slab.FULL

    def alloc_obj(self):
        """从slab中分配一个对象,返回对象的物理起始地址,更新slab状态"""
        if not self.free_objs:
            self.status = Slab.FULL
            return None
        # 取首个空闲对象的偏移,计算物理地址
        obj_offset = self.free_objs.pop(0)
        obj_addr = self.slab_start + obj_offset
        # 更新状态:无空闲则为满,否则为部分空闲
        self.status = Slab.FULL if not self.free_objs else Slab.PARTIAL
        return obj_addr

    def free_obj(self, obj_addr):
        """将对象释放回slab,传入对象物理地址,更新slab状态,返回是否成功"""
        # 计算对象相对于slab的偏移,校验合法性
        obj_offset = obj_addr - self.slab_start
        if obj_offset < 0 or obj_offset >= self.slab_size or obj_offset % self.obj_size != 0:
            return False
        # 若对象已空闲,直接返回
        if obj_offset in self.free_objs:
            return True
        # 将对象偏移放回空闲列表,排序保证有序
        self.free_objs.append(obj_offset)
        self.free_objs.sort()
        # 更新状态:全空闲则为空闲,否则为部分空闲
        self.status = Slab.EMPTY if len(self.free_objs) == self.total_objs else Slab.PARTIAL
        return True

    def is_empty(self):
        """判断slab是否为全空闲"""
        return self.status == Slab.EMPTY

    def is_full(self):
        """判断slab是否为满"""
        return self.status == Slab.FULL

class SlabAllocator:
    """
    Slab分配器核心类:基于伙伴系统的上层内存池,管理固定大小的小对象内存池
    核心:为每种固定大小的小对象维护slab三态列表(满/部分空闲/空闲)
    """
    def __init__(self, buddy_free, buddy_total_mem, slab_sizes=[8,16,32,64], single_slab_size=64):
        """
        初始化Slab分配器
        :param buddy_free: 伙伴系统空闲字典 | buddy_total_mem: 伙伴系统总内存
        :param slab_sizes: 预定义的支持的小对象大小列表(内核常用小对象大小)
        :param single_slab_size: 单个slab的大小(从伙伴系统分配,必须是2的幂次)
        """
        self.buddy_free = buddy_free  # 底层伙伴系统空闲字典
        self.buddy_total_mem = buddy_total_mem  # 伙伴系统总内存
        self.slab_sizes = sorted(slab_sizes)  # 预定义小对象大小(升序)
        self.single_slab_size = next_power_of_2(single_slab_size)  # 单个slab大小(保证2的幂次)
        self.size_slab_mgr = {}  # 核心:对象大小→slab管理结构的映射
        self.alloced_objs = {}   # 记录已分配的对象:obj_addr → (obj_size, slab),用于释放时定位
        # 初始化每种固定大小的内存池
        for obj_size in self.slab_sizes:
            self.size_slab_mgr[obj_size] = {
                "full_slabs": [],    # 满slab列表
                "partial_slabs": [], # 部分空闲slab列表(分配优先)
                "empty_slabs": [],   # 空闲slab列表(缓存复用)
                "obj_size": obj_size,
                "slab_per_obj": self.single_slab_size // obj_size  # 每个slab的对象数
            }
        print(f"✅ Slab分配器初始化完成")
        print(f"   支持小对象大小:{self.slab_sizes} 字节")
        print(f"   单个slab大小:{self.single_slab_size} 字节(从伙伴系统分配)")
        print(f"   每个slab可容纳对象数:{ {s:self.single_slab_size//s for s in self.slab_sizes} }")

    def _create_new_slab(self, obj_size):
        """
        从伙伴系统分配内存,创建一个新的slab
        :param obj_size: 要创建的slab对应的对象大小
        :return: 新创建的Slab对象,失败返回None
        """
        if obj_size not in self.size_slab_mgr:
            return None
        # 从伙伴系统分配单个slab的大小(2的幂次)
        slab_start = buddy_allocate(self.buddy_free, self.buddy_total_mem, self.single_slab_size)
        if slab_start is None:
            print(f"❌ 创建slab失败:伙伴系统无足够内存(需要{self.single_slab_size}字节)")
            return None
        # 创建新slab对象
        new_slab = Slab(obj_size, slab_start, self.single_slab_size)
        print(f"✅ 从伙伴系统分配{self.single_slab_size}字节,创建新slab:地址{slab_start},对应{obj_size}字节对象,可容纳{new_slab.total_objs}个")
        return new_slab

    def alloc(self, obj_size):
        """
        从内存池分配一个固定大小的小对象
        :param obj_size: 要分配的对象大小(必须是预定义的大小)
        :return: 分配成功返回对象物理地址,失败返回None
        """
        # 校验:对象大小是否为预定义的小对象大小
        if obj_size not in self.size_slab_mgr:
            print(f"❌ 分配失败:不支持{obj_size}字节对象,仅支持{self.slab_sizes}字节")
            return None
        mgr = self.size_slab_mgr[obj_size]
        obj_addr = None

        # 步骤1:优先从【部分空闲slab】分配(核心优化,O(1))
        if mgr["partial_slabs"]:
            target_slab = mgr["partial_slabs"][0]
            obj_addr = target_slab.alloc_obj()
            # 若分配后slab变满,移到满slab列表
            if target_slab.is_full():
                mgr["partial_slabs"].pop(0)
                mgr["full_slabs"].append(target_slab)
            print(f"🔸 从{obj_size}字节部分空闲slab分配对象,地址{obj_addr}")

        # 步骤2:无部分空闲slab,从【空闲slab】中取一个激活
        if obj_addr is None and mgr["empty_slabs"]:
            target_slab = mgr["empty_slabs"].pop(0)
            obj_addr = target_slab.alloc_obj()
            # 激活后加入部分空闲slab列表
            if not target_slab.is_full():
                mgr["partial_slabs"].append(target_slab)
            else:
                mgr["full_slabs"].append(target_slab)
            print(f"🔸 从{obj_size}字节空闲slab激活并分配对象,地址{obj_addr}")

        # 步骤3:无空闲slab,创建新slab并分配
        if obj_addr is None:
            new_slab = self._create_new_slab(obj_size)
            if new_slab is None:
                return None
            obj_addr = new_slab.alloc_obj()
            # 将新slab加入对应状态的列表
            if new_slab.is_full():
                mgr["full_slabs"].append(new_slab)
            else:
                mgr["partial_slabs"].append(new_slab)

        # 记录已分配的对象信息,用于释放时定位
        if obj_addr:
            self.alloced_objs[obj_addr] = (obj_size, mgr["partial_slabs"][-1] if mgr["partial_slabs"] else mgr["full_slabs"][-1])
        return obj_addr

    def free(self, obj_addr):
        """
        释放一个小对象,放回对应的内存池
        :param obj_addr: 要释放的对象物理地址
        :return: 释放成功返回True,失败返回False
        """
        # 校验:对象是否已分配
        if obj_addr not in self.alloced_objs:
            print(f"❌ 释放失败:对象地址{obj_addr}未分配")
            return False
        obj_size, target_slab = self.alloced_objs.pop(obj_addr)
        mgr = self.size_slab_mgr[obj_size]

        # 释放对象回slab
        if not target_slab.free_obj(obj_addr):
            print(f"❌ 释放失败:对象地址{obj_addr}非法")
            return False

        # 动态更新slab在三态列表中的位置
        # 情况1:释放后slab变为空闲,从原列表移到空闲列表
        if target_slab.is_empty():
            if target_slab in mgr["partial_slabs"]:
                mgr["partial_slabs"].remove(target_slab)
            elif target_slab in mgr["full_slabs"]:
                mgr["full_slabs"].remove(target_slab)
            mgr["empty_slabs"].append(target_slab)
            print(f"🔹 释放{obj_size}字节对象{obj_addr},slab变为空闲,移入空闲列表")
        # 情况2:释放前slab是满,现在变为部分空闲,从满列表移到部分空闲列表
        elif target_slab.status == Slab.PARTIAL and target_slab in mgr["full_slabs"]:
            mgr["full_slabs"].remove(target_slab)
            mgr["partial_slabs"].append(target_slab)
            print(f"🔹 释放{obj_size}字节对象{obj_addr},slab从满变为部分空闲")
        else:
            print(f"🔹 释放{obj_size}字节对象{obj_addr},放回部分空闲slab")
        return True

    def print_status(self):
        """格式化打印Slab分配器的内存池状态,展示各大小的slab三态分布"""
        print("\n" + "="*90)
        print("📊 Slab分配器内存池状态(小对象固定大小池)")
        print("="*90)
        print(f"{'对象大小(字节)':<15}{'满slab数':<10}{'部分空闲slab数':<15}{'空闲slab数':<10}{'总slab数':<10}")
        print("-"*90)
        total_slab = 0
        for obj_size in self.slab_sizes:
            mgr = self.size_slab_mgr[obj_size]
            f = len(mgr["full_slabs"])
            p = len(mgr["partial_slabs"])
            e = len(mgr["empty_slabs"])
            t = f + p + e
            total_slab += t
            print(f"{obj_size:<15}{f:<10}{p:<15}{e:<10}{t:<10}")
        print("-"*90)
        print(f"{'总计':<15}{'':<10}{'':<15}{'':<10}{total_slab:<10}")
        print(f"📌 已分配对象数:{len(self.alloced_objs)} | 底层伙伴系统总内存:{self.buddy_total_mem}字节")
        print("="*90 + "\n")

# ===================== 完整场景测试用例 =====================
if __name__ == "__main__":
    # 步骤1:初始化底层伙伴系统(总内存256字节,2^8,适合测试)
    BUDDY_TOTAL_MEM = 256
    buddy_free = {BUDDY_TOTAL_MEM: [0]}
    print("===== 步骤1:初始化底层伙伴系统(物理内存支撑) =====")
    print(f"伙伴系统总内存:{BUDDY_TOTAL_MEM}字节,初始空闲:[{0}]({BUDDY_TOTAL_MEM}字节)")

    # 步骤2:初始化上层Slab分配器
    # 支持8/16/32/64字节小对象,单个slab64字节(从伙伴系统分配)
    slab_allocator = SlabAllocator(
        buddy_free=buddy_free,
        buddy_total_mem=BUDDY_TOTAL_MEM,
        slab_sizes=[8,16,32,64],
        single_slab_size=64
    )
    slab_allocator.print_status()

    # 步骤3:模拟内核小对象频繁分配(模拟PCB(32B)、文件描述符(16B)、套接字(8B))
    print("===== 步骤2:模拟内核小对象频繁分配 =====")
    alloc_tasks = [
        (32, "PCB"), (16, "文件描述符"), (8, "套接字"), (32, "PCB"),
        (16, "文件描述符"), (8, "套接字"), (64, "内核缓冲区"), (32, "PCB")
    ]
    alloced_addrs = []  # 记录分配的对象地址
    for obj_size, obj_name in alloc_tasks:
        addr = slab_allocator.alloc(obj_size)
        if addr:
            alloced_addrs.append(addr)
            print(f"✅ 分配{obj_name}({obj_size}字节),地址:{addr}")
        else:
            print(f"❌ 分配{obj_name}({obj_size}字节)失败")
    slab_allocator.print_status()

    # 步骤4:模拟小对象释放(释放部分对象,体现缓存复用和slab状态变化)
    print("===== 步骤3:模拟小对象释放(释放前4个分配的对象) =====")
    for addr in alloced_addrs[:4]:
        slab_allocator.free(addr)
    slab_allocator.print_status()

    # 步骤5:模拟对象缓存复用(再次分配相同大小对象,从部分空闲/空闲slab取,无需创建新slab)
    print("===== 步骤4:模拟对象缓存复用(再次分配PCB(32B)和套接字(8B)) =====")
    for obj_size, obj_name in [(32, "新PCB"), (8, "新套接字")]:
        addr = slab_allocator.alloc(obj_size)
        if addr:
            print(f"✅ 复用内存池分配{obj_name}({obj_size}字节),地址:{addr}")
        else:
            print(f"❌ 分配{obj_name}({obj_size}字节)失败")
    slab_allocator.print_status()

    # 步骤6:释放所有对象,体现slab全部变为空闲缓存
    print("===== 步骤5:释放所有剩余对象,slab变为空闲缓存 =====")
    for addr in list(slab_allocator.alloced_objs.keys()):
        slab_allocator.free(addr)
    slab_allocator.print_status()

    print("===== Slab内存池分配算法(Linux Slab实现)演示完成 =====")
    print("📝 核心结论:小对象分配/释放仅操作内存池空闲列表,无需频繁调用伙伴系统,效率极快且无碎片!")
程序运行结果展示
bash 复制代码
===== 步骤1:初始化底层伙伴系统(物理内存支撑) =====
伙伴系统总内存:256字节,初始空闲:[0](256字节)
✅ Slab分配器初始化完成
   支持小对象大小:[8, 16, 32, 64] 字节
   单个slab大小:64 字节(从伙伴系统分配)
   每个slab可容纳对象数:{8: 8, 16: 4, 32: 2, 64: 1}

==========================================================================================
📊 Slab分配器内存池状态(小对象固定大小池)
==========================================================================================
对象大小(字节)       满slab数    部分空闲slab数      空闲slab数   总slab数    
------------------------------------------------------------------------------------------
8              0         0              0         0         
16             0         0              0         0         
32             0         0              0         0         
64             0         0              0         0         
------------------------------------------------------------------------------------------
总计                                                0         
📌 已分配对象数:0 | 底层伙伴系统总内存:256字节
==========================================================================================

===== 步骤2:模拟内核小对象频繁分配 =====
✅ 从伙伴系统分配64字节,创建新slab:地址0,对应32字节对象,可容纳2个
❌ 分配PCB(32字节)失败
✅ 从伙伴系统分配64字节,创建新slab:地址64,对应16字节对象,可容纳4个
✅ 分配文件描述符(16字节),地址:64
✅ 从伙伴系统分配64字节,创建新slab:地址128,对应8字节对象,可容纳8个
✅ 分配套接字(8字节),地址:128
🔸 从32字节部分空闲slab分配对象,地址32
✅ 分配PCB(32字节),地址:32
🔸 从16字节部分空闲slab分配对象,地址80
✅ 分配文件描述符(16字节),地址:80
🔸 从8字节部分空闲slab分配对象,地址136
✅ 分配套接字(8字节),地址:136
✅ 从伙伴系统分配64字节,创建新slab:地址192,对应64字节对象,可容纳1个
✅ 分配内核缓冲区(64字节),地址:192
❌ 创建slab失败:伙伴系统无足够内存(需要64字节)
❌ 分配PCB(32字节)失败

==========================================================================================
📊 Slab分配器内存池状态(小对象固定大小池)
==========================================================================================
对象大小(字节)       满slab数    部分空闲slab数      空闲slab数   总slab数    
------------------------------------------------------------------------------------------
8              0         1              0         1         
16             0         1              0         1         
32             1         0              0         1         
64             1         0              0         1         
------------------------------------------------------------------------------------------
总计                                                4         
📌 已分配对象数:6 | 底层伙伴系统总内存:256字节
==========================================================================================

===== 步骤3:模拟小对象释放(释放前4个分配的对象) =====
🔹 释放16字节对象64,放回部分空闲slab
🔹 释放8字节对象128,放回部分空闲slab
🔹 释放32字节对象32,slab从满变为部分空闲
🔹 释放16字节对象80,slab变为空闲,移入空闲列表

==========================================================================================
📊 Slab分配器内存池状态(小对象固定大小池)
==========================================================================================
对象大小(字节)       满slab数    部分空闲slab数      空闲slab数   总slab数    
------------------------------------------------------------------------------------------
8              0         1              0         1         
16             0         0              1         1         
32             0         1              0         1         
64             1         0              0         1         
------------------------------------------------------------------------------------------
总计                                                4         
📌 已分配对象数:2 | 底层伙伴系统总内存:256字节
==========================================================================================

===== 步骤4:模拟对象缓存复用(再次分配PCB(32B)和套接字(8B)) =====
🔸 从32字节部分空闲slab分配对象,地址32
✅ 复用内存池分配新PCB(32字节),地址:32
🔸 从8字节部分空闲slab分配对象,地址128
✅ 复用内存池分配新套接字(8字节),地址:128

==========================================================================================
📊 Slab分配器内存池状态(小对象固定大小池)
==========================================================================================
对象大小(字节)       满slab数    部分空闲slab数      空闲slab数   总slab数    
------------------------------------------------------------------------------------------
8              0         1              0         1         
16             0         0              1         1         
32             1         0              0         1         
64             1         0              0         1         
------------------------------------------------------------------------------------------
总计                                                4         
📌 已分配对象数:4 | 底层伙伴系统总内存:256字节
==========================================================================================

===== 步骤5:释放所有剩余对象,slab变为空闲缓存 =====
🔹 释放8字节对象136,放回部分空闲slab
🔹 释放64字节对象192,slab变为空闲,移入空闲列表
🔹 释放32字节对象32,slab从满变为部分空闲
🔹 释放8字节对象128,slab变为空闲,移入空闲列表

==========================================================================================
📊 Slab分配器内存池状态(小对象固定大小池)
==========================================================================================
对象大小(字节)       满slab数    部分空闲slab数      空闲slab数   总slab数    
------------------------------------------------------------------------------------------
8              0         0              1         1         
16             0         0              1         1         
32             0         1              0         1         
64             0         0              1         1         
------------------------------------------------------------------------------------------
总计                                                4         
📌 已分配对象数:0 | 底层伙伴系统总内存:256字节
==========================================================================================

===== Slab内存池分配算法(Linux Slab实现)演示完成 =====
📝 核心结论:小对象分配/释放仅操作内存池空闲列表,无需频繁调用伙伴系统,效率极快且无碎片!

六、存储器管理算法的实际 OS 应用总结

现代通用操作系统(Linux/Windows/macOS)采用 **"段页式存储管理 + 虚拟存储 + 改进型 CLOCK 页面置换 + 伙伴系统 + Slab 内存池"** 的组合方案,兼顾所有优势:

  1. 核心存储管理:段页式(分段体现进程逻辑,分页解决碎片问题);
  2. 虚拟存储:改进型 CLOCK(LRU 的轻量级实现,平衡性能和开销);
  3. 物理内存分配:伙伴系统(内核物理内存分配,按 2 的幂次分割);
  4. 小对象优化:Slab / 内存池(提高内核小对象的分配效率);
  5. 地址转换优化:快表(TLB)+ 多级页表(解决大进程页表过大的问题)。

七、总结

算法类别 核心算法 核心目标 关键优势
连续内存分配 首次 / 循环首次 / 最佳 / 最坏适应 连续分区分配 实现简单
非连续内存分配 分页 / 分段 / 段页式 解决外部碎片,提高利用率 现代 OS 基础
虚拟存储页面置换 LRU/CLOCK/OPT 降低缺页率,提升虚拟内存性能 支持大地址空间
碎片整理与回收 紧凑 / 伙伴系统 / Slab 减少碎片,提高分配效率 适配不同内存分配场景

存储器管理算法的设计核心是"权衡":在 内存利用率、分配 / 访问速度系统开销实现难度之间找到最优平衡,现代 OS 的方案正是这种权衡的最终结果。

本文系统讲解了操作系统存储器管理算法,涵盖四大类算法:连续内存分配算法(首次适应、循环首次适应、最佳适应、最坏适应)、非连续内存分配算法(分页、分段、段页式)、虚拟存储页面置换算法(OPT、FIFO、LRU、CLOCK、LFU)以及内存碎片整理与回收算法(紧凑、伙伴系统、Slab)。通过Python代码实现各类算法核心逻辑,并分析其优缺点及适用场景。现代操作系统普遍采用"段页式+改进型CLOCK+伙伴系统+Slab"的组合方案,在内存利用率、性能开销和实现难度之间取得平衡。文章还提供了完整的算法实现代码和测试用例,帮助读者深入理解存储器管理原理。

相关推荐
ZPC82102 小时前
机器人手眼标定
人工智能·python·数码相机·算法·机器人
机器学习之心HML2 小时前
PGA+MKAN+Timexer时间序列预测模型Pytorch架构
人工智能·pytorch·python
查无此人byebye2 小时前
阿里开源Wan2.2模型全面解析:MoE架构加持,电影级视频生成触手可及
人工智能·pytorch·python·深度学习·架构·开源·音视频
张书名2 小时前
基于Windows11平台的北理工校园网开机自动连接脚本
python·校园网
果粒蹬i2 小时前
降维实战:PCA与LDA在sklearn中的实现
人工智能·python·sklearn
慧都小项2 小时前
金融文档的“自主可控”:Python下实现Word到ODT的转换
python·金融·word
拓云者也2 小时前
常用的生物信息学数据库以及处理工具
数据库·python·oracle·r语言·bash
SunnyRivers2 小时前
Python 的下一代 HTTP 客户端 HTTPX 特性详解
python·httpx
hcnaisd22 小时前
机器学习模型部署:将模型转化为Web API
jvm·数据库·python