当前位置:首页>Linux>《深入Linux内核架构》读书笔记——Linux内核页缓存、块缓存与数据同步

《深入Linux内核架构》读书笔记——Linux内核页缓存、块缓存与数据同步

  • 2026-10-11 06:33:24
《深入Linux内核架构》读书笔记——Linux内核页缓存、块缓存与数据同步
Hello,大家好,我是程序媛MM。

本文约4100字,最近又遇到看起来像缓存与数据同步的问题了,读书解惑,今天来读一读《深入Linux内核架构》中文版第16章的内容,深入理解一下缓存和数据同步的知识。

关注公众号, 即可获得与Linux相关的电子书籍(含《深入Linux内核架构》中文版)以及常用开发工具,文末有文档清单。


前言

日常开发运维中我们经常遇到一类经典问题:程序调用write()写完文件后断电,数据直接丢失;日志写入后磁盘看不到最新内容;数据库批量写入性能极高但崩溃后出现数据截断。这一切的根源,都来自Linux内核的缓存机制与异步回写逻辑。

Linux为消除低速块设备(磁盘)带来的I/O瓶颈,设计了页缓存(Page Cache) 与块缓存(Buffer Cache) 双层缓存体系,配合pdflush后台回写线程完成脏数据持久化。缓存大幅提升读写吞吐量,但延迟写(延迟落盘)带来的数据丢失风险,是所有IO密集型业务必须解决的核心痛点。

本文基于《深入Linux内核架构》第16、17章核心原理,分析缓存底层实现、脏页同步完整流程,并整理日常常规情况下,文件不及时落盘的解决方案。

一 Linux两大缓存体系:页缓存与块缓存

1.1 缓存设计核心目的

磁盘IO速度比内存低数万倍,内核利用空闲物理内存缓存块设备数据,实现两大收益:

  1. 读加速:热点数据常驻内存,重复读取无需访问磁盘;
  2. 写合并:多次零散写操作在内存合并为批量IO,减少磁盘寻道,提升整体性能。

缓存采用延迟写机制:用户调用write仅修改内存缓存,不会立刻同步磁盘;脏页(内存与磁盘不一致的数据)由后台线程定时/定量刷盘。但延迟写存在致命缺陷:断电、内核崩溃时,未回写脏页直接丢失。

缓存分为两类:通用页缓存、细粒度块缓存,二者互补协同工作。

1.2 页缓存(Page Cache):现代Linux核心缓存

页缓存是内核主流缓存机制,以内存页(4KB) 为最小操作单元,支撑文件读写、mmap内存映射、预读等几乎所有文件操作,承担90%以上缓存工作。

1.2.1 底层数据结构:基数树Radix Tree

内核使用基数树管理所有缓存页,每个文件/块设备对应一个address_space地址空间,地址空间内嵌基数树根节点page_tree:

  • 键:文件页偏移pgoff_t,快速索引对应物理页struct page;
  • 双层标记:树节点自带PAGECACHE_TAG_DIRTY(脏标记)、PAGECACHE_TAG_WRITEBACK(回写中标记);
  • 优化:标记自上而下传递,遍历脏页时可直接跳过无脏数据的子树,无需扫描全部页,大幅降低开销。

每个struct page绑定所属地址空间,记录页偏移、引用计数、脏/回写状态;页的增删查统一使用add_to_page_cache、find_get_page等API,所有读写操作对应用程序完全透明。

1.2.2 地址空间address_space:缓存与磁盘的桥梁

address_space是内核核心抽象,隔离内存缓存与底层块设备,核心职责:

  1. 关联宿主inode:文件、裸块设备、tmpfs共享内存均通过inode绑定地址空间;
  2. 存储基数树根,维护当前缓存总页数nrpages;
  3. 挂载address_space_operations操作集:提供readpage/writepage/fsync等文件系统读写回调(ext4、xfs、裸设备各自实现);
  4. 绑定backing_dev_info:记录磁盘预读上限、是否支持回写、设备拥塞状态。

区分重点:虚拟地址空间是进程用户态内存,地址空间是内核缓存专属抽象,二者完全无关。

1.2.3 预读机制:进一步优化顺序读性能

内核内置自适应预读,针对顺序文件读取场景:

  1. 同步预读:首次缺页时一次性读取连续多页进入缓存;
  2. 异步预读:检测到连续读取时,后台预读后续页面,设置PG_Readahead标记;
  3. 控制参数:每个文件维护file_ra_state,记录预读起始、窗口大小、异步阈值,上限由/proc/sys/vm/readahead控制。

1.3 块缓存(Buffer Cache):细粒度元数据缓存

Linux早期唯一缓存,如今退居辅助角色,以文件系统块(512B/1KB/4KB) 为单位,通过struct buffer_head缓冲头管理。

1.3.1 缓冲头核心结构

structbuffer_head {sector_t b_blocknr; // 磁盘块号char *b_data;      // 指向页内块数据unsignedlong b_state; // 状态:BH_Dirty/BH_Uptodate/BH_Lockstructpage *b_page;// 所属内存页structbuffer_head *b_this_page;// 同页缓冲区环形链表};

一页可分割多个缓冲区,通过page->private挂载环形链表,实现页内局部落盘:仅同步修改过的块,无需写整页,减少IO开销。

1.3.2 块缓存两大使用场景

  1. 文件系统元数据读写:超级块、inode、间接块、目录项都是按块存储,必须使用buffer_head读取;
  2. 独立LRU小块缓存:每个CPU维护8项LRU缓冲数组,缓存少量高频访问块,调用__bread/__getblk快速读取裸块。

1.4 页缓存与块缓存协同逻辑

  1. 普通大文件读写:优先使用页缓存+BIO批量IO,性能更高;
  2. 局部修改文件:页拆分多个buffer_head,仅将脏块提交磁盘;
  3. 元数据操作:仅依赖块缓存;
  4. 新旧兼容:2.6内核统一两套缓存,缓冲区数据共享页内存,无需双份拷贝同步。

二 脏页异步同步机制:pdflush完整工作流

缓存核心痛点是脏页不会实时落盘,所有延迟持久化逻辑由pdflush内核线程集群负责,本章拆解回写触发条件、控制参数、同步流程。

2.1 脏页的定义与三大落盘触发方式

内存中页内容与磁盘不一致即为脏页,内核三类刷盘触发机制:

  1. 周期性后台刷盘(定时触发)定时器wb_timer每dirty_writeback_centisecs(默认5秒)唤醒pdflush,调用wb_kupdate,仅同步脏龄超过dirty_expire_centisecs(默认30秒)的脏页,保证长时间未修改数据落地。
  2. 阈值强制刷盘(容量触发)
    • dirty_background_ratio:脏页占总内存10%,后台pdflush主动刷盘,不阻塞用户进程;
    • dirty_ratio:脏页占总内存40%,新write系统调用直接阻塞,同步刷脏页,防止内存耗尽。
  3. 显式同步(程序手动触发)应用调用sync/fsync/fdatasync/msync,强制同步全部/单个文件脏页,无视时间与阈值。

2.2 pdflush线程调度模型

早期单bdflush线程存在磁盘队列阻塞问题,现代内核采用动态多pdflush线程:

  1. 线程数量:最小2个,最大8个;空闲1秒自动销毁,无空闲线程1秒后新建;
  2. 任务分发:不同磁盘设备的回写请求并行处理,单设备拥塞不阻塞其他磁盘;
  3. 工作载体:pdflush_work结构体绑定回写回调(wb_kupdate/background_writeout),内核唤醒线程执行批量刷盘。

2.3 脏页同步完整执行链路

  1. 入口层:定时器/内存分配/sys_sync唤醒pdflush,传入回写控制结构体writeback_control;
  2. 超级块同步:优先刷所有文件系统超级块,防止元数据损坏;
  3. Inode遍历:遍历所有挂载超级块,处理s_dirty脏inode链表;
    • s_io:待同步inode队列;s_more_io:单次未处理完的inode;
  4. 单inode回写__writeback_single_inode:
    • do_writepages:调用文件系统writepage批量写脏页到块层;
    • write_inode:同步inode元数据(大小、时间戳等);
    • 若为强同步(WB_SYNC_ALL),调用filemap_fdatawait阻塞等待IO完成;
  5. 块层处理:封装BIO请求下发磁盘,完成后清除页PG_writeback标记。

2.4 磁盘拥塞处理机制

块设备请求队列达到阈值会标记为拥塞,内核逻辑:

  1. 拥塞阈值:队列空闲request低于nr_congestion_on触发拥塞;高于nr_congestion_off解除;
  2. 阻塞策略:回写遇到拥塞时调用congestion_wait睡眠等待队列空闲,避免无限提交IO压垮磁盘;
  3. 区分读写拥塞,各自独立等待队列。

2.5 关键内核可调参数(/proc/sys/vm)

参数
默认值
作用
dirty_background_ratio
10
脏页占内存10%,后台自动刷盘
dirty_ratio
40
脏页40%,write阻塞同步刷盘
dirty_writeback_centisecs
500(5s)
周期性回写间隔,单位厘秒
dirty_expire_centisecs
3000(30s)
脏页最长驻留内存时间
laptop_mode
0
笔记本省电模式,合并写减少磁盘启停

三 写文件无法及时落盘的根本原因

结合内核原理,总结业务中数据写完丢失、磁盘无更新的4大核心诱因:

  1. 默认延迟写机制:write()仅写入页缓存,不触发磁盘IO,需等待30秒定时刷盘或达到10%脏页阈值;
  2. 仅同步文件数据,不同步元数据:fdatasync只刷文件内容,fsync才同步inode属性;
  3. 大量小IO堆积,未触发批量回写:频繁小文件写入,脏页占比未达后台阈值,无pdflush主动工作;
  4. 硬件/系统缓冲叠加:磁盘自带硬件缓存、RAID卡缓存,内核刷盘仅到硬件缓存,断电依然丢数据;
  5. 内存充足场景:服务器大内存,脏页很难达到10%后台阈值,30秒内断电直接丢失全部新写入数据。

四 生产环境:文件及时落盘决方案

4.1 应用层API修改

方案1:写完调用fsync(强持久,数据库标准用法)

// write写入缓存后,强制同步文件数据+inode元数据到磁盘write(fd, buf, len);fsync(fd);

适用场景:订单、日志、数据库事务、计费数据,崩溃零丢失。

方案2:fdatasync(轻量同步,无需更新元数据)

仅同步文件内容,跳过inode时间戳、权限等元数据,IO开销低于fsync,纯日志场景优选。

方案3:打开文件添加O_DIRECT/O_SYNC标志

  1. O_SYNC:所有write调用同步阻塞,数据直接落盘,每次写等待IO完成,性能损耗极大,仅少量关键数据使用;
  2. O_DIRECT:绕过页缓存,直接用户内存与磁盘交换,适合数据库自建缓存(MySQL、RocksDB),消除内核缓存双重拷贝。

方案4:mmap映射文件使用msync

内存映射文件无法fsync,修改后调用msync(addr, len, MS_SYNC)强制落盘。

4.2 系统全局参数调优

适用于日志服务、离线写入服务,缩短脏页驻留时间,降低丢失窗口:

# 临时生效,重启失效echo 1 > /proc/sys/vm/dirty_background_ratio    # 脏页1%就后台刷echo 500 > /proc/sys/vm/dirty_expire_centisecs # 脏页最长5秒落盘echo 10 > /proc/sys/vm/dirty_ratio             # 10%脏页直接阻塞写入# 永久生效 /etc/sysctl.confvm.dirty_background_ratio = 1vm.dirty_expire_centisecs = 500vm.dirty_ratio = 10sysctl -p

优势:无需修改业务代码;缺点:最小丢失窗口仍为数秒,极端断电仍存在丢数风险,不可用于金融核心数据。

4.3 写入流程优化:主动触发批量刷盘

  1. 批量聚合写入:积累多条数据一次性write,更容易触发pdflush批量回写;
  2. 定时批量fsync:每1/5秒执行一次fsync,平衡性能与数据安全;
  3. 程序退出/重启前强制sync:进程捕获SIGINT/SIGTERM,遍历所有打开文件执行fsync。

4.4 硬件层面:关闭磁盘写缓存

内核fsync仅保证数据写入磁盘硬件缓存,磁盘断电缓存数据丢失,关键业务需关闭硬件缓存:

  1. 机械盘/SSD:hdparm -W 0 /dev/sda关闭磁盘写缓存;
  2. RAID卡:阵列卡设置关闭WriteBack缓存,强制WriteThrough;
  3. 云服务器:云盘默认关闭硬件缓存,裸金属需手动配置。

4.5 系统工具手动强制同步

  1. sync命令:全局刷所有脏页、超级块、元数据,阻塞直到完成;
  2. sync_file_range:指定文件区间同步,兼顾性能与安全性;
  3. 挂载参数同步:文件系统挂载添加sync参数,所有写入实时落盘,性能暴跌,仅特殊设备使用。

4.6 方案选型对照表

方案
性能损耗
数据丢失窗口
适用业务
fsync/fdatasync
中
0(写完即持久)
数据库、订单、核心交易
调vm脏页参数
低
5~30秒
普通日志、离线统计文件
O_DIRECT
高
0
数据库、自建缓存存储
O_SYNC
极高
0
极小体量关键配置文件
sync全局刷盘
极高
0
运维关机、重启前应急

五 总结

  1. Linux依靠页缓存为主、块缓存为辅的双层缓存机制,通过延迟写大幅提升IO性能,底层依托基数树管理缓存页、pdflush线程异步回写脏数据;
  2. 脏页落盘分为定时后台刷、容量阈值刷、程序显式同步三类,默认30秒最长内存驻留时间是数据丢失的核心隐患;
  3. 业务解决文件不及时落盘分两层:核心交易使用fsync强同步保证零丢失;日志类业务可调内核脏页参数缩小丢失窗口;
  4. 完整数据安全链路 = 应用层同步API + 内核缓存控制 + 关闭磁盘硬件缓存,缺一不可。

缓存是Linux IO性能的基石,但延迟写带来的数据一致性风险必须结合业务场景取舍。性能优先选择参数调优,数据安全优先必须显式调用同步接口,二者结合才能兼顾吞吐量与持久化可靠性。

往期文章(欢迎订阅技术分享栏目全部文章):

【从零开始撸内核驱动源码】:以ttyserial(串口驱动)为例,串联字符设备驱动基础知识点的学习计划
Linux内核源码顶层 Makefile分析并单独编译调试内核自带的驱动
【从零开始撸内核驱动源码】:ttynull驱动
Linux内核驱动安装失败问题调试及解决方法
Linux内核驱动源码走读之编译内核及外部驱动实操指南
“谢谢你看到这里”

这里是女程序员的笔记本

 15年+嵌入式软件工程师兼二胎宝妈

分享读书心得、工作经验,自我成长和生活方式。

希望我的文字能对你有所帮助

最新文章

随机文章