ngx_output_chain_to_iovec

ngx_output_chain_to_iovec 1 定义ngx_output_chain_to_iovec 函数 定义在 ./nginx-1.24.0/src/os/unix/ngx_writev_chain.cngx_chain_t*ngx_output_chain_to_iovec(ngx_iovec_t*vec,ngx_chain_t*in,size_tlimit,ngx_log_t*log){size_ttotal,size;u_char*prev;ngx_uint_tn;structiovec*iov;iovNULL;prevNULL;total0;n0;for(/* void */;intotallimit;inin-next){if(ngx_buf_special(in-buf)){continue;}if(in-buf-in_file){break;}if(!ngx_buf_in_memory(in-buf)){ngx_log_error(NGX_LOG_ALERT,log,0,bad buf in output chain t:%d r:%d f:%d %p %p-%p %p %O-%O,in-buf-temporary,in-buf-recycled,in-buf-in_file,in-buf-start,in-buf-pos,in-buf-last,in-buf-file,in-buf-file_pos,in-buf-file_last);ngx_debug_point();returnNGX_CHAIN_ERROR;}sizein-buf-last-in-buf-pos;if(sizelimit-total){sizelimit-total;}if(previn-buf-pos){iov-iov_lensize;}else{if(nvec-nalloc){break;}iovvec-iovs[n];iov-iov_base(void*)in-buf-pos;iov-iov_lensize;}previn-buf-possize;totalsize;}vec-countn;vec-sizetotal;returnin;}2 目的1 设计意图ngx_output_chain_to_iovec是 Nginx 输出链到 POSIXwritev()系统调用的桥梁函数负责将 Nginx 内部的ngx_chain_t缓冲区链表转换为 POSIX 标准的struct iovec数组并在转换过程中合并物理上相邻的内存缓冲区以减少writev()的系统调用段数。该函数属于 Nginx 事件驱动架构中的平台抽象层src/os/unix/是ngx_writev_chain函数的核心辅助函数。其上游是 Nginx 的缓冲区链ngx_chain_t由各种 body filter 或 content handler 产生下游是ngx_writev该函数基于填充好的ngx_iovec_t调用 POSIXwritev()系统调用将数据批量写入 socket。在整体架构中的位置上游ngx_writev_chain将本轮待发送的缓冲区链表in和剩余可发送字节数limit - send传入本函数。in链中的缓冲区已经过上游 body filter 和ngx_output_chain的处理是准备就绪的内存数据。本函数遍历缓冲区链跳过控制标记缓冲区flush/sync/last_buf拒绝文件缓冲区writev无法处理验证内存缓冲区的合法性按limit截断超出部分并在转换过程中合并物理地址连续的相邻缓冲区——这是本函数最核心的性能优化将多个连续的小缓冲区合并为单个iovec段直接减少了writev()系统调用的 iovec 数组长度。下游ngx_writev接收填充好的ngx_iovec_t内含iovs数组、count段数、size总字节数直接调用writev(c-fd, vec-iovs, vec-count)完成批量写操作。这种缓冲区链 → iovec 数组 → writev 系统调用的三段式设计体现了 Nginx 对操作系统原语的高效利用writev允许一次系统调用发送多段不连续的内存区域避免了多次write()调用的开销而本函数的相邻合并进一步让writev的系统调用参数更紧凑。3 详解1 函数签名ngx_chain_t*ngx_output_chain_to_iovec(ngx_iovec_t*vec,ngx_chain_t*in,size_tlimit,ngx_log_t*log)1 返回值ngx_chain_t *返回值含义in非 NULL正常返回指向链表中第一个未被转换的节点。可能是limit耗尽后的剩余节点、nalloc槽位耗尽的后续节点、文件缓冲区节点、或全部转换完毕后的NULLNGX_CHAIN_ERROR错误返回发现既非内存也非文件也非特殊的非法缓冲区(ngx_chain_t *) -1值为0xFFFFFFFFFFFFFFFF返回值对调用方ngx_writev_chain的影响返回NGX_CHAIN_ERROR→ 调用方立即向上传递错误终止当前连接的写操作返回NULL→ 所有数据已全部装入 iovec调用方执行writev后结束循环返回非 NULL → 调用方检查该节点的buf-in_file标志若为文件缓冲区则报错否则执行writev之后通过ngx_chain_update_sent推进到剩余节点继续下一轮循环2 函数名ngx_output_chain_to_iovec词段含义ngxNginx 源码前缀output_chain操作对象是输出缓冲区链表明该函数属于输出管道中的一环to转换方向iovec目标格式是 POSIXstruct iovec供writev/readv系统调用使用3 参数列表参数类型含义来源约束vecngx_iovec_t *输入/输出参数预先分配好iovs数组的容器函数填充iovs元素、count段数、size总字节数调用方ngx_writev_chain在栈上分配vec.iovs指向预分配的iovs[NGX_IOVS_PREALLOCATE]64个元素或动态分配的内存非 NULLvec-iovs必须指向有效内存vec-nalloc必须是iovs数组的容量inngx_chain_t *待转换的 Nginx 缓冲区链表头节点由调用方ngx_writev_chain维护指向本轮尚未发送的缓冲区可为 NULL表示无数据链上每个节点的buf必须有效limitsize_t本轮允许累积的最大字节数ngx_writev_chain传入limit - send总限额减去已累积量由调用方保证limit 0若total limit则循环终止logngx_log_t *Nginx 日志对象用于错误日志输出连接对象的日志上下文c-log非 NULL2 逻辑流程ngx_output_chain_to_iovec(vec, in, limit, log) ├─ [1] 循环遍历缓冲区链 │ └─ 条件in ! NULL 且 total limit → 推进 in in-next │ 条件不满足 → 跳过循环体设置 vec-count/size返回 in ├─ [2] 特殊控制缓冲区跳过 │ └─ ngx_buf_special(in-buf) 为真 → continue不消耗 iovec 槽位不计入 total ├─ [3] 文件缓冲区阻断 │ └─ in-buf-in_file 为真 → breakwritev 只能处理内存地址文件数据需由 sendfile 处理 ├─ [4] 非法缓冲区检测 │ └─ !ngx_buf_in_memory(in-buf) 为真 → 记录 ALERT 日志 ngx_debug_point() 返回 NGX_CHAIN_ERROR └─ [5] 内存缓冲区 → iovec 转换 ├─ [5.1] 计算有效长度 → size min(in-buf-last - in-buf-pos, limit - total) ├─ [5.2] 相邻缓冲区合并 │ └─ prev in-buf-pos 为真 → 仅将 size 累加到当前 iov-iov_len不新增段 └─ [5.3] 新 iovec 段分配 ├─ [5.3.A] 槽位耗尽 → n vec-nalloc → break无法继续装填返回剩余链 └─ [5.3.B] 设置 iov_base/iov_len → n记录新的段起始地址和长度{size_ttotal,size;u_char*prev;ngx_uint_tn;structiovec*iov;iovNULL;prevNULL;total0;n0;}局部变量声明1 循环遍历缓冲区链for(/* void */;intotallimit;inin-next){进入条件in ! NULL还有缓冲区未处理且total limit未达到本轮发送上限。循环出口全部节点处理完毕in NULL或达到limit上限total limit。无论哪种出口都执行vec-count n; vec-size total; return in;——返回的in指向第一个尚未处理的节点或NULL。in in-next循环的迭代步进表达式。每轮处理完当前节点后移动到链表的下一个节点。注意当continue或break被触发时步进表达式仍然执行for循环的语义因此跳过/阻断当前节点后下一轮循环自动处理in-next。2 特殊控制缓冲区跳过if(ngx_buf_special(in-buf)){continue;}进入条件ngx_buf_special(in-buf)返回真。处理逻辑continue跳过当前节点不将其数据装入 iovec。这类缓冲区是纯控制信号——例如flush标记告诉下游需要刷新发送缓冲区last_buf标记这是最后一块数据sync标记用于同步点。它们不携带实际数据因此不应占据 iovec 槽位也不应计入total。为什么即使不装入 iovec 也要返回给调用方调用方ngx_writev_chain在writev之后通过ngx_chain_update_sent推进in链。特殊缓冲区最终会传递到下游 filter如ngx_http_write_filter处理其控制语义。如果在此处丢弃控制信号会丢失。3 文件缓冲区阻断if(in-buf-in_file){break;}进入条件in-buf-in_file 1缓冲区数据在文件中不在内存中。处理逻辑break跳出循环停止转换。vec-count和vec-size记录的是截止此文件节点之前已装填的内存缓冲区。返回的in指向该文件节点。设计意图writev()只能处理内存地址iov_base指向进程虚拟地址空间无法处理文件描述符。文件数据的发送走的是sendfile路径零拷贝不属于writev的职责范围。4 非法缓冲区检测if(!ngx_buf_in_memory(in-buf)){ngx_log_error(NGX_LOG_ALERT,log,0,bad buf in output chain t:%d r:%d f:%d %p %p-%p %p %O-%O,in-buf-temporary,in-buf-recycled,in-buf-in_file,in-buf-start,in-buf-pos,in-buf-last,in-buf-file,in-buf-file_pos,in-buf-file_last);ngx_debug_point();returnNGX_CHAIN_ERROR;}进入条件!ngx_buf_in_memory(in-buf)为真。到达此分支意味着缓冲区既不是特殊控制信号未被ngx_buf_special捕获也不是文件数据未被in_file阻断也不是内存数据temporary/memory/mmap全为 0。这是数据损坏或上游模块 bug。处理逻辑记录一条NGX_LOG_ALERT级别的详细日志方便排查是哪个模块产出了非法缓冲区ngx_debug_point()在调试配置下触发断点或异常返回NGX_CHAIN_ERROR通知调用方终止当前操作安全考量如果不清查出非法缓冲区而继续执行in-buf-last - in-buf-pos的计算结果没有意义pos/last字段对非内存缓冲区无定义可能导致size为一个巨大的随机值进而导致iov_len溢出或内存越界访问。5 内存缓冲区 → iovec 转换当缓冲区确认是合法的内存缓冲区后进入核心转换逻辑5.1 计算有效长度sizein-buf-last-in-buf-pos;if(sizelimit-total){sizelimit-total;}处理逻辑in-buf-last - in-buf-pos当前缓冲区的有效数据长度已写入但尚未发送的字节数如果该长度超过本轮剩余限额limit - total则将size截断到剩余限额截断语义截断后当前缓冲区的部分数据被装入 iovec剩余部分pos size到last留待下一轮ngx_writev_chain循环再次传入本函数处理。ngx_chain_update_sent会更新buf-pos指针下一轮调用时last - pos自然就是剩余数据长度。5.2 相邻缓冲区合并if(previn-buf-pos){iov-iov_lensize;进入条件当前缓冲区的起始地址in-buf-pos等于上一个缓冲区的结束地址prev。这意味着两个缓冲区在物理内存中是连续的。处理逻辑不分配新的 iovec 段直接将size累加到上一个段的iov-iov_len中。这是一种轻量级的缓冲区聚合——writev最终看到的 iovec 段数减少了系统调用内部的处理也更高效。prev的计算在每次装填完成后更新为in-buf-pos size即当前缓冲区本次装填数据后的逻辑结束地址。典型触发场景Nginx 的ngx_output_chain使用固定大小的临时缓冲区如 32KB拷贝数据时连续分配的缓冲区可能在内存池中恰好相邻。或者在发送静态文件的 HTTP 响应头 响应体时如果头部和首个数据块在同一内存页上相邻就可以合并。边界分析即使prev in-buf-pos但iov NULL循环第一轮也不会出问题——因为prev初始化为NULL而in-buf-pos在正常情况下不会是NULL两者不相等所以一定走 else 分支。5.3 新 iovec 段分配}else{if(nvec-nalloc){break;}iovvec-iovs[n];iov-iov_base(void*)in-buf-pos;iov-iov_lensize;}5.3.A 槽位耗尽if(nvec-nalloc){break;}进入条件已使用的 iovec 段数n达到vec-nalloc预分配容量。NGX_IOVS_PREALLOCATE宏定义src/os/unix/ngx_os.h#if(IOV_MAX64)#defineNGX_IOVS_PREALLOCATE64#else#defineNGX_IOVS_PREALLOCATEIOV_MAX#endif如果系统IOV_MAX 64预分配 64 个 iovec 槽位覆盖大多数场景避免栈上占用过多空间如果系统IOV_MAX≤ 64使用系统的实际上限如某些嵌入式系统的IOV_MAX可能只有 16处理逻辑break跳出循环不再装填更多缓冲区。已装填的部分照常通过vec-count/vec-size返回。writev先发送已装填的数据下一轮循环继续处理剩余节点。为什么用break而不是returnbreak 后执行vec-count n; vec-size total; return in;返回的in指向未装填的第一个节点。调用方ngx_writev_chain执行writev后会通过ngx_chain_update_sent推进in下一轮循环继续将剩余节点的数据装入新的 iovec——这确保了即使缓冲区链很长也能分批发完。5.3.B 设置 iov_base/iov_leniovvec-iovs[n];iov-iov_base(void*)in-buf-pos;iov-iov_lensize;处理逻辑取vec-iovs数组中下标为n的槽位n自增后置确保iov指向新分配的槽位设置iov_base为缓冲区的读指针posvoid *转换是 POSIXstruct iovec的标准要求设置iov_len为 5.1 计算的有效长度sizestruct iovec的语义POSIX 标准structiovec{void*iov_base;/* 起始地址 */size_tiov_len;/* 字节数 */};writev(fd, iovs, count)从iovs[0].iov_base开始依次发送iov_len字节直到iovs[count-1]。各段可以是不连续的内核负责将不连续的内存区域聚合为连续的网络数据流。previn-buf-possize;totalsize;}vec-countn;vec-sizetotal;returnin;循环收尾每轮循环结束后更新prev当前缓冲区的逻辑结束地址和total累计字节数。函数收尾循环退出后无论正常结束、limit截断、文件阻断、还是槽位耗尽设置vec-count有效 iovec 段数和vec-size总字节数返回in指向第一个未处理的节点或NULL。vec-sizevstotalvec-size是即将调用writev的预期总发送量。ngx_writev用它来对比writev的实际返回值判断是否全部发送完毕send - prev_send ! sent。