1. 申请内存块
ptmalloc2中,内存分配的核心逻辑是从__libc_malloc开始,最终进入_int_malloc。其流程是一个由快到慢、由简单到复杂的搜索过程。
1.1 __libc_malloc函数
__libc_malloc是glibc中malloc的第一个底层入口。当你调用malloc(size)时,实际上绝大部分逻辑都在这个函数及其调用的子函数中完成。ptmalloc(glibc 内存分配器)中,__libc_malloc是malloc函数在C库层面的真实实现入口。它是对核心分配引擎_int_malloc的一层外围封装,主要负责钩子检查、线程本地缓存(Tcache)处理以及分配区(Arena)锁的管理:
void * __libc_malloc (size_t bytes){
mstate ar_ptr;
void *victim;
// 1. 钩子检查:检查并执行 malloc hook (用于调试,新版本已弃用)
void *(*hook) (size_t, const void *) = atomic_forced_read (__malloc_hook);
if (__builtin_expect (hook != NULL, 0))
return (*hook)(bytes, RETURN_ADDRESS (0));
// 2. 尝试从 Tcache 分配 (Glibc 2.26+ 引入)
#if USE_TCACHE
size_t tbytes;
// 将用户请求的大小转换为内部 chunk 大小
checked_request2size (bytes, tbytes);
// 根据大小计算对应的 tcache bin 索引
int tc_idx = csize2tidx (tbytes);
// 检查该索引是否在有效范围内,且该 bin 中是否有空闲块
if (tc_idx < mp_.tcache_bins
&& tcache
&& tcache->counts[tc_idx] > 0)
{
// 核心操作:从对应的 tcache bin 链表头部取出一个块
return tcache_get (tc_idx);
}
#endif
// 3. 如果 Tcache 没中,再进入获取 Arena 锁试图分配内存
arena_get (ar_ptr, bytes);、
// 4. 调用 _int_malloc 函数去申请对应的内存,执行真正的分配逻辑
victim = _int_malloc (ar_ptr, bytes);
// 5. 如果分配失败的话,ptmalloc会尝试再去寻找一个可用的arena,并分配内存
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);
}
// 6. 如果申请到了arena,那么在退出之前还得解锁。
if (ar_ptr != NULL)
(void) mutex_unlock (&ar_ptr->mutex);
// 7. 判断一下目前的状态:申请到的内存是否在其所分配的 arena 中
assert(!victim || chunk_is_mmapped(mem2chunk(victim)) ||
ar_ptr == arena_for_chunk(mem2chunk(victim)));
// 8. 返回内存
return victim;
}
1.2 _int_malloc函数
_int_malloc是ptmalloc的核心引擎。它不负责锁管理和Tcache,而是专门负责在Arena(分配区)内部进行复杂的内存搜索、拆分和合并逻辑。它实现了从Fast Bins到Top Chunk的完整搜索与分配逻辑。
1.2.1 初始化与变量定义
首先,函数定义了一系列需要的变量,它会将用户请求的大小转换为内部对齐后的nb(request2size),即chunk大小:
static void *_int_malloc(mstate av, size_t bytes) {
INTERNAL_SIZE_T nb; /* normalized request size 最终申请的对齐大小 */
unsigned int idx; /* associated bin index bin索引*/
mbinptr bin; /* associated bin bin指针*/
mchunkptr victim; /* inspected/selected chunk 目标chunk*/
INTERNAL_SIZE_T size; /* its size */
int victim_index; /* its bin index */
mchunkptr remainder; /* remainder from a split */
unsigned long remainder_size; /* its size */
unsigned int block; /* bit map traverser */
unsigned int bit; /* bit map traverser */
unsigned int map; /* current word of binmap */
mchunkptr fwd; /* misc temp for linking */
mchunkptr bck; /* misc temp for linking */
const char *errstr = NULL;
checked_request2size(bytes, nb);
1.2.2 Fast Bins分配(极快路径)
如果申请的chunk大小属于Fast Bin范围,直接从单向链表头部取chunk:
if ((unsigned long) (nb) <= (unsigned long) (get_max_fast())) {
// 得到对应的fastbin的下标
idx = fastbin_index(nb);
// 得到对应的fastbin的头指针
mfastbinptr *fb = &fastbin(av, idx);
mchunkptr pp = *fb;
// 利用fd遍历对应的bin内是否有空闲的chunk块,
do {
victim = pp;
if (victim == NULL) break;
} while ((pp = catomic_compare_and_exchange_val_acq(fb, victim->fd,
victim)) != victim);
// 存在可以利用的chunk
if (victim != 0) {
// 检查取到的 chunk 大小是否与相应的 fastbin 索引一致。
// 根据取得的 victim ,利用 chunksize 计算其大小。
// 利用fastbin_index 计算 chunk 的索引。
if (__builtin_expect(fastbin_index(chunksize(victim)) != idx, 0)) {
errstr = "malloc(): memory corruption (fast)";
errout:
malloc_printerr(check_action, errstr, chunk2mem(victim), av);
return NULL;
}
// 细致的检查。。只有在 DEBUG 的时候有用
check_remalloced_chunk(av, victim, nb);
// 将获取的到chunk转换为mem模式
void *p = chunk2mem(victim);
// 如果设置了perturb_type, 则将获取到的chunk初始化为 perturb_type ^ 0xff
alloc_perturb(p, bytes);
return p;
}
}
1.2.3 Small Bins分配(快速路径)
如果Fast Bin没中,且属于Small Bin范围,则尝试从双向链表末尾取块,此处触发unlink:
if (in_smallbin_range(nb)) {
// 获取 small bin 的索引
idx = smallbin_index(nb);
// 获取对应 small bin 中的 chunk 指针
bin = bin_at(av, idx);
// 先执行 victim = last(bin),获取 small bin 的最后一个 chunk
// 如果 victim = bin ,那说明该 bin 为空。
// 如果不相等,那么会有两种情况
if ((victim = last(bin)) != bin) {
// 第一种情况,small bin 还没有初始化。
if (victim == 0) /* initialization check */
// 执行初始化,将 fast bins 中的 chunk 进行合并
malloc_consolidate(av);
// 第二种情况,small bin 中存在空闲的 chunk
else {
// 获取 small bin 中倒数第二个 chunk 。
bck = victim->bk;
// 检查 bck->fd 是不是 victim,防止伪造
if (__glibc_unlikely(bck->fd != victim)) {
errstr = "malloc(): smallbin double linked list corrupted";
goto errout;
}
// 设置 victim 对应的 inuse 位
set_inuse_bit_at_offset(victim, nb);
// 修改 small bin 链表,将 small bin 的最后一个 chunk 取出来
bin->bk = bck;
bck->fd = bin;
// 如果不是 main_arena,设置对应的标志
if (av != &main_arena) set_non_main_arena(victim);
// 细致的检查,非调试状态没有作用
check_malloced_chunk(av, victim, nb);
// 将申请到的 chunk 转化为对应的 mem 状态
void *p = chunk2mem(victim);
// 如果设置了 perturb_type , 则将获取到的chunk初始化为 perturb_type ^ 0xff
alloc_perturb(p, bytes);
return p;
}
}
}
1.2.4 遍历Unsorted Bin(最核心的for循环)
如果以上的bin(fast bin,small bin)中的chunk都没有满足请求,或者申请的是Large Bin,分配器会进入一个巨大的循环。large chunk则是在这个大循环中处理。大循环中主要做了以下操作:
- 按照FIFO的方式逐个将unsorted bin链表中的chunk取出来;
- 如果是small request,考虑是不是正好满足,如果满足则直接返回;
- 如果不满足的话,放到对应的bin中;
- 尝试从large bin中分配用户所需的chunk内存块;
该大循环尝试重新分配small bin的chunk,还承担了整理内存碎片的工作:
// 源码逻辑简化
for (int iters = 0; iters < 10000; ++iters) // 防止死循环的上限
{
victim = unsorted_chunks (av)->bk; // 获取链表中最后一个 chunk
if (victim == unsorted_chunks (av)) break; // 链表为空,退出循环
bck = victim->bk; // 记录下一个要处理的块
size = chunksize (victim); // 获取当前块大小
...
unsorted bin 遍历
分配器从Unsorted Bin的 表尾(bk 指针方向) 开始逆序扫描:
第一步:取出Chunk (Victim):从链表中摘下一个chunk,记为victim,获取该chunk的大小size。
// 如果 unsorted bin 不为空
// First In First Out
while ((victim = unsorted_chunks(av)->bk) != unsorted_chunks(av)) {
// victim 为 unsorted bin 的最后一个 chunk
// bck 为 unsorted bin 的倒数第二个 chunk
bck = victim->bk;
// 判断得到的 chunk 是否满足要求,不能过小,也不能过大
// 一般 system_mem 的大小为132K
if (__builtin_expect(chunksize_nomask(victim) <= 2 * SIZE_SZ, 0) ||
__builtin_expect(chunksize_nomask(victim) > av->system_mem, 0))
malloc_printerr(check_action, "malloc(): memory corruption",
chunk2mem(victim), av);
// 得到victim对应的chunk大小。
size = chunksize(victim);
第二步:如果用户的请求为small bin chunk,且Unsorted Bin中只有一个chunk,并且这个chunk恰好是上次分配剩下的(last_remainder),且足够大:
- 直接切分:切出需要的chunk大小给用户,剩下的部分继续留在Unsorted Bin作为新的last_remainder;
- 直接返回:逻辑到此结束;
/*
If a small request, try to use last remainder if it is the
only chunk in unsorted bin. This helps promote locality for
runs of consecutive small requests. This is the only
exception to best-fit, and applies only when there is
no exact fit for a small chunk.
*/
if (in_smallbin_range(nb) && bck == unsorted_chunks(av) &&
victim == av->last_remainder &&
(unsigned long) (size) > (unsigned long) (nb + MINSIZE)) {
/* split and reattach remainder */
// 获取新的 remainder 的大小
remainder_size = size - nb;
// 获取新的 remainder 的位置
remainder = chunk_at_offset(victim, nb);
// 更新 unsorted bin 的情况
unsorted_chunks(av)->bk = unsorted_chunks(av)->fd = remainder;
// 更新 av 中记录的 last_remainder
av->last_remainder = remainder;
// 更新last remainder的指针
remainder->bk = remainder->fd = unsorted_chunks(av);
if (!in_smallbin_range(remainder_size)) {
remainder->fd_nextsize = NULL;
remainder->bk_nextsize = NULL;
}
// 设置victim的头部,
set_head(victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
// 设置 remainder 的头部
set_head(remainder, remainder_size | PREV_INUSE);
// 设置记录 remainder 大小的 prev_size 字段,因为此时 remainder 处于空闲状态。
set_foot(remainder, remainder_size);
// 细致的检查,非调试状态下没有作用
check_malloced_chunk(av, victim, nb);
// 将 victim 从 chunk 模式转化为mem模式
void *p = chunk2mem(victim);
// 如果设置了perturb_type, 则将获取到的chunk初始化为 perturb_type ^ 0xff
alloc_perturb(p, bytes);
return p;
}
第三步:如果上述特例不成立,将victim从Unsorted Bin中正式移除(即修改fd/bk指针)。此时会检查:
- chunksize(victim) 是否在合法范围内(最小MINSIZE,最大系统限制);
- 双向链表完整性:检查victim->fd->bk == victim。
/* remove from unsorted list */
unsorted_chunks(av)->bk = bck;
bck->fd = unsorted_chunks(av);
第四步:精确匹配(Best Case),如果从unsorted bin中取出来的chunk大小正好合适,就直接使用,即victim的大小完全等于用户申请的大小chunk(nb),分配成功即设置该块的inuse位:
/* Take now instead of binning if exact fit */
if (size == nb) {
set_inuse_bit_at_offset(victim, size);
if (av != &main_arena) set_non_main_arena(victim);
check_malloced_chunk(av, victim, nb);
void *p = chunk2mem(victim);
alloc_perturb(p, bytes);
return p;
}
第五步:归位。如果大小不匹配,这个victim就需要离开Unsorted Bin,分流到small bin或者large bin:
- 放入small bin:如果size属于Small Bin范围,插入到对应index的smallbin头部;
- 放入large bin:如果属于Large Bin,根据大小降序排列插入,并维护fd_nextsize和bk_nextsize指针;
/* place chunk in bin */
if (in_smallbin_range(size)) {
victim_index = smallbin_index(size);
bck = bin_at(av, victim_index);
fwd = bck->fd;
} else {
// large bin 范围
victim_index = largebin_index(size);
bck = bin_at(av, victim_index); // 当前 large bin 的头部
fwd = bck->fd;
/* maintain large bins in sorted order */
/* 从这里我们可以总结出,largebin 以 fd_nextsize 递减排序。
同样大小的 chunk,后来的只会插入到之前同样大小的 chunk 后,
而不会修改之前相同大小的fd/bk_nextsize,这也很容易理解,
可以减低开销。此外,bin 头不参与 nextsize 链接。*/
// 如果 large bin 链表不空
if (fwd != bck) {
/* Or with inuse bit to speed comparisons */
// 加速比较,应该不仅仅有这个考虑,因为链表里的 chunk 都会设置该位。
size |= PREV_INUSE;
/* if smaller than smallest, bypass loop below */
// bck->bk 存储着相应 large bin 中最小的chunk。
// 如果遍历的 chunk 比当前最小的还要小,那就只需要插入到链表尾部。
// 判断 bck->bk 是不是在 main arena。
assert(chunk_main_arena(bck->bk));
if ((unsigned long) (size) <
(unsigned long) chunksize_nomask(bck->bk)) {
// 令 fwd 指向 large bin 头
fwd = bck;
// 令 bck 指向 largin bin 尾部 chunk
bck = bck->bk;
// victim 的 fd_nextsize 指向 largin bin 的第一个 chunk
victim->fd_nextsize = fwd->fd;
// victim 的 bk_nextsize 指向原来链表的第一个 chunk 指向的 bk_nextsize
victim->bk_nextsize = fwd->fd->bk_nextsize;
// 原来链表的第一个 chunk 的 bk_nextsize 指向 victim
// 原来指向链表第一个 chunk 的 fd_nextsize 指向 victim
fwd->fd->bk_nextsize =
victim->bk_nextsize->fd_nextsize = victim;
} else {
// 当前要插入的 victim 的大小大于最小的 chunk
// 判断 fwd 是否在 main arena
assert(chunk_main_arena(fwd));
// 从链表头部开始找到不比 victim 大的 chunk
while ((unsigned long) size < chunksize_nomask(fwd)) {
fwd = fwd->fd_nextsize;
assert(chunk_main_arena(fwd));
}
// 如果找到了一个和 victim 一样大的 chunk,
// 那就直接将 chunk 插入到该chunk的后面,并不修改 nextsize 指针。
if ((unsigned long) size ==
(unsigned long) chunksize_nomask(fwd))
/* Always insert in the second position. */
fwd = fwd->fd;
else {
// 如果找到的chunk和当前victim大小不一样
// 那么就需要构造 nextsize 双向链表了
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;
}
} else
// 如果空的话,直接简单使得 fd_nextsize 与 bk_nextsize 构成一个双向链表即可。
victim->fd_nextsize = victim->bk_nextsize = victim;
}
第六步:最终取出,放到对应的bin中,构成bck<–>victim<–>fwd:
// 放到对应的 bin 中,构成 bck<-->victim<-->fwd。
mark_bin(av, victim_index);
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;
1.2.5 Large Bin分配
如果遍历完Unsorted Bin还没找到合适的,就会去搜索Large Bins:
/*
If this is a large request, consolidate fastbins before continuing.
While it might look excessive to kill all fastbins before
even seeing if there is space available, this avoids
fragmentation problems normally associated with fastbins.
Also, in practice, programs tend to have runs of either small or
large requests, but less often mixtures, so consolidation is not
invoked all that often in most programs. And the programs that
it is called frequently in otherwise tend to fragment.
*/
else {
// 获取large bin的下标。
idx = largebin_index(nb);
// 如果存在fastbin的话,会处理 fastbin
if (have_fastchunks(av)) malloc_consolidate(av);
}
1.2.6 Top Chunk切割
如果所有的bin中的chunk都没有办法满足要求(即不合并),或者说都没有空闲的chunk。那么我们就只能切割使用top chunk了:
use_top:
/*
If large enough, split off the chunk bordering the end of memory
(held in av->top). Note that this is in accord with the best-fit
search rule. In effect, av->top is treated as larger (and thus
less well fitting) than any other available chunk since it can
be extended to be as large as necessary (up to system
limitations).
We require that av->top always exists (i.e., has size >=
MINSIZE) after initialization, so if it would otherwise be
exhausted by current request, it is replenished. (The main
reason for ensuring it exists is that we may need MINSIZE space
to put in fenceposts in sysmalloc.)
*/
// 获取当前的top chunk,并计算其对应的大小
victim = av->top;
size = chunksize(victim);
// 如果分割之后,top chunk 大小仍然满足 chunk 的最小大小,那么就可以直接进行分割。
if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE)) {
remainder_size = size - nb;
remainder = chunk_at_offset(victim, nb);
av->top = remainder;
// 这里设置 PREV_INUSE 是因为 top chunk 前面的 chunk 如果不是 fastbin,就必然会和
// top chunk 合并,所以这里设置了 PREV_INUSE。
set_head(victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head(remainder, remainder_size | PREV_INUSE);
check_malloced_chunk(av, victim, nb);
void *p = chunk2mem(victim);
alloc_perturb(p, bytes);
return p;
}
// 否则,判断是否有 fast chunk
/* When we are using atomic ops to free fast chunks we can get
here for all block sizes. */
else if (have_fastchunks(av)) {
// 先执行一次fast bin的合并
malloc_consolidate(av);
/* restore original bin index */
// 判断需要的chunk是在small bin范围内还是large bin范围内
// 并计算对应的索引
// 等待下次再看看是否可以
if (in_smallbin_range(nb))
idx = smallbin_index(nb);
else
idx = largebin_index(nb);
}
1.2.7 sysmalloc申请内存
当_int_malloc无法通过所有Bins(包括Unsorted Bin)和Top Chunk满足分配需求时,ptmalloc就会调用最后的保底手段:sysmalloc。它的核心任务是向操作系统内核直接索要内存。
1.2.7.1 sysmalloc的决策选择?
sysmalloc会根据申请内存的大小(nb)选择不同的系统调用路径:
路径1:mmap(大块内存)
当申请大小超过了mmap_threshold(默认通常是 128 KB,但会动态调整),直接调用mmap系统调用,在内存映射区开辟一块独立的匿名映射:
- 独立性:这种内存不属于Arena的Top Chunk;
- 释放即归还:调用free时,会直接调用munmap将内存还给内核,不会留在Bins中;
- 标记:该chunk的size字段会设置IS_MMAPPED标志位;
路径2:sbrk(堆扩展)
当申请大小较小,或者不满足mmap条件时,调用sbrk扩展数据段(Heap)的上限(program break):
- 扩展Top Chunk:将原有的Top Chunk与新申请的内存合并;
- 连续性:如果sbrk成功且返回地址与原Top Chunk连续,则直接增大Top Chunk的size;
- 非连续处理:如果内核返回的地址不连续(由于中间有其他内存映射),旧的Top Chunk会被free掉(进入Unsorted Bin),新的内存块成为新的Top Chunk;
1.2.7.2 基本结构定义
static void *sysmalloc(INTERNAL_SIZE_T nb, mstate av) {
mchunkptr old_top; /* incoming value of av->top */
INTERNAL_SIZE_T old_size; /* its size */
char *old_end; /* its end address */
long size; /* arg to first MORECORE or mmap call */
char *brk; /* return value from MORECORE */
long correction; /* arg to 2nd MORECORE call */
char *snd_brk; /* 2nd return val */
INTERNAL_SIZE_T front_misalign; /* unusable bytes at front of new space */
INTERNAL_SIZE_T end_misalign; /* partial page left at end of new space */
char *aligned_brk; /* aligned offset into brk */
mchunkptr p; /* the allocated/returned chunk */
mchunkptr remainder; /* remainder frOm allocation */
unsigned long remainder_size; /* its size */
size_t pagesize = GLRO(dl_pagesize);
bool tried_mmap = false;
//pagesize定义
#ifndef EXEC_PAGESIZE
#define EXEC_PAGESIZE 4096
#endif
# define GLRO(name) _##name
size_t _dl_pagesize = EXEC_PAGESIZE;
//即pagesize=4096=0x1000
选择mmap
满足条件:申请大小超过了mmap_threshold(默认通常是128 KB,但会动态调整),它不在堆(Heap)段内,而是在内存映射区,默认的临界值为:
static struct malloc_par mp_ = {
.top_pad = DEFAULT_TOP_PAD,
.n_mmaps_max = DEFAULT_MMAP_MAX,
.mmap_threshold = DEFAULT_MMAP_THRESHOLD,
.trim_threshold = DEFAULT_TRIM_THRESHOLD,
#define NARENAS_FROM_NCORES(n) ((n) * (sizeof(long) == 4 ? 2 : 8))
.arena_test = NARENAS_FROM_NCORES(1)
#if USE_TCACHE
,
.tcache_count = TCACHE_FILL_COUNT,
.tcache_bins = TCACHE_MAX_BINS,
.tcache_max_bytes = tidx2usize(TCACHE_MAX_BINS - 1),
.tcache_unsorted_limit = 0 /* No limit. */
#endif
};
DEFAULT_MMAP_THRESHOLD 为128*1024字节,即128 K:
#ifndef DEFAULT_MMAP_THRESHOLD
#define DEFAULT_MMAP_THRESHOLD DEFAULT_MMAP_THRESHOLD_MIN
#endif
/*
MMAP_THRESHOLD_MAX and _MIN are the bounds on the dynamically
adjusted MMAP_THRESHOLD.
*/
#ifndef DEFAULT_MMAP_THRESHOLD_MIN
#define DEFAULT_MMAP_THRESHOLD_MIN (128 * 1024)
#endif
#ifndef DEFAULT_MMAP_THRESHOLD_MAX
/* For 32-bit platforms we cannot increase the maximum mmap
threshold much because it is also the minimum value for the
maximum heap size and its alignment. Going above 512k (i.e., 1M
for new heaps) wastes too much address space. */
#if __WORDSIZE == 32
#define DEFAULT_MMAP_THRESHOLD_MAX (512 * 1024)
#else
#define DEFAULT_MMAP_THRESHOLD_MAX (4 * 1024 * 1024 * sizeof(long))
#endif
#endif
Main_arena处理:
//计算可以满足请求的内存大小:
/* Request enough space for nb + pad + overhead */
size = nb + mp_.top_pad + MINSIZE;
//默认情况下top_pad定义为:
#ifndef DEFAULT_TOP_PAD
# define DEFAULT_TOP_PAD 131072
#endif
//是否连续 ¶
//如果我们希望堆的空间连续的话,那么其实可以复用之前的内存
if (contiguous(av))
size -= old_size;
//对齐页大小
size = ALIGN_UP(size, pagesize);
//申请内存
if (size > 0) {
brk = (char *)(MORECORE(size));
LIBC_PROBE(memory_sbrk_more, 2, brk, size);
}
//申请内存成功
if (brk != (char *)(MORECORE_FAILURE)) {
/* Call the `morecore' hook if necessary. */
void (*hook)(void) = atomic_forced_read(__after_morecore_hook);
if (__builtin_expect(hook != NULL, 0))
(*hook)();
}
如果申请内存失败,则考虑mmap:
else {
/*
If have mmap, try using it as a backup when MORECORE fails or
cannot be used. This is worth doing on systems that have "holes" in
address space, so sbrk cannot extend to give contiguous space, but
space is available elsewhere. Note that we ignore mmap max count
and threshold limits, since the space will not be used as a
segregated mmap region.
*/
/* Cannot merge with old top, so add its size back in */
if (contiguous(av))
size = ALIGN_UP(size + old_size, pagesize);
/* If we are relying on mmap as backup, then use larger units */
if ((unsigned long)(size) < (unsigned long)(MMAP_AS_MORECORE_SIZE))
size = MMAP_AS_MORECORE_SIZE;
/* Don't try if size wraps around 0 */
if ((unsigned long)(size) > (unsigned long)(nb)) {
char *mbrk = (char *)(MMAP(0, size, PROT_READ | PROT_WRITE, 0));
if (mbrk != MAP_FAILED) {
/* We do not need, and cannot use, another sbrk call to find end */
brk = mbrk;
snd_brk = brk + size;
/*
Record that we no longer have a contiguous sbrk region.
After the first time mmap is used as backup, we do not
ever rely on contiguous space since this could incorrectly
bridge regions.
*/
set_noncontiguous(av);
}
}
}
2. 释放内存
ptmalloc2(glibc的默认内存分配器)中,释放内存的过程主要由内部函数_int_free实现。其核心目标是高效回收内存,同时通过合并(Coalescing)相邻空闲块来减少堆碎片。__libc_free是面向用户的入口包装函数,而_int_free是执行具体回收算法的核心引擎。
2.1 __libc_free函数
__libc_free是GNU C Library (glibc) 中用于释放动态分配内存的核心内部函数。当你在程序中调用标准 C 库函数free(ptr)时,底层实际上是跳转到__libc_free执行具体的内存回收逻辑。它的主要职责是进行初步检查并决定如何处理这块内存。
2.1.1 核心工作流程
- 1.检查 Hook 钩子:首先检查是否存在自定义的内存释放钩子函数(如全局变量__free_hook)。如果存在,则优先执行该钩子函数。这在调试、内存泄漏检测或特定的漏洞利用(PWN)中非常关键;
- 2.处理空指针:如果传入的指针为NULL,函数会直接返回,不执行任何操作;
- 3.获取内存块(Chunk)信息:将用户提供的指针转换为堆管理系统的内部数据结构mchunkptr。通过指针地址向前偏移,找到记录内存块大小、使用状态等元数据的chunk头部;
- 4.判断分配方式:
- mmap 释放:如果该内存块是通过mmap系统调用分配的大内存块(具有IS_MMAPPED标志),则调用munmap_chunk将其直接返还给内核;
- 普通堆释放:对于常规的小额分配,函数会定位该内存所属的分配区(Arena),并锁定该区以进行后续的回收操作;
- 5.调用底层实现
_int_free:核心的回收逻辑由_int_free函数完成。它负责将释放的chunk放入对应的bin链表(如fast bins、unsorted bin等),并在必要时执行内存合并动作,以减少堆碎片。
2.1.2 函数原型
在源码级别,它的定义通常如下(位于glibc的 malloc.c 文件中):
void __libc_free(void *mem) {
mstate ar_ptr;
mchunkptr p; /* chunk corresponding to mem */
// 判断是否有钩子函数 __free_hook
void (*hook)(void *, const void *) = atomic_forced_read(__free_hook);
if (__builtin_expect(hook != NULL, 0)) {
(*hook)(mem, RETURN_ADDRESS(0));
return;
}
// free NULL没有作用
if (mem == 0) /* free(0) has no effect */
return;
// 将mem转换为chunk状态
p = mem2chunk(mem);
// 如果该块内存是mmap得到的
if (chunk_is_mmapped(p)) /* release mmapped memory. */
{
/* See if the dynamic brk/mmap threshold needs adjusting.
Dumped fake mmapped chunks do not affect the threshold. */
if (!mp_.no_dyn_threshold && chunksize_nomask(p) > mp_.mmap_threshold &&
chunksize_nomask(p) <= DEFAULT_MMAP_THRESHOLD_MAX &&
!DUMPED_MAIN_ARENA_CHUNK(p)) {
mp_.mmap_threshold = chunksize(p);
mp_.trim_threshold = 2 * mp_.mmap_threshold;
LIBC_PROBE(memory_mallopt_free_dyn_thresholds, 2,
mp_.mmap_threshold, mp_.trim_threshold);
}
munmap_chunk(p);
return;
}
// 根据chunk获得分配区的指针
ar_ptr = arena_for_chunk(p);
// 执行释放
_int_free(ar_ptr, p, 0);
}
2.2 _int_free
_int_free是glibc内存管理器中最为核心的函数,它承载了几乎所有的堆内存回收算法。它的核心任务是:验证合法性、处理缓存(Bins)以及执行内存合并。
2.2.1 核心流程
_int_free内部按照内存块(chunk)的大小和状态,依次尝试以下回收路径:
- 1. 基础检查 (Safety Checks)
- 指针对齐:检查chunk的地址是否对齐(通常是 8 或 16 字节);
- 大小限制:检查size是否小于最小chunk大小,或者是否超过了系统的最大寻址范围;
- Double Free 检查:检查当前释放的块是否已经是空闲状态(通过检查下一个块的PREV_INUSE标志位);
- 2. 放入Fast Bins (极速回收):
- 适合非常小的内存块,默认是小于128字节;
- 不合并:直接将chunk插入到对应大小的Fast Bin单向链表头部;
- 小内存分配极其频繁,不合并可以极大提高下一次分配相同大小内存的速度;
- 检查Fast Bin的顶部块是否就是当前块(简单的 Double Free 检测);
- 3.Tcache 处理(在现代glibc中):在进入_int_free之前或初期,通常会优先尝试将内存放入线程私有的缓存(Tcache)中;
- 4.放入Unsorted Bin (缓冲区):非Fast Bin大小的普通内存块,且不属于mmap分配
- 向前合并:检查物理相邻的前一个块是否空闲。如果空闲,调用unlink宏将其从原链表拿出来,并与当前块合并;
- 向后合并:检查物理相邻的后一个块。如果是Top Chunk:直接把当前块并入Top Chunk;如果是普通空闲块:调用unlink将其合并;
- 入链:将合并后的块(或未合并的块)放入Unsorted Bin。这给内存一个“重新做人”的机会,下次分配时会优先在这里找;
- 5.触发收缩 (Trim):如果合并后的块导致Top Chunk变得非常大(超过mp_.trim_threshold),_int_free可能会触发系统调用,将物理内存还给内核;
2.2.2 基本结构
_int_free函数的定义:
static void _int_free(mstate av, mchunkptr p, int have_lock) {
INTERNAL_SIZE_T size; /* its size */
mfastbinptr * fb; /* associated fastbin */
mchunkptr nextchunk; /* next contiguous chunk */
INTERNAL_SIZE_T nextsize; /* its size */
int nextinuse; /* true if nextchunk is used */
INTERNAL_SIZE_T prevsize; /* size of previous contiguous chunk */
mchunkptr bck; /* misc temp for linking */
mchunkptr fwd; /* misc temp for linking */
const char *errstr = NULL;
int locked = 0;
size = chunksize(p);
基础检查:包含指针对齐、大小限制、Double Free检查:
/* Little security check which won't hurt performance: the
allocator never wrapps around at the end of the address space.
Therefore we can exclude some size values which might appear
here by accident or by "design" from some intruder. */
// 指针不能指向非法的地址, 必须小于等于-size,为什么???
// 指针必须得对齐,2*SIZE_SZ 这个对齐得仔细想想
if (__builtin_expect((uintptr_t) p > (uintptr_t) -size, 0) ||
__builtin_expect(misaligned_chunk(p), 0)) {
errstr = "free(): invalid pointer";
errout:
if (!have_lock && locked) __libc_lock_unlock(av->mutex);
malloc_printerr(check_action, errstr, chunk2mem(p), av);
return;
}
/* We know that each chunk is at least MINSIZE bytes in size or a
multiple of MALLOC_ALIGNMENT. */
// 大小没有最小的chunk大,或者说,大小不是MALLOC_ALIGNMENT的整数倍
if (__glibc_unlikely(size < MINSIZE || !aligned_OK(size))) {
errstr = "free(): invalid size";
goto errout;
}
// 检查该chunk是否处于使用状态,非调试状态下没有作用
check_inuse_chunk(av, p);
/* Check if m has acceptable alignment */
#define aligned_OK(m) (((unsigned long) (m) &MALLOC_ALIGN_MASK) == 0)
#define misaligned_chunk(p) \
((uintptr_t)(MALLOC_ALIGNMENT == 2 * SIZE_SZ ? (p) : chunk2mem(p)) & \
MALLOC_ALIGN_MASK)
2.2.3 fast bin(极速回收)
如果内存块非常小且符合Fast Bin范围,则直接放入Fast Bin链表头部:
/*
If eligible, place chunk on a fastbin so it can be found
and used quickly in malloc.
*/
if ((unsigned long) (size) <= (unsigned long) (get_max_fast())
#if TRIM_FASTBINS
/*
If TRIM_FASTBINS set, don't place chunks
bordering top into fastbins
*/
//默认 #define TRIM_FASTBINS 0,因此默认情况下下面的语句不会执行
// 如果当前chunk是fast chunk,并且下一个chunk是top chunk,则不能插入
&& (chunk_at_offset(p, size) != av->top)
#endif
) {
// 下一个chunk的大小不能小于两倍的SIZE_SZ,并且
// 下一个chunk的大小不能大于system_mem, 一般为132k
// 如果出现这样的情况,就报错。
if (__builtin_expect(
chunksize_nomask(chunk_at_offset(p, size)) <= 2 * SIZE_SZ, 0) ||
__builtin_expect(
chunksize(chunk_at_offset(p, size)) >= av->system_mem, 0)) {
/* We might not have a lock at this point and concurrent
modifications
of system_mem might have let to a false positive. Redo the test
after getting the lock. */
if (have_lock || ({
assert(locked == 0);
__libc_lock_lock(av->mutex);
locked = 1;
chunksize_nomask(chunk_at_offset(p, size)) <= 2 * SIZE_SZ ||
chunksize(chunk_at_offset(p, size)) >= av->system_mem;
})) {
errstr = "free(): invalid next size (fast)";
goto errout;
}
if (!have_lock) {
__libc_lock_unlock(av->mutex);
locked = 0;
}
}
// 将chunk的mem部分全部设置为perturb_byte
free_perturb(chunk2mem(p), size - 2 * SIZE_SZ);
// 设置fast chunk的标记位
set_fastchunks(av);
// 根据大小获取fast bin的索引
unsigned int idx = fastbin_index(size);
// 获取对应fastbin的头指针,被初始化后为NULL。
fb = &fastbin(av, idx);
/* Atomically link P to its fastbin: P->FD = *FB; *FB = P; */
// 使用原子操作将P插入到链表中
mchunkptr old = *fb, old2;
unsigned int old_idx = ~0u;
do {
/* Check that the top of the bin is not the record we are going to
add
(i.e., double free). */
// so we can not double free one fastbin chunk
// 防止对 fast bin double free
if (__builtin_expect(old == p, 0)) {
errstr = "double free or corruption (fasttop)";
goto errout;
}
/* Check that size of fastbin chunk at the top is the same as
size of the chunk that we are adding. We can dereference OLD
only if we have the lock, otherwise it might have already been
deallocated. See use of OLD_IDX below for the actual check. */
if (have_lock && old != NULL)
old_idx = fastbin_index(chunksize(old));
p->fd = old2 = old;
} while ((old = catomic_compare_and_exchange_val_rel(fb, p, old2)) !=
old2);
// 确保fast bin的加入前与加入后相同
if (have_lock && old != NULL && __builtin_expect(old_idx != idx, 0)) {
errstr = "invalid fastbin entry (free)";
goto errout;
}
}
2.2.4 合并非mmap的空闲chunk到unsorted bin
非Fast Bin大小的普通内存块触发unlink,合并后的chunk指向合并的chunk的低地址。合并顺序是:
- 先考虑物理低地址空闲块;
- 后考虑物理高地址空闲块;
没有锁的情况下,先获得锁:
/*
Consolidate other non-mmapped chunks as they arrive.
*/
else if (!chunk_is_mmapped(p)) {
if (!have_lock) {
__libc_lock_lock(av->mutex);
locked = 1;
}
nextchunk = chunk_at_offset(p, size);
简单的检测、释放填充
/* Lightweight tests: check whether the block is already the
top block. */
// 当前free的chunk不能是top chunk
if (__glibc_unlikely(p == av->top)) {
errstr = "double free or corruption (top)";
goto errout;
}
// 当前free的chunk的下一个chunk不能超过arena的边界
/* Or whether the next chunk is beyond the boundaries of the arena. */
if (__builtin_expect(contiguous(av) &&
(char *) nextchunk >=
((char *) av->top + chunksize(av->top)),
0)) {
errstr = "double free or corruption (out)";
goto errout;
}
// 当前要free的chunk的使用标记没有被标记,double free
/* Or whether the block is actually not marked used. */
if (__glibc_unlikely(!prev_inuse(nextchunk))) {
errstr = "double free or corruption (!prev)";
goto errout;
}
// 下一个chunk的大小
nextsize = chunksize(nextchunk);
// next chunk size valid check
// 判断下一个chunk的大小是否不大于2*SIZE_SZ,或者
// nextsize是否大于系统可提供的内存
if (__builtin_expect(chunksize_nomask(nextchunk) <= 2 * SIZE_SZ, 0) ||
__builtin_expect(nextsize >= av->system_mem, 0)) {
errstr = "free(): invalid next size (normal)";
goto errout;
}
//将指针的mem部分全部设置为perturb_byte
free_perturb(chunk2mem(p), size - 2 * SIZE_SZ);
后向合并 – 合并低地址 chunk
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = prev_size(p);
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
unlink(av, p, bck, fwd);
}
下一块不是top chunk – 前向合并 – 合并高地址chunk,并将合并后的chunk放入到unsorted bin中:
// 如果下一个chunk不是top chunk
if (nextchunk != av->top) {
/* get and clear inuse bit */
// 获取下一个 chunk 的使用状态
nextinuse = inuse_bit_at_offset(nextchunk, nextsize);
// 如果不在使用,合并,否则清空当前chunk的使用状态。
/* consolidate forward */
if (!nextinuse) {
unlink(av, nextchunk, bck, fwd);
size += nextsize;
} else
clear_inuse_bit_at_offset(nextchunk, 0);
/*
Place the chunk in unsorted chunk list. Chunks are
not placed into regular bins until after they have
been given one chance to be used in malloc.
*/
// 把 chunk 放在 unsorted chunk 链表的头部
bck = unsorted_chunks(av);
fwd = bck->fd;
// 简单的检查
if (__glibc_unlikely(fwd->bk != bck)) {
errstr = "free(): corrupted unsorted chunks";
goto errout;
}
p->fd = fwd;
p->bk = bck;
// 如果是 large chunk,那就设置nextsize指针字段为NULL。
if (!in_smallbin_range(size)) {
p->fd_nextsize = NULL;
p->bk_nextsize = NULL;
}
bck->fd = p;
fwd->bk = p;
set_head(p, size | PREV_INUSE);
set_foot(p, size);
check_free_chunk(av, p);
}
下一块是top chunk – 合并到top chunk
/*
If the chunk borders the current high end of memory,
consolidate into top
*/
// 如果要释放的chunk的下一个chunk是top chunk,那就合并到 top chunk
else {
size += nextsize;
set_head(p, size | PREV_INUSE);
av->top = p;
check_chunk(av, p);
}
2.2.5 将物理内存还给内核,释放mmap的chunk
/*
If freeing a large space, consolidate possibly-surrounding
chunks. Then, if the total unused topmost memory exceeds trim
threshold, ask malloc_trim to reduce top.
Unless max_fast is 0, we don't know if there are fastbins
bordering top, so we cannot tell for sure whether threshold
has been reached unless fastbins are consolidated. But we
don't want to consolidate on each free. As a compromise,
consolidation is performed if FASTBIN_CONSOLIDATION_THRESHOLD
is reached.
*/
// 如果合并后的 chunk 的大小大于FASTBIN_CONSOLIDATION_THRESHOLD
// 一般合并到 top chunk 都会执行这部分代码。
// 那就向系统返还内存
if ((unsigned long) (size) >= FASTBIN_CONSOLIDATION_THRESHOLD) {
// 如果有 fast chunk 就进行合并
if (have_fastchunks(av)) malloc_consolidate(av);
// 主分配区
if (av == &main_arena) {
#ifndef MORECORE_CANNOT_TRIM
// top chunk 大于当前的收缩阙值
if ((unsigned long) (chunksize(av->top)) >=
(unsigned long) (mp_.trim_threshold))
systrim(mp_.top_pad, av);
#endif // 非主分配区,则直接收缩heap
} else {
/* Always try heap_trim(), even if the top chunk is not
large, because the corresponding heap might go away. */
heap_info *heap = heap_for_ptr(top(av));
assert(heap->ar_ptr == av);
heap_trim(heap, mp_.top_pad);
}
}
if (!have_lock) {
assert(locked);
__libc_lock_unlock(av->mutex);
} } else {
// If the chunk was allocated via mmap, release via munmap().
munmap_chunk(p);
}
3. unlink宏
unlink是ptmalloc2中处理 双向链表的核心宏,它负责从双向链表中移除一个chunk。
3.1 基本逻辑
在不考虑安全检查的情况下,其逻辑就是标准的双向链表节点删除:
- 令P为要移除的chunk;
- FD = P->fd (前向指针,指向下一个块);
- BK = P->bk (后向指针,指向上一个块);
- 执行:FD->bk = BK 且 BK->fd = FD;
3.2 unlink宏的用途
unlink宏主要应用于malloc内存分配、free内存释放、内存合并、再分配:
- malloc内存分配
- 从大小合适的large bin中获取chunk,其他bin没有使用unlink宏;
- 从比请求的chunk所在的bin大的bin中取chunk;
- free内存释放
- 向前合并:如果物理相邻的低地址块是空闲的,系统需要把这个邻居从它原本呆着的链表里取出来,它跟当前块合并成一个更大的块(除了top chunk);
- 向后合并:如果高地址块(非Top Chunk)是空闲的,也要调用unlink把它取出来进行合并;
- malloc_consolidate
- 向前合并:如果物理相邻的低地址块是空闲的,系统需要把这个邻居从它原本呆着的链表里取出来,它跟当前块合并成一个更大的块(除了top chunk);
- 向后合并:如果高地址块(非Top Chunk)是空闲的,也要调用unlink把它取出来进行合并;
- realloc再分配:合并物理相邻高地址空闲chunk(除了top chunk);
3.3 unlink宏的结构
unlink被实现成了一个宏:
/* Take a chunk off a bin list */
// unlink p
#define unlink(AV, P, BK, FD) { \
// 由于 P 已经在双向链表中,所以有两个地方记录其大小,所以检查一下其大小是否一致。
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) \
malloc_printerr ("corrupted size vs. prev_size"); \
FD = P->fd; \
BK = P->bk; \
// 防止攻击者简单篡改空闲的 chunk 的 fd 与 bk 来实现任意写的效果。
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr (check_action, "corrupted double-linked list", P, AV); \
else { \
FD->bk = BK; \
BK->fd = FD; \
// 下面主要考虑 P 对应的 nextsize 双向链表的修改
if (!in_smallbin_range (chunksize_nomask (P)) \
// 如果P->fd_nextsize为 NULL,表明 P 未插入到 nextsize 链表中。
// 那么其实也就没有必要对 nextsize 字段进行修改了。
// 这里没有去判断 bk_nextsize 字段,可能会出问题。
&& __builtin_expect (P->fd_nextsize != NULL, 0)) { \
// 类似于小的 chunk 的检查思路
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0) \
|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0)) \
malloc_printerr (check_action, \
"corrupted double-linked list (not small)", \
P, AV); \
// 这里说明 P 已经在 nextsize 链表中了。
// 如果 FD 没有在 nextsize 链表中
if (FD->fd_nextsize == NULL) { \
// 如果 nextsize 串起来的双链表只有 P 本身,那就直接拿走 P
// 令 FD 为 nextsize 串起来的
if (P->fd_nextsize == P) \
FD->fd_nextsize = FD->bk_nextsize = FD; \
else { \
// 否则我们需要将 FD 插入到 nextsize 形成的双链表中
FD->fd_nextsize = P->fd_nextsize; \
FD->bk_nextsize = P->bk_nextsize; \
P->fd_nextsize->bk_nextsize = FD; \
P->bk_nextsize->fd_nextsize = FD; \
} \
} else { \
// 如果在的话,直接拿走即可
P->fd_nextsize->bk_nextsize = P->bk_nextsize; \
P->bk_nextsize->fd_nextsize = P->fd_nextsize; \
} \
} \
} \
}
4. Tcache技术
Tcache (Thread Local Cache) 是glibc 从 2.26版本开始引入的一种内存管理机制,旨在通过减少线程间的锁竞争来提升内存分配性能。它是内存申请和释放流程中的“第一站”,优先级高于传统的Fast bins和Unsorted Bin。
4.1 核心设计与结构
Tcache为每个线程维护了一块私有的缓存区域,其核心数据结构如下:
- tcache_perthread_struct:每个线程拥有一个该结构,包含counts(记录每个bin中的chunk数量)和entries(指向每个bin链表头部的指针数组);
- tcache_entry是用于管理Tcache单向链表的核心结构体。它非常精简,直接复用了空闲内存块(chunk)的 用户数据区(Payload) 来存储链表指针;
- 单向链表结构:Tcache包含64个bins,每个bin是一个单向链表,默认最多存放7个chunk;
- 范围与大小:在64位系统上,它通常处理大小在16到1032字节(
0x408)之间的chunk;
- 指针位置:与Fastbins不同,Tcache链表的指针直接指向chunk的 用户数据区(Payload),而不是chunk头部;
4.2 工作原理
Tcache遵循LIFO(后进先出) 原则,且操作过程中不涉及加锁:
- 申请内存时:系统首先检查Tcache中是否有合适大小的空闲块。如果有,直接摘取并返回给用户,跳过后续复杂的_int_malloc流程;
- 释放内存时:
- 如果释放的chunk大小符合Tcache范围且对应bin未满(计数 < 7),则直接放入Tcache并返回;
- 放入Tcache的chunk不会清除In-use位,也不会与相邻块合并;
- 填充机制:如果从Fast bins或Small Bins中取出一个chunk,系统会顺便把该bin中剩余的块尽量填入Tcache,直到填满为止;
4.3 tcache_perthread_struct结构
tcache_perthread_struct是Tcache机制的核心管理中心,每个线程都有一个独立的该结构实例(存储在线程局部存储TLS中),结构体定义如下:
typedef struct tcache_perthread_struct
{
uint16_t counts[TCACHE_MAX_BINS]; //记录每个 bin 链表中当前存放的空闲 chunk 数量,默认上限为7,当计数达到 7 时,新释放的内存将绕过 Tcache,进入传统的 _int_free 流程。
tcache_entry *entries[TCACHE_MAX_BINS]; //存储每个 size 对应的单向链表表头指针,指针数组,指向 tcache_entry 结构,TCACHE_MAX_BINS 通常为 64。
} tcache_perthread_struct;
# define TCACHE_MAX_BINS 64
static __thread tcache_perthread_struct *tcache = NULL;
4.4 tcache_entry结构
glibc 的源码中,tcache_entry是用于管理Tcache单向链表的核心结构体。它非常精简,直接复用了空闲内存块(chunk)的用户数据区(Payload) 来存储链表指针。结构定义如下:
typedef struct tcache_entry
{
struct tcache_entry *next; //指向链表中下一个相同大小的空闲块,它占据了用户数据区的起始位置(即原本 chunk->fd 的位置)。
//在 glibc 2.32+ 版本中,这个指针会被加密(与当前地址进行异或运算),以防止堆溢出直接篡改地址。
/* This field exists to detect double frees. */
struct tcache_perthread_struct *key; //紧跟在 next 之后(即原本 chunk->bk 的位置),专门用于 Double Free 检测
//当一个块被放入 Tcache 时,key 字段会被设置为该线程 tcache 结构的起始地址。如果再次释放同一个块,系统会检查 key 是否匹配;如果匹配,则遍历链表确认是否重复释放。
} tcache_entry;
4.5 __tcache_init()
glibc的堆管理体系中,__tcache_init(通常在源码中表现为tcache_init宏或相关的内部初始化逻辑)负责为当前线程开辟并初始化私有的Tcache管理结构。它是Tcache机制运行的基石。
__tcache_init并不是在程序启动时立即为所有线程执行,而是采用延迟初始化(Lazy Initialization)策略:
- 当一个线程第一次调用malloc(或相关分配函数)时,系统会检查该线程是否已经拥有tcache结构;
- 如果不存在,则触发初始化流程;
该函数的主要任务是创建tcache_perthread_struct:
- 1.申请管理空间:调用
_int_malloc或直接从Arena中申请一块足以容纳tcache_perthread_struct的内存(在64位系统上大小约为0x400字节); - 2.清理与重置:
- 将
counts数组全部置为0(表示初始时所有 bin 都没有空闲块); - 将
entries指针数组全部置为NULL;
- 将
- 3.线程绑定:将分配到的结构体地址存储在线程局部存储(TLS)的变量中。这样,该线程后续的
malloc和free操作就能通过偏移量快速找到属于自己的Tcache;
函数定义如下:
tcache_init(void)
{
mstate ar_ptr;
void *victim = 0;
const size_t bytes = sizeof (tcache_perthread_struct);
if (tcache_shutting_down)
return;
arena_get (ar_ptr, bytes); // 找到可用的 arena
victim = _int_malloc (ar_ptr, bytes); // 申请一个 sizeof(tcache_perthread_struct) 大小的 chunk
if (!victim && ar_ptr != NULL)
{
ar_ptr = arena_get_retry (ar_ptr, bytes);
victim = _int_malloc (ar_ptr, bytes);
}
if (ar_ptr != NULL)
__libc_lock_unlock (ar_ptr->mutex);
/* In a low memory situation, we may not be able to allocate memory
- in which case, we just keep trying later. However, we
typically do this very early, so either there is sufficient
memory, or there isn't enough memory to do non-trivial
allocations anyway. */
if (victim) // 初始化 tcache
{
tcache = (tcache_perthread_struct *) victim;
memset (tcache, 0, sizeof (tcache_perthread_struct));
}
}
4.6 tcache_get()
tcache_get是一个非常简洁且高效的内联函数(Inline Function),它的唯一任务是:从指定的Tcache Bin中取出最顶端的内存块(chunk)并返回给用户。函数定义如下:
/* Caller must ensure that we know tc_idx is valid and there's
available chunks to remove. */
static __always_inline void *
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
assert (tc_idx < TCACHE_MAX_BINS);
assert (tcache->entries[tc_idx] > 0);
tcache->entries[tc_idx] = e->next;
--(tcache->counts[tc_idx]); // 获得一个 chunk,counts 减一
return (void *) e;
}
当__libc_malloc确定某个size对应的Tcache Bin中有空闲块(即counts[tc_idx] > 0)时,会调用tcache_get:
- 1.定位 Bin:根据请求的大小索引
tc_idx,找到tcache->entries[tc_idx]指向的单向链表头; - 2.提取 Chunk:将链表头部的第一个
tcache_entry提取出来作为“受害者”(victim); - 3.更新链表:将链表头指针更新为
victim->next(即指向下一个空闲块);在 glibc 2.32+ 中,这里的next指针是经过异或加密的,tcache_get会在此时进行解密操作; - 4.递减计数:将该bin对应的
counts[tc_idx]减 1; - 5.清理 Key:将取出块的
key字段(原本用于Double Free检测)清零,防止干扰后续使用; - 6.返回指针:返回该内存块的用户数据区地址;
4.7 tcache_put()
tcache_put是与tcache_get对应的内联函数。它的唯一任务是:将一个刚刚释放的内存块(chunk)放入线程私有的Tcache链表中。它是free流程中命中Tcache逻辑后的核心执行步骤。函数定义如下:
/* Caller must ensure that we know tc_idx is valid and there's room
for more chunks. */
static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
assert (tc_idx < TCACHE_MAX_BINS);
e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}
当__libc_free函数确定某个chunk大小符合Tcache范围且对应Bin未满(计数 < 7)时,会调用tcache_put:
- 1.转换为 Entry:将用户指针(chunk的payload部分)转换为tcache_entry结构指针;
- 2.设置Key(安全检查):在chunk的key字段(对应bk位置)写入当前线程tcache结构的地址;后续free时如果发现key已存在,则触发Double Free检查;
- 3.入链(LIFO):将该chunk插入到对应size的单向链表头部:
- 将e->next指向当前的tcache->entries[tc_idx];
- 在glibc 2.32+中,会对next指针执行异或加密(PROTECT_PTR);
- 4.更新表头:将tcache->entries[tc_idx]指向这个新加入的chunk;
- 5.增加计数:将该bin对应的counts[tc_idx]加1;
完全在线程本地内存操作,不涉及 mutex 竞争;且不改变Chunk状态,因此不会触发内存合并。
5. 参考链接
1. 一般免责声明:本文所提供的技术信息仅供参考,不构成任何专业建议。读者应根据自身情况谨慎使用且应遵守《中华人民共和国网络安全法》,作者及发布平台不对因使用本文信息而导致的任何直接或间接责任或损失负责。
2. 适用性声明:文中技术内容可能不适用于所有情况或系统,在实际应用前请充分测试和评估。若因使用不当造成的任何问题,相关方不承担责任。
3. 更新声明:技术发展迅速,文章内容可能存在滞后性。读者需自行判断信息的时效性,因依据过时内容产生的后果,作者及发布平台不承担责任。