CSAPP malloclab实验

2023-10-26

书本配套实验地址
构造一个分配器是一件富有挑战的任务。设计空间很大,有多种块格式、空闲链表格式,以及放置、分割和合并策略可供选择。另一个挑战就是我们经常被迫在类型系统的安全和熟悉的限定之外编程,依赖于容易出错的指针强制类型转换和指针运算,这些操作都属于典型的低层系统编程。


一、理解动态内存分配器是个什么东西?

其实动态内存分配器就是我们平时在C语言上用的malloc和free,realloc,通过分配堆上的内存给程序,我们通过向堆申请一块连续的内存,然后将堆中连续的内存按malloc所需要的块来分配,不够了,就继续向堆申请新的内存,也就是扩展堆。
动态内存分配器维护着一个进程的虚拟内存区域,称为堆(heap)。不同系统之间存在一些细节上的不同,但不失通用性。分配器将堆视为一组大小不同的块block的集合来维护;每个块就是一个连续的虚拟存储器片,即页面大小;要么是已分配的,要么是空闲的。
分配器分为两种:显示分配器和隐式分配器。
在这里插入图片描述

二、为什么要使用动态内存分配?

程序使用动态存储器分配的最重要原因:经常直到程序实际运行时,它们才知道某些数据结构的大小。

三、分配器的要求和目标

要求:处理任意请求序列、立即响应请求、对齐块、不修改已分配的块。
目标:吞吐率最大化和存储器使用率最大化。但这两个性能要求通常是相互冲突的,分配器设计的一个有趣的挑战就是在两者之间找到一个平衡。
通常会考虑以下几个问题:

  • 空闲块组织: 如何记录空闲块?
  • 放置: 如何选择一个合适的空闲快来放置一个新分配的块?
  • 分割: 将一个新分配的块放入某个空闲块后,如何处理这个空闲快中的剩余部分?
  • 合并: 我们如何处理一个刚刚被释放的块?

四、实现分配器

弄了一圈发现分数并没有提高,所以就主要以书本为基础。
1、操作空闲链表哦的基本常数和宏

/* single word (4) or double word (8) alignment */
#define ALIGNMENT 8

/* rounds up to the nearest multiple of ALIGNMENT */
#define ALIGN(size) (((size) + (ALIGNMENT-1)) & ~0x7)

#define SIZE_T_SIZE (ALIGN(sizeof(size_t)))

/* Basic constants and macros */
#define WSIZE 4   /* Word and header/footer size (bytes) */
#define DSIZE 8   /* Double word size (bytes)  */
#define CHUNKSIZE (1<<12) /* Extend heap by this amount (bytes) */

#define MAX(x,y) ((x) > (y)? (x):(y))

/* Pack a size and allocated bit into a word */
#define PACK(size,alloc) ((size) | (alloc))

/* Read and write a word at address p */
#define GET(p) (*(unsigned int *)(p))
#define PUT(p,val) (*(unsigned int *)(p)=(val))
#define GET_ADDRESS(p) (*(void **)(p))

//总size,包括头尾
/* Read the size and allocated fields from address p */
#define GET_SIZE(p) (GET(p) & ~0x7)
#define GET_ALLOC(p) (GET(p) & 0x1)

/* Given block ptr bp, compute address of its header and footer */
#define HDRP(bp) ((char *)(bp)-WSIZE) //头部的指针
#define FTRP(bp) ((char *)(bp)+ GET_SIZE(HDRP(bp))-DSIZE) //脚部的指针
#define PRED(bp) ((char *)(bp))       //祖先节点
#define SUCC(bp) ((char *)(bp))       //后继节点,只留后继结点

/* 获取有效字节,即获取总的size数-头尾指针(因为busyblock没有前继和后继指针) */
#define GET_PAYLOAD(bp) (GET_SIZE(HDRP(bp))-DSIZE)

/* Given block ptr bp, compute address of next and previous blocks */
#define NEXT_BLKP(bp) ((char *)(bp)+GET_SIZE(((char *)(bp)-WSIZE)))
#define PREV_BLKP(bp) ((char *)(bp)-GET_SIZE(((char *)(bp)-DSIZE)))

字的大小(WSIZE)和双字的大小(DSIZE),初始空闲块的大小和扩展堆时的默认大小(CHUNKSIZE)。
PACK宏将大小和已分配位结合起来并返回一个值,可以把它存放在头部或者脚部。
GET宏读取和返回参数p引用的字。类型转换很重要,参数p是一个(void* )指针,不可以直接进行间接引用。类似地,PUT宏将val存放在参数p指向的字中。
GET_SIZE和GET_ALLOC宏从地址p处的头部或者脚部分别返回大小和已分配位。剩下的宏是对块指针(block pointer,用bp表示)的操作,块指针指向第一个有效载荷字节。给定一个块指针bp,HDRP和FTRP宏分别返回指向这个块的头部和脚部的指针。NEXT_BLKP和PREV_BLKP宏分别返回指向后面的块和前面的块的块指针。
可以用多种方式来编辑宏,以操作空闲链表。比如,给定一个指向当前块的指针bp,我们可以使用下面的代码行来确定内存中后面的块的大小:

size_t size = GET_SIZE(HDRP(NEXT_BLKP(bp)));

2、创建初始空闲链表
在调用mm_malloc或者mm_free之前,应用必须通过调用mm_init函数来初始化堆。mm_init函数从内存系统得到4个字,并将它们初始化,创建一个空的空闲链表。


/* 
 * mm_init - initialize the malloc package.
 */int mm_init(void)
{

    /* Create the initial empty heap */
    if((heap_listp=mem_sbrk(4*WSIZE))==(void *)-1)
        return -1;


    //链表初始化
    for(int i=0;i<MAX_SIZE;++i)
        linkhead[i]=NULL;


    PUT(heap_listp,0);       /* Alignment padding */
    PUT(heap_listp+(1*WSIZE),PACK(DSIZE,1));  /* Prologue header */
    PUT(heap_listp+(2*WSIZE),PACK(DSIZE,1));  /* Prologue footer */
    PUT(heap_listp+(3*WSIZE),PACK(0,1));      /* Epilogue header */
    heap_listp+=(2*WSIZE);

    if (extend_heap(CHUNKSIZE/WSIZE)==NULL)
        return -1;

    return 0;
}

然后调用extend_heap函数,这个函数将堆扩展CHUNKSIZE字节,并且创建初始的空闲块。此刻,分配器已经初始化了,并且准备好接受来自应用的分配和释放请求。
extend_heap函数会在两种不同的环境中被调用:1)当堆被初始化时;2)当mm_malloc不能找到一个合适的匹配块时。为了保持对齐,extend_heap将请求大小向上舍入为最接近的2字(8字节)的倍数,然后向内存系统请求额外的堆空间。
堆开始于一个双字对齐的边界,并且每次对extend_heap的调用都返回一个块,该块的大小时双字的整数倍。因此,对mem_sbrk的每次调用都返回一个双字对齐的内存片,紧跟在结尾块的头部后面。这个头部变成了新的空闲块的头部,并且这个片的最后一个字变成了新的结尾块的头部。最后,在很可能出现的前一个堆以一个空闲块结束的情况中,我们调用coalesce函数来合并两个空闲块,并返回指向合并后的块的块指针。

//扩展堆的大小
static void *extend_heap(size_t words)
{
    char *bp;
    size_t size;

    /* Allocate an even number of words to maintain alignment */
    size=(words %2)? (words+1)*WSIZE: words*WSIZE;
    if((long)(bp=mem_sbrk(size))==-1)
        return NULL;

    /* Initialize free block header/footer and the epilogue header */
    PUT(HDRP(bp),PACK(size,0));  /* Free block header */
    PUT(FTRP(bp),PACK(size,0));  /* Free block footer */
    PUT(HDRP(NEXT_BLKP(bp)),PACK(0,1));   /* New epilogue header */


    /* Coalesce if the previous block was free */
    return coalesce(bp);
}

3、释放和合并块
应用通过调用mm_free函数,来释放一个以前分配的块,这个函数释放所请求的块(bp),然后使用边界标记合并技术将之于邻接的空闲块合并起来。
Knuth提出过一个技术,叫做边界标记(boundary tag),允许在常数时间内进行对前面块的合并。这思想是在每个块的结尾处添加一个脚部(footer),其中脚部就是头部的一个副本。如果每个块包括这样一个脚部,那么分配器就可以通过检查它的脚部,判断前面一个块的起始位置和状态,这个脚部总是在距当前块开始位置一个字的距离。
它也存在一个潜在的缺陷。它要求每个块都保持一个头部和脚部,在应用程序操作许多个小块时,会产生显著的内存开销。但也有一种优化方法,因为只有在前面的块是空闲时才需要用到它的脚部,所以我们可以把前面块的已分配/空闲位存放在当前块中多出来的低位中,那么已分配的块就不需要脚部了,这样我们就可以将多出来的空间用作有效载荷了。不过,空闲块任然需要脚部。
在这里插入图片描述在这里插入图片描述coalesce函数中的代码是上面四种情况的一种简单直接地实现。我们选择空闲链表格式(它的序言块和结尾块总是标记为已分配)允许我们忽略潜在的麻烦边界情况,也就是,请求块bp在堆的起始处或者是在堆的结尾处。如果我们没有这些特殊块,代码将混乱很多,更容易出错,并且更慢,因为我们将不得不在每次释放请求时,都去检查这些并不常见的边界情况。

/*
 * mm_free - Freeing a block does nothing.
 */
void mm_free(void *ptr)
{
    size_t size=GET_SIZE(HDRP(ptr));
    //头尾归为free的block
    PUT(HDRP(ptr),PACK(size,0));
    PUT(FTRP(ptr),PACK(size,0));
    coalesce(ptr);
}
static void *coalesce(void *bp)
{
    //关于这一块的改free操作已经在free函数的过程中执行了

    size_t prev_alloc=GET_ALLOC(FTRP(PREV_BLKP(bp)));
    size_t next_alloc=GET_ALLOC(HDRP(NEXT_BLKP(bp)));
    size_t size=GET_SIZE(HDRP(bp));

    //情况一,前一块和后一块都被申请了
    if(prev_alloc && next_alloc)
    {
        return bp;
    }

    //情况二,后一块是空闲的
    else if(prev_alloc && !next_alloc)
    {
        size+=GET_SIZE(HDRP(NEXT_BLKP(bp)));
        PUT(HDRP(bp),PACK(size,0));
        //改完头部大小就变了,只能直接访问尾部,对尾部进行改大小的操作
        PUT(FTRP(bp),PACK(size,0));
        return bp;
    }

    //情况三,前一块是空闲的
    else if(!prev_alloc && next_alloc)
    {
        size+=GET_SIZE(FTRP(PREV_BLKP(bp)));
        PUT(FTRP(bp),PACK(size,0));
        PUT(HDRP(PREV_BLKP(bp)),PACK(size,0));
        return PREV_BLKP(bp);
    }

    //情况四,前后都是空的
    else
    {
        size+=(GET_SIZE(HDRP(NEXT_BLKP(bp)))+GET_SIZE(FTRP(PREV_BLKP(bp))));
        PUT(FTRP(NEXT_BLKP(bp)),PACK(size,0));
        PUT(HDRP(PREV_BLKP(bp)),PACK(size,0));
        return PREV_BLKP(bp);
    }
}

4、分配块
一个应用通过调用mm_malloc函数来向内存请求大小为size字节的块。在检查完请求的真假之后,分配器必须调整请求块的大小,从而为头部和脚部留有空间,并满足双字对齐的要求。

/* 
 * mm_malloc - Allocate a block by incrementing the brk pointer.
 *     Always allocate a block whose size is a multiple of the alignment.
 */
void *mm_malloc(size_t size)
{
    size_t asize;   /* Adjusted block size */
    size_t extendsize; /* Amount to extend heap if no fit */
    char *bp;

    /* Ignore spurious requests */
    if(size==0)
    {
        return NULL;
    }

    /* Adjust block size to include overhead and alignment reqs. */
    //要加上头尾两个指针
    if(size<=DSIZE)
        asize=2*DSIZE;  //强制了最小块大小是16字节,8字节用来满足对齐要求,另外8字节用来存放头部和脚部。
    else
        asize=DSIZE*((size+(DSIZE)+(DSIZE-1))/DSIZE); //对于超过8字节的请求,一般规则是加上开销字节,然后向上舍入到最接近8的整数倍

    /* Search the free list for a fit */
    if((bp=find_fit(asize))!=NULL) { //一旦分配器调整了请求的大小,它就会搜索空闲链表,寻找一个合适的空闲块
        place(bp,asize); //如果有合适的,分配器就放置这个请求块,并可选地分割出多余的部分
        return bp; //然后返回新分配块的地址
    }

    /* No fit found. Get more memory and place the block */
    //如果分配器不能够发现一个匹配的块,那么就用一个新的空闲块来扩展堆
    extendsize=MAX(asize,CHUNKSIZE); 
    if((bp=extend_heap(extendsize/WSIZE))==NULL)
        return NULL;
  	//把请求块放置在这个新的空闲块里,可选地分割这个块,然后返回一个指针,指向新分配地块 
    place(bp,asize); 
    return bp;
}

5、放置策略

  • 首次适配:从头开始搜索空闲链表,选择第一个合适地空闲块。优点是趋向于将大的空闲块保留在链表的后面;缺点是它去向于在靠近链表起始处留下小空闲块的“碎片”,增加了堆较大快的搜索时间。
  • 下一次适配:和首次适配相似,只不过是从上一次查询结束的地方开始。它比首次适配运行起来更快,但内存利用率低得多。
  • 最佳适配:检查每个空闲块,选择适合所需请求大小的最小空闲块。它比首次适配和最佳适配的内存利用率都高,但它要求堆堆进行彻底的搜索。
  • 分离适配:用分离式空闲链表组织,它接近于最佳适配策略,不需要进行彻底的对搜索。它的搜索时间减少了,因为搜索被限制在堆的某个地方,而不是整个堆;内存利用率得到改善,因为对分离空闲链表的简单的首次适配搜索,其内存利用率近似于对整个对的最佳适配搜索。
    1)首次适配
//寻找合适的块
static void *find_fit(size_t asize)
{
    /* First fit search */
   void *bp;

   for (bp=heap_listp; GET_SIZE(HDRP(bp))>0; bp=NEXT_BLKP(bp)) {
       if (!GET_ALLOC(HDRP(bp))&&(asize<=GET_SIZE(HDRP(bp)))) {
           return bp;
       }
   }

   return NULL; // No fit
}

2)分离适配
分离适配的基本思想就是将所有空闲块分成大小类,分别分成0-8,9-16,17-32,33-64,65-128,…… ,2049-4096,4097-正无穷,这么几个大小类的空闲链表,然后我们想要进行malloc的时候,就将空闲块进行筛选,将其分到对应的大小块中进行搜索,这样就可以将malloc搜索块的时间从所有空的空闲块降低到局部链表的空闲块中,提高了效率。并且事实证明,当分到对应的大小类链表的时候,它的空间也会在大小类链表的范围里面,这样使得即使是首次适配也可以是空间利用率接近最佳适配。那么,他的free也是相同,在合并的时候,将前后空闲块从链表中删除,然后合并,合并后再加入对应的空闲链表。分割的时候,也是分割后将分割块插入适当的空闲链表中。

//寻找合适的块
static void *find_fit(size_t size)
{
    for(int index=findlink(size);index<MAX_SIZE;++index)
    {
        void* bp=linkhead[index];
        while(bp!=NULL)
        {
            if(GET_SIZE(HDRP(bp))>=size) return bp;
            bp=GET_ADDRESS(SUCC(bp));
        }
    }

    return NULL;    
}

这里测试的时候两种策略的得分居然是一样的,我也很懵逼~

6、place函数
对于这个分配器,最小块是16字节。如果分割后剩下的块大于或者等于最小块的大小,那么我们就分割这个块;这里有一个技巧:要意识到在移动到下一块之前(bp=NEXT_BLKP(bp)

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

CSAPP malloclab实验 的相关文章

随机推荐

  • IDEA 方法注释 自动获取返回值和传参

    一 设置 1 添加自定义注释快捷键 2 注释内容 desciption params return returns Author junwei Date date time 点击右边的edit variables 设置函数 下面3个内容选择
  • 前端面试题梳理

    一 技术方面 60 1 实现一个元素的水平垂直居中的几种方式 2 vue中 双向绑定的原理 3 vueX的原理 4 实现一个左边固定 右边自适应的布局 5 pomise的理解 6 对浏览器兼容的理解 如何兼容低版本浏览器 7 地址栏输入一个
  • UnityEditor.BuildPlayerWindow+BuildMethodException

    unity3D安卓打包报错 UnityEditor BuildPlayerWindow BuildMethodException 61 errors at UnityEditor BuildPlayerWindow DefaultBuild
  • hive 查询输入中文乱码

    设置 home 用户 profile 文件中LANG en US UTF 8即可
  • envi5.3处理高分二号影像数据详细过程记录

    目录 一 多光谱影像处理 1 辐射定标 2 大气校正 1 需要准备一些数据 2 大气校正过程 3 正射校正 二 全色影像处理 1 辐射定标 2 正射校正 三 图像融合 1 几何配准 2 图像融合 高分二号处理流程 envi5 3的安装教程
  • C3P0的详细配置说明(com.mchange.v2.c3p0.ComboPooledDataSource)

    C3P0是一个开放源代码的JDBC连接池 它在lib目录中与Hibernate一起发布 包括了实现jdbc3和jdbc2扩展规范说明的Connection 和Statement 池的DataSources 对象 c3p0 config gt
  • 使用Pytorch进行多卡训练

    当一块GPU不够用时 我们就需要使用多卡进行并行训练 其中多卡并行可分为数据并行和模型并行 具体区别如下图所示 由于模型并行比较少用 这里只对数据并行进行记录 对于pytorch 有两种方式可以进行数据并行 数据并行 DataParalle
  • 数据库课程设计:图书信息管理系统(Java+MySQL)(附程序)

    期末数据库课程设计做了个图书信息管理系统 由于老师给的选题给得早 所以我在开学后的几周就开学搞了 删删改改整了好多 在此整理分享一下 项目简介 随着社会的发展 人们对知识的需求也在不断增长 书籍作为人们获取并增长知识的只要途径 使得书城 书
  • 如何通过远程桌面重启计算机?

    使用远程桌面连接远程计算机后 在开始菜单中只有 注销 和 关机 选项 无法直接重启现场终端 可以使用命令行重启现场终端 使用运行命令 Windows R键 输入命令行shutdown r t 0 Shutdown r t 5 关闭 重启 延
  • 看尚电视adb安装当贝桌面,并开机自启

    1 链接 可以电脑下奇兔刷机 实用工具里有adb 点开直接用 ADB 链接好后输入命令验证 c adb gt adb devices 如显示 192 168 5555 device字样表示链接成功 不同的adb前面几个字母也许不一样 2 开
  • 每个初级程序员都希望有一天能成为一名高级开发工程师。

    当程序员想要转向更高需求以及更高层次的角色时 他们的能力也必须随之提升 但也正因如此 很多人都会在这种转变中失败 程序员们通常认为 成为一名高级开发工程师必定要积累一定年限的经验以及十分擅长编程 虽然这些的确是必要因素 但想要成为一名高级开
  • 多线程创建的方式

    多线程的创建方式有以下几种 1 继承Thread类创建多线程 继承java lang Thread类 重写Thread类的run 方法 在run 方法中实现运行在线程上的代码 调用start 方法开启线程 Thread 类本质上是实现了 R
  • 【Ubuntu换源教程】不同Ubuntu系统版本换清华源

    今天在新电脑上装了虚拟机VMware Workstation Pro 16 在虚拟机上安装了Ubuntu20 04系统 在做Ubuntu20 04系统换源的时候 发现源要和Ubuntu的版本匹配 之前一直不知道 一直都是盲目换源 版本如果不
  • unity给localRotation赋值

    transform localPosition和transform localScale都是直接赋值三元数 给旋转赋值需要用 方法一 xxx transform localEulerAngles new Vector3 0 0f 0 0f
  • JS面试中常见的算法题

    1 验证一个数是否是素数 1 如果这个数是 2 或 3 一定是素数 2 如果是偶数 一定不是素数 3 如果这个数不能被3至它的平方根中的任一数整除 num必定是素数 而且除数可以每次递增 排除偶数 function isPrime num
  • 优秀logo设计解析_优秀Logo设计!具象表现手法!

    文 王新华 具象标志以客观物象的自然形态为造型基础 经过提炼 概括 抓住客观对象的精神内涵 强化其主要特征 忽略与舍弃次要因素 达到直观 感性的视觉效果 人物形 人是万物之灵 是社会的主宰 以人物为题材是标志设计的重要表现内容 人体的各种动
  • C++中memset函数详解

    memset函数定义于
  • Django中分页功能的实现及封装与调用(超详细)

    在django开发过程中 实现前端页面的分页是一个基本且常用的功能 下面就同小编一起完成分页功能的实现及封装与调用 一 在pycharm中创建django项目 小编默认看客朋友们都会创建 故不在赘述 若不熟悉 猛戳这里 二 在mysql中创
  • React事件处理、事件的特点、事件语法、React事件处理函数里的this、事件对象、阻止浏览器的默认行为

    React事件的特点 1 React 事件绑定属性的命名采用驼峰式写法 而不是小写 如 onClick 2 如果采用 JSX 的语法你需要传入一个函数作为事件处理函数 而不是一个字符串 DOM 元素的写法 函数不写小圆括号 3 在 Reac
  • CSAPP malloclab实验

    书本配套实验地址 构造一个分配器是一件富有挑战的任务 设计空间很大 有多种块格式 空闲链表格式 以及放置 分割和合并策略可供选择 另一个挑战就是我们经常被迫在类型系统的安全和熟悉的限定之外编程 依赖于容易出错的指针强制类型转换和指针运算 这