吗?
7.2 进程写⽂件时,进程发⽣了崩溃,已写⼊的数据会丢失
吗?

因为进程在执⾏ write (使⽤缓冲 IO )系统调⽤的时候,实际上是将⽂件数据写到了内核的 page cache ,它是⽂件系统中⽤于缓存⽂件数据的缓冲,所以即使进程崩溃了,⽂件数据还是保留在内
核的 page cache ,我们读数据的时候,也是从内核的 page cache 读取,因此还是依然读的进程崩
溃前写⼊的数据。
内核会找个合适的时机,将 page cache 中的数据持 久化到磁盘。但是如果 page cache ⾥的⽂件
数据,在持久化到磁盘化到磁盘之前,系统发⽣了崩溃,那这部分数据就会丢失了。
当然, 我们也可以在程序⾥调⽤ fsync 函数,在写⽂⽂件的时候,⽴刻将⽂件数据持 久化到磁
盘,这样就可以解决系统崩溃导致的⽂件数据丢失的问题。
Page Cache 是什么 ?
为了 理解 Page Cache ,我们不妨先看⼀下 Linux 的⽂件 I/O 系统,如下图所⽰:

上图中,红⾊部分为 Page Cache 。可⻅ Page Cache 的本质是由 Linux 内核管理的内存区域。我们
通过 mmap 以及 buffered I/O 将⽂件读取到内存空间实际上都是读取到 Page Cache 中。
如何查看系统的 Page Cache ?
通过读取 /proc/meminfo ⽂件,能够实时获取系统内存情况:
$ cat /proc/meminfo
.. .Buffers: 1224 kB
Active: 6224232 kB
Inactive: 979432 kB
Active(anon): 6173036 kB
Inactive(anon): 927932 kB
Active(file): 51196 kB
Inactive(file): 51500 kB
...Shmem: 10000 kB
...SReclaimable: 43532 kB
...根据上⾯的数据,你可以简单得出这样的公式(等式两边之和都是 112696 KB ):
Buffers + Cached + SwapCached = Active(file) + Inactive(file) + Shmem + SwapCached两边等式都是 Page Cache ,即:
Page Cache = Buffers + Cached + SwapCached通过阅读下⾯的⼩节,就能够理解为什么 SwapCached 与 Buffers 也是 Page Cache 的⼀部分。
page 与 Page Cache
page 是内存管理分配的基本单位, Page Cache 由多个 page 构成。page 在操作系统中通常为
4KB ⼤⼩(32bits/64bits ),⽽ Page Cache 的⼤⼩则为 4KB 的整数 倍。
另⼀⽅⾯,并不是所有 page 都被组织 为 Page Cache 。
Linux 系统上供⽤⼾可访问的内存分为两个 类型,即:
File-backed pages :⽂件备份⻚也就是 Page Cache 中的 page ,对应于磁盘上的若⼲数据块;
对于这些⻚最⼤的问题是脏⻚回盘;
Anonymous pages :匿名⻚不对应磁盘上的任何 磁盘数据块,它们是进程的运⾏是内存空间
(例如⽅法栈、局部变量表等属性);
为什么 Linux 不把 Page Cache 称为 block cache ,这不是更好吗?
这是因为从 磁盘中加载到内存的数据不仅仅 放在 Page Cache 中,还放在 buffer cache 中。
Cached: 111472 kB SwapCached: 36364 kB Active: 6224232 kB Inactive: 979432 kB Active (anon ): 6173036 kB Inactive (anon ): 927932 kB Active (file ): 51196 kB Inactive (file ): 51500 kB.. .Shmem: 10000 kB
.. .SReclaimable: 43532 kB
.. .
Buffers + Cached + SwapCached = Active(file) + Inactive(file) + Shmem + SwapCached
Page Cache = Buffers + Cached + SwapCached例如通过 Direct I/O 技术的磁盘⽂件就不会进⼊ Page Cache 中。当然,这个问题也有 Linux 历史
设计 的原因,毕竟这只是⼀个称呼,含义随着 Linux 系统的演进也逐渐不同。
下⾯⽐较⼀下 File-backed pages 与 Anonymous pages 在 Swap 机制下的性能。
内存是⼀种珍惜资源,当内存不够⽤时,内存管理单元(Memory Mangament Unit )需要提供调
度算法来回收相关内 存空间。内存空间回收的⽅式通常就是 swap ,即交换到持久化存储设备上。
File-backed pages (Page Cache )的内存回收代价 较低。Page Cache 通常对应于⼀个⽂件上的若
⼲顺序块,因此可以通过顺序 I/O 的⽅式落盘。另⼀⽅⾯,如果 Page Cache 上没有进⾏写操作
(所谓的没有脏⻚),甚⾄不会将 Page Cache 回盘,因为数据的内容完 全可以通过再次读取磁盘
⽂件得到。
Page Cache 的主要难点在于脏⻚回盘,这个内容会在后⾯进⾏详细说明。
Anonymous pages 的内存回收代价 较⾼。这是因为 Anonymous pages 通常随机地写⼊持久化交
换设备。另⼀⽅⾯,⽆论是否有写操作,为了 确保数据不丢 失,Anonymous pages 在 swap 时必
须持久化到磁盘。
Swap 与缺⻚中断
Swap 机制指的是当物理内存不够⽤,内存管理单元(Memory Mangament Unit ,MMU )需要提
供调度算法来回收相关内 存空间,然后将清理出来的内存空间给当前内存申请⽅。
Swap 机制存在的本质原因是 Linux 系统提供了虚拟内存管理机制,每⼀个进程认为其独占内存空
间,因此所有进程的内存空间之和远远 ⼤于物理内存。所有进程的内存空间之和超过物理内存的
部分就需要交换到磁盘上。
操作系统以 page 为单位管理内存,当进程发现需要访问的数据不在内存时,操作系统可能会将数
据以⻚的⽅式加载到内存中。上述过程被称为缺⻚中断,当操作系统发⽣缺⻚中断时,就会通过
系统调⽤将 page 再次读到内存中。
但主内存的空间是有限的,当主内存中不 包含可 以使 ⽤的空间时,操作系统会从选择合适的物理
内存⻚驱逐回磁盘,为新的内存⻚让出位置,选择待驱逐⻚的过程在操作系统中叫做⻚⾯替换
(Page Replacement ),替换操作⼜会触发 swap 机制。
如果物理内存⾜够⼤,那么可能不需要 Swap 机制,但是 Swap 在这种情况下还是有⼀定优势:对
于有发⽣内存泄漏⼏率的应⽤程序(进程),Swap 交换分区更是重要,这可以确保内存泄露不⾄
于导致物理内存不够⽤,最终导致系统崩溃。但内存泄露会引起频繁的 swap ,此时⾮常影响操作
系统的性能。
Linux 通过⼀个 swappiness 参数来控制 Swap 机制:这个参数值可为 0-100 ,控制系统 swap 的优
先级:
⾼数值:较⾼频率的 swap ,进程不活跃时主动将其转换出物理内存。
低数值:较低频率的 swap ,这可以确保交互 式不因为内存空间频繁地交换到磁盘⽽提⾼响应延
迟。
最后,为什么 SwapCached 也是 Page Cache 的⼀部分?
这是因为当匿名⻚(Inactive(anon) 以及 Active(anon) )先被交换(swap out )到磁盘上后,然后再加载回(swap in )内存中,由于读⼊到内存后原来的 Swap File 还在,所以 SwapCached 也可
以认为是 File-backed page ,即属于 Page Cache 。这个过程如下图所⽰。

Page Cache 与 buffer cache
执⾏ free 命令,注意到会有两列名为 buffers 和 cached ,也有⼀⾏名为 “-/+ buffers/cache” 。
~ free -m
total used free shared buffers cached
Mem: 128956 96440 32515 0 5368 39900
-/+ buffers/cache: 51172 77784
Swap: 16002 0 16001
其中,cached 列表⽰当前的⻚缓存(Page Cache )占⽤量,buffers 列表⽰当前的块缓存(buffer cache )占⽤量。
⽤⼀句话来解释:Page Cache ⽤于缓存⽂件的⻚数据,buffer cache ⽤于缓存块设备(如磁盘)
的块数据。
⻚是逻辑上的概念,因此 Page Cache 是与⽂件系统同级的;
块是物理上的概念,因此 buffer cache 是与块设备驱动程序同级的。
~ free -m total used free shared buffers cached Mem: 128956 96440 32515 0 5368 39900 -/+ buffers/cache: 51172 77784 Swap: 16002 0 16001
Page Cache 与 buffer cache 的共同⽬的都是加速数据 I/O :
写数据时⾸先写 到缓存,将写⼊的⻚标记为 dirty ,然后向 外部存储 flush ,也就是缓存写机制中
的 write-back (另⼀种是 write-through ,Linux 默认情况下不 采⽤);
读数据时⾸先读取缓存,如果未 命中,再去外部存储读取,并且将读取来的数据也加⼊缓存。
操作系统总是积极地将所有空闲内存都⽤作 Page Cache 和 buffer cache ,当内存不够⽤时也会
⽤ LRU 等算 法淘汰缓存⻚。
在 Linux 2.4 版本的内核之前,Page Cache 与 buffer cache 是完全分离的。但是,块设备⼤多是磁
盘,磁盘上的数据⼜⼤多通过⽂件系统来组织 ,这种设计 导致很多数据被缓存了两 次,浪费内
存。
所以在 2.4 版本内核之后,两块缓存近似融合在了⼀起:如果⼀个⽂件的⻚加载到了 Page Cache ,那么同时 buffer cache 只需要维护块指向⻚的指针就可以了。只有那些没有⽂件表⽰的
块,或者绕过了⽂件系统直接操作(如dd 命令)的块,才会真正放到 buffer cache ⾥。
因此,我们现在提起 Page Cache ,基本上都同时指 Page Cache 和 buffer cache 两者,本⽂之后
也不 再区分,直接统称为 Page Cache 。
下图近似地⽰出 32-bit Linux 系统中可能的⼀种 Page Cache 结构,其中 block size ⼤⼩为 1KB ,
page size ⼤⼩为 4KB 。

Page Cache 中的每个⽂件都是⼀棵基数树(radix tree ,本质上是多叉搜索树),树的每个节点都
是⼀个⻚。根据⽂件内的偏移量就可以快速定位到所在的⻚,如下图所⽰。关于基数树的原理可
以参⻅英⽂维基,这⾥就不细说了。

Page Cache 与预读
操作系统为基于 Page Cache 的读缓存机制提供预读机制(PAGE_READAHEAD ),⼀个例⼦是:
⽤⼾线程仅仅 请求读取磁盘上⽂件 A 的 offset 为 0-3KB 范围内的数据,由于磁盘的 基本读写单
位为 block (4KB ),于是操作系统⾄少会读 0-4KB 的内容,这恰好可以在⼀个 page 中装下。
但是操作系统出于局部性原理会选择将磁盘块 offset [4KB,8KB) 、[8KB,12KB) 以及 [12KB,16KB)都加载到内存,于是额外在内存中申请了 3 个 page ;
下图代表了操作系统的预读机制:

上图中,应⽤程序利⽤ read 系统调动读取 4KB 数据,实际上内核使⽤ readahead 机制完成了
16KB 数据的读取。
Page Cache 与⽂件持久化的⼀致性&可靠性
现代 Linux 的 Page Cache 正如其名,是对磁盘上 page (⻚)的内存缓存,同时可以⽤于读/写操
作。
任何 系统引⼊缓存,就会引发⼀致性问题:内存中的数据与磁盘中的数据不⼀致,例如常⻅后端
架构 中的 Redis 缓存与 MySQL 数据库就存在⼀致性问题。
Linux 提供多种机制来保证数据⼀致性,但⽆论是单机上的内存与磁盘⼀致性,还是分布式组件中
节点 1 与节点 2 、节点 3 的数据⼀致性问题,理解的关键是 trade-off :吞吐 量与数据⼀致性保证
是⼀对⽭盾。
⾸先,需要我们理解⼀下⽂件的数据。⽂件 = 数据 + 元数据。元数据⽤来描述⽂件的各种属性,也必须存储在磁盘上。因此,我们说保证⽂件⼀致性其实包含了两个 ⽅⾯:数据⼀致+元数据⼀
致。
⽂件的元数据包括:⽂件⼤⼩、创建时间、访问时间、属主属组等信息。
我们考虑如下⼀致性问题:如果发⽣写操作并且对应的数据在 Page Cache 中,那么写操作就会直
接作⽤于 Page Cache 中,此时如果数据还没刷新到磁盘,那么内存中的数据就领先于磁盘,此时
对应 page 就被称为 Dirty page 。
当前 Linux 下以两种⽅式实现⽂件⼀致性:
- Write Through (写穿):向⽤⼾层提供特定接⼝,应⽤程序可主动调⽤接⼝来保证⽂件⼀致
性;
- Write back (写回):系统中存在定期任务(表现形式 为内核线程),周期性地同步⽂件系统中
⽂件脏数据块,这是默认的 Linux ⼀致性⽅案;
上述两种⽅式最终都依赖于系统调⽤,主要分为如下三 种系统调⽤:
⽅法 含义
fsync(i ntfd) fsync(fd) :将 fd 代表的⽂件的脏数据和脏元数据全部刷新⾄磁盘中。⽅法 含义
fdatasy nc(int fd)
fdatasync(fd) :将 fd 代表的⽂件的脏数据刷新⾄磁盘,同时对必要的元数据刷新⾄磁盘中,这⾥所说的必要的概念是指:对接下来访问⽂件有关键作⽤的信息,
如⽂件⼤⼩,⽽⽂件修改时间等不属于必要信息
sync() sync() :则是对系统中所有的脏的⽂件数据元数据刷新⾄磁盘中上述三种系统调⽤可以分别 由⽤⼾进程与内核进程发起。下⾯我们研究⼀下内核线程的相关特
性。
- 创建的针对回写任务的内核线程数由系统中持久存储设备决定,为每个存储设备创建单独的刷
新线程;
- 关于多线程的架构 问题,Linux 内核采取了 Lighthttp 的做法,即系统中存在⼀个管理线程和多
个刷新线程(每个持久存储设备对应⼀个刷新线程)。管理线程监控设备上的脏⻚⾯情况,若设
备⼀段时间内没有产⽣脏⻚⾯,就销毁设备上的刷新线程;若监测到设备上有脏⻚⾯需要回写
且尚未为该设 备创建刷新线程,那么创建刷新线程处理脏⻚⾯回写。⽽刷新线程的任务较为单
调,只负责 将设备中的脏⻚⾯回写⾄持久存储设备中。
- 刷新线程刷新设备上脏⻚⾯⼤致设计 如下:
每个设备保存脏⽂件链表,保存的是该设 备上存储的脏⽂件的 inode 节点。所谓的回写⽂件
脏⻚⾯即回写该 inode 链表上的某些⽂件的脏⻚⾯;
系统中存在多个回写时机,第⼀是应⽤程序主动调⽤回写接⼝(fsync ,fdatasync 以及 sync
等),第⼆管理线程周期性地唤醒设备上的回写线程进⾏回写,第三是某些应⽤程序/内核任
务发现内存不⾜时要回收部分缓存⻚⾯⽽事先进⾏脏⻚⾯回写,设计 ⼀个统⼀的框架来 管理
这些回写任务⾮常有必要。
Write Through 与 Write back 在持久化的可靠性上有所不同:
Write Through 以牺牲 系统 I/O 吞吐 量作为代价 ,向上层应⽤确保⼀旦写⼊,数据就已经落盘,
不会丢失;
Write back 在系统发⽣宕机的情况下⽆法确保数据已经落盘,因此存在数据丢失的问题。不
过,在程序挂了,例如被 kill -9 ,Page Cache 中的数据操作系统还是会确保落盘;
Page Cache 的优劣势
Page Cache 的优势
- 加快数据访问
如果数据能够在内存中进⾏缓存,那么下 ⼀次访问就不需要通过磁盘 I/O 了,直接命中内存缓存
即可。
由于内存访问⽐磁盘访问快很 多,因此加快数据访问是 Page Cache 的⼀⼤优势。
- 减少 I/O 次数,提⾼系统磁盘 I/O 吞吐 量
得益于 Page Cache 的缓存以及预读能⼒,⽽程序⼜往往 符合局部性原理,因此通过⼀次 I/O 将多
个 page 装⼊ Page Cache 能够减少磁盘 I/O 次数, 进⽽提⾼系统磁盘 I/O 吞吐 量。
Page Cache 的劣势
page cache 也有其劣势 ,最直接的缺点是需要占⽤额外物理内存空间,物理内存在⽐较紧俏的时
候可能会导致频繁的 swap 操作,最终导致系统的磁盘 I/O 负载的上升。
Page Cache 的另⼀个缺陷是对应⽤层并没有提供很好的管理 API ,⼏乎是透明管理。应⽤层即使
想优化 Page Cache 的使⽤策略也很难进⾏。因此⼀些应⽤选择在⽤⼾空间实现⾃⼰的 page 管
理,⽽不使⽤ page cache ,例如 MySQL InnoDB 存储引擎以 16KB 的⻚进⾏管理。
Page Cache 最后⼀个缺陷是在某些应⽤场景下⽐ Direct I/O 多⼀次磁盘读 I/O 以及磁盘写 I/O 。
Direct I/O 即直接 I/O 。其名字中的”直接”⼆字⽤于区分使⽤ page cache 机制的缓存 I/O 。
缓存⽂件 I/O :⽤⼾空间要读写⼀个⽂件并不直接与磁盘交互 ,⽽是中间夹了⼀层缓存,即
page cache ;
直接⽂件 I/O :⽤⼾空间读取的⽂件直接与磁盘交互 ,没有中间 page cache 层;
“直接”在这⾥还有另⼀层语义:其他所有技术中,数据⾄少需要在内核空间存储⼀份,但是在
Direct I/O 技术中,数据直接存储在⽤⼾空间中,绕过了内核。
Direct I/O 模式如下图所⽰:

此时⽤⼾空间直接通过 DMA 的⽅式与磁盘以及⽹卡进⾏数据拷 ⻉。
Direct I/O 的读写⾮常有特点:
Write 操作:由于其不使⽤ page cache ,所以其进⾏写⽂件,如果返回成功,数据就真的落盘
了(不考虑磁盘⾃带的缓存);
Read 操作:由于其不使⽤ page cache ,每次 读操作是真的从磁盘中读取,不会从⽂件系统的
缓存中读取。
