详解数据传输——零拷贝、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

相关推荐
love is sour2 小时前
深入浅出 jmap:Java 内存分析的“显微镜“
java·开发语言·测试工具·性能优化
DemonAvenger3 小时前
Redis与MySQL双剑合璧:缓存更新策略与数据一致性保障
数据库·redis·性能优化
武子康5 小时前
Java-205 RabbitMQ 工作模式实战:Work Queue 负载均衡 + fanout 发布订阅(手动ACK/QoS/临时队列)
java·性能优化·消息队列·系统架构·rabbitmq·java-rabbitmq·mq
云边有个稻草人6 小时前
金仓数据库全链路性能优化:从SQL到存储的效率提升方案
性能优化·国产数据库·金仓数据库·kes
-大头.6 小时前
SQL性能优化与索引策略实战
数据库·sql·性能优化
郝学胜-神的一滴7 小时前
Linux C++ 守护进程开发指南
linux·运维·服务器·开发语言·c++·程序人生·性能优化
子非愚7 小时前
Linux系统调用实现原理(基于ARM 64, kernel-6.6)
操作系统
chasten087 小时前
Android开发wsl直接使用adb方法
操作系统
福大大架构师每日一题8 小时前
ollama v0.13.4 发布——全新模型与性能优化详解
stm32·嵌入式硬件·性能优化·ollama
拾忆,想起8 小时前
设计模式三大分类完全解析:构建高质量软件的基石
xml·微服务·设计模式·性能优化·服务发现