申请内存块
__libc_malloc
一般我们会使用 malloc 函数来申请内存块,可是当仔细看 glibc 的源码实现时,其实并没有 malloc 函数。其实该函数真正调用的是 __libc_malloc 函数。为什么不直接写个 malloc 函数呢,因为有时候我们可能需要不同的名称。此外,__libc_malloc 函数只是用来简单封装 _int_malloc 函数。_int_malloc 才是申请内存块的核心。下面我们来仔细分析一下具体的实现。
该函数会首先检查是否有内存分配函数的钩子函数(__malloc_hook),这个主要用于用户自定义的堆分配函数,方便用户快速修改堆分配函数并进行测试。这里需要注意的是,用户申请的字节一旦进入申请内存函数中就变成了无符号整数。
// wapper for int_malloc
void *__libc_malloc(size_t bytes) {
mstate ar_ptr;
void * victim;
// 检查是否有内存分配钩子,如果有,调用钩子并返回.
void *(*hook)(size_t, const void *) = atomic_forced_read(__malloc_hook);
if (__builtin_expect(hook != NULL, 0))
return (*hook)(bytes, RETURN_ADDRESS(0));
接着会寻找一个 arena 来试图分配内存。
arena_get(ar_ptr, bytes);然后调用 _int_malloc 函数去申请对应的内存。
victim = _int_malloc(ar_ptr, bytes);如果分配失败的话,ptmalloc 会尝试再去寻找一个可用的 arena,并分配内存。
/* Retry with another arena only if we were able to find a usable arena
before. */
if (!victim && ar_ptr != NULL) {
LIBC_PROBE(memory_malloc_retry, 1, bytes);
ar_ptr = arena_get_retry(ar_ptr, bytes);
victim = _int_malloc(ar_ptr, bytes);
}如果申请到了 arena,那么在退出之前还得解锁。
判断目前的状态是否满足以下条件
要么没有申请到内存
要么是 mmap 的内存
要么申请到的内存必须在其所分配的arena中
最后返回内存。
_int_malloc
_int_malloc 是内存分配的核心函数,其核心思路有如下
它根据用户申请的内存块大小以及相应大小 chunk 通常使用的频度(fastbin chunk, small chunk, large chunk),依次实现了不同的分配方法。
它由小到大依次检查不同的 bin 中是否有相应的空闲块可以满足用户请求的内存。
当所有的空闲 chunk 都无法满足时,它会考虑 top chunk。
当 top chunk 也无法满足时,堆分配器才会进行内存块申请。
在进入该函数后,函数立马定义了一系列自己需要的变量,并将用户申请的内存大小转换为内部的chunk大小。
arena
fast bin
如果申请的 chunk 的大小位于 fastbin 范围内,需要注意的是这里比较的是无符号整数。此外,是从 fastbin 的头结点开始取 chunk。
small bin
如果获取的内存块的范围处于 small bin 的范围,那么执行如下流程
large bin
当 fast bin、small bin 中的 chunk 都不能满足用户请求 chunk 大小时,就会考虑是不是 large bin。但是,其实在 large bin 中并没有直接去扫描对应 bin 中的chunk,而是先利用 malloc_consolidate(参见malloc_state相关函数) 函数处理 fast bin 中的chunk,将有可能能够合并的 chunk 先进行合并后放到 unsorted bin 中,不能够合并的就直接放到 unsorted bin 中,然后再在下面的大循环中进行相应的处理。为什么不直接从相应的 bin 中取出 large chunk 呢?这是ptmalloc 的机制,它会在分配 large chunk 之前对堆中碎片 chunk 进行合并,以便减少堆中的碎片。
大循环-遍历 unsorted bin
如果程序执行到了这里,那么说明 与 chunk 大小正好一致的 bin (fast bin, small bin) 中没有 chunk可以直接满足需求 ,但是 large chunk 则是在这个大循环中处理。
在接下来的这个循环中,主要做了以下的操作
按照 FIFO 的方式逐个将 unsorted bin 中的 chunk 取出来
如果是 small request,则考虑是不是恰好满足,是的话,直接返回。
如果不是的话,放到对应的 bin 中。
尝试从 large bin 中分配用户所需的内存
该部分是一个大循环,这是为了尝试重新分配 small bin chunk,这是因为我们虽然会首先使用 large bin,top chunk 来尝试满足用户的请求,但是如果没有满足的话,由于我们在上面没有分配成功 small bin,我们并没有对fast bin 中的 chunk 进行合并,所以这里会进行 fast bin chunk 的合并,进而使用一个大循环来尝试再次分配small bin chunk。
unsorted bin 遍历
先考虑 unsorted bin,再考虑 last remainder ,但是对于 small bin chunk 的请求会有所例外。
注意 unsorted bin 的遍历顺序为 bk。
small request
如果用户的请求为 small bin chunk,那么我们首先考虑 last remainder,如果 last remainder 是 unsorted bin 中的唯一一块的话, 并且 last remainder 的大小分割后还可以作为一个 chunk ,为什么没有等号?
初始取出
exact fit
如果从 unsorted bin 中取出来的 chunk 大小正好合适,就直接使用。这里应该已经把合并后恰好合适的 chunk 给分配出去了。
place chunk in small bin
把取出来的 chunk 放到对应的 small bin 中。
place chunk in large bin
把取出来的 chunk 放到对应的 large bin 中。
最终取出
while 迭代次数
while 最多迭代10000次后退出。
large chunk
注: 或许会很奇怪,为什么这里没有先去看 small chunk 是否满足新需求了呢?这是因为small bin 在循环之前已经判断过了,这里如果有的话,就是合并后的才出现chunk。但是在大循环外,large chunk 只是单纯地找到其索引,所以觉得在这里直接先判断是合理的,而且也为了下面可以再去找较大的chunk。
如果请求的 chunk 在 large chunk 范围内,就在对应的 bin 中从小到大进行扫描,找到第一个合适的。
寻找较大 chunk
如果走到了这里,那说明对于用户所需的chunk,不能直接从其对应的合适的bin中获取chunk,所以我们需要来查找比当前 bin 更大的 fast bin , small bin 或者 large bin。
找到一个合适的 map
找到合适的 bin
简单检查 chunk
真正取出 chunk
使用 top chunk
如果所有的 bin 中的 chunk 都没有办法直接满足要求(即不合并),或者说都没有空闲的 chunk。那么我们就只能使用 top chunk 了。
堆内存不够
如果堆内存不够,我们就需要使用 sysmalloc 来申请内存了。
_libc_calloc
calloc 也是 libc 中的一种申请内存块的函数。在 libc中的封装为 _libc_calloc,具体介绍如下
sysmalloc
正如该函数头的注释所言,该函数用于当前堆内存不足时,需要向系统申请更多的内存。
基本定义
我们可以主要关注一下 pagesize,其
所以,pagesize=4096=0x1000。
考虑 mmap
正如开头注释所言如果满足如下任何一种条件
没有分配堆。
申请的内存大于
mp_.mmap_threshold,并且mmap 的数量小于最大值,就可以尝试使用 mmap。
默认情况下,临界值为
DEFAULT_MMAP_THRESHOLD 为 128*1024 字节,即 128 K。
下面为这部分代码,目前不是我们关心的重点,可以暂时跳过。
mmap 失败或者未分配堆
如果是这两种情况中的任何一种,其实就可以退出了。。
记录旧堆信息
检查旧堆信息1
这个检查要求满足其中任何一个条件
old_top == initial_top(av) && old_size == 0,即如果是第一次的话,堆的大小需要是 0。新的堆,那么
(unsigned long)(old_size) >= MINSIZE && prev_inuse(old_top),堆的大小应该不小于MINSIZE,并且前一个堆块应该处于使用中。((unsigned long)old_end & (pagesize - 1)) == 0),堆的结束地址应该是页对齐的,由于页对齐的大小默认是0x1000,所以低 12 个比特需要为 0。
检查旧堆信息2
根据 malloc 中的定义
nb 应该是已经加上 chunk 头部的字节,为什么还要加上 MINSIZE 呢?这是因为 top chunk 的大小应该至少预留 MINSIZE 空间,以便于合并。
非 main_arena
这里暂时不是关心的重点,暂且不分析。
Main_arena 处理
计算内存
计算可以满足请求的内存大小。
默认情况下 top_pad定义为
即 131072 字节,0x20000 字节。
是否连续
如果我们希望堆的空间连续的话,那么其实可以复用之前的内存。
对齐页大小
申请内存
可能成功
这里竟然调用了一个 hook,有点意思。
失败
失败,考虑 mmap。
内存可能申请成功
情况 1
情况 2 - 意外内存耗尽
处理其他意外情况
处理连续内存
处理不连续内存
调整
需要注意的是,在这里程序将旧的 top chunk 进行了释放,那么其会根据大小进入不同的 bin 或 tcache 中。
更新最大内存
分配内存块
获取大小
切分 top
捕捉所有错误
Last updated