Skip to content

零拷贝技术

传统文件传输需要多次数据拷贝和上下文切换,性能开销大。零拷贝技术通过减少数据拷贝次数和上下文切换次数,提升 I/O 性能。

传统文件传输

数据拷贝路径

磁盘 → 内核缓冲区(DMA 拷贝)→ 用户缓冲区(CPU 拷贝)→ 内核 socket 缓冲区(CPU 拷贝)→ 网卡缓冲区(DMA 拷贝)。共 4 次拷贝(2 次 DMA,2 次 CPU),4 次上下文切换。

性能瓶颈

CPU 拷贝占用 CPU 时间,拷贝大文件时 CPU 负载高。上下文切换保存和恢复寄存器、栈,切换开销大。多次拷贝占用内存带宽,Cache 命中率下降。

mmap

mmap 将文件映射到用户空间虚拟内存,用户空间可以直接访问文件数据,减少一次 CPU 拷贝。

mmap 的拷贝路径

磁盘 → 内核缓冲区(DMA 拷贝)→ 用户缓冲区(共享内存,不需要拷贝)→ 内核 socket 缓冲区(CPU 拷贝)→ 网卡缓冲区(DMA 拷贝)。共 3 次拷贝(2 次 DMA,1 次 CPU),4 次上下文切换。

mmap 的问题

映射大文件会占用大量虚拟内存,可能导致内存不足。文件截断时访问映射区域会触发 SIGBUS,需要信号处理。多个进程映射同一文件时,一个进程的写操作会影响其他进程,需要同步。

sendfile

sendfile 是 Linux 特有的系统调用,专门用于文件传输,减少数据拷贝和上下文切换。

sendfile 的拷贝路径

磁盘 → 内核缓冲区(DMA 拷贝)→ 网卡缓冲区(DMA 拷贝)。共 2 次拷贝(都是 DMA),2 次上下文切换。数据不经过用户空间,直接在内核空间传输。

sendfile 2.4+ 的优化

Linux 2.4+ 引入 DMA 收集拷贝,内核缓冲区的数据直接传输到网卡缓冲区,不需要经过 socket 缓冲区。进一步减少一次内存拷贝。

sendfile 的限制

只能用于文件传输到 socket,不能用于 socket 到 socket。不支持用户空间的修改,数据直接从文件传输到网络。需要硬件支持 DMA(现代网卡都支持)。

splice

splice 是 Linux 2.6.17 引入的系统调用,在两个文件描述符之间移动数据,不需要经过用户空间。

splice 的工作原理

splice 在内核中创建一个管道,将数据从一个文件描述符 splice 到管道,再从管道 splice 到另一个文件描述符。数据不经过用户空间,减少拷贝。

splice 的应用场景

文件到 socket(类似 sendfile)、socket 到 socket(数据转发)、socket 到文件(数据保存)。splice 比 sendfile 更通用,可以用于任意两个文件描述符。

splice 的限制

至少有一个文件描述符必须是管道(或类似的文件类型)。某些文件系统不支持 splice(如 NFS)。splice 的性能受限于管道大小,可能需要多次调用。

tee

tee 在两个管道之间复制数据,数据不消耗,可以继续被读取。tee 与 splice 类似,但数据不消耗,适合数据复制场景。

零拷贝的应用

文件服务器

Nginx 使用 sendfile 提供静态文件服务,减少 CPU 开销,提高吞吐量。配置 sendfile on 启用零拷贝。

消息队列

Kafka 使用 mmap 和 sendfile 提供日志文件传输,减少 CPU 开销。Kafka 的零拷贝包括:生产者写入 mmap 文件,消费者通过 sendfile 读取文件。

数据库

PostgreSQL 的 WAL 日志可以使用 mmap 减少拷贝,提高写入性能。Redis 的 AOF 重写可以使用 splice 减少拷贝。

零拷贝的权衡

零拷贝减少数据拷贝和上下文切换,提高性能。但零拷贝不支持用户空间的修改,限制了灵活性。零拷贝需要硬件支持 DMA,某些平台不支持。零拷贝的 API 复杂,编程难度高。

理解零拷贝技术,有助于选择合适的 I/O 方式,在高性能场景下做出正确的权衡。