文章目录
malloc 和 free 作为 c 语言中的内存申请函数(new是运算符),malloc 实际上依据申请的空间大小,大空间调用os API(HeapAlloc()),小空间使用SBH(小于等于1K)。
1. SBH 概观
SBH 是 Small Block Heap 的缩写,可见于 VC6 的源码当中,而迭代到 VC10 及之后版本,SBH机制虽继续存在,但已经整合至操作系统 API 当中去了(Windows Heap)。以下内容均以 VC6 版本为例进行展开。
SBH 顾名思义,即小内存区块栈。在实际使用中,若是请求的内存空间 size > threshold 则使用 HeapAlloc() 从 _crtheap 当中取用内存,若 size < threshold(_sbh_threshold = 1016 = 1024-8,SBH负责1KB及以下大小内存区块的分配请求,同时1KB内存大小又有固定的两个4Bytes的cookie开销,所以实际大小为1016Bytes)则从 SBH 当中取(实际区块来自于 VirtualAlloc(),也是os API,先分配虚拟地址池)。
了解 SBH 的整个运作流程,需对流程过程有个大致的概念,概念最好的建立方法是看 SBH 运行时的系统调用栈(Call Stack),见下图:

2. SBH 运作流程
对于整个 SBH 进行小于 1KB(1016Bytes) 的内存分配的流程结合系统调用栈总结如下:
2.1 _heap_init() 和 __sbh_heap_init()
作为 SBH 的开始阶段所调用的两个函数。由 __cdel_heap_init()
创建/申请出一个大的内存区块 _crtheap;若是申请内存成功,调用 __cdecl_sbh_heap_init()
在其上配置/创建 SBH 运作所需的 HEADERs, REGIONs 等控制/组织信息做管理之用。


可以由上图看到,其中的 BITVEC 是 unsigned int 类型的别名。对于 bitEntryHi 和 bitEntryLo 则均为32的空间,使用拼接的概念,高32位和低32位(不是很清楚作用,设想是存储下一个Header地址?针对64bit系统的话,不过Header大小固定的话若是连续区间也无需这么设定?);还一个 bitvCommit 字段,是为了记录相应序号的 Group 是否已经 VirtualAlloc(addr, 32KB, MEM_COMMIT,…) 过,即是否已经实际分配了内存。
2.2 _ioinit()
在经过 2.1 中步骤后,SBH 的管理内容所包含的 16个HEADERs 的建置即告一段落,接下来即是对 分配内存请求 进行处理的过程了。
2.2.1 _malloc_dbg()阶段
依据调用栈的调用顺序,在依次调用 _heap_init() 和 __sbh_heap_init() 并逆序返回后,应当调用 _ioinit() 函数了,遵照首次分配为 dbg 模式的设定?,首先调用 _malloc_dbg()
(在 #ifndef _DEBUG 无效/未命中的情况下)

2.2.2 _heap_alloc_dbg()阶段
首先了解 SBH 对所分配的内存区块的结构设计/约定是怎样的。可分为三部分 ①_CrtMemBlockHeader;②data[nDataSize];③anotherGap[nNoMansLandSize],由此也可看到最终所分配出去的 data[nDataSize] 的数据空间实际所耗用的内存空间大小 blockSize = sizeof(_CtrMemBlockHeader) + nSize + nNoMansLandSize,如下图所示:
![]() |
![]() |
图(1)中的 nBlockUse 字段表示的是当前内存块的类型,一般是 _NORMAL_BLOCK 和 _CRT_BLOCK(系统使用)两种类型?见_malloc_dbg()阶段的配图;图(2) 对应填充的意思是对内存空间进行抹除/格式化,而不是初始化。
2.2.3 _heap_alloc_base() 阶段
以上阶段计算出了为处理该次内存请求所需的实际内存空间大小并对相应大小的内存空间进行抹除或格式化。
那么该阶段负责所分配内存空间 实际的内存分配 和 组织信息的建置更新。
- 内存分配方案的选用 & __sbh_alloc_block():
//若是所需实际内存大小小于1K,减去cookie大小4*2,使用SBH进行分配
if (size <= _sbh_threshold) { //_sbh_threshold = 3F8, i.e. 1016
//1024 - 8 = 1016,减去两个4bytes的cookie
pvReturn = __sbh_alloc_block(size);
if (pvReturn) return pvReturn;
}
//若是大于1K,则使用HeapAlloc()这一os API进行内存分配
if (size = 0) size = 1;
size = (size+ ...) & ~(...); //将size调整为8的倍数?
return HeapAlloc(_crtheap, 0, size);
接下来假设所分配的内存大小小于1016bytes,即使用SBH进行内存块的分配。所以接下来依据调用栈,将调用 __sbh_alloc_block()
函数,其间由于无法保证 size 是16的倍数,所以类似于std::alloc中的ROUNDUP()行为,将其大小向上调整为16的倍数。
// add 8 bytes entry overhead and round up to next para size
sizeEntry = (intSize + 2 * sizeof(int)
+ (BYTES_PER_PARA - 1))
& ~(BYTES_PER_PARA - 1);
在操作系统中一般将 16 倍数大小的内存空间称为一个 PARA,4 倍数大小的内存空间成为一个 PAGE;
- __sbh_alloc_new_region()
SBH中的 REGION 的结构包含四个字段,如下图所示:

- __sbh_alloc_new_group()
观察以下两图,可以看到group的具体结构,每个group可支配和管理的内存大小为32KB,而后又分为8个page,每个page是4KB,这也是为何计算机分配内存的最小单位为4KB的原因。在看时可结合上图中的group的具体实现结构进行理解。
![]() |
![]() |
经由以上步骤,SBH 已经将一定数量的内存空间纳入自己的管理之下,并能够依照自己的规则将这些内存分配给客户以满足客户所需的请求。
2.3 VC6 内存管理的分配和释放
由ioinit.c,line#81申请大小为100h的内存空间,通过加上head和tail等控制信息后大小为130h,因为是第一次分配,所以从#63链表进行分配,实际上应当由#18链表进行分配(130h=若该序号的链表上有空闲空间的话,130h=304,304/16-1=18)。
因为是第一次分配,之前所进行的虚拟地址分配(MEM_RESERVE)就需要进行实际分配物理地址了(MEM_COMMIT)了(注意到每次申请1MB的内存空间),分配完毕并将内存交给客户之前,还须把对应的group当中的cntEntries字段+1。同时给交给客户的内存空间两端建立区隔,区隔当中的数值应当是该块内存数据空间大小+1。

若是经过了14轮内存分配之后,第15次的操作为内存释放,此时所归还的内存空间仍从属于#0 group管理,归还的空间大小为240h(240h=576, 576/16-1=35),即应当归还至#35序号的链表,图片中的序列共16位,表示的是16进制下的64位数据。可以看到归还后的数据应当为(0x 0000 0000 1000 0001,即第36位应该更改状态为1,表示第36个链表有可支配的内存空间)。

经由以上步骤,第16次操作是申请190h大小的内存空间(190h=400,400/16-1=24),所以应当由#24list来提供内存空间,但是此时#24list并没有可供分配的空间,由15次操作可知更大空间链表中#35list有空间,所以从240h中划出190h供客户使用,并建立新的数据隔离区隔。下面图片有点问题,划分后的b0大小的区块应当是可以分配的,类似std::alloc作为碎片整理到对应的链表当中去。

3. SBH 实现细节

- 释放区块后对连续空闲区块合并
为了尽量减小内存空间的碎片化,将连续空闲的区块进行合并是SBH进行碎片管理的思路。分为向上合并和向下合并。示意图如上所示。 free(p)
时如何确定p所处的Header,Group,free-list内
思路:由p计算出其处于哪个Header所管理的内存地址范围,再进一步根据p的确切值得到Group号(指针减掉头大小再除以32KB),最后计算得到free-list的序号(长度除以16bytes);- 由上介绍可以看到SBH是分段式对内存进行管理的,其显而易见的好处是可以判断内存是否可以进行全回收,即交还给OS,靠的就是Group当中的cntEntries字段,若是为0,则可进行全回收;
- 内存归还的延迟机制,正如概述图片中所讲,为了内存分配的高效性,对内存的回收采用了Defering的延迟机制,若是有新的内存请求,则先从Defering group当中分配满足需求,若是有新的回收请求,则对上次的回收空间进行真正的回收,将该次回收请求的空间设为Defering group;