malloc/free函数的简单实现及思考
用于内存管理的malloc/free这对函数,对于使用C语言的程序员应该很熟悉。前段时间听说有的IT公司以“实现一个简单功能的malloc”作为面试题,正好最近在复习K&R,上面有所介绍,因此花了些时间仔细研究了一下。毕竟把题目做出来是次要的,了解实现思想、提升技术才是主要的。本文主要是对malloc/free实现思路的介绍,蓝色部分文字是在个人思考中觉得比较核心的东西;另外对于代码的说明,有一些K&R上的解释,使用绿色加亮。
在研究K&R第八章第七节的实现之前,不妨先看看其第五章第四节的alloc/afree实现,虽然这段代码主要目的是展示地址运算。
alloc实现
这种简单实现的缺点:
1.作为代表内存资源的allocbuf,其实是预先分配好的,可能存在浪费。
2.分配和释放的顺序类似于栈,即“后进先出”,释放时如果不按顺序会造成异常。
这个实现虽然比较简陋,但是依然提供了一个思路。如果能把这两个缺点消除,就能够实现比较理想的malloc/free。
仅仅依靠地址运算来进行定位,是限制分配回收灵活性的原因,它要求已使用部分和未使用部分必须通过某个地址分开成两个相邻区域。为了能让这两个区域能够互相交错,甚至其中还包括一些没有分配的地址空间,需要使用指针把同类的内存空间连接起来形成链表,这样就可以处理地址不连续的一系列内存空间。但是为什么只连接了空闲空间而不连接使用中的空间?这么问可能出于在对图中二者类比时的直觉而没有经过思考,这很简单,因为没有必要。前者相互链接是为了能够在内存分配时遍历所有空闲空间,并且在使用free()回收已使用空间时进行重新插入。而对于使用中的空间,由于我们在分配空间时已经知道它们的地址了,回收时可以直接告诉free(),并不用像malloc()时进行遍历。
既然提到了链表,可能对数据结构稍有了解的人会立刻写下一个struct来代表一个内存区域,其中包含一个指向下一个内存区域的指针,但是这个struct的其他成员该怎么写呢?作为待分配的内存区域,大小是不定的,如果把它声明为struct的成员变量显然不妥;如果声明为一个指向某个其他的区域的指针,这似乎又和上面的直观表示不相符合。(当然,这么做也是可以实现的,它看上去是介于上图的两者之间,把管理结构和实际分配的空间相剥离,在文末我会专门的讨论一下这种实现方法)因此,这里仍然把控制结构和空闲空间相分开,但保持它们在内存地址中相邻,形成下图的形式,而正由这个特点,我们可以利用对控制结构指针的指针运算来定位对应的内存区域:
对应地,把控制信息定义为Header:
复制代码
typedef long Align;/for alignment to long boundary/
union header {
struct {
union header ptr; /*next block if on free list/
unsigned size; /size of this block/
} s;
Align x;
};
typedef union header Header;
复制代码
使用union而不是直接使用struct的原因是为了地址对齐。这里是long对齐,union的x永远不会使用。
这样,malloc的主要工作就是对这些Header和其后的内存块的管理。
malloc()
实际分配的空间是Header大小的整数倍,并且多出一个Header大小的空间用于放置Header。但是直观来看这并不是nunits = (nbytes+sizeof(Header)-1)/sizeof(Header) + 1啊?如果用(nbytes+sizeof(Header))/sizeof(Header)+1岂不是刚好?其实不是这样,如果使用后者,(nbytes+sizeof(Header))%sizeof(Header) == 0时,又多分配了一个Header大小的空间了,因此还要在小括号里减去1,这时才能符合要求。
malloc()第一次调用时建立一个退化链表base,只有一个大小是0的空间,并指向它自己。freep用于标识空闲链表的某个元素,每次查找时可能发生变化;中间的查找和分配过程是基本的链表操作,在空闲链表中不存在合适大小的空闲空间时调用morecore()获得更多内存空间;最后的返回值是空闲空间的首地址,即Header之后的地址,这个接口与库函数一致。
morecore()
morecore()从系统申请更多的可用空间,并加入。由于调用了sbrk(),系统开销比较大,为避免morecore()本身的调用次数,设定了一个NALLOC,如果每次申请的空间小于NALLOC,就申请NALLOC大小的空间,使得后续malloc()不必每次都需要调用morecore()。对于sbrk(),在后面会有介绍。
这里有个让人惊讶的地方:malloc()调用了morecore(),morecore()又调用了free()!第一次看到这里时可能会觉得不可思议,因为按照惯性思维,malloc()和free()似乎应该是相互分开的,各司其职啊?但请再思考一下,free()是把空闲链表进行扩充,而malloc()在空闲链表不足时,从系统申请到更多内存空间后,也要先把它们转化成空闲链表的一部分,再进行利用。这样,malloc()调用free()完成后面的工作也是顺理成章了。根据这个思想,后面是free()的实现。在此之前,还有几个morecore()自身的细节:
1.如果系统也没有空间可以分配,sbrk()返回-1。cp是char *类型,在有的机器上char无符号,这里需要一次强制类型转换。
2.morecore()调用的返回值看上去比较奇怪,别担心,freep会在free()中修改的。使用这个返回值也是为了在malloc()里的判断、p = freep的再次赋值的语句能够紧凑。
free()
free()首先定位要释放的ap对应的bp与空闲链表的相对位置,找到它的的最近的上一个和下一个空闲空间,或是当它在整个空闲空间的前面或后面时找到空闲链表的首尾元素。注意,由于malloc()的分配方式和free()的回收时的合并方式(下文马上要提到),可以保证整个空闲空间的链表总是从低地址逐个升高,在最高地址的空闲空间回指向低地址第一个空闲空间。
定位后,根据要释放的空间与附近空间的相邻性,进行合并,也即修改对应空间的Header。两个if并列可以使得bp可以同时与高地址和低地址空闲空间结合(如果都相邻),或者进行二者之一的合并,或者不合并。
完成了这三部分代码后(注意放到同一源文件中,sbrk()需要#include