目录
[1. 首次适应算法(First Fit, FF)](#1. 首次适应算法(First Fit, FF))
[2. 循环首次适应算法(Next Fit, NF)](#2. 循环首次适应算法(Next Fit, NF))
适用场景:进程内存需求大小较均匀的多道系统,改进首次适应的碎片问题。
[3. 最佳适应算法(Best Fit, BF)](#3. 最佳适应算法(Best Fit, BF))
[4. 最坏适应算法(Worst Fit, WF)](#4. 最坏适应算法(Worst Fit, WF))
适用场景:存在大量大内存需求进程的系统,优先保证大进程的分配。
[1. 分页存储管理算法](#1. 分页存储管理算法)
适用场景:所有现代通用操作系统(Windows/Linux/UNIX)的基础内存管理方式。
[2. 分段存储管理算法](#2. 分段存储管理算法)
[适用场景:注重进程逻辑结构、模块共享和内存保护的系统(如早期 UNIX、部分嵌入式 OS)。](#适用场景:注重进程逻辑结构、模块共享和内存保护的系统(如早期 UNIX、部分嵌入式 OS)。)
[3. 段页式存储管理算法](#3. 段页式存储管理算法)
[适用场景:现代通用操作系统(Windows 10/11、Linux、macOS)的核心存储管理算法,兼顾所有优势。](#适用场景:现代通用操作系统(Windows 10/11、Linux、macOS)的核心存储管理算法,兼顾所有优势。)
[1. 最优置换算法(Optimal, OPT)](#1. 最优置换算法(Optimal, OPT))
作用:作为其他实际算法的性能基准,用于对比评价实际算法的优劣。
[2. 先进先出置换算法(First-In First-Out, FIFO)](#2. 先进先出置换算法(First-In First-Out, FIFO))
适用场景:进程页面访问序列较简单、无频繁重复访问的场景,仅用于简单系统。
[3. 最近最少使用置换算法(Least Recently Used, LRU)](#3. 最近最少使用置换算法(Least Recently Used, LRU))
[适用场景:现代 OS 的核心置换算法之一(如 Linux 的基础 LRU),兼顾性能和实现难度。](#适用场景:现代 OS 的核心置换算法之一(如 Linux 的基础 LRU),兼顾性能和实现难度。)
[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 主流))
[5. 最不常用置换算法(Least Frequently Used, LFU)](#5. 最不常用置换算法(Least Frequently Used, LFU))
[1. 内存回收算法](#1. 内存回收算法)
[2. 外部碎片整理算法](#2. 外部碎片整理算法)
适用场景:连续内存分配系统,碎片过多导致无法分配大进程时,偶尔执行(非实时)。
[3. 伙伴系统分配算法(Buddy System)](#3. 伙伴系统分配算法(Buddy System))
[适用场景:现代 OS 的内核内存分配(如 Linux 的 slab 分配器基于伙伴系统),用于分配小的内核对象。](#适用场景:现代 OS 的内核内存分配(如 Linux 的 slab 分配器基于伙伴系统),用于分配小的内核对象。)
[4. 内存池分配算法(Slab/Zone Allocator)](#4. 内存池分配算法(Slab/Zone Allocator))
[经典实现:Linux 的 Slab 分配器](#经典实现:Linux 的 Slab 分配器)
[适用场景:现代 OS 的内核小对象分配(Linux/Windows),是伙伴系统的上层优化。](#适用场景:现代 OS 的内核小对象分配(Linux/Windows),是伙伴系统的上层优化。)
[六、存储器管理算法的实际 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),核心是先分段、再分页:
- 将进程的逻辑地址空间按逻辑结构划分为若干段;
- 每个段再按固定页大小 划分为若干页;
- 物理内存按页大小划分为物理块;
- 建立段表 + 页表 :段表记录每个段的页表基址和页表长度,页表记录页号与物理块号的映射;
- 地址转换:段号→段表→页表基址→页号→页表→物理块号→物理地址(段内偏移量 = 页号 × 页大小 + 页内偏移)。
关键特点
- 无内部碎片 (分页解决),无严重外部碎片(分页的块大小固定,仅段表 / 页表占用少量空间);
- 兼顾进程逻辑结构(分段)和内存利用率(分页),支持模块共享、动态链接和内存保护;
- 地址转换需三次访问内存 (段表→页表→物理内存),依赖快表(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,页加入物理块,同时加入访问记录末尾(最新状态);
- 缺页 + 物理块满:缺页次数 + 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 的轻量级改进 ,降低实现开销,又称 **"第二次机会算法",核心是通过访问位 ** 标记页面的访问状态:
- 为每个页设置一个访问位(0/1),页面被访问时,访问位置为 1;
- 维护一个循环链表(时钟环),存放所有调入物理内存的页;
- 缺页时,从当前指针位置开始遍历循环链表:
- 若页的访问位为 0,淘汰该页,将新页调入该位置,指针后移;
- 若页的访问位为 1,将访问位重置为 0,指针后移,继续遍历;
- 遍历过程中,为所有访问位为 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(仅访问位,核心:第二次机会)
- 数据结构 :用
clock_ring列表模拟循环时钟环 ,每个元素是元组(页号, 访问位),访问位1表示近期被访问,0表示未被访问;用pointer整数记录当前遍历指针,初始为 0,通过取模实现循环。 - 核心规则 :
- 页面命中 :找到对应页,将访问位置 1(标记为近期访问,获得第二次机会);
- 缺页 + 空闲块:缺页次数 + 1,新页以
(页号, 1)加入时钟环,指针位置不变; - 缺页 + 环满:从当前指针开始循环遍历 ,遇到
访问位=1则置 0(重置机会)、指针后移;遇到访问位=0则直接淘汰该页,新页以(页号,1)替换该位置,指针后移(取模保证循环)。
- 循环实现 :指针后移公式
pointer = (pointer + 1) % len(clock_ring),完美模拟循环链表的环形遍历。
改进版 CLOCK(访问位 + 修改位,现代 OS 主流)
- 数据结构升级 :时钟环元素为元组
(页号, 访问位, 修改位),修改位1表示页面被修改(置换时需写回外存,开销大),0表示未修改(置换无写回开销)。 - 淘汰优先级(核心优化) :按置换开销从低到高 淘汰,优先级严格为:
(0,0) 未访问+未修改→(0,1) 未访问+已修改→(1,0) 已访问+未修改→(1,1) 已访问+已修改 - 遍历规则 :缺页置换时,按优先级从高到低遍历时钟环,找到第一类符合条件的页立即淘汰,无需遍历整个环,兼顾访问频率和置换开销,是 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 的核心是基于访问频率淘汰 ,为每个页面维护访问计数器,同时处理计数相同的情况,选用三结构联动实现(新手易理解,贴合经典原理):
frames:模拟物理块,存储当前驻留的页面;freq_count:字典,记录每个页面的访问次数计数器(键 = 页号,值 = 访问次数,页面被访问则 + 1);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. 内存回收算法
进程结束 / 换出时,操作系统需要回收 其占用的内存空间,并将其加入空闲分区表 / 链,核心是合并相邻的空闲分区 (又称拼接 / 紧凑),避免产生大量分散的小空闲分区。
核心步骤
- 标记进程占用的内存空间为空闲;
- 检查该空闲分区的前 / 后相邻分区 是否为空闲,若为空闲则合并为一个大的空闲分区;
- 更新空闲分区表 / 链,记录合并后的空闲分区的起始地址和大小。
实现方式
- 空闲分区表:直接修改表项的起始地址和大小,删除被合并的表项;
- 空闲分区链:修改链表节点的指针,合并相邻节点。
算法实现思路
内存回收的核心是 **"标记空闲 + 相邻合并 + 表更新",基于空闲分区表(按起始地址升序)** 实现,核心设计如下:
- 数据结构 :空闲分区表用列表存储字典 实现,每个字典代表一个空闲分区,含
start(起始地址)、size(分区大小)两个核心字段,始终按start升序排列(方便查找相邻分区); - 相邻分区判断规则 (核心):
- 前邻空闲分区:存在分区满足
前分区.start + 前分区.size == 回收分区.start; - 后邻空闲分区:存在分区满足
回收分区.start + 回收分区.size == 后分区.start;
- 前邻空闲分区:存在分区满足
- 四合一合并逻辑 :
- 无相邻:直接将回收分区作为新表项加入空闲分区表;
- 仅前邻:合并前邻分区与回收分区(新 start = 前邻 start,新 size = 前邻 size + 回收 size),删除原前邻表项;
- 仅后邻:合并回收分区与后邻分区(新 start = 回收 start,新 size = 回收 size + 后邻 size),删除原后邻表项;
- 前后均邻:合并前邻 + 回收 + 后邻三区(新 start = 前邻 start,新 size = 前邻 + 回收 + 后邻 size),删除原前、后邻表项;
- 完整流程 :模拟实际 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 最常用的实现方式),核心是进程连续重排 + 地址重定位 + 碎片彻底合并,完整步骤贴合操作系统经典原理,同时体现其核心特点:
- 数据结构设计 :分开维护两个核心表,避免混淆,贴合实际 OS 内存管理:
- 进程分区表:列表存储字典,含
pid(进程 ID)、start(物理起始地址)、size(分区大小),记录进程的内存占用; - 空闲分区表:延续之前的结构,
start+size,记录分散的外部碎片;
- 进程分区表:列表存储字典,含
- 紧凑核心步骤 (低地址端):
- 暂停所有进程:打印提示,体现紧凑算法 "进程暂停运行" 的特点;
- 进程排序:将所有进程分区按原物理起始地址升序排列,保证移动顺序的合理性;
- 地址重定位:从内存低地址
0开始,为每个进程重新分配连续的物理地址 (前一个进程的结束地址 = 后一个进程的起始地址),更新进程表的start字段,记录重定位信息; - 计算总占用:统计所有进程的总内存占用大小,确定空闲分区的起始地址(进程总占用结束地址);
- 碎片合并:清空原有分散的空闲分区表,生成一个唯一的大空闲分区(起始 = 进程总占用结束地址,大小 = 内存总大小 - 进程总占用);
- 进程恢复运行:打印提示,体现紧凑的完成;
- 开销量化 :统计紧凑过程中总移动数据量(所有进程的大小之和),体现其 "系统开销极大" 的特点;
- 完整场景模拟 :先通过多次分配 + 非连续回收制造明显的外部碎片,再执行紧凑整理,直观对比碎片前后的差异,验证算法效果。
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 的幂次划分空闲分区:
- 将物理内存划分为一个大小为 2^n的初始空闲分区(如 2^30 字节 = 1GB);
- 分配时,若当前分区大小 > 2× 进程需求,则将分区对半分割为两个大小相等的 "伙伴分区",重复分割直到分区大小≥进程需求且为 2 的幂次;
- 回收时,检查被回收的分区的伙伴分区 是否为空闲,若为空闲则合并为一个大的伙伴分区,重复合并直到无法合并。
特点
- 优点:分配 / 回收速度快(按 2 的幂次分割 / 合并,无需遍历),减少外部碎片,支持高效的内存分配;
- 缺点:存在内部碎片(分区大小为 2 的幂次,可能大于进程需求)。
适用场景:现代 OS 的内核内存分配(如 Linux 的 slab 分配器基于伙伴系统),用于分配小的内核对象。
算法实现思路
伙伴系统的核心是基于 2 的幂次的分区管理,解决连续内存分配的外部碎片问题,同时保证分配 / 回收的高效性,核心设计严格贴合 Linux 内核原理:
- 核心数据结构 :
buddy_free字典,键为 2 的幂次的空闲分区大小 (如 8、16、32),值为对应大小的空闲分区起始地址列表(地址按对应大小对齐,保证伙伴关系),这是伙伴系统高效的关键(无需遍历,直接按大小索引)。 - 前置条件 :物理内存总大小必须是2 的整数次幂 ,初始时
buddy_free中仅包含一个初始大小的键,值为[0](内存起始地址为 0)。 - 关键辅助计算 :
- 2 的幂次向上取整 :将进程的内存需求,转换为大于等于需求的最小 2 的幂次(如需求 5→8,需求 10→16),保证分区大小为 2 的幂次;
- 伙伴地址计算 :通过异或运算 快速计算(Linux 内核原生实现),对于大小
2^k、起始地址s的分区,伙伴地址 =s ^ (2^k),比加减运算更高效,且能统一处理 "前伙伴 / 后伙伴"; - 地址对齐检查 :大小
2^k的分区,起始地址必须是2^k的整数倍,保证伙伴分区的合法性。
- 分配核心步骤 :
- 计算需求的最小 2 的幂次
alloc_size; - 从
alloc_size开始,按 2 的幂次递增查找首个有空闲分区的大小; - 若找到的大小等于
alloc_size,直接分配该大小的首个空闲地址; - 若找到的大小大于
alloc_size,递归对半分割 该分区为两个伙伴分区,直到分割为alloc_size,分配其中一个,另一个加入对应大小的空闲列表; - 记录分配信息,计算内部碎片(分区大小 - 需求大小)。
- 计算需求的最小 2 的幂次
- 回收核心步骤 :
- 校验回收分区的大小(2 的幂次)、地址(对齐)、是否已分配;
- 计算当前分区的伙伴地址,检查伙伴是否为空闲(在对应大小的空闲列表中);
- 若伙伴空闲,删除伙伴地址 ,将当前分区与伙伴分区合并为更大的 2 的幂次分区(大小 ×2);
- 对合并后的大分区递归执行回收逻辑,直到伙伴不空闲,将最终的分区地址加入对应大小的空闲列表。
- 核心特点体现 :分配 / 回收时打印分割 / 合并过程 ,分配后打印内部碎片 ,回收后打印合并过程,直观体现伙伴系统的优缺点。
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 原理,同时兼顾教学可读性:
- 层级依赖 :底层 由伙伴系统管理物理内存,为 Slab 分配器提供大的连续内存块 (用于创建 slab);上层 Slab 分配器将大内存块划分为固定大小的小对象,构建内存池,供内核小对象分配使用。
- 核心数据结构 :
- Slab 类:封装单个 slab 的属性,包含所属对象大小、slab 起始地址、总对象数、空闲对象偏移列表、slab 状态(满 / 部分空闲 / 空闲),是内存池的基本单位。
- Slab 分配器核心字典 :
size_slab_mgr,键为预定义的小对象大小 (如 8/16/32 字节),值为该大小的内存池管理结构(包含满、部分空闲、空闲三种状态的 slab 列表,以及单个对象大小、每个 slab 的对象数等元信息),实现固定大小内存池的快速索引。
- Slab 三态管理 (Linux 经典设计):
- 满 slab:slab 内所有对象均被分配,无空闲;
- 部分空闲 slab :slab 内有部分对象空闲,分配时优先从这里取对象(核心优化,避免频繁创建新 slab);
- 空闲 slab:slab 内所有对象均空闲,释放对象后若 slab 变为全空闲则移入此列表,可缓存复用或回收给伙伴系统。
- 预定义内存池 :初始化时指定支持的固定小对象大小列表(如 8、16、32、64 字节,均为内核常用小对象大小),每个大小对应一个独立内存池,避免跨大小分配的碎片问题。
- 分配核心逻辑 (快分配的关键):
- 对指定大小的对象,优先从「部分空闲 slab」中取首个空闲对象(直接操作列表,O (1) 时间);
- 若无部分空闲 slab,从「空闲 slab」中取一个激活为部分空闲,再取对象;
- 若既无部分空闲也无空闲 slab,从伙伴系统分配大内存创建新 slab,初始化空闲对象列表,加入部分空闲 slab 后取对象。
- 释放核心逻辑 (快释放 + 对象缓存的关键):
- 根据释放对象的归属信息(所属 slab、对象大小),将对象放回对应 slab 的空闲列表;
- 动态更新 slab 状态:若释放后 slab全空闲 ,从部分空闲移入空闲列表(缓存复用);若释放前 slab 是满状态,从满移入部分空闲列表;
- 释放的对象始终留在内存池中,无需频繁调用伙伴系统回收,实现对象缓存。
- 核心特点体现:分配 / 释放仅操作内存池的空闲列表(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 内存池"** 的组合方案,兼顾所有优势:
- 核心存储管理:段页式(分段体现进程逻辑,分页解决碎片问题);
- 虚拟存储:改进型 CLOCK(LRU 的轻量级实现,平衡性能和开销);
- 物理内存分配:伙伴系统(内核物理内存分配,按 2 的幂次分割);
- 小对象优化:Slab / 内存池(提高内核小对象的分配效率);
- 地址转换优化:快表(TLB)+ 多级页表(解决大进程页表过大的问题)。
七、总结
| 算法类别 | 核心算法 | 核心目标 | 关键优势 |
|---|---|---|---|
| 连续内存分配 | 首次 / 循环首次 / 最佳 / 最坏适应 | 连续分区分配 | 实现简单 |
| 非连续内存分配 | 分页 / 分段 / 段页式 | 解决外部碎片,提高利用率 | 现代 OS 基础 |
| 虚拟存储页面置换 | LRU/CLOCK/OPT | 降低缺页率,提升虚拟内存性能 | 支持大地址空间 |
| 碎片整理与回收 | 紧凑 / 伙伴系统 / Slab | 减少碎片,提高分配效率 | 适配不同内存分配场景 |
存储器管理算法的设计核心是"权衡":在 内存利用率、分配 / 访问速度 、系统开销 、实现难度之间找到最优平衡,现代 OS 的方案正是这种权衡的最终结果。
本文系统讲解了操作系统存储器管理算法,涵盖四大类算法:连续内存分配算法(首次适应、循环首次适应、最佳适应、最坏适应)、非连续内存分配算法(分页、分段、段页式)、虚拟存储页面置换算法(OPT、FIFO、LRU、CLOCK、LFU)以及内存碎片整理与回收算法(紧凑、伙伴系统、Slab)。通过Python代码实现各类算法核心逻辑,并分析其优缺点及适用场景。现代操作系统普遍采用"段页式+改进型CLOCK+伙伴系统+Slab"的组合方案,在内存利用率、性能开销和实现难度之间取得平衡。文章还提供了完整的算法实现代码和测试用例,帮助读者深入理解存储器管理原理。