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;iov=NULL;prev=NULL;total=0;n=0;for(/* void */;in&&total<limit;in=in->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;}size=in->buf->last-in->buf->pos;if(size>limit-total){size=limit-total;}if(prev==in->buf->pos){iov->iov_len+=size;}else{if(n==vec->nalloc){break;}iov=&vec->iovs[n++];iov->iov_base=(void*)in->buf->pos;iov->iov_len=size;}prev=in->buf->pos+size;total+=size;}vec->count=n;vec->size=total;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槽位耗尽的后续节点、文件缓冲区节点、或全部转换完毕后的NULL |
NGX_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
| 词段 | 含义 |
|---|---|
ngx | Nginx 源码前缀 |
output_chain | 操作对象是"输出缓冲区链",表明该函数属于输出管道中的一环 |
to | 转换方向 |
iovec | 目标格式是 POSIXstruct iovec,供writev/readv系统调用使用 |
3 参数列表
| 参数 | 类型 | 含义 | 来源 | 约束 |
|---|---|---|---|---|
vec | ngx_iovec_t * | 输入/输出参数:预先分配好iovs数组的容器,函数填充iovs元素、count段数、size总字节数 | 调用方ngx_writev_chain在栈上分配,vec.iovs指向预分配的iovs[NGX_IOVS_PREALLOCATE](64个元素)或动态分配的内存 | 非 NULL;vec->iovs必须指向有效内存;vec->nalloc必须是iovs数组的容量 |
in | ngx_chain_t * | 待转换的 Nginx 缓冲区链表头节点 | 由调用方ngx_writev_chain维护,指向本轮尚未发送的缓冲区 | 可为 NULL(表示无数据);链上每个节点的buf必须有效 |
limit | size_t | 本轮允许累积的最大字节数 | ngx_writev_chain传入limit - send(总限额减去已累积量) | 由调用方保证limit > 0;若total >= limit则循环终止 |
log | ngx_log_t * | Nginx 日志对象,用于错误日志输出 | 连接对象的日志上下文c->log | 非 NULL |
2 逻辑流程
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 为真 → break(writev 只能处理内存地址,文件数据需由 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;iov=NULL;prev=NULL;total=0;n=0;}局部变量声明
1 循环遍历缓冲区链
for(/* void */;in&&total<limit;in=in->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 计算有效长度
size=in->buf->last-in->buf->pos;if(size>limit-total){size=limit-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(prev==in->buf->pos){iov->iov_len+=size;进入条件:
当前缓冲区的起始地址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(n==vec->nalloc){break;}iov=&vec->iovs[n++];iov->iov_base=(void*)in->buf->pos;iov->iov_len=size;}5.3.A 槽位耗尽
if(n==vec->nalloc){break;}进入条件:已使用的 iovec 段数n达到vec->nalloc预分配容量。
NGX_IOVS_PREALLOCATE宏定义(src/os/unix/ngx_os.h):
#if(IOV_MAX>64)#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而不是return:
break 后执行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_len
iov=&vec->iovs[n++];iov->iov_base=(void*)in->buf->pos;iov->iov_len=size;处理逻辑:
- 取
vec->iovs数组中下标为n的槽位,n自增(后置++确保iov指向新分配的槽位) - 设置
iov_base为缓冲区的读指针pos(void *转换是 POSIXstruct iovec的标准要求) - 设置
iov_len为 5.1 计算的有效长度size
struct iovec的语义(POSIX 标准):
structiovec{void*iov_base;/* 起始地址 */size_tiov_len;/* 字节数 */};writev(fd, iovs, count)从iovs[0].iov_base开始,依次发送iov_len字节,
直到iovs[count-1]。各段可以是不连续的,
内核负责将不连续的内存区域聚合为连续的网络数据流。
prev=in->buf->pos+size;total+=size;}vec->count=n;vec->size=total;returnin;循环收尾:
每轮循环结束后更新prev(当前缓冲区的逻辑结束地址)和total(累计字节数)。
函数收尾:
循环退出后(无论正常结束、limit截断、文件阻断、还是槽位耗尽),
设置vec->count(有效 iovec 段数)和vec->size(总字节数),
返回in指向第一个未处理的节点(或NULL)。
vec->sizevstotal:vec->size是即将调用writev的预期总发送量。ngx_writev用它来对比writev的实际返回值,判断是否全部发送完毕(send - prev_send != sent)。