本章导读
池是第 5 章分配器的"批发放电商":等大对象按槽分配——页满自动加页、释放即时进 LIFO 空闲表、下一次分配拿到的是最近释放的槽(缓存还热)。连接对象、帧结构、消息头这类"同一种东西成千上万个"的场景,池把分配成本从"系统调用级"压到"空闲表弹出一个指针",同时消除了等大分配天然的碎片问题——槽永远不会因为大小不齐而散布。本章讲固定池 xpool 的完整生命周期(分页、LIFO 复用、两种标记回收)与变长池(大小混杂的场景),并用第 6 章统计验证池化收益——pooled 与 backing 的比值曲线在池化后是什么形状,跑一遍就有直觉。
22.1引入
一个网关服务维护十万条连接,每条连接对象 48 字节。逐个 xrtMalloc/xrtFree:每次断连重连都过系统分配器,碎片慢慢累积,第 6 章统计里 backing 次数随流量线性上涨。换成固定池:连接对象从槽里取、还回槽里——分配与释放都是指针操作;系统调用只发生在"加页"那一瞬间(一页约 64KB,装 256 个 48 字节的槽之后才有下一页)。流量再大,backing 曲线是台阶状:每 256 次分配才跳一格。这就是池的全部价值——把分配的粒度从对象级提到页级。
什么时候不该用池?对象大小五花八门(用变长池或直接堆)、对象数量极少(池的管理成本大于收益)、生命周期极短且整齐(第 7 章 arena 更合适——池解决"频繁取放",arena 解决"一起回收",两者是不同问题的答案,常被混淆却从不可互换)。这段"什么时候不用"与前面的"什么时候用"同样重要:池的正确使用从正确的"不用"开始。
22.2概念
固定池模型
LIFO 复用不是实现细节,是缓存友好的设计决策:最近释放的槽大概率还在各级缓存里,复用它比取一个久未触碰的槽更快——示例会实证 pReused == pReleased。Owns 判断指针是否属于本池——防御外来指针混入释放(把堆指针 Free 给池是未定义行为,Owns 是那道安检门)。
两种标记回收
逐个 Free 在批量处理场景是浪费——一千个对象逐个还,不如标记后一次处理。池提供两个方向:Sweep 保留标记、回收未标记(批量处理器标记"还要的",其余一扫而空——缓存逐出、请求结束清理);FreeMarked 释放标记、保留其余(引用计数归零者被标记后放回——与第 3 章引用计数配合的半自动回收)。两个方向覆盖了"留少数删多数"与"删少数留多数"两种形态,选方向看哪边标记更少。
变长池与池化统计
变长池(memory_pool 范例演示)处理大小混杂的分配:内部按尺寸分桶,小分配走桶、大分配直通。第 6 章 xmemstats 的 pooled(池化命中)与 backing(系统调用)两列就是池化效果的可观测面——池化良好的服务,业务分配次数远大于 backing,且曲线呈台阶状(加页时刻)。容量规划与内存预算也在这一步落地:live/peak 告诉你池的实际水位。
页与归还
SetRetain 设定 Reset 后保留多少空页(应对突发流量的预热);Trim 主动归还空页(内存紧张期);Create/CreateLayout 堆创建形态可显式指定每页槽数(对齐你的业务批量)。页的生命周期归池——你从不直接碰页,只通过槽接口进出。
22.3示例
完整程序:固定池全生命周期
来自仓库范例 examples/memory/pool/main.c——分页增长、LIFO 复用、标记回收、统计验证一网打尽:
*/ #include <stdio.h> #include <xrt.h> /* 固定大小任务对象:32 字节(4 + 28),池按此步长切槽。 */ typedef struct examplejob { uint32 Id; char Name[28]; } examplejob; /* 访问器:按槽下标顺序回调;本例同时校验下标连续性。 */ static bool exampleVisitJob(ptr pObject, size_t iIndex, ptr pUserData) { size_t* pCount = (size_t*)pUserData; if ( (pObject == NULL) || (iIndex != *pCount) ) { return false; } (*pCount)++; return true; } int main(void) { xpool tPool; xpool* pCreated; xpool* pCompact; examplejob* arrJob[300]; examplejob* pReleased; examplejob* pReused; xpoolinfo tInfo; size_t iVisited = 0; /* 32 字节对齐:适合 SIMD 或缓存行对齐敏感的对象。 */ if ( !xrtPoolInitAligned(&tPool, sizeof(examplejob), 32) ) { return 1; } /* 300 个对象触发跨页增长(每页约 256 槽 → 2 页)。 */ for ( size_t i = 0; i < 300; i++ ) { arrJob[i] = (examplejob*)xrtPoolCalloc(&tPool); if ( arrJob[i] == NULL ) { xrtPoolUnit(&tPool); return 2; } arrJob[i]->Id = (uint32)i; } /* LIFO 复用验证:释放 10 号槽后立即取回——必须是同一个地址。 */ pReleased = arrJob[10]; if ( !xrtPoolFree(&tPool, pReleased) ) { xrtPoolUnit(&tPool); return 3; } pReused = (examplejob*)xrtPoolAlloc(&tPool); if ( pReused != pReleased ) { xrtPoolUnit(&tPool); return 4; } pReused->Id = 10; /* 归属自检 + 遍历计数:300 个全部在池且下标连续。 */ if ( !xrtPoolOwns(&tPool, pReused) ) { xrtPoolUnit(&tPool); return 5; } if ( xrtPoolVisit(&tPool, exampleVisitJob, &iVisited) != 300 ) { xrtPoolUnit(&tPool); return 6; } /* Sweep:只标记 0 号 → 回收其余 299 个。 */ if ( !xrtPoolMark(&tPool, arrJob[0]) || (xrtPoolSweep(&tPool) != 299) ) { xrtPoolUnit(&tPool); return 7; } /* FreeMarked:标记 0 号 → 释放它(与 Sweep 方向相反)。 */ if ( !xrtPoolMark(&tPool, arrJob[0]) || (xrtPoolFreeMarked(&tPool) != 1) ) { xrtPoolUnit(&tPool); return 8; } */ if ( xrtPoolTrim(&tPool, 0) == 0 ) { xrtPoolUnit(&tPool); return 9; } xrtPoolSetRetain(&tPool, 2); for ( size_t i = 0; i < 300; i++ ) { arrJob[i] = (examplejob*)xrtPoolCalloc(&tPool); if ( arrJob[i] == NULL ) { xrtPoolUnit(&tPool); return 10; } } /* Reset 批量释放全部 300 个,保留 2 个空页(见输出 empty=2)。 */ if ( xrtPoolReset(&tPool) != 300 ) { xrtPoolUnit(&tPool); return 11; } xrtPoolGet(&tPool, &tInfo); printf( "page_capacity=%zu pages=%zu empty=%zu live=%zu peak=%zu visited=%zu\n", tInfo.PageCapacity, tInfo.PageCount, tInfo.EmptyPages, tInfo.LiveCount, tInfo.PeakCount, iVisited ); xrtPoolUnit(&tPool); */ pCompact = xrtPoolCreate(8192); if ( (pCompact == NULL) || (pCompact->PageCapacity != 8) || (xrtPoolAlloc(pCompact) == NULL) || (pCompact->Pages->MemorySize > XRT_POOL_PAGE_BYTES_DEFAULT) ) { xrtPoolDestroy(pCompact); return 12; } xrtPoolDestroy(pCompact); /* 显式布局:每页恰好 128 槽(容量可查证)。 */ pCreated = xrtPoolCreateLayout(sizeof(examplejob), 32, 128); if ( pCreated == NULL ) { return 13; } if ( pCreated->PageCapacity != 128 ) { xrtPoolDestroy(pCreated); return 14; } xrtPoolDestroy(pCreated); /* 默认对齐 Init + 显式布局 Init + 指定对齐 Create。 */ { xpool DefaultPool; xpool LayoutPool; xpool* pAligned; if ( !xrtPoolInit(&DefaultPool, sizeof(examplejob)) ) { return 15; } if ( !xrtPoolInitLayout(&LayoutPool, sizeof(examplejob), 32u, 64u) || (LayoutPool.PageCapacity != 64u) ) { xrtPoolDestroy(&LayoutPool); return 16; } pAligned = xrtPoolCreateAligned(sizeof(examplejob), 32u); if ( (pAligned == NULL) || (pAligned->Alignment < 32u) ) { xrtPoolDestroy(pAligned); xrtPoolDestroy(&LayoutPool); xrtPoolDestroy(&DefaultPool); return 17; } xrtPoolDestroy(pAligned); xrtPoolDestroy(&LayoutPool); xrtPoolDestroy(&DefaultPool); } return 0; }
刚才发生了什么。 ① 分配 300 个对象:每页 256 槽,前两页装 512——pages=2、peak=300。② 释放与复用的实证:释放某槽后立刻分配,拿回同一个地址——LIFO 空闲表把"最近释放优先复用"写进了可断言的行为。③ 标记回收后 live=0、empty=2:两页全部空闲但页还保留着(SetRetain 的预热语义);visited=300 来自 Visit 的按槽序遍历——遍历活动对象做统计、做 GC 标记起点都是这个接口。④ 输出最后一行的每个数字都能从第 6 章统计侧对上——池的可观测性与分配器收口是同一套体系。
完整程序:变长池与水位统计
来自 examples/memory/memory_pool/main.c,大小混杂场景的池化:
*/ #include <stdio.h> #include <string.h> #include <xrt.h> /* 访问器:每块回调一次(指针/大小/对齐);返回 false 终止遍历。 */ static bool exampleVisitMemory( ptr pMemory, size_t iSize, size_t iAlignment, ptr pUserData ) { size_t* pCount = (size_t*)pUserData; if ( (pMemory == NULL) || (iSize == 0) || (iAlignment == 0) ) { return false; } (*pCount)++; return true; } int main(void) { xmempool tPool; /* 栈句柄;128 为初始保留字节提示 */ xmempool* pCreated; char* sText; bytes pPacket; bytes pAligned; xmempoolinfo tInfo; size_t iVisited = 0; if ( !xrtMemPoolInit(&tPool, 128) ) { return 1; } /* 三种分配形态:清零小块 / 4KB 大块 / 64 字节对齐块。 */ sText = (char*)xrtMemPoolCalloc(&tPool, 1, 24); pPacket = (bytes)xrtMemPoolAlloc(&tPool, 4096); pAligned = (bytes)xrtMemPoolAllocAligned(&tPool, 7, 64); if ( (sText == NULL) || (pPacket == NULL) || (pAligned == NULL) ) { xrtMemPoolUnit(&tPool); return 2; } /* Realloc 扩容:内容保留("xrt memory pool" 原样可读)。 */ memcpy(sText, "xrt memory pool", 16); sText = (char*)xrtMemPoolRealloc(&tPool, sText, 80); if ( (sText == NULL) || (strcmp(sText, "xrt memory pool") != 0) ) { xrtMemPoolUnit(&tPool); return 3; } /* 三项自检:池归属 / 块大小 ≥ 80 / 对齐块地址 % 64 == 0。 */ if ( !xrtMemPoolOwns(&tPool, sText) || (xrtMemPoolSize(&tPool, sText) < 80) || (((uintptr_t)pAligned % 64) != 0) ) { xrtMemPoolUnit(&tPool); return 4; } /* 访问器应数到 3 个活动块。 */ if ( xrtMemPoolVisit(&tPool, exampleVisitMemory, &iVisited) != 3 ) { xrtMemPoolUnit(&tPool); return 5; } */ if ( !xrtMemPoolMark(&tPool, sText) || !xrtMemPoolMark(&tPool, pPacket) || (xrtMemPoolSweep(&tPool) != 1) ) { xrtMemPoolUnit(&tPool); return 6; } */ if ( !xrtMemPoolMark(&tPool, pPacket) || (xrtMemPoolFreeMarked(&tPool) != 1) || !xrtMemPoolFree(&tPool, sText) ) { xrtMemPoolUnit(&tPool); return 7; } /* Reset 批量释放活动块(返回释放数);Trim 把尺寸类空页还给系统。 */ if ( (xrtMemPoolAlloc(&tPool, 16) == NULL) || (xrtMemPoolCalloc(&tPool, 2, 32) == NULL) || (xrtMemPoolReset(&tPool) != 2) ) { xrtMemPoolUnit(&tPool); return 8; } (void)xrtMemPoolTrim(&tPool, 0); /* 状态查询:reset 后 live 归零,峰值仍记录历史最高水位。 */ xrtMemPoolGet(&tPool, &tInfo); printf( "visited=%zu small=%zu large=%zu live=%zu peak_bytes=%zu\n", iVisited, tInfo.SmallCount, tInfo.LargeCount, tInfo.LiveCount, tInfo.PeakBytes ); xrtMemPoolUnit(&tPool); /* Create/Destroy:池对象本身放堆上的场景(跨函数传递)。 */ pCreated = xrtMemPoolCreate(0); if ( pCreated == NULL ) { return 9; } xrtMemPoolDestroy(pCreated); return 0; }
刚才发生了什么。 ① 混合大小分配:小对象命中尺寸桶(small 计数)、大对象直通(large 计数)——变长池对"大部分小 + 少数大"的真实负载最有性价比。② peak_bytes 给出内存预算的实测口径:池的水位峰值就是容量规划的输入。③ 全部释放后 live=0——池回到初始状态可复用,Unit 时整体归还。对比固定池:变长池牺牲一点槽定位的速度,换来"不必为每种大小建一个池"的管理简便——两种池的选择就看你的对象大小分布是否整齐。
池与第 5 章分配器的关系
池不是分配器的替代品,是它上面的批发层。最灵活的组合方式正是第 5 章的分配器替换:把自定义分配器实现成按大小路由到对应池,整个程序的分配行为就在一处收口——这正是内核 memory_stats 里 pooled 计数的来源(第 6 章示例的 48 字节命中尺寸类缓存,就是内核内建的小对象池在工作)。业务侧自建池则更直接:连接池、帧池、消息池各管各的等大对象。两条路线不冲突:内核池兜底通用小对象,业务池承载领域对象。
对象构造与槽的分工
池只管槽的取放,不管对象的构造与析构——从池里拿到的槽内容是未定义的(Calloc 版本除外,零初始化)。取放与构造的标准配对是:Alloc 拿槽、业务初始化构造对象;析构对象、Free 还槽。需要析构回调的批量场景用 Visit 遍历活动对象逐个收尾,再 Reset/Trim 批量回收——遍历收尾、批量回收、页管理这三步是池化对象生命周期管理的完整闭环,第 67 章连接表会用到它的完全体。
22.4契约
- 等大槽:固定池按
Init时的大小+对齐分槽;不同大小各自建池或改用变长池。 - LIFO 复用:释放的槽即时回空闲表,下次分配优先复用最近释放者。
- 安检:
Owns判指针归属;外来指针不得进池的 Free。 - 标记方向:留少删多数用 Sweep;删少留多数用 FreeMarked——按标记少的一边选。
- 页管理:
SetRetain预热空页、Trim归还、Reset全清;页归池管,业务只碰槽。 - 统计对账:池化效果用第 6 章
pooled/backing验证;水位用live/peak规划容量。
从示例到工程:池的三个宿主
池句柄的宿主位置决定它的角色。服务级常驻池:挂在服务对象上,启动时按容量规划预热(Reserve 语义的池版本是预分配首页),运行期只做槽取放——十万连接的网关,连接池就是这种形态。请求级短命池:一批处理内创建、批末 Reset 留 Retain 页、彻底结束时 Unit——配合第 7 章 arena 分工(池管对象取放、arena 管临时片段)。全进程批发层:第 5 章分配器替换接池——业务代码无感知,分配行为整体池化。三种宿主对池的配置侧重不同:常驻池重容量规划与水位监控、短命池重 Retain/Trim 节奏、批发层重大小分布统计。
常见错误
症状:空闲表被外来指针污染,后续分配返回野指针——崩溃点在完全无关的代码里,是最难追的一类事故。
原因:池的 Free 只是"把槽挂回空闲表",它不校验指针来源(校验有成本,热路径不背);混入的堆指针下次被当槽分配出去。
ptr pObject = xrtMalloc(64); /* 堆分配 */ /* ... 混乱的代码之后 ... */ xrtPoolFree(&tPool, pObject); /* 堆指针进池——空闲表被污染 */
ptr pObject = xrtMalloc(64); /* 不确定来源就先问池 */ if ( xrtPoolOwns(&tPool, pObject) ) { xrtPoolFree(&tPool, pObject); } else { xrtFree(pObject); /* 归还给正确的所有者 */ }
症状:批量处理后访问旧对象指针,读到新数据或已归还页的内容——"幽灵数据"。
原因:Reset/Sweep/Trim 都是批量回收语义,一次性作废全部(或未标记的)槽——此前发出的槽指针全部过期。
connection* pConn = xrtPoolAlloc(&tPool); /* 拿到槽 */ ProcessRequest(pConn); xrtPoolReset(&tPool); /* 批量回收:pConn 已过期 */ Reply(pConn); /* 幽灵访问 */
connection* pConn = xrtPoolAlloc(&tPool); ProcessRequest(pConn); Reply(pConn); /* 用完再回收——指针有效期止于回收点 */ xrtPoolFree(&tPool, pConn); /* 或交给标记回收在批次边界统一处理 */
编程练习
分配 10 个槽,释放第 3 个后立刻分配——断言新地址等于被释放地址;再释放 3 个连续槽,观察复用顺序(后释放的先回来);最后用 Visit 清点活动对象数应为 10-3-1+1(按你的操作序列推一遍再验证)。
用"分配处理 + Sweep 标记回收"实现一批请求:处理过程中存活请求逐个 Mark,批次结束 Sweep 一次性回收未标记者;输出回收数与 live 水位。提示:Sweep 方向适合"多数请求完成、少数挂起跨批"的形态。
写一个十万次"取连接-还连接"的模拟:先用裸 xrtMalloc/xrtFree 跑一遍、再用固定池跑一遍,两遍都用第 6 章统计记录 backing 次数与耗时。验收标准:池化版 backing 次数呈台阶(约每 256 次分配一跳)、耗时显著低于裸堆版;两张对照表随练习输出。
小结
| 知识点 | 速查 |
|---|---|
| 固定池 | 等大槽 + 分页增长(约 64KB/页)+ LIFO 空闲表复用,槽永不碎片化 |
| 复用实证 | 释放后立即分配拿回同地址——缓存还热 |
| 安检 | Owns 判归属;外来指针禁止进 Free——混入即空闲表污染 |
| 标记回收 | Sweep 留标记删其余 / FreeMarked 删标记留其余 |
| 页管理 | SetRetain 预热、Trim 归还空页、Reset 全清 |
| 变长池 | 尺寸分桶 + 大块直通;大小混杂负载的池化 |
| 对账工具 | 第 6 章 pooled/backing 台阶曲线 + live/peak 水位 |
| 与 arena 分工 | 池管"频繁取放";arena 管"一起回收";两者常在同一个服务里并肩 |
| 三宿主 | 服务常驻池 / 请求短命池 / 分配器批发层——配置侧重各不同 |
| 构造配对 | Alloc 拿槽→业务构造;析构→还槽;批量用 Visit 收尾 |
📖 本章涉及的 API 参考手册:pool(69 个条目)memory_stats(5 个条目)
点击查看完整签名、参数约束、返回值与错误说明