吉林沈阳网站建设,织梦做网站简单吗,视频制作公司需要什么资质,青岛建设管理局网站本文将从文件传输场景以及零拷贝技术深究 Linux I/O 的发展过程、优化手段以及实际应用。前言
存储器是计算机的核心部件之一#xff0c;在完全理想的状态下#xff0c;存储器应该要同时具备以下三种特性#xff1a;
速度足够快#xff1a;存储器的存取速度应当快于 CPU …本文将从文件传输场景以及零拷贝技术深究 Linux I/O 的发展过程、优化手段以及实际应用。前言
存储器是计算机的核心部件之一在完全理想的状态下存储器应该要同时具备以下三种特性
速度足够快存储器的存取速度应当快于 CPU 执行一条指令这样 CPU 的效率才不会受限于存储器容量足够大容量能够存储计算机所需的全部数据价格足够便宜价格低廉所有类型的计算机都能配备。
但是现实往往是残酷的我们目前的计算机技术无法同时满足上述的三个条件于是现代计算机的存储器设计采用了一种分层次的结构 从顶至底现代计算机里的存储器类型分别有寄存器、高速缓存、主存和磁盘 这些存储器的速度逐级递减而容量逐级递增。
存取速度最快的是寄存器 因为寄存器的制作材料和 CPU 是相同的所以速度和 CPU 一样快CPU 访问寄存器是没有时延的然而因为价格昂贵因此容量也极小一般 32 位的 CPU 配备的寄存器容量是 32✖️32 Bit64 位的 CPU 则是 64✖️64 Bit不管是 32 位还是 64 位寄存器容量都小于 1 KB且寄存器也必须通过软件自行管理。
第二层是高速缓存 也即我们平时了解的 CPU 高速缓存 L1、L2、L3 一般 L1 是每个 CPU 独享L3 是全部 CPU 共享而 L2 则根据不同的架构设计会被设计成独享或者共享两种模式之一比如 Intel 的多核芯片采用的是共享 L2 模式而 AMD 的多核芯片则采用的是独享 L2 模式。
第三层则是主存 也即主内存通常称作随机访问存储器Random Access Memory, RAM。是与 CPU 直接交换数据的内部存储器。它可以随时读写刷新时除外而且速度很快通常作为操作系统或其他正在运行中的程序的临时资料存储介质。
至于磁盘 则是图中离用户最远的一层了读写速度相差内存上百倍另一方面自然针对磁盘操作的优化也非常多如零拷贝**、** direct I/O**、** 异步 I/O 等等这些优化的目的都是为了提高系统的吞吐量另外操作系统内核中也有磁盘高速缓存区、PageCache、TLB等可以有效的减少磁盘的访问次数。
现实情况中大部分系统在由小变大的过程中最先出现瓶颈的就是I/O尤其是在现代网络应用从 CPU 密集型转向了 I/O 密集型的大背景下I/O越发成为大多数应用的性能瓶颈。
传统的 Linux 操作系统的标准 I/O 接口是基于数据拷贝操作的即 I/O 操作会导致数据在操作系统内核地址空间的缓冲区和用户进程地址空间定义的缓冲区之间进行传输。设置缓冲区最大的好处是可以减少磁盘 I/O 的操作 如果所请求的数据已经存放在操作系统的高速缓冲存储器中那么就不需要再进行实际的物理磁盘 I/O 操作然而传统的 Linux I/O 在数据传输过程中的数据拷贝操作深度依赖 CPU 也就是说 I/O 过程需要 CPU 去执行数据拷贝的操作因此导致了极大的系统开销限制了操作系统有效进行数据传输操作的能力。
这篇文章就从文件传输场景以及零拷贝 技术深究 Linux I/O的发展过程、优化手段以及实际应用。 需要了解的词
DMA DMA全称 Direct Memory Access即直接存储器访问是为了避免 CPU 在磁盘操作时承担过多的中断负载而设计的在磁盘操作中CPU 可将总线控制权交给 DMA 控制器由 DMA 输出读写命令直接控制 RAM 与 I/O 接口进行 DMA 传输无需 CPU 直接控制传输也没有中断处理方式那样保留现场和恢复现场过程使得 CPU 的效率大大提高。MMU Memory Management Unit—内存管理单元主要实现 竞争访问保护管理需求 需要严格的访问保护动态管理哪些内存页/段或区为哪些应用程序所用。这属于资源的竞争访问管理需求高效的翻译转换管理需求 需要实现快速高效的映射翻译转换否则系统的运行效率将会低下高效的虚实内存交换需求 需要在实际的虚拟内存与物理内存进行内存页/段交换过程中快速高效。Page Cache 为了避免每次读写文件时都需要对硬盘进行读写操作Linux 内核使用 页缓存Page Cache 机制来对文件中的数据进行缓存。此外由于读取磁盘数据的时候需要找到数据所在的位置但是对于机械磁盘来说就是通过磁头旋转到数据所在的扇区再开始「顺序」读取数据但是旋转磁头这个物理动作是非常耗时的为了降低它的影响PageCache 使用了「预读功能」 。比如假设 read 方法每次只会读 32 KB 的字节虽然 read 刚开始只会读 0 32 KB 的字节但内核会把其后面的 32 64 KB 也读取到 PageCache这样后面读取 32 64 KB 的成本就很低如果在 32 64 KB 淘汰出 PageCache 前有进程读取到它了收益就非常大。
虚拟内存 在计算机领域有一句如同摩西十诫般神圣的哲言计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决 从内存管理、网络模型、并发调度甚至是硬件架构都能看到这句哲言在闪烁着光芒而虚拟内存则是这一哲言的完美实践之一。虚拟内存为每个进程提供了一个一致的、私有且连续完整的内存空间 所有现代操作系统都使用虚拟内存使用虚拟地址取代物理地址主要有以下几点好处利用上述的第一条特性可以优化可以把内核空间和用户空间的虚拟地址映射到同一个物理地址 这样在 I/O 操作时就不需要来回复制了。 多个虚拟内存可以指向同一个物理地址虚拟内存空间可以远远大于物理内存空间应用层面可管理连续的内存空间减少出错。NFS 文件系统 网络文件系统是 FreeBSD 支持的文件系统中的一种也被称为 NFSNFS 允许一个系统在网络上与它人共享目录和文件通过使用 NFS用户和程序可以象访问本地文件 一样访问远端系统上的文件。Copy-on-write 写入时复制Copy-on-writeCOW是一种计算机程序设计领域的优化策略。其核心思想是如果有多个调用者callers同时请求相同资源如内存或磁盘上的数据存储他们会共同获取相同的指针指向相同的资源直到某个调用者试图修改资源的内容时系统才会真正复制一份专用副本private copy给该调用者而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的。此作法主要的优点是如果调用者没有修改该资源就不会有副本private copy被创建 因此多个调用者只是读取操作时可以共享同一份资源。资料直通车Linux内核源码技术学习路线视频教程内核源码 学习直通车Linux内核源码内存调优文件系统进程管理设备驱动/网络协议栈 为什么要有 DMA
在没有 DMA 技术前I/O 的过程是这样的
CPU 发出对应的指令给磁盘控制器然后返回磁盘控制器收到指令后于是就开始准备数据会把数据放入到磁盘控制器的内部缓冲区中然后产生一个中断 CPU 收到中断信号后停下手头的工作接着把磁盘控制器的缓冲区的数据一次一个字节地读进自己的寄存器然后再把寄存器里的数据写入到内存而在数据传输的期间 CPU 是被阻塞的状态无法执行其他任务。整个数据的传输过程都要需要 CPU 亲自参与拷贝数据而且这时 CPU 是被阻塞的简单的搬运几个字符数据那没问题但是如果我们用千兆网卡或者硬盘传输大量数据的时候都用 CPU 来搬运的话肯定忙不过来。
计算机科学家们发现了事情的严重性后于是就发明了 DMA 技术也就是直接内存访问Direct Memory Access 技术。
简单理解就是在进行 I/O 设备和内存的数据传输的时候数据搬运的工作全部交给 DMA 控制器而 CPU 不再参与任何与数据搬运相关的事情这样 CPU 就可以去处理别的事务 。
具体流程如下图 用户进程调用 read 方法向操作系统发出 I/O 请求请求读取数据到自己的内存缓冲区中进程进入阻塞状态操作系统收到请求后进一步将 I/O 请求发送 DMA释放 CPUDMA 进一步将 I/O 请求发送给磁盘磁盘收到 DMA 的 I/O 请求把数据从磁盘读取到磁盘控制器的缓冲区中当磁盘控制器的缓冲区被读满后向 DMA 发起中断信号告知自己缓冲区已满DMA 收到磁盘的信号将磁盘控制器缓冲区中的数据拷贝到内核缓冲区中此时不占用 CPUCPU 依然可以执行其它事务 当 DMA 读取了足够多的数据就会发送中断信号给 CPUCPU 收到 中断信号将数据从内核拷贝到用户空间系统调用返回。
在有了 DMA 后整个数据传输的过程CPU 不再参与与磁盘交互的数据搬运工作而是全程由 DMA 完成但是 CPU 在这个过程中也是必不可少的因为传输什么数据从哪里传输到哪里都需要 CPU 来告诉 DMA 控制器。
早期 DMA 只存在在主板上如今由于 I/O 设备越来越多数据传输的需求也不尽相同所以每个 I/O 设备里面都有自己的 DMA 控制器。
传统文件传输的缺陷
有了 DMA 后我们的磁盘 I/O 就一劳永逸了吗并不是的拿我们比较熟悉的下载文件举例服务端要提供此功能比较直观的方式就是将磁盘中的文件读出到内存再通过网络协议发送给客户端。
具体的 I/O 工作方式是数据读取和写入是从用户空间到内核空间来回复制而内核空间的数据是通过操作系统层面的 I/O 接口从磁盘读取或写入。
代码通常如下一般会需要两个系统调用
read(file, tmp_buf, len)
write(socket, tmp_buf, len)
代码很简单虽然就两行代码但是这里面发生了不少的事情 这其中有
4 次用户态与内核态的上下文切换 两次系统调用 read() 和 write()中每次系统调用都得先从用户态切换到内核态 等内核完成任务后再从内核态切换回用户态上下文切换 的成本并不小一次切换需要耗时几十纳秒到几微秒在高并发场景下很容易成为性能瓶颈参考线程切换和协程切换的成本差别 。4 次数据拷贝 两次由 DMA 完成拷贝另外两次则是由 CPU 完成拷贝我们只是搬运一份数据结果却搬运了 4 次过多的数据拷贝无疑会消耗 额外的资源大大降低了系统性能。
所以要想提高文件传输的性能就需要减少用户态与内核态的上下文切换 和内存拷贝 的次数。
如何优化传统文件传输
减少「用户态与内核态的上下文切换」
读取磁盘数据的时候之所以要发生上下文切换这是因为用户空间没有权限操作磁盘或网卡内核的权限最高这些操作设备的过程都需要交由操作系统内核来完成所以一般要通过内核去完成某些任务的时候就需要使用操作系统提供的系统调用函数。
而一次系统调用必然会发生 2 次上下文切换首先从用户态切换到内核态当内核执行完任务后再切换回用户态交由进程代码执行。
减少「数据拷贝」次数
前面提到传统的文件传输方式会历经 4 次数据拷贝但很明显的可以看到从内核的读缓冲区拷贝到用户的缓冲区 和从用户的缓冲区里拷贝到 socket 的缓冲区 」这两步是没有必要的。
因为在下载文件或者说广义的文件传输场景中我们并不需要在用户空间对数据进行再加工 所以数据并不需要回到用户空间中。
零拷贝
那么零拷贝 技术就应运而生了它就是为了解决我们在上面提到的场景——跨过与用户态交互的过程直接将数据从文件系统移动到网络接口而产生的技术。
零拷贝实现原理
零拷贝技术实现的方式通常有 3 种
mmap writesendfilesplice
mmap write
在前面我们知道read() 系统调用的过程中会把内核缓冲区的数据拷贝到用户的缓冲区里于是为了省去这一步我们可以用 mmap() 替换 read() 系统调用函数伪代码如下
buf mmap(file, len)
write(sockfd, buf, len)
mmap的函数原型如下
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
mmap() 系统调用函数会在调用进程的虚拟地址空间中创建一个新映射直接把内核缓冲区里的数据「映射 」到用户空间这样操作系统内核与用户空间就不需要再进行任何的数据拷贝操作。 具体过程如下
应用进程调用了 mmap() 后DMA 会把磁盘的数据拷贝到内核的缓冲区里应用进程跟操作系统内核「共享」这个缓冲区应用进程再调用 write()操作系统直接将内核缓冲区的数据拷贝到 socket 缓冲区中这一切都发生在内核态由 CPU 来搬运数据最后把内核的 socket 缓冲区里的数据拷贝到网卡的缓冲区里这个过程是由 DMA 搬运的。
我们可以看到通过使用 mmap() 来代替 read() 可以减少一次数据拷贝的过程。
但这还不是最理想的零拷贝因为仍然需要通过 CPU 把内核缓冲区的数据拷贝到 socket 缓冲区里且仍然需要 4 次上下文切换因为系统调用还是 2 次。
sendfile
在 Linux 内核版本 2.1 中提供了一个专门发送文件的系统调用函数 sendfile()如下
#include sys/socket.h
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
它的前两个参数分别是目的端和源端的文件描述符后面两个参数是源端的偏移量和复制数据的长度返回值是实际复制数据的长度。
首先它可以替代前面的 read() 和 write() 这两个系统调用这样就可以减少一次系统调用也就减少了 2 次上下文切换的开销。
其次该系统调用可以直接把内核缓冲区里的数据拷贝到 socket 缓冲区里不再拷贝到用户态这样就只有 2 次上下文切换和 3 次数据拷贝。如下图 带有 scatter/gather 的 sendfile 方式
Linux 2.4 内核进行了优化提供了带有 scatter/gather 的 sendfile 操作这个操作可以把最后一次 CPU COPY 去除。其原理就是在内核空间 Read BUffer 和 Socket Buffer 不做数据复制而是将 Read Buffer 的内存地址、偏移量记录到相应的 Socket Buffer 中这样就不需要复制。其本质和虚拟内存的解决方法思路一致就是内存地址的记录。
你可以在你的 Linux 系统通过下面这个命令查看网卡是否支持 scatter-gather 特性
$ ethtool -k eth0 | grep scatter-gather
scatter-gather: on
于是从 Linux 内核 2.4 版本开始起对于支持网卡支持 SG-DMA 技术的情况下 sendfile() 系统调用的过程发生了点变化具体过程如下
第一步通过 DMA 将磁盘上的数据拷贝到内核缓冲区里第二步缓冲区描述符和数据长度传到 socket 缓冲区这样网卡的 SG-DMA 控制器就可以直接将内核缓存中的数据拷贝到网卡的缓冲区里此过程不需要将数据从操作系统内核缓冲区拷贝到 socket 缓冲区中这样就减少了一次数据拷贝
所以这个过程之中只进行了 2 次数据拷贝如下图 splice 方式
splice 调用和sendfile 非常相似用户应用程序必须拥有两个已经打开的文件描述符一个表示输入设备一个表示输出设备。与sendfile不同的是splice允许任意两个文件互相连接而并不只是文件与socket进行数据传输。对于从一个文件描述符发送数据到socket这种特例来说一直都是使用sendfile系统调用而splice一直以来就只是一种机制它并不仅限于sendfile的功能。也就是说 sendfile 是 splice 的一个子集。
splice() 是基于 Linux 的管道缓冲区 (pipe buffer) 机制实现的所以splice()的两个入参文件描述符要求必须有一个是管道设备。
使用 splice() 完成一次磁盘文件到网卡的读写过程如下
用户进程调用 pipe()从用户态陷入内核态创建匿名单向管道pipe() 返回上下文从内核态切换回用户态用户进程调用 splice()从用户态陷入内核态DMA 控制器将数据从硬盘拷贝到内核缓冲区从管道的写入端拷贝进管道splice()返回上下文从内核态回到用户态用户进程再次调用 splice()从用户态陷入内核态内核把数据从管道的读取端拷贝到socket缓冲区DMA 控制器将数据从socket缓冲区拷贝到网卡splice() 返回上下文从内核态切换回用户态。在 Linux 2.6.17 版本引入了 splice而在 Linux 2.6.23 版本中 sendfile 机制的实现已经没有了但是其 API 及相应的功能还在只不过 API 及相应的功能是利用了 splice 机制来实现的。
和 sendfile 不同的是splice 不需要硬件支持。
零拷贝的实际应用
Kafka
事实上Kafka 这个开源项目就利用了「零拷贝」技术从而大幅提升了 I/O 的吞吐率这也是 Kafka 在处理海量数据为什么这么快的原因之一。
如果你追溯 Kafka 文件传输的代码你会发现最终它调用了 Java NIO 库里的 transferTo 方法
Overridepublic
long transferFrom(FileChannel fileChannel, long position, long count) throws IOException {return fileChannel.transferTo(position, count, socketChannel);
}
如果 Linux 系统支持 sendfile() 系统调用那么 transferTo() 实际上最后就会使用到 sendfile() 系统调用函数。
Nginx
Nginx 也支持零拷贝技术一般默认是开启零拷贝技术这样有利于提高文件传输的效率是否开启零拷贝技术的配置如下
http {
...sendfile on
...
}
由于文章篇幅过长下文继续讲解