CSAPP Lab5- MallocLab

2023-11-16

实验目标

本实验需要用c语言实现一个动态的存储分配器,也就是你自己版本的malloc,free,realloc函数。

实验步骤

tar xvf malloclab-handout.tar解压文件

我们需要修改的唯一文件是mm.c,包含如下几个需要实现的函数

int mm_init(void);

void *mm_malloc(size_t size);

void mm_free(void *ptr);

void *mm_realloc(void *ptr, size_t size);

mm_init:在调用mm_malloc,mm_realloc或mm_free之前,调用mm_init进行初始化,正确返回0。

mm_malloc:在堆区域分配指定大小的块,分配的空间,返回的指针应该是8字节对齐的

mm_free:释放指针指向的block

mm_realloc:返回指向一个大小为size的区域指针,满足以下条件:

if ptr is NULL, the call is equivalent to mm_malloc(size);

if size is equal to zero, the call is equivalent to mm_free(ptr);

if ptr is not NULL:先按照size指定的大小分配空间,将原有数据从头到尾拷贝到新分配的内存区域,而后释放原来ptr所指内存区域

实验验证

mdriver.c:负责测试mm.c的正确性,空间利用率和吞吐量

-f <tracefile>:  -f后添加一些trace file来测试我们实现的函数

-V:打印出诊断信息

./mdriver -V  -f short1-bal.rep

编程规则

  • 不能改变mm.c中函数接口
  • 不能直接调用任何内存管理的库函数和系统函数malloc, calloc, free, realloc, sbrk, brk  
  • 不能定义任何全局或者静态复合数据结构如arrays, structs, trees,允许使用integers, floats, and pointers等简单数据类型
  • 返回的指针需要8字节对齐

只要提交mm.c文件


背景知识 

在实验开始之前,我想先引入有关brk和sbrk的相关知识作为铺垫,以便于更好地理解本次实验。

brk和sbrk主要的工作是实现虚拟内存到内存的映射。

虚拟内存

(1)每个进程都有各自独立的4G 字节的虚拟地址空间(对于X86系统32位而言)。4G的进程空间分为两部分,0~3G-1 为用户空间,3G~ 4G-1 为内核空间。

(2)用户程序中使用的都是虚拟地址空间中的地址,永远无法直接访问实际物理地址。

(3)虚拟内存到物理内存的映射由操作系统动态维护。

(4)虚拟内存一方面保护了操作系统的安全,另一方面允许应用程序使用比实际物理内存更大的地址空间。

(5)用户空间中的代码不能直接访问内核空间中的代码和数据,但是可以通过系统调用进入内核态,间接地与内核交互。

(6)对内存的越权访问,或访问未建立映射的虚拟内存(野指针、不在映射表中),将会导致段错误。

(7)用户空间对应进程,进程一切换,用户空间随即变换。

  • 内核空间由操作系统内核使用,不会随进程切换而变化。
  • 内核空间由内核根据独立且唯一的页表init_mm.pgd 进行映射,而用户空间的页表则每个进程一份。

(8)每个进程的内存空间完全独立,因此在不同进程之间交换虚拟地址毫无意义。

(9)虚拟内存到物理内存的映射,以页(4096字节)为单位

brk和sbrk的接口

下面是brk和sbrk的声明接口(详情见brk(2) - Linux manual page

#include <unistd.h>

      int brk(void *end_data_segment);

      void *sbrk(inptr_t increment);

DESCRIPTION

      brk  sets  the  end  of  the  data  segment  to  the value specified by

      end_data_segment, when that value is reasonable, the system  does  have

      enough  memory  and  the process does not exceed its max data size (see

      setrlimit(2)).

      sbrk increments the program's data  space  by  increment  bytes.   sbrk

      isn't a system call, it is just a C library wrapper.  Calling sbrk with

      an increment of 0 can be used to find the current location of the  pro-

      gram break.

RETURN VALUE

      On  success,  brk returns zero, and sbrk returns a pointer to the start

      of the new area.  On error, -1 is returned, and errno is set to ENOMEM.

sbrk不是系统调用,是C库函数。系统调用通常提供一种最小接口,而库函数通常提供比较复杂的功能。

Linux中的实现

在Linux系统上,程序被载入内存时,内核为用户进程地址空间建立了代码段、数据段和堆栈段,在数据段与堆栈段之间的空闲区域用于动态内存分配。

内核数据结构mm_struct中的成员变量start_code和end_code是进程代码段的起始和终止地址,start_data和end_data是进程数据段的起始和终止地址,start_stack是进程堆栈段起始地址,start_brk是进程动态内存分配起始地址(堆的起始地址),还有一个brk(堆的当前最后地址),就是动态内存分配当前的终止地址。

C语言的动态内存分配基本函数是malloc(),在Linux上的基本实现是通过内核的brk系统调用。

brk()是一个非常简单的系统调用,只是简单地改变mm_struct结构的成员变量brk的值。

mmap系统调用实现了更有用的动态内存分配功能,可以将一个磁盘文件的全部或部分内容映射到用户空间中,进程读写文件的操作变成了读写内存的操作。【在linux/mm/mmap.c文件的do_mmap_pgoff()函数,是mmap系统调用实现的核心。】

do_mmap_pgoff()的代码,只是新建了一个vm_area_struct结构,并把file结构的参数赋值给其成员变量vm_file,并没有把文件内容实际装入内存。

Linux内存管理的基本思想之一,是只有在真正访问一个地址的时候才建立这个地址的物理映射。

C语言内存分配方式

(1) 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。

(2) 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

(3)从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由我们决定,使用非常灵活,但产生的问题也最多。


C语言跟内存申请相关的函数主要有alloca,calloc,malloc,free,realloc,sbrk等。其中,

  • alloca是向栈申请内存,因此无需释放。
  • malloc分配的内存是位于堆中的,并且没有初始化内存的内容,因此基本上malloc之后,调用函数memset来初始化这部分的内存空间。
  • calloc则将初始化这部分的内存并设置为0。
  • realloc则对malloc申请的内存进行大小的调整。
  • 申请的内存最终需要通过函数free来释放。
  • sbrk则是用来增加数据段的大小。

malloc/calloc/free基本上都是C函数库实现的,跟OS无关。C函数库内部通过一定的结构来保存当前有多少可用内存。如果程序malloc的大小超出了库里所留存的空间,那么将首先调用brk系统调用来增加可用空间,然后再分配空间。free时,释放的内存并不立即返回给os,而是保留在内部结构中。可以打个比方:brk类似于批发,一次性的向OS申请大的内存,而malloc等函数则类似于零售,满足程序运行时的要求。这套机制有点类似于缓冲。使用这套机制的原因:系统调用不能支持任意大小的内存分配(有的系统调用只支持固定大小以及其倍数的内存申请,这样的话,对于小内存的分配会造成浪费;系统调用申请内存代价昂贵,涉及到用户态和核心态的转换。)

当程序运行过程中malloc了,但是没有free的话,会造成内存泄漏。一部分的内存没有被使用,但是由于没有free,因此系统认为这部分内存还在使用,造成不断的向系统申请内存,系统可用内存不断减少。但是,内存泄漏仅仅指程序在运行时,程序退出时,OS将回收所有的资源。因此,适当的重新启动一下程序,有时候还是有点作用hh~

实验内容

分配器通过显式空闲列表实现。每个分配的块结构如下:

   31      ...           3| 2  1  0
   --------------------------------
  | 00 ... size (29 bits) | 0 0 a/f| header
  |       content ...              |
  |       content ...              |
  | 00 ... size (29 bits) | 0 0 a/f| footer

其中高29位表示size,后三位表示是否被分配。 

每个空闲块结构都是这样的

   31      ...           3| 2  1  0
   --------------------------------
  | 00 ... size (29 bits) | 0 0 a/f     | header
  |      succ_addr (successor)          | succ_addr
  |      pred_addr (predecessor)        | pred_addr
  | 00 ... size (29 bits) | 0 0 a/f     | footer
   --------------------------------

注:prev和succ是反着的,因为这样head可以省下一个WSIZE ,也就是说,这个循环链表只是 head.next->...->tail.next->head。所以head.prev 是不需要的,因此4个字节就够了。

/*
 * mm.c - The fastest, least memory-efficient malloc package.
 *
 * In this naive approach, a block is allocated by simply incrementing
 * the brk pointer.  A block is pure payload. There are no headers or
 * footers.  Blocks are never coalesced or reused. Realloc is
 * implemented directly using mm_malloc and mm_free.
 *
 * NOTE TO STUDENTS: Replace this header comment with your own header
 * comment that gives a high level description of your solution.
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "memlib.h"
#include "mm.h"

/*
 * If NEXT_FIT defined use next fit search, else use first fit search
 */
#define BEST_FIT
/* Team structure */
team_t team = {
    /* Team name */
    "SA22225284",
    /* First member's full name */
    "majingnan",
    /* First member's email address */
    "majingnan36@163.com",
    /* Second member's full name (leave blank if none) */
    "",
    /* Second member's email address (leave blank if none) */
    ""};

/* 基本常量和宏 */
#define WSIZE 4             /* 字长(字节)*/
#define DSIZE 8             /* 双字大小(字节) */
#define CHUNKSIZE (1 << 12) /* 扩展堆大小(字节) */
#define OVERHEAD 8          /* 头和尾的开销 (bytes) */

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

/* 将一个size和分配的位打包成一个字 */
#define PACK(size, alloc) ((size) | (alloc))

/* 在地址 p 读写一个字 */
#define GET(p) (*(size_t *)(p))
#define PUT(p, val) (*(size_t *)(p) = (val))

/* 从地址 p 读取size 分配的字段 */
#define GET_SIZE(p) (GET(p) & ~0x7)
#define GET_ALLOC(p) (GET(p) & 0x1)

/* 给定块指针 bp,计算其header和footer的地址 */
#define HDRP(bp) ((char *)(bp)-WSIZE)
#define FTRP(bp) ((char *)(bp) + GET_SIZE(HDRP(bp)) - DSIZE)

/* 给定块指针 bp,计算下一个和上一个块的地址 */
#define NEXT_BLKP(bp) ((char *)(bp) + GET_SIZE(((char *)(bp)-WSIZE)))
#define PREV_BLKP(bp) ((char *)(bp)-GET_SIZE(((char *)(bp)-DSIZE)))

/* 指向第一个块的指针 */
static char *heap_listp; // 堆头

/* 内部辅助function的函数原型 */
static void *extend_heap(size_t words);
static void place(void *bp, size_t asize);
static void *find_fit(size_t asize);
static void *coalesce(void *bp);

/*
    这样初始化是为了保证这个链表 有头节点和尾节点,这样我们在删除的时候,就不需要特殊处理。
    tail 取 8个字节,是因为我的 prev 放在了后4个字节,tail 需要 prev,所以取8个字节。
    head 取4个字节是因为我觉得没有必要维护 head.prev , 也就是说我这个循环链表只是 head.next->...->tail.next->head.
    所以head.prev 是不需要的,因此4个字节就够了。
*/
static unsigned int list;
static long long t;
static void *head = &list;
static void *tail = &t;

#define PREV(bp) ((char *)(bp) + WSIZE) // prev 在第二个位置
#define SUCC(bp) ((char *)(bp))         // succ 在第一个位置,即bp的地方
#define GETP(p) (*(char **)p)
#define GET_PREV(bp) (GETP(PREV(bp)))
#define GET_SUCC(bp) (GETP(SUCC(bp)))
#define PUTP(p, val) (*(char **)(p) = (char *)(val))

// 在pos前面插入node
#define insert_before(pos, node)                                      \
    {                                                                 \
        GET_SUCC(GET_PREV(pos)) = (char *)node; /* pos.PREV -> node*/ \
        GET_PREV(node) = GET_PREV(pos);         /*pos.PREV<-node*/    \
        GET_PREV(pos) = (char *)node;           /*node<-pos*/         \
        GET_SUCC(node) = (char *)pos;           /*node->pos*/         \
    }
// 从空闲链表中删除pos
#define del(pos)                                                  \
    {                                                             \
        GET_SUCC(GET_PREV(pos)) = GET_SUCC(pos); /*PREV -> NEXT*/ \
        GET_PREV(GET_SUCC(pos)) = GET_PREV(pos); /*PREV<-NEXT*/   \
    }
// 将bp插入到空闲链表中
#define insertFreeList(bp)        \
    {                             \
        char *p = GET_SUCC(head); \
        insert_before((p), bp);   \
    }
#define freeNode(bp) del(bp);

int mm_init(void)
{
    GET_SUCC(head) = tail; // 建立链表
    GET_PREV(tail) = head; 
    // 使用 mem_sbrk() 函数获取一块大小为 4 * WSIZE 的空闲空间,并将指针 heap_listp 指向该空间的起始地址。
    // 其中 WSIZE 是字(word)的大小,它是对齐的基本单位。
    if ((heap_listp = mem_sbrk(4 * WSIZE)) == (void *)-1)
        return -1;
    // 在获取到空间之后,代码进行了一些堆的初始化工作。
    // 初始化过程需要对齐,因此在堆首部分配2*WSIZE字节的对齐填充,然后将指针heap_listp向右移动DSIZE位,以便让它指向排除对齐填充的内存块的开头。
    // 接着,把 prologue 块和 epilogue 块插入到头部和尾部,用于标识整个堆的状态。
    PUT(heap_listp, 0);                                 /* alignment padding */
    PUT(heap_listp + (1 * WSIZE), PACK(OVERHEAD, 1));   /* prologue header */
    PUT(heap_listp + (2 * WSIZE), PACK(OVERHEAD, 1));   /* prologue footer */
    PUT(heap_listp + (3 * WSIZE), PACK(0, 1));          /* epilogue header */
    heap_listp += (2 * WSIZE);

    // 一开始会分配64字节的堆空间,用于存储我们的数据。
    // 这样,一开始,一个只包含头尾块的堆会被建立,之后其他的空闲块会不断的被分配或者释放到堆中。返回值0表示初始化成功,-1表示失败。
    // 经过不断地测试发现64有最高的效率,可以达到90分(best_fit)
    if (extend_heap((1 << 6) / DSIZE) == NULL)  // 将字节大小转化为doubleword的个数
        return -1;
    return 0;
}

// malloc, 分配size大小的字节空间,成功返回首地址否则返回NULL
void *mm_malloc(size_t size)
{
    // 程序的入口参数size是待申请的内存大小。
    // 首先程序根据需要申请的大小size计算出需要分配的块的大小asize。
    // 如果size不足以填充一个头部和脚部,则asize根据对齐规则计算得到。
    size_t asize;      /* adjusted block size */
    size_t extendsize; /* amount to extend heap if no fit */
    char *bp;

    if (size <= 0)
        return NULL;

    if (size <= DSIZE) // 对齐
        asize = DSIZE + OVERHEAD;
    else
        asize = DSIZE * ((size + (OVERHEAD) + (DSIZE - 1)) / DSIZE);

    // 接下来,程序在空闲链表free_listp中查找足够大的空闲块。
    // 如果找到了合适的空闲块bp,则将该块分配给请求,并调用place函数将剩余的空间插入到空闲链表中。
    // 如果没有找到合适的空闲块,则需要通过调用extend_heap函数来扩展堆,并将扩展的块分配给请求。
    if ((bp = find_fit(asize)) == NULL)
    {   
        // 在空闲链表中寻找足够大的块, 找不着则扩大
        // MAX(asize, CHUNKSIZE)表示需要扩展的块的大小为asize和CHUNKSIZE中的较大值,CHUNKSIZE为申请新的堆内存块的大小。
        extendsize = MAX(asize, CHUNKSIZE);
        if ((bp = extend_heap(extendsize / DSIZE)) == NULL)
            return NULL;
    }
    // place函数根据块的大小将块分为两个部分:一个用于存储请求的数据,另一个部分作为剩余空间插入到空闲链表中。
    place(bp, asize);
    return bp;
}
// 释放bp所占的内存
void mm_free(void *bp)
{
    // 函数首先判断传入的指针是否为空指针,若是则直接返回,不做任何操作。
    if (!bp)
        return;

    // 获取块的大小size,通过HDRP宏获取该块的头部指针,使用PUT和PACK宏将头部和脚部标记为未分配。
    size_t size = GET_SIZE(HDRP(bp));

    // PUT和PACK宏分别用于在内存中存储和读取数据
    PUT(HDRP(bp), PACK(size, 0));
    PUT(FTRP(bp), PACK(size, 0));

    // coalesce函数用于将释放的空间与相邻的的未分配空间合并。若相邻的空间处于空闲状态,则可以合并成为一个较大的空闲块,放入到空闲链表中进行维护。
    coalesce(bp);
}

// 这个函数是将ptr 指向的已分配块, 从oldSize 减小为 newSize
// 它将已经分配的块的剩余空间划分成一个新的空闲块,并更新分配块的大小。
// 该函数主要用于空间分配时,如果总分配空间比当前块的大小大,将当前块切割,剩下的空间分配给其他块。
// 执行内存分配,并更新原分配的块的标志位,并将未使用的空间分割成为一个新的空闲块。
inline static void place_alloc(void *ptr, size_t oldSize, size_t newSize)
{
    // 参数ptr指向分配块的起始地址,oldSize为原分配块的大小,newSize为新分配块的大小。
    // 利用HDRP和FTRP宏获取ptr指向块的头部和脚部,将它们的标志位设置为已分配的状态,即第0位为1。
    PUT(HDRP(ptr), PACK(newSize, 1));
    PUT(FTRP(ptr), PACK(newSize, 1));

    // 通过NEXT_BLKP宏获取ptr指向块的下一个块的地址,并将该地址赋值给newFreeBlock变量。
    // 然后,将newFreeBlock的头部和脚部标记为未分配,即第0位为0,剩余位为oldSize - newSize
    void *newFreeBlock = NEXT_BLKP(ptr);
    PUT(HDRP(newFreeBlock), PACK(oldSize - newSize, 0));
    PUT(FTRP(newFreeBlock), PACK(oldSize - newSize, 0));

    // 最后,调用coalesce函数对newFreeBlock进行合并操作
    coalesce(newFreeBlock);
}

// 重新扩大大小为newSize
void *mm_realloc(void *ptr, size_t newSize)
{
    // 首先,如果newSize为0,则释放ptr指向的块,并返回NULL
    if (newSize == 0)
    {
        mm_free(ptr);
        return NULL;
    }

    // 如果ptr为NULL,则直接调用mm_malloc函数分配新的内存空间。
    if (ptr == NULL)
        return mm_malloc(newSize);

    // 先将newSize对齐
    // 如果newSize小于等于DSIZE(16字节),则将其增加为DSIZE+OVERHEAD的大小;
    if (newSize <= DSIZE) // 对齐
        newSize = DSIZE + OVERHEAD;
    else  // 否则,将其舍入为DSIZE的倍数加上OVERHEAD大小。
        newSize = DSIZE * ((newSize + (OVERHEAD) + (DSIZE - 1)) / DSIZE);

    // 获取指针ptr指向的块的大小oldSize,如果oldSize与newSize相等,直接返回ptr指向的块
    void *newp;
    size_t oldSize = GET_SIZE(HDRP(ptr));
    if (oldSize == newSize)
        return ptr;
    // 如果oldSize大于等于newSize, 缩减,并且缩减之后能多出空闲块,就分割
    else if (oldSize >= newSize)
    {
        // 如果缩减之后分配块仍然大于最小块的大小(即DSIZE+OVERHEAD),则使用place_alloc函数将原来的块重新分配,并返回ptr。
        if (oldSize >= newSize + (DSIZE + OVERHEAD))
            place_alloc(ptr, oldSize, newSize);
            // 如果剩余的内存大小足够重新形成一个大小为newSize的分配块,那么只需要将原先块的大小改变为newSize并分割成为一个新的空闲块,即可满足需要。
        return ptr;
    }
    // 如果以上两个情况都不满足,意味着需要将ptr所指向块的大小扩大到newSize
    // 如果ptr所指向的块后面紧跟着一个未分配的块,则考虑将两个块合并并进行分配。
    if (!GET_ALLOC(HDRP(NEXT_BLKP(ptr))))
    { 
        // 首先获取当前块和下一个块的大小
        size_t trySize = GET_SIZE(HDRP(ptr)) + GET_SIZE(HDRP(NEXT_BLKP(ptr)));
        // 如果合并后的大小大于等于newSize,则将两个块合并,并通过调用place_alloc函数将新块切割为大小为newSize
        if (trySize - newSize >= 0)
        {
            /* 合并:空闲链表中的free块并设置新块的头尾  */
            freeNode(NEXT_BLKP(ptr));
            PUT(HDRP(ptr), PACK(trySize, 1));
            PUT(FTRP(ptr), PACK(trySize, 1));
            if (trySize - newSize >= DSIZE + OVERHEAD)
                place_alloc(ptr, trySize, newSize);
            return ptr;
        }
    }

    // 如果两个块的合并无法满足需求,只能重新分配一个大小为newSize的新块。
    // 调用mm_malloc函数尝试分配一块新的内存,并使用memcpy函数将原来的内存中的数据拷贝到新的内存中。
    if ((newp = mm_malloc(newSize)) == NULL)
    {
        printf("ERROR: mm_malloc failed in mm_realloc\n");
        exit(1);
    }
    memcpy(newp, ptr, oldSize);
    
    // 最后,释放原来的内存,并返回新分配的内存的首地址。
    mm_free(ptr);
    return newp;
}

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

    /* 分配doubleword以保持对齐方式 最小size是 16字节 即 2 * DSIZE*/
    size = (words % 2) ? (words + 1) * DSIZE : words * DSIZE;

    // 使用 mem_sbrk 函数,把空闲空间分配给调用者。如果这一步失败了,则函数返回 NULL,表示空间不足。
    if ((bp = mem_sbrk(size)) == (void *)-1)
        return NULL;

    /* Initialize free block header/footer and the epilogue header */
    // 初始化空闲块的头和尾,并为堆中的结尾块设置一个新的头。头和尾的大小为8字节,然后把头和尾填入块中的相应位置。
    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 */

    // 将新分配的块与上一个空闲块合并。返回指向新空闲块的指针。
    return coalesce(bp);
}

// 分割出空闲块bp中 大小为asize空间, bp 变为分配块
static void place(void *bp, size_t asize)
{
    // 获取空闲块的大小 csize
    size_t csize = GET_SIZE(HDRP(bp));

    // 调用 freeNode 函数从空闲列表中删除相应的空闲块。因为该空闲块将被分配给用户,所以不再是空闲块
    freeNode(bp);

    // 检查如果将分配的块放置在该空闲块中,是否会留下一个足够大的空闲块,以便将其添加到空闲列表中。
    // 这个检查是用来避免碎片的产生,碎片产生时会浪费内存空间。

    // 如果所剩空间大于等于所需的最小空间(即用户请求的大小与头和尾的总大小再加上 OVERHEAD),则将空间分配给用户,并将剩余的空间放回空闲列表。
    // 分配的空间大小由 asize 指定。
    if ((csize - asize) >= (DSIZE + OVERHEAD))
    {
        PUT(HDRP(bp), PACK(asize, 1));
        PUT(FTRP(bp), PACK(asize, 1));
        bp = NEXT_BLKP(bp);
        PUT(HDRP(bp), PACK(csize - asize, 0));
        PUT(FTRP(bp), PACK(csize - asize, 0));
        insertFreeList(bp); // 加入新的空闲块
    }
    else // 如果剩余的空间不足以成为一个新的空闲块,则直接将整个空闲块分配给用户, 将空闲块标记为已分配。
    {
        PUT(HDRP(bp), PACK(csize, 1));
        PUT(FTRP(bp), PACK(csize, 1));
    }
}

static void *find_first_fit(size_t size);
static void *find_best_fit(size_t size);
static inline void *find_fit(size_t asize)
{
    void *bp = find_best_fit(asize);
    return bp;
}

// 首次适配
static void *find_first_fit(size_t size)
{
    for (void *bp = GET_SUCC(head); bp != tail; bp = GET_SUCC(bp))
    {
        if (GET_SIZE(HDRP(bp)) >= size)
        {
            return bp;
        }
    }
    return NULL;
}
// 最佳适配 size 是用户请求的大小
static void *find_best_fit(size_t size)
{
    size_t size_gap = 1 << 30;
    void *best_addr = NULL, *temp;
    // 遍历空闲链表,对于每个块,计算块大小与请求大小之间的差距 size_gap。
    for (void *bp = GET_SUCC(head); bp != tail; bp = GET_SUCC(bp))
    {
        temp = HDRP(bp); // 使用 HDRP 宏来获取当前块的头部指针
        // 如果 size_gap 小于先前观察到的最小差距,则更新最佳地址 best_addr 和最小差距 size_gap。
        if (GET_SIZE(temp) - size < size_gap)
        {
            size_gap = GET_SIZE(temp) - size;
            best_addr = bp;
            // 如果找到一个大小与请求大小相等的块,则直接返回该地址。
            if (GET_SIZE(temp) == size)
                return best_addr; // 相等就是最佳,可直接返回
        }
    }
    return best_addr;
}

// 这个函数负责加入 新出来的空闲块到显式空闲链表并且合并相邻的空闲块
static void *coalesce(void *bp)
{
    // 程序的入口参数bp是待合并的空闲块的指针。首先程序获取了与bp相邻的前后两个块的状态,以及bp块的大小。
    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));

    // prev_alloc和next_alloc均为1(即当前块前后均已分配)。
    // 此时不需要合并相邻的块,只需要将bp块插入空闲链表。
    if (prev_alloc && next_alloc)
    {
        insertFreeList(bp);
    }
    // prev_alloc为1,next_alloc为0(即当前块的后面是一个未分配的块)。
    // 此时需要将当前块和后面的块合并,然后将合并后的块放入空闲链表中。
    // 合并后块的大小,应当为它们本身的大小以及中间的header和footer的大小之和。
    else if (prev_alloc && !next_alloc)
    {
        freeNode(NEXT_BLKP(bp));
        size += GET_SIZE(HDRP(NEXT_BLKP(bp)));
        PUT(HDRP(bp), PACK(size, 0));
        PUT(FTRP(bp), PACK(size, 0));
        insertFreeList(bp);
    }
    
    // prev_alloc为0,next_alloc为1(即当前块的前面是一个未分配的块)。
    // 此时需要将前面的块和当前块合并,然后返回合并后的块的指针。
    // 合并后块的大小,应当为它们本身的大小以及中间的header和footer的大小之和。
    else if (!prev_alloc && next_alloc)
    {
        size += GET_SIZE(HDRP(PREV_BLKP(bp)));
        PUT(FTRP(bp), PACK(size, 0));
        PUT(HDRP(PREV_BLKP(bp)), PACK(size, 0));
        bp = PREV_BLKP(bp);
    }
    
    // prev_alloc为0,next_alloc为0(即当前块的前后均未分配)。
    // 此时需要将前后两块和当前块合并,然后返回合并后的块的指针。
    // 合并后块的大小,应当为它们本身的大小以及中间的header和footer的大小之和。
    else
    {
        freeNode(NEXT_BLKP(bp));
        size += GET_SIZE(HDRP(PREV_BLKP(bp))) + GET_SIZE(FTRP(NEXT_BLKP(bp)));
        PUT(HDRP(PREV_BLKP(bp)), PACK(size, 0));
        PUT(FTRP(NEXT_BLKP(bp)), PACK(size, 0));
        bp = PREV_BLKP(bp);
    }

    return bp;
}

实验结果


笔者才疏学浅,本次实验参考了许多其他博客,笔者对这些博主表示感谢,感谢各位的精彩分享,让我受益良多,谢谢!

参考链接:

brk和sbrk_八两半斤的博客-CSDN博客

谈谈物理内存与虚拟内存之间的映射(超详细~) - 知乎

CSAPP: MallocLab多个链表版本的实现与推荐 - 知乎

Malloc tutorial

brk(2) - Linux manual page

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

CSAPP Lab5- MallocLab 的相关文章

  • ASP.NET Web 应用程序中的身份验证遇到问题

    我正在尝试对从登录页面登录我的 Web 应用程序的用户进行身份验证 我正在使用本教程 http support microsoft com kb 301240作为指南 它几乎准确地解释了我希望做什么 但是当我输入用户名和密码时 验证不起作用
  • STL之类的容器typedef快捷方式?

    STL 容器的常见模式是这样的 map
  • NDK 应用 onDestroy 清理 - 如何 DetachCurrentThread

    因此 如果我们连接 我们必须在完成后分离线程 对吗 JNIEnv get jni env JNIEnv res JAVA VM gt GetEnv void res JNI VERSION 1 6 Using cached JavaVM J
  • 平滑手绘曲线

    我有一个允许用户绘制曲线的程序 但这些曲线看起来不太好 它们看起来摇摇欲坠 而且是手绘的 所以我想要一种能够自动平滑它们的算法 我知道平滑过程中存在固有的模糊性 因此它不会每次都完美 但这种算法似乎确实存在于多个绘图包中 并且它们工作得很好
  • 在没有 epsilon 的情况下可以将浮点数与 0.0 进行比较吗?

    我知道 要比较两个浮点值 需要使用一些 epsilon 精度 因为它们并不精确 但是 我想知道是否存在边缘情况 我不需要那个 epsilon 特别是 我想知道这样做是否总是安全的 double foo double x if x lt 0
  • 如何从 Qt 应用程序通过 ODBC 连接到 MySQL 数据库?

    我有一个新安装的 MySQL 服务器 它监听 localhost 3306 从 Qt 应用程序连接到它的正确方法是什么 原来我需要将MySQL添加到ODBC数据源 我在遵循这个视频教程后做到了这一点 https youtu be K3GZi
  • 嵌入资源文件的路径

    我的资源文件中有一个图标 我想引用它 这是需要图标文件路径的代码 IWshRuntimeLibrary IWshShortcut MyShortcut MyShortcut IWshRuntimeLibrary IWshShortcut W
  • ContentDialog 未与 UWP 中心对齐

    据我所知 ContentDialog的默认行为应该是使其在 PC 上居中并在移动设备上与顶部对齐 但就我而言 即使在 PC 上我也将其与顶部对齐 但我不明白发生了什么 我正在使用代码隐藏来创建它 这是我正在使用的代码片段 Creates t
  • 在c#中获取没有时间的日期

    我的表上有一列 缺勤日期时间 日期 当我想要获取包含日期的行时 它返回 0 行 这是我的 C 代码 DateTime ClassDate DateTime Parse lblDate Content ToString var Abs dbs
  • 配置:错误:无法运行C编译的程序

    我正在尝试使用 Debian Wheezy 操作系统在我的 Raspberry Pi 上安装不同的软件 当我运行尝试配置软件时 我尝试安装我得到此输出 checking for C compiler default output file
  • 您对“大规模 C++ 软件设计”的看法 [关闭]

    Closed 这个问题是基于意见的 help closed questions 目前不接受答案 正在阅读亚马逊评论 https rads stackoverflow com amzn click com 0201633620 and ACC
  • 如何构建一棵与或树?

    我需要一个支持 与 和 或 的树结构 例如 给定一个正则表达式 如ab c d e 我想把它变成一棵树 所以 一开始我们有两个 或 分支 它可以向下ab or c d e 如果你低头ab分支 你得到两个节点 a and b or a其次是b
  • 使用数据绑定,如何将包含表情符号的文本绑定到标签并使其正确显示?

    我正在编写一个应用程序来连接 WordPress BuddyPress API 该应用程序将允许用户通过 API 相互发送消息 当这些消息包含表情符号时 我很难正确显示它们 以下是 API 返回的消息文本的简短示例 Hi x1f642 ho
  • 传递数组时在 C 中的函数参数中强制指定数组大小

    Context 在 C 中 我有一个以数组作为参数的函数 该参数用作该函数的输出 输出的大小始终相同 我会 让阅读代码的人清楚所需的大小 不过它已经在函数注释中了 理想情况下 编译会输出警告或错误 这样我就可以在编译时而不是运行时防止出现问
  • 如何同步nosql db(ravendb)中的更改

    我已经开始在 RavenDB 的示例上学习 NoSQL 我从一个最简单的模型开始 假设我们有由用户创建的主题 public class Topic public string Id get protected set public stri
  • 选择合适的IDE

    您会推荐使用以下哪种 IDE 语言来在 Windows 下开发涉及识别手势并与操作系统交互的项目 我将使用 OpenCV 库来执行图像处理任务 之后 我将使用 win32 API 或 NET 框架与操作系统交互 具体取决于您建议的工具 性能
  • 卸载程序

    我正在尝试使用此代码卸载程序 但它似乎不起作用 我尝试过其他答案 但似乎也不起作用 有人可以帮助我吗 我正在尝试按给定名称 displayName 卸载该程序 例如 我给出 displayName Appname 那么此代码应该从我的计算机
  • Boost.asio和异步链,unique_ptr?

    我对异步编程不太熟悉 我有一个问题 我的问题如下 给出 boost asio 中 C 11 的 echo server 示例 http www boost org doc libs 1 60 0 doc html boost asio ex
  • 为什么在构造函数中设置字段是(或不是)线程安全的?

    假设您有一个像这样的简单类 class MyClass private readonly int a private int b public MyClass int a int b this a a this b b public int
  • 使用 CodeDOM 将程序集添加到 BuildManager 会导致间歇性错误

    我正在使用 CodeDOM 在运行时创建内存中程序集 如下所示 public Assembly Compile CodeCompileUnit targetUnit string path Path GetDirectoryName new

随机推荐

  • 【php】密码正则格式校验

    密码由字母或数组组成 function is passwd v pattern 0 9a zA Z 6 16 i if preg match pattern v System FileLog FileLog write h5密码重置 tru
  • el-table渲染二级对象数组

    1 序言 项目地址如下 https gitee com liu wenxin complexELTable git 想要渲染这样的数据 el table官网给的例子都是一级对象数组 如果想要渲染二级对象数组 直接 table tableDa
  • 什么是多阶段 Docker 镜像?

    介绍 让我们从基础开始 码头工人 这是一个很棒的工具 可以让您在这些简洁的小容器中创建 部署和运行应用程序 将它们想象成微型虚拟机 但只有足够的资源来运行您的应用程序 这太棒了 因为这意味着您可以在任何平台上运行您的应用程序 从笔记本电脑到
  • Raneto

    Raneto Raneto是一个采用Node js开发的免费开源知识库平台 它使用Markdown文件来存储知识库 Raneto也可以叫作一个 静态网站生成器 因为它不需要数据库 你的所有内容都存储在 Markdown md 文件中 整个知
  • Vue2 +Element-ui实现前端页面

    1 页面项目 以一个简单的前端页面为例 主要是利用vue和element ui实现 里面涉及的主要包括 新建vue项目 一行多个输入框 页面实现等 2 项目流程 1 新建项目 首先安装nodejs 这部分在此就不讲啦 然后安装vue cli
  • 关于BIO、AIO、NIO的 区别于联系

    目录 IO BIO NIO AIO 区别及联系 各自适用场景 使用方式 IO 什么是IO 它是指计算机与外部世界或者一个程序与计算机的其余部分的之间的接口 它对于任何计算机系统都非常关键 因而所有 I O 的主体实际上是内置在操作系统中的
  • 深度学习——贝叶斯神经网络

    文章目录 前言 什么是贝叶斯神经网络 How to train BNN BNN背后的数学原理 pytorch实现BNN 参考文献 前言 本文将总结贝叶斯神经网络 首先 我将简单介绍一下什么是贝叶斯神经网络 BNN 接着我将介绍BNN是怎么训
  • Orcad Captue原理图更改后同步更新到Allegro PCB

    Cadence软件的原理图和PCB是两个软件 原理图到PCB需要创建网表 然后在PCB中放置已经建好分封装的元器件 如果对于已经画好的PCB 后期发现需要更改原理图的某个部分 原理图更改后 怎么同步更新到PCB中 而不变动原来已经布好局 布
  • MySQL备份及恢复

    目录 MySQL备份 MySQL备份方法 备份策略 mysql的完全备份 mysql的增量备份 MySQL恢复 mysql完全恢复 mysql增量备份的恢复 MySQL备份 MySQL备份是基于对MySQL的日志进行备份 且恢复也是通过日志
  • Multispectral Pedestrian Detection using Deep Fusion Convolutional Neural Networks (深度学习多光谱行人检测综述)

    Now salient detection methods most of current pedestrian detectors explored color images of good lighting and they are v
  • 基于Arduino的双向交通灯系统

    学 号 231 姓 名 福福 指导教师 赵志鹏 年 7 月 2 日 实训题目 交通灯系统 1 系统设计 设计要求 设计任务 设计基于Arduino的双向交通灯系统 实现控制和方向显示功能 性能指标要求 1 基本要求 1 按照题目要求独立完成
  • 动态显示带图片列表【Android】

    一 功能描述 1 动态获取服务器端商品信息显示 2 动态加载服务器端图片显示 二 技术点 ListView BaseAdapter JSON数据解析 Handler Thread HttpUrlConnection AsyncTask Ht
  • unity 字体 素材_教程篇

    Unity异步加载 在使用Unity引擎开发项目时 很多时候需要用到异步加载场景或资源 如需打开一个非常大的场景时需要等待 两个场景之间的一个过渡 游戏关卡的加载等等 通过添加一个进度条的方式可以很好的增强用户体验 并让用户了解场景的加载进
  • 【DevOps】Centos 7.9 安装、部署与使用 k8s集群(v1.21.3)

    卸载 k8s集群 1 平滑移除 Node kubectl get node kubectl cordon node name 不可调度 kubectl drain node name 驱逐资源 kubectl delete node nam
  • 【Python_requests学习笔记(一)】基于requests和re模块,爬取百度图片

    基于requests和re模块 爬取百度图片 前言 此篇文章中介绍requests的基本用法 和基于requests和re模块 爬取百度图片的案例 正文 1 requests模块 1 1 requests模块介绍 requests模块 类似
  • 看书标记【统计学习理论与方法】1

    第一章 概率论基础 在R中 分布函数名为func 则形如dfunc的函数就提供了相应的概率分布函数 dfunc x p1 p2 形如pfunc的函数提供了相应的累积分布函数 pfunc q p1 p2 分位数函数 p为由概率构成的向量 qf
  • go 首字母大写,代表对外部可见,首字母小写代表对外部不可见,适用于所有对象,包括函数、方法

    go中根据首字母的大小写来确定可以访问的权限 无论是方法名 常量 变量名还是结构体的名称 如果首字母大写 则可以被其他的包访问 如果首字母小写 则只能在本包中使用 可以粗暴的理解为首字母大写是公有的 首字母小写是私有的
  • yolov5车辆识别、目标检测教程

    车辆识别视频 yolov5车辆识别视频 车牌识别视频 订阅专栏获得源码 我们首先介绍一下最原始的YOLO模型 然后简要介绍一下YOLOv5版本的改进 主要通过具体的例子一起看看怎么把YOLOv5模型用好 YOLOv1的网络结构并没有什么特别
  • Nacos启动出现Error creating bean with name ‘memoryMonitor‘ 、‘externalDumpService‘

    目录 问题 解决方法 这里是CS大白话专场 让枯燥的学习变得有趣 没有对象不要怕 我们new一个出来 每天对ta说不尽情话 好记性不如烂键盘 自己总结不如收藏别人 问题 用KubeSphere创建Nacos时出现Error creating
  • CSAPP Lab5- MallocLab

    实验目标 本实验需要用c语言实现一个动态的存储分配器 也就是你自己版本的malloc free realloc函数 实验步骤 tar xvf malloclab handout tar解压文件 我们需要修改的唯一文件是mm c 包含如下几个