ngx_output_chain_as_is
1 定义
ngx_output_chain_as_is 函数 定义在 src/core/ngx_output_chain.cstaticngx_inlinengx_int_tngx_output_chain_as_is(ngx_output_chain_ctx_t*ctx,ngx_buf_t*buf){ngx_uint_tsendfile;if(ngx_buf_special(buf)){return1;}#if(NGX_THREADS)if(buf->in_file){buf->file->thread_handler=ctx->thread_handler;buf->file->thread_ctx=ctx->filter_ctx;}#endifsendfile=ctx->sendfile;#if(NGX_SENDFILE_LIMIT)if(buf->in_file&&buf->file_pos>=NGX_SENDFILE_LIMIT){sendfile=0;}#endif#if!(NGX_HAVE_SENDFILE_NODISKIO)/* * With DIRECTIO, disable sendfile() unless sendfile(SF_NOCACHE) * is available. */if(buf->in_file&&buf->file->directio){sendfile=0;}#endifif(!sendfile){if(!ngx_buf_in_memory(buf)){return0;}buf->in_file=0;}if(ctx->need_in_memory&&!ngx_buf_in_memory(buf)){return0;}if(ctx->need_in_temp&&(buf->memory||buf->mmap)){return0;}return1;}2 目的
1 设计意图
ngx_output_chain_as_is是ngx_output_chain引擎的"转发判定器"——
它回答一个核心问题:
某个给定的缓冲区(ngx_buf_t)是否可以"原样"交给下游output_filter,
而不需要先拷贝到临时缓冲区。
该函数属于 Nginx 输出链基础设施层(src/core/ngx_output_chain.c),
是协议无关的通用组件。
该函数本质上是一个逐层收紧的准入过滤器——
从一个"默认可转发"的假设出发,依次叠加六层约束条件。
只要任何一层判定不通过,就返回 0(不可转发),要求上层执行数据拷贝。
这种"宽进严出"的设计使得缓冲区在绝大多数常规场景下都能走零拷贝路径,
只有在必须拷贝时才付出拷贝开销。
在 Nginx 整体架构中,该函数连接了上游数据生产者与下游数据发送者:
上游模块(如静态文件、FastCGI、proxy)产出的缓冲区经过此函数判定后,
要么以原始指针形式传递给write_filter(零拷贝,适合sendfile场景),
要么被拷贝到对齐后的临时缓冲区中(适合需要修改数据或内存已满的场景)。
3 详解
1 函数签名
staticngx_inlinengx_int_tngx_output_chain_as_is(ngx_output_chain_ctx_t*ctx,ngx_buf_t*buf)1 返回值:ngx_int_t
| 返回值 | 值 | 含义 |
|---|---|---|
1 | 1 | 缓冲区可"原样转发"(as-is),无需拷贝即可直接交给下游output_filter |
0 | 0 | 缓冲区不可直接转发,需要先拷贝到临时缓冲区 |
注意:该函数本身不返回NGX_ERROR。
所有检查都是"通过/不通过"的二元判定,
错误由上层ngx_output_chain的拷贝流程(如内存分配失败)来处理。
2 函数名:ngx_output_chain_as_is
| 词段 | 含义 |
|---|---|
ngx | Nginx 源码前缀 |
output_chain | 属于输出链子系统 |
as_is | “照原样”——判定缓冲区是否可以按原始形态直接转发,不经过拷贝 |
as_is这个命名精准地传达了函数的核心语义:
不改变、不复制、照原样传递。
与ngx_output_chain_copy_buf(执行实际数据拷贝)形成互补关系。
3 参数列表
| 参数 | 类型 | 含义 | 来源 | 约束 |
|---|---|---|---|---|
ctx | ngx_output_chain_ctx_t * | 输出链上下文,维护输出通道的全部配置和状态 | 调用方(ngx_output_chain)传入 | 非 NULL;其内部的sendfile、need_in_memory、need_in_temp等字段是本函数判定的核心依据 |
buf | ngx_buf_t * | 待判定的单个缓冲区 | ctx->in->buf(内循环中)或in->buf(快速路径中) | 非 NULL;其in_file、memory、mmap、flush等标记位决定判定结果 |
2 逻辑流程
ngx_output_chain_as_is(ctx, buf) ├─ [1] 特殊缓冲区检测 │ └─ ngx_buf_special(buf) → return 1(标记型缓冲区,无条件放行) ├─ [2] 线程模式文件回调设置(编译时可选) │ └─ buf->in_file → 将 ctx 的 thread_handler/thread_ctx 挂载到 file 对象上 ├─ [3] sendfile 上限检测(编译时可选) │ └─ buf->in_file && file_pos >= NGX_SENDFILE_LIMIT → 强制禁用 sendfile ├─ [4] DirectIO 冲突检测(编译时可选) │ └─ buf->in_file && file->directio → 禁用 sendfile(SF_NOCACHE 不可用时) ├─ [5] 非 sendfile 路径的内存约束 │ ├─ !sendfile && !ngx_buf_in_memory(buf) → return 0(文件数据 + 无 sendfile = 不可转发) │ └─ !sendfile && ngx_buf_in_memory(buf) → buf->in_file = 0(清除残留文件标记) ├─ [6] need_in_memory 强制内存约束 │ └─ ctx->need_in_memory && !ngx_buf_in_memory(buf) → return 0 ├─ [7] need_in_temp 可写性约束 │ └─ ctx->need_in_temp && (buf->memory || buf->mmap) → return 0 └─ → return 1(全部检查通过,可原样转发){ngx_uint_tsendfile;局部变量声明
sendfile是一个局部拷贝。它将ctx->sendfile的值复制到栈上,
然后各编译时条件分支可能在此基础上将其清零。
使用局部变量而非直接修改ctx->sendfile的原因:
每个缓冲区的判定是独立的——对当前缓冲区禁用 sendfile 不应影响后续缓冲区的判定。
1 特殊缓冲区检测
if(ngx_buf_special(buf)){return1;}进入条件:ngx_buf_special(buf)为真。
即:这是一个纯控制信号缓冲区,不携带任何实际数据。
例如:body filter 链发出的last_buf标记缓冲区通知下游"这是最后一段数据";flush标记缓冲区要求下游立即刷新。
处理逻辑:直接返回 1。
特殊缓冲区不需要数据拷贝——它们本身就是元信号,数据量为零。
如果走拷贝流程反而会导致无谓的临时缓冲区分配。
为什么放在第一步:
特殊缓冲区虽然不常见,但判定成本极低(只检查几个位标记),
放在第一位可以在所有其他复杂判定之前快速放行
2 线程模式文件回调设置
#if(NGX_THREADS)if(buf->in_file){buf->file->thread_handler=ctx->thread_handler;buf->file->thread_ctx=ctx->filter_ctx;}#endif进入条件(编译时):NGX_THREADS宏已定义(configure 阶段配置了线程池支持)。
进入条件(运行时):buf->in_file == 1(缓冲区引用了一个文件)。
处理逻辑:
将输出链上下文中的线程处理回调函数指针和上下文指针写入到ngx_file_t结构体中:
buf->file->thread_handler = ctx->thread_handler:
设置文件读取使用的线程处理函数buf->file->thread_ctx = ctx->filter_ctx:
设置线程处理函数的上下文参数
3 sendfile 上限检测
sendfile=ctx->sendfile;#if(NGX_SENDFILE_LIMIT)if(buf->in_file&&buf->file_pos>=NGX_SENDFILE_LIMIT){sendfile=0;}#endif进入条件(编译时):NGX_SENDFILE_LIMIT宏已定义且非零。
该宏仅在一种真实场景下被定义
条件:NGX_HAVE_SENDFILE64未定义时——
即 Linux 内核或 glibc 版本不支持 sendfile64()(64 位偏移版本),
只能使用传统的 32 位 sendfile() 系统调用。
NGX_HAVE_SENDFILE64由 configure 脚本自动检测:
编译测试程序是否能在包含 <sys/sendfile.h> 后成功调用 sendfile64()。
现代 Linux 系统(2.6+ 内核、较新 glibc)通常都支持 sendfile64(),
因此绝大多数生产环境不会定义NGX_SENDFILE_LIMIT。
在 Linux 平台上,
sendfile()系统调用使用 32 位off_t参数
(即使内核支持 64 位偏移,g
libc 的sendfile封装仍使用off_t的 32 位版本作为传输偏移参数),
因此当文件偏移超过 2GB(0x80000000)时,sendfile()将无法正确定位。NGX_SENDFILE_LIMIT标记了这个上限。
进入条件(运行时):
buf->in_file == 1且buf->file_pos >= NGX_SENDFILE_LIMIT(文件读取偏移已达到或超过 2GB 限制)。
处理逻辑:将局部变量
sendfile强制清零。这意味着即使ctx->sendfile为 1(全局启用了 sendfile),对于这个特定缓冲区也必须走"先拷贝到内存再发送"的路径而非 sendfile 零拷贝。
ctx->sendfile字段(src/core/ngx_buf.h中ngx_output_chain_ctx_s):
unsignedsendfile:1;1 位位域,标识输出链是否启用了 sendfile 优化。
该值由上层调用者在初始化ngx_output_chain_ctx_t时设置
为什么用局部变量而非直接修改ctx->sendfile:ctx->sendfile是全局开关,对后续其他缓冲区也有效。
当前缓冲区只是恰好偏移超过了 sendfile 限制,不代表后续缓冲区也不能使用 sendfile。
局部覆盖保证了判定的逐缓冲区独立性。
安全考量:如果不做此检查,在文件偏移 > 2GB 时调用sendfile()
会导致截断的偏移值、数据错位甚至静默发送错误内容。
这是一个必须在进入内核之前拦截的条件。
4 DirectIO 冲突检测
#if!(NGX_HAVE_SENDFILE_NODISKIO)/* * With DIRECTIO, disable sendfile() unless sendfile(SF_NOCACHE) * is available. */if(buf->in_file&&buf->file->directio){sendfile=0;}#endif进入条件(编译时):NGX_HAVE_SENDFILE_NODISKIO宏未定义。
该宏仅在 FreeBSD 上可用,
表示 FreeBSD 的sendfile()支持SF_NOCACHE标志——
在发送文件数据后不将其保留在系统页缓存中。
进入条件(运行时):buf->in_file == 1
且buf->file->directio == 1(文件以 DirectIO 模式打开,
即绕过操作系统页缓存直接从磁盘读取)。
处理逻辑:将局部变量sendfile清零,强制禁用 sendfile。
ngx_file_t的directio字段(src/core/ngx_file.h):
unsigneddirectio:1;1 位位域,标识文件是否以 DirectIO(O_DIRECT)模式打开。
设计意图:DirectIO 与 sendfile 存在语义冲突。
DirectIO 的设计目标是将数据直接从磁盘读到用户空间,绕过页缓存。
而sendfile()的工作方式是将数据从页缓存直接发送到 socket。
如果文件以 DirectIO 打开但数据尚未在页缓存中(恰好被 bypass 了),sendfile()将无法获取数据。
在 FreeBSD 上,SF_NOCACHE标志允许sendfile()在 DirectIO 模式下正常工作
(先读入临时缓冲区再发送,发送后丢弃缓存)。
但在其他平台上缺少这个能力时,最安全的做法是直接禁用 sendfile,
走"先读取到内存 → 再发送"的两步流程。
安全考量:如果不做此检查,在 DirectIO 文件上尝试 sendfile 可能导致:
数据发送不完整(部分数据不在页缓存中)、静默发送错误内容(页缓存中有过期数据)、
甚至 socket 挂起(等待页缓存填充但 DirectIO 永远不填充页缓存)。
5 非 sendfile 路径的内存约束
if(!sendfile){if(!ngx_buf_in_memory(buf)){return0;}buf->in_file=0;}进入条件:经过上述所有检查后,局部变量sendfile == 0。可能的原因包括:
ctx->sendfile原始值为 0(全局禁用 sendfile)- 文件偏移超过 sendfile 限制(分支 [3])
- DirectIO 冲突(分支 [4])
处理逻辑——分两步:
第 1 步:内存检查
如果缓冲区不含内存数据(!ngx_buf_in_memory(buf),
即既不是temporary/memory/mmap,也不是in_file已经为 0 的情况),
则返回 0(不可转发)。
这里的逻辑需要结合上下文理解:
当sendfile被禁用时,文件数据无法通过sendfile()零拷贝发送。
而纯文件缓冲区(in_file == 1、数据在磁盘上、尚未读入内存)的数据不在内存中,
下游的write_filter无法直接发送。因此必须返回 0,
让ngx_output_chain走拷贝流程——
在ngx_output_chain_copy_buf中将文件数据先读到临时内存缓冲区,再发送。
第 2 步:清除文件标记
buf->in_file = 0:
当缓冲区含内存数据(通过第 1 步检查)但sendfile被禁用时,清除in_file标记。
这是一种标记规范化——既然不能走 sendfile 路径,
文件偏移信息(file_pos/file_last)对下游已无意义。
将in_file清零可以避免下游函数(如write_filter)被残留的文件标记误导。
6 need_in_memory 强制内存约束
if(ctx->need_in_memory&&!ngx_buf_in_memory(buf)){return0;}进入条件:ctx->need_in_memory == 1(输出链上下文要求数据必须在内存中)。
ctx->need_in_memory字段(src/core/ngx_buf.h中ngx_output_chain_ctx_s):
unsignedneed_in_memory:1;1 位位域。当为 1 时,表示下游要求数据必须在内存缓冲区中(而非文件引用)。
此约束比sendfile禁用更强——即使 sendfile 可用,如果need_in_memory为真,
文件型缓冲区也必须先拷贝到内存。
为什么need_in_memory优先级高于 sendfile:
sendfile 的启用/禁用影响的是"数据如何到达 socket",
而need_in_memory影响的是"数据在哪个媒介上"。
某些下游模块(如 SSL 模块需要对数据进行加密)必须在内存中操作数据,
sendfile 在此场景下不可行。因此即使 sendfile 全局启用、偏移未超限,
只要need_in_memory为真,文件缓冲区就必须走拷贝流程。
注意此检查的位置:它位于 sendfile 相关检查([3][4][5])之后。
如果 sendfile 未禁用且need_in_memory为真,本检查仍然会触发返回 0。
这是因为need_in_memory和sendfile是正交的约束维度——
前者约束数据媒介,后者约束传输方式。
7 need_in_temp 可写性约束
if(ctx->need_in_temp&&(buf->memory||buf->mmap)){return0;}进入条件:ctx->need_in_temp == 1(输出链上下文要求数据在可写临时缓冲区中)。
ctx->need_in_temp字段(src/core/ngx_buf.h中ngx_output_chain_ctx_s):
unsignedneed_in_temp:1;1 位位域。当为 1 时,表示下游要求数据必须在可写(temporary)缓冲区中,
而不是只读的memory或mmap缓冲区。
ngx_buf_t的三种内存类型对比:
| 字段 | 含义 | 可写性 | 来源示例 |
|---|---|---|---|
temporary:1 | 堆分配的临时缓冲区 | 可读可写 | ngx_create_temp_buf()分配的结果 |
memory:1 | 静态/只读内存 | 只读,不可修改 | 硬编码字符串、只读共享内存段 |
mmap:1 | 内存映射文件 | 只读,不可修改 | ngx_http_static_module对静态文件的 mmap |
处理逻辑:
如果缓冲区的类型是memory或mmap(只读)且上下文要求数据可写,返回 0。
这两种缓冲区的内容不可修改,
如果下游需要对数据做变换(如 SSL 加密、chunked 编码包装),
直接转发只读缓冲区会导致段错误或数据损坏。
为什么temporary不触发此检查:temporary缓冲区是可读可写的堆内存,完全可以被下游安全地修改。
因此只检查不可写的memory和mmap。
设计意图:need_in_temp和need_in_memory共同构成一对"数据媒介约束":
need_in_memory要求数据在内存中(排除纯文件引用)need_in_temp要求数据在可写内存中(排除只读内存引用)
最后一行:
return1;当缓冲区成功通过了全部六层检查,返回 1——
可以"原样转发"。这是最常见的路径:
对于典型的内存临时缓冲区(temporary == 1),
既不涉及文件、也没有只读约束,几乎是无条件通过。
架构视角:为什么判定逻辑要独立成一个函数
将"是否可转发"的判定提取为独立函数,而非内联在ngx_output_chain中,
体现了以下设计考量:
复用:避免代码重复
职责单一:
ngx_output_chain负责"调度"(积压 → 判定 → 拷贝 → 发送),ngx_output_chain_as_is只负责"判定"。
两者解耦后,修改判定逻辑不需要理解整个输出链的控制流
