
tcacheThread 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 中当前有多少个空闲块上限为 7entries[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 的角色发生了变化特性tcacheFastbin位置线程私有TLSArena 内共享锁无锁需要 Arena 锁大小范围32~1040 字节32~160 字节默认数量限制每个 bin 最多 7 个无硬性限制合并❌ 不合并❌ 不合并优先级最高先查 tcache次之tcache 空/满时使用实际流程free时优先放 tcachetcache 满了才放 Fastbin。malloc时优先查 tcachetcache 空了才查 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 从“第一道防线”退居为“第二道防线”。