1. 实验背景与核心概念
动态分区分配是操作系统内存管理中的经典算法实践,我在大学讲授操作系统课程时,发现很多学生对这块内容存在理解偏差。这个实验通过模拟四种常见分配策略(首次适应、循环首次适应、最佳适应和最坏适应),帮助学生直观理解内存分配与回收的全过程。
动态分区分配的核心在于处理内存请求时,系统需要从可用分区链表中找到合适的内存块。与固定分区相比,它的优势在于避免了内部碎片,但会产生外部碎片问题。实验中我们需要重点关注:
- 分区表的维护方式(通常用双向链表实现)
- 分配策略的算法差异
- 合并相邻空闲区的时机判断
注意:实验代码建议用C语言实现,因为需要直接操作内存地址和指针,这对理解底层机制非常重要。Python等高级语言会隐藏太多细节。
2. 数据结构设计与实现
2.1 内存块结构体定义
c复制typedef struct memory_block {
int start_addr;
int size;
int process_id; // -1表示空闲
struct memory_block *prev;
struct memory_block *next;
} MemoryBlock;
这个结构体包含四个关键字段:
- 起始地址(start_addr):记录该分区在模拟内存中的起始位置
- 大小(size):分区长度(单位可以是KB或字节)
- 进程ID(process_id):标识占用该分区的进程,-1表示空闲
- 前后指针:维护分区链表关系
2.2 全局变量初始化
c复制MemoryBlock *free_list = NULL; // 空闲分区链
MemoryBlock *allocated_list = NULL; // 已分配分区链
int total_memory = 1024; // 假设总内存1MB
初始化时需要创建一个表示整个空闲内存的节点:
c复制free_list = (MemoryBlock*)malloc(sizeof(MemoryBlock));
free_list->start_addr = 0;
free_list->size = total_memory;
free_list->process_id = -1;
free_list->prev = free_list->next = NULL;
3. 分配算法实现细节
3.1 首次适应算法(First Fit)
遍历空闲链表,选择第一个足够大的分区:
c复制MemoryBlock* first_fit(int size, int pid) {
MemoryBlock *current = free_list;
while(current != NULL) {
if(current->size >= size && current->process_id == -1) {
return current; // 找到合适块
}
current = current->next;
}
return NULL; // 分配失败
}
特点:实现简单,但容易在低地址区产生碎片
3.2 最佳适应算法(Best Fit)
找到能满足要求的最小空闲块:
c复制MemoryBlock* best_fit(int size, int pid) {
MemoryBlock *best = NULL;
MemoryBlock *current = free_list;
while(current != NULL) {
if(current->size >= size && current->process_id == -1) {
if(best == NULL || current->size < best->size) {
best = current;
}
}
current = current->next;
}
return best;
}
特点:减少大空闲块被切碎的情况,但会产生大量小碎片
3.3 内存分配通用流程
无论采用哪种策略,分配成功后都需要:
- 从空闲链移除该块
- 如果剩余空间大于阈值(如1KB),分割出新的空闲块
- 将分配块加入已分配链
示例代码:
c复制void allocate_memory(MemoryBlock *block, int size, int pid) {
// 从空闲链移除
if(block->prev) block->prev->next = block->next;
if(block->next) block->next->prev = block->prev;
// 处理剩余空间
if(block->size > size + MIN_SIZE) {
MemoryBlock *new_free = create_block(
block->start_addr + size,
block->size - size,
-1
);
insert_to_free_list(new_free);
block->size = size;
}
// 设置进程ID并加入分配链
block->process_id = pid;
insert_to_allocated_list(block);
}
4. 内存回收与碎片处理
4.1 基本回收流程
当进程释放内存时:
- 在已分配链中找到对应块
- 将其process_id置为-1
- 移回空闲链
- 检查相邻块是否空闲,进行合并
关键合并函数:
c复制void merge_adjacent(MemoryBlock *block) {
// 合并后向相邻块
if(block->next && block->next->process_id == -1
&& block->start_addr + block->size == block->next->start_addr) {
block->size += block->next->size;
remove_from_list(block->next);
}
// 合并前向相邻块
if(block->prev && block->prev->process_id == -1
&& block->prev->start_addr + block->prev->size == block->start_addr) {
block->prev->size += block->size;
remove_from_list(block);
}
}
4.2 碎片整理策略
当外部碎片过多时,可考虑:
- 紧凑技术(Compaction):移动已分配块合并空闲空间
- 分区对换:将某些进程换出内存
实现紧凑的伪代码:
code复制将所有已分配块移到内存一端
更新所有进程的地址引用
合并剩余空间为单个大分区
5. 实验效果可视化
建议用以下方式展示模拟结果:
bash复制[0-100KB] 进程A
[100-200KB] 空闲
[200-300KB] 进程B
[300-1024KB] 空闲
统计指标应包括:
- 内存利用率 = 已分配内存 / 总内存
- 平均碎片大小
- 分配成功率
- 每次操作后的分区表状态
6. 常见问题与调试技巧
-
链表断裂问题
- 现象:执行合并操作后出现段错误
- 原因:未正确维护前后指针关系
- 解决:每次修改链表节点时,同步更新相邻节点的指针
-
分配失败但存在足够空间
- 检查是否忘记合并相邻空闲区
- 确认分配策略实现是否正确(特别是最佳适应算法)
-
内存泄漏检测
- 在程序退出前遍历所有链表,释放节点内存
- 使用valgrind工具检查(Linux环境下)
调试技巧:在每次操作后打印整个内存状态,包括:
- 所有空闲块的起止地址和大小
- 所有已分配块的进程ID和位置
这能快速定位逻辑错误
7. 实验扩展建议
- 实现带优先级的分配策略(系统进程优先使用低地址)
- 模拟虚拟内存的分页机制对比
- 增加图形化界面展示内存状态变化
- 统计不同策略下的碎片率随时间变化
- 实现动态内存增长/收缩的模拟
我在实际教学中发现,学生最容易混淆的是各种分配策略的适用场景。建议通过以下用例对比:
- 频繁申请释放小内存块时,最佳适应表现最差
- 长期运行的大型进程适合用最坏适应算法
- 默认情况下首次适应综合性能最好