详解数据传输——零拷贝、direct IO

之前介绍 消息队列发展史 时,介绍过 kafka 做到高吞吐的一个优化点就是引入了「零拷贝」技术,今天从零拷贝入口了解下 linux 的 IO 过程

专业名词字典

可以等到读到后面的时候再来翻一下:

  • DMA: Direct Memory Access,直接存储器访问。代替 cpu 控制磁盘 IO,避免 cpu 在磁盘操作时承担过多的中断负载
  • MMU: Memory Management Unit,内存管理单元
  • Page Cache : 页缓存,以数据页为单位对数据进行缓存。是内核态的缓存 ,使用了磁盘「预读」
  • 虚拟内存 : 一个虚拟的内存地址,可以映射到物理地址。目的为每个进程提供一个私有连续的内存空间,可以把用户空间和内核空间的虚拟地址映射到同一个物理地址,这样就不用实际复制数据
  • NFS: 网格文件系统,允许用户和程序像访问本地文件一样访问远端系统文件
  • COW: Copy-on-write,写入时复制。多个调用者访问相同资源时共同持有该资源,只有有修改时才单独为修改方提供一个副本。减少对象的复制成本

磁盘 IO 中 CPU 的瓶颈

我们对存储器的美好愿景:

  • 速度足够快: 存取速度只要慢于CPU,就会成为瓶颈
  • 容量足够大
  • 足够便宜

愿景是美好的,实际存储中做了分层架构:

  • 寄存器: 速度和 cpu 一样快,cpu 访问没有时延。价格贵,容量极小
  • 高速缓存: cpu中的多级缓存, 参考 CPU中的多级缓存实现及问题
  • 主存: 主内存,通常称为 随机访问存储器(Random Access Memory, RAM),与 cpu 直接交换数据
  • 磁盘: 读写慢,有很多优化方式,零拷贝、异步IO、direct IO 等等

缓存区可以减少磁盘 IO 操作,但传统 IO 操作深度依赖 cpu,限制了操作系统有效进行数据传输操作的能力。

怎么解放 cpu 的生产力?

在没有DMA之前,传输数据的过程如下图,cpu需要参与数据的传输过程,期间是阻塞性的

DMA 就是在 cpu 和磁盘之间增加了一层代理,把 cpu负责的磁盘数据搬运委托给下一层处理,从而可以把 cpu 解放出来

但该过程中,cpu 仍要负责内核态的 page cache 与用户态缓冲区之间的数据交换。我们来看个实际场景:读取磁盘文件并通过网络发送给客户端。大概需要两行代码:

bash 复制代码
read(file, tmp_buf, len)  
write(socket, tmp_buf, len)

实际的系统交互:

这个过程中发生的耗时操作:

  • 4次 用户态/内核态 的切换
  • 4次数据拷贝,2 次 DMA 完成,2 次 cpu 完成

为什么读取数据涉及到用户态和内核态的上下文切换?

Answer: 用户态没有权限操作磁盘和网卡,只能通过系统调用的方式,交由内核态完成

那么优化的思路就变为了:

  • 尽量减少用户态和内核态的切换
  • 尽量减少数据拷贝的次数

在这个背景下,「零拷贝」技术就应运而生

什么是零拷贝?

零拷贝解决的问题: 跨过与用户态交互的过程,直接将数据从文件系统移动到网络接口。常用的实现方式:

  • mmap + write
  • sendfile
  • splice

1. mmap + write 优化读取成本

使用 mmap 代替 read 函数,mmap 会在调用进程的虚拟地址空间中创建一个新映射,直接把内核缓冲区的数据「映射」到用户空间,内核与用户空间就不会再有数据拷贝操作

优化后效果:4次上下文切换 + 2次 DMA 拷贝 + 1次 CPU 拷贝

2. sendfile 优化

linux 2.1 之后专门提供了发送文件的系统调用函数 sendfile,可以用来代替 read 和 write 两个函数,同时也不再涉及到数据的复制过程。优化后的过程如下:

优化后效果:2次上下文切换 + 2次 DMA 拷贝 + 1次 CPU 拷贝

linux 2.4 之后,提供了 scatter/gather 的 sendfile 操作,网卡的 socket buffer维护一个虚拟地址,省去一次内核态的 cpu 复制

优化后效果: 2次上下文切换 + 2次 DMA 拷贝 + 0次 CPU 拷贝

3. splice

splice并限制数据需要发送到socket,支持任意两个文件之间互相连接,不需要硬件支持。内部实现依赖 pipe buffer,因此输入/输出文件描述符必须有一个是管道设备。执行过程如下图所示:

优化后效果: 2次上下文切换 + 2次 DMA 拷贝 + 0次 CPU 拷贝

前面提到的 kafka 也利用了「零拷贝」技术,从而大幅提升了I/O,减少了数据的复制和传输时间,最终底层应用也是 sendfile。Nginx 也默认开启了零拷贝。

但是如果文件比较大,达到了GB,这时 page cache 会失效,此时怎么处理呢?

大文件传输的解决方案 - Direct I/O

我们先考虑读取文件时采用异步IO:

  • 阶段1: 内核向磁盘发起请求,随后可以继续处理其他事
  • 阶段2: 内核将磁盘中数据复制到进程缓冲区后,用户进程会收到内核的通知去处理数据

通过这个图可以看出来,异步IO没有经过 pageCache,而是直接将磁盘控制器缓冲区的数据复制到了用户缓冲区,原因是 pageCache 的填写过程是阻塞的,不符合异步要求。这个复制过程使用的就是 direct I/O.

内核态和用户态传输优化

前面提到的零拷贝旨在解决用户不需要感知并处理磁盘数据的场景,但是更多的场景下是需要处理数据的,那么怎么优化呢?

  • COW 写时拷贝
  • 缓冲区共享

1. COW 写时拷贝

主要思路:异步对共享缓存区进行读写

实现机制:基于虚拟内存重映射,并需要 MMU 硬件支持标记内存页状态,尝试在共享页写入数据时再分配一份物理内存并复制数据。

适用场景:读多写少

实际应用:Redis

2. 缓冲区共享

主要思路:避免过多的虚拟地址转换 和 TLB刷新的开销

TLB: TLB是Translation Lookaside Buffer的简称,可翻译为"地址转换后援缓冲器",也可简称为"快表"。简单地说,TLB就是页表的Cache,其中存储了当前最可能被访问到的页表项,其内容是部分页表项的一个副本。只有在TLB无法完成地址翻译任务时,才会到内存中查询页表,这样就减少了页表查询导致的处理器性能下降

实现机制: 定义一些缓存空间,会被同时映射到用户内存空间和内核内存空间,解决存储一致性问题

大多还处于实验阶段

总结

大致总结一下本文聊的问题和解决方案:

  • 同步复制阻塞CPU -> DMA
  • 减少复制过程中的上下文切换和复制开销 -> 零拷贝
  • 大文件传输不能使用零拷贝 -> 异步I/O + direct I/O
  • 必须需要上下文切换和复制数据,想降低传输成本 -> COW

其中读写数据的性能开销汇总如下表:

参考文档

从Linux零拷贝深入了解Linux-I/O

相关推荐
刘大猫266 小时前
二、搭建MyBatis采用xml方式,验证CRUD(增删改查操作)
操作系统·自动化运维·设计
别说我什么都不会7 小时前
使用Multipass编译OpenHarmony工程
操作系统·嵌入式·harmonyos
别说我什么都不会9 小时前
鸿蒙轻内核M核源码分析系列二一 05 文件系统FatFS
操作系统·嵌入式·harmonyos
砖厂小工14 小时前
Compose Performance Review
性能优化·android jetpack
银色火焰战车14 小时前
基于编译器特性浅析C++程序性能优化
开发语言·c++·重构·系统架构·操作系统
Python数据分析与机器学习16 小时前
《基于锂离子电池放电时间常数的自动化电量评估系统设计》k开题报告
运维·性能优化·自动化·软件工程·软件构建·个人开发
weixin_748877001 天前
【2025年后端开发终极指南:云原生、AI融合与性能优化实战】
人工智能·云原生·性能优化
NoneCoder1 天前
工程化与框架系列(22)--前端性能优化(中)
前端·性能优化·状态模式
大模型铲屎官1 天前
Python 性能优化:从入门到精通的实用指南
开发语言·人工智能·pytorch·python·性能优化·llm·编程