Nginx 中的 sendfile 指令是如何利用零拷贝(Zero-copy)技术的?
在 Nginx 中,sendfile 指令(通常在 nginx.conf 中配置为 sendfile on;)是优化静态文件传输性能的核心配置。它通过调用操作系统的 零拷贝(Zero-copy) 技术,极大地降低了 CPU 的负载和内存带宽的消耗。
要理解 Nginx 是如何利用零拷贝的,我们需要对比一下“传统文件传输”和“零拷贝文件传输”的工作原理。
1. 传统的文件传输方式(不使用 sendfile)
如果关闭 sendfile,Nginx 处理一个静态文件下载请求(将磁盘上的文件发送到网络)的过程如下:
Nginx 在用户空间(User Space)运行,它需要调用两个系统调用:read() 和 write()。
- 第一次上下文切换:Nginx 发起
read()系统调用,CPU 从用户态切换到内核态。 - 第一次拷贝(DMA拷贝):硬件 DMA(直接内存访问)控制器将数据从磁盘读取到内核空间的页缓存(Page Cache)中。
- 第二次拷贝(CPU拷贝):CPU 将数据从内核态的页缓存拷贝到用户态的 Nginx 缓冲区(Buffer)中。
- 第二次上下文切换:
read()调用返回,CPU 从内核态切换回用户态。 - 第三次上下文切换:Nginx 发起
write()系统调用,CPU 再次从用户态切换到内核态。 - 第三次拷贝(CPU拷贝):CPU 将数据从用户态的 Nginx 缓冲区拷贝到内核态的Socket 缓冲区(Socket Buffer)中。
- 第四次拷贝(DMA拷贝):DMA 控制器将数据从 Socket 缓冲区拷贝到网卡(NIC)中准备发送。
- 第四次上下文切换:
write()调用返回,CPU 从内核态切换回用户态。
总结传统方式的代价:
- 4 次 用户态和内核态的上下文切换。
- 4 次 数据拷贝(2 次 DMA 拷贝,2 次 CPU 拷贝)。
对于 Nginx 这种高并发的 Web 服务器来说,频繁的上下文切换和毫无意义的 CPU 拷贝会严重消耗系统资源。
2. Nginx 中的 Zero-copy 技术(使用 sendfile)
开启 sendfile on; 后,Nginx 不再使用 read() 和 write(),而是直接调用 Linux 系统提供的 sendfile() 系统调用。
所谓“零拷贝”,并不是指完全没有数据拷贝,而是指没有数据从内核空间拷贝到用户空间,全程没有 CPU 参与搬运数据。
具体流程如下:
阶段一:基本的 sendfile()
- 第一次上下文切换:Nginx 发起
sendfile()系统调用,CPU 从用户态切换到内核态。 - 第一次拷贝(DMA拷贝):DMA 控制器将数据从磁盘读取到内核的页缓存(Page Cache)。
- 第二次拷贝(CPU拷贝):CPU 将数据从页缓存直接拷贝到内核的 Socket 缓冲区。(注意:这里数据完全没有经过用户空间)。
- 第三次拷贝(DMA拷贝):DMA 控制器将数据从 Socket 缓冲区拷贝到网卡发送。
- 第二次上下文切换:
sendfile()返回,切换回用户态。
代价降低到:2 次上下文切换,3 次拷贝(1 次 CPU,2 次 DMA)。
阶段二:真正的 Zero-copy(sendfile + DMA Scatter/Gather)
在 Linux 2.4 及之后的内核版本中,配合支持 Scatter-Gather (SG-DMA) 技术的网卡,sendfile 实现了真正的 CPU 零拷贝:
- 第一次上下文切换:Nginx 发起
sendfile()。 - 第一次拷贝(DMA拷贝):DMA 将数据从磁盘读到内核的页缓存。
- 没有 CPU 数据拷贝:CPU 不再把数据复制到 Socket 缓冲区,而是仅仅把数据的内存地址和长度等描述符(Descriptor)追加到 Socket 缓冲区中。
- 第二次拷贝(DMA Gather拷贝):网卡的 SG-DMA 控制器根据 Socket 缓冲区中的描述符,直接从内核页缓存中读取数据并发送到网络。
- 第二次上下文切换:
sendfile()返回。
总结零拷贝方式的极致优势:
- 仅 2 次 上下文切换。
- 仅 2 次 数据拷贝(全部由硬件 DMA 完成,CPU 拷贝次数为 0)。
- 数据根本不需要离开内核空间。
3. Nginx 中 sendfile 的实际收益
- 极高的吞吐量:非常适合用来做静态文件服务器(图片、视频、静态 HTML 等)。
- 节省 CPU 资源:CPU 被解放出来,可以去处理更多的 TCP 连接、解析 HTTP 协议等,从而大幅提升并发能力。
- 减少内存带宽占用:数据不需要在内存条的不同区域(内核区和用户区)之间来回复制。
4. Nginx 使用 sendfile 的局限性(必须知道的坑)
虽然 sendfile 很强大,但因为它绕过了用户空间,这意味着 Nginx(运行在用户空间)无法看到或修改文件的内容。
在以下场景中,sendfile 会失效或被退化为传统读取:
- 动态压缩(gzip on):如果 Nginx 需要在内存中实时对文件进行 Gzip 压缩,它必须把文件内容读到用户空间,此时
sendfile不起作用。(注:如果是gzip_static on;,即直接发送磁盘上已经压缩好的.gz文件,则依然可以完美使用sendfile)。 - HTTPS (TLS/SSL):传统情况下,数据需要先在用户态(如 OpenSSL)进行加密,然后再发送,所以无法使用零拷贝。(注:现代 Linux 内核引入了 kTLS (Kernel TLS),允许在内核态完成加密并结合 sendfile,但这需要较新的系统和 Nginx 配置支持)。
- 内容替换(如 sub_filter / SSI):如果 Nginx 需要修改返回的 HTML 内容,必须读取到内存中,无法使用零拷贝。
因此,在 Nginx 架构中,sendfile 是静态文件分发的一柄利器,它深刻体现了底层操作系统特性与上层应用架构结合的威力。