什么是零拷贝?零拷贝其实是泛指减少 CPU 复制数据的技术。要讨论“减少”,必须得弄清楚原本为什么需要。
所以:想理解好零拷贝,重点还在于理解为什么需要拷贝,以及不同零拷贝技术的对比。想理解好 I/O 原理,必须先弄清楚数据结构。
掌握了下面这张图,了解各每个组件的作用,自然就理解了 I/O 及零拷贝。
本文重点在于总结 Linux I/O 相关知识之间的联系,帮助读者系统的了解相关原理,因此不会很深入地讨论某一个技术点。另外,本文只包含磁盘 I/O 部分及部分零拷贝内容,后续还会分享网络I/O,敬请期待《简述 Linux IO 原理及零拷贝(中)——网络 I/O》。
绿色的图形表示数据存储的位置,绿色的箭头则表示数据的复制。
图-1
从左到右,Linux IO 包含两部分:磁盘 IO 和网络 IO,这个大家都能理解。
从上到下,存储又被划分为三部分:用户空间、内核空间以及物理设备。
我们既然讲数据拷贝,肯定是按是否存在数据拷贝来划分的。内存和外存储是不同的硬件介质,他们之间相互访问自然是靠数据拷贝的,所以要分开。
另外,Linux 操作系统为了安全考虑,其内核管理了几乎所有的硬件设备,不允许用户进程直接访问。因此,逻辑上计算机被分为用户空间和内核空间(外设及其驱动是被划分在内核空间的)。
运行在用户空间的进程就是用户态,运行在内核空间的进程就是内核态。用户态的程序,访问不了内核空间的数据,所以就需要由内核态的进程把数据拷贝到用户态。
磁盘I/O
理解了上面讲的分层逻辑,我们拿 read 举例。
数据从硬盘拷贝到内核空间,再从内核空间拷贝到用户空间,这种 I/O 方式就是标准 I/O。
当线程多次访问同一份磁盘上的数据时,Linux 并不会傻傻地多次访问磁盘。因为磁盘相对内存来时实在是太慢了,为了减少读盘的次数,被加载在内核空间的那份数据会被重复使用(页缓存/Page cache)。
同理,线程的写入操作也会先写入页缓存中,在根据 Linux 的延迟写机制,写入到磁盘中。因此,标准 I/O 又被称作缓存 I/O 大多数文件系统的默认 I/O 操作都是缓存 I/O。
讲到了 Buffered I/O,就得讲一下 Linux 是怎么管理内核空间中那份数据缓存的。早期的 Linux,把缓存分为了两种:Page cache 和 Buffer cache。
Buffer cache 也叫块缓冲,是对物理磁盘上的一个磁盘块进行的缓冲。其大小为通常为1k,磁盘块也是磁盘的组织单位。设立 Buffer cache 的目的是为在程序多次访问同一磁盘块时,减少访问时间。
Page cache 也叫页缓冲或文件缓冲。是由好几个磁盘块构成,大小通常为4k,在64位系统上为8k。构成的几个磁盘块在物理磁盘上不一定连续,文件的组织单位为一页,也就是一个Page cache大小。
文件读取是由外存上不连续的几个磁盘块,到 Buffer cache,然后组成 Page cache,然后供给应用程序。
从图中可以看到 Page cache 是建立在文件系统(Ext4)之上的,因此其缓存的是逻辑数据 。Buffer cache 是建立在块层之上的,因此其缓存的是物理辑数据。
Linux 大约在2.4.10之后的 disk cache 只有 Page cache。而 Buffer cache 只是 Page cache 中的 buffer_head 描述符。
换句话说,Page cache 和 Buffer cache 已经合并了。
(所以图中 Buffer cache 是灰色的,为了更容易理解 IO 原理,黄色和灰色部分都可以不考虑了)
如果出现进程死,内核死,掉电这样事件发生。数据会丢失吗?
进程死:如果数据还处在 application cache 或 CLib cache 时候,数据会丢失。
内核死:即使进入了 page cache(完成了write),如果没有进行 sync 操作,数据还是会丢失。
掉电:进行了 sync,数据就一定写入了磁盘了吗?答案是:不一定。
注意到图-1中,磁盘旁边的绿色图形了吗?它表示的是磁盘上的缓存。写数据达到一个程度时才真正写入磁盘。
磁盘缓存在磁盘上就表现为一块 RAM 芯片。磁盘上必须有缓存,用来接收指令和数据,还被用来进行预读。磁盘缓存分为读缓存和写缓存。
读缓存是指,操作系统为已读取的文件数据,在内存较空闲的情况下留在内存空间中。当下次软件或用户再次读取同一文件时就不必重新从磁盘上读取,从而提高速度。
写缓存实际上就是将要写入磁盘的数据先保存于系统为写缓存分配的内存空间中。当保存到内存池中的数据达到一个程度时,便将数据保存到硬盘中。这样可以减少实际的磁盘操作,有效地保护磁盘免于重复的读写操作而导致的损坏,也能减少写入所需的时间。
所谓磁盘缓存的禁用是指的 Write Through 模式。即:磁盘收到写入指令和数据后,必须先将其写入盘片,然后才向控制器返回成功信号(实际还是先写入缓存,再写入盘片的),这样就相当于“禁用”了缓存。
对一致性要求高的应用,比如 DB/磁盘阵列,都会禁用磁盘写缓存的。
DMA 是我们最常见的数据传输方式,DMA 的技术细节并不是本文介绍的重点。下文通过对比不同数据传输方式的差别,帮助我们理解 DMA 对于提升性能的重要意义。Linux 提供了4种磁盘与主存之间的数据传输机制。
直接控制(程序 I/O )(Programmed I/O)
基于循环对 I/O 端口进行不断检测。CPU 和 I/O 设备只能串行工作,导致CPU的利用率相当低。
中断驱动方式(Interrupt)
当数据到达时,磁盘主动向 CPU 发起中断请求,由 CPU 自身负责数据的传输过程。由于数据中的每个字在存储器与 I/O 控制器之间的传输都必须经过 CPU,这就导致了中断驱动方式仍然会消耗较多的 CPU 时间。而且整个过程中,中断次数很多,导致太多次的上下文切换,所以性能很差。
DMA(直接内存访问)方式
DMA 是一种与 CPU 共享内存总线的设备。它可以代替 CPU,把数据从内存到设备之间进行拷贝。仅在传送一个或多个数据块的开始和结束时,才需 CPU 干预(发送 DMA 中断),整块数据的传送是在 DMA控制器的控制下完成的。
通道控制方式(不常见)
通道相当于一个功能简单的处理机。包含通道指令(读,写,控制,转移),并可执行用这些指令编写的通道程序。I/O 通道方式是 DMA 方式的加强,它可以进一步减少 CPU 的干预。即把对一个数据块的读(或写)为单位的干预,减少为对一组数据块的读(或写)及有关的控制和管理为单位的干预。同时,又可以实现 CPU、通道和 I/O 设备三者的并行操作,从而更有效地提高整个系统的资源利用率。
图-2
图-3
PIO,中断 IO 都属于 CPU 拷贝。通道控制,是 DMA 的增强,严格来讲属于 DMA 技术。
I/O 通道并不常见,而程序 I/O,中断 I/O 的效率又太低了。所以,我们常见的磁盘 I/O 和网络 I/O 都是 DMA 方式,后文中也不再提及其他传输机制。
在缓存 I/O 机制中,DMA 方式可以将数据直接从磁盘读到页缓存中,或者将数据从页缓存直接写回到磁盘上,而不能直接在应用程序地址空间和磁盘之间进行数据传输。这样的话,数据在传输过程中需要在应用程序地址空间和页缓存之间进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。
对于某些特殊的应用程序来说,避开操作系统内核缓冲区,而直接在应用程序地址空间和磁盘之间传输数据,会比使用操作系统内核缓冲区获取更好的性能,因此引入"Direct I/O"。
从2.6.0内核开始支持直接 I/O。
凡是通过直接 I/O 方式进行数据传输,数据均直接在用户地址空间的缓冲区和磁盘之间直接进行传输,完全不需要页缓存的支持。
进程在打开文件的时候设置对文件的访问模式为 O_DIRECT ,这样就等于告诉操作系统进程在接下来使用 read() 或者 write() 系统调用去读写文件的时候使用的是直接 I/O 方式,所传输的数据均不经过操作系统内核缓存空间。
使用直接 I/O 读写数据必须要注意缓冲区对齐( buffer alignment )。
从第一张图中可以看到,Direct I/O 跨过了文件系统,由块设备执行直接 I/O 提供的支持,因此 O_DIRECT 要求的对齐基本单位是底层块设备的逻辑块大小(Logical Block Size 也叫 Sector Size)。
最大的优点就是减少操作系统缓冲区和用户地址空间的拷贝次数。降低了 CPU 的开销,和内存带宽。对于某些应用程序来说简直是福音,将会大大提高性能。
直接 I/O 并不总能让人如意。直接 I/O 的开销也很大,应用程序没有控制好读写,将会导致磁盘读写的效率低下。磁盘的读写是通过磁头的切换到不同的磁道上读取和写入数据,如果需要写入数据在磁盘位置相隔比较远,就会导致寻道的时间大大增加,写入读取的效率大大降低。
有的文章把直接 I/O 解释成,数据直接跨过内核进行传输,容易造成误解。用户空间的进程是不允许访问内核空间的,因此 Direct I/O 本质是 DMA 设备把数据从用户空间拷贝到设备,或是从设备拷贝到用户空间。
不过事事总有例外,mmap 就是个特别的存在。
(文档化的定义)mmap 将一个文件或者其它对象映射进内存。普通文件被映射到进程地址空间后,进程可以像访问普通内存一样对文件进行访问,不必再调用read(),write()等操作。
offset 是文件中映射的起始位置,length 是映射的长度。
图-4
mmap 在用户空间映射调用系统中作用很大。
图-5
mmap内存映射过程:
进程在虚拟地址空间中为映射创建虚拟映射区域。
内核把文件物理地址和进程虚拟地址进行映射。
进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝。
换句话说,在调用 mmap 后,只是在进程的虚拟空间中分配了一段空间,真实的物理地址还不会分配的。当进程第一次访问这段空间(当作内存一样),CPU 陷入 OS 内核执行异常处理。然后异常处理会在这个时间分配物理内存,并用文件的内容填充这片内存,然后才返回进程的上下文,这时进程才会感知到这片内存里有数据。
忘掉文档化的定义。
mmap 本质是内存共享机制,它把 page cache 地址空间映射到用户空间,换句话说,mmap 是一种特殊的Buffered I/O。
因为底层有 CPU 的 MMU 支持,自然会转换到物理区域,对于进程而言是无感知。所以,磁盘数据加载到 page cache 后,用户进程可以通过指针操作直接读写 page cache,不再需要系统调用和内存拷贝。
因此,offset 必须是按 page size 对齐的(不对齐的话就会映射失败)。
mmap 映射区域大小必须是物理页大小(page size)的整倍数(32位系统中通常是4k)。length 对齐是靠内核来保证的,比如文件长度是10KB,你映射了5KB,那么内核会将其扩充到8KB。
图-6
了解 mmap 内存映射原理,有助于了解其性能。
mmap 减少了数据在内核空间与用户空间的复制,从这个角度讲,提高了文件读取效率。但是,mmap 在数据加载到 page cache 的过程中,会触发大量的 page fault 和建立页表映射的操作,开销并不小。
另一方面,随着硬件性能的发展,内存拷贝消耗的时间已经大大降低了。所以很多情况下,mmap 的性能反倒是比不过 read 和 write 的。
有的文章把 mmap 形容成"跨过了页缓存",因此减少了数据的拷贝次数。这种表述方式是错误的。
mmap 可分为共享映射和私有映射两种。
共享映射,修改对所有进程可见。也就是说,如果进程 A 修改了其中某个 page 上的数据,进程 B 之后读取这个 page 得到的就是修改后的内容。
私有映射,进程 A 的修改对进程 B 是不可见的,都是同一份数据,这是如何做到的呢?这里利用的是 Copy On Write(COW) 机制。
当进程 A 试图修改某个 page 上的数据时,内核会将这个 page 的内容拷贝一份。之后 A 的写操作实际是在这个拷贝的 page 上进行的(进程 A 中对应这个page的页表项也需要被修改,以指向新拷贝的 page),这样进程 B 看到的这个 page 还是原来未经改动的。这种修改只会存在于内存中,不会同步到外部的磁盘文件上(事实上也没法同步,因为不同进程所做的修改是不同的)。
Page fault(页缺失,又名缺页中断,缺页异常),属于硬件中断,是由中央处理器的内存管理单元(MMU)所发出的中断。
结婚后,你把工资卡上交给了老婆。你就是用户态,只能管理自己的钱包。你老婆就是内核态,她可以访问银行,管理自己的钱包,也包括你的钱包。
缓存 I/O
你需要钱的时候跟老婆申请。你老婆会从自己的钱包里把钱放到你的钱包。如果她的钱包里也没钱了,她自己会去银行取钱。
直接 I/O
你最近用钱比较频繁,需要钱的时候同样需要跟老婆申请。你老婆去银行取了钱,直接放到了你的钱包里。
mmap
你老婆允许你从她的钱包里拿钱。你老婆钱包里没钱的时候,她自己会去银行取钱。
CPU 拷贝/ DMA 拷贝
你丈母娘都是自己去银行取钱的,这就是 CPU 拷贝。你老婆要忙着追剧,她懒得去取钱,让银行把钱送你家来,这就是 DMA 拷贝。实际上你老婆一直比较忙,她从来没去过银行,一直是银行把钱送你家来的。
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8