当前位置: 首页 > news >正文

内存池项目(2)——内存池设计之边界标识法

边界标识法(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.并发性能较差

在多线程环境下,由于边界标志法需要频繁地修改内存块的标志信息和空闲块列表,为了保证数据的一致性,需要使用同步机制(如锁)来保护这些共享数据。这会增加线程间的同步开销,降低系统的并发性能,尤其是在高并发的场景下,可能会成为系统的性能瓶颈。

相关文章:

  • File 类的用法和 InputStream, OutputStream 的用法
  • 【虚拟化安全】虚拟化安全知识全攻略:保障云端数据安全
  • 数据库设计工具drawDB本地部署与远程在线协作实测让效率翻倍
  • Hibernate核心方法总结
  • 阿里云oss视频苹果端无法播放问题记录
  • 项目二 - 任务5:打印乘法九九表
  • Qt饼状图在图例上追踪鼠标落点
  • 人脸表情识别数据集分享(AffectNet、RAF-DB、FERPlus、FER2013、ck+)
  • NVIDIA Jetson 环境安装指导 PyTorch | Conda | cudnn | docker
  • 【qiankun】简易前端微应用搭建
  • 企业工厂生产线马达保护装置 功能参数介绍
  • 4.6学习总结
  • 网络中级(HCIP)项目实践一MGRE的两种架构的私有网段 OSPF 动态路由协议的互联实验(手把手教您,包学会的)
  • 使用 STM32F103C8 连接 ESP8266:创建 Web 服务器
  • 数据结构与算法-数学-基础数学2(扩展欧几里得算法,组合数问题)
  • C++中的类和对象(上)
  • CSS 锚点定位
  • spring-ai-openai调用Xinference1.4.1报错
  • 【ZYNQ Linux移植】1-前期准备
  • 【C++初阶】--- string类模拟实现
  • 工信部网站域名备案/站长素材免费下载
  • 青岛专业设计网站公司/新郑网络推广外包
  • 房产汽车网站模板/购物网站哪个最好
  • 日照外贸网站建设/黑龙江头条今日新闻
  • 域名注册需要多久/外贸seo推广招聘
  • 郑州艾特网站建设/市场营销专业课程