在 Linux之文件系统前世今生(一) VFS中,我们提到了文件的读写,并给出了简要的读写示意图,本文将分析文件I/O的细节。
一、Buffered I/O(缓存I/O)& Directed I/O(直接I/O)
1.1、Page Cache
我们读写一个文件时,会从磁盘加载文件到内存中,以便我们快速读写文件;我们把内存中用于缓存文件的这块区域记为 Page Cache
,Page Cache 位于内核态
(所以也叫OS cache)。
- page 是内存管理分配的基本单位,
Page Cache
由多个 page 构成;- page 在操作系统中通常为 4KB 大小,而
Page Cache
的大小则为 4KB 的整数倍;- 更多 page 细节参见 Linux之内存管理前世今生(一)。
1.2、预读
根据程序的局部性原理,加载文件时除了加载文件指定位置内容,同时会加载该位置后续一部分连续内容到内存中,这个机制就是预读。所以 Page Cache
中额外包含了程序后续可能读写的内容。
1.2.1、Page Cache + 预读优势
- 加速数据访问
由于内存访问比磁盘访问快的多,且预读了后续数据;
- 提高系统磁盘I/O吞吐量
通过一次 I/O 将多个 page 装入
Page Cache
能够减少磁盘 I/O 次数, 进而提高系统磁盘 I/O 吞吐量;
1.3、Write back(写回)& Write Through(写穿)
由于我们在内核态引入的Page Cache
机制,所以我们对文件的读写都是基于Page Cache
,但文件最终还是需要持久化到磁盘中去的。Linux 提供两种策略将Page Cache
中 脏页(dirty page) 刷回磁盘:
Write back
(写回)- 内核线程周期性地将脏页刷回磁盘,Linux 默认采用此策略 ;
- 该策略存在数据丢失的风险(比如遇到系统宕机、断电),理论上操作系统不宕机,数据就保证会刷回磁盘,即使用户程序崩溃;
Write Through
(写穿)- 向用户层提供特定接口,应用程序可主动调用接口来直接刷新数据到磁盘;
- 以牺牲系统 I/O 吞吐量作为代价,向上层应用确保一旦写入,数据就已经落盘,不会丢失;
1.3.1、Page Cache刷盘涉及的系统调用
Write back
(写回)& Write Through
(写穿)这两种写策略均依赖系统调用,分为如下3种:
sync()
将所有修改过的缓冲区排入写队列,然后就返回了,它并不等实际的写磁盘的操作结束。所以它的返回并不能保证数据的安全性。通常会有一个update系统守护进程每隔30s调用一次sync。
fsync(fd)
- 将
fd
代表的文件的脏数据和文件属性全部刷新至磁盘中; - 确保一直到写磁盘操作结束才会返回。数据库一般使用
fsync
。
- 将
fdatasync(fd)
- 将
fd
代表的文件的脏数据刷新至磁盘,fdatasync的功能与fsync类似,但是仅仅在必要的情况下才会同步文件属性,因此可以减少一次IO写操作; - 举例来说,文件的尺寸(st_size)如果变化,是需要立即同步的,否则OS一旦崩溃,即使文件的数据部分已同步,由于文件属性没有同步,依然读不到修改的内容。而最后访问时间(atime)/修改时间(mtime)是不需要每次都同步的,只要应用程序对这两个时间戳没有苛刻的要求,基本无伤大雅。
- 将
1.3.2、Write back 刷盘时机
Page Cache
脏页数量超过设定阈值;Page Cache
脏页缓存超过设定缓存时间;- 应用程序主动刷盘,即调用
sync()
、fdatasync(fd)
、fsync(fd)
三者任一; - 物理内存分配告警;
1.4、Buffered I/O(缓存I/O)& Directed I/O(直接I/O)
- 前面我们在内核态引入了
Page Cache
用于加速文件I/O的操作就是 Buffered I/O(缓存I/O);
- 相反,如果在内核态关闭
Page Cache
的使用(通过参数O_DIRECT
),文件I/O直接与磁盘交互,我们称为Directed I/O(直接I/O)。
问题来了:Page Cache
这么好,什么场景需要关闭?
- Page Cache 位于内核态,对用户态提供的API灵活性差,用户态的应用程序无法对Page Cache 进行个性化定制,比如什么时间刷盘,刷哪些数据……
- Page Cache 容量受限,大文件读写时,很快会把Page Cache消耗完,导致之前缓存的常用的、热点数据被移出内存,下次访问热点数据时产生磁盘I/O,从而降低系统性能;即Page Cache 缓存的是小文件的热点数据。
- 举例:Mysql 中 InnoDB :
- Buffer Pool 关闭了Page Cache,即不在内核态缓存数据,直接在用户态缓存数据;
- redo log buffer 通过参数
innodb_flush_log_at_trx_commit
(取值为0,1,2)设置为2来开启 Page Cache。
二、Blocking I/O(阻塞I/O)& Non Blocking I/O(非阻塞I/O)
- 前面我们从
Page Cache
的维度,将 I/O分为 缓存I/O 和 直接I/O; - 接下来,我们从进程阻塞阶段的维度,将 I/O 分为 阻塞I/O 和 非阻塞I/O;
2.1、阻塞定义
阻塞 的主体是进程,当进程进入阻塞状态,是不占用CPU资源的。
2.2、阻塞时机
正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等,则由系统自动执行阻塞原语(Block)
,使当前进程由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,所以只有处于运行态(获得CPU)的进程,才可能将其转为阻塞状态。
2.3、阻塞I/O
由前面定义,I/O时期待的事件未发生,产生阻塞,那到底期待啥呢?
等待内核将数据准备好,换言之,等待 Page Cache
中有程序请求的数据。
以文件读取为例:当一个read
操作发生时,它会经历两个阶段:
第一阶段:等待数据准备 (Waiting for the data to be ready)。
第二阶段:将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)。
2.3.1、阻塞I/O vs 非阻塞I/O
当应用程序发起read
时,且Page Cache
中没有程序请求的数据时,内核会加载磁盘数据,若加载数据同时,
-
read调用立即返回告诉程序,数据没有准备好,这就是非阻塞I/O;
非阻塞 I/O 在I/O执行的第二个阶段仍然被阻塞了。
-
相反,内核闷声干活,直到数据加载完,并且数据从内核拷贝到应用程序中,才返回,这就是阻塞I/O。
阻塞 I/O 在I/O执行的两个阶段都被阻塞了。
三、同步 I/O(synchronous I/O)& 异步 I/O(asynchronous I/O)
POSIX(Portable Operating System Interface, 可移植操作系统接口)关于同步I/O和异步I/O的定义如下:
A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
An asynchronous I/O operation does not cause the requesting process to be blocked;
说人话就是,同步I/O会阻塞进程,异步I/O不会阻塞进程。
我们之前提到的 阻塞I/O 和 非阻塞I/O 都是同步I/O。
- 阻塞I/O 两个阶段都阻塞;
- 非阻塞I/O 第二个阶段阻塞;
四、小节
Page Cache
的维度,将 I/O分为 缓存I/O 和 直接I/O;- 进程阻塞阶段的维度,将 I/O 分为 阻塞I/O 和 非阻塞I/O;
- 进程阻塞的维度,将 I/O 分为 同步I/O 和 异步I/O。
文件 I/O 至此基本介绍完毕,后续会介绍网络 I/O。