之前写过关于内存管理的几篇文章, 但是比较零碎, 网上很多文章又偏于理论或者局限一块内容, 少有一个系列的分析. 一直想自己写个, 正好借助这次培训机会写篇文章, 从源码分析申请内存之后到实际访问内存之间系统究竟做了什么, 探讨一下源码作者如此设计内存管理模块的目的与意义.
暂时规划分四部分完成:
glibc堆内存管理
内核如何管理虚拟地址空间
虚拟内存与物理内存的映射
内核如何处理内存地址异常
本文将以malloc()函数作为入口, 首先分析glibc如何管理动态内存申请与释放, 以此理解一个通用内存管理工具的设计目标. 然后从malloc()调用的系统调用入手, 理解内核如何管理虚拟地址空间的. 第三部分讨论虚拟地址空间与物理页表的映射关系. 最后分析下内核如何处理内存地址异常.
PS: 从源码入手分析往往只见树木不见森林, 更多的相关知识还需要读者自己学习了.
PPS: 源码版本ptmalloc3 / linux-3.10.
PS3: 内核代码与架构强相关, 本文默认以32bit ARMv7为平台分析, 中间可能会掺杂64bit x86分析.
PSP: 想到再说.
正文开始.
glibc的内存管理
glibc使用ptmalloc(最早由Doug Lea实现的dlmalloc经Wolfram Gloger优化多线程而来)做为内存管理工具. 目前ptmalloc最新版为ptmalloc3(2006.5.31), 本文基于此版本分析其实现(主要分析dlmalloc), 源码见http://www.malloc.de/en/, 其中malloc.c即dlmalloc实现,ptmalloc.c为dlmalloc.c的多线程封装.
ptmalloc源码并不复杂, 内存管理仅一个文件约五千行, 其中一半多是注释, 网上还有一堆介绍. 但思考作者设计数据结构与算法的目的是理解代码的最终目的, 换言之思考实现一个快速高效的通用内存管理工具的目标有哪些:
1. 能够应对不同大小内存分配请求(通用)
2. 能够快速分配释放以及回收内存(快速)
3. 尽可能减少管理开销(高效)
3. 其它特点, 如对不同平台兼容性, 内存越界检查, 保留内存供异常时使用等等
在我司代码中也有许多技巧来实现以上目标, 比如预留静态内存, 预留内存分片, 未分配内存首部做双向链表等等. 然而我司的内存管理目标是稳定的码流, 其特点是每次内存请求大小较平均, 一般不会出现特别小如四字节这种情况, 同时前后端稳定收发流, 较早申请的内存也较早释放, 索引内存往往是顺序的, 管理内存块仅需链表即可. 反观glibc, 首先其申请的大小是不限定的, 可能零字节可能上千兆, 分配与释放的顺序也是乱序的, 可能先分配的后释放, 也可能反过来. 因此设计glibc内存管理器要复杂的多: 首先针对第一点需要区分申请的目标, 对于几个字节与几兆的内存申请肯定不能使用同一内存分配器, 第二设计合理的数据结构来根据一个长度索引最合适的空闲内存或根据一个指针查找一个已分配的内存块, 合理的缩减管理头部, 降低额外开销(第三点).
我们首先从dlmalloc的数据结构入手, 分析作者使用何种方式来达成以上目标, 然后我们再分析malloc()函数的实现, 理解glibc究竟是如何管理内存的. 另外提一句评价内存管理工具是否有效, 看的是该工具是否满足使用需求而不是是否完备. 举例而言, dlmalloc虽然很通用可是在分配我司的码流缓存时显然不及我司的内存管理器, 因其大段代码都是无效的, 而有效的代码分配效率也不如我司代码, 因此还是要根据业务需求设计模型.
在分析数据结构之前, 我们先来看下作者的说明(在malloc.c文件首部), 可以更好的帮助我们理解代码.
1. 支持指针与size_t互相转换: size_t必须是无符号类型且位宽与指针一致(4字节或8字节), 否则只能使用较早的2.7.2版本.
2. 对齐(alignment)要求: 默认8字节对齐, 当前大部分机器与C编译器都满足该需求. 但是你也可以定义MALLOC_ALIGNMENT来扩大对齐(最大128字节), 后面我们会发现它必须是8的倍数的原因是因为第三位被用作标记位.
3. 每个内存块(chunk)的最小前缀(overhead): 4或8字节(size_t为4字节), 8或16字节(size_t为8字节). 每个已分配的内存块有一个字长的隐藏前缀用来保存长度及状态信息. 如果定义了FOOTERS则还有一个字长的交叉检查标记.
4. 每个内存块的最小分配长度(包括前缀): 16字节(4字节指针), 32字节(8字节指针). 即使请求0字节(malloc(0))也会分配的空间, 后面我们会发现这个长度实际是sizeof(malloc_chunk). 通常情况下最大前缀浪费(即实际分配减去申请长度)小于等于最小分配长度, 但有个例外是当申请长度大于等于mmap_threshold, 转而使用mmap()申请时会浪费系统页的剩余部分(举例申请一个内存页, 加上管理前缀却申请了两个页).
5. 安全: 指抵挡恶意代码故意出错的能力(比如释放当前未分配的空间或覆盖前一内存块的尾部). 目前可以保证不修改任何内存地址低于堆起始地址的空间, 程序还能侦测部分不合适的free()与realloc(). 如果定义FOOTERS非零则每个已分配的内存块会额外携带一个检查字来确保它被正确分配. 默认情况下侦测到错误会导致程序abort(), 可以定义PROCEED_ON_ERROR来修改错误处理机制.
6. 线程安全: 除非定义USE_LOCKS否则不是线程安全的. 定义USE_LOCKS后, 每次调用malloc, free等都会调用pthread mutex或spinlock(win32). 这会降低运行速度, 可能会成为一个瓶颈. 如果你需要在同步环境中使用malloc, 建议考虑nedmalloc或ptmalloc.
系统需求: 只要定义MORECORE和/或MMAP/MUNMAP宏即可. dlmalloc可以通过unix sbrk或其它模拟机制(通过定义CALL_MORECORE)和/或mmap/munmap或其它模拟机制(通过定义CALL_MMAP/CALL_MUNMAP)来申请与释放系统内存. 在大部分unix系统上它倾向于同时使用两者. 在win32平台上它使用基于VirtualAlloc的模拟机制.
7. 算法概述: 大多数情况下dlmalloc是一个最适合选择分配器. 对于给定请求通常它会选择已存在的最合适的内存块, 使用LRU顺序(使用该策略减少分片). 对于小于256字节的请求, 如果不存在恰好合适的内存块, 则偏向使用(大小)邻近的内存块, 使用MRU顺序. 对于大于256字节的请求依赖与系统内存映射的策略.
8. MSPACES: 如果定义MSPACES, 则除malloc, free外还存在一组mspace_malloc, mspace_free接口. 这组接口会额外接收一个mspace参数, 该参数通过create_mspace接口生成. 如果定义ONLY_MSPACES则只有该组接口会被编译. 可以用来创建线程局部分配器.
9. 编译宏: 太多了, 略.
废话完了, 让我们开始正文. 首先为了记录一个已分配的内存块我们必须要设计一个内存块管理结构, 在dlmalloc中即malloc_chunk. 当一个内存块被分配时, 该结构也会与请求内存一起分配且该结构在申请内存的前部(因此也称overhead).
1 struct malloc_chunk { 2 size_t prev_foot; //记录前一块内存块的长度与状态 3 size_t head; //当前块的长度与状态 4 struct malloc_chunk* fd; //同一大小的内存块的双向链表的前驱 5 struct malloc_chunk* bk; //同一大小的内存块的双向链表的后驱 6 }; 7 typedef struct malloc_chunk mchunk; 8 typedef struct malloc_chunk* mchunkptr; 9 typedef struct malloc_chunk* sbinptr;
这是一个令人迷惑的数据结构, 因为其设计结构与实际内存布局并不一致, 在代码中计算偏移时常常让人困惑. 所以我们先从最初模型开始分析: 一个内存块的管理结构需要什么成员? 首先我们需要一个长度字段记录它的长度(否则free时仅凭一个指针我们无法完成释放内存的工作), 如果该内存块是空闲的我们需要使用双向链表将它管理起来. 另外为了尽可能减少内存碎片我们需要合并空闲内存, 对于当前内存块的后一块起始地址我们是已知的, 然而前一块起始地址我们是未知的, 所以还需要记录前一内存块的长度(与状态). 因此我们需要4个size_t空间存放这些信息. 这是一个非常大的开销, 在32bit平台上即16个字节, 对于一次16字节的请求实际需要分配32字节, 如何降低管理结构的开销? 首先考虑的是时分复用: 管理结构中的双向链表仅在未分配时起效, 当内存被分配后脱离链表管理, 因此这两个指针实际无需分配空间. 同理我们再考虑是否需要前一块内存的长度呢? 当且仅当前一块内存为空闲时我们才会需要合并内存, 因此当前块内存无需记录前一块内存的长度(由前一块内存自己在自己尾部记录, 如果它是空闲内存的话), 只需记录前一块内存是否是空闲的. 因此malloc_chunk结构中的prev_foot实际是前一块内存块的空间, 而fd与bk指针则是当前块的空间, 真正分配的管理字段仅head, 所以前文指出最小管理前缀开销为sizeof(size_t). 这里还需注意的是prev_foot与head字段不仅仅保存长度还保存块状态. 另外空闲内存块的前后必定是非空闲内存块, 否则在内存块释放时必然会合并. 如之前所述, 当前块必须知道前一块是否空闲才能决定prev_foot值是否可信, 因此需要标记内存块的状态. 恰好内存块都是以8字节对齐的, 因此低3位可以用来做为标记位. 其中head字段最低位为pinuse, 指示前一块的状态, 当其置零时前一块内存块为空闲状态, prev_foot包含了前一块内存块的长度, 当其置位时表明前一块内存块已被使用, 无法获取前一块内存块的长度. 通常第一个内存块的pinuse都是置位的, 防止访问不存在的内存. 第二位为cinuse, 指示当前块的状态, 该位主要用于(在free与realloc时)内部检查. 每个刚分配的内存块的cinuse与pinuse位均应该置位. prev_foot字段最低位代表map标记. 以上规则有个例外是mmap申请的大块内存pinuse不会置位且其prev_foot中mmap标记必须置位, 因为mmap的内存往往是单独申请, 需要自己携带prev_foot(用来记录它在mmap区域中的偏移), 每个mmap的内存块后还会跟着下一个内存块管理结构的前两个成员(保证通过检查).
可以看到malloc_chunk结构的前两个成员作用是以O(1)复杂度实现释放内存操作, 而链表的作用是加快内存申请. 以上这种边界标记的方式最早由高德纳提出, 可以参见ftp://ftp.cs.utexas.edu/pub/garbage/allocsrv.ps了解相关技巧.
对于malloc_chunk结构而言, 其双向链表中必须只能包含同一大小内存块, 否则在索引链表时存在不同长度的内存块必然导致低下的分配效率. 然而为每一个长度设计一个链表显然也不合理. 因此dlmalloc又引入了另一个内存块管理结构, malloc_tree_chunk.
1 //前四字节与malloc_chunk一致以保证相互转化 2 struct malloc_tree_chunk { 3 size_t prev_foot; 4 size_t head; 5 struct malloc_tree_chunk* fd; 6 struct malloc_tree_chunk* bk; 7 struct malloc_tree_chunk* child[2]; 8 struct malloc_tree_chunk* parent; 9 bindex_t index; 10 }; 11 typedef struct malloc_tree_chunk tchunk; 12 typedef struct malloc_tree_chunk* tchunkptr; 13 typedef struct malloc_tree_chunk* tbinptr;
malloc_tree_chunk与malloc_chunk的不同之处在于它引入了二叉树. 树上的每一个节点代表一种长度的空闲内存块, 同一长度的内存块仍然通过双向链表链接起来, 只有最早的内存块(也是下一个被使用的, FIFO顺序)会加入二叉树中(该内存块是否在树中通过parent指针是否为空判断). 理论上一棵树即可管理所有不同大小内存块, 但出于加速内存分配的考虑, 实际有多颗树分别管理以2的n次幂的长度区间的内存块(这块下文再详述). 每颗树的左右子树分别管理区间的左右部分, 即右子树上节点的内存块长度永远大于左子树上节点的内存块长度. 但实际上子树的根节点没有特定的排序关系(决定子树长度分界线是取决于整个树的关系). 寻找树上最小的空闲内存块可以通过一直索引左子树找到. 与通常的二叉树一直寻找左子树直到遇到空节点的方式不同我们会在左子树为空时查找右子树直到一个节点的左右子树均为空, 最小的内存块就在这条路径上(这段话稍稍有些难以理解, 后文会结合代码分析该二叉树的实现). 最坏情况下执行增加/查找/移除节点的步骤与bin的个数相关, 在32bit下为6到21次, 64bit下最高到53次(原因下文再分析).
讲完两种最基本的内存块管理单元, 接下来我们来看下dlmalloc核心数据结构malloc_state.
1 #define NSMALLBINS (32U) 2 #define NTREEBINS (32U) 3 #define SMALLBIN_SHIFT (3U) 4 #define SMALLBIN_WIDTH (SIZE_T_ONE << SMALLBIN_SHIFT) 5 #define TREEBIN_SHIFT (8U) 6 #define MIN_LARGE_SIZE (SIZE_T_ONE << TREEBIN_SHIFT) 7 #define MAX_SMALL_SIZE (MIN_LARGE_SIZE - SIZE_T_ONE) 8 #define MAX_SMALL_REQUEST (MAX_SMALL_SIZE - CHUNK_ALIGN_MASK - CHUNK_OVERHEAD) 9 struct malloc_state { 10 binmap_t smallmap; 11 binmap_t treemap; 12 size_t dvsize; 13 size_t topsize; 14 char* least_addr; 15 mchunkptr dv; 16 mchunkptr top; 17 size_t trim_check; 18 size_t release_checks; 19 size_t magic; 20 mchunkptr smallbins[(NSMALLBINS+1)*2]; 21 tbinptr treebins[NTREEBINS]; 22 size_t footprint; 23 size_t max_footprint; 24 flag_t mflags; 25 #if USE_LOCKS 26 MLOCK_T mutex; //dlmalloc自带锁, 一般未使用 27 #endif 28 msegment seg; 29 void* extp; //暂未使用 30 size_t exts; 31 }; 32 typedef struct malloc_state* mstate;
观察malloc_state结构可以发现它有四个内存块指针, 分别是类型为mchunkptr的dv, top, smallbins与类型为tbinptr的treebins, 其中smallbins与treebins是两个数组, 而dlmalloc的内存管理就是围绕着这四个内存块展开的. 再展开malloc_state结构之前我们先思考一个问题: 内存管理器是否应对所有内存申请请求一视同仁? 答案显然是否定的, 对于小长度的内存申请往往更频繁且长度更稳定(往往是某种数据结构, 长度一般固定), 而反之较大的内存申请更少见(如果有频繁的申请肯定会采取预留内存或外部分配的方式). 因此我们的内存管理工具要能更快的响应小块内存申请, 同时兼顾其它长度的内存申请, 即既要考虑速度又要兼顾广度, 让我们看看dlmalloc是如何平衡两者的. dlmalloc设计了smallbins与treebins数组来完成这项工作. smallbins是一个包含32个双向链表的数组(实际长度是66个指针, 这个问题后面再讨论), 每个双向链表指向一种长度的malloc_chunk. 其长度覆盖8字节到256字节(长度步进为8), 即第一个双向链表指向长度为8的空闲内存块, 第二个双向链表指向长度为16的空闲内存块, 依次类推. 位图smallmap用来标记对应长度的空闲内存块是否存在, 举例第一位置零即不存在长度为8的空闲内存块, 反之亦然. 在引入位图与对应的双向链表数组后就能保证以O(1)的速度索引所有小于256字节的空闲内存块. 关于smallbins还有一个值得一提的小技巧, 那就是smallbins的数组长度是66, 这是为什么呢? 假设我们使用32个malloc_chunk的数组我们需要128个size_t大小的空间, 然而我们只使用了其中的一半空间(双向链表), 因此dlmalloc将每个双向链表的前两个成员用作前一个malloc_chunk的双向链表, 由此节省了一半的空间, 但是对于第一个成员它之前没有空间了, 于是还需要额外增加2个size_t的空间. 那有人问我不将smallbins看做malloc_chunk数组, 仅将其看做双向链表头数组, 不也节省了空间吗? 是的, 但是在后面我们会看到将其看做malloc_chunk可以简化很多代码判断(因为malloc_chunk中的fd/bk不是指针链表头的指针, 而是指向malloc_chunk的指针).
对于大于256字节的内存块仅仅考虑快速分配是不够了, 还要考虑扩大管理区间来降低管理开销. 为了能够加速分配时索引最合适的空闲内存, 大块内存分配也使用了位图treemap, 位图的每一位代表了一颗二叉树, 每两颗二叉树管理的长度区间是按2的幂次增长的(从2^8到2^24, 最后一棵树管理所有比以上区间大的空闲内存块), 具体排布如下:
idx range count
0 [256,384) 128
1 [384,512) 128
2 [512,768) 256
3 [768,1024) 256
4 [1024,1536) 512
......
通过以上两个指针数组即可将所有空闲内存块管理起来, 然而为了提升内存分配的效率dlmalloc还增加了两个内存块指针dv与top. 当请求大小的空闲内存块不存在时dlmalloc会选择稍大的一块内存并将其切分成两部分: 一部分用于响应内存申请, 另一部分即dv(designated victim). dv作用是保存最近的切分的内存块中剩余部分, dvsize即该内存块的长度. 它被单独保存在dv中, 直到再次出现给定长度内存块不存在需要切分大块内存时再将它挂入对应链表/二叉树, 如果有任何新的内存申请都会首先测试dv是否满足需求, 借此dlmalloc尽可能减少内存的碎片化. top则是指当前活动内存段中最顶部的内存块, 其大小保存在topsize中. 该内存块的真实大小是topsize加上TOP_FOOT_SIZE, 即除自身长度外还包括隔离用的尾部malloc_chunk及段数据记录的空间. 谈到内存段的概念我们顺带来看下malloc_segment.
1 struct malloc_segment { 2 char* base; //段基址 3 size_t size; //段长度 4 struct malloc_segment* next; //下一个段地址 5 flag_t sflags; //标记位 6 }; 7 #define is_mmapped_segment(S) ((S)->sflags & IS_MMAPPED_BIT) 8 #define is_extern_segment(S) ((S)->sflags & EXTERN_BIT) 9 typedef struct malloc_segment msegment; 10 typedef struct malloc_segment* msegmentptr;
每个堆空间可能包含多个非连续的内存段, 每个内存段的信息都记录在malloc_segment中, 保存在最顶部的空间中. 通过mmap直接分配的大块内存不保存在该表中, 它们是独立创建与销毁的, 因而不跟踪它们. 段管理主要用于mmap分配的空间, 通过mmap调用返回的地址可能与已存在的段相邻也可能不相邻, 不像MORECORE通常连续扩展当前地址(其地址总是相邻的, 因而更容易处理, dlmalloc更偏向于使用它申请内存). 除了最上层的内存段, 其它内存段记录都保存在这个段的尾部, 内存段通过将段记录压入mstate.seg链表中来维护. 内存段标记位作用: 如果EXTERN_BIT置位则不分配/释放/合并该内存段(当前仅通过create_mspace_with_base初始化内存段时使用), 如果IS_MMAPPED_BIT置位则说明该段可以与其它mmap的内存段合并且需要通过munmap释放, 如果没有比特位置位则说明该内存段通过MORECORE申请且需要使用对应接口释放.
到此dlmalloc的基本数据结构已经讲解完了, 在进入接口分析之前我们最后看一个保存全局变量的结构.
1 struct malloc_params { 2 size_t magic; 3 size_t page_size; 4 size_t granularity; 5 size_t mmap_threshold; 6 size_t trim_threshold; 7 flag_t default_mflags; 8 }; 9 static struct malloc_params mparams;
mparams是静态全局变量, 通过init_mparams()初始化. OK, 让我们来看下dlmalloc的实现吧.
先复述下源码对分配算法的注释. 对于小于256字节的内存请求, 首先从smallbins中查找是否存在对应大小的空闲内存块, 如存在则直接分配. 否则查询dv是否满足长度需求, 如满足需求则使用dv内存块. 否则从smallbins中寻找一个满足大小要求的内存块将其切分, 剩余部分存储在dv中. 如仍未成功分配则尝试使用top. 如top也不满足要求尝试从系统获取内存. 对于更大的内存请求, 首先从treebins中查找满足长度需求的最小内存块与dv比较, 两者中较合适的用于分配. 如两者均不满足则尝试使用top进行分配. 如top不满足需求且请求长度大小大于mmap门槛则尝试直接映射内存. 否则向系统申请更多内存.
1 void* dlmalloc(size_t bytes) { 2 //gm为全局malloc_state结构, PREACTION用于定义USE_LOCKS时判断是否加锁 3 if (!PREACTION(gm)) { 4 void* mem; 5 size_t nb; 6 if (bytes <= MAX_SMALL_REQUEST) { 7 bindex_t idx; 8 binmap_t smallbits; 9 //实际申请长度为申请长度加管理前缀后按8字节对齐, 注意最小长度保护是sizeof(malloc_chunk) 10 nb = (bytes < MIN_REQUEST)? MIN_CHUNK_SIZE : pad_request(bytes); 11 idx = small_index(nb); 12 smallbits = gm->smallmap >> idx; 13 /** 14 * 查询smallbins对应位是否有空闲内存块, 注意此处与0x3的含义 15 * 如果下一个内存块被切分为两块, 其剩余的8字节无法独立作为一块内存块存在 16 * 所以不如在此处直接判断是否有需求, 有则直接分配 17 * 18 **/ 19 if ((smallbits & 0x3U) != 0) { 20 mchunkptr b, p; 21 idx += ~smallbits & 1; 22 //smallbin_at宏展开是((sbinptr)((char*)&((M)->smallbins[(i)<<1]))) 23 b = smallbin_at(gm, idx); 24 p = b->fd; 25 assert(chunksize(p) == small_index2size(idx)); 26 //将p从链表中取出, 如果p为最后一个节点还要清除位图对应位 27 unlink_first_small_chunk(gm, b, p, idx); 28 //置位cinuse与pinuse, 原因见前文叙述, 注意还要置位后一块内存块的pinuse 29 set_inuse_and_pinuse(gm, p, small_index2size(idx)); 30 //chunk2mem宏展开是((void*)((char*)(p)+TWO_SIZE_T_SIZES)) 31 mem = chunk2mem(p); 32 check_malloced_chunk(gm, mem, nb); 33 goto postaction; 34 } 35 //如果smallbins中没有合适的选择, 那么先判断dv是否满足需求 36 else if (nb > gm->dvsize) { 37 //如果dv满足不了长度需求再判断smallbins中是否有大于该长度的空闲内存块 38 if (smallbits != 0) { 39 mchunkptr b, p, r; 40 size_t rsize; 41 bindex_t i; 42 /** 43 * left_bits宏展开是((x<<1) | -(x<<1)) 44 * least_bit宏展开是((x) & -(x)) 45 * same_or_left_bits宏展开是((x) | -(x)) 46 * 关于以上位元操作的详细解释见HACKER DELIGHT 47 * 48 **/ 49 binmap_t leftbits = (smallbits << idx) & left_bits(idx2bit(idx)); 50 binmap_t leastbit = least_bit(leftbits); 51 //二分查找位元位置, 此技巧的解释也可见HACKER DELIGHT 52 compute_bit2idx(leastbit, i); 53 b = smallbin_at(gm, i); 54 p = b->fd; 55 assert(chunksize(p) == small_index2size(i)); 56 unlink_first_small_chunk(gm, b, p, i); 57 rsize = small_index2size(i) - nb; 58 //如果size_t大小不为4字节(64bit平台)且剩余大小又不足MIN_CHUNK_SIZE则不切分内存块 59 if (SIZE_T_SIZE != 4 && rsize < MIN_CHUNK_SIZE) 60 set_inuse_and_pinuse(gm, p, small_index2size(i)); 61 else { 62 set_size_and_pinuse_of_inuse_chunk(gm, p, nb); 63 r = chunk_plus_offset(p, nb); 64 set_size_and_pinuse_of_free_chunk(r, rsize); 65 //此处先将old dv放回smallbin中, 再将切割出多余部分设为dv 66 replace_dv(gm, r, rsize); 67 } 68 mem = chunk2mem(p); 69 check_malloced_chunk(gm, mem, nb); 70 goto postaction; 71 } 72 //尝试从treebin中分配, tmalloc_small()见后文分析 73 else if (gm->treemap != 0 && (mem = tmalloc_small(gm, nb)) != 0) { 74 check_malloced_chunk(gm, mem, nb); 75 goto postaction; 76 } 77 } 78 } 79 //过大的请求直接返回失败 80 else if (bytes >= MAX_REQUEST) 81 nb = MAX_SIZE_T; 82 //对于大于256字节的请求走该分支处理 83 else { 84 nb = pad_request(bytes); 85 if (gm->treemap != 0 && (mem = tmalloc_large(gm, nb)) != 0) { 86 check_malloced_chunk(gm, mem, nb); 87 goto postaction; 88 } 89 } 90 //尝试从dv中分配 91 if (nb <= gm->dvsize) { 92 size_t rsize = gm->dvsize - nb; 93 mchunkptr p = gm->dv; 94 if (rsize >= MIN_CHUNK_SIZE) { 95 mchunkptr r = gm->dv = chunk_plus_offset(p, nb); 96 gm->dvsize = rsize; 97 set_size_and_pinuse_of_free_chunk(r, rsize); 98 set_size_and_pinuse_of_inuse_chunk(gm, p, nb); 99 } 100 else { 101 size_t dvs = gm->dvsize; 102 gm->dvsize = 0; 103 gm->dv = 0; 104 set_inuse_and_pinuse(gm, p, dvs); 105 } 106 mem = chunk2mem(p); 107 check_malloced_chunk(gm, mem, nb); 108 goto postaction; 109 } 110 //最后尝试从top中分配 111 else if (nb < gm->topsize) { 112 size_t rsize = gm->topsize -= nb; 113 mchunkptr p = gm->top; 114 mchunkptr r = gm->top = chunk_plus_offset(p, nb); 115 r->head = rsize | PINUSE_BIT; 116 set_size_and_pinuse_of_inuse_chunk(gm, p, nb); 117 mem = chunk2mem(p); 118 check_top_chunk(gm, gm->top); 119 check_malloced_chunk(gm, mem, nb); 120 goto postaction; 121 } 122 //以上都失败求助sys_alloc(), 见后文分析 123 mem = sys_alloc(gm, nb); 124 postaction: 125 POSTACTION(gm); 126 return mem; 127 } 128 return 0; 129 }
dlmalloc()中主要是smallbins分配的实现, treebins分配见tmalloc_small()与tmalloc_large().
1 static void* tmalloc_small(mstate m, size_t nb) { 2 tchunkptr t, v; 3 size_t rsize; 4 bindex_t i; 5 binmap_t leastbit = least_bit(m->treemap); 6 compute_bit2idx(leastbit, i); 7 v = t = *treebin_at(m, i); 8 rsize = chunksize(t) - nb; 9 //查找长度最小的节点, 注意leftmost_child宏在左子树为空时索引右子树 10 while ((t = leftmost_child(t)) != 0) { 11 size_t trem = chunksize(t) - nb; 12 if (trem < rsize) { 13 rsize = trem; 14 v = t; 15 } 16 } 17 if (RTCHECK(ok_address(m, v))) { 18 mchunkptr r = chunk_plus_offset(v, nb); 19 assert(chunksize(v) == rsize + nb); 20 if (RTCHECK(ok_next(v, r))) { 21 unlink_large_chunk(m, v); 22 //剩余内存块小于MIN_CHUNK_SIZE则一起分配, 否则存放在dv中, 与smallbins逻辑类似 23 if (rsize < MIN_CHUNK_SIZE) 24 set_inuse_and_pinuse(m, v, (rsize + nb)); 25 else { 26 set_size_and_pinuse_of_inuse_chunk(m, v, nb); 27 set_size_and_pinuse_of_free_chunk(r, rsize); 28 replace_dv(m, r, rsize); 29 } 30 return chunk2mem(v); 31 } 32 } 33 CORRUPTION_ERROR_ACTION(m); 34 return 0; 35 }
由于是从treebins中分配小块内存需求, 所以tmalloc_small()直接查找最小的非空二叉树上最小的空闲内存块. tmalloc_small()的难点主要在对二叉树的操作, 这里我们来分析下两个相关的宏insert_large_chunk()/unlink_large_chunk()来加强对二叉树的理解.
1 #define insert_large_chunk(M, X, S) {\ 2 tbinptr* H;\ 3 bindex_t I;\ 4 compute_tree_index(S, I);\ 5 H = treebin_at(M, I);\ 6 X->index = I;\ 7 X->child[0] = X->child[1] = 0;\ 8 if (!treemap_is_marked(M, I)) {\ 9 mark_treemap(M, I);\ 10 *H = X;\ 11 X->parent = (tchunkptr)H;\ 12 X->fd = X->bk = X;\ 13 }\ 14 else {\ 15 tchunkptr T = *H;\ 16 size_t K = S << leftshift_for_tree_index(I);\ 17 for (;;) {\ 18 if (chunksize(T) != S) {\ 19 tchunkptr* C = &(T->child[(K >> (SIZE_T_BITSIZE-SIZE_T_ONE)) & 1]);\ 20 K <<= 1;\ 21 if (*C != 0)\ 22 T = *C;\ 23 else if (RTCHECK(ok_address(M, C))) {\ 24 *C = X;\ 25 X->parent = T;\ 26 X->fd = X->bk = X;\ 27 break;\ 28 }\ 29 else {\ 30 CORRUPTION_ERROR_ACTION(M);\ 31 break;\ 32 }\ 33 }\ 34 else {\ 35 tchunkptr F = T->fd;\ 36 if (RTCHECK(ok_address(M, T) && ok_address(M, F))) {\ 37 T->fd = F->bk = X;\ 38 X->fd = F;\ 39 X->bk = T;\ 40 X->parent = 0;\ 41 break;\ 42 }\ 43 else {\ 44 CORRUPTION_ERROR_ACTION(M);\ 45 break;\ 46 }\ 47 }\ 48 }\ 49 }\ 50 }
insert_large_chunk()宏的作用是将长度为S的内存块X插入treebins的管理中. 在这其中有两种情况, 如树中已存在相同长度内存块则仅需将X加入双向链表, 否则该内存块需先加入二叉树管理. 而后者又有两种情况, 一是长度S所在的区间的树不存在则X为该树的根节点, 否则需要为X索引一个合适的叶子节点. 让我们来看看insert_large_chunk()的实现: 首先根据长度S计算treebins的下标I, 初始化X的索引为I与左右子树为空. 如treemap对应位置零则将X作为该树的根节点. 否则需要索引X在树中的位置, 索引的方式是根据长度S的高位来判断是属于左子树还是右子树. 注意此处leftshift_for_tree_index()宏作用: 对于给定下标I的树其管理的长度是2^(7+I/2), 即长度的有效位在0到(7+I/2)(高位都是0), 所以左移(25-I/2)位, 高位即长度位. 通过循环判断高位状态选择左右子树, 直到该节点为空或子树节点的长度与X相同. 如果节点为空即说明不存在该长度的内存块, 即需要在当前位置插入一个新节点, 如果节点的长度与X相同则将X加入节点的双向链表中管理.
1 #define unlink_large_chunk(M, X) {\ 2 tchunkptr XP = X->parent;\ 3 tchunkptr R;\ 4 if (X->bk != X) {\ 5 tchunkptr F = X->fd;\ 6 R = X->bk;\ 7 if (RTCHECK(ok_address(M, F))) {\ 8 F->bk = R;\ 9 R->fd = F;\ 10 }\ 11 else {\ 12 CORRUPTION_ERROR_ACTION(M);\ 13 }\ 14 }\ 15 else {\ 16 tchunkptr* RP;\ 17 if (((R = *(RP = &(X->child[1]))) != 0) ||\ 18 ((R = *(RP = &(X->child[0]))) != 0)) {\ 19 tchunkptr* CP;\ 20 while ((*(CP = &(R->child[1])) != 0) ||\ 21 (*(CP = &(R->child[0])) != 0)) {\ 22 R = *(RP = CP);\ 23 }\ 24 if (RTCHECK(ok_address(M, RP)))\ 25 *RP = 0;\ 26 else {\ 27 CORRUPTION_ERROR_ACTION(M);\ 28 }\ 29 }\ 30 }\ 31 if (XP != 0) {\ 32 tbinptr* H = treebin_at(M, X->index);\ 33 if (X == *H) {\ 34 if ((*H = R) == 0) \ 35 clear_treemap(M, X->index);\ 36 }\ 37 else if (RTCHECK(ok_address(M, XP))) {\ 38 if (XP->child[0] == X) \ 39 XP->child[0] = R;\ 40 else \ 41 XP->child[1] = R;\ 42 }\ 43 else\ 44 CORRUPTION_ERROR_ACTION(M);\ 45 if (R != 0) {\ 46 if (RTCHECK(ok_address(M, R))) {\ 47 tchunkptr C0, C1;\ 48 R->parent = XP;\ 49 if ((C0 = X->child[0]) != 0) {\ 50 if (RTCHECK(ok_address(M, C0))) {\ 51 R->child[0] = C0;\ 52 C0->parent = R;\ 53 }\ 54 else\ 55 CORRUPTION_ERROR_ACTION(M);\ 56 }\ 57 if ((C1 = X->child[1]) != 0) {\ 58 if (RTCHECK(ok_address(M, C1))) {\ 59 R->child[1] = C1;\ 60 C1->parent = R;\ 61 }\ 62 else\ 63 CORRUPTION_ERROR_ACTION(M);\ 64 }\ 65 }\ 66 else\ 67 CORRUPTION_ERROR_ACTION(M);\ 68 }\ 69 }\ 70 }
再来看下如何从树中删除一个节点, 同样存在两种情况, 与插入操作不同的是被删除的节点同时是二叉树上的节点也可能仅仅是双向链表中的一个节点. 如果X仅存在双向链表管理中, 仅仅修改链表管理即可. 如果存在多个长度S的内存块则X需将自己在树中位置信息复制给下一个链表的节点, 同时其父节点与左右子树也需要对应修改. 如果长度为S的内存块仅X一个, 则需要选择X的右子树(没有则左子树)替代原X的位置.
分析完基本的二叉树操作后我们再来看看如何从treebins中分配大块内存.
1 static void* tmalloc_large(mstate m, size_t nb) { 2 tchunkptr v = 0; 3 size_t rsize = -nb; 4 tchunkptr t; 5 bindex_t idx; 6 //根据给定长度返回所在区间对应的树的下标 7 compute_tree_index(nb, idx); 8 if ((t = *treebin_at(m, idx)) != 0) { 9 size_t sizebits = nb << leftshift_for_tree_index(idx); 10 tchunkptr rst = 0; 11 for (;;) { 12 tchunkptr rt; 13 size_t trem = chunksize(t) - nb; 14 //遍历子树每次保存最合适的内存块, 如存在长度一致的内存块则中断循环 15 if (trem < rsize) { 16 v = t; 17 if ((rsize = trem) == 0) 18 break; 19 } 20 rt = t->child[1]; 21 t = t->child[(sizebits >> (SIZE_T_BITSIZE-SIZE_T_ONE)) & 1]; 22 //保存右子树节点, 当索引左子树为空时返回到右子树选择该节点 23 if (rt != 0 && rt != t) 24 rst = rt; 25 if (t == 0) { 26 t = rst; 27 break; 28 } 29 sizebits <<= 1; 30 } 31 } 32 //最合适的树上没有去相邻的树上寻找, 此时必然寻找最小的内存块 33 if (t == 0 && v == 0) { 34 binmap_t leftbits = left_bits(idx2bit(idx)) & m->treemap; 35 if (leftbits != 0) { 36 bindex_t i; 37 binmap_t leastbit = least_bit(leftbits); 38 compute_bit2idx(leastbit, i); 39 t = *treebin_at(m, i); 40 } 41 } 42 //接上面只要t非空就一直查找最小的内存块 43 while (t != 0) { 44 size_t trem = chunksize(t) - nb; 45 if (trem < rsize) { 46 rsize = trem; 47 v = t; 48 } 49 t = leftmost_child(t); 50 } 51 //比较索引到的内存块与dv选择小的内存块进行切分与分配, 尽量保留大块内存 52 if (v != 0 && rsize < (size_t)(m->dvsize - nb)) { 53 if (RTCHECK(ok_address(m, v))) { 54 mchunkptr r = chunk_plus_offset(v, nb); 55 assert(chunksize(v) == rsize + nb); 56 if (RTCHECK(ok_next(v, r))) { 57 unlink_large_chunk(m, v); 58 if (rsize < MIN_CHUNK_SIZE) 59 set_inuse_and_pinuse(m, v, (rsize + nb)); 60 else { 61 set_size_and_pinuse_of_inuse_chunk(m, v, nb); 62 set_size_and_pinuse_of_free_chunk(r, rsize); 63 insert_chunk(m, r, rsize); 64 } 65 return chunk2mem(v); 66 } 67 } 68 CORRUPTION_ERROR_ACTION(m); 69 } 70 return 0; 71 }
经过前面的描述, tmalloc_large()也不难理解了, 此处稍微值得一提的是compute_tree_index()宏. 该宏有多个实现, 为了便于分析我们使用纯软件实现的算法.
1 #define compute_tree_index(S, I)\ 2 {\ 3 size_t X = S >> TREEBIN_SHIFT;\ 4 if (X == 0)\ 5 I = 0;\ 6 else if (X > 0xFFFF)\ 7 I = NTREEBINS-1;\ 8 else {\ 9 unsigned int Y = (unsigned int)X;\ 10 unsigned int N = ((Y - 0x100) >> 16) & 8;\ 11 unsigned int K = (((Y <<= N) - 0x1000) >> 16) & 4;\ 12 N += K;\ 13 N += K = (((Y <<= K) - 0x4000) >> 16) & 2;\ 14 K = 14 - N + ((Y <<= K) >> 15);\ 15 I = (K << 1) + ((S >> (K + (TREEBIN_SHIFT-1)) & 1));\ 16 }\ 17 }
算法核心是基于二分法快速获取给定长度所在区间的下标. 前文提到过treebins的数组区间, 对于小于256字节使用序号最小的树, 大于2^(16+8)的使用序号最大的树. 而在两者之间的长度则需要使用二分法快速定位所在区间的序号. 因符号扩展, 如果长度减去一个定值后为负数, 其右移后高位全置位, 所以N即用来计算前导零的个数, 而K为15减去N. 对于指数级的映射求出前导零后相减即可获取下标, 但treebins是每两个树长度指数增长1, 所以实际位置为2*K且还要通过次高位判断是属于奇数树区间还是偶数树区间.
如果以上分配均失败则需求助与系统.
1 static void* sys_alloc(mstate m, size_t nb) { 2 char* tbase = CMFAIL; 3 size_t tsize = 0; 4 flag_t mmap_flag = 0; 5 //初始化mparams, 第一次分配才会进入这里 6 init_mparams(); 7 //大块内存直接mmap, 减少内核页表地址占用 8 if (use_mmap(m) && nb >= mparams.mmap_threshold) { 9 void* mem = mmap_alloc(m, nb); 10 if (mem != 0) 11 return mem; 12 } 13 if (MORECORE_CONTIGUOUS && !use_noncontiguous(m)) { 14 char* br = CMFAIL; 15 //获取当前段位置, 如果top为0即第一次分配, 否则查找top地址落在哪个段上 16 msegmentptr ss = (m->top == 0)? 0 : segment_holding(m, (char*)m->top); 17 size_t asize = 0; 18 ACQUIRE_MORECORE_LOCK(); 19 if (ss == 0) { 20 //第一次分配需先获取堆的起始位置, sbrk(0)即获取当前数据段地址 21 char* base = (char*)CALL_MORECORE(0); 22 if (base != CMFAIL) { 23 asize = granularity_align(nb + TOP_FOOT_SIZE + SIZE_T_ONE); 24 if (!is_page_aligned(base)) 25 asize += (page_align((size_t)base) - (size_t)base); 26 if (asize < HALF_MAX_SIZE_T && (br = (char*)(CALL_MORECORE(asize))) == base) { 27 tbase = base; 28 tsize = asize; 29 } 30 } 31 } 32 else { 33 asize = granularity_align(nb - m->topsize + TOP_FOOT_SIZE + SIZE_T_ONE); 34 if (asize < HALF_MAX_SIZE_T && (br = (char*)(CALL_MORECORE(asize))) == ss->base+ss->size) { 35 tbase = br; 36 tsize = asize; 37 } 38 } 39 if (tbase == CMFAIL) { 40 if (br != CMFAIL) { 41 if (asize < HALF_MAX_SIZE_T && asize < nb + TOP_FOOT_SIZE + SIZE_T_ONE) { 42 size_t esize = granularity_align(nb + TOP_FOOT_SIZE + SIZE_T_ONE - asize); 43 if (esize < HALF_MAX_SIZE_T) { 44 char* end = (char*)CALL_MORECORE(esize); 45 if (end != CMFAIL) 46 asize += esize; 47 else { 48 (void) CALL_MORECORE(-asize); 49 br = CMFAIL; 50 } 51 } 52 } 53 } 54 if (br != CMFAIL) { 55 tbase = br; 56 tsize = asize; 57 } 58 else 59 //使用MORECORE分配连续内存失败, 之后不再尝试 60 disable_contiguous(m); 61 } 62 RELEASE_MORECORE_LOCK(); 63 } 64 //尝试MMAP 65 if (HAVE_MMAP && tbase == CMFAIL) { 66 size_t req = nb + TOP_FOOT_SIZE + SIZE_T_ONE; 67 size_t rsize = granularity_align(req); 68 if (rsize > nb) { 69 char* mp = (char*)(CALL_MMAP(rsize)); 70 if (mp != CMFAIL) { 71 tbase = mp; 72 tsize = rsize; 73 mmap_flag = IS_MMAPPED_BIT; 74 } 75 } 76 } 77 //仍然失败尝试非连续的MORECORE 78 if (HAVE_MORECORE && tbase == CMFAIL) { 79 size_t asize = granularity_align(nb + TOP_FOOT_SIZE + SIZE_T_ONE); 80 if (asize < HALF_MAX_SIZE_T) { 81 char* br = CMFAIL; 82 char* end = CMFAIL; 83 ACQUIRE_MORECORE_LOCK(); 84 br = (char*)(CALL_MORECORE(asize)); 85 end = (char*)(CALL_MORECORE(0)); 86 RELEASE_MORECORE_LOCK(); 87 if (br != CMFAIL && end != CMFAIL && br < end) { 88 size_t ssize = end - br; 89 if (ssize > nb + TOP_FOOT_SIZE) { 90 tbase = br; 91 tsize = ssize; 92 } 93 } 94 } 95 } 96 if (tbase != CMFAIL) { 97 if ((m->footprint += tsize) > m->max_footprint) 98 m->max_footprint = m->footprint; 99 if (!is_initialized(m)) { 100 m->seg.base = m->least_addr = tbase; 101 m->seg.size = tsize; 102 m->seg.sflags = mmap_flag; 103 m->magic = mparams.magic; 104 m->release_checks = MAX_RELEASE_CHECK_RATE; 105 init_bins(m); 106 #if !ONLY_MSPACES 107 if (is_global(m)) 108 init_top(m, (mchunkptr)tbase, tsize - TOP_FOOT_SIZE); 109 else 110 #endif 111 { 112 mchunkptr mn = next_chunk(mem2chunk(m)); 113 init_top(m, mn, (size_t)((tbase + tsize) - (char*)mn) -TOP_FOOT_SIZE); 114 } 115 } 116 else { 117 //尝试合并内存段 118 msegmentptr sp = &m->seg; 119 while (sp != 0 && tbase != sp->base + sp->size) 120 sp = (NO_SEGMENT_TRAVERSAL) 0 : sp->next; 121 if (sp != 0 && !is_extern_segment(sp) && 122 (sp->sflags & IS_MMAPPED_BIT) == mmap_flag && 123 segment_holds(sp, m->top)) { 124 sp->size += tsize; 125 init_top(m, m->top, m->topsize + tsize); 126 } 127 else { 128 if (tbase < m->least_addr) 129 m->least_addr = tbase; 130 sp = &m->seg; 131 while (sp != 0 && sp->base != tbase + tsize) 132 sp = (NO_SEGMENT_TRAVERSAL) 0 : sp->next; 133 if (sp != 0 && !is_extern_segment(sp) && 134 (sp->sflags & IS_MMAPPED_BIT) == mmap_flag) { 135 char* oldbase = sp->base; 136 sp->base = tbase; 137 sp->size += tsize; 138 return prepend_alloc(m, tbase, oldbase, nb); 139 } 140 else 141 add_segment(m, tbase, tsize, mmap_flag); 142 } 143 } 144 if (nb < m->topsize) { 145 size_t rsize = m->topsize -= nb; 146 mchunkptr p = m->top; 147 mchunkptr r = m->top = chunk_plus_offset(p, nb); 148 r->head = rsize | PINUSE_BIT; 149 set_size_and_pinuse_of_inuse_chunk(m, p, nb); 150 check_top_chunk(m, m->top); 151 check_malloced_chunk(m, chunk2mem(p), nb); 152 return chunk2mem(p); 153 } 154 } 155 MALLOC_FAILURE_ACTION; 156 return 0; 157 }
从系统申请内存的策略是尽可能保持连续内存: 如果MORECORE能连续扩展则使用MORECORE, 否则尝试使用MMAP(可能连续可能不连续), 最后再使用非连续地址的MORECORE. 在申请到内存后还要检查内存段是否可合并. 而对于超过一个系统页的内存申请策略又有所区别, 对这类内存申请直接使用MMAP方便其释放. 注意通过MMAP申请内存时需要加上6个size_t长度, 原因在前文已有解释, 该长度用来保存本块内存的前缀(1)加上前一块内存的脚标(1)加上后一块内存的最小大小(4).
1 static void* mmap_alloc(mstate m, size_t nb) { 2 size_t mmsize = mmap_align(nb + SIX_SIZE_T_SIZES + CHUNK_ALIGN_MASK); 3 if (mmsize > nb) { 4 char* mm = (char*)(DIRECT_MMAP(mmsize)); 5 if (mm != CMFAIL) { 6 size_t offset = align_offset(chunk2mem(mm)); 7 size_t psize = mmsize - offset - MMAP_FOOT_PAD; 8 mchunkptr p = (mchunkptr)(mm + offset); 9 p->prev_foot = offset | IS_MMAPPED_BIT; 10 (p)->head = (psize|CINUSE_BIT); 11 mark_inuse_foot(m, p, psize); 12 chunk_plus_offset(p, psize)->head = FENCEPOST_HEAD; 13 chunk_plus_offset(p, psize+SIZE_T_SIZE)->head = 0; 14 if (mm < m->least_addr) 15 m->least_addr = mm; 16 if ((m->footprint += mmsize) > m->max_footprint) 17 m->max_footprint = m->footprint; 18 assert(is_aligned(chunk2mem(p))); 19 check_mmapped_chunk(m, p); 20 return chunk2mem(p); 21 } 22 } 23 return 0; 24 }
最后我们看下dlfree(), 其实现大部分已在前文描述. 释放内存的逻辑是先判断与当前内存块前后相邻的内存是否空闲(可合并). 如果可合并则将其从对应的管理结构中分出来(), 重新计算合并后大小并执行合并, 注意合并的优先级是top最高, dv其次, 最后是bin.
1 void dlfree(void* mem) { 2 if (mem != 0) { 3 mchunkptr p = mem2chunk(mem); 4 #if FOOTERS 5 //脚标检查, 用于保护内存 6 mstate fm = get_mstate_for(p); 7 if (!ok_magic(fm)) { 8 USAGE_ERROR_ACTION(fm, p); 9 return; 10 } 11 #else 12 #define fm gm 13 #endif 14 if (!PREACTION(fm)) { 15 check_inuse_chunk(fm, p); 16 if (RTCHECK(ok_address(fm, p) && ok_cinuse(p))) { 17 size_t psize = chunksize(p); 18 mchunkptr next = chunk_plus_offset(p, psize); 19 //先判断前一块内存与当前内存块的关系 20 if (!pinuse(p)) { 21 size_t prevsize = p->prev_foot; 22 //前一块空闲且有mmap标记说明是直接mmap的 23 if ((prevsize & IS_MMAPPED_BIT) != 0) { 24 prevsize &= ~IS_MMAPPED_BIT; 25 psize += prevsize + MMAP_FOOT_PAD; 26 if (CALL_MUNMAP((char*)p - prevsize, psize) == 0) 27 fm->footprint -= psize; 28 goto postaction; 29 } 30 //否则就合并两块内存 31 else { 32 mchunkptr prev = chunk_minus_offset(p, prevsize); 33 psize += prevsize; 34 p = prev; 35 if (RTCHECK(ok_address(fm, prev))) { 36 /** 37 * 如果前一块内存不是dv则必然存在于bin中 38 * 不可能是top, 因为top是最后一块内存 39 * 如果是dv则将当前块合并给dv 40 * 如果是bin中则仅将前一块内存解除管理 41 * 合并后的内存属于哪个bin在最后判断 42 * 43 **/ 44 if (p != fm->dv) { 45 unlink_chunk(fm, p, prevsize); 46 } 47 else if ((next->head & INUSE_BITS) == INUSE_BITS) { 48 fm->dvsize = psize; 49 set_free_with_pinuse(p, psize, next); 50 goto postaction; 51 } 52 } 53 else 54 goto erroraction; 55 } 56 } 57 //再判断当前内存块与后一块内存的关系 58 if (RTCHECK(ok_next(p, next) && ok_pinuse(next))) { 59 if (!cinuse(next)) { 60 /** 61 * 后一块内存多了一种可能性, 可能为top或dv或在bin中 62 * 如果为top或dv则将当前块与前一块(如果有)合并给top 63 * 如果是bin中则仅将前一块内存解除管理, 合并后的内存属于哪个bin在最后判断 64 * 65 **/ 66 if (next == fm->top) { 67 size_t tsize = fm->topsize += psize; 68 fm->top = p; 69 p->head = tsize | PINUSE_BIT; 70 if (p == fm->dv) { 71 fm->dv = 0; 72 fm->dvsize = 0; 73 } 74 if (should_trim(fm, tsize)) 75 sys_trim(fm, 0); 76 goto postaction; 77 } 78 else if (next == fm->dv) { 79 size_t dsize = fm->dvsize += psize; 80 fm->dv = p; 81 set_size_and_pinuse_of_free_chunk(p, dsize); 82 goto postaction; 83 } 84 else { 85 size_t nsize = chunksize(next); 86 psize += nsize; 87 unlink_chunk(fm, next, nsize); 88 set_size_and_pinuse_of_free_chunk(p, psize); 89 if (p == fm->dv) { 90 fm->dvsize = psize; 91 goto postaction; 92 } 93 } 94 } 95 else 96 set_free_with_pinuse(p, psize, next); 97 //最后判断合并后的内存块长度属于哪个bin管理并插入到对应结构中 98 if (is_small(psize)) { 99 insert_small_chunk(fm, p, psize); 100 check_free_chunk(fm, p); 101 } 102 else { 103 tchunkptr tp = (tchunkptr)p; 104 insert_large_chunk(fm, tp, psize); 105 check_free_chunk(fm, p); 106 if (--fm->release_checks == 0) 107 release_unused_segments(fm); 108 } 109 goto postaction; 110 } 111 } 112 erroraction: 113 USAGE_ERROR_ACTION(fm, p); 114 postaction: 115 POSTACTION(fm); 116 } 117 } 118 #if !FOOTERS 119 #undef fm 120 #endif 121 }
至此dlmalloc的基本实现已分析完, 出于简化代码考虑未讨论FOOTERS宏与MSPACES宏. 实际上这两者的用处还是比较大的, 前者用于判断内存操作是否越界, 后者是多线程ptmalloc实现的基础, 本文就不展开讨论了. 最后稍稍提及一下dlmalloc的多线程实现ptmalloc.
我们知道glibc内置函数都是以__libc_*命名的, 内存分配函数也不例外. 在ptmalloc3.c中会将这组接口重定义为public_*(比如public_mALLOc/public_fREe). 以public_mALLOc()为例, 它通过mspace_malloc实现内存分配. 每个mspace对应一个arena, public_mALLOc()会首先调用arena_get宏获取arena(其思路是首先尝试该线程最近一次成功获取的arena, 如获取失败则循环链表尝试获取一个, 如果都失败则直接初始化一个新的arena), 通过arena获取对应的mspace. 这种方式较dlmalloc中直接给全局变量加锁的方式明显降低了线程并发申请内存的冲突. 限于篇幅关于ptmalloc更多的实现就请各位自己分析吧.