news 2026/7/22 5:01:14

ngx_output_chain_to_iovec

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ngx_output_chain_to_iovec

1 定义

ngx_output_chain_to_iovec 函数 定义在 ./nginx-1.24.0/src/os/unix/ngx_writev_chain.c
ngx_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

词段含义
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个元素)或动态分配的内存非 NULL;vec->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非 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
循环的迭代步进表达式。每轮处理完当前节点后,移动到链表的下一个节点。
注意:当continuebreak被触发时,步进表达式仍然执行(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_chainwritev之后通过ngx_chain_update_sent推进in链。
特殊缓冲区最终会传递到下游 filter(如ngx_http_write_filter)处理其控制语义。
如果在此处丢弃,控制信号会丢失。


3 文件缓冲区阻断

if(in->buf->in_file){break;}

进入条件
in->buf->in_file == 1(缓冲区数据在文件中,不在内存中)。

处理逻辑
break跳出循环,停止转换。
vec->countvec->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

处理逻辑

  1. 记录一条NGX_LOG_ALERT级别的详细日志,方便排查是哪个模块产出了非法缓冲区
  2. ngx_debug_point():在调试配置下触发断点或异常
  3. 返回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;}

处理逻辑

  1. in->buf->last - in->buf->pos
    当前缓冲区的有效数据长度(已写入但尚未发送的字节数)
  2. 如果该长度超过本轮剩余限额(limit - total),则将size截断到剩余限额

截断语义
截断后当前缓冲区的部分数据被装入 iovec,
剩余部分(pos + sizelast
留待下一轮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->posiov == 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;

处理逻辑

  1. vec->iovs数组中下标为n的槽位,n自增(后置++确保iov指向新分配的槽位)
  2. 设置iov_base为缓冲区的读指针posvoid *转换是 POSIXstruct iovec的标准要求)
  3. 设置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->sizevstotalvec->size即将调用writev的预期总发送量ngx_writev用它来对比writev的实际返回值,判断是否全部发送完毕(send - prev_send != sent)。



版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/22 5:01:09

影刀RPA 系统信息获取:时间、日期、电脑名、用户名

影刀RPA 系统信息获取&#xff1a;时间、日期、电脑名、用户名 作者&#xff1a;林焱 RPA流程中经常需要获取系统信息——用当前时间生成文件名、判断是否工作日再执行、获取电脑名区分不同运行环境、获取用户名定位用户目录。这些信息虽然简单&#xff0c;但用对了能让流程更…

作者头像 李华
网站建设 2026/7/22 5:00:37

WAIC上海活动前瞻:大模型、AI芯片与自动驾驶技术趋势解析

这次我们来看WAIC&#xff08;世界人工智能大会&#xff09;上海活动的规格升级和预期热点。作为全球AI领域的重要盛会&#xff0c;WAIC每年都会带来前沿技术展示和产业风向标&#xff0c;而今年的上海活动在规格、参展方、技术演示和本地化服务方面都有明显提升。从核心亮点来…

作者头像 李华
网站建设 2026/7/22 4:58:25

RabbitMQ recovery.dets文件损坏问题分析与修复

1. 问题现象与背景分析 当RabbitMQ服务启动时遇到"not_a_dets_file"错误&#xff0c;通常会在日志中看到类似如下的报错信息&#xff1a; CRASH REPORT exception exit: {{badmatch, {error, {not_a_dets_file,"/var/lib/rabbitmq/mnesia/rabbitSfabrici-Dem…

作者头像 李华
网站建设 2026/7/22 4:56:03

2026杭州GEO服务商实力对比:十方境生GEO为何成为本地企业优选

发布日期&#xff1a;2026年7月21日 2026年&#xff0c;生成式AI搜索已成为杭州企业获取客户的核心渠道。据行业数据&#xff0c;豆包月活3.45亿&#xff0c;DeepSeek月活1.27亿&#xff0c;AI搜索流量占比已突破52%。杭州作为国内GEO服务商最集中的城市之一&#xff0c;余杭未…

作者头像 李华
网站建设 2026/7/22 4:53:34

Linux文件搜索太慢?试试FSearch:3分钟让你告别等待!

Linux文件搜索太慢&#xff1f;试试FSearch&#xff1a;3分钟让你告别等待&#xff01; 【免费下载链接】fsearch A fast file search utility for Unix-like systems based on GTK3 项目地址: https://gitcode.com/gh_mirrors/fs/fsearch 还在为Linux系统中查找文件而烦…

作者头像 李华
网站建设 2026/7/22 4:53:03

YOLOv8在有机果蔬智能检测中的优化与应用

1. 项目概述&#xff1a;基于YOLOv8的有机果蔬智能检测系统这个开源项目提供了一个完整的有机水果蔬菜检测解决方案&#xff0c;从数据集准备到模型训练再到Web端展示的全流程实现。系统采用当前最先进的YOLOv8目标检测算法&#xff0c;并针对果蔬检测场景进行了70项改进优化&a…

作者头像 李华