基于本文回答

播面 播面

刷题像听歌,多听自然懂
0
评论

Nginx 中的 sendfile 指令是如何利用零拷贝(Zero-copy)技术的?

在 Nginx 中,sendfile 指令(通常在 nginx.conf 中配置为 sendfile on;)是优化静态文件传输性能的核心配置。它通过调用操作系统的 零拷贝(Zero-copy) 技术,极大地降低了 CPU 的负载和内存带宽的消耗。

要理解 Nginx 是如何利用零拷贝的,我们需要对比一下“传统文件传输”“零拷贝文件传输”的工作原理。


1. 传统的文件传输方式(不使用 sendfile)

如果关闭 sendfile,Nginx 处理一个静态文件下载请求(将磁盘上的文件发送到网络)的过程如下:

Nginx 在用户空间(User Space)运行,它需要调用两个系统调用:read()write()

  1. 第一次上下文切换:Nginx 发起 read() 系统调用,CPU 从用户态切换到内核态。
  2. 第一次拷贝(DMA拷贝):硬件 DMA(直接内存访问)控制器将数据从磁盘读取到内核空间的页缓存(Page Cache)中。
  3. 第二次拷贝(CPU拷贝):CPU 将数据从内核态的页缓存拷贝到用户态的 Nginx 缓冲区(Buffer)中。
  4. 第二次上下文切换read() 调用返回,CPU 从内核态切换回用户态。
  5. 第三次上下文切换:Nginx 发起 write() 系统调用,CPU 再次从用户态切换到内核态。
  6. 第三次拷贝(CPU拷贝):CPU 将数据从用户态的 Nginx 缓冲区拷贝到内核态的Socket 缓冲区(Socket Buffer)中。
  7. 第四次拷贝(DMA拷贝):DMA 控制器将数据从 Socket 缓冲区拷贝到网卡(NIC)中准备发送。
  8. 第四次上下文切换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()

  1. 第一次上下文切换:Nginx 发起 sendfile() 系统调用,CPU 从用户态切换到内核态。
  2. 第一次拷贝(DMA拷贝):DMA 控制器将数据从磁盘读取到内核的页缓存(Page Cache)
  3. 第二次拷贝(CPU拷贝):CPU 将数据从页缓存直接拷贝到内核的 Socket 缓冲区。(注意:这里数据完全没有经过用户空间)。
  4. 第三次拷贝(DMA拷贝):DMA 控制器将数据从 Socket 缓冲区拷贝到网卡发送。
  5. 第二次上下文切换sendfile() 返回,切换回用户态。

代价降低到:2 次上下文切换,3 次拷贝(1 次 CPU,2 次 DMA)。

阶段二:真正的 Zero-copy(sendfile + DMA Scatter/Gather)

在 Linux 2.4 及之后的内核版本中,配合支持 Scatter-Gather (SG-DMA) 技术的网卡,sendfile 实现了真正的 CPU 零拷贝:

  1. 第一次上下文切换:Nginx 发起 sendfile()
  2. 第一次拷贝(DMA拷贝):DMA 将数据从磁盘读到内核的页缓存
  3. 没有 CPU 数据拷贝:CPU 不再把数据复制到 Socket 缓冲区,而是仅仅把数据的内存地址和长度等描述符(Descriptor)追加到 Socket 缓冲区中。
  4. 第二次拷贝(DMA Gather拷贝):网卡的 SG-DMA 控制器根据 Socket 缓冲区中的描述符,直接从内核页缓存中读取数据并发送到网络。
  5. 第二次上下文切换sendfile() 返回。

总结零拷贝方式的极致优势:

  • 2 次 上下文切换。
  • 2 次 数据拷贝(全部由硬件 DMA 完成,CPU 拷贝次数为 0)。
  • 数据根本不需要离开内核空间。

3. Nginx 中 sendfile 的实际收益

  • 极高的吞吐量:非常适合用来做静态文件服务器(图片、视频、静态 HTML 等)。
  • 节省 CPU 资源:CPU 被解放出来,可以去处理更多的 TCP 连接、解析 HTTP 协议等,从而大幅提升并发能力。
  • 减少内存带宽占用:数据不需要在内存条的不同区域(内核区和用户区)之间来回复制。

4. Nginx 使用 sendfile 的局限性(必须知道的坑)

虽然 sendfile 很强大,但因为它绕过了用户空间,这意味着 Nginx(运行在用户空间)无法看到或修改文件的内容

在以下场景中,sendfile 会失效或被退化为传统读取:

  1. 动态压缩(gzip on):如果 Nginx 需要在内存中实时对文件进行 Gzip 压缩,它必须把文件内容读到用户空间,此时 sendfile 不起作用。(注:如果是 gzip_static on;,即直接发送磁盘上已经压缩好的 .gz 文件,则依然可以完美使用 sendfile)。
  2. HTTPS (TLS/SSL):传统情况下,数据需要先在用户态(如 OpenSSL)进行加密,然后再发送,所以无法使用零拷贝。(注:现代 Linux 内核引入了 kTLS (Kernel TLS),允许在内核态完成加密并结合 sendfile,但这需要较新的系统和 Nginx 配置支持)。
  3. 内容替换(如 sub_filter / SSI):如果 Nginx 需要修改返回的 HTML 内容,必须读取到内存中,无法使用零拷贝。

因此,在 Nginx 架构中,sendfile静态文件分发的一柄利器,它深刻体现了底层操作系统特性与上层应用架构结合的威力。

00:00
00:00