tcache(Thread Local Caching,线程本地缓存)是 Glibc 2.26 引入的机制,目的是为每个线程提供一个无锁的小内存缓存,从而大幅减少多线程环境下的锁竞争,提升小内存分配和释放的速度。
可以把它理解为每个线程私有的“随身小口袋”——释放的小内存块先塞进自己口袋,下次申请时直接从口袋里掏,不需要去公共仓库(Arena)排队。
1. 为什么需要 tcache?
在 tcache 出现之前,小内存的分配路径是:
malloc → Arena 锁 → Fastbin/Small Bin → 返回 free → Arena 锁 → Fastbin/Small Bin → 入链
每次操作都要获取 Arena 锁,多线程下锁竞争严重,性能瓶颈明显。
tcache 的设计目标是:
无锁:每个线程独立访问自己的 tcache,不需要任何锁。
快速:分配和释放都是 O(1) 的链表头操作。
局部性:利用线程的时间局部性,最近释放的块最可能被同一线程再次使用。
2. 数据结构
每个线程拥有一个独立的tcache_perthread_struct,通过线程局部存储(TLS)访问:
typedef struct tcache_perthread_struct { uint16_t counts[TCACHE_MAX_BINS]; // 每个 bin 当前的 chunk 数量 tcache_entry *entries[TCACHE_MAX_BINS]; // 每个 bin 的单向链表头 } tcache_perthread_struct; typedef struct tcache_entry { struct tcache_entry *next; // 指向下一个空闲块 struct tcache_perthread_struct *key; // 用于双重释放检测 } tcache_entry;关键字段:
| 字段 | 作用 |
|---|---|
counts[i] | 记录第i个 bin 中当前有多少个空闲块(上限为 7) |
entries[i] | 指向第i个 bin 的单向链表头 |
next | 链表指针,指向下一个空闲块 |
key | 指向所属的 tcache 结构体,用于检测双重释放 |
TCACHE_MAX_BINS通常为 64,覆盖 32~1040 字节(步长 16 字节)的小内存块。
3. 核心工作流程
A. 释放路径(free→ tcache)
当程序调用free(p)释放一块小内存时:
计算 bin 索引:根据 chunk 大小,计算对应的 tcache bin 索引。
检查 tcache 是否已满:
如果
counts[idx] < 7(未满),直接将 chunk 插入entries[idx]链表头部。如果
counts[idx] == 7(已满),则回退到传统路径:进入 Fastbin 或 Unsorted Bin。
设置
key字段:将 chunk 的key指向当前线程的 tcache 结构体,用于后续检测双重释放。更新计数:
counts[idx]++。
关键点:整个过程不需要获取 Arena 锁,速度极快。
B. 分配路径(malloc→ tcache)
当程序调用malloc(size)申请一块小内存时:
计算 bin 索引:根据请求大小,计算对应的 tcache bin 索引。
检查 tcache 是否为空:
如果
entries[idx] != NULL,直接从链表头部取出一个 chunk,返回给用户。如果
entries[idx] == NULL(空),则回退到传统路径:去 Fastbin、Small Bin 或 Unsorted Bin 查找。
更新计数:
counts[idx]--。
关键点:同样不需要获取 Arena 锁,O(1) 完成。
4. tcache 的容量限制
每个 bin 最多缓存7 个chunk(由TCACHE_FILL_COUNT定义,通常为 7)。
为什么是 7?
平衡内存开销和命中率:7 个块足以应对大多数“释放-重分配”的短周期模式。
避免过度缓存:如果缓存太多,会导致内存无法归还给 Arena,造成浪费。
经验值:Glibc 开发者通过实际测试发现 7 是一个较好的平衡点。
当 tcache 满了之后,后续释放的块会进入 Fastbin 或 Unsorted Bin,参与传统的合并和整理流程。
5. tcache 与 Fastbin 的关系
tcache 出现后,Fastbin 的角色发生了变化:
| 特性 | tcache | Fastbin |
|---|---|---|
| 位置 | 线程私有(TLS) | Arena 内(共享) |
| 锁 | 无锁 | 需要 Arena 锁 |
| 大小范围 | 32~1040 字节 | 32~160 字节(默认) |
| 数量限制 | 每个 bin 最多 7 个 | 无硬性限制 |
| 合并 | ❌ 不合并 | ❌ 不合并 |
| 优先级 | 最高(先查 tcache) | 次之(tcache 空/满时使用) |
实际流程:
free时:优先放 tcache,tcache 满了才放 Fastbin。malloc时:优先查 tcache,tcache 空了才查 Fastbin。
这意味着 Fastbin 现在更多扮演“tcache 的后备仓库”角色。
6. 安全性:双重释放检测
tcache 引入了一个简单的双重释放检测机制:
当 chunk 被放入 tcache 时,它的
key字段被设置为指向当前线程的 tcache 结构体。当再次释放同一个 chunk 时,如果
key字段仍然指向有效的 tcache 结构体,说明这是双重释放,触发malloc_printerr报错。
但注意:这个检测不是万无一失的。如果攻击者能修改key字段,或者 chunk 被重新分配后key被覆盖,仍然可能绕过检测。
7. 完整流程图
malloc(size): │ ▼ 计算 tcache bin 索引 │ ▼ tcache.entries[idx] 非空? │ ├── 是 → 取出链表头,返回 (无锁,O(1)) │ └── 否 → 回退到传统路径 (Fastbin → Small Bin → Unsorted Bin → Top Chunk) free(p): │ ▼ 计算 tcache bin 索引 │ ▼ tcache.counts[idx] < 7? │ ├── 是 → 插入 tcache 链表头,设置 key (无锁,O(1)) │ └── 否 → 回退到传统路径 (Fastbin → Unsorted Bin)
8. 总结
| 要点 | 说明 |
|---|---|
| 核心目标 | 为每个线程提供无锁的小内存缓存,减少锁竞争 |
| 数据结构 | counts[64]+entries[64],每个 bin 最多 7 个块 |
| 释放路径 | 优先放 tcache,满了才放 Fastbin |
| 分配路径 | 优先查 tcache,空了才查 Fastbin |
| 大小范围 | 32~1040 字节(64 位系统) |
| 安全性 | 通过key字段检测双重释放,但并非绝对可靠 |
| 性能影响 | 小内存分配/释放几乎无锁,速度极快 |
一句话理解:
tcache 是每个线程的“私有小口袋”,让小内存的分配和释放绕过了 Arena 锁,是 Glibc 在性能优化上的一次重要飞跃。它的出现让 Fastbin 从“第一道防线”退居为“第二道防线”。