Skip to content

第八十八章:日志之道

斩灵期

涉及内核源码:

林小源在 ext4 巨树的根部发现了一条暗河。

暗河的水流湍急,水面上漂浮着无数细小的光点。每个光点都在闪烁,像是在记录什么。暗河的入口处立着一块石碑,碑上刻着"JBD2"三个字。

"这是日志,"巨树的声音从头顶传来,"我的记忆之河。"

林小源蹲下来,伸手触碰水面。指尖接触的瞬间,一股信息涌入脑海——他看到了一段完整的操作序列:TxB(事务开始),然后是一连串元数据修改,最后是 TxC(事务提交)。

"每次修改元数据之前,"巨树说,"先把操作记录到这条河里。写完日志,再写实际位置。如果中间出了岔子——比如突然断电——日志还在。重启的时候,内核读日志,重放操作,文件系统就恢复到一致状态。"

林小源看着水面上的光点。每个光点都是一个事务记录,有的已经提交(TxC),有的还在进行中。

"如果崩溃发生在写日志之前呢?"

"没有日志,没有修改,"巨树说,"文件系统保持原状。什么都不丢。"

"如果崩溃发生在写日志之后、写实际位置之前?"

"日志里有完整事务,"巨树说,"重放日志,把修改写到实际位置。数据不丢。"

"如果崩溃发生在写实际位置之后?"

"数据已经安全写入,"巨树说,"日志可以忽略。"

林小源沿着暗河走了很远。

河水的颜色在不断变化——有的河段是蓝色的,有的是绿色的,有的是红色的。每种颜色代表一种日志模式。

"蓝色是什么?"他问。

"journal 模式,"暗河中传出一个声音,低沉而谨慎,"元数据和数据都写日志。最安全,但最慢——每次写入都是双份。"

"绿色呢?"

"ordered 模式,"声音变得沉稳了一些,"只对元数据写日志。但有一个规矩——数据必须在元数据之前写入。这样即使崩溃,你看到的要么是旧数据,要么是新数据,不会看到半截新半截旧。"

林小源点了点头。ordered 模式是默认模式——安全性够用,性能也不错。

"红色呢?"

"writeback 模式,"声音变得急促,"只对元数据写日志,数据随便什么时候写。最快,但风险最大——崩溃后可能看到乱序的数据。"

林小源蹲下来,用手指在河边的泥土上画了三个方框:journal 方框里有两层(数据+日志),ordered 方框里数据在前、元数据在后,writeback 方框里只有元数据有日志。

"ordered 是平衡点,"他低声说。

"对,"暗河的声音说,"大部分系统用 ordered。只有对数据完整性要求极高的数据库才会用 journal。对性能要求极高、不太在乎数据完整性的场景才用 writeback。"

林小源在暗河的尽头发现了一个巨大的漩涡。

漩涡的中心是一个发光的圆环,圆环上刻着"commit"的字样。所有已提交的事务最终都会流入这个漩涡——从日志空间中释放出来,回到空闲池。

"这就是事务提交,"巨树说,"commit 之后,日志空间可以重用。但如果 commit 之前崩溃——日志还在,可以重放。"

林小源看着漩涡,忽然问:"日志的代价是什么?"

"双份写入,"巨树的声音变得沉重,"每次修改元数据,都要写两次——一次写日志,一次写实际位置。写入性能至少减半。"

"值得吗?"

"当然值得,"巨树说,"没有日志的文件系统,崩溃后需要 fsck 全盘扫描。磁盘越大,扫描越慢——几个小时甚至几天。有了日志,几秒钟就能恢复。"

林小源站起身。他看着暗河——水流不停,光点闪烁,每一个光点都是一份安全保障。日志是文件系统的"后悔药"——代价是性能,换来的是崩溃后的快速恢复。

"安全有代价,"他低声说。

"但值得,"巨树说。

"日志就是给时间做的备份,"他自言自语,"代价是性能,换来的是不怕断电的底气。"


c
/*
 * JBD2 (Journaling Block Device 2) 的工作原理:
 *
 * 1. 写入日志
 *    把要修改的元数据写到日志区
 *    标记事务开始 (TxB)
 *    写入元数据
 *    标记事务提交 (TxC)
 *
 * 2. 写入实际位置
 *    把元数据写到磁盘的实际位置
 *
 * 3. 释放日志
 *    事务完成后,释放日志空间
 *
 * 崩溃恢复:
 *   如果崩溃发生在 1 之后、2 之前:
 *     日志中有完整事务,重放日志
 *   如果崩溃发生在 2 之后:
 *     数据已写到实际位置,忽略日志
 *
 * 日志模式:
 *   journal — 元数据和数据都写日志(最安全,最慢)
 *   ordered — 只元数据写日志,数据先写(默认)
 *   writeback — 只元数据写日志(最快,最不安全)
 */

printf("=== 日志 — 崩溃恢复的保障 ===\n\n");

printf("日志的工作流程:\n\n");

printf("1. 写入日志:\n");
printf("   ┌─────┐\n");
printf("   │ TxB │  事务开始\n");
printf("   │ ... │  元数据操作\n");
printf("   │ TxC │  事务提交\n");
printf("   └─────┘\n\n");

printf("2. 写入实际位置:\n");
printf("   日志 → 磁盘实际位置\n\n");

printf("3. 释放日志:\n");
printf("   事务完成,释放空间\n\n");

printf("--- 崩溃场景 ---\n");
printf("场景 1: 崩溃在写日志之前\n");
printf("  没有日志,没有修改\n");
printf("  文件系统保持原状\n\n");

printf("场景 2: 崩溃在写日志之后\n");
printf("  日志中有完整事务\n");
printf("  恢复时重放日志\n\n");

printf("场景 3: 崩溃在写实际位置之后\n");
printf("  数据已安全写入\n");
printf("  忽略日志\n\n");

printf("--- 日志模式 ---\n");
printf("journal (最安全):\n");
printf("  元数据和数据都写日志\n");
printf("  最慢\n\n");
printf("ordered (默认):\n");
printf("  数据先写,元数据写日志\n");
printf("  保证数据在元数据之前\n\n");
printf("writeback (最快):\n");
printf("  只元数据写日志\n");
printf("  数据可能乱序\n\n");

printf("--- 挂载选项 ---\n");
printf("data=journal\n");
printf("data=ordered\n");
printf("data=writeback\n");

道藏笔记

内核启示

日志是文件系统的一致性保障机制。原理不复杂:修改元数据之前,先把操作记到日志里;写完日志,再写实际位置;最后释放日志空间。崩溃发生在任何一步都有对应的恢复策略——日志写完之前崩溃,什么都没改,文件系统原样;日志写完之后崩溃,重放日志就能恢复一致。

日志有三种模式。journal 最安全,元数据和数据都写日志,但每次写入都是双份,性能最差。ordered 是默认模式,只对元数据写日志,但规矩是数据必须在元数据之前写入——这样崩溃后你看到的要么是旧数据要么是新数据,不会半新半旧。writeback 最快,只对元数据写日志,数据随便什么时候写,风险也最大。

没有日志的文件系统,崩溃后需要 fsck 全盘扫描,磁盘越大扫越慢。有了日志,几秒钟就能恢复。代价是写入性能至少减半——但对需要数据安全的场景来说,这笔账算得过来。


破关试炼

日志之试

日志之道中,事务最终要落盘提交;本章代码和概念里反复出现的提交动作叫什么?

答对后才能继续滑动和进入下一章。

以修仙之名,悟内核之道