1 定义ngx_chain_update_chains 函数 定义在 src/core/ngx_buf.cvoidngx_chain_update_chains(ngx_pool_t*p,ngx_chain_t**free,ngx_chain_t**busy,ngx_chain_t**out,ngx_buf_tag_ttag){ngx_chain_t*cl;if(*out){if(*busyNULL){*busy*out;}else{for(cl*busy;cl-next;clcl-next){/* void */}cl-next*out;}*outNULL;}while(*busy){cl*busy;if(cl-buf-tag!tag){*busycl-next;ngx_free_chain(p,cl);continue;}if(ngx_buf_size(cl-buf)!0){break;}cl-buf-poscl-buf-start;cl-buf-lastcl-buf-start;*busycl-next;cl-next*free;*freecl;}}2 目的1 设计意图ngx_chain_update_chains的核心职责是维护ngx_output_chain_ctx_t中三条链表free、busy、out的状态转移实现ngx_chain_t链节的对象池复用和已清空缓冲区的复位回收。该函数位于 Nginx 的buf/chain 基础设施层src/core/ngx_buf.c是ngx_output_chain输出链框架的核心配套工具。它不直接参与数据的发送与接收而是在每次output_filter调用完成后被调用充当三条链表之间的调度员。3 详解1 函数签名voidngx_chain_update_chains(ngx_pool_t*p,ngx_chain_t**free,ngx_chain_t**busy,ngx_chain_t**out,ngx_buf_tag_ttag)1 返回值void返回值含义void函数无返回值所有结果通过修改外部链表头指针*free、*busy、*out来体现与大多数 Nginx 函数不同该函数不产生错误——它不分配内存、不发起 I/O唯一的副作用是链表指针的重新编排。2 函数名ngx_chain_update_chains词段含义ngx_Nginx 标准前缀chain操作对象为ngx_chain_t链表update更新而非创建或销毁——在三链表之间转移节点维护对象池状态chains复数强调该函数同时操作free、busy、out三条链表的联动更新3 参数列表参数类型含义来源约束pngx_pool_t *内存池用于ngx_free_chain归还链节到pool-chain调用者传入通常为r-pool或ctx-pool不可为 NULLfreengx_chain_t **指向空闲链表头指针的二级指针被回收的链节通过头插法放入此链ctx-free不可为 NULLbusyngx_chain_t **指向忙链表头指针的二级指针存放已交网络层但未完全消费的缓冲区ctx-busy不可为 NULLoutngx_chain_t **指向输出链表头指针的二级指针存放上一轮构造并已交output_filter处理的数据链out调用者局部变量不可为 NULL函数返回后*out必为 NULLtagngx_buf_tag_t模块标识用于区分不同模块分配的缓冲区实际上就是void *调用者传入通常为(ngx_buf_tag_t) ngx_http_xxx_module应设为非 NULL 的唯一值若为 NULL 则所有 tagNULL 的缓冲区均视为匹配2 逻辑流程ngx_chain_update_chains(p, free, busy, out, tag) ├─ [1] 输出链表迁移*out 非空 │ ├─ [1.1] 忙链表为空 │ │ └─ *busy *out → *out NULL → 进入 [2] │ └─ [1.2] 忙链表非空 │ └─ 遍历 busy 找尾部 → 追加 *out 到尾部 → *out NULL → 进入 [2] └─ [2] 忙链回收扫描遍历 *busy 头部节点 ├─ [2.1] tag 不匹配 │ └─ 从 busy 摘下 → ngx_free_chain 归还链节到 pool → 继续下一节点 ├─ [2.2] 缓冲区仍有数据ngx_buf_size ! 0 │ └─ break停止扫描此节点及之后所有节点留在 busy 中等待网络层消费 └─ [2.3] 缓冲区已清空ngx_buf_size 0且 tag 匹配 └─ 复位 bufposstart, laststart→ 从 busy 摘下 → 头插法放入 free → 继续下一节点{ngx_chain_t*cl;局部变量声明1 输出链表迁移将 *out 追加到 *busy 尾部if(*out){if(*busyNULL){*busy*out;}else{for(cl*busy;cl-next;clcl-next){/* void */}cl-next*out;}*outNULL;}进入条件*out ! NULL。即上一轮output_filter调用之前调用者构造了一条非空的输出链经output_filter处理后需要将链的所有权移交给忙链表。*out的来源以典型调用方ngx_output_chain为例src/core/ngx_output_chain.clastctx-output_filter(ctx-filter_ctx,out);if(lastNGX_ERROR||lastNGX_DONE){returnlast;}ngx_chain_update_chains(ctx-pool,ctx-free,ctx-busy,out,ctx-tag);last_outout;out是主循环中逐缓冲区累积的输出链。output_filter拿到这条链后可能只消费了头部几个缓冲区就返回如 socket 发送缓冲区已满也可能全部消费完毕。无论消费了多少剩余的链都需要由*busy持有等待网络层后续继续发送。整体语义这一段 if 是所有权转移。将*out指向的整条后继链整体移交给*busy然后将*out置 NULL——调用方不再持有对这条链的引用防止外部误操作。1.1忙链表为空*busy*out;进入条件*busy NULL忙链表中没有任何节点。处理逻辑直接将*out赋给*busyO(1) 操作。不需要遍历——因为没有已存在的尾部需要定位。典型场景ngx_output_chain首次被调用或上一轮所有忙链节点都已被回收或消费完毕。1.2忙链表非空for(cl*busy;cl-next;clcl-next){/* void */}cl-next*out;进入条件*busy ! NULL忙链表中还有未被完全消费的旧数据。处理逻辑遍历*busy找到尾部节点cl-next NULL。循环体为空——纯粹定位尾部是一个 O(n) 操作。将*out链接到尾部cl-next。这保证忙链表保持FIFO 顺序先产生的数据在头部后产生的在尾部。为什么追加到尾部而非头部网络层按顺序发送数据——先加入忙链的缓冲区先被 socket 发送。如果将*out插入头部新数据会抢在旧数据之前被发送破坏 HTTP 响应的顺序性。追加到尾部保证了发送顺序与数据产生的顺序一致。2 忙链回收扫描while(*busy){cl*busy;if(cl-buf-tag!tag){*busycl-next;ngx_free_chain(p,cl);continue;}if(ngx_buf_size(cl-buf)!0){break;}cl-buf-poscl-buf-start;cl-buf-lastcl-buf-start;*busycl-next;cl-next*free;*freecl;}进入条件无条件执行。即使*busy NULLwhile (*busy)直接跳过函数返回——这是正常路径表示忙链表为空本轮无回收任务。整体语义从忙链表头部开始逐个检查每个节点决定三种去向之一去向触发条件动作归还到pool-chaintag 不匹配摘除 →ngx_free_chain→ 继续留在 busy 中有数据未发完break停止整个扫描回收到*free已清空且 tag 匹配复位缓冲区 → 摘除 → 头插到 free → 继续为什么从头部开始扫描忙链表是 FIFO 顺序头部是最早加入、最早被发送的缓冲区因此最有可能已被完全消费。从头部开始扫描能最大化回收率。*busy指针的更新每个分支在摘下头节点后通过*busy cl-next将忙链表头前进到下一个节点同时通过外部二级指针busy将这一变更写回到调用者——因此循环的每一轮都可能改变调用者看到的*busy值。2.1tag 不匹配——归还异主链节if(cl-buf-tag!tag){*busycl-next;ngx_free_chain(p,cl);continue;}ngx_buf_tag_t就是void *。在 Nginx 中每个模块在分配ngx_buf_t时将buf-tag设为其ngx_module_t结构体的地址如(ngx_buf_tag_t) ngx_http_gzip_filter_module。ngx_module_t是全局唯一的静态变量其地址天然唯一适合做模块标识。进入条件cl-buf-tag ! tag。当前忙链表头节点的缓冲区不属于调用本函数的模块。处理逻辑*busy cl-next从忙链表头部摘下该节点。ngx_free_chain(p, cl)将ngx_chain_t链节归还到内存池的全局空闲链表。continue继续处理忙链表的下一个头节点此时*busy已前进到下一个节点。设计意图——模块隔离tag 不匹配意味着忙链表中混入了其他模块的缓冲区。如果不做 tag 检查就将这些异主缓冲区回收到本模块的*free链会导致其他模块的缓冲区被劫持原模块后续执行时会访问到已被改写的内存本模块的空闲链被不属于自己的缓冲区污染后续ngx_chain_get_free_buf返回的类型和大小不可预期。因此异主链节直接归还到全局pool-chain而非本模块的*free是最安全的选择。2.2缓冲区仍有数据——停止扫描if(ngx_buf_size(cl-buf)!0){break;}进入条件cl-buf-tag tag通过上一检查且ngx_buf_size(cl-buf) ! 0缓冲区中还有未发送的数据。处理逻辑break——遇到第一个仍有数据的缓冲区后立即终止整个 while 循环。这是本函数最关键的性能决策忙链表保持 FIFO 顺序头部最先加入最先被发送如果头部节点还有数据未发完说明网络层尚未消费完毕由于发送是顺序的头部之后的所有节点不可能已被完全消费——检查它们是纯浪费典型时序时间线 T1: ngx_output_chain 产生 buf_A, buf_B, buf_C → output_filter → busy[A,B,C] T2: socket 只发送了 buf_A 的 2000/4096 字节 → *busy 扫描到此停止 T3: socket 发完 buf_A 剩余 2096 字节 → 下一轮 ngx_chain_update_chains 回收 buf_A T4: socket 开始发 buf_B ...安全视角如果此处不break而是continue被部分消费的缓冲区在ngx_buf_size ! 0时不会被复位回收因为进不了分支 [2.3]但会浪费 CPU 遍历后续不可能已清空的节点。2.3缓冲区已清空——复位并回收cl-buf-poscl-buf-start;cl-buf-lastcl-buf-start;*busycl-next;cl-next*free;*freecl;进入条件cl-buf-tag tag且ngx_buf_size(cl-buf) 0——缓冲区数据已被网络层完全消费链节和缓冲区均可安全复用。处理逻辑逐行cl-buf-pos cl-buf-start;— 将读/发送指针复位到缓冲区起始位置。cl-buf-last cl-buf-start;— 将有效数据尾指针复位到起始位置。复位后pos last startngx_buf_size 0缓冲区处于已分配内存但无有效数据的干净状态可被下一轮ngx_output_chain通过ngx_chain_get_free_buf取出后写入新数据。*busy cl-next;— 从忙链表头部摘下本节点。cl-next *free;— 将本节点的next指向当前空闲链表的头部可能为 NULL。*free cl;— 将本节点设置为空闲链表的新头部。采用LIFO 头插法O(1) 插入。LIFO 的意义在于缓存局部性最近回收的缓冲区数据可能仍在 CPU 缓存中下一次分配时优先命中缓存。循环继续处理忙链表的下一个头节点已在第 3 步更新。设计意图——对象池模式零 malloc/free这是 Nginx 高性能内存管理的核心设计之一。ngx_chain_t链节和ngx_buf_t缓冲区通过三链表循环在请求生命周期内从不进入系统的malloc/free具体生命周期ngx_output_chain通过ngx_chain_get_free_buf(p, free)从*free取出一个链节O(1)将新数据写入其buf。装满数据的链节被放入out链经output_filter交给网络层。output_filter返回后ngx_chain_update_chains将*out移入*busy分支 [1]。网络层逐步发送数据完成后ngx_buf_size归零。下一轮ngx_chain_update_chains回收该链节本分支将其放回*free。全过程零次malloc/free仅在请求池销毁时统一释放所有内存。这避免了频繁分配释放导致的内存碎片和系统调用开销是 Nginx 能在 C10K 场景下保持高性能的关键机制之一。边界条件——*free初始为 NULL首次调用时*free为 NULL空闲链尚未建立。执行cl-next *free即cl-next NULL和*free cl后空闲链从单节点开始构建。后续每次回收不断在头部累积节点。ngx_chain_get_free_buf的分配逻辑是先尝试从*free取若为空则通过ngx_alloc_chain_link(p)从池中分配新链节——两者对调用者透明。