[Linux]学习笔记系列 -- [fs]fs-writeback


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)
    • [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)

https://github.com/wdfk-prog/linux-study

fs-writeback

fs-writeback.c 是 Linux 内核中负责处理文件系统数据写回(writeback)机制的核心模块。它的主要功能是管理脏页和脏 inode 的写回操作,以确保数据从内存缓冲区最终写入磁盘。以下是对其原理、使用场景、优缺点以及其他方案的详细阐述:


原理

  1. 脏页与脏 inode:

    • 当文件系统中的数据被修改时,内核会将这些修改暂时存储在内存中(页缓存或 inode 缓存),并标记为"脏"。
    • 脏页指的是页缓存中未写入磁盘的修改数据,脏 inode 则是文件元数据(如时间戳、权限等)的未写入部分。
  2. 写回机制:

    • fs-writeback.c 通过后台线程(flusher threads)定期扫描脏页和脏 inode,并将它们写入磁盘。
    • 写回操作可以是异步的(WB_SYNC_NONE),也可以是同步的(WB_SYNC_ALL),具体取决于调用场景。
  3. 核心组件:

    • bdi_writeback: 表示一个设备的写回上下文,管理该设备的脏页和脏 inode。
    • wb_writeback_work: 描述写回任务的结构体,包括写回页数、同步模式等。
    • wb_workfn: 写回线程的主函数,负责执行写回任务。
  4. 调度与触发:

    • 写回任务可以由以下事件触发:
      • 内存压力(如内存不足时触发写回以释放页缓存)。
      • 定期写回(如 dirty_writeback_interval 配置的时间间隔)。
      • 显式调用(如 sync 系统调用或 fsync 文件操作)。

使用场景

  1. 数据完整性:

    • 确保文件系统的数据和元数据最终写入磁盘,避免数据丢失。
    • 在系统崩溃或断电时,脏页和脏 inode 的及时写回可以减少数据丢失的风险。
  2. 性能优化:

    • 写回机制通过延迟写入操作,减少频繁的磁盘 I/O,提高系统性能。
    • 通过批量写回,优化磁盘写入效率。
  3. 文件系统操作:

    • 文件系统的 syncfsync 操作依赖写回机制来确保数据一致性。
    • 数据库等应用程序通常使用 fsync 来确保事务的持久性。

优缺点

优点
  1. 性能提升:

    • 延迟写入减少了磁盘 I/O 的频率,提高了系统的整体性能。
    • 批量写回优化了磁盘的写入效率。
  2. 灵活性:

    • 支持多种写回模式(异步、同步),适应不同场景的需求。
    • 可配置参数(如 dirty_writeback_intervaldirty_ratio)允许用户根据系统负载调整写回行为。
  3. 数据安全性:

    • 通过定期写回和显式触发写回,确保数据最终写入磁盘,减少数据丢失的风险。
缺点
  1. 延迟风险:

    • 延迟写入可能导致数据丢失,尤其是在系统崩溃或断电时。
    • 如果写回线程无法及时处理脏页,可能导致内存压力增加。
  2. 复杂性:

    • 写回机制涉及多个线程和复杂的调度逻辑,可能增加代码维护难度。
    • 在高负载场景下,写回线程可能成为性能瓶颈。
  3. I/O 干扰:

    • 批量写回可能导致突发的磁盘 I/O 峰值,影响其他 I/O 操作的性能。

其他方案

1. Direct I/O
  • 原理: 直接将数据写入磁盘,绕过页缓存。
  • 优点 :
    • 减少内存使用。
    • 提供更低的写入延迟。
  • 缺点 :
    • 性能可能较低,尤其是小块数据写入。
    • 不支持缓存优化。
2. Journaling 文件系统
  • 原理: 使用日志记录文件系统的元数据和数据操作,确保一致性。
  • 优点 :
    • 提供更高的数据可靠性。
    • 快速恢复能力。
  • 缺点 :
    • 增加了额外的磁盘写入开销。
3. 用户空间缓存
  • 原理: 应用程序在用户空间实现自己的缓存机制。
  • 优点 :
    • 应用程序可以完全控制数据写入时机。
  • 缺点 :
    • 增加了开发复杂性。
    • 需要额外的内存管理。
4. 内存映射文件(mmap)
  • 原理: 将文件映射到内存,直接操作内存中的数据。
  • 优点 :
    • 提供高效的文件访问。
  • 缺点 :
    • 需要显式调用 msyncmunmap 来确保数据写入磁盘。

总结

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)机制, 但以一种同步的方式来驱动它:

  1. 构建"工作订单" : 函数首先会创建一个wb_writeback_work结构体。这可以被理解为一个详细的"工作订单", 它精确地描述了需要执行的写回任务:

    • sb: 任务目标是哪个文件系统。
    • sync_mode = WB_SYNC_ALL: 任务模式是"全部同步", 意味着不考虑任何延迟策略, 必须写回所有脏数据。
    • nr_pages = LONG_MAX: 任务范围是"无限", 再次强调要写回所有脏页。
    • done = &done: 这是关键的同步点。它将这个工作订单与一个wb_completion完成事件关联起来。
  2. 提交工作并等待 : 函数并不会自己去一个一个地写数据。它调用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函数才能继续执行。

  3. 最终清理等待 : 在主体的写回完成后, 它还会调用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 子系统中一个精妙的、用于保证数据完整性的设计,它由一个周期性任务和一个用户配置接口组成。

  1. 问题的根源 : 当一个文件被读取时,其 inode 的访问时间(atime)会被更新。这使得 inode 在内存中变为"脏"状态。然而,因为没有文件数据被修改,所以不会有脏的数据页(dirty pages)产生。内核的回写机制(flusher threads)主要由脏数据页的阈值来驱动。因此,在一个 I/O 不频繁的系统上,一个仅有时间戳更新的 inode 可能会无限期地停留在内存中,如果此时系统崩溃,更新后的时间戳将会丢失。

  2. 延迟工作队列 (delayed_work) 作为周期性定时器:

    • start_dirtytime_writeback 函数在内核启动时被调用,它做的第一件事就是调用 schedule_delayed_work 来安排 dirtytime_workdirtytime_expire_interval 秒之后首次执行。
    • dirtytime_work 的处理函数 wakeup_dirtytime_writeback 执行到最后时,它会再次调用 schedule_delayed_work 来重新安排下一次的执行。这种"自我调度"的模式,将一个延迟工作队列变成了一个周期性的定时任务。
  3. 全局扫描与唤醒 (wakeup_dirtytime_writeback):

    • 这个工作队列的处理函数并不亲自执行 I/O 操作。它的职责是遍历系统中所有后端存储设备(backing_dev_info)的所有回写队列(bdi_writeback)。
    • RCU 安全遍历 : 遍历 bdi_listwb_list 这两个可能被并发修改的全局链表时,使用了 rcu_read_lock 进行保护。这允许在不阻塞设备注册/注销的情况下安全地进行只读遍历。
    • 检查目标 : 对于每一个回写队列 wb,它检查其 b_dirty_time 链表是否为空。这个链表专门用于挂接那些仅因时间戳而变脏的 inode。
    • 唤醒而非回写 : 如果 b_dirty_time 链表非空,它就调用 wb_wakeup(wb)。这个函数仅仅是唤醒与该队列关联的内核回写线程(flusher thread)。被唤醒的线程接下来会按照标准流程检查其队列,发现 b_dirty_time 链表上有待处理的 inode,然后将它们写回磁盘。这种设计将"发现问题"的策略与"解决问题"的执行完全解耦。
  4. 用户空间可配置性 (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);
相关推荐
遇见火星2 小时前
详解 Linux 中的 /etc/fstab 文件
linux·运维·服务器
charlie1145141912 小时前
嵌入式现代C++教程:C++98——从C向C++的演化(3)
c语言·开发语言·c++·笔记·学习·嵌入式
menggb072 小时前
在Linux系统上安装和使用Prometheus+Grafana
linux·运维·prometheus
RanceGru2 小时前
LLM学习笔记8——多模态CLIP、ViLT、ALBEF、VLMo、BLIP
笔记·学习
中屹指纹浏览器2 小时前
动态IP场景下指纹浏览器的实时协同适配技术研究与实现
经验分享·笔记
2501_941148153 小时前
从边缘节点到云端协同的分布式缓存一致性实现原理实践解析与多语言代码示例分享笔记集录稿
笔记·分布式·物联网·缓存
AI视觉网奇3 小时前
audio2face ue插件形式实战笔记
笔记·ue5
wregjru3 小时前
【操作系统】linux常用指令
linux·运维·服务器
华舞灵瞳3 小时前
学习FPGA(七)正弦信号合成
学习·fpga开发