Skip to content

零拷贝和传统IO

用户态和内核态

  • 用户态(User Mode ):
    • 描述:用户使用的(如程序运行)所在的受限状态(无法直接访问硬件或系统关键资源
    • 需要依赖系统级操作(如read/write切换内核态
  • 内核态(Kernel Mode):
    • 操作系统内核运行时所处的特权状态,能够完全控制和管理硬件、内存、进程等核心资源,执行所有特权指令。
    • 所有的系统级调用都在内核态完成
  • DMA:
    • Direct Memory Access / 直接内存访问
    • 释放了CPU,只需要CPU少量参与(初始化传输和处理完成中断)
    • 硬件到内存直接传输DMA 控制器将数据直接在设备和内存之间流动。

用户态/内核态切换是有开销的,需要保存和恢复 CPU 状态等信息。零拷贝技术的目标之一就是减少这种切换次数。

想象一家银行:

  • 用户态:你在银行大厅(用户空间),可以办理常规业务(运行应用程序代码)。但你不能直接进入金库(硬件)或者看别人的账户信息(其他进程内存)。
  • 内核态:银行的后台和金库区域(内核空间)。银行职员(内核代码)有权限访问金库,操作账户系统(硬件、核心资源)。
  • 系统调用:你填写的业务申请单(系统调用)。当你需要取大额现金(访问文件/网络)时,你把申请单交给柜员(发起系统调用)。
  • 上下文切换:柜员(CPU)放下手头给你服务的工作(用户态任务),进入后台(切换到内核态)去处理你的请求(执行内核代码),处理完再回到柜台(切换回用户态)把结果给你(返回数据)。

传统IO流程

读取磁盘文件发送到网络为例

描述

  • read()调用:
    • 切换状态App(用户态)->Kernel (内核态)
    • DMA拷贝:数据从磁盘->页缓存
    • CPU拷贝:页缓存->用户空间缓冲区
    • 切换回用户态:此时应用缓冲区有数据了。可以调用发送了
  • write()调用:
    • 切换状态
    • CPU拷贝:用户空间缓冲区->Socket发送缓冲区
    • DMA拷贝Socket缓冲区->网络接口(NIC)缓冲区发送
    • 切换回用户态

image-20250429173729373

问题

  • 四次数据拷贝:两次 DMA 拷贝(硬件完成很快,不消耗 CPU)和两次 CPU 拷贝(消耗 CPU 周期和内存带宽)。
  • 四次上下文切换:两次 read 调用和两次 write 调用带来的用户态/内核态切换开销。

两次 CPU 拷贝多次上下文切换是主要的性能瓶颈

零拷贝技术

通过不同的系统调用或机制来优化上述流程,减少或消除 CPU 拷贝和上下文切换

常见的零拷贝技术

  • mmap + write
  • sendfile (Linux)

mmap + write

介绍

mmap(Memory-Mapped Files)是一种内存映射技术

  • 说明:
    • 允许将文件或设备直接映射到进程的虚拟地址空间中,使进程能够像访问内存一样直接操作文件数据。
    • 32位系统可能无法映射超大文件
  • 用途:
    • 优化传统IO::直接通过内存指针操作文件,省去read/write系统调用和数据拷贝,尤其适合大文件或频繁访问的场景(如数据库)
    • 共享内存通信:多个进程映射同一文件,共享同一物理内存区域,实现高效进程间通信(IPC),节约内存
    • 硬件与驱动交互:将硬件设备(如显卡显存、传感器寄存器)映射到用户空间,直接通过内存地址访问设备

流程

  • 调用mmap() 将文件映射到用户空间
    • 切换到内核态,将文件的一部分(或者全部)映射到进程的虚拟地址空间
    • 映射:内核会设置虚拟内存页表,将用户空间地址与实际的物理页(或者文件的块)关联起来。注意:映射不一定会立即触发磁盘数据加载,通常是按需分页(即访问时才加载)。如下方的第一次复制
    • 切换回用户态
  • 应用程序调用 write() 将数据发送到网络。
    • 切换到内核态
    • 第1次复制(DMA):如果数据不在页缓存,DMA 将数据从磁盘读入页缓存。(如果已在缓存则跳过)
    • 第2次复制(CPU):页缓存(同时也是映射区域)->socket缓冲区
    • 第3次复制(DMA)DMA 将数据从 Socket 发送缓冲区复制到网卡。
    • 切换回用户态

image-20250429173744060

效果

减少了 1 次 CPU 拷贝:消除了从内核页缓存到用户缓冲区的拷贝。

sendfile (传统,无硬件支持)

sendfile() 是一个专门用于在两个文件描述符之间传输数据的系统调用(通常源文件块设备或常规文件【不支持管道等文件描述符】,目标文件Socket)。数据传输完全在内核空间内完成。

流程

  • 调用sendfile(socket_fd, file_fd, ...)
    • 切换到内核态
    • 第1次复制(DMA)DMA 将数据从磁盘读入内核页缓存。
    • 第2次复制(CPU)CPU 将数据从页缓存复制到与 Socket 关联的内核缓冲区。
    • 第3次复制(DMA)DMA 将数据从 Socket 内核缓冲区复制到网卡。
    • 切换到用户态

image-20250429173809132

效果

  • 减少了 1 次 CPU 拷贝:消除了用户空间缓冲区的参与。
  • 减少了 2 次上下文切换:只需要一次系统调用。

sendfile(现代,硬件支持)

Linux 2.4及更高版本的内核中,结合支持scatter-gather功能的网络设备,sendfile可以实现真正的零拷贝(无CPU拷贝参与)。

硬件前提

  • scatter-gather I/O 支持
    • 也称为向量I/O,允许硬件处理不连续的内存缓冲区
    • 网卡需要支持此功能(可通过ethtool -k 网卡名称 | grep scatter-gather检查)
    • 现代网卡大多支持此特性

image-20250505165004916

流程

  • 切换到内核态
  • 第1次传输(DMA):DMA 将数据从磁盘读取到内核页缓存(Page Cache)。
  • 零拷贝处理
    • CPU 不再进行数据拷贝
    • CPU 的工作是创建指向 Page Cache 中数据块位置和长度的描述符(scatter-gather列表)。
    • 这个描述符被传递给网络协议栈(逻辑上是套接字缓冲区,但数据本身不拷贝过去)
  • 第2次传输(DMA):网卡的DMA引擎直接从页缓存读取数据传输到网络,无需CPU介入
  • 切换回用户态

image-20250505165531705

效果

  • 完全消除CPU拷贝:无CPU参与的数据复制过程
  • 减少了2次上下文切换:只需一次系统调用
  • 显著降低CPU使用率:在高性能场景下,CPU使用率可减少50-60%
  • 提高吞吐量:特别是大文件传输时性能提升明显

总结

对比

  • CPU占用低
  • 大文件传输快

image-20250505173110966

使用

零拷贝技术最擅长处理 将数据从一个地方原封不动地搬到另一个地方 的场景,特别是涉及大量数据时

  • 数据量大
  • 数据无需修改(修改也可以使用mmap【适合读多写少】)
  • 静态文件服务器
    • 需要将磁盘上的文件(如图片、视频、HTML 页面、软件包)通过网络发送给客户端。
    • 数据从磁盘读出后,通常不需要在应用程序层面进行任何修改,直接发送到网络即可。sendfile 系统调用在这种场景下非常高效,如Nginx
  • 消息队列
    • Kafka 这样的系统,需要将生产者发送的数据(通常先写入磁盘日志)高效地传递给消费者。
    • Broker(中间件服务器)通常只是数据的中转站。从磁盘读取数据,然后将其发送到网络上的消费者,这个过程数据本身往往不需要修改。Kafka 就大量使用了 sendfile 来实现高效的数据传输
  • 视频/音频流媒体服务