title: fs-writeback
categories:
- linux
- fs
tags: - linux
- fs
abbrlink: 88ab2b13
date: 2025-10-03 09:01:49
文章目录
- fs-writeback
- include/linux/fs.h
-
- [mark_inode_dirty_sync 将 inode 标记为脏同步](#mark_inode_dirty_sync 将 inode 标记为脏同步)
- [inode_unhashed inode 无hash](#inode_unhashed inode 无hash)
- [mapping_tagged 用于检查页面缓存(address_space)中的任何页面是否被标记为指定的标签(tag)](#mapping_tagged 用于检查页面缓存(address_space)中的任何页面是否被标记为指定的标签(tag))
- [mark_inode_dirty_sync 将 inode 标记为脏同步](#mark_inode_dirty_sync 将 inode 标记为脏同步)
- fs/fs-writeback.c
-
- [locked_inode_to_wb_and_lock_list 从给定的 inode 中提取关联的 bdi_writeback 结构,并在锁的上下文中进行转换](#locked_inode_to_wb_and_lock_list 从给定的 inode 中提取关联的 bdi_writeback 结构,并在锁的上下文中进行转换)
- [wb_io_lists_populated 检查 bdi_writeback 是否已经标记为有脏 IO 数据。如果没有脏 IO 数据,它会设置 WB_has_dirty_io 标志,并更新相关的写带宽统计](#wb_io_lists_populated 检查 bdi_writeback 是否已经标记为有脏 IO 数据。如果没有脏 IO 数据,它会设置 WB_has_dirty_io 标志,并更新相关的写带宽统计)
- [wb_io_lists_depopulated 清除 WB_has_dirty_io 标志,当 bdi_writeback 的所有 IO 列表都为空时,同时更新写带宽统计](#wb_io_lists_depopulated 清除 WB_has_dirty_io 标志,当 bdi_writeback 的所有 IO 列表都为空时,同时更新写带宽统计)
- [inode_io_list_move_locked 将 inode 移动到 bdi_writeback IO 列表中](#inode_io_list_move_locked 将 inode 移动到 bdi_writeback IO 列表中)
-
- [**1. `wb->b_dirty`**](#1.
wb->b_dirty) - [**2. `wb->b_io`**](#2.
wb->b_io) - [**3. `wb->b_more_io`**](#3.
wb->b_more_io) - [**4. `wb->b_dirty_time`**](#4.
wb->b_dirty_time)
- [**1. `wb->b_dirty`**](#1.
- [wb_wakeup_delayed 唤醒相应的 bdi 线程,然后该线程应该负责定期后台写出脏 inode](#wb_wakeup_delayed 唤醒相应的 bdi 线程,然后该线程应该负责定期后台写出脏 inode)
- [__mark_inode_dirty 用于将 inode 标记为脏](#__mark_inode_dirty 用于将 inode 标记为脏)
- [write_inode_now 立即将一个脏的 inode 写入磁盘](#write_inode_now 立即将一个脏的 inode 写入磁盘)
- [write_inode 标记 inode(文件系统中的索引节点)为脏状态](#write_inode 标记 inode(文件系统中的索引节点)为脏状态)
- [inode_wait_for_writeback 等待 inode 的写回操作完成](#inode_wait_for_writeback 等待 inode 的写回操作完成)
- [__writeback_single_inode 将 inode 的脏数据(包括页面和元数据)写入磁盘,并清除相关的脏标志(I_DIRTY)以维护文件系统的一致性](#__writeback_single_inode 将 inode 的脏数据(包括页面和元数据)写入磁盘,并清除相关的脏标志(I_DIRTY)以维护文件系统的一致性)
- [writeback_single_inode 将单个 inode 写回磁盘](#writeback_single_inode 将单个 inode 写回磁盘)
- [sync_inodes_sb: 同步并等待单个文件系统的Inode写回](#sync_inodes_sb: 同步并等待单个文件系统的Inode写回)
- [脏时间 Inode 周期性回写:start_dirtytime_writeback 与 wakeup_dirtytime_writeback](#脏时间 Inode 周期性回写:start_dirtytime_writeback 与 wakeup_dirtytime_writeback)

fs-writeback
fs-writeback.c 是 Linux 内核中负责处理文件系统数据写回(writeback)机制的核心模块。它的主要功能是管理脏页和脏 inode 的写回操作,以确保数据从内存缓冲区最终写入磁盘。以下是对其原理、使用场景、优缺点以及其他方案的详细阐述:
原理
-
脏页与脏 inode:
- 当文件系统中的数据被修改时,内核会将这些修改暂时存储在内存中(页缓存或 inode 缓存),并标记为"脏"。
- 脏页指的是页缓存中未写入磁盘的修改数据,脏 inode 则是文件元数据(如时间戳、权限等)的未写入部分。
-
写回机制:
- fs-writeback.c 通过后台线程(flusher threads)定期扫描脏页和脏 inode,并将它们写入磁盘。
- 写回操作可以是异步的(WB_SYNC_NONE),也可以是同步的(WB_SYNC_ALL),具体取决于调用场景。
-
核心组件:
bdi_writeback: 表示一个设备的写回上下文,管理该设备的脏页和脏 inode。wb_writeback_work: 描述写回任务的结构体,包括写回页数、同步模式等。wb_workfn: 写回线程的主函数,负责执行写回任务。
-
调度与触发:
- 写回任务可以由以下事件触发:
- 内存压力(如内存不足时触发写回以释放页缓存)。
- 定期写回(如
dirty_writeback_interval配置的时间间隔)。 - 显式调用(如
sync系统调用或fsync文件操作)。
- 写回任务可以由以下事件触发:
使用场景
-
数据完整性:
- 确保文件系统的数据和元数据最终写入磁盘,避免数据丢失。
- 在系统崩溃或断电时,脏页和脏 inode 的及时写回可以减少数据丢失的风险。
-
性能优化:
- 写回机制通过延迟写入操作,减少频繁的磁盘 I/O,提高系统性能。
- 通过批量写回,优化磁盘写入效率。
-
文件系统操作:
- 文件系统的
sync和fsync操作依赖写回机制来确保数据一致性。 - 数据库等应用程序通常使用
fsync来确保事务的持久性。
- 文件系统的
优缺点
优点
-
性能提升:
- 延迟写入减少了磁盘 I/O 的频率,提高了系统的整体性能。
- 批量写回优化了磁盘的写入效率。
-
灵活性:
- 支持多种写回模式(异步、同步),适应不同场景的需求。
- 可配置参数(如
dirty_writeback_interval和dirty_ratio)允许用户根据系统负载调整写回行为。
-
数据安全性:
- 通过定期写回和显式触发写回,确保数据最终写入磁盘,减少数据丢失的风险。
缺点
-
延迟风险:
- 延迟写入可能导致数据丢失,尤其是在系统崩溃或断电时。
- 如果写回线程无法及时处理脏页,可能导致内存压力增加。
-
复杂性:
- 写回机制涉及多个线程和复杂的调度逻辑,可能增加代码维护难度。
- 在高负载场景下,写回线程可能成为性能瓶颈。
-
I/O 干扰:
- 批量写回可能导致突发的磁盘 I/O 峰值,影响其他 I/O 操作的性能。
其他方案
1. Direct I/O
- 原理: 直接将数据写入磁盘,绕过页缓存。
- 优点 :
- 减少内存使用。
- 提供更低的写入延迟。
- 缺点 :
- 性能可能较低,尤其是小块数据写入。
- 不支持缓存优化。
2. Journaling 文件系统
- 原理: 使用日志记录文件系统的元数据和数据操作,确保一致性。
- 优点 :
- 提供更高的数据可靠性。
- 快速恢复能力。
- 缺点 :
- 增加了额外的磁盘写入开销。
3. 用户空间缓存
- 原理: 应用程序在用户空间实现自己的缓存机制。
- 优点 :
- 应用程序可以完全控制数据写入时机。
- 缺点 :
- 增加了开发复杂性。
- 需要额外的内存管理。
4. 内存映射文件(mmap)
- 原理: 将文件映射到内存,直接操作内存中的数据。
- 优点 :
- 提供高效的文件访问。
- 缺点 :
- 需要显式调用
msync或munmap来确保数据写入磁盘。
- 需要显式调用
总结
fs-writeback.c 是 Linux 文件系统中不可或缺的模块,负责管理脏页和脏 inode 的写回操作。它通过延迟写入和批量处理优化了系统性能,同时提供了数据完整性保障。然而,它也存在延迟写入的风险和复杂性问题。在实际应用中,可以根据场景选择合适的写回机制或替代方案,以平衡性能与数据安全性。
include/linux/fs.h
mark_inode_dirty_sync 将 inode 标记为脏同步
c
static inline void mark_inode_dirty_sync(struct inode *inode)
{
__mark_inode_dirty(inode, I_DIRTY_SYNC);
}
inode_unhashed inode 无hash
c
static inline int inode_unhashed(struct inode *inode)
{
return hlist_unhashed(&inode->i_hash);
}
mapping_tagged 用于检查页面缓存(address_space)中的任何页面是否被标记为指定的标签(tag)
- 页面缓存是文件系统中用于存储文件数据的内存区域,而标签(tag)是用于标识页面状态的标记,例如页面是否脏、是否正在写回等。
c
/*
*如果映射中的任何页面标有 tag,则返回 true。
*/
static inline bool mapping_tagged(struct address_space *mapping, xa_mark_t tag)
{
/* xa_marked 是一个通用的函数,用于检查 XArray 数据结构中的某个标记是否存在 */
return xa_marked(&mapping->i_pages, tag);
}
mark_inode_dirty_sync 将 inode 标记为脏同步
c
static inline void mark_inode_dirty_sync(struct inode *inode)
{
__mark_inode_dirty(inode, I_DIRTY_SYNC);
}
fs/fs-writeback.c
locked_inode_to_wb_and_lock_list 从给定的 inode 中提取关联的 bdi_writeback 结构,并在锁的上下文中进行转换
c
static struct bdi_writeback *
locked_inode_to_wb_and_lock_list(struct inode *inode)
__releases(&inode->i_lock)
__acquires(&wb->list_lock)
{
/* 从 inode 中提取与其关联的 bdi_writeback 结构。bdi_writeback 是 Linux 内核中用于管理后台写回操作的核心结构 */
struct bdi_writeback *wb = inode_to_wb(inode);
/* 首先释放 inode->i_lock 锁,表示当前线程不再需要保护 inode 的状态 */
spin_unlock(&inode->i_lock);
/* 然后获取 wb->list_lock 锁,用于保护 bdi_writeback 的列表操作。这种锁转换确保了线程安全,同时避免了死锁 */
spin_lock(&wb->list_lock);
return wb;
}
wb_io_lists_populated 检查 bdi_writeback 是否已经标记为有脏 IO 数据。如果没有脏 IO 数据,它会设置 WB_has_dirty_io 标志,并更新相关的写带宽统计
c
static bool wb_io_lists_populated(struct bdi_writeback *wb)
{
/* 检查 wb 是否已经有脏 IO 数据 */
if (wb_has_dirty_io(wb)) {
return false;
} else {
/* 没有脏 IO 数据 */
/* 设置 WB_has_dirty_io 标志,表示该写回设备现在有脏 IO 数据 */
set_bit(WB_has_dirty_io, &wb->state);
/* 检查 avg_write_bandwidth 是否为零。如果为零,发出警告,因为写带宽统计不应为零 */
WARN_ON_ONCE(!wb->avg_write_bandwidth);
atomic_long_add(wb->avg_write_bandwidth,
&wb->bdi->tot_write_bandwidth);
return true;
}
}
wb_io_lists_depopulated 清除 WB_has_dirty_io 标志,当 bdi_writeback 的所有 IO 列表都为空时,同时更新写带宽统计
c
static void wb_io_lists_depopulated(struct bdi_writeback *wb)
{
/* 否有脏 IO 数据
检查 b_dirty、b_io 和 b_more_io 列表是否为空。
如果所有列表都为空,表示没有脏 IO 数据 */
if (wb_has_dirty_io(wb) && list_empty(&wb->b_dirty) &&
list_empty(&wb->b_io) && list_empty(&wb->b_more_io)) {
/* 清除 WB_has_dirty_io 标志,表示写回设备不再有脏 IO 数据 */
clear_bit(WB_has_dirty_io, &wb->state);
/* 更新总写带宽统计。
如果结果小于零,发出警告,因为总写带宽统计不应为负值 */
WARN_ON_ONCE(atomic_long_sub_return(wb->avg_write_bandwidth,
&wb->bdi->tot_write_bandwidth) < 0);
}
}
inode_io_list_move_locked 将 inode 移动到 bdi_writeback IO 列表中
- 将 inode->i_io_list 移动到目标 bdi_writeback 的指定列表中(如 b_dirty、b_io、b_more_io 或 b_dirty_time)。
- 同时,它会设置 WB_has_dirty_io 标志以表示该写回设备有脏 IO 数据
c
/**
* inode_io_list_move_locked - 将 inode 移动到 bdi_writeback IO 列表中
* @inode:要移动的 inode
* @wb:目标bdi_writeback
* @head:@wb->b_{dirty|io|more_io|dirty_time} 之一
*
* 将 @inode->i_io_list 移动到 @wb 的 @list 并设置 %WB_has_dirty_io。
* 如果 @inode 是 !dirty_time IO 列表的第一个占用者,则返回 %true;否则,LSE.
*/
static bool inode_io_list_move_locked(struct inode *inode,
struct bdi_writeback *wb,
struct list_head *head)
{
assert_spin_locked(&wb->list_lock);
assert_spin_locked(&inode->i_lock);
/* 检查 inode 是否处于释放状态(I_FREEING) */
WARN_ON_ONCE(inode->i_state & I_FREEING);
/* 用 list_move 将 inode->i_io_list 从当前列表移动到目标列表 head。 */
list_move(&inode->i_io_list, head);
/* 如果目标列表不是 wb->b_dirty_time 设置 WB_has_dirty_io 标志*/
if (head != &wb->b_dirty_time)
return wb_io_lists_populated(wb);
/* 当目标列表是 `wb->b_dirty_time` 时,可以清除 `WB_has_dirty_io` 标志,
因为 `b_dirty_time` 列表中的 inode 不被视为真正的脏 IO 数据,直到其时间戳过期。*/
wb_io_lists_depopulated(wb);
return false;
}
1. wb->b_dirty
- 含义: 存储脏的 inode,这些 inode 的数据需要被写回磁盘。
- 用途: 这是主要的脏 inode 列表,表示需要立即处理的写回任务。
- 场景: 当 inode 的数据被修改后,它会被标记为脏并加入该列表。
2. wb->b_io
- 含义: 存储正在进行写回操作的 inode。
- 用途: 这是一个临时列表,用于存储当前正在被写回线程处理的 inode。
- 场景 : 当写回线程开始处理某个 inode 时,它会从
b_dirty移动到b_io。
3. wb->b_more_io
- 含义: 存储需要进一步写回的 inode。
- 用途: 这是一个延迟处理的列表,表示写回操作未完成,需要后续处理。
- 场景 : 当写回线程无法一次性完成某个 inode 的写回任务时,该 inode 会被移到
b_more_io。
4. wb->b_dirty_time
- 含义: 存储时间戳脏的 inode,这些 inode 的元数据(如时间戳)需要被写回磁盘。
- 用途: 专门用于管理时间戳相关的写回任务,与数据写回任务分开。
- 场景: 当 inode 的时间戳被修改但数据未修改时,它会被加入该列表。
wb_wakeup_delayed 唤醒相应的 bdi 线程,然后该线程应该负责定期后台写出脏 inode
c
/*
* "kupdate"样式写回之间的间隔
*/
unsigned int dirty_writeback_interval = 5 * 100; /* 厘秒 */
EXPORT_SYMBOL_GPL(dirty_writeback_interval);
/*
* 当此 wb 的第一个 inode 标记为 dirty 时,将使用此函数。
* 它唤醒相应的 bdi 线程,然后该线程应该负责定期后台写出脏 inode。
* 由于写出从现在开始只会开始 "dirty_writeback_interval" centisecs,因此我们只需设置一个计时器,稍后唤醒 bdi 线程。
*
* 请注意,我们不会费心设置计时器,但这个函数在快速路径上(由 '__mark_inode_dirty()' 使用),因此我们通过延迟唤醒来节省很少的上下文切换。
*
* 我们必须小心,如果计划提前进行冲洗工作,请不要推迟。因此我们使用 queue_delayed_work()。
*/
static void wb_wakeup_delayed(struct bdi_writeback *wb)
{
unsigned long timeout;
/* 使用 dirty_writeback_interval(以百分秒为单位)计算延迟时间,并将其转换为 jiffies */
timeout = msecs_to_jiffies(dirty_writeback_interval * 10);
spin_lock_irq(&wb->work_lock);
if (test_bit(WB_registered, &wb->state))
/* 调用 queue_delayed_work 将写回任务添加到延迟工作队列中,并设置唤醒时间点 */
queue_delayed_work(bdi_wq, &wb->dwork, timeout);
spin_unlock_irq(&wb->work_lock);
}
__mark_inode_dirty 用于将 inode 标记为脏
c
/**
* __mark_inode_dirty - inode 标记为脏
*
* @inode: 要标记的 inode
* @flags: 指定脏的类型,可以是多个 I_DIRTY_* 标志的组合,但 I_DIRTY_TIME 不能与 I_DIRTY_PAGES 组合使用
*
* 该函数的主要作用是标记 inode 为脏,并将其添加到适当的脏列表中,以便后续的写回操作。
* 它会通知文件系统 inode 的状态变化,并根据 flags 更新 inode 的状态。
* 注意:未哈希的 inode 不会被添加到脏列表中,即使稍后被哈希处理也不会重新处理。
*/
void __mark_inode_dirty(struct inode *inode, int flags)
{
struct super_block *sb = inode->i_sb; // 获取 inode 所属的超级块
int dirtytime = 0; // 标记是否是时间戳相关的脏状态
struct bdi_writeback *wb = NULL; // 用于管理写回的上下文
trace_writeback_mark_inode_dirty(inode, flags); // 跟踪写回事件
if (flags & I_DIRTY_INODE) { // 如果标记为 I_DIRTY_INODE,表示 inode 本身需要被标记为脏
/*
* 如果 inode 的状态中包含 I_DIRTY_TIME,表示需要更新时间戳。
* 通过加锁确保线程安全,并清除 I_DIRTY_TIME 标志,同时将其添加到 flags 中。
*/
if (inode->i_state & I_DIRTY_TIME) {
spin_lock(&inode->i_lock);
if (inode->i_state & I_DIRTY_TIME) {
inode->i_state &= ~I_DIRTY_TIME; // 清除 I_DIRTY_TIME 标志
flags |= I_DIRTY_TIME; // 将 I_DIRTY_TIME 添加到 flags 中
}
spin_unlock(&inode->i_lock);
}
/*
* 通知文件系统 inode 被标记为脏。
* 如果文件系统实现了 dirty_inode 回调函数,则调用它以更新磁盘字段或日志。
*/
trace_writeback_dirty_inode_start(inode, flags);
if (sb->s_op->dirty_inode)
sb->s_op->dirty_inode(inode, flags & (I_DIRTY_INODE | I_DIRTY_TIME));
trace_writeback_dirty_inode(inode, flags);
flags &= ~I_DIRTY_TIME; // I_DIRTY_INODE 优先于 I_DIRTY_TIME
} else {
/*
* 如果不是 I_DIRTY_INODE,则可能是 I_DIRTY_PAGES 或 I_DIRTY_TIME。
* 注意:I_DIRTY_PAGES 和 I_DIRTY_TIME 不能同时设置。
*/
dirtytime = flags & I_DIRTY_TIME;
WARN_ON_ONCE(dirtytime && flags != I_DIRTY_TIME); // 如果同时设置,发出警告
}
/*
* 内存屏障,确保 i_state 的状态更新对其他线程可见。
* 与 __writeback_single_inode() 中的 smp_mb() 配对使用。
*/
smp_mb();
if ((inode->i_state & flags) == flags) // 如果 inode 已经处于目标状态,则直接返回
return;
spin_lock(&inode->i_lock); // 加锁保护 inode 的状态更新
if ((inode->i_state & flags) != flags) { // 如果 inode 的状态需要更新
const int was_dirty = inode->i_state & I_DIRTY; // 检查 inode 是否已经是脏状态
inode_attach_wb(inode, NULL); // 将 inode 附加到写回上下文
inode->i_state |= flags; // 更新 inode 的状态
/*
* 如果 inode 之前不是脏状态,则需要将其添加到脏列表中。
* 这需要先获取写回上下文的锁。
*/
if (!was_dirty) {
wb = locked_inode_to_wb_and_lock_list(inode); // 获取写回上下文并加锁
spin_lock(&inode->i_lock); // 重新加锁 inode
}
/*
* 如果 inode 已经被写回线程处理,则只更新状态,不重新添加到列表。
*/
if (inode->i_state & I_SYNC_QUEUED)
goto out_unlock;
/*
* 只有有效的(已哈希或块设备) inode 才会被添加到超级块的脏列表中。
*/
if (!S_ISBLK(inode->i_mode)) { // 如果不是块设备
if (inode_unhashed(inode)) // 如果 inode 未哈希,则跳过
goto out_unlock;
}
if (inode->i_state & I_FREEING) // 如果 inode 正在释放,则跳过
goto out_unlock;
/*
* 如果 inode 已经在脏列表中,则不重新定位,以保持时间顺序。
*/
if (!was_dirty) {
struct list_head *dirty_list; // 指向目标脏列表
bool wakeup_bdi = false; // 是否需要唤醒写回线程
inode->dirtied_when = jiffies; // 记录脏时间
if (dirtytime)
inode->dirtied_time_when = jiffies; // 如果是时间戳相关的脏状态,记录时间戳
if (inode->i_state & I_DIRTY)
dirty_list = &wb->b_dirty; // 如果是普通脏状态,添加到 b_dirty 列表
else
dirty_list = &wb->b_dirty_time; // 如果是时间戳相关的脏状态,添加到 b_dirty_time 列表
wakeup_bdi = inode_io_list_move_locked(inode, wb, dirty_list); // 将 inode 移动到目标列表
spin_unlock(&wb->list_lock); // 解锁写回上下文
spin_unlock(&inode->i_lock); // 解锁 inode
trace_writeback_dirty_inode_enqueue(inode); // 跟踪事件
/*
* 如果这是该写回设备的第一个脏 inode,则唤醒写回线程以确保后续写回操作。
*/
if (wakeup_bdi && (wb->bdi->capabilities & BDI_CAP_WRITEBACK))
wb_wakeup_delayed(wb);
return;
}
}
out_unlock:
if (wb)
spin_unlock(&wb->list_lock); // 解锁写回上下文
spin_unlock(&inode->i_lock); // 解锁 inode
}
EXPORT_SYMBOL(__mark_inode_dirty); // 导出符号,允许其他模块调用
write_inode_now 立即将一个脏的 inode 写入磁盘
c
/**
* write_inode_now - 将 inode 写入磁盘
* @inode:写入磁盘的 inode
* @sync:写入是否应该是同步的
*
* 如果 inode 脏了,此函数会立即将 inode 提交到磁盘。这主要是 knfsd 需要的。
*
* 调用方必须在 inode 上具有 ref 或必须已设置 I_WILL_FREE。
*/
int write_inode_now(struct inode *inode, int sync)
{
struct writeback_control wbc = {
/* 设置为 LONG_MAX,表示写回操作的最大数量 */
.nr_to_write = LONG_MAX,
/* WB_SYNC_ALL:同步写回,确保数据立即写入磁盘。
WB_SYNC_NONE:异步写回,允许数据暂时保留在缓存中。 */
.sync_mode = sync ? WB_SYNC_ALL : WB_SYNC_NONE,
/* 示写回整个 inode 的数据 */
.range_start = 0,
.range_end = LLONG_MAX,
};
/* 检查 inode 的数据映射(i_mapping)是否支持写回操作 */
if (!mapping_can_writeback(inode->i_mapping))
wbc.nr_to_write = 0;
might_sleep();
return writeback_single_inode(inode, &wbc);
}
EXPORT_SYMBOL(write_inode_now);
write_inode 标记 inode(文件系统中的索引节点)为脏状态
c
static int write_inode(struct inode *inode, struct writeback_control *wbc)
{
int ret;
if (inode->i_sb->s_op->write_inode && !is_bad_inode(inode)) {
trace_writeback_write_inode_start(inode, wbc);
ret = inode->i_sb->s_op->write_inode(inode, wbc);
trace_writeback_write_inode(inode, wbc);
return ret;
}
return 0;
}
inode_wait_for_writeback 等待 inode 的写回操作完成
c
/*
* 等待 inode 上的写回完成。在持有 i_lock 的情况下被调用。
* 调用方必须确保在我们丢弃 inode 时 inode 不会消失i_lock。
*/
void inode_wait_for_writeback(struct inode *inode)
{
struct wait_bit_queue_entry wqe;
struct wait_queue_head *wq_head;
assert_spin_locked(&inode->i_lock);
if (!(inode->i_state & I_SYNC))
return;
/* 使用 inode_bit_waitqueue 初始化等待队列头(wq_head)和等待队列条目(wqe */
wq_head = inode_bit_waitqueue(&wqe, inode, __I_SYNC);
/* 等待 I_SYNC 标志被清除 */
for (;;) {
/* 当前线程设置为不可中断的等待状态 */
prepare_to_wait_event(wq_head, &wqe.wq_entry, TASK_UNINTERRUPTIBLE);
/* 使用 inode->i_lock 检查 I_SYNC 可保证内存排序。 */
if (!(inode->i_state & I_SYNC))
break;
spin_unlock(&inode->i_lock);
/* schedule 让出 CPU,等待其他线程完成写回操作 */
schedule();
spin_lock(&inode->i_lock);
}
/* 调用 finish_wait 清理等待队列条目,恢复线程的正常运行状态 */
finish_wait(wq_head, &wqe.wq_entry);
}
__writeback_single_inode 将 inode 的脏数据(包括页面和元数据)写入磁盘,并清除相关的脏标志(I_DIRTY)以维护文件系统的一致性
c
/*
* 写出一个 inode 及其脏页(或它的一些脏页,取决于 @wbc->nr_to_write),并从 i_state 中清除相关的脏标志。
*
* 这不会从它所在的写回列表中删除 inode,除非由于 timestampexpiration 可能会将其从 b_dirty_time 移动到 b_dirty。 否则,调用方负责写回列表处理。
*
* 调用方还负责事先设置 I_SYNC 标志,并在之后调用 inode_sync_complete() 将其清除。
*/
static int
__writeback_single_inode(struct inode *inode, struct writeback_control *wbc)
{
struct address_space *mapping = inode->i_mapping;
long nr_to_write = wbc->nr_to_write;
unsigned dirty;
int ret;
WARN_ON(!(inode->i_state & I_SYNC));
trace_writeback_single_inode_start(inode, wbc, nr_to_write);
/* 写回 inode 的页面数据,具体写回行为由 writeback_control(wbc)控制 */
ret = do_writepages(mapping, wbc);
/*
* 确保在写出元数据之前等待数据。
* 这对于在数据 I/O 完成时修改元数据的文件系统非常重要。
* 我们不对 sync(2) writeback 执行此作,
* 因为它有一个单独的外部 IO 完成路径和 ->sync_fs 来保证 inode 元数据被正确写回。
*/
/* 在同步模式(WB_SYNC_ALL)下,函数确保页面数据写回完成后再写回元数据
这对于依赖数据写回完成来更新元数据的文件系统非常重要 */
if (wbc->sync_mode == WB_SYNC_ALL && !wbc->for_sync) {
int err = filemap_fdatawait(mapping);
if (ret == 0)
ret = err;
}
/*
* 如果 inode 有脏时间戳,我们需要写入它们,请调用 mark_inode_dirty_sync() 通知文件系统并将 I_DIRTY_TIME 更改为 I_DIRTY_SYNC。
*/
if ((inode->i_state & I_DIRTY_TIME) &&
(wbc->sync_mode == WB_SYNC_ALL ||
time_after(jiffies, inode->dirtied_time_when +
dirtytime_expire_interval * HZ))) {
trace_writeback_lazytime(inode);
mark_inode_dirty_sync(inode);
}
/*
* 从 i_state 获取并清除脏标志。
* 这需要在调用 writepages 之后完成,因为由于 delalloc,某些文件系统可能会在 writepages 期间弄脏 inode。
* 还需要在处理时间戳过期后完成,因为这也可能弄脏 inode。
*/
/* 函数在持有锁的情况下清除 inode 的脏标志(I_DIRTY),确保状态一致性 */
spin_lock(&inode->i_lock);
dirty = inode->i_state & I_DIRTY;
inode->i_state &= ~dirty;
/*
* 与 __mark_inode_dirty() 中的 smp_mb() 配对。 这允许 __mark_inode_dirty() 在不抓取 i_lock 的情况下测试 i_state - 要么他们看到 I_DIRTY 位被清除,要么我们看到脏污的 inode。
*
* I_DIRTY_PAGES 总是一起清除上面的内容@mapping即使它仍然有脏页。 如有必要,该标志将在 smp_mb() 后恢复。 这保证了 __mark_inode_dirty() 看到清晰的I_DIRTY_PAGES或者我们看到 PAGECACHE_TAG_DIRTY。
*/
smp_mb();
/* 如果页面缓存中仍有脏页面,函数重新设置 I_DIRTY_PAGES 标志,表示页面数据仍需写回。 */
if (mapping_tagged(mapping, PAGECACHE_TAG_DIRTY))
inode->i_state |= I_DIRTY_PAGES;
else if (unlikely(inode->i_state & I_PINNING_NETFS_WB)) {
if (!(inode->i_state & I_DIRTY_PAGES)) {
inode->i_state &= ~I_PINNING_NETFS_WB;
wbc->unpinned_netfs_wb = true;
dirty |= I_PINNING_NETFS_WB; /* Cause write_inode */
}
}
spin_unlock(&inode->i_lock);
/* 如果 inode 的脏标志不只是页面数据(例如元数据) */
if (dirty & ~I_DIRTY_PAGES) {
int err = write_inode(inode, wbc);
if (ret == 0)
ret = err;
}
wbc->unpinned_netfs_wb = false;
trace_writeback_single_inode(inode, wbc, nr_to_write);
return ret;
}
writeback_single_inode 将单个 inode 写回磁盘
c
/*
* 按需将 inode 的脏数据和元数据写入磁盘。这种写回操作与由刷新线程(flusher threads)执行的批量写回不同,它是单独触发的,并由 writeback_control(wbc)结构控制写回的具体行为,例如是否执行数据完整性同步(WB_SYNC_ALL)或异步写回(WB_SYNC_NONE)
*
* 为防止 inode 消失,调用方必须具有对 inode 的引用,或者 inode 必须设置 I_WILL_FREE 或 I_FREEING。
*/
static int writeback_single_inode(struct inode *inode,
struct writeback_control *wbc)
{
struct bdi_writeback *wb;
int ret = 0;
spin_lock(&inode->i_lock);
if (!atomic_read(&inode->i_count))
/* 为零,则 inode 必须处于 I_WILL_FREE 或 I_FREEING 状态 */
WARN_ON(!(inode->i_state & (I_WILL_FREE|I_FREEING)));
else
WARN_ON(inode->i_state & I_WILL_FREE);
if (inode->i_state & I_SYNC) {
/*
* 写回已在 inode 上运行。
* 对于异步写回(WB_SYNC_NONE),直接返回。
* 对于同步写回(WB_SYNC_ALL),等待当前写回完成后再继续。
*/
if (wbc->sync_mode != WB_SYNC_ALL)
goto out;
inode_wait_for_writeback(inode);
}
WARN_ON(inode->i_state & I_SYNC);
/*
* 如果 inode 已经完全干净(没有脏数据或元数据),且不需要数据完整性同步
*
* 对于数据完整性同步,还需要检查页面缓存是否有正在写回的页面
*/
if (!(inode->i_state & I_DIRTY_ALL) &&
(wbc->sync_mode != WB_SYNC_ALL ||
!mapping_tagged(inode->i_mapping, PAGECACHE_TAG_WRITEBACK)))
goto out;
/* 将 inode 状态标记为 I_SYNC,表示写回操作正在进 */
inode->i_state |= I_SYNC;
/* spin_unlock(&inode->i_lock); */
wbc_attach_and_unlock_inode(wbc, inode);
/* 进行实际的写回操作,并传递 wbc 控制写回行为 */
ret = __writeback_single_inode(inode, wbc);
/* 写回完成后,解除 wbc 与 inode 的关联 */
wbc_detach_inode(wbc);
wb = inode_to_wb_and_lock_list(inode);
spin_lock(&inode->i_lock);
/*
* 如果 inode 已经完全干净,则将其从写回列表中移除
*/
if (!(inode->i_state & I_FREEING)) {
/*
* 如果仍有脏数据,根据状态更新写回列表,例如将 inode 移动到适当的队列中。
*/
if (!(inode->i_state & I_DIRTY_ALL))
inode_cgwb_move_to_attached(inode, wb);
else if (!(inode->i_state & I_SYNC_QUEUED)) {
if ((inode->i_state & I_DIRTY))
redirty_tail_locked(inode, wb);
else if (inode->i_state & I_DIRTY_TIME) {
inode->dirtied_when = jiffies;
inode_io_list_move_locked(inode,
wb,
&wb->b_dirty_time);
}
}
}
spin_unlock(&wb->list_lock);
/* 写回操作完成后,清除 I_SYNC 状态,表示同步结束 */
inode_sync_complete(inode);
out:
spin_unlock(&inode->i_lock);
return ret;
}
sync_inodes_sb: 同步并等待单个文件系统的Inode写回
此函数是Linux VFS(虚拟文件系统)层中一个至关重要的I/O同步函数。它的核心作用是为一个指定的文件系统(sb), 强制启动其所有脏Inode(包括其关联的数据页)的写回(writeback)操作, 并且阻塞等待, 直到这些写回操作全部完成。它提供了从"数据在内存"到"数据已提交到存储设备"的强一致性保证。
该函数的原理是利用内核中复杂且高效的后台写回(background writeback)机制, 但以一种同步的方式来驱动它:
-
构建"工作订单" : 函数首先会创建一个
wb_writeback_work结构体。这可以被理解为一个详细的"工作订单", 它精确地描述了需要执行的写回任务:sb: 任务目标是哪个文件系统。sync_mode = WB_SYNC_ALL: 任务模式是"全部同步", 意味着不考虑任何延迟策略, 必须写回所有脏数据。nr_pages = LONG_MAX: 任务范围是"无限", 再次强调要写回所有脏页。done = &done: 这是关键的同步点。它将这个工作订单与一个wb_completion完成事件关联起来。
-
提交工作并等待 : 函数并不会自己去一个一个地写数据。它调用
bdi_split_work_to_wbs将这个"工作订单"提交给与该文件系统底层块设备相关联的写回工作队列(writeback workqueues)。内核的I/O工作者线程会接收这个任务并开始在后台执行实际的I/O操作。提交任务后,sync_inodes_sb函数立即调用wb_wait_for_completion(&done), 在此处进入休眠状态, 等待 。当后台的工作者线程完成了"工作订单"中指定的所有I/O操作后, 它们会发出done事件的完成信号, 此时wb_wait_for_completion才会返回,sync_inodes_sb函数才能继续执行。 -
最终清理等待 : 在主体的写回完成后, 它还会调用
wait_sb_inodes。这是一个额外的、更强的保证步骤, 用于等待那些可能由其他原因(并非本次sync调用)发起但尚未完成的Inode I/O。
在STM32H750这样的单核抢占式系统上, 这种机制依然至关重要。虽然不存在多核并行执行, 但并发依然存在于任务与中断之间, 或高优先级任务对低优先级任务的抢占。
bdi_down_write_wb_switch_rwsem等锁机制可以防止在提交写回任务时, I/O队列的结构被其他任务(例如, 响应系统负载变化的管理任务)修改, 保证了任务提交的原子性。- 将实际I/O操作推迟到内核工作者线程, 并通过完成事件进行同步, 是一个标准的内核设计模式, 它将I/O发起者(可能是任何任务)与I/O执行者(专职的工作者线程)解耦, 使得代码结构更清晰, 更容易管理复杂的I/O状态。
c
/**
* sync_inodes_sb - 同步一个超级块的所有inode页面
* @sb: 超级块
*
* 这个函数会写回并等待属于此超级块的任何脏inode.
*/
void sync_inodes_sb(struct super_block *sb)
{
/*
* 获取与该超级块关联的 backing_dev_info (BDI) 结构体.
* BDI 代表了底层块设备的I/O能力和状态, 是所有写回操作的管理核心.
*/
struct backing_dev_info *bdi = sb->s_bdi;
/*
* DEFINE_WB_COMPLETION 是一个宏, 用于定义并初始化一个名为'done'的写回完成事件.
* 这个事件将用于阻塞等待, 直到写回操作完成.
*/
DEFINE_WB_COMPLETION(done, bdi);
/*
* 定义并初始化一个 wb_writeback_work 结构体.
* 这相当于创建一个详细的 "工作订单".
*/
struct wb_writeback_work work = {
.sb = sb, // 目标是这个超级块
.sync_mode = WB_SYNC_ALL, // 同步模式: 全部同步, 不留任何脏数据
.nr_pages = LONG_MAX, // 要写的页数: 无限, 即全部
.range_cyclic = 0, // 不进行循环扫描
.done = &done, // 操作完成后, 发信号给 'done' 这个完成事件
.reason = WB_REASON_SYNC, // 发起原因: sync调用
.for_sync = 1, // 这是一个同步操作
};
/*
* 检查BDI是否为 noop_backing_dev_info.
* 这是一个特殊的BDI, 用于那些没有持久化存储的文件系统 (如 ramfs).
* 如果是, 说明没有地方可以写回, 直接返回.
*/
if (bdi == &noop_backing_dev_info)
return;
/*
* 这是一个内核断言. 它警告开发者, 如果在调用此函数时,
* 没有持有超级块的 s_umount 读写信号量, 这是一个潜在的bug.
* 这个锁可以防止在同步过程中, 文件系统被另一个线程卸载.
*/
WARN_ON(!rwsem_is_locked(&sb->s_umount));
/*
* 获取BDI的写回切换写锁.
* 这是一个内部锁, 用于保护BDI的写回队列列表, 防止在提交工作时该列表发生变化.
*/
bdi_down_write_wb_switch_rwsem(bdi);
/*
* 将 "工作订单" work 分发给BDI管理的各个写回工作队列.
* 这一步会唤醒内核的工作者线程, 它们将开始执行实际的I/O操作.
*/
bdi_split_work_to_wbs(bdi, &work, false);
/*
* 等待 'done' 事件的完成信号.
* 函数将在此处休眠, 直到所有由 bdi_split_work_to_wbs 启动的I/O操作都已完成.
* 这是将异步的后台写回操作转换为同步等待的关键.
*/
wb_wait_for_completion(&done);
/*
* 释放写回切换写锁.
*/
bdi_up_write_wb_switch_rwsem(bdi);
/*
* 调用 wait_sb_inodes. 这是一个额外的等待步骤.
* 它确保超级块的 s_io 和 s_dirty 列表上所有标记为 I_SYNC 的inode
* (可能由其他并发操作发起)都已经完成了它们的I/O.
* 这提供了更强的完成保证.
*/
wait_sb_inodes(sb);
}
/*
* 将 sync_inodes_sb 函数导出, 使其对其他内核模块可用.
*/
EXPORT_SYMBOL(sync_inodes_sb);
脏时间 Inode 周期性回写:start_dirtytime_writeback 与 wakeup_dirtytime_writeback
本代码片段实现了一个内核后台的回写(writeback)安全网机制。其核心功能是周期性地唤醒系统的回写线程,以确保那些仅因时间戳(如访问时间 atime)更新而被标记为"脏"的 inode 能够被最终写入持久存储。这个机制通过一个可配置的、自调度(self-scheduling)的延迟工作队列(delayed work)来实现,弥补了常规回写机制主要由脏数据页驱动的不足,从而保证了文件系统元数据的最终一致性。
实现原理分析
该机制是内核 I/O 子系统中一个精妙的、用于保证数据完整性的设计,它由一个周期性任务和一个用户配置接口组成。
-
问题的根源 : 当一个文件被读取时,其 inode 的访问时间(atime)会被更新。这使得 inode 在内存中变为"脏"状态。然而,因为没有文件数据被修改,所以不会有脏的数据页(dirty pages)产生。内核的回写机制(flusher threads)主要由脏数据页的阈值来驱动。因此,在一个 I/O 不频繁的系统上,一个仅有时间戳更新的 inode 可能会无限期地停留在内存中,如果此时系统崩溃,更新后的时间戳将会丢失。
-
延迟工作队列 (
delayed_work) 作为周期性定时器:start_dirtytime_writeback函数在内核启动时被调用,它做的第一件事就是调用schedule_delayed_work来安排dirtytime_work在dirtytime_expire_interval秒之后首次执行。- 当
dirtytime_work的处理函数wakeup_dirtytime_writeback执行到最后时,它会再次调用schedule_delayed_work来重新安排下一次的执行。这种"自我调度"的模式,将一个延迟工作队列变成了一个周期性的定时任务。
-
全局扫描与唤醒 (
wakeup_dirtytime_writeback):- 这个工作队列的处理函数并不亲自执行 I/O 操作。它的职责是遍历系统中所有后端存储设备(
backing_dev_info)的所有回写队列(bdi_writeback)。 - RCU 安全遍历 : 遍历
bdi_list和wb_list这两个可能被并发修改的全局链表时,使用了rcu_read_lock进行保护。这允许在不阻塞设备注册/注销的情况下安全地进行只读遍历。 - 检查目标 : 对于每一个回写队列
wb,它检查其b_dirty_time链表是否为空。这个链表专门用于挂接那些仅因时间戳而变脏的 inode。 - 唤醒而非回写 : 如果
b_dirty_time链表非空,它就调用wb_wakeup(wb)。这个函数仅仅是唤醒与该队列关联的内核回写线程(flusher thread)。被唤醒的线程接下来会按照标准流程检查其队列,发现b_dirty_time链表上有待处理的 inode,然后将它们写回磁盘。这种设计将"发现问题"的策略与"解决问题"的执行完全解耦。
- 这个工作队列的处理函数并不亲自执行 I/O 操作。它的职责是遍历系统中所有后端存储设备(
-
用户空间可配置性 (
sysctl):start_dirtytime_writeback还通过register_sysctl_init注册了一个 sysctl 接口,即/proc/sys/vm/dirtytime_expire_seconds。这允许系统管理员在运行时动态调整检查周期。dirtytime_interval_handler是这个 sysctl 的处理函数。它有一个重要的附加逻辑:当用户写入 一个新的时间间隔时,它会调用mod_delayed_work(..., 0)。这个调用会立即修改定时器(如果正在等待),并以0秒的延迟重新调度它,这意味着工作队列会"几乎立即"执行一次,然后开始按新的时间间隔进行周期调度。这使得配置的更改能够迅速生效。
代码分析
c
/**
* @var dirtytime_expire_interval
* @brief "脏时间" inode 回写检查的周期,单位为秒。
*
* 默认值为12小时。这是周期性任务 wakeup_dirtytime_writeback 的执行间隔。
*/
static unsigned int dirtytime_expire_interval = 12 * 60 * 60;
// 前向声明 work_struct 的处理函数。
static void wakeup_dirtytime_writeback(struct work_struct *w);
// 声明一个名为 dirtytime_work 的延迟工作队列项,其处理函数为 wakeup_dirtytime_writeback。
static DECLARE_DELAYED_WORK(dirtytime_work, wakeup_dirtytime_writeback);
/**
* @brief wakeup_dirtytime_writeback - 周期性任务,用于唤醒回写线程处理脏时间 inode。
* @param w: 指向 work_struct 的指针 (未使用)。
*/
static void wakeup_dirtytime_writeback(struct work_struct *w)
{
struct backing_dev_info *bdi; /// < 用于遍历 bdi 链表的指针。
// 进入 RCU 读侧临界区,以安全地遍历 bdi_list。
rcu_read_lock();
// RCU 安全地遍历系统中所有已注册的后端设备信息 (bdi)。
list_for_each_entry_rcu(bdi, &bdi_list, bdi_list) {
struct bdi_writeback *wb; /// < 用于遍历 bdi 内 wb_list 的指针。
// RCU 安全地遍历该 bdi 下所有的回写工作器 (wb)。
list_for_each_entry_rcu(wb, &bdi->wb_list, bdi_node)
// 如果该回写工作器的 b_dirty_time 链表非空 (即存在脏时间inode),
if (!list_empty(&wb->b_dirty_time))
// 则唤醒对应的回写内核线程 (flusher thread) 来处理它。
wb_wakeup(wb);
}
// 退出 RCU 读侧临界区。
rcu_read_unlock();
// 重新调度此任务,在下一个 dirtytime_expire_interval 周期后再次执行。
schedule_delayed_work(&dirtytime_work, dirtytime_expire_interval * HZ);
}
/**
* @brief dirtytime_interval_handler - /proc/sys/vm/dirtytime_expire_seconds 的处理函数。
* @param table: 指向 ctl_table 结构的指针。
* @param write: 标志位,非0表示写操作。
* @param buffer: 指向用户空间数据的缓冲区。
* @param lenp: 指向数据长度的指针。
* @param ppos: 指向文件偏移量的指针。
* @return int: 成功返回0,否则返回错误码。
*/
static int dirtytime_interval_handler(const struct ctl_table *table, int write,
void *buffer, size_t *lenp, loff_t *ppos)
{
int ret;
// 使用内核提供的标准函数来处理整数类型的 sysctl 读写。
ret = proc_dointvec_minmax(table, write, buffer, lenp, ppos);
// 如果写入操作成功...
if (ret == 0 && write)
// ...则立即以0延迟重新调度 dirtytime_work,使新设置的间隔尽快生效。
mod_delayed_work(system_percpu_wq, &dirtytime_work, 0);
return ret;
}
// 定义 sysctl 表,用于在 /proc/sys/vm/ 目录下创建文件。
static const struct ctl_table vm_fs_writeback_table[] = {
{
.procname = "dirtytime_expire_seconds", // 文件名
.data = &dirtytime_expire_interval, // 关联的内核变量
.maxlen = sizeof(dirtytime_expire_interval),// 最大长度
.mode = 0644, // 文件权限
.proc_handler = dirtytime_interval_handler, // 读写处理函数
.extra1 = SYSCTL_ZERO, // 最小值约束为0
},
};
/**
* @brief start_dirtytime_writeback - 初始化脏时间回写机制的函数。
* @return int: 始终返回0。
*/
static int __init start_dirtytime_writeback(void)
{
// 安排第一次的延迟工作队列任务。
schedule_delayed_work(&dirtytime_work, dirtytime_expire_interval * HZ);
// 注册上述定义的 sysctl 表。
register_sysctl_init("vm", vm_fs_writeback_table);
return 0;
}
// 使用 __initcall 宏,确保该初始化函数在内核启动过程的早期被调用。
__initcall(start_dirtytime_writeback);