当前位置: 首页 > news >正文

内存池仿Nginx C++实现

本篇不是逐行剖析 Nginx 源码的学习笔记——网上这类文章已经很多。这里记录的是我读懂源码之后对应的 C 实现思路。实现之后我把它接入了 C17 的std::pmr::memory_resource作为底层内存分配源用来优化项目里的 HTTP 路由解析。这部分本篇不讲。虽然不逐行解析 Nginx 内存池源码但设计思想、以及 C 在 Coding 层面与 C 的差异会在行文中对照说明。核心内存管理概念概念部分稍显枯燥但要理解 Nginx 内存池的设计这是绕不开的前置。核心是两个Bump allocation与Arena allocator。它们解决的是同一类问题——小对象频繁分配带来的开销。前提是这批对象生命周期一致可以整体一次性释放。Arena allocator传统分配如malloc每次都向操作系统要内存频繁申请会带来内存碎片和性能损耗。Arena 分配则相反提前向系统要一块足够大的内存即 Arena之后所有小对象都在这块场地内部划分不再陷入内核。Bump allocationBump allocation 译作指针碰撞是一种靠指针移动来分配内存的技术通常配合 Arena 使用。先拿到一块大内存再用几个指针描述它的状态start记录起始位置end记录结束位置last指向当前尚未分配的位置。需要分配时不做任何复杂查找直接把last指针向后**“推”Bump**一段划出请求的大小并返回。这种方式很适合堆上频繁产生的临时对象。比如 C 里的std::string小字符串的频繁分配与释放可能会占据一部分 CPU 热点。代价也很直接无法单独释放某个对象。回收只能整体进行——析构内存后令last start达到重置效果。指针只能前向移动或整体重置没有中间状态。Nginx 内存池结构动手写 C 版本之前得先看懂 Nginx 自身的结构设计。它的内存池结构体内部嵌套了多条链表。// typedef struct ngx_pool_s ngx_pool_t in ngx_core.hstructngx_pool_s{ngx_pool_data_td;size_tmax;// 4095ngx_pool_t*current;ngx_chain_t*chain;ngx_pool_large_t*large;ngx_pool_cleanup_t*cleanup;ngx_log_t*log;};实际编写时我删掉了chain和log两个字段前者服务于 I/O buffer后者是调试日志都与内存池的核心机制无关。structngx_pool_s{ngx_pool_data_td;size_tmax;// 4095ngx_pool_t*current;ngx_pool_large_t*large;ngx_pool_cleanup_t*cleanup;};large和cleanup留到后文这里先把它们都当作普通链表看待从ngx_pool_data_t d入手。ngx_pool_data_tNginx 小内存的基本单位我称之为chunk而ngx_pool_data_t正是用来描述一个chunk的。typedefstruct{u_char*last;u_char*end;ngx_pool_t*next;ngx_uint_tfailed;}ngx_pool_data_t;举个例子。假设堆上分配了一个chunk大小为chunk_size由一个ngx_pool_data_t d来描述----------------------|已分配|未用空间|----------------------^^d-last----|||d-end----------------|d-next指向下一个chunk d.failed失败次数(详见下文代码)这就是 Arena 与 Bump 的图解通过 bumplast指针快速为小对象划出地址end标记边界、用于越界检查。这里有两个值得澄清的点。第一ngx_pool_data_t d在栈上还是堆上需要用户手动申请吗不需要用户申请交由内存池自己处理放在堆上。第二ngx_pool_data_t d与chunk是分开存储的吗这是 Nginx 设计的一个理解关键结构体本身内嵌进chunk。原本的chunk_size会在最前面取出sizeof(ngx_pool_data_t)的空间存放它自己。也就是说前sizeof(ngx_pool_data_t)字节是描述信息本身后面chunk_size - sizeof(ngx_pool_data_t)才是真正能分给对象的可用空间暂不考虑内存对齐。-------------------------||^----------------------------------------|last|end|next|failed|已分配|未用空间|----------------------------------------|^---------------------------------|内嵌式结构的好处是省了一次malloc代价则是占用了一部分chunk的可用内存。以上讨论适用于第二块及之后的chunk。首块是特殊的下一节单独说明。ngx_pool_t前面的结构代码已经表明ngx_pool_t本身就包含一个ngx_pool_data_t。沿用ngx_pool_data_t的内嵌思路Nginx 把首块chunk直接嵌进了内存池本体。// first chunk-----------------------||d.last-------------------------------------|d|max|current|...|已分配|未用空间|-------------------------------------|d.end------------------------------|所以首块与其余 chunk 的区别就在于内嵌内容不同首块内嵌的是内存池本体ngx_pool_t第二块及之后内嵌的则是更轻量的ngx_pool_data_t。更准确地说首块和其余每个 chunk连同各自内嵌的结构体的整体size是相同的但真正可用于分配的chunk_size不同因为内嵌结构体的开销不一样首块chunk_size size - sizeof(ngx_pool_t);其余chunk_size size - sizeof(ngx_pool_data_t);理清了内嵌关系再回头看ngx_pool_t的各个字段typedefstructngx_pool_s{ngx_pool_data_td;size_tmax;// 4095ngx_pool_t*current;ngx_pool_large_t*large;ngx_pool_cleanup_t*cleanup;}ngx_pool_t;d是内存池本体内嵌的那个ngx_pool_data_t它内部的next指针把后续所有 chunk 串成一条单链表——这是整个池子的骨架。max表示每个 chunk含内嵌结构体的size4095是单个 chunk 的上限。这个值同时也是小内存与大内存分配的分水岭超过max的请求会走单独的大内存路径。current指向当前有效的 chunk——所谓有效是指它内部还有足够空间分配对象。它需要配合ngx_pool_data_t里的failed一起理解当某个 chunk 的failed累计超过阈值说明它已经反复装不下新请求了current便跳过它指向下一块若后续没有可用块就触发新 chunk 的分配。这样做的意义在于分配时不必每次都从头遍历那些大概率已经填满的旧块。large用于大内存分配cleanup用于资源清理二者都是后文的主题。光看描述很懵看代码会对这些概念有更清晰的认识。C 实现我对Nginx源码进行了C的一种重写 删减了一部分 但架构几乎一样。如果你读懂下面的代码 那么读Nginx内存池源码自然水到渠成 反过来如果你读过源码且有一定C基础 这就是一份项目上能用的C翻版Nginx内存池头文件先展示代码 然后下文挑重点说。 其余靠注释自行理解 将下文代码喂给Claude code是一个好的方式。classPool:publicruntime::base::NonCopyable{public:inlinestaticconstexprstd::size_t kDefaultChunkSize112;inlinestaticconstexprstd::size_t kMaxSmallAllockDefaultChunkSize-1;inlinestaticconstexprstd::size_t kFailedThreshold12;inlinestaticconstexprstd::size_t kMinChunkSize17;structDeleter{voidoperator()(Pool*p)constnoexcept;};usingPtrstd::unique_ptrPool,Deleter;// Pool must be placement-newed at the beginning of its own arena memory,// so stack allocation and direct new are intentionally disallowed.staticPtrCreate(std::size_t chunk_sizekDefaultChunkSize);// size max_ uses the bump arena fast path.// Larger allocations bypass the arena and use the large-allocation path.void*Allocate(std::size_t size);void*AllocateAligned(std::size_t size,std::size_t align);void*AllocateUnaligned(std::size_t size);void*Callocate(std::size_t size);// Only valid for large allocations.// Small allocations are reclaimed by Reset() or Pool destruction.voidFree(void*p)noexcept;// Does not execute cleanup handlers.// Releases large allocations and rewinds all chunk bump pointers.voidReset()noexcept;// handler(data) is executed in LIFO order during Pool destruction.// Returned data memory is allocated from the arena itself.void*RegisterCleanup(void(*handler)(void*),std::size_t data_size);std::size_tChunkCount()constnoexcept;std::size_tLargeCount()constnoexcept;std::size_tByteUsed()constnoexcept;private:structChunkHeader{std::byte*last;std::byte*end;ChunkHeader*next;std::uint32_tfailed;};structLargeNode{void*alloc;LargeNode*next;};structCleanupNode{void(*handler)(void*);void*data;CleanupNode*next;};explicitPool(std::size_t chunk_size)noexcept;~Pool()default;voidDestroyArena()noexcept;void*AllocateSmall(std::size_t size,std::size_t alignment);void*AllocateLarge(std::size_t size);ChunkHeader*AllocateChunk();// reinterpret_castChunkHeader*(this) d_ChunkHeader d_;std::size_t max_;ChunkHeader*current_;LargeNode*large_;CleanupNode*cleanup_;};下面挑四个真正影响设计的点说明其余靠注释自解释。一、Create工厂 placement-newPool 住在自己的 arena 里这是整个类最反直觉、也最关键的设计。Pool的构造函数是private的唯一入口是静态的Create。原因在于Pool对象本身并不独立存在于某处它就坐落在它所管理的那块 arena 内存的开头。理解的要点是 C 申请原始字节与构造对象可视为两步。::operator new对应 C 中的malloc;placement-new构造对象。对照 Nginx首块 chunk 内嵌的是ngx_pool_t本体——C 版要复现这一点就必须先::operator new出整块 arena再用 placement-new 把Pool构造在这块内存的起始地址上。正因如此栈分配和普通new都被刻意禁止如果Pool被分配在别处它的this就不再是 arena 的起点d_.last this sizeof(Pool)这套地址推算会整个失效。二、Ptr与自定义Deleter析构路径不能交给默认行为为什么要定义删除器Deleter, 不是直接delete?因为Pool是 placement-new 出来的它的销毁就不能走delete——delete会同时调析构和::operator delete但 placement-new 的对象内存不归它管。所以这里用std::unique_ptrPool, Deleter包装Deleter里手动编排了正确的三步先DestroyArena()清理资源与后续 chunk再显式调~Pool()最后才::operator delete释放首块 arena。这套逻辑钉进Deleter使用者只需持有一个PtrRAII 自动兜底无需内存管理。三、ChunkHeader取代ngx_pool_data_t并复用this d_d_是ChunkHeader类型且它是类的第一个数据成员因此reinterpret_castChunkHeader*(this) d_成立。这让首块在遍历时可以和其余 chunk 一视同仁地当作ChunkHeader处理省去为首块单独写一套逻辑。failed字段从 Nginx 的ngx_uint_t收窄成了std::uint32_t——计数器不需要 64 位顺便压一点结构体体积。四、三类节点分离small / large / cleanup 各走各的链LargeNode和CleanupNode被拆成独立的小结构体分别串成两条链表与 bump arena 的主链彻底解耦。这对应 Nginx 里large与cleanup各自成链的设计大内存被单独Free清理回调需要按 后进先出 LIFO 触发。侵入式链表结构的一个特征是所有权平行分离。LargeNode和CleanupNode二者的生命周期语义都和只进不退的 bump 主链不同混在一起会互相掣肘。源文件源文件里Create/Deleter/ 构造函数对应的就是头文件讲过的placement-new 三步走 具体逻辑自行阅读。统计函数ChunkCount/LargeCount/ByteUsed和Reset都是直白的链表遍历与计数操作看代码即可。真正值得展开的是两条分配路径 小对象走快路径分配 读文件(大内存)走大内存路径单独mallocAllocateSmallbump fast-path 的核心void*Pool::AllocateSmall(std::size_t size,std::size_t align){for(ChunkHeader*ccurrent_;/* void */;cc-next){std::byte*alignedAlignPtr(c-last,align);if(alignedc-endstatic_caststd::size_t(c-end-aligned)size){c-lastalignedsize;returnaligned;}if(c-nextnullptr)break;}// No existing chunk has enough space. Allocate a new chunk.ChunkHeader*freshAllocateChunk();std::byte*alignedAlignPtr(fresh-last,align);void*resultaligned;fresh-lastalignedsize;// nginx-style heuristic:// increment failed counters for skipped chunks and// gradually advance current_ toward newer chunks.ChunkHeader*walkcurrent_;for(;walk-next!nullptr;walkwalk-next){if(walk-failedkFailedThreshold){current_walk-next;}}walk-nextfresh;returnresult;}快路径就是前半段的循环从current_出发对每个 chunk 先把last按align对齐再做一次边界检查——对齐后的地址不越过end、且剩余空间够size就把last向后推并返回。整个过程没有查找、没有空闲链表这正是 bump allocation 快的根源。慢路径在所有现有 chunk 都装不下时触发分配一块新 chunk从它身上划出内存。有意为之的实现选择和 Nginx 原版略有不同。Nginx 是在分配前遍历的过程中递增failed我是在新 chunk 分配完成后单独走一遍walk循环来递增沿途 chunk 的failed并在超过kFailedThreshold时把current_往后挪。语义上效果一致——某个 chunk 反复装不下就挪动current_到有效的位置后续分配从更新的块起步——只是我把计数与快路径判断拆开了快路径循环保持纯粹只管分配。AllocateLarge越过 arena 的大内存路径void*Pool::AllocateLarge(std::size_t size){void*alloc::operatornew(size);std::size_t probe0;for(LargeNode*llarge_;l!nullptr;ll-next){if(l-allocnullptr){l-allocalloc;returnalloc;}if(probekLargeSlotSearch)break;}auto*nodestatic_castLargeNode*(AllocateSmall(sizeof(LargeNode),alignof(LargeNode)));node-allocalloc;node-nextlarge_;large_node;returnalloc;}超过max_的请求直接::operator new绕过 bump arena——arena 是为小对象的密集分配设计的大块内存塞进去会浪费可用空间也破坏整体一次性释放的前提大内存需要能被Free单独回收。两个细节值得一提。其一新分配的指针不是无脑挂链先探测链表前kLargeSlotSearch个节点如果有被Free置空alloc nullptr的槽位就直接复用省一次LargeNode分配。其二LargeNode这个节点本身只有十几字节让它也从 bump arena 里划出来——管理结构借住在它所管理对象的对立路径上“能省一次分配就省一次”。至于Free它只对大内存有效遍历large_链找到匹配指针::operator delete后把槽位置空留给上面的复用逻辑。小对象不支持单独释放——这是 bump allocation 的固有代价前文已经说过。小结到这里一个翻版 Nginx 内存池的 C 实现就完整了。小对象只进不退、批量重置;大对象单独管理、单独释放。如上所说 我删减了Nginx 内存池的一部分 另外的差异是语言层面的设计思路。工厂模式设计, 全堆分配unique_ptr 自定义Deleter接管销毁路径 RAII封装 无需手动管理内存、std::byte与显式内存对齐。这种内存池适用于游戏引擎和编译器生成语法树 这些我只停留在描述上。但我可以肯定 它在HTTP路由解析和网关路由协议改写 这是非常高效的。 这也是我最初学习并优化它的原因。另外 把它接入 C17 的std::pmr::memory_resource——它能作为标准的memory_resource暴露出去std::pmr::string、std::pmr::vector这些容器就能直接以它为分配源。 C 也提供Nginx风格的分配源 感兴趣自行了解吧。参考附录 版权声明Nginx 源码 经典中经典 必看 代码简洁优雅。Apache 源码: Nginx 作者早期参考的经典 代码过长自行阅读。我的实现 头文件 觉得不错的star一下呗我的实现 源文件Arena 和 BumpC语言 Arena起源论文- By D R.HNginx内存池源码Apache内存池源码
http://www.zskr.cn/news/1376057.html

相关文章:

  • 基于CRISP-DM与HMM的国有企业内部威胁安全成熟度评估框架
  • 从安装到卸载:我在macOS Big Sur上使用雷蛇雷云2.0驱动的完整踩坑记录
  • Type-C接口水冷散热器
  • 2026照片去水印免费软件全攻略:一看就会的保姆级教程,赶紧收藏
  • 从PDB到Mol:手把手教你用PyMOL和Open Babel搞定蛋白质-小分子复合物的结构文件转换
  • 鸿蒙PC:Qt适配OpenHarmony实战【番茄刻】:工作和休息两种倒计时如何写成一个 QML 状态机
  • 手把手教你:把Ubuntu 20.04完整系统塞进U盘,打造随身便携开发环境
  • 如何快速配置Windows任务栏透明美化:TranslucentTB新手完整入门指南
  • YOLOv13涨点改进| TGRS 2026|独家创新首发、特征融合改进篇| 引入CGIM 通道组交互融合模块,增强目标关联信息的建模,助力目标检测、遥感目标检测、双时相遥感变化检测、图像融合有效涨点
  • YOLOv13涨点改进| TGRS 2026 |独家创新首发、特征融合改进篇| 引入SGAM空间高斯注意力融合模块,助力YOLOv13模型目标检测、遥感目标检测、双时相遥感变化检测、图像分割有效涨点
  • Unity商业游戏逆向解剖:天命6源码的真实结构与设计逻辑
  • 鸿蒙数学 108 篇 第十五篇:阴阳对称运算规则
  • 医学影像AI迁移学习:如何科学选择预训练数据集?
  • 猫抓:5步掌握网页资源嗅探工具,轻松下载全网视频
  • G-Helper深度解析:华硕笔记本性能调优实战手册
  • 量子生成模型:原理、优势与应用场景解析
  • RePKG深度技术解析:逆向工程驱动的Wallpaper Engine资源处理框架
  • DownKyi终极指南:5步轻松下载B站高清视频的完整解决方案
  • GitHub 汉化插件:解决英文界面困扰,3步实现全中文操作体验
  • 基于CNN的食双星参数快速预测:ebop_maven模型原理与应用
  • Java 入门实验:手把手实现 Tank 坦克类(面向对象基础实战)
  • Terraform 实战:用 for 表达式将列表元素转换为大写
  • sudo高危漏洞CVE-2023-27350原理与1.9.5p2修复实战
  • 基于Transformer的行星大气辐射传输仿真器:百倍加速与1%精度
  • 中医馆升级|结合瑞式养老模式的医养结合完整落地方案
  • topcode【随机算法题】【2026.5.24打卡-java版本】
  • 《道德经》第二十章
  • 华硕笔记本终极优化指南:如何用G-Helper轻量级工具全面提升使用体验
  • 别再折腾VMware Tools了!用FileZilla+SSH搞定Windows与Ubuntu虚拟机文件互传(保姆级教程)
  • VMware Workstation Pro 17上快速体验Rocky Linux 8.6:从镜像下载到命令行登录的5分钟极简流程