使用了 Segregated Fits 算法来管理内存。
将空闲块按照大小分成不同的类,并将每个类的空闲块用链表连接起来。
每个空闲块都有一个 header 和一个 footer,与 Implicit list 算法相同。
每个 free block 的第一个字保存指向 list 中下一个 free block 的指针,第二个字保存指向前一个 free block 的指针。
因此,每个 block 的最小值为 16 bytes,heap 地址的前 18 个字分别保存 18 个 list 的头指针,平衡了搜索时间和空间利用率.
mem_sbrk
来扩展堆的大小,以满足额外的内存需求。作用:扩展heap的大小
流程如下:
bp
和一个大小变量size
。words
来计算需要分配的字节大小size
。并且保证了size
是一个偶数,以维持对齐。mem_sbrk
用于增加堆的大小。返回一个指向新分配的内存块的指针bp
,如果分配失败,就返回-1。bp
是否为-1,如果是,就返回NULL
,表示扩展堆失败。coalesce
,用于合并相邻的空闲内存块的函数,以减少内存碎片。并且返回一个指向合并后的内存块的指针。作用:合并free block
根据前后相邻的内存块的分配状态,进行不同的操作
流程如下:
最后,把合并后的空闲块插入到空闲链表中,然后返回合并后的块的指针。
作用:得到大小为size的块应该在哪个list中
根据size
的值,返回一个对应的列表的偏移量。列表的偏移量是一个整数,表示这个数据应该存储在哪个列表中
流程如下:
if-else
语句,来判断size
的范围。if-else
语句都有一个常量SIZE1
到SIZE17
,表示不同的大小的阈值。如果size
小于等于某个阈值,就返回相应的偏移量。作用:将free block插入到相应大小的free list中, 插入位置为表头
流程如下:
调用getListOffset
函数,根据内存块的大小size
,得到一个列表的偏移量index
。
使用宏定义的函数,如GET_PTR
,PUT_PTR
,HDRP
等,来操作内存块的头部和指针域。
如果heap_listp + WSIZE * index
处的指针为空,表示这个列表还没有任何内存块,那么就将bp
作为第一个内存块插入到这个列表中,并将它的前驱和后继指针都设为NULL
。
如果heap_listp + WSIZE * index
处的指针不为空,表示这个列表已经有一些内存块,那么就将bp
作为第一个内存块插入到这个列表的头部,并将它的后继指针指向原来的第一个内存块,同时将原来的第一个内存块的前驱指针指向bp
。
最后,将heap_listp + WSIZE * index
处的指针更新为bp
。
作用:删除链表结点
流程如下:
首先,获取结点的大小和位置的偏移量。然后,根据结点在链表中的位置,分为四种情况来处理:
heap_listp
数组中对应的头指针设为结点的前指针,表示链表的头部移动了,然后把结点的前指针所指向的结点的后指针设为NULL,表示这个结点已经不在链表中了。作用:寻找一个合适size的free list
函数的具体逻辑如下:
getListOffset
,根据asize
的值来确定一个索引index
,这个索引表示一个内存块的类别,也就是它的大小范围。index
开始,一直到17
为止heap_listp
中获取一个指针ptr
,这个指针指向当前类别的内存块链表的头部。ptr
为空为止。HDRP
和GET_SIZE
来获取当前内存块的头部和大小,然后判断是否满足asize
的要求,如果是,就返回ptr
作为结果。ptr
更新为下一个内存块的指针,这个指针是通过宏GET_PTR
从当前内存块中获取的。index
加一,进入下一个类别的循环,直到找到合适的内存块或者遍历完所有的类别为止。NULL
作为结果,表示失败。作用:将一个空闲的内存块bp
分割为两部分,一部分用于分配给用户,另一部分保持空闲
函数的具体逻辑如下:
delete_list
,将bp
从空闲链表中删除bp
的当前大小csize
,并判断是否可以将其分割为两个内存块,一个大小为asize
,另一个大小为csize - asize
。这里的条件是csize - asize
必须大于等于2 * DSIZE
,也就是最小的内存块大小。bp
的头部和尾部设置为asize
和已分配的标志,然后将bp
指向下一个内存块,将其头部和尾部设置为csize - asize
和未分配的标志,最后将这个新的空闲内存块插入到空闲链表中,调用insert_list
函数。bp
的头部和尾部设置为csize
和已分配的标志,不做其他操作。mem_sbrk
分配 LISTS_NUM + 4
个字(每个字是 4 字节),作为初始堆空间。LISTS_NUM
是空闲块链表的数量。LISTS_NUM
个空闲块链表头部,并将它们连接到堆的开头。LISTS_NUM * WSIZE
字节的空间。extend_heap
函数,将堆的大小扩展为 CHUNKSIZE
字节。CHUNKSIZE
是在没有足够空闲块的情况下,用来扩展堆空间的默认大小。asize = DSIZE * ((size + (DSIZE) + (DSIZE - 1)) / DSIZE)
。find_fit
函数,在空闲块链表中寻找第一个大小大于等于 asize
的空闲块。place
函数来将其分配出去。extend_heap
来扩展堆空间,并在新的空间上分配内存块。ptr
,标记对应内存块的头部和尾部为未分配状态。PUT
函数将头部和尾部的标志位设置为未分配状态。coalesce
函数来尝试合并释放的块与相邻的空闲块。coalesce
函数会检查前后相邻的块是否也是未分配的,如果是,则会合并这些块,以释放更多连续的空间。NULL
。asize
,这个过程与 mm_malloc
中的大小调整类似。mm_malloc
分配一个新块,并将旧块的内容复制到新块中。首次适配:在 mm_malloc
中,采用 find_fit
函数来在空闲块链表中寻找第一个合适大小的空闲块。这个函数会顺序查找链表,返回第一个大小满足需求的空闲块。
立即分割:当找到的空闲块大小大于请求大小时,在 place
函数中会立即将其分割成两部分,一部分满足用户请求,剩余部分作为新的空闲块。
基本合并策略:在 coalesce
函数中实现了合并相邻的空闲块的逻辑。
mm_malloc
函数中使用了首次适配策略,需要在空闲块链表中顺序查找第一个满足大小的空闲块。当链表中的空闲块数量增多时,查找所需的时间可能会线性增长。mm_free
函数内部主要涉及标记内存块为未分配状态,并尝试进行合并操作。这些操作的时间复杂度主要取决于标记和合并的步骤,而不会随着空闲块链表的大小增加而增加。mm_realloc
函数会在重新分配内存块时,根据新旧大小的比较来决定是缩小、扩大还是重新分配内存块。如果需要重新分配内存块,可能需要调用 mm_malloc
和 mm_free
函数,其中 mm_malloc
的时间复杂度是 O(n)。