dlmalloc(一)

本文深入探讨动态内存管理的概念,包括内核层面与用户层面的内存管理,解释了如何合理管理内存申请与释放。重点介绍了内存分配器的作用与工作机制,如dlmalloc的实现细节,包括其内存申请、分配、释放策略,以及如何优化内存使用效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

        我们写过很多C程序了,经常会分配内存。记得刚学C语言时老师说过,可以向两个地方申请内存:一个是栈、一个是堆。小块内存向栈申请,函数调用结束后程序会自动释放内存。大块内存向堆申请,记得一定要自己释放,否则会造成内存泄漏。向堆申请内存直接调用malloc()就可以了,参数是你申请的内存量。释放内存时直接调用free()就可以了,参数是内存块指针。

        看似平静的海面,海底则波涛汹涌。当时还没有学操作系统原理,更没有读过Linux内核代码。现在仔细想想才发现申请动态内存是一件多么麻烦的事情。动态内存管理涉及到两个层面的问题:内核层面和用户层面。系统中的内存如何管理这是内核考虑的事情,总不能让应用程序随便使用系统中的内存吧。内核向应用程序提供了接口(为此Linux提供了两个系统调用brk和mmap),当应用程序需要申请内存时向内核提出请求,内核查找并分配一块可用内存供应用程序使用。这部分内容属于内核范畴,不属于C基础库,因此不深入说了。那么用户层面做什么呢?用户层面需要合理管理内存申请和释放请求。比如:brk()可以扩充或收缩堆的大小,你总不能每分配一次内存就调用一次brk()吧?释放内存时更麻烦,你必须保证内存块的释放顺序。比如先申请了内存块a,然后申请了内存块b,然后释放a(b仍然在使用),如果释放a时调用了brk()就会出问题。你不能在使用b的同时释放a。

        好在出现了一个叫做“内存分配器”的东西,内存分配器接管了应用程序申请内存和释放内存的请求,应用程序再也不需要直接调用brk()和mmap()了,而是向内存分配器提交申请。有了内存分配器,我们只需要记住malloc()和free()两个接口函数就可以了,其他繁琐事情全部交给内存分配器负责了。申请内存时,内存分配器会一次向内核申请大量内存,然后分批交给应用程序,从而提高了效率。释放内存时,应用程序也是将内存释放给内存分配器,内存分配器在合适的时候再将内存释放会内核。

        dlmalloc就是一种内存分配器,由Doug Lea在1987年开发完成,这是Android系统中使用的内存分配器。而Linux系统中采用的是ptmalloc,ptmalloc在dlmalloc的基础上进行了改进,以更好适应多线程。dlmalloc采用两种方式申请内存,如果应用程序单次申请的内存量小于256kb,dlmalloc调用brk()扩展进程堆空间,但是dlmalloc向内核申请的内存量大于应用程序申请的内存量,申请到内存后dlmalloc将内存分成两块,一块返回给应用程序,另一块作为空闲内存先保留起来。下次应用程序申请内存时dlmalloc就不需要向内核申请内存了,从而加快内存分配效率。当应用程序调用free()释放内存时,如果内存块小于256kb,dlmalloc并不马上将内存块释放回内存,而是将内存块标记为空闲状态。这么做的原因有两个:一是内存块不一定能马上释放会内核(比如内存块不是位于堆顶端),二是供应用程序下次申请内存使用(这是主要原因)。当dlmalloc中空闲内存量达到一定值时dlmalloc才将空闲内存释放会内核。如果应用程序申请的内存大于256kb,dlmalloc调用mmap()向内核申请一块内存,返回返还给应用程序使用。如果应用程序释放的内存大于256kb,dlmalloc马上调用munmap()释放内存。dlmalloc不会缓存大于256kb的内存块,因为这样的内存块太大了,最好不要长期占用这么大的内存资源。

        从下篇文章开始,我们将详细讲解dlmalloc的代码,环境是Linux 32位系统。


链接:dlmalloc

dlmalloc是目前个十分流行的内存分配器,其由Doug Lea(主页为http://gee.cs.oswego.edu/)从1987年开始编写,到目前为止,最新版本为2.8.3(可以从ftp://g.oswego.edu/pub/misc/malloc.c获取),由于其高效率等特点被广泛的使用(比如些linux系统等用的就是dlmalloc或其变形,比如ptmalloc,主页为http://www.malloc.de/en/index.html)和研究(各位可以搜索关键字“GCspy”)。 dlmalloc的实现只有个源文件(还有个头文件),大概5000行,其内注释占了大量篇幅,由于有这么多注释存在的情况下,表面上看上去很容易懂,的确如此,在不追求细节的情况,对其大致思想的确很容易了解(没错,就只是了解而已),但是dlmalloc作为个高品质的佳作,实现上使用了非常多的技巧,在实现细节上不花费定的精力是没有办法深入理解其为什么这么做,这么做的好处在哪,只有当真正读懂后回味起来才发现它是如此美妙。 lenky0401个人博客将陆续推出对dlmalloc的解析(针对Doug Lea Malloc的最新版Version 2.8.3,未做说明的情况下以32位平台,8字节对齐作为假定平台环境设置考虑),由于个人水平有限,因此也不能完全保证对dlmalloc的所有理解都准备无误, 但是所有内容均出自个人的理解而并非存心妄自揣测来愚人耳目,所以如果读者发现其中有什么错误,请勿见怪,如果可以则请来信告之,并欢迎来信讨论(lenky0401@163.com)。 这系列文章是lenky0401在看完dlmalloc的大部分代码后的再总结,不能保证对dlmalloc的整体完全把握,贴出这些只是希望可以提前收到对此有研究的网友的指点,以便在最后对这系列文章整理而形成的PDF文档中错误能少些。至于对于现在贴出来的内容中包含的错误给大家造成的不便提前说声抱歉。:) 描述的内容不会包含dlmalloc全部代码,但会将这其中涉及到的些技巧尽量讲出,我相信对dlmalloc源代码不感兴趣的朋友也可以学到这些独立的技巧而使用在自己的编程实践中。:) 最后,转载请保留本博客地址连接[http://lenky0401.cublog.cn],谢谢。
<think>嗯,用户想了解dlmalloc内存分配器的工作原理。之前提供的引用材料提到了ptmalloc、dlmalloc以及它们的些特性,比如分箱式内存管理、多线程支持、内存分配可能多出8字节等。用户现在明确要深入理解dlmalloc的机制。 dlmalloc是Doug Lea开发的内存分配器,虽然现在Linux主要用ptmalloc2(基于dlmalloc改进),但理解dlmalloc有助于掌握基础原理。根据引用[1][2],关键点包括分箱式管理(类似哈希桶)和多线程支持(ptmalloc2的优势)。引用[3]则提到分配时可能多出8字节的细节,这涉及chunk块管理。 需要从几个核心层面解释: 首先是chunk结构——dlmalloc用边界标记法,每个chunk头部存大小和状态位(P位指示前块是否在用),这样free时能快速合并相邻空闲块。引用[3]提到的"多出8字节"实际是chunk头部开销(用户申请10字节时,系统可能分配最小chunk如24字节)。 其次是分箱策略:小内存按大小分类到不同bin(如16字节的bin放16-31字节请求),大内存用树结构管理。引用[1]的"哈希桶"即指这种分箱机制。 最后是分配流程:先尝试精确匹配的bin,若无则找更大bin分割(分割后剩余部分进新bin),大内存直接走mmap。释放时合并相邻空闲块防止碎片,引用[3]的"不分割"例子说明碎片控制策略。 用户可能是系统开发者或学习者,需要避免过度技术术语。可以类比快递仓库:不同货架(bin)放不同尺寸包裹(chunk),新包裹优先放合适货架,取包裹后合并相邻空位。这样能呼应引用中的"分箱式管理"概念。 相关问题需覆盖优化(如ptmalloc的线程本地存储)、对比(dlmalloc vs ptmalloc)及实际影响(内存碎片),这样既延续引用材料又拓展应用场景。</think>好的,我们来详细解析 Doug Lea's Malloc (`dlmalloc`) 内存分配器的工作原理。它是许多现代分配器(如 `ptmalloc`)的基础。 ### dlmalloc 的核心原理和工作机制 `dlmalloc` 的核心目标是高效管理进程堆内存,响应用户的 `malloc` 和 `free` 请求,同时尽量减少内存碎片和提高分配/释放速度。其核心机制包括: 1. **内存块 (Chunk) 结构**: * `dlmalloc` 将堆内存视为系列连续的 **内存块 (chunks)**。 * 每个 chunk 包含个 **头部 (header)** 和可用的 **用户数据区**。用户通过 `malloc` 获得的是指向用户数据区的指针。 * 头部存储关键元数据: * **大小 (size)**: 当前 chunk 的总大小(包括头部和用户数据区)。通常以字节为单位,并且大小值通常是某个对齐值(如 8 字节)的倍数。这使得地址计算高效。 * **前个块是否在使用中 (P - Previous in use)**: 个标志位,指示内存中紧邻在当前 chunk **之前** 的那个 chunk 是否正在被使用(即尚未被 `free`)。这是实现边界标记法的关键。 * 结构示意(假设最小对齐为 8 字节): ``` ... | [前个chunk] | [当前chunk 头部 (size | P-bit)] | [用户数据区 (malloc返回的指针指向这里)] | ... ``` * 当个 chunk 被 `free` 时,其用户数据区的空间会被复用,存储些链表指针(见下文 Bins)。 2. **边界标记法 (Boundary Tags)**: * 这是 `dlmalloc` 减少碎片的核心技术之。 * 每个 chunk 的头部不仅存储自身的大小和状态位,其 **尾部**(或者更准确地说,下个 chunk 的头部)也隐式地存储了 **当前** chunk 的大小信息(通过计算偏移)。 * **关键作用**: * **合并 (Coalescing)**: 当释放 (`free`) 个 chunk 时,`dlmalloc` 可以快速检查其 **前个** chunk(利用当前 chunk 头部的 P 位)和 **后个** chunk(利用下个 chunk 头部的隐式大小信息)是否也是空闲的。如果相邻 chunk 空闲,则立即将它们合并成个更大的空闲 chunk。这极大地减少了外部碎片。 * **分割 (Splitting)**: 当分配 (`malloc`) 内存时,如果找到个比请求大的空闲 chunk,`dlmalloc` 可以将其分割成两部分:个满足请求大小的 chunk 分配给用户,剩余部分形成个新的空闲 chunk(只要剩余部分足够大能容纳个最小 chunk 的头部)。 3. **分箱管理 (Binning)**: * `dlmalloc` 维护多个链表(称为 **bins**),用于组织不同大小的空闲 chunk,以实现快速查找。 * **小内存块 (Small Bins)**: * 管理固定大小的 chunk(例如,每个 bin 负责 16 字节的间隔:16-31, 32-47, 48-63, ... 字节)。 * 通常实现为 **双向循环链表 (doubly-linked lists)**。链表操作(插入、删除)是 O(1) 的。 * 分配时,找到对应请求大小的 bin。如果 bin 非空,直接取出第个 chunk(精确匹配或稍大,无需分割)。释放时,直接放回对应 bin。 * **大内存块 (Large Bins)**: * 管理大于小 bin 最大尺寸的空闲 chunk。 * 每个 bin 覆盖个范围(例如,512-1023, 1024-2047, ... 字节)。 * 链表中的 chunk 按大小 **排序**(通常从大到小或从小到大)。分配时需要遍历链表寻找最合适(Best-fit 或 Good-fit)的 chunk。释放时插入到合适位置保持有序。 * **Unsorted Bin**: * 个临时的 bin。当用户 `free` 个 chunk 时,它不会立即放入小 bin 或大 bin,而是先放入 unsorted bin。 * 在后续的 `malloc` 调用中,`dlmalloc` 会优先搜索 unsorted bin,尝试满足请求。如果满足则分配;如果不满足(大小不合适),则将其从 unsorted bin 中移除,并放入对应的小 bin 或大 bin 中。 * 这相当于种缓存策略,能提高最近释放的 chunk 被快速重用的机会,减少立即分类的开销。 * **Top Chunk (Wilderness)**: * 堆内存区域最顶端的个特殊 chunk。 * 当所有 bins 中都没有合适的空闲 chunk 时,`dlmalloc` 会尝试从 top chunk 中分割部分来满足请求。 * 如果 top chunk 不够大,`dlmalloc` 会通过系统调用(如 `sbrk` 或 `mmap`)向操作系统申请更多内存来扩展 top chunk。 4. **分配算法 (malloc)**: 1. 如果请求大小很小(在某个阈值内,如 256 字节): * 计算对应的 **小 bin** 索引。 * 检查该 bin 是否为空。 * 如果非空,取出第个 chunk,返回其用户数据区指针(精确匹配,速度快)。 2. 如果请求大小属于小 bin 范围但对应的 bin 为空,或者请求大小属于大 bin 范围: * 首先检查 **unsorted bin**(最近释放的 chunk),尝试找到个大小匹配的 chunk。如果找到,将其移除并返回(或分割后返回)。 * 如果 unsorted bin 没有合适的,则搜索对应的 **大 bin**(或更大的小 bin),寻找满足请求的最小 chunk(Best-fit)。找到后分割,返回用户部分,剩余部分放入 unsorted bin(如果够大)。 3. 如果以上步骤都失败: * 尝试从 **top chunk** 分割。如果 top chunk 足够大,分割并返回用户部分。 * 如果 top chunk 不够大,调用系统调用(`sbrk` 或 `mmap`)扩展堆,增大 top chunk,然后从新的 top chunk 中分割。 4. 如果请求非常大(超过某个阈值,如 128KB),`dlmalloc` 通常会绕过上述机制,直接使用 `mmap` 分配块独立的内存区域(以避免碎片化主堆),并在释放时直接用 `munmap` 归还给操作系统。 5. **释放算法 (free)**: 1. 检查传入的指针是否有效(例如,是否对齐)。 2. 根据指针找到对应的 chunk 头部。 3. 检查前个 chunk 是否空闲(通过当前 chunk 头部的 P 位)。如果空闲,将其从 bin 中取出,与当前 chunk **合并**。 4. 检查后个 chunk 是否空闲(通过后个 chunk 的头部信息)。如果空闲,将其从 bin 中取出,与当前(可能已合并)的 chunk **合并**。 5. 将最终合并得到的空闲 chunk 放入 **unsorted bin**。 6. **优化策略**: * **Fast Bins**: 这是 `ptmalloc` 对 `dlmalloc` 的重要扩展(虽然现代 `dlmalloc` 版本也可能引入)。管理些非常小的 chunk(如 16, 24, 32, ..., 80 字节)。释放时,chunk 被放入 fast bin,但 **不立即合并**(P 位不清除)。分配对应小内存时,优先从 fast bin 取,速度极快(单链表)。这优化了小内存高频分配释放的性能,但牺牲了部分碎片控制。 * **线程本地存储 (TLS) / Arena**: `ptmalloc` 的核心改进,`dlmalloc` 本身是单线程的。`ptmalloc` 引入了多个 `arena`(每个 arena 本质是个独立的堆,包含自己的 bins、top chunk 等结构)。每个线程默认绑定个 arena,大部分分配释放操作都在线程自己的 arena 中进行,避免了全局锁竞争,极大地提升了多线程性能[^2]。 ### 引用材料的相关点解释 * **引用[1]**: 提到的 "分箱式内存管理机制"、"类似哈希桶结构" 直接对应了 dlmalloc 的核心机制 **Binning**。 * **引用[2]**: 说明了 `dlmalloc` 是 `ptmalloc` 的前身,`ptmalloc` 通过增加 **多线程支持(Arena)** 取代了 `dlmalloc` 在 Linux 中的地位。 * **引用[3]**: 解释了内存分配开销和最小分配单位: * "块头、对齐等开销" 指的是每个 chunk 的 **头部元数据** 和 **地址对齐** 要求。 * "malloc分配的内存也可能会比应用程序实际应分配内存多" 指的是 `dlmalloc` 分配的最小 chunk 大小(例如,包含头部和对齐后是 24 字节)。即使用户申请 10 字节,系统也会分配个 24 字节的 chunk(头部 8 字节 + 用户数据 16 字节,或类似结构)。用户得到指向这 16 字节的指针。 * "10,这种情况时,dlmalloc分配的内存将多出8个字节。由于8个字节不足以组成个chunk块,因此也就没有必要进行分割" 描述的场景是:假设最小 chunk 是 16 字节(头部 8 + 用户 8)。用户申请 10 字节,需要的最小用户空间是 10 字节。最小 chunk 需要 16 字节(8 头 + 8 用户)。但 8 字节用户空间 < 10,不够。所以分配器必须分配下个尺寸的 chunk,例如 24 字节(8 头 + 16 用户)。用户得到 16 字节中的前 10 字节使用。此时,如果这个 chunk 被分割,剩下的部分应该是 24 - (8头 + 10用户) = 6 字节。但这 6 字节连个最小的 chunk 头部(8 字节)都放不下,因此分配器**不会分割**,而是将整个 24 字节的 chunk 分配给用户(用户用了 10 字节,实际分配了 24 字节,多出 14 字节)。引用中的 "多出8个字节" 可能是指用户实际需要 10,但分配器因结构限制至少给 16(用户区),所以多给 6 字节用户空间?或者是对最小 chunk 结构的简化描述。核心概念是:**分配器有最小分配单位(个 chunk 的大小),如果用户申请量接近但不满足分割条件(剩余空间不足以形成新 chunk),分配器会将整个 chunk 分配出去,导致内部碎片**。 ### 总结 `dlmalloc` 是个高效、可移植的内存分配器,其核心在于: 1. **Chunk 结构**:管理内存的基本单位,包含元数据。 2. **边界标记法**:实现 O(1) 时间的相邻空闲 chunk 合并,减少外部碎片。 3. **分箱管理 (Bins)**:使用不同策略(小 bin 精确匹配,大 bin 排序查找,unsorted bin 缓存)组织空闲 chunk,加速查找。 4. **Top Chunk**:作为堆扩展的缓冲区和最后分配来源。 5. **分配/释放流程**:结合 bin 查找、分割、合并和系统调用扩展堆。 `ptmalloc` 在 `dlmalloc` 的基础上,主要增加了 **Fast Bins** 和 **Arena (TLS)** 机制,以优化多线程环境下的性能[^2]。理解 `dlmalloc` 是理解现代 `glibc` 内存管理(`ptmalloc`)的基础。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值