os_kernel_lab/related_info/lab2/kr_malloc_free.md

6.9 KiB
Raw Blame History

K&R C book's malloc &Free

来源

from malloc/free函数的简单实现及思考 by 五岳

简单思路

管理free block的数据结构

仅仅依靠地址运算来进行定位是限制分配回收灵活性的原因它要求已使用部分和未使用部分必须通过某个地址分开成两个相邻区域。为了能让这两个区域能够互相交错甚至其中还包括一些没有分配的地址空间需要使用指针把同类的内存空间连接起来形成链表这样就可以处理地址不连续的一系列内存空间。但是为什么只连接了空闲空间而不连接使用中的空间这么问可能出于在对图中二者类比时的直觉而没有经过思考这很简单因为没有必要。前者相互链接是为了能够在内存分配时遍历所有空闲空间并且在使用free()回收已使用空间时进行重新插入。而对于使用中的空间由于我们在分配空间时已经知道它们的地址了回收时可以直接告诉free()并不用像malloc()时进行遍历。

既然提到了链表可能对数据结构稍有了解的人会立刻写下一个struct来代表一个内存区域其中包含一个指向下一个内存区域的指针但是这个struct的其他成员该怎么写呢作为待分配的内存区域大小是不定的如果把它声明为struct的成员变量显然不妥如果声明为一个指向某个其他的区域的指针这似乎又和上面的直观表示不相符合。当然这么做也是可以实现的它看上去是介于上图的两者之间把管理结构和实际分配的空间相剥离在文末我会专门的讨论一下这种实现方法因此这里仍然把控制结构和空闲空间相分开但保持它们在内存地址中相邻形成下图的形式而正由这个特点我们可以利用对控制结构指针的指针运算来定位对应的内存区域。使用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 <unistd.h>就可以使用了。当然要注意命名和stdlib.h中的同名函数是冲突的可以自行改名。   

some Q&A

1.Header与空闲空间相剥离Header中包含一个指向其空闲空间的指针

  这样做未必不可相应地算法需要改动。同时由于Header和空闲空间不再相邻sbrk()获得的空间也应该包含Header的部分内存的分布可能会更加琐碎。当然这也可能带来好处即用其他数据结构对链表进行管理比如按大小进行hash这样查找起来更快。

2.关于sbrk()

  sbrk()也是库函数它能使堆往栈的方向增长具体可以参考brk(), sbrk() 用法详解。

3.可以改进的方面

  空闲空间的寻找是线性的查找过程在内存分配中可以看作是循环首次适应算法在某些情况下可能很慢如果再建立一个数据结构如hash表对不同大小的空间进行索引肯定可以加快查找本身并且能实现一些算法比如最佳匹配。但查找加快的代价是修改这个索引会占用额外的时间这是需要权衡的。

  morecore()中的最小分配空间是宏定义,在实际使用中完全可以作为参数传递,根据需要设定最小分配下限。

4.这个malloc()和系统提供的malloc()的差别?

  其实库函数的malloc在参数为0时可以返回一个NULL而以上写malloc在头部分配成功后总不会返回NULL。这是差别之一换句话说二者实现还是不同的这里只是为了演示malloc的原理并非真正的库函数。