对于伙伴系统而言,有大小固定且相同的内存块,将大小相同的内存块链接在一起时,为了达到写平衡,使用双向链表实现,这样可以在回收回的内存块时,将其插入双向链表的链尾,在申请内存块时,从链头删除内存块;而将链接不同大小内存块的双向链表链接起来时,因为内存块大小种类固定所以使用顺序表实现,故使用顺序表加双向链表的形式实现伙伴系统。
双向链表设计思路及定义
细化而言,对于内存块,因为是在双向链表中,所以每个内存块中需要一个header,其中包含的信息有前驱 llink、后继 rlink;同时还需要一个标记内存块是否被占用的信息 tag,tag = 0 则内存块空闲,tag = 1则内存块占用;需要一个标记内存块大小的信息 kval,内存块大小为 2k 。
所以我们在实现内存池时,以一个header的大小16个字节为单位进行内存管理,每个单位称之为WORD.
typedef struct WORD
{
struct WORD *llink;
int tag;
int k;
struct WORD *rlink;
}WORD, *Space;
顺序表设计思路及定义
对于顺序表,则需要一个记录内存块大小的信息 nodesize;一个记录链接与 nodesize 大小相同的内存块的双向链表的结点信息 first。
typedef struct HNode
{
int size;
WORD *next;
}HNode, *PHNode;
实现动态内存管理
内存初始化
通过初始化实现如上图所示的内存池。同时在这里需要建立一个内存池头结点、全局变量 HEAD 记录内存池首地址,作用下文会有详细说明。
void InitMemory(PHNode *ppav) /*初始化:创建一个需要进行内存管理的内存池*/
{
WORD word[SIZE];
HNode head[K];
HEAD = word;//保存内存池首地址
word[0].llink = word;//初始化内存池
word[0].tag = 0;
word[0].k = K - 1;
word[0].rlink = word;
*ppav = head;//将顺序表链接到头指针
int i;
int size = 1;
for(i = 0; i < K; i++, size *= 2)//初始化顺序表
{
head[i].size = size;
head[i].next = NULL;
}
head[K - 1].next = word;//将内存池链接到顺序表
}
动态内存申请
WORD *Mymalloc(PHNode ppav, int size) /*动态内存申请*/
{
assert(ppav != NULL);
if (ppav == NULL)
{
return NULL;
}
int i;
//int j = -1;
for (i = 0; i < K; i++)
{
//if (ppav[i].size >= size && j == -1)
//{
// j = i;
//}
if (ppav[i].next != NULL && ppav[i].size >= size)//判断是否有足够剩余空间
{
break;
}
}
if (i == K)//剩余空间不足
{
return NULL;
}
Space q;
Space head = ppav[i].next;
Space p = head;
//有剩余空间
if (head->llink == head)//该双向链表中只有一个节点
{
//head = NULL; Error: 无法改变ppav[i].next的值,只改变head的值,函数运行完成后销毁,无用
ppav[i].next = NULL;
}
else//该双向链表不只有一个节点
{
ppav[i].next = head->rlink;
head->llink->rlink = head->rlink;
head->rlink->llink = head->llink;
}
/*方法一: 从左向右由小到大切割内存块
q = p + ppav[j].size;
p->llink = p;
p->k = j;
p->tag = 1;
p->rlink = p;
while (j < i)
{
q->llink = q;
q->tag = 0;
q->k = j;
q->rlink = q;
ppav[j].next = q;
j++;
q = q + ppav[j].size;
}
*/
/*方法二: 从右向左有大到小切割内存块*/
for(i--; ppav[i].size >= size; i--)//大块内存块切割小块内存块
{
q = p + ppav[i].size;
q->llink = q;
q->tag = 0;
q->k = i;
q->rlink = q;
ppav[i].next = q;
}
p->llink = p;
p->tag = 1;
p->k = i + 1;
p->rlink = p;
return p;
}
动态内存申请时要注意:
- 当双向链表值在顺序表中的头指针为NULL的情况
- 双向链表只有一个节点的情况
动态内存申请的难点在于处理大块内存块切割小块内存块的情况,这时有两种方法,方法一: 从左向右由小到大切割内存块,方法二: 从右向左有大到小切割内存块。
动态内存回收
void Myfree(PHNode ppav, WORD *p) /*动态内存回收*/
{
p->tag = 0;
int i;
int k = p->k;
int size = powx(2, k);
Space q;
int flag;
while(k < K)
{
if ((p - HEAD) % (size * 2) == 0)//判断左块、右块
{
flag = 0;//左块
q = p + size;
}
else
{
flag = 1;//右块
q = p - size;
}
if (q->tag == 0 && flag == 1 && q->k == p->k)//p为右块且有p的左块存在
{
if(q->rlink == q)
{
ppav[k].next = NULL;
}
else
{
ppav[k].next = q->rlink;//易错: 若p的左块为ppav[k].next所指的空间块,不写则在输出时会陷入死循环
q->llink->rlink = q->rlink;
q->rlink->llink = q->llink;
}
q->k++;
p = q;
k++;
}
else if (q->tag == 0 && flag == 0 && q->k == p->k)//p位左块且有p的右块存在
{
if(q->rlink == q)
{
ppav[k].next = NULL;
}
else
{
ppav[k].next = q->rlink;//易错: 若p的左块为ppav[k].next所指的空间块,不写则在输出时会陷入死循环
q->llink->rlink = q->rlink;
q->rlink->llink = q->llink;
}
p->k++;
k++;
}
else//p没有伙伴块
{
break;
}
size *= 2;
}
if(ppav[p->k].next == NULL)
{
p->llink = p;
p->rlink = p;
ppav[p->k].next = p;
}
else
{
q = ppav[k].next;
q->llink->rlink = p;//将p插入内存池
p->llink = q->llink;
q->llink = p;
p->rlink = q;
}
}
在动态内存回收中需要注意:
- 在判断回收的内存块的伙伴是否空闲时应使用q->tag == 0 && flag == 0 && q->k == p->k进行判断,缺一不可
- 代码第33、50行不可缺少
- 当双向链表值在顺序表中的头指针为NULL的情况
- 双向链表只有一个节点的情况
动态内存回收的难点在于内存回收时对于伙伴块的合并,应使用循环,当回收的内存块有伙伴时,与伙伴合并成为新的内存块,新的内存块再次查看是否有伙伴,直至新的内存块没有伙伴,将其插入和其大小的匹配的双向链表中,正如整体设计中所说,这时双向链表的价值就体现出来,为了写平衡,双向链表可轻易将新内存块插入双向链表的链表尾。
在这其中伙伴是一个很重要的概念,何为“伙伴”?将一个大块内存切割成为两个大小相同的小块内存,两个小块内存互为伙伴。