1 定义
ngx_writev 函数 定义在 src/os/unix/ngx_writev_chain.cssize_tngx_writev(ngx_connection_t*c,ngx_iovec_t*vec){ssize_tn;ngx_err_terr;eintr:n=writev(c->fd,vec->iovs,vec->count);ngx_log_debug2(NGX_LOG_DEBUG_EVENT,c->log,0,"writev: %z of %uz",n,vec->size);if(n==-1){err=ngx_errno;switch(err){caseNGX_EAGAIN:ngx_log_debug0(NGX_LOG_DEBUG_EVENT,c->log,err,"writev() not ready");returnNGX_AGAIN;caseNGX_EINTR:ngx_log_debug0(NGX_LOG_DEBUG_EVENT,c->log,err,"writev() was interrupted");gotoeintr;default:c->write->error=1;ngx_connection_error(c,err,"writev() failed");returnNGX_ERROR;}}returnn;}2 目的
1 设计意图
ngx_writev是 Nginx 对 POSIXwritev(2)系统调用的薄封装函数,
负责将一组struct iovec数组
一次性写入到 TCP 套接字文件描述符。
它是发送链路的最底层——直接与内核交互的系统调用入口。
2 上下游关系
ngx_writev_chain (发送链管理) │ 将 ngx_chain_t 链表转为 iovec 数组 │ 调用 ngx_writev 执行实际发送 ▼ ngx_writev (本函数) │ 1. 调用 writev(2) 系统调用 │ 2. 将 POSIX errno 映射为 Nginx 内部错误码 │ 3. 处理 EAGAIN / EINTR / 其他错误 ▼ writev() 系统调用 ──► 内核 TCP 协议栈核心职责拆分:
- 系统调用适配:
将 Nginx 的ngx_iovec_t结构转换为 POSIXwritev的参数格式(vec->iovs、vec->count)。 - 错误码归一化:
将 POSIX 的errno(EAGAIN、EINTR等)映射为 Nginx 内部统一的错误码
(NGX_AGAIN、NGX_ERROR),
使上层调用者不直接依赖errno。 - EINTR 安全重试:对信号中断(
EINTR)自动重试,保证信号安全语义。 - 调试日志:在每次系统调用后记录实际发送字节数与预期量的关系,便于问题排查。
3 详解
1 函数签名
ssize_tngx_writev(ngx_connection_t*c,ngx_iovec_t*vec)1 返回值:ssize_t
| 返回值 | 含义 | 上层处理 |
|---|---|---|
> 0 | 实际发送的字节数 | ngx_writev_chain据此推进链指针 |
NGX_AGAIN(-2) | socket 发送缓冲区满,暂不可写 | 调用者设置wev->ready = 0,等待事件通知后重试 |
NGX_ERROR(-1) | 发生不可恢复的错误(连接重置等) | 调用者返回NGX_CHAIN_ERROR,上层关闭连接 |
注意:本函数不会返回 0——POSIXwritev对 TCP 套接字不会返回 0
2 函数名:ngx_writev
| 词段 | 含义 |
|---|---|
ngx | Nginx 命名空间前缀 |
writev | 直接对应 POSIXwritev(2)系统调用,表明这是一个系统调用的封装 |
3 参数列表
| 参数名 | 类型 | 含义 | 来源 | 约束 |
|---|---|---|---|---|
c | ngx_connection_t * | 目标 TCP 连接 | 上层ngx_writev_chain的上游调用者传入 | 非 NULL;c->fd为有效 socket fd;c->write指向写事件对象 |
vec | ngx_iovec_t * | 待发送的 iovec 数组封装 | ngx_writev_chain通过ngx_output_chain_to_iovec填充 | 非 NULL;vec->iovs指向有效的 iovec 数组;vec->count为条目数 |
2 逻辑流程
ngx_writev(c, vec) │ ├─ [1] 调用 writev 系统调用 │ └─ n = writev(c->fd, vec->iovs, vec->count) │ ├─ [2] 调试日志 │ └─ 记录 "writev: %z of %uz",n 和 vec->size │ ├─ [3] 成功路径 │ └─ n != -1 → 返回 n(实际发送字节数) │ └─ [4] 错误路径(n == -1) ├─ err = ngx_errno(保存 errno) │ ├─ [4.1] EAGAIN 处理 │ └─ err == NGX_EAGAIN → 记录 "not ready",返回 NGX_AGAIN │ ├─ [4.2] EINTR 处理 │ └─ err == NGX_EINTR → 记录 "was interrupted",goto eintr 重试 │ └─ [4.3] 其他错误处理 └─ default → c->write->error = 1,记录日志,返回 NGX_ERROR3 局部变量声明
{ssize_tn;ngx_err_terr;}局部变量声明
n:保存writev的返回值(已发送字节数或 -1)。err:保存 POSIXerrno的快照(见 [4] 分支的设计意图)。
1 调用 writev 系统调用
eintr:n=writev(c->fd,vec->iovs,vec->count);进入条件:每次函数被调用时首先执行,以及 EINTR 重试时跳转至此。
处理逻辑:
调用 POSIXwritev(2)系统调用,执行 scatter-gather 写操作。参数:
c->fd:目标 socket 文件描述符。
该 fd 在连接建立时被设置为非阻塞模式(O_NONBLOCK),
因此writev不会阻塞进程。vec->iovs:
由调用者(ngx_output_chain_to_iovec)填充的struct iovec数组,
每个元素包含iov_base(数据起始地址)和iov_len(数据长度)。vec->count:
iovec 数组中的有效条目数。
writev(2)是 POSIX.1-2001 标准系统调用(声明在<sys/uio.h>),
语义为"将多个不连续内存区域的数据一次性写入文件描述符"。
对于 TCP 套接字,实际发送的字节数可能小于请求的总字节数(vec->size)——
当内核发送缓冲区不足以容纳所有数据时,writev会发送尽可能多的数据并返回实际发送量。
2 调试日志
ngx_log_debug2(NGX_LOG_DEBUG_EVENT,c->log,0,"writev: %z of %uz",n,vec->size);处理逻辑:
记录一次writev调用的结果:
实际发送字节数n(格式符%z对应ssize_t)和
请求发送的总字节数vec->size(格式符%uz对应size_t)。
设计意图:%z of %uz的日志格式直观地展示"实际发送 / 请求发送",
例如"writev: 4096 of 8192"表示只发了一半。这在排查部分发送问题时非常有用。
3 成功路径
returnn;进入条件:n != -1(即writev返回值 >= 0)。
处理逻辑:
直接返回n(实际发送的字节数)。
对于 TCP 套接字,n >= 0且n <= vec->size。n可能为 0 的极端情况(当所有iov_len均为 0 时),
但 Nginx 的正常路径不会构造空的 iovec。
4 错误路径
if(n==-1){err=ngx_errno;switch(err){caseNGX_EAGAIN:...caseNGX_EINTR:...default:...}}进入条件:writev返回 -1,表示发生错误。
处理逻辑:
首先通过ngx_errno保存errno的值。
必须在switch之前立即保存——
因为后续的ngx_log_debug0、ngx_connection_error等日志函数内部可能会修改errno
(如调用write到日志文件描述符),
如果在switch中直接使用errno,可能会读到被日志函数覆盖后的错误码。
ngx_errno宏定义(src/os/unix/ngx_errno.h):
#definengx_errnoerrno在 Unix 平台上直接映射为 POSIXerrno。
4.1 EAGAIN 处理
caseNGX_EAGAIN:ngx_log_debug0(NGX_LOG_DEBUG_EVENT,c->log,err,"writev() not ready");returnNGX_AGAIN;进入条件:err == NGX_EAGAIN(即errno == EAGAIN或errno == EWOULDBLOCK)。
处理逻辑:EAGAIN表示 socket 发送缓冲区已满,数据暂时无法写入。
这是非阻塞 I/O 的正常信号,不是真正的错误。
记录一条调试日志后,返回NGX_AGAIN(-2)。
调用者ngx_writev_chain会将wev->ready置为 0,
然后等待 epoll/kqueue 通知 socket 再次可写。
4.2 EINTR 处理
caseNGX_EINTR:ngx_log_debug0(NGX_LOG_DEBUG_EVENT,c->log,err,"writev() was interrupted");gotoeintr;进入条件:err == NGX_EINTR(即writev被信号处理程序中断)。
处理逻辑:EINTR是 POSIX 信号安全语义的一部分:
当进程收到信号并执行了信号处理函数后,
某些"慢系统调用"(slow system call)会返回 -1 并将errno设为EINTR。
对于writev,此时没有数据被发送,调用者应当重新发起系统调用。
代码通过goto eintr跳回writev调用点,实现自动重试。
4.3 其他错误处理
default:c->write->error=1;ngx_connection_error(c,err,"writev() failed");returnNGX_ERROR;进入条件:err不是EAGAIN也不是EINTR——即发生了真正的错误。
处理逻辑:
分两步:
设置错误标志:
c->write->error = 1。c->write是连接的写事件对象(ngx_event_t),
其error位字段标记该事件已进入错误状态。
上层事件处理循环会检测此标志,触发连接关闭流程。记录错误日志:
调用ngx_connection_error(c, err, "writev() failed"),
根据错误类型以适当的日志级别记录错误信息。返回
NGX_ERROR(-1),通知调用者发生了不可恢复的错误。
设计意图:
c->write->error = 1在日志记录之前设置,
这是 Nginx 的错误处理惯例——先标记状态,再记录日志。
如果日志记录本身触发了错误(极端情况),状态已经正确反映。返回
NGX_ERROR而非直接关闭连接,将关闭决策留给上层——保持职责分离。