Skip to content

零拷贝介绍

1. 传统 I/O

1.1 内核空间与用户空间

现代操作系统(如 Linux)为了保护系统的稳定性,将虚拟内存划分为两个区域:

  • 内核空间(Kernel Space):操作系统内核运行于此,拥有访问所有硬件设备(如磁盘、网卡)的权限。

  • 用户空间(User Space):普通应用程序(如 Web 服务器、数据库)运行于此,不能直接访问硬件。

当一个应用程序需要读取磁盘文件或发送网络数据时,它不能直接操作硬件,必须通过“系统调用”(System Call)向内核发出请求。这个从用户空间到内核空间的切换,以及处理完请求后从内核空间返回用户空间的过程,被称为“上下文切换”(Context Switch),它本身是有性能开销的。

1.2 传统文件传输的流程

假设一个 Web 服务器需要将一个磁盘上的文件通过网络发送给客户端,传统的 read() + write() 流程如下:

  1. 应用程序调用 read() 函数,发起系统调用,导致一次从“用户态”到“内核态”的上下文切换。

  2. DMA(直接内存访问)控制器将数据从磁盘读取到内核空间的“读缓冲区”(Page Cache/Read Buffer)中。 (第 1 次拷贝:DMA 拷贝)

  3. CPU 将数据从内核空间的“读缓冲区”复制到应用程序在用户空间的“用户缓冲区”(User Buffer)中。 (第 2 次拷贝:CPU 拷贝)

  4. read() 函数返回,导致一次从“内核态”回到“用户态”的上下文切换。此时,数据已准备好在用户空间。

  5. 应用程序调用 write() 函数,发起系统调用,导致又一次从“用户态”到“内核态”的上下文切换。

  6. CPU 将数据从“用户缓冲区”再次复制到内核空间的“套接字缓冲区”(Socket Buffer)中。 (第 3 次拷贝:CPU 拷贝)

  7. DMA 控制器将数据从“套接字缓冲区”复制到网卡中,以便进行网络传输。 (第 4 次拷贝:DMA 拷贝)

  8. write() 函数返回,导致最后一次从“内核态”回到“用户态”的上下文切换。

瓶颈总结:

整个过程发生了 4 次上下文切换 和 4 次数据拷贝。其中,两次 DMA 拷贝是硬件层面的,不可避免且不消耗 CPU。但另外两次 CPU 拷贝(内核->用户,用户->内核)是完全冗余的,因为应用程序通常只是扮演一个“搬运工”的角色,并不需要修改数据。这两次 CPU 拷贝和频繁的上下文切换是主要的性能瓶颈。

2. 零拷贝

零拷贝(Zero-Copy)并非指完全没有数据拷贝,而是指避免在用户空间和内核空间之间进行不必要的 CPU 数据拷贝。其核心目标是:

  1. 减少或消除冗余的 CPU 数据拷贝

  2. 减少上下文切换的次数

Linux 内核提供了多种实现或接近零拷贝的方式。

2.1 mmap + write (内存映射)

  • 原理mmap 是一个系统调用,它将内核的读缓冲区(Page Cache)的一部分地址直接映射到用户空间的虚拟地址上。这样,应用程序就可以像访问普通内存一样访问内核缓冲区,从而省去了一次从内核到用户的 CPU 拷贝。

  • 流程

    1. 应用调用 mmap(),DMA 将磁盘数据拷贝到内核的读缓冲区。

    2. 内核将这个读缓冲区与用户空间的虚拟地址进行映射。

    3. 应用调用 write(),CPU 直接将数据从内核的读缓冲区(现在也被用户空间映射了)拷贝到内核的套接字缓冲区。 (第 1 次 CPU 拷贝)

    4. DMA 将数据从套接字缓冲区拷贝到网卡。

  • 效果

    • CPU 拷贝:从 2 次减少到 1 次。

    • DMA 拷贝:2 次(不变)。

    • 上下文切换:4 次(不变,因为 mmapwrite 是两次独立的系统调用)。

  • 局限

    • 虽然性能有所提升,但仍存在一次 CPU 拷贝和 4 次上下文切换。

    • 对于小文件,内存映射可能导致页对齐造成的内存浪费。

    • 存在一个隐患:当一个文件被 mmap 后,如果其他进程截断了这个文件,那么 write 操作可能会因为访问非法内存地址而收到 SIGBUS 信号,导致进程被终止。

2.2 sendfile

  • 原理sendfile 是 Linux 2.1 内核引入的专用系统调用,旨在简化两个文件描述符(一个代表文件,一个代表网络套接字)之间的数据传输。数据完全在内核空间内流动,不进入用户空间。

  • 流程

    1. 应用调用 sendfile(),发起一次系统调用。

    2. DMA 将磁盘数据拷贝到内核的读缓冲区。

    3. CPU 将数据从内核的读缓冲区直接拷贝到内核的套接字缓冲区。 (第 1 次 CPU 拷贝)

    4. DMA 将数据从套接字缓冲区拷贝到网卡。

  • 效果

    • CPU 拷贝:1 次。

    • DMA 拷贝:2 次。

    • 上下文切换:从 4 次减少到 2 次(因为只有一次 sendfile 调用)。

2.3 sendfile + DMA Gather Copy

  • 原理:在 Linux 2.4 内核之后,如果网卡硬件支持“分散-收集”(Scatter-Gather)功能,sendfile 的性能可以达到极致。在这种模式下,CPU 不再拷贝任何实际的数据。

  • 流程

    1. 应用调用 sendfile()

    2. DMA 将磁盘数据拷贝到内核的读缓冲区。

    3. CPU 不拷贝数据,而是将指向读缓冲区中数据位置和长度的“描述符”(Descriptor)附加到套接字缓冲区。

    4. DMA 控制器根据套接字缓冲区中的描述符,直接从内核的读缓冲区“收集”数据,然后将其“分散”地发送到网卡。

  • 效果

    • CPU 拷贝:0 次。

    • DMA 拷贝:2 次。

    • 上下文切换:2 次。

  • 局限:需要底层硬件(网卡)的支持。

2.4 splice

  • 原理splice 是 Linux 2.6.17 引入的系统调用,它在两个文件描述符之间建立了一个内核内部的“管道”(Pipe),从而实现了数据传输,并且不需要硬件支持。

  • 流程

    1. 应用调用 splice()

    2. DMA 将磁盘数据拷贝到内核的读缓冲区。

    3. CPU 在内核的读缓冲区和套接字缓冲区之间建立一个管道,数据通过这个管道流动,避免了 CPU 的显式拷贝。

    4. DMA 将数据从套接字缓冲区拷贝到网卡。

  • 效果

    • CPU 拷贝:0 次。

    • DMA 拷贝:2 次。

    • 上下文切换:2 次。

  • 局限:它的两个文件描述符参数中,至少有一个必须是管道设备。

I/O 方式 CPU 拷贝次数 DMA 拷贝次数 上下文切换次数 系统调用次数
传统 read + write 2 2 4 2
mmap + write 1 2 4 2
sendfile 1 2 2 1
sendfile + DMA Gather 0 2 2 1
splice 0 2 2 1

3. 零拷贝的应用

3.1 Java NIO 中的实现

Java 的 NIO(New I/O)库为上层应用封装了操作系统的零拷贝能力。

  • FileChannel.map():这个方法底层就是通过 mmap 系统调用实现的,它返回一个 MappedByteBuffer。当你对这个 Buffer 进行读写时,实际上是直接在操作内核的页缓存,适用于需要对文件内容进行频繁随机读写的场景。

  • FileChannel.transferTo() / transferFrom():这个方法在 Linux 和 Unix 系统上,底层就是通过 sendfile 系统调用实现的。它提供了一种在两个通道(Channel)之间高效传输数据的方式,非常适合大文件的顺序传输。

3.2 消息队列:Kafka vs. RocketMQ

这是一个非常经典的零拷贝应用案例:

  • Kafka:在消费端从 Broker 拉取消息数据时,大量使用了 sendfile 机制。因为日志文件通常是顺序读取且一次性发送大量数据,sendfile 的“一次调用,全程托管”模式非常适合这种高吞吐量的场景。

  • RocketMQ:主要使用 mmap 机制来读写其核心的 CommitLog 文件。mmap 将文件映射到内存,使得 RocketMQ 可以像操作内存一样读写文件,这为实现更复杂的读写逻辑(如随机读)提供了便利,非常适合业务消息这种对延迟和灵活性要求更高的场景。

3.3 Web 服务器

像 Nginx、Tomcat、Apache 等高性能 Web 服务器,在处理静态文件(如图片、HTML、CSS)请求时,都会优先使用 sendfile,这能极大地提升文件传输效率,降低服务器的 CPU 负载。

3.4 Netty

Netty 中的零拷贝概念更广泛,它包含了两个层面:

  • 操作系统层面:通过 DefaultFileRegion 包装 FileChannel.transferTo() 来利用 sendfile

  • Java 用户态层面:通过 CompositeByteBuf(组合多个 Buffer 为一个逻辑 Buffer)、slice(创建共享同一内存区域的子 Buffer)等技术,避免了在 JVM 内部进行不必要的内存数组拷贝,这是一种数据操作层面的优化。

总而言之,零拷贝是通过与操作系统内核和硬件的紧密协作,优化数据传输路径,消除冗余步骤,从而显著提升 I/O 性能的关键技术。