前言在 KVStore 项目中每次 SET/DEL/HASH 操作都会产生大量小对象分配。如果每次都直接调用malloc()轻则延迟抖动重则内存碎片化导致系统变慢甚至 OOM。本文基于一个实战级内存池实现逐层剖析它的设计思路、核心数据结构、多线程优化以及实际使用中遇到的坑。项目源码KVStore with MemPool适用场景高并发短生命周期小对象KV存储、协议处理、游戏服务器一、整体架构pool_alloc(size_t n) │ ├── n 512大块→ 直接 malloc 链表管理 │ └── n ≤ 512小块→ 从 Size Class 分配 │ ├── 线程本地缓存TLC命中→ 直接返回无锁 ├── 全局自由链表有空闲→ 加锁取出 └── 都没有→ 分配新 Chunk头插到 free_list内存池的核心思想一次批量申请一大块Chunk切分成 N 个小Block按需分配/回收。二、核心数据结构2.1 Size Class大小分级staticconstsize_tsize_classes[]{64,128,256,512};#defineNUM_CLASSES(sizeof(size_classes)/sizeof(size_classes[0]))只管理 4 个档位Class 0Class 1Class 2Class 3≤64B≤128B≤256B≤512B超过 512 字节的请求直接回退到malloc不参与池化管理。2.2 Chunk物理块typedefstructchunk{structchunk*next;void*mem;// malloc 申请的大块内存起始地址size_tblocks;// 这个大块包含多少个小块}chunk;每次class_add_chunk申请blocks × block_size大小的内存然后切成小块串成链表。2.3 Class Meta管理结构typedefstructclass_meta{size_tblock_size;// 该 class 的块大小chunk*chunks;// 所有物理块链表free_node*global_free;// 全局空闲链表头插法pthread_mutex_tlock;// 保护该 class 的并发访问size_ttotal_bytes;// 已申请总字节}class_meta;staticclass_meta g_classes[NUM_CLASSES];2.4 线程本地缓存Thread-Local Cachetypedefstructtlc_bucket{free_node*items[THREAD_LOCAL_CACHE_LIMIT];// 栈存空闲块指针inttop;}tlc_bucket;typedefstructtlc{tlc_bucket buckets[NUM_CLASSES];// 每个 class 一个桶}tlc;static__thread tlc*thread_cacheNULL;// TLS每个线程独立这是无锁分配的关键分配时先查本地缓存只有本地缓存满了才访问全局链表加锁。2.5 大块管理链表typedefstructlarge_node{structlarge_node*next;void*ptr;size_tsize;}large_node;超过 512 字节的分配走malloc用这个链表记录释放时能找到并正确free。三、最关键的设计Size 追踪表这是本实现最有技术含量的部分。问题pool_free(p, n)要求调用者传入当初分配时的大小。但应用层代码往往不知道/记不住这个大小很容易传错// 应用层pool_free(node-key,512);// 实际分配的是 strlen(key)1可能不是 512传错大小会导致class_index_for_size(512)算出的 class 索引不对块被放错链表后续取出来用会越界最严重堆损坏 → malloc assertion failure解决方案分配记录表Hash Table#defineHT_SIZE4096typedefstruct{void*ptr;size_tsize;intin_use;// 0空, 1使用中}alloc_record;staticalloc_record*g_alloc_tableNULL;// 全局哈希表staticintht_hash(void*ptr){return((uintptr_t)ptr)%HT_SIZE;// 简单的取模哈希}分配时记录void*pool_alloc(size_tn){// ...ht_insert(node,g_classes[idx].block_size);// 记录真实分配大小return(void*)node;}释放时查找voidpool_free(void*p,size_tn){size_tactual_sizeht_lookup(p);// 查表获取真实大小if(actual_size0){nactual_size;// 用真实大小覆盖传入的大小ht_remove(p);// 移除记录}// ... 正常处理}即使应用层传入错误的大小也能靠这张表自我纠正这就是为什么之前kvs_hash.c里写pool_free(node, sizeof(hashnode_t))不会马上崩溃——只是浪费了表的空间。四、核心 API 详解4.1 pool_initintpool_init(size_tinitial_bytes){g_alloc_tablecalloc(HT_SIZE,sizeof(alloc_record));// 初始化哈希表// 初始化每个 class 的互斥锁和元数据for(inti0;iNUM_CLASSES;i){g_classes[i].block_sizesize_classes[i];g_classes[i].chunksNULL;g_classes[i].global_freeNULL;pthread_mutex_init(g_classes[i].lock,NULL);}// 默认分配BLOCKS_PER_CHUNK(4096) × 最大块(512) ≈ 2MB 初始池if(initial_bytes0)initial_bytesBLOCKS_PER_CHUNK_DEFAULT*g_classes[NUM_CLASSES-1].block_size;// 为每个 class 预分配一个 chunksize_tper_classinitial_bytes/NUM_CLASSES;for(inti0;iNUM_CLASSES;i){size_tblocksper_class/g_classes[i].block_size;if(blocks0)blocksBLOCKS_PER_CHUNK_DEFAULT;class_add_chunk(i,blocks);}return0;}4.2 pool_alloc三层分发void*pool_alloc(size_tn){if(n0)returnNULL;// 第一层超过 512直接走 mallocintidxclass_index_for_size(n);if(idx-1){void*pmalloc(n);register_large(p,n);// 记录到大块链表ht_insert(p,n);// 追踪大小returnp;}// 第二层查线程本地缓存完全无锁极快tlc_bucket*bucketthread_cache-buckets[idx];if(bucket-top0){bucket-top--;free_node*nodebucket-items[bucket-top];memset(node,0,block_size);ht_insert(node,block_size);return(void*)node;}// 第三层查全局自由链表需要加锁pthread_mutex_lock(g_classes[idx].lock);free_node*nodeg_classes[idx].global_free;if(node){g_classes[idx].global_freenode-next;pthread_mutex_unlock(g_classes[idx].lock);ht_insert(node,block_size);return(void*)node;}// 第四层全局也没有申请新 Chunkif(class_add_chunk(idx,BLOCKS_PER_CHUNK_DEFAULT)!0){pthread_mutex_unlock(g_classes[idx].lock);returnNULL;}nodeg_classes[idx].global_free;g_classes[idx].global_freenode-next;pthread_mutex_unlock(g_classes[idx].lock);ht_insert(node,block_size);return(void*)node;}分配路径总结pool_alloc(100) → class_index_for_size(100) → idx1 (128B class) → TLC 有空块→ 直接返回无锁 → 全局 free_list 有空块→ 加锁取出 → 都没有→ 分配新 Chunk → 取出一块返回4.3 pool_free智能回收voidpool_free(void*p,size_tn){if(!p)return;// 关键从追踪表获取真实大小自我纠正size_tactualht_lookup(p);if(actual0)nactual,ht_remove(p);intidxclass_index_for_size(n);if(idx-1){unregister_and_free_large(p);return;}tlc_bucket*bucketthread_cache-buckets[idx];// 本地缓存没满 → 直接压入本地栈无锁if(bucket-topTHREAD_LOCAL_CACHE_LIMIT){bucket-items[bucket-top](free_node*)p;return;}// 本地缓存满了 → 批量将一半块放回全局 free_listinthalfTHREAD_LOCAL_CACHE_LIMIT/2;pthread_mutex_lock(g_classes[idx].lock);for(inti0;ihalf;i){free_node*nodebucket-items[--bucket-top];node-nextg_classes[idx].global_free;g_classes[idx].global_freenode;}// 当前要释放的块也加入全局链表free_node*node_cur(free_node*)p;node_cur-nextg_classes[idx].global_free;g_classes[idx].global_freenode_cur;pthread_mutex_unlock(g_classes[idx].lock);}五、在 KVStore 中的应用5.1 切换开关#defineNONPOOL0#defineMEMPOOL1#definePOOL_SELECTMEMPOOL// 改成 NONPOOL 就用系统 malloc5.2 典型使用模式以 Hash 表节点创建为例hashnode_t*node(hashnode_t*)kvs_malloc(sizeof(hashnode_t));// node 始终用系统 malloc固定大小管理简单#if(POOL_SELECTMEMPOOL)char*kcopy(char*)pool_alloc(klen);char*kvalue(char*)pool_alloc(vlen);// 分配失败回退if(!kcopy){kvs_free(node);returnNULL;}if(!kvalue){pool_free(kcopy,klen);kvs_free(node);returnNULL;}#elsechar*kcopykvs_malloc(strlen(key)1);// ...#endif5.3 致命陷阱混用内存分配器以下代码会导致堆损坏曾经的真实 bug// ❌ 错误node 用系统 malloc 分配却用 pool_free 释放hashnode_t*node(hashnode_t*)kvs_malloc(sizeof(hashnode_t));// ...pool_free(node,sizeof(hashnode_t));// ← 灾难pool 不知道这块是 malloc 的// ✅ 正确系统分配 → 系统释放池分配 → 池释放kvs_free(node);// 系统 malloc → 系统 freepool_free(node,sizeof(hashnode_t));// 池分配 → 池释放最佳实践结构体节点本身用kvs_malloc固定大小key/value 内容用pool_alloc变长。释放时 node 结构体kvs_freekey/valuepool_free。六、锁竞争优化内存池的最大瓶颈是全局锁。本实现通过两层缓存将锁竞争降到最低线程 A: pool_alloc() → TLC 命中 → 0 次加锁 线程 B: pool_alloc() → TLC 命中 → 0 次加锁 线程 C: pool_alloc() → TLC 满 → 全局加锁 1 次 线程 D: pool_free() → TLC 满 → 全局加锁 1 次TLC 本地缓存充当了隔离层大多数分配完全无锁。七、调试与排查7.1 常见崩溃原因pool_free传入大小与实际不符→ ht_lookup 查不到块放错链表 → 下次取用越界混用分配器→ 系统 malloc 的块传给 pool_free → 堆元数据彻底破坏重复释放→ ht_remove 标记为空后再次释放同一指针 → 未定义行为7.2 调试技巧// 1. 打印池状态printf(Pool capacity: %zu bytes\n,pool_capacity());printf(Total allocated: %zu bytes\n,pool_total_allocated());// 2. 内存池关闭时全量检测// pool_destroy() 会遍历所有 chunk 和 large_node 释放// 如果有遗漏会直接暴露八、总结特性实现方式小对象分配≤512B固定-size class Chunk 切分大对象512B直接 malloc 链表管理无锁分配TLS 线程本地缓存大块并发安全独立互斥锁 大块专用链表大小自我纠正Hash 表追踪 (ptr→size)批量回收本地缓存满时批量头插回全局这个内存池在 KVStore 的 4 种存储引擎Array / RBTREE / Hash / SkipList中运行稳定经受了高并发和大量数据反复增删的考验。核心设计理念小事不走系统 malloc大事不走池——各司其职井水不犯河水。