1. 从零开始:理解动态内存分配器的骨架
如果你学过C语言,肯定用过malloc和free。它们就像内存世界的“包租公”和“清洁工”:malloc负责给你划一块地皮(内存)用,free负责在你用完后退租时把地皮打扫干净,以便下次出租。CSAPP的MallocLab实验,就是让你亲手扮演这个“包租公”和“清洁工”,从零开始搭建一套内存管理系统。听起来是不是有点吓人?别怕,我当年做这个实验也踩了不少坑,但一步步走下来,你会发现它其实是理解计算机系统底层运作的绝佳窗口,比单纯看书要深刻得多。
这个实验的核心,是让你在mm.c这个文件里,实现四个函数:mm_init, mm_malloc, mm_free, mm_realloc。系统会给你一个非常原始的内存接口mem_sbrk,它只能像伸懒腰一样,向操作系统申请一大块连续的内存空间。你的任务就是在这块“原始荒地”上,建立起一套精细的“物业管理体系”,能够高效地处理不同租户(程序)随时随地的“租地”(申请内存)和“退租”(释放内存)请求。难点在于,租户的需求大小不一,时间交错,你怎么才能避免内存浪费(碎片),又能快速找到合适的空闲地块呢?
实验通常会提供一个基础框架,里面定义了一些关键的宏和常数,比如WSIZE(字大小)、DSIZE(双字大小)。这些是构建我们“物业管理体系”的砖瓦。最经典也最基础的数据结构是隐式空闲链表。你可以把整片堆内存想象成一串首尾相连的“内存块”珠子。每个珠子(块)都有两个关键部分:头部和脚部。头部在块的开头,脚部在块的末尾,它们就像块的“身份证”,记录了这块地有多大(size),以及当前是否有人租用(alloc位,1表示已分配,0表示空闲)。为什么需要脚部?这是为了能方便地从后往前找邻居,进行“合并”操作,这个我们后面会细说。
初始化时,我们会在堆的开头设置几个特殊的“哨兵块”:一个序言块和一个结尾块。序言块是一个很小的、已分配的块,它永远存在,简化了我们处理堆边界的情况——比如第一块空闲块的前面永远是一个已分配的序言块,我们就不用特殊判断了。结尾块是一个大小为0、标记为已分配的块,它就像链表末尾的NULL指针,告诉我们“到此为止了”。在这两个哨兵之间,就是我们可以自由分配和回收的广阔天地了。mm_init函数的工作,就是调用mem_sbrk申请初始内存,然后搭建好这个包含序言块、初始空闲块和结尾块的初始结构。
2. 核心机制:分配、释放与合并的舞蹈
理解了骨架,我们来看看血肉——分配和释放内存时具体发生了什么。这整个过程就像一场精心编排的舞蹈,每一步都要考虑前后衔接。
2.1 内存分配:寻找与安置
当程序调用mm_malloc(size)时,我们不是简单地把size字节交给它。首先,我们需要规整化请求大小。因为我们要对齐内存(通常是8字节),并且每个块都需要额外的空间存放头部和脚部(各4字节),所以实

380

被折叠的 条评论
为什么被折叠?



