如何制作小程序的详细步骤江北seo综合优化外包
边界标识法(boundary tag method)是操作系统中用以进行动态分区分配的一种存储管理方法,将所有的空闲块链接在一个双向循环链表结构的可利用空间表中。
特点:在每个内存区的头部和底部两个边界上分别设有标识,以标识该区域为占用块或空闲块,方便在释放时对相邻的空闲块进行合并。
1.表结构
表结构如下:
定义如下:
typedef struct WORD{union{WORD *llink; //头部域,指向前驱结点WORD *uplink; //底部域,指向本结点头部};int tag; //标识,0空闲,1占用,头部和尾部都有int size; //头部域,块大小,以WORD为单位WORD *rlink; //头部域,指向后继结点
} WORD, head, foot, *Space; //Space:可利用空间指针类型
系统需要一个记录所有空闲块的链表,如下所示:
2.分配算法
假定我们采取首次拟合法,即从头指针位置查找第一个符合条件的结点,为了使整个系统更有效地运行,在边界标识法中还作了如下两个设计:
(1).分配的空闲块的容量为n个字WORD(包括头部和底部),若空闲块剩余的大小小于系统设定的边界值就将整个空闲块整块分配给用户:反之,则需要进行分割。同时,为了避免过多修改指针,约定将该结点中的高地址部分分配给用户。
(2).如果每次分配都从同一个结点开始查找的话,势必造成存储量小的结点密集在头指针pav所指结点附近,这同样会增加査询较大空闲块的时间。反之,如果每次分配从不同的结点开始进行查找,使分配后剩余的小块均匀地分布在链表中,则可避免上述弊病。实现的方法是,在每次分配之后令指针pav指向刚进行过分配的结点的后继结点。
初始化过程如下:
#define _CRT_SECURE_NO_WARNINGS //这一句必须放在第一行
#include <stdio.h> //标准输入输出文件
#include <stdlib.h>//不带头节点的首次拟合法
typedef struct WORD { //字union {WORD* llink; //头部域,指向前驱结点WORD* uplink; //底部域,指向本结点头部};int tag; //标识,0空闲,1占用,头部和尾部都有int size; //头部域,块大小,以WORD为单位WORD* rlink; //头部域,指向后继结点
} WORD, *Space; //Space:可利用空间指针类型#define SIZE 10000 //内存池的大小(WORD)
#define e 10 //碎片临界值,当剩余的空闲块小于e时,整个空闲块都分配出去static Space FootLoc(Space p) //通过p返回p的尾
{return p + p->size - 1;
}Space InitMem() //初始化内存池
{Space pav = (Space)malloc(SIZE * sizeof(WORD)); //内存池//处理pav的头pav->llink = pav;pav->rlink = pav;pav->tag = 0;pav->size = SIZE;//处理p的尾Space p = FootLoc(pav); //p指向尾p->uplink = pav;p->tag = 0;return pav;
}
分配算法实现如下:
//向内存池pav,申请n个WORD,成功返回申请内存的地址,失败返回NULL
//利用首次拟合法
WORD* MyMalloc(Space* pav, int n)
{if (*pav == NULL)//内存池已经空了return NULL;Space p = *pav;do //首次拟合法,找第一个符合条件的空闲块{if (p->size >= n)//找到了break;} while (p != *pav);if (p->size < n)//没有满足条件的空闲块return NULL;if (p->size - n < e)//整个空闲块都分配{WORD* q = p;//q->llink,q->rlink,占用块不需要处理q->tag = 1; //占用//q->size 不改变p = FootLoc(p); //尾部//p->uplink不改变p->tag = 1; //占用//把q从链表中剔除if (q->rlink == q)//空闲链表中只有q这一个结点*pav = NULL;else//有多个结点{*pav = q->rlink; //头指针指向下一个结点q->llink->rlink = q->rlink;q->rlink->llink = q->llink;}return q;}else //空闲块一部分分配出去,把下面(高地址)分出去{*pav = p->rlink; //头指针指向下一个结点p->size -= n; //p分割后的新大小WORD *p2 = FootLoc(p); //新结点的尾p2->uplink = p;p2->tag = 0;//处理占用块WORD* q = p2+1; //指向需要返回的地址q->tag = 1; //占用q->size = n; //申请的大小p2 = FootLoc(q); //q的尾巴p2->uplink = q;p2->tag = 1;return q;}
}
测试代码:
void Show(WORD* p)//测试函数,输出p的数据
{printf("p的地址:%p,p->tag:%d,p->size:%d(WORD),p的尾巴的tag:%d\n",p,p->tag,p->size,FootLoc(p)->tag);
}int main()
{Space pav = InitMem();//创建并初始化好的内存池WORD* p1 = MyMalloc(&pav, 1000);WORD* p2 = MyMalloc(&pav, 1500);WORD* p3 = MyMalloc(&pav, 500);WORD* p4 = MyMalloc(&pav, 1000);WORD* p5 = MyMalloc(&pav, 1000);Show(p1);Show(p2);Show(p3);Show(p4);Show(p5);return 0;
}
3.回收算法
用户释放占用块,为了使物理地址毗邻的空闲块结合成一个尽可能大的结点,则首先需要检查刚释放的占用块的左右紧邻是否为空闲块。本系统在每个内存区(无论是占用块或空闲块)的边界上都设有标志值,很容易检测这一点。假设用户释放的内存区的头部地址为p,则与其低地址紧邻的内存区的底部地址为p-1;与其高地址紧邻的内存区的头部地址为p+p->size,它们中的标志域就表明了这两个邻区的使用状况:若(p-1)->tag=0;则表明其左邻为空闲块若(p+p->size)->tag=0;则表明其右邻为空闲块,
若释放块的左、右邻区均为占用块,处理最为简单,只要将此新的空闲块作为一个结点插人到可利用空闲表中即可:若只有左邻区是空闲块,则应与左邻区合并成一个结点;若只有右邻区是空闲块,则应与右邻区合并成一个结点若左、右邻区都是空闲块,则应将3块合起来成为一个结点留在可利用空间表中。
下面我们就这4种情况分别描述它们的算法:
(1)释放块的左、右邻区均为占用块。此时只要作简单插入即可。由于边界标识法在按首次拟合进行分配时对可利用空间表的结构没有任何要求,则新的空闲块插入在表中任何位置均可。
(2)释放块的左邻区为空闲块,而右邻区为占用块。由于释放块的头部和左邻空闲块的底部毗邻,因此只要改变左邻空闲块的结点:增加结点的size域的值且重新设置结点的底部即可。
(3)释放块的右邻区为空闲块,而左邻区为占用块。由于释放块的底部和右邻空闲块的头部毗邻,因此,当表中结点由原来的右邻空闲块变成合并后的大空闲块时,结点的底部位置不变,但头部要变,由此,链表中的指针也要变。
(4)释放块的左、右邻区均为空闲块。为使3个空闲块连接在一起成为一个大结点留在可利用空间表中,只要增加左邻空闲块的space容量,同时在链表中删去右邻空闲块结点即可。
测试时请注意,实际的实现分配是从后往前
红色部分为"墙",防止越界
void MyFree(Space* pav, WORD* p)//释放p
{WORD* pl = p - 1;//左块的尾巴WORD* pr = FootLoc(p) + 1;//右块的头if (pl->tag == 1 && pr->tag == 1)//左块占用,右块占用,直接插入{p->tag = 0; //释放后是空闲块FootLoc(p)->tag = 0;if (*pav == NULL)//可利用空间表为NULL,p是第一个结点{*pav = p;p->llink = p->rlink = p;}else{ //将p插入在pav的前面,即p成为可利用空间表的最后一个结点WORD* p1 = *pav;//第一个结点WORD* p2 = p1->llink;//最后一个结点//把p插入在p1和p2的中间,即p1的前面,p2的后面p->rlink = p1;p1->llink = p;p->llink = p2;p2->rlink = p;}}else if (pl->tag == 0 && pr->tag == 1)//左块为空闲块,右块为占用块{ //只需要把p加到左块的下面即可WORD* q = pl->uplink;//左块的头q->size += p->size; //合并后空闲块的大小//处理新块的尾FootLoc(q)->tag = 0;FootLoc(q)->uplink = q;}else if (pl->tag==1 && pr->tag==0)//左块为占用块,右块为空闲块{ //把右块从可利用空间表剔除,再把右块合并到p的下面,再把p插入到可利用空间表中//1.p的右块pr从可利用空间表剔除if (pr->rlink == pr)//只有一个空闲结点{*pav = NULL;}//后续代码存在缺失,这里先按已有内容输出}
}
总之,边界标识法由于在每个结点的头部和底部设立了标识域,使得在回收用户释放的内存块时,很容易判别与它毗邻的内存区是否是空闲块,且不需要查询整个可利用空间表便能找到毗邻的空闲块与其合并;:再者,由于可利用空间表上结点既不需依结点大小有序,也不需依结点地址有序,则释放块插人时也不需查找链表。由此,不管是哪一种情况,回收空闲块的时间都是个常量,和可利用空间表的大小无关。惟一的缺点是增加了结点底部所占的存储量。
4.优缺点
优点
1.高效的内存块合并
边界标志法在每个内存块的头部和尾部都记录了该块的使用状态和大小等信息。当一个内存块被释放时,系统可以通过检查其相邻内存块头部和尾部的标志信息,快速判断相邻块是否空闲。如果相邻块空闲,就能够立即将它们合并成一个更大的空闲块。
2.支持任意大小的内存分配
·边界标志法可以根据程序的实际需求,分配任意大小的内存块。
缺点
1.额外的空间开销
标志信息占用内存:边界标志法需要在每个内存块的头部和尾部都设置标志信息,这些标志信息会占用一定的内存空间,从而增加了内存管理的额外开销。尤其是在管理大量小内存块时,这种开销可能会变得比较明显,降低了内存的有效利用率
2.分配效率问题
查找合适空闲块耗时:在进行内存分配时,系统需要遍历空闲内存块列表,查找大小合适的块。如果空闲块列表较长,查找过程可能会比较耗时,特别是在内存使用比较碎片化的情况下,可能需要遍历多个空闲块才能找到合适的块,从而影响了内存分配的效率。
3.并发性能较差
在多线程环境下,由于边界标志法需要频繁地修改内存块的标志信息和空闲块列表,为了保证数据的一致性,需要使用同步机制(如锁)来保护这些共享数据。这会增加线程间的同步开销,降低系统的并发性能,尤其是在高并发的场景下,可能会成为系统的性能瓶颈。