前言
这篇文章用来记录操作系统实验之 动态分区分配与回收。
不想从网上copy代码,打算自己从头到尾写一下,没想到却花了我整整两个晚上的时间,好在终于写完了…
动态分区分配采用的算法是最佳适应(best fit, BF)算法,回收内存参考文章见后面。
一、 实验目的
深入了解动态分区存储管理方式主存分配回收的实现。
二、 实验要求
编写程序完成动态分区存储管理方式的主存分配回收的实现。实验具体包括:首先确定主存空间分配表;然后采用最优适应算法完成主存空间的分配和回收;最后编写主函数对所做工作进行测试。
三、 实验设计
1 动态分区分配中数据结构的设计
为了实现动态分区分配,系统中必须配置相应的数据结构用以描述空闲分区和已分配分区的情况,为分配提供数据。常用的数据结构有空闲分区表和空闲分区链两种方式。由于在上一次时间转轮调度算法的实现中我采用了数组进行PCB的模拟,因此这次我采用了不同的方式——链表,来模拟空闲分区链以及内存的组织。
为了实现对空闲分区的分配和链接,在每个分区的其实部分设置一些用于控制分区的信息,这些信息包括:用于唯一标识每个分区的标识——便于后续分区的回收以及分配,分区的状态——空闲抑或是装满。同时在每个分区设置链接各分区的前向指针,在分区尾部则设置一后向指针。通过前、后链接指针,将所有空闲分区链接成一个双指针。
struct Node{ // 空闲分区链结点 and 已使用分区链结点
int p_id; // 当前空闲分区结点的id,唯一
long address, size;
int status;
struct Node *pre, *next;
};
基于上面定义的分区结点的数据结构,分别定义用于链接空闲分区的空闲分区链、用于链接已使用分区的分区链以及代表整块内存的内存链如下。
struct Node *usedLink, *freeLink, *initMemory;
2 内存状态的定义
系统的内存块会面对两种状态:装满以及空闲,因此定义两个状态标识FREE和FULL来分别代表这两种状态。
#define FREE 0 // 内存空闲状态
#define FULL 1 // 内存占满状态
3 动态分区分配算法的设计
基于顺序搜索动态分区分配采用的算法可以有四种:首次适应算法,循环首次适应算法,最佳适应算法以及最坏适应算法。本次实验我对与最佳适应算法进行模拟。
所谓“最佳”是指,每次为作业分配内存时,总是把能够满足要求,又是最小的空闲分区分配给作业,避免“大材小用”。为了加速查找,本算法将所有的空闲分区按照其容量从小到大的顺序形成一空闲分区链,这样,第一次找到的能够满足要求的空闲区必然是最佳的。
在内存的分配上,由于本代码采用C语言并基于指针实现,因此会调用C库函数中的malloc进行内存分配。若此方法申请内存失败,则报告给用户并退出程序。同时,分配的内存大小还应该满足低于最大内存限制,若大小总和超过内存尾地址,依然给出报错信息。
本程序事先规定允许的最小剩余片段大小MIN_MEMORY_DIFFER,即若空闲结点大小减掉程序片大小低于此阈值时,将此空闲节点状态变为FULL不再切割,这样做是为了防止内存过于碎片化,因为过小的内存很难装入数据或程序。在将程序或数据装入到空闲分区链对应结点时,面对两种情况:
(1) 装入此程序或数据后剩余容量为零或者低于阈值MIN_MEMORY_DIFFER,此时将该内存结点加入已使用分区链。
(2) 装入后未装满,此时链结状态仍然为FREE,在修改该内存结点大小后,按照空间大小顺序将此结点插入到空闲分区链中。
void bestFitAlloc(long prgSize){ // 最佳适应算法
struct Node *p = freeLink->next, *preNode = freeLink;
// 查找到应当存入的内存块
while(p && prgSize > p->size){
p = p->next;
preNode = preNode->next;
}
if(!p){
printf("程序大于最大内存片长度,无法装入!");
return;
}
p->size = p->size - prgSize;
if(!p->size || p->size < MIN_MEMORY_DIFFER) // 装满
{
p->status = FULL;
preNode->next = p->next;
if(p->next)
p->next->pre = preNode;
struct Node *q = usedLink;
p->status = FULL;
p->next = q->next;
if(q->next)
q->next->pre = p;
q->next = p;
p->pre = q;
}
else{
preNode->next = p->next;
if(p->next)
p->next->pre = preNode;
sortLink(p);
}
printf("freeLink:\n");
displayLink(freeLink);
printf("usedLink:\n");
displayLink(usedLink);
}
考虑链表的特点,这里采用插入排序作为排序算法,比较的值为每个链结存储的内存空间的大小。若每次都对整个空闲分区链排序,效率势必很低,因此我只将定位到的内存片减去程序片大小后"摘下",然后重新对这个单个结点进行插入排序,这样每次插入的时间复杂度便由 O ( N 2 ) O(N^2) O(N2)变为了 O ( N ) O(N) O(N).
void sortLink(struct Node *temp){ // 插入排序构造最优适应的空闲分区链
struct Node *p = freeLink->next;
struct Node *preNode = freeLink;
// 如果是首次添加结点,无需排序
if(!p){
freeLink->next = temp;
temp->pre = freeLink;
return;
}
// 按照空闲分区容量从小到大进行插入排序,获取第一个需要插入的位置
while(p && temp->size > p->size){
p = p->next;
preNode = preNode->next;
}
temp->next = p;
if(p)
p->pre = temp;
preNode->next = temp;
temp->pre = preNode;
}
4 内存回收算法的实现
当进程运行完毕释放内存时,系统根据回收区的首址,从空闲分区链中找到相应的插入点,此时可能出现以下四种情况之一:
(1) 回收区与插入点的前一个空闲分区F1相邻接,如图3-1(a) 。此时应该将回收区与插入点的前一分区合并,不必为回收区分配新表项,而只需修改其前一个分区F1的大小。
(2) 回收区与插入点的后一个空闲分区F2相邻接,如图3-1(b) 。此时也可将两分区合并,形成新的分区,但用回收区的首址作为新空闲区的首址,大小为两者之和。
(3) 回收区同时与插入点的前后两个分区邻接,如图3-1(c)。此时将三个分区合并,使用F1的表项和F1的首址,取消F2的表项,大小为三者之和。
(4) 回收区既不与F1邻接,又不与F2邻接,如图3-1(d)。这时应该为回收区单独建立一个新表项,填写回收区的首址和大小,并根据其首址插入到空闲链中的适当位置。

首先设计出内存回收算法的“查找”部分,这部分被用来查找指定回收的分区id。若未找到该id对应的分区,则返回主程序并向用户报告错误。“查找”部分代码如下。
// 使用initMemory 以及 freeLink
// 将从initMemory 回收的内存链结 使用sortLink() 方法插入到空闲分区链 freeLink 中
// 检索整个initMemory,找到p_id 所在的内存,判断与之邻接的上下两块内存的状态
int find = 0;
struct Node *p = initMemory->next;
struct Node *copyNode = (struct Node*)malloc(sizeof(struct Node));
while(p){
if(p->p_id == p_id){
find = 1;
break;
}
p = p->next;
}
if(!find){
printf("未找到指定 id 的内存!");
return;
}
然后是根据内存回收可能面对的四种情况的分别处理。
// 找到对应id的内存结点,内存指针为 p,获取邻接的两块内存的状态
struct Node *freeHead = freeLink->next;
struct Node *preNode = p->pre, *nextNode = p->next;
int preNodeStatus = preNode->status, nextNodeStatus = nextNode->status;
if(preNodeStatus < nextNodeStatus){ // 前空闲后满
printf("\n回收区 %d 与插入点的前一个空闲分区相邻接\n", p->p_id);
// 只修改前一个分区的大小,删除定位到的这个分区
preNode->size += p->size;
preNode->next = nextNode;
if(nextNode)
nextNode->pre = preNode;
free(p);
}
else if(preNodeStatus > nextNodeStatus){ // 后空闲前满
// 只修改后一个分区的大小,删除定位到的这个分区
printf("\n回收区 %d 与插入点的后一个空闲分区相邻接\n", p->p_id);
preNode->next = nextNode;
if(nextNode){
nextNode->size += p->size;
nextNode->address = p->address;
nextNode->pre = preNode;
}
if(nextNode==freeLink->next)
return;
free(p);
}
else if(!nextNodeStatus){ // 前后均不满
// 将相邻的三个分区合并
printf("\n回收区 %d 既不与前一个分区邻接,又不与后一个分区相邻接\n", p->p_id);
preNode->size += p->size;
if(nextNode)
preNode->size += nextNode->size;
preNode->next = NULL;
free(p);
free(nextNode);
}
else{ // 前后均满
// 为回收区单独建立表项,填写回收区首址及大小,并根据回收区大小插入到空闲分区链的适当位置
printf("\n回收区 %d 同时与插入点的前、后两个分区邻接\n", p->p_id);
struct Node *temp = (struct Node *)malloc(sizeof(struct Node));
temp->pre = temp->next = NULL;
temp->address = p->address;
temp->size = p->size;
temp->p_id = p->p_id;
temp->status = FREE;
p->status = FREE;
}
至此便已完成动态分区分配及内存回收的核心部分。其他诸如用户接口、分配函数调用、回收函数调用、分区链显示等函数将在整体代码中展示。
四、 实验结果
为便于连续验证本代码中内存分配模块与回收模块的正确性,首先由用户指定整体内存块的起始地址及其所占空间的大小。由于最初内存中并无任何进程,因此整体内存块均为FREE的状态,此时空闲分区链指向整块内存,首地址为整块内存的首地址,大小亦为整块内存的大小。
而后由用户指定进程数量及进程相关信息(首址、大小等),在内存中自上而下顺序分配内存块。

接着调用内存回收模块,指定要回收的内存标识id,在内存链中查找到标识为该id的链结,判断其与邻接内存结点的状态,根据不同状态分别调用不同回收算法。

此时空闲分区链上会有多个不同的分区结点。最后再次由用户指定装入内存的进程数量,由于空闲分区链上已有多个不同的空闲分区结点,因此调用最佳适应算法,根据空闲分区链中各结点存放的空间大小,将进程装入合适内存块,并改变其内存块大小。若装入后内存块满或低于指定的碎片大小阈值,则改变其状态为FULL,否则将变更大小后的分区结点重新插入到空闲分区链的合适位置。

五、总结
本次实验使我深入了解了动态分区存储管理方式主存分配回收的实现。最初觉得动态分区分配相关的算法及内存块的回收理解起来很容易,但真正编程的时候却遇到了瓶颈。由于空闲分区表实现起来较为简单,但我又偏想使用分区链的方式管理已使用内存分区以及空闲分区,因此编程时采用了后者。
我采用了最佳适应算法来实现本次动态分区分配实验的分配部分。这部分并没有什么难度,由于想要使程序具有较好的模块化,插入排序算法可以被不同模块应用,因此我将最佳适应算法中内存结点选择部分与插入排序部分分离开,每找到并更改一次空闲分区结点的大小后,若其状态仍未空闲,都将该结点从空闲分区链中摘下,并根据其所占内存空间大小重新插入到分区链的合适位置。这样可以有效的将排序操作的时间复杂度由
O
(
N
2
)
O(N^2)
O(N2) 优化到
O
(
N
)
O(N)
O(N)。
对于内存的回收,则参考了所学教材《连续分配存储管理方式》[1] 一节,在内存的释放过程中,遇到了很多bug,但我最终清晰了解了内存回收的四种情况及其处理方式的原理,使得内存回收部分暂无错误之虞。
六、参考文献
[1] 汤小丹,哲凤屏,等. 计算机操作系统(第四版)[M]. 西安电子科技大学出版社,2018.
附录
#include<stdio.h>
#define FREE 0 // 内存空闲状态
#define FULL 1 // 内存占满状态
#define MAX_ALLOC_SIZE 65536 // 假设系统允许最大分配空间的大小
#define MIN_MEMORY_DIFFER 2 // 防止碎片化太过严重,定义最小差值(空闲区-程序片大小)
struct Node{ // 空闲分区链结点 and 已使用分区链结点
int p_id; // 当前空闲分区结点的id,唯一
long address, size;
int status;
struct Node *pre, *next;
};
struct Node *usedLink, *freeLink, *initMemory;
long start_add, end_add, memory_size; // 起始地址,终止地址,内存大小
int initLink();
char *getStatus(int status);
struct Node *deepCopyNode(struct Node *p1, struct Node *p2);
void insertNode(struct Node *link, struct Node *p);
void sortLink(struct Node *temp);
void memoryRecovery(int p_id);
void bestFitAlloc(long prgSize);
void callAllocAlgorithm();
void callRecoveryAlgorithm();
void userInterface();
void displayLink(struct Node *head);
int initLink(){
// 空闲分区链 & 已使用分区链 , 说明:带有头结点,方便后续操作
usedLink = (struct Node *)malloc(sizeof(struct Node));
freeLink = (struct Node *)malloc(sizeof(struct Node));
initMemory = (struct Node *)malloc(sizeof(struct Node));
if(!freeLink || !usedLink || !initMemory){
printf("空间申请失败!");
return 0;
}
// 初始化已使用分区链表
usedLink->p_id = -1;
usedLink->size = 0;
usedLink->address = 0;
usedLink->pre = usedLink->next = NULL;
usedLink->status = FULL;
// 初始化空闲分区链表
freeLink->p_id = -1;
freeLink->size = 0;
freeLink->address = 0;
freeLink->pre = freeLink->next = NULL;
freeLink->status = FULL;
// 初始化内存空间
initMemory->p_id = -1;
initMemory->size = 0;
initMemory->address = 0;
initMemory->pre = initMemory->next = NULL;
initMemory->status = FULL;
return 1;
}
char *getStatus(int status){
return status ? "占满" : "空闲";
}
struct Node *deepCopyNode(struct Node *p1, struct Node *p2){ // 对节点进行深拷贝
p1->next = p1->pre = NULL;
p1->address = p2->address;
p1->p_id = p2->p_id;
p1->size = p2->size;
p1->status = p2->status;
return p1;
}
void insertNode(struct Node *link, struct Node *p){
link->next = p;
p->pre = link;
}
void sortLink(struct Node *temp){ // 插入排序构造最优适应的空闲分区链
// 带头结点,因此此处 freeLink 一定非空
struct Node *p = freeLink->next;
struct Node *preNode = freeLink;
// 如果是首次添加结点,无需排序
if(!p){
freeLink->next = temp;
temp->pre = freeLink;
return;
}
// 按照空闲分区容量从小到大进行插入排序,获取第一个需要插入的位置
while(p && temp->size > p->size){
p = p->next;
preNode = preNode->next;
}
temp->next = p;
if(p)
p->pre = temp;
preNode->next = temp;
temp->pre = preNode;
// printf("111\n");
}
void memoryRecovery(int p_id){ // 回收内存
// 使用initMemory 以及 freeLink
// 将从initMemory 回收的内存链结 使用sortLink() 方法插入到空闲分区链 freeLink 中
// 检索整个initMemory,找到p_id 所在的内存,判断与之邻接的上下两块内存的状态
int find = 0;
struct Node *p = initMemory->next;
struct Node *copyNode = (struct Node*)malloc(sizeof(struct Node));
while(p){
if(p->p_id == p_id){
// do something
find = 1;
break;
}
p = p->next;
}
if(!find){
printf("未找到指定 id 的内存!");
return;
}
// 找到对应id的内存结点,内存指针为 p,获取邻接的两块内存的状态
struct Node *freeHead = freeLink->next;
struct Node *preNode = p->pre, *nextNode = p->next;
int preNodeStatus = preNode->status, nextNodeStatus = nextNode->status;
// printf("%d\n%\d\n", nextNodeStatus, preNodeStatus);
if(preNodeStatus < nextNodeStatus){ // 前空闲后满
printf("\n前空闲后满\n");
// 只修改前一个分区的大小,删除定位到的这个分区
preNode->size += p->size;
preNode->next = nextNode;
if(nextNode)
nextNode->pre = preNode;
free(p);
}
else if(preNodeStatus > nextNodeStatus){ // 后空闲前满
// 只修改后一个分区的大小,删除定位到的这个分区
printf("\n后空闲前满\n");
preNode->next = nextNode;
if(nextNode){
nextNode->size += p->size;
nextNode->address = p->address;
nextNode->pre = preNode;
}
if(nextNode==freeLink->next)
return;
free(p);
}
else if(!nextNodeStatus){ // 前后均不满
// 将相邻的三个分区合并
printf("\n前后均不满\n");
preNode->size += p->size;
if(nextNode)
preNode->size += nextNode->size;
preNode->next = NULL;
free(p);
free(nextNode);
}
else{ // 前后均满
// 为回收区单独建立表项,填写回收区首址及大小,并根据回收区大小插入到空闲分区链的适当位置
printf("\n前后均满\n");
struct Node *temp = (struct Node *)malloc(sizeof(struct Node));
temp->pre = temp->next = NULL;
temp->address = p->address;
temp->size = p->size;
temp->p_id = p->p_id;
temp->status = FREE;
p->status = FREE;
}
}
void bestFitAlloc(long prgSize){ // 最佳适应算法
struct Node *p = freeLink->next, *preNode = freeLink;
// 查找到应当存入的内存块
while(p && prgSize > p->size){
p = p->next;
preNode = preNode->next;
}
if(!p){
printf("程序大于最大内存片长度,无法装入!");
return;
}
p->size = p->size - prgSize;
if(!p->size || p->size < MIN_MEMORY_DIFFER) // 装满
{
p->status = FULL;
preNode->next = p->next;
if(p->next)
p->next->pre = preNode;
struct Node *q = usedLink;
p->status = FULL;
p->next = q->next;
if(q->next)
q->next->pre = p;
q->next = p;
p->pre = q;
}
else{
preNode->next = p->next;
if(p->next)
p->next->pre = preNode;
// 若每次都对整个空闲分区链排序,效率势必很低
// 因此我只将定位到的内存片减去程序片大小后"摘下",然后重新对这个单个结点进行插入排序
sortLink(p);
}
printf("freeLink:\n");
displayLink(freeLink);
printf("usedLink:\n");
displayLink(usedLink);
}
void callAllocAlgorithm(){ // 调用分配算法,此处主要用于用户输入信息
int prgNum = 0, i = 0;
long prgSize = 0, p_id = -1;
printf("请输入添加的程序数量:");
scanf("%d", &prgNum);
for(;i < prgNum;i++){
printf("\n请输入第 %d 个程序的 id 及大小:", i + 1);
scanf("%ld%d", &p_id, &prgSize);
if(prgSize <= 0 || prgSize >= MAX_ALLOC_SIZE){
printf("内存申请失败!");
return;
}
bestFitAlloc(prgSize);
}
}
void callRecoveryAlgorithm(){
struct Node *mHead = initMemory, *fHead = freeLink;
int prgNum = 0, i = 0;
int p_id = -1;
printf("请输入回收的内存数量:");
scanf("%d", &prgNum);
for(;i < prgNum;i++){
printf("\n请输入第 %d 块内存的 id:", i + 1);
scanf("%ld", &p_id);
if(p_id == -1){
printf("无此内存可以回收!");
return;
}
memoryRecovery(p_id);
printf("\ninitMemory after recovery:\n");
displayLink(initMemory);
}
while(mHead){
if(mHead->status == FREE){
fHead->next = mHead;
fHead = mHead;
}
mHead = mHead->next;
}
printf("freeLink:\n");
displayLink(freeLink);
}
void userInterface(){ // 用户界面
int i = 0;
long curAdd = 0;
int init_success = 0;
struct Node *temp = (struct Node*)malloc(sizeof(struct Node));
struct Node *lastNode, *preNode;
// 初始化起始地址与终止地址
start_add = end_add = -1;
printf("请初始化内存块的首地址及内存大小:");
scanf("%ld%ld", &start_add, &memory_size);
if(memory_size > MAX_ALLOC_SIZE){
printf("申请内存超过内存大小阈值!");
return;
}
init_success = initLink();
if(!init_success){
printf("内存申请失败!");
return;
}
// 内存申请成功,执行下面代码,最初连续分配内存空间
temp->p_id = -1;
temp->address = start_add;
temp->size = memory_size;
temp->pre = temp->next = NULL;
temp->status = FREE;
insertNode(initMemory, temp);
lastNode = temp; // lastNode 指向初始内存最后一个结点
preNode = temp->pre;
int p_id = -1, p_num = 0;
long p_size = -1;
printf("请输入申请资源的进程个数:");
scanf("%d", &p_num);
for(i = 0;i < p_num;i++){
printf("请输入第 %d 个进程调入的分区号及其大小:", i + 1);
scanf("%d%ld", &p_id, &p_size);
// 开始划分连续分区
struct Node *p = (struct Node*)malloc(sizeof(struct Node));
p->address = curAdd;
p->p_id = p_id;
p->size = p_size;
p->status = FULL;
p->pre = p->next = NULL;
curAdd += p_size;
if(curAdd > start_add + memory_size){
printf("内存分配超出系统限制!");
return;
}
lastNode->address = curAdd;
lastNode->size -= p_size;
p->next = lastNode;
lastNode->pre = p;
p->pre = preNode;
preNode->next = p;
preNode = p;
}
// 上面代码测试无误,initMemory内存被连续使用
printf("initMemory:\n");
displayLink(initMemory);
callRecoveryAlgorithm();
callAllocAlgorithm();
}
void displayLink(struct Node *head){
struct Node *p = head->next;
while(p){
printf("id:%d 首地址:%ld 大小:%ld 状态:%s\n", p->p_id, p->address, p->size, getStatus(p->status));
p = p->next;
}
}
int main()
{
userInterface();
return 0;
}