> For the complete documentation index, see [llms.txt](https://freebsd-journal-cn.bsdcn.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://freebsd-journal-cn.bsdcn.org/2014-0304-pkg-8/journaled-soft-updates.md).

# 日志化软更新

* 原文：[Journaled软更新](https://freebsdfoundation.org/wp-content/uploads/2014/03/Journaled-Soft-Updates.pdf)
* 作者：**Marshall Kirk McKusick**、**Jeff Roberson**

软更新依赖追踪系统于 1998 年被 FreeBSD 采用，作为流行的日志文件系统技术的替代方案。

虽然软更新的运行时性能和一致性保证与日志文件系统相当 \[Seltzer, Ganger, McKusick et al, 2000]，但日志文件系统在崩溃后依赖昂贵且耗时的后台文件系统恢复操作。

本文概述一种方法，通过使用一个小型日志记录软更新中可能出现的仅有的两种不一致，来消除昂贵的前台或后台全文件系统检查。第一种是已分配但未引用的块；第二种是偏高的链接计数。偏高的链接计数包括正在被删除的未引用 inode 和已 unlink 但仍打开的文件 \[Ganger, McKusick, & Patt, 2000]。这一日志使日志分析程序能在数秒内完成恢复，且与文件系统大小无关。

崩溃后，老牌 `fsck` 程序的变体遍历日志，识别并释放丢失的资源。只有检测到日志与文件系统之间存在不一致时，才需要运行全文件系统的 `fsck`。日志非常小，通常 16MB 就足够，与文件系统大小无关。

虽然日志处理需要在重启前完成，但处理时间通常只有几秒，最坏情况下一分钟。

## 与其他实现的兼容性

使用软更新日志无需构建新的文件系统。日志通过 `tunefs` 启用，仅需少量空闲超级块字段和 16MB 空闲块用于日志。这些最低要求使它能在现有 FreeBSD 文件系统上轻松启用。日志的文件系统块放置在文件系统根目录下名为 `.sujournal` 的 inode 中，并设置文件系统标志，使得较旧的非日志内核在挂载先前已日志化的卷时触发全文件系统检查。挂载日志化文件系统时，较旧内核会清除表示日志化已进行的标志，这样当日志化文件系统下次被支持日志的内核遇到时，它会知道日志无效，确保文件系统一致并清除日志，然后恢复使用该文件系统。

## 日志格式

日志以分段的循环日志形式保存，每段包含描述元数据操作的记录。如果日志填满，文件系统必须完成足够的操作以让日志条目过期，然后才能允许新操作。实践中，日志几乎从不会填满。

每个日志段包含一个唯一的序列号和一个标识文件系统挂载实例的时间戳，以便在日志处理期间丢弃旧段。日志条目聚合到段中以减少对日志的写入次数。每段包含写入日志时的最后一个有效序列号，让 `fsck` 通过扫描整个日志恢复头尾。段大小可变，为磁盘块大小的整数倍，并原子写入以避免在运行的文件系统中出现读/改/写循环。

日志分析已整合进 `fsck` 程序。整合进现有 `fsck` 程序有几大好处。现有启动脚本已经调用 `fsck` 来判断是否需要在前台或后台运行。对于运行日志化软更新的文件系统，`fsck` 可以请求在前台运行，在文件系统上线前完成所需的日志化操作。如果日志因某种原因失败，它可以报告需要运行完整 `fsck` 作为传统回退。因此，这一新功能可以在不改变系统管理员启动系统方式的情况下引入。最后，调用 `fsck` 意味着日志处理完成后，出于调试目的可以顺带运行文件系统的完整检查，确保日志工作正常。

日志条目大小为 32 字节，提供了高密度表示，每个 4KB 扇区可容纳 128 条条目。日志创建在文件系统的单个区域，分配尽可能连续。我们曾考虑将其分散到柱面组以优化写入的局部性，但它实在太小，这种做法不切实际，且会使清理时扫描整个日志过慢。

日志块由具名的不可变 inode 占用。这种方式允许用户级访问日志用于调试和统计收集，同时提供与不支持日志的旧内核的向后兼容。我们发现 16MB 的日志大小即使在最苛刻的最坏情况基准测试中也足够。16MB 日志可覆盖超过 500,000 个命名空间操作或 16GB 的未完成分配（假设标准 32KB 块大小）。

## 需要日志化的修改

本小节描述必须日志化的操作，使 `fsck` 能获得清理文件系统所需的信息。

### 链接计数增加

链接计数可能通过硬链接或文件创建而增加。在 rename 期间链接计数会临时增加。此处的操作相同。inode 号、父 inode 号、目录偏移和初始链接计数都记录在日志中。软更新保证 inode 链接计数在任何目录写入前在磁盘上完成增加并稳定。日志写入必须发生在更新链接计数的 inode 写入之前，如果是新分配的 inode，还要发生在分配该 inode 的位图写入之前。

### 链接计数减少

inode 链接计数通过 unlink 或 rename 减少。inode 号、父 inode、目录偏移和初始链接计数都记录在日志中。被删除的目录项保证在链接计数下调前写入。与增加链接计数一样，日志写入必须发生在所有其他写入之前。

### 引用时的 Unlink

已 unlink 但仍被引用的文件给日志化文件系统带来难题。在 POSIX 中，inode 的存储要到最终名称被移除且最后一个引用被关闭后才会回收。简单地让日志条目在等待应用关闭悬空引用期间保持有效行不通，因为它会轻易耗尽日志空间。需要一种能扩展到文件系统中 inode 总数的解决方案。至少有两种可行方法：复制 inode 分配位图，或要释放的 inode 的链表。我们选择链表方法。

链表方式被多个文件系统（xfs、ext4 等）采用：超级块中保存 inode 号，作为待释放 inode 单链表的表头，每个 inode 在链表中存放下一个 inode 的指针。这种方式的好处是恢复时 `fsck` 只需检查超级块中已在内存中的指针。缺点是内核必须维护内存中的双向链表，以便在 inode 不再被引用时快速移除它。这种方式在设计上固化了文件系统范围的锁，并在维护链表时产生非局部写入。实践中我们发现未引用 inode 很少发生，这种方式不构成瓶颈。从链表移除可以延迟进行，但必须在 inode 任何重用前完成。向链表添加必须在为最终 unlink 回收日志空间前稳定，但除此之外可以延迟足够久，以至于如果文件很快关闭就完全不需要这次写入。添加和移除只涉及一次写入，更新前驱指针指向后继 inode。

### 目录偏移变更

每当目录压缩移动目录项时，必须创建一条日志条目，描述该目录项的旧位置和新位置。内核在移动时不知道随后是否会跟随一次 remove，因此目前所有偏移变更都日志化。没有这一信息，`fsck` 将无法区分同一目录块的多次修订。

### 块分配与释放

执行块分配或释放时，无论是片段、间接块、目录块、直接块还是扩展属性，记录都相同。文件的 inode 号和块在文件内的偏移被记录，间接块和扩展属性块使用负偏移。此外，磁盘块地址和片段数也包含在日志记录中。日志条目必须在任何分配或释放前写入磁盘。

释放间接块时，只记录间接块树的根。因此，对于截断，我们最多需要 15 条日志条目，12 条用于直接块，3 条用于间接块。这 15 条日志条目让我们能用最少的日志开销释放大量空间。恢复期间，`fsck` 会跟踪间接块并释放任何后代，包括其他间接块。要让此算法工作，间接块的内容必须在日志记录释放前保持有效，以免用户数据与间接块指针混淆。

## 回收日志空间

要从先前写入的记录回收日志空间，内核必须知道日志记录所描述的操作已在磁盘上稳定。这一要求意味着，创建新文件时，日志记录在柱面组位图、inode、目录块、目录 inode 以及可能的若干间接块的写入完成后才能释放。分配新块时，日志记录在 inode 或间接块中的新块指针、柱面组位图以及块本身的写入完成后才能释放。间接块内的块指针在所有父间接块通过 inode 间接块指针在磁盘上完全可达之前不算稳定。为简化满足这些要求，描述这些操作的依赖关系携带指向日志中包含描述未完成操作的日志条目的最旧段结构的指针。

某些操作可能由多条条目描述。例如，创建新目录时，其添加会创建三个新名称。每个名称关联到其所指 inode 上的引用计数。当其中一个依赖被满足时，如果日志条目所依赖的另一操作尚未完成，它可能将其日志条目引用传递给另一依赖。如果操作已完成，日志记录上的最终引用被释放。当日志段中所有日志记录的引用都被释放时，其空间被回收，最旧有效段序列号被调整。我们只能释放最旧的空闲日志段，因为日志被视为循环队列。

## 日志化的额外要求

一些在软更新下此前不需要跟踪的操作，在引入日志化时需要被跟踪。本小节描述这些新要求。

### 柱面组回滚

软更新此前不需要柱面组回滚，因为它们总是一组变更中的首次或末次写入。当一个块或 inode 已被分配但其日志记录尚未写入磁盘时，写入更新后的位图和相关的分配信息并不安全。写入带有 `bmsafemap` 依赖的块的例程现在会回滚任何具有未写入日志操作的分配。

### Inode 回滚

inode 链接计数必须回滚到任何未写入日志条目之前的链接计数。允许它增长超过此计数不会导致文件系统损坏，但会妨碍日志恢复正确调整链接计数。软更新已经防止链接计数在目录项移除前下降，因为过早递减会导致文件系统损坏。

当一个已 unlink 的文件已关闭，其 inode 在零清的块指针写入磁盘前不能归还到 inode 空闲链表，以便其块能被释放并从磁盘上的未链接文件链表中移除。未链接文件 inode 要等到链表中前驱 inode 的 next 指针在磁盘上更新为指向后继 inode 后，才从未链接文件链表中完全移除。如果未链接文件 inode 是未链接文件链表的第一个 inode，则要等到超级块中未链接文件链表头指针在磁盘上更新为指向后继 inode 后，才从链表中完全移除。

## 处理填满的日志

如果日志变满，我们必须阻止创建任何新日志条目，直到最旧的有效条目退役腾出空间。阻止创建新日志记录的有效方式是使用为快照准备的机制挂起文件系统。一旦挂起，文件系统上正在进行的操作允许完成，但想要修改文件系统的新操作会被投入睡眠，直到挂起解除。

我们在每次会改变链接计数或分配块的操作前检查日志空间。如果发现日志接近填满状态，我们挂起文件系统，并加快软更新工作列表处理的进度，以提高日志条目退役的速率。由于执行检查的操作已经启动，允许它完成，但后续操作被阻塞。因此，必须在仍有足够日志空间完成已在进行中的操作时挂起操作。当足够多的日志条目被释放后，文件系统挂起解除，恢复正常操作。

实践中，我们不得不创建最小尺寸的日志（4MB），并运行旨在产生大量链接计数变更、块分配和块释放的脚本来触发日志填满状态。即便在这些测试下，文件系统挂起也很少见且短暂，持续不到一秒。

## 恢复流程

本小节描述 `fsck` 在崩溃后使用日志清理文件系统。

### 扫描日志

进行恢复时，`fsck` 程序必须首先从头到尾扫描日志，发现最旧的有效序列号。我们曾考虑保留日志头尾指针，然而那需要对超级块区域额外写入。由于日志很小，扫描它以识别有效日志头尾所花的额外时间，似乎是降低维护日志头尾指针运行时成本的合理折中。因此，`fsck` 程序必须发现包含仍有效序列号的第一个段并从这里开始工作。然后按顺序解析日志记录。日志记录标记有时间戳，必须与文件系统挂载时间匹配，并有 CRC 保护内容的有效性。

### 调整链接计数

对于每条记录链接增加的日志记录，`fsck` 需要检查所提供偏移处的目录，看所记录 inode 号的目录项是否存在于磁盘上。如果不存在，但 inode 链接计数已增加，则记录的链接计数需要递减。

对于每条记录链接减少的日志记录，`fsck` 需要检查所提供偏移处的目录，看所记录 inode 号的目录项是否存在于磁盘上。如果已在磁盘上删除，但 inode 链接计数未递减，则记录的链接计数需要递减。

对正在跟踪的目录项的目录偏移压缩使上述链接调整方案复杂化。由于目录块不是同步写入的，`fsck` 必须在每个目录项的所有可能位置查找。

当一个 inode 多次被添加到目录并从中移除时，`fsck` 无法根据上述算法正确评估链接计数。所选的解决方案是预处理日志，将所有与同一 inode 相关的条目链接在一起。这样，所有未确认已提交到磁盘的操作可以并发检查，以确定相对于第一条日志条目之前已知的稳定计数，应存在多少链接。当一个 inode 在同一偏移多次添加和删除时产生的重复记录被丢弃，从而得到一致的计数。

### 更新已分配的 Inode 位图

链接计数调整后，`fsck` 必须释放链接计数降为零的任何 inode。此外，`fsck` 必须释放系统崩溃时已 unlink 但仍在使用的任何 inode。未引用 inode 链表的表头位于超级块，如本文前文所述。`fsck` 程序必须遍历这条未链接 inode 链表并释放它们。

释放 inode 的第一步是将它的所有块加入待释放块列表。接着，inode 需要被清零以表明它不再使用。最后，它所在柱面组的 inode 位图必须更新以反映该 inode 可用，并更新所有相关文件系统统计信息以反映该 inode 的可用性。

### 更新已分配的块位图

日志扫描完成后，它提供一份打算释放的块列表。日志条目列出要释放块所属的 inode。恢复时，`fsck` 处理每条释放记录，检查该块是否仍被其关联的 inode 占用。如果发现块不再被占用，就释放它。

对于因 inode 解除分配或通过上述识别过程而释放的每个块，其所在柱面组的块位图必须更新以反映其可用，并更新所有相关文件系统统计信息以反映其可用性。释放片段时，还必须更新片段可用性统计。

## 性能

日志化给传统软更新要求增加了额外的运行时间和内存分配，以及写日志的额外 I/O 操作。额外运行时间和内存分配的开销在我们运行的基准测试中无法测出。额外 I/O 主要体现在单个操作完成的延迟增加。操作完成时间通常只在应用执行导致其等待文件落盘的 `fsync` 系统调用时才显现。否则，写日志的额外 I/O 只在启用日志化前受文件系统 I/O 带宽限制的基准测试中才会显现。

总结来说，运行日志化软更新的系统永远不会比运行不带日志化的软更新的系统更快。因此，文件系统较小的系统（如嵌入式系统）通常希望运行不带日志化的软更新，并在系统崩溃后花时间运行 `fsck`。

日志化项目的主要目的是消除漫长的文件系统检查时间。40TB 的卷可能需要整整一天和相当可观的内存来检查。

我们运行了几个场景来理解和验证恢复时间。开发者的一项典型操作是运行并行 buildworld。此场景的崩溃恢复展示了从中等写入工作负载恢复的时间。250GB 磁盘被 FreeBSD 源码树的副本填满到 80%。随机选中副本，8 路 buildworld 进行 10 分钟后机器被复位。从日志恢复耗时 0.9 秒。额外用传统 `fsck` 运行了一次，以验证文件系统安全恢复。`fsck` 耗时约 27 分钟，是前者的 1,800 倍。

测试志愿者在 3ware RAID 控制器上跨越 14 块硬盘、占用率 92% 的 11TB 卷上，通过并行写入随机长度文件产生了数百兆字节的脏数据，然后复位机器。最终的恢复操作不到一分钟完成。该文件系统上一次正常的 `fsck` 运行大约需要 10 小时。

## 参考文献

* G. Ganger, M. McKusick, & Y. Patt, “软更新: A Solution to the Metadata Update Problem in Filesystems,” *ACM Transactions on Computer Systems* 18(2), p. 127−153 (May 2000).
* M. Seltzer, G. Ganger, M. K. McKusick, K. Smith, C. Soules, & C. Stein, “Journaling versus软更新: Asynchronous Meta-data Protection in File Systems,” *Proceedings of the San Diego Usenix Conference*, pp. 71-84 (June 2000).

***

**Marshall Kirk McKusick** 撰写书籍和文章，提供咨询，并讲授 Unix 和 BSD 相关主题的课程。在加州大学伯克利分校期间，他实现了 4.2BSD 快速文件系统，并担任伯克利计算机系统研究组（CSRG）的研究计算机科学家，监督 4.3BSD 和 4.4BSD 的开发与发布。他曾两次担任 Usenix 协会董事会主席，目前是 FreeBSD 基金会董事会成员、ACM Queue 杂志和 The FreeBSD 期刊编辑委员会成员、IEEE 高级会员，以及 Usenix 协会、ACM 和 AAAS 的会员。可通过电子邮件 <mckusick@mckusick.com> 与他联系。

**Jeff Roberson** 是顾问，居住在夏威夷群岛的毛伊岛。不在骑车、徒步或以其他方式享受海岛生活时，他靠改进 FreeBSD 谋生。他特别关注服务器安装面临的问题，并涉猎内核内存分配器、线程调度器、文件系统接口和网络包存储等多个领域。可通过电子邮件 <jroberson@jroberson.net> 与他联系。


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://freebsd-journal-cn.bsdcn.org/2014-0304-pkg-8/journaled-soft-updates.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
