VC6 malloc
想要搞清malloc是怎么运作的,就必须直到malloc首次分配内存进行了哪些操作,进行了哪些初始化。
很多人认为我们执行一个cpp文件是从main函数开始,但是实际上系统还会为我们进行一系列的环境准备,这些动作通常包含了环境变量的获取、堆的初始化等等一系列操作,在不同环境下可能略有不同。在cpp文件执行之后也存在很多善后操作,这个会在c++前世今生详细介绍。
下面来看一下vc环境下会有哪些动作.
这是c++执行程序的函数栈信息,main函数竟然只排在第8步。我们需要重点关注的函数是_heap_alloc_dbg(),这是真正要分配内存的函数,在这之前,我们先来看看需要分配的内存内容和大小是什么吧。
上述这个结构体用来做内存分配中表头的信息,包含了一个32bit的高位,32bits的低位,32bits的commit信息,一个指针和一个tagRegion结构体指针。
ioinit函数调用malloc_crt函数,这个函数是一个宏定义,在debug模式最终调用_malloc_dbg函数
1 | _malloc_dbg(s,_CRT_BLOCK,_THISFILE,__LINE__); |
上图展示了首次分配内存的大小和初始化情况。在第一次内存分配中,我们首先将nSize作为函数参数传入(256 bytes),_CrtMemBlockHeader是debug模式下要附加到数据上的内容。总的来看数据前面加上了调试信息,对应_CrtMemBlockHeader的每一个字段,数据后面添加了nNoManLandSize = 4B,然后经过对齐之后,我们需要blocksize大小的内存。最后调用heap_alloc_base这个函数申请内存。
在_heap_alloc_dbg()这个函数中,申请完了内存,我们就对它进行初始化,最后返回数据。注意到这是一个双向链表,有指向下一个区块和前一个区块的指针。在debug模式下,数据块两端附着了很多信息,用于变量的跟踪和调试。数据块两端填充的值在图中可以看出,是规定好的值。
获得内存大小之后,判断需要的内存大小是否比阈值小,如果是,就使用sbh处理,否则向操作系统申请Heap内存。为什么阈值是3F8(1016字节)?因为1016+上下两个cookie之后是1024。
在_heap_alloc_base内接下来进行内存对齐。
上面展示了管理1MB内存的数据结构。还记得开始的时候Head的设计吗,它有两个指针,其中一个就是tagRegion指针,这个结构体的内容如右图所示,首先是一个整数,接下来是一个64位char数组,接下来是32个32bits的高位数组和低位数组共32组64bits,用来管理这块内存中某个区块有或没有。最后是32个
tagGroup结构体。
group结构体里面首先是一个整数,接下来有64个listhead,而listhead里面存放双向指针。
然后我们尝试从整体把握。我们要用一个group管理1MB的空间,然后需要new出来这么多结构管理,这些结构本身占16K左右空间。我们的想法是用这32个group来管理1MB的32个片段(32K)。32K对应8页。SBH将这8个page使用双向链表串联起来,然后挂在最后一个listhead上。
我们并不是在一开始就申请1MB的空间,而是申请32K,如果用完了再申请,因此开始时我们拥有的是虚地址。
接下来详细看这个结构,在每个page都有一段保留空间(字节对齐),保留空间下面和最下面都有一段0xffffffff(-1)用来做空间合并,即page内合并不影响page之间的合并。4080记录空间空间的大小。
下面我们来整理一下之前的所有动作。如上图所示:
- 首先在crt的ioinit.c函数的81行申请了100h大小的内存,经过debug header,内存对齐之后大小为130h。
- 初始化64个header,每个header用来管理1MB的内存,此时调用的是VirtualAlloc函数,注意MEM_RESERVE表示保留这些内存(现在是虚地址)。
- 然后初始化region,每个region有32个group,每个group有64个双向指针,这些指针用来管理不同大小的block。
- 然后真正向操作系统申请32k的内存,注意此时的参数是MEM_COMMIT,然后将此块内存分为8个page,使用双向链表连接在一起,挂在group 0 的最后一个双向指针上(group的64个指针管理不同大小区块,从8B到1024B,如果大于1024B就挂在最后一个链表上,由于8K>1K,因此挂在最后)。
- 更改region的32 x 64bits数组的第0号链表的最后一位为1,表示已经有内存块挂载。
- 然后在page1切割内存,4080-130, 改变cookie大小(ec0),下面表示切割的内存,cookie为131,1表示被占用,130表示大小。
回头我们来看一下region的32 x 64bits数组有什么用,哈哈,现在理解了,用来表示32个group的64个链表是否被占用,可以用来快速查询。
那group的int有什么用呢?用来记录分配的次数,如果分配一次就加一,回收一次就减一,便于记录是否回收完备。
当我们第二次分配的时候,先在region里面查找最近的大于目标大小的group的list,然后在上面切割即可。
VC6 free
归还的时候会发生什么呢?请看下图。
假如我们在第15次需要归还240h大小的内存。我们需要做的是:
- 更改group 的count值:14->13.
- 先计算240h/16 = 36,我们应该将这块内存归还到第36(index = 35)链表中。
- 然后利用嵌入式指针技术,将35号链表的指针和这块内存的指针构成一个链表。
那么当有相邻的内存被释放,应该怎么合并呢,请看:
这里就凸显了上下cookie的作用,在free 300这块空间,首先往下看(指针+*指针+4)看下方是否为空,再往上看(指针 - 4)是否为空,如果为空,合并成一个大区间。