Nginx共享内存深度揭秘:annotated_nginx注解版ngx_slab分配器实现细节
【免费下载链接】annotated_nginxAnnotated Nginx Source(中文)项目地址: https://gitcode.com/gh_mirrors/an/annotated_nginx
annotated_nginx 是 Nginx 源码的中文注解版本,逐行为核心代码添加了通俗易懂的中文注释。本文基于该注解版源码,带你看懂Nginx 共享内存的核心——ngx_slab分配器的实现细节:它如何把一块 mmap 出来的内存切分、复用、加锁管理。
想要对照源码阅读?获取注解版源码:
git clone https://gitcode.com/gh_mirrors/an/annotated_nginx一、为什么 Nginx 需要共享内存
Nginx 是多进程模型:master 进程负责管理配置和监听,worker 进程负责处理连接,各进程地址空间相互隔离。凡是"跨进程必须一致"的状态,都不能放在某个 worker 的私有内存里:
| 功能 | 为什么需要共享内存 |
|---|---|
limit_req/limit_conn | 所有 worker 必须看到同一份限流计数 |
| upstream 被动健康检查 | 后端挂掉要被所有 worker 知晓 |
open_file_cache | 文件元数据跨 worker 复用 |
stub_status | 活跃连接数需要全局汇总 |
图:Nginx 进程模型与 ngx_cycle_t 结构关系。共享内存 zone 就挂在 ngx_cycle_t 的 conf_ctx 里,由 master 创建后传给所有 worker
而普通业务内存走的是进程私有的内存池ngx_pool(nginx/src/core/ngx_palloc.c),不可跨进程共享。共享内存的管理者,就是本文主角——slab 分配器。
二、ngx_slab 的三大核心结构
源码位置:nginx/src/core/ngx_slab.h(注解版,逐行有中文注释)。
1. ngx_slab_pool_t:共享内存池的"总管家"
它是共享内存区域的起始内容,64 位系统上约 200 字节,关键字段:
| 字段 | 含义 |
|---|---|
lock/mutex | 跨进程互斥锁(自旋 + 信号量) |
min_shift/min_size | 最小分配单位,2^3 = 8字节 |
pages | 页管理数组,一个元素管一个 4K 页 |
free | 空闲整页链表的头节点(哨兵) |
pfree | 空闲页数量 |
start/end | 真正可用内存的起止地址 |
2. ngx_slab_page_t:每个 4K 页的管理信息
仅 3 个字段、24 字节,却靠指针低 2 位复用承载了 4 种页状态:
| 页类型 | 值 | 适用场景 |
|---|---|---|
NGX_SLAB_PAGE | 0 | 整页分配(>2K 的大对象) |
NGX_SLAB_BIG | 1 | 64B < 对象 ≤ 2K |
NGX_SLAB_EXACT | 2 | 对象恰好 ≤ 64B,一个位图指针刚好管满一页 |
NGX_SLAB_SMALL | 3 | 对象 < 64B,借用页内头部空间做位图 |
3. slots 数组:9 条"规格链表"
共享内存头部紧跟一个 9 元素的数组(ngx_slab_init中初始化),分别管理8 / 16 / 32 / … / 1024 / 2048 字节规格的半满页。分配时按"最接近的 2 的幂"选槽位,这就是 ngx_slab 的best-fit(最佳适配)策略。
💡 注解版里一句话概括:分成 8/16/32…2K/4K 的多个 slot,找最合适的分配。
三、4MB 共享内存如何被初始化
入口在nginx/src/core/ngx_cycle.c的ngx_init_zone_pool():
- master 启动时先调用
ngx_slab_sizes_init()(nginx/src/core/nginx.c),按页大小算出两个关键常量:ngx_slab_max_size = 页大小 / 2,通常2K——超过它直接整页分配;ngx_slab_exact_size = 64(64 位系统),一个uintptr_t位图恰好能管理一整页。
- 对每个共享 zone 设置
sp->min_shift = 3(最小 8 字节)、sp->end(区域末尾)。 - 创建跨进程互斥锁
ngx_shmtx_create()。 - 调用
ngx_slab_init()完成布局:
| pool 结构(200B) | slots 9×24B | stats 9×32B | 页数组 24B/页 | ←对齐4K→ | 真正可用的内存页 |注解版源码算过一笔账:管理信息总消耗约 704 字节 + 每页 24 字节,4MB 共享内存的利用率约 99.3%,几乎不浪费。
四、分配流程:ngx_slab_alloc 内部细节
对外只有 4 个入口(ngx_slab.h):alloc / calloc / free及对应的_locked免锁版本。核心逻辑在ngx_slab_alloc_locked(),决策路径非常清晰:
分配 size ├─ size > 2K ───────────► ngx_slab_alloc_pages() 在空闲页链表找连续页,直接整页切走 ├─ 8B < size ≤ 2K ──────► 找到对应 slot 的半满页链表 │ ├─ < 64B (SMALL):在页内位图找空位 │ ├─ ≤ 64B (EXACT):用 page->slab 本身当 64 位位图 │ └─ > 64B (BIG):用 slab 高 32 位当位图 └─ slot 链表为空 ────────► 申请一个全新的 4K 空闲页,按规格初始化后挂入链表几个注解版点出的精妙之处:
- 指针低 2 位复用:
ngx_slab_page_prev()宏先& ~3还原真实指针,ngx_slab_page_type()取& 3判断页类型,一个字段省出一个字段的空间; - 位图即状态:SMALL 页直接拿页内存的开头当位图,EXACT 页 64 位位图恰好管 64 个 64B 块(64×64 = 4096),无一浪费;
- 页满了自动摘链:某 slot 链表上的页被分满后,会从链表摘下并打上对应类型标记,释放时再挂回去。
五、释放与页合并:ngx_slab_free
ngx_slab_free_locked()(nginx/src/core/ngx_slab.c)先校验指针必须落在start ~ end区间内(防止野指针),再按页类型反向操作:
- 把对应位图的 bit 清 0;
- 若整页全部释放,调用
ngx_slab_free_pages()归还整页; - 归还时会自动与前后相邻的空闲页合并(1.7.x 起支持),有效对抗碎片化——这是 slab 分配器能长期稳定运行的关键。
分配侧对应的是ngx_slab_alloc_pages():从空闲链表找到足够长的连续空闲段,多页时切分并把剩余部分挂回链表,page->slab高位打NGX_SLAB_PAGE_START标记 + 低位记录页数。
六、跨进程并发控制:ngx_shmtx
所有 alloc/free 都必须持有pool->mutex。它不是 pthread 锁,而是nginx/src/core/ngx_shmtx.h定义的自旋 + 信号量混合锁:
- 先做 CAS 原子写(用 PID 做锁值,保证只有持锁者能解锁,避免"锁误删");
- 抢不到锁就自旋若干轮;
- 仍失败则通过 POSIX 信号量睡眠,由解锁者唤醒——比纯自旋更省 CPU。
这也是为什么 alloc/free 各提供_locked免锁版本:模块可以一次性持锁做多次分配,减少锁开销。
七、给运维和开发者的实用建议
| 场景 | 建议 |
|---|---|
| zone 大小怎么选 | 估算:条目数 × 每条大小,再乘 1.5~2 的碎片余量,如limit_req_zone ... zone=10m |
日志出现ngx_slab_alloc() failed: no memory | zone 太小或碎片化,优先调大 zone,其次检查模块是否泄漏不释放 |
| 高频分配热路径 | 用_locked版本批量操作,减少锁竞争 |
小结
ngx_slab 分配器用极小的管理开销(约 0.7%)实现了跨进程、加锁、best-fit、自动合并的完整内存池:pool管全局、page管 4K 单元、slots管 8B~2K 九种规格、位图管页内小块。配合 annotated_nginx 中逐行中文注释的nginx/src/core/ngx_slab.c,即使是新手也能一步步跟完分配与释放的全过程。
【免费下载链接】annotated_nginxAnnotated Nginx Source(中文)项目地址: https://gitcode.com/gh_mirrors/an/annotated_nginx
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考