零拷贝和传统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
)缓冲区发送- 切换回用户态
问题
- 四次数据拷贝:两次 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
发送缓冲区复制到网卡。- 切换回用户态
效果
减少了 1 次 CPU 拷贝:消除了从内核页缓存到用户缓冲区的拷贝。
sendfile (传统,无硬件支持)
sendfile()
是一个专门用于在两个文件描述符之间传输数据的系统调用(通常源文件是块设备或常规文件【不支持管道等文件描述符】,目标文件是Socket
)。数据传输完全在内核空间内完成。
流程
调用sendfile(socket_fd, file_fd, ...)
- 切换到内核态
- 第1次复制(DMA):
DMA
将数据从磁盘读入内核页缓存。- 第2次复制(CPU):
CPU
将数据从页缓存复制到与Socket
关联的内核缓冲区。- 第3次复制(DMA):
DMA
将数据从Socket
内核缓冲区复制到网卡。- 切换到用户态
效果
- 减少了 1 次 CPU 拷贝:消除了用户空间缓冲区的参与。
- 减少了 2 次上下文切换:只需要一次系统调用。
sendfile(现代,硬件支持)
在
Linux 2.4
及更高版本的内核中,结合支持scatter-gather
功能的网络设备,sendfile
可以实现真正的零拷贝(无CPU
拷贝参与)。
硬件前提
scatter-gather I/O
支持
- 也称为向量
I/O
,允许硬件处理不连续的内存缓冲区- 网卡需要支持此功能(可通过
ethtool -k 网卡名称 | grep scatter-gather
检查)- 现代网卡大多支持此特性
流程
- 切换到内核态
- 第1次传输(DMA):DMA 将数据从磁盘读取到内核页缓存(
Page Cache
)。- 零拷贝处理
CPU
不再进行数据拷贝。CPU
的工作是创建指向Page Cache
中数据块位置和长度的描述符(scatter-gather
列表)。- 这个描述符被传递给网络协议栈(逻辑上是套接字缓冲区,但数据本身不拷贝过去)
- 第2次传输(DMA):网卡的DMA引擎直接从页缓存读取数据传输到网络,无需
CPU
介入- 切换回用户态
效果
- 完全消除CPU拷贝:无
CPU
参与的数据复制过程- 减少了2次上下文切换:只需一次系统调用
- 显著降低CPU使用率:在高性能场景下,CPU使用率可减少50-60%
- 提高吞吐量:特别是大文件传输时性能提升明显
总结
对比
CPU
占用低- 大文件传输快
使用
零拷贝技术最擅长处理 将数据从一个地方原封不动地搬到另一个地方 的场景,特别是涉及大量数据时
- 数据量大
- 数据无需修改(修改也可以使用
mmap
【适合读多写少】)
- 静态文件服务器
- 需要将磁盘上的文件(如图片、视频、HTML 页面、软件包)通过网络发送给客户端。
- 数据从磁盘读出后,通常不需要在应用程序层面进行任何修改,直接发送到网络即可。
sendfile
系统调用在这种场景下非常高效,如Nginx
- 消息队列
Kafka
这样的系统,需要将生产者发送的数据(通常先写入磁盘日志)高效地传递给消费者。Broker
(中间件服务器)通常只是数据的中转站。从磁盘读取数据,然后将其发送到网络上的消费者,这个过程数据本身往往不需要修改。Kafka
就大量使用了sendfile
来实现高效的数据传输- 视频/音频流媒体服务