【linux内核分析-存储】EXT4源码分析之“文件删除”原理【七万字超长合并版】(源码+关键细节分析)

EXT4源码分析之"文件删除"原理【七万字超长合并版】(源码+关键细节分析),详细的跟踪了ext4文件删除的核心调用链,分析关键函数的细节,解答了开篇中提出的三个核心疑问。


文章目录

  • 提示
  • 前言
  • 全文重点索引
  • 1.源码解析
    • [1.1 入口函数ext4_unlink](#1.1 入口函数ext4_unlink)
    • [1.2 找到待删除目录项ext4_find_entry](#1.2 找到待删除目录项ext4_find_entry)
      • [1.2.1 主体流程](#1.2.1 主体流程)
      • [1.2.2 目录内联](#1.2.2 目录内联)
      • [1.2.3 htree索引](#1.2.3 htree索引)
    • [1.3 同步标志](#1.3 同步标志)
      • [1.3.1 判断同步标志](#1.3.1 判断同步标志)
      • [1.3.2 设置同步标志](#1.3.2 设置同步标志)
      • [1.3.3 若是同步](#1.3.3 若是同步)
      • [1.3.4 若不是同步](#1.3.4 若不是同步)
    • [1.4 删除目录项ext4_delete_entry](#1.4 删除目录项ext4_delete_entry)
      • [1.4.1 主体流程](#1.4.1 主体流程)
      • [1.4.2 关键细节](#1.4.2 关键细节)
    • [1.5 释放inode](#1.5 释放inode)
      • [1.5.1 为什么只减少引用](#1.5.1 为什么只减少引用)
      • [1.5.2 Orphan机制介绍](#1.5.2 Orphan机制介绍)
      • [1.5.3 添加至孤立列表 ext4_orphan_add源码分析](#1.5.3 添加至孤立列表 ext4_orphan_add源码分析)
      • [1.5.4 实际清除 ext4_orphan_cleanup源码分析](#1.5.4 实际清除 ext4_orphan_cleanup源码分析)
      • [1.5.5 inode截断 ext4_truncate源码分析(块释放核心点)](#1.5.5 inode截断 ext4_truncate源码分析(块释放核心点))
        • [1.5.5.1 extent模式下的截断 ext4_ext_truncate源码分析](#1.5.5.1 extent模式下的截断 ext4_ext_truncate源码分析)
        • [1.5.5.2 核心块释放点 ext4_free_blocks源码分析](#1.5.5.2 核心块释放点 ext4_free_blocks源码分析)
        • [1.5.5.3 间接索引模式下的截断 ext4_ind_truncate源码分析](#1.5.5.3 间接索引模式下的截断 ext4_ind_truncate源码分析)
      • [1.5.6 inode删除 iput源码分析](#1.5.6 inode删除 iput源码分析)
        • [1.5.6.1 fs层通用逻辑](#1.5.6.1 fs层通用逻辑)
        • [1.5.6.2 EXT4中的evict_inode源码分析](#1.5.6.2 EXT4中的evict_inode源码分析)
  • 2.文件删除一览流程图
  • 3.总结
  • 4.参考

提示

本文是超长合并版,全文7.2万字左右(包括源码分析),请参照索引按需阅读或阅读小节版本!!!

前言

这是系列的第4篇文章,在前面的几篇文章中,我们研究了文件的创建、文件的写入,今天主要分析文件的删除,当我们删除了一个文件,底层到底发生了什么样的事情,让我们一起分析源码来看看吧!

在开始前,我们可以利用现有的知识(前面几篇文章的内容)猜测一下文件删除的主要逻辑:

  1. 释放相关inode,从目录中移除。
  2. 释放文件所占用的块。

大致思路肯定跑不开这几步,但是有一些细节值得我们去源码中寻找:

1. 释放inode和目录项,是否清空了这里面的数据?
2. 这些操作什么时候会同步到磁盘上?
3. 释放文件所占用的块后,这些块会被清空吗?

让我们带着疑问,去源码中一探究竟吧!

注:这篇文章我们换个思路,先去研究源码,再探讨里面的细节,最后画出文件删除的整体流程图。

全文重点索引

由于本文从入口开始跟踪源码并逐个分析函数,所以内容非常多,为了方便读者迅速从中得到重要信息,做了这个重点索引。

章节1的整体流程是从入口函数开始,一直跟踪到最终的删除逻辑。

序号 内容 关键函数 细节
1 ext4中的删除入口函数 [ext4_unlink(章节1.1)](#序号 内容 关键函数 细节 1 ext4中的删除入口函数 ext4_unlink(章节1.1) 在正式跟进关键删除流程前,会分析一些主流程中的东西 2 找到待删除的目录项 ext4_find_entry (章节1.2) 分析是如何查找目录项的 3 哈希索引 ext4_dx_find_entry (章节1.2.3) 分析查找目录项时对哈希索引的使用 4 同步标志 文字描述 (章节1.3) 文字描述EXT4中是如何使用同步的,解决问题2 5 删除目录项 ext4_generic_delete_entry (章节1.4) 分析了两个核心源码,清除了如何是如何删除目录项的,解决问题1的一半 6 inode添加到孤立列表 ext4_orphan_add (章节1.5.3) 分析了孤立列表机制,以及inode如何放入孤立列表的 7 inode对应数据块核心清理过程 ext4_truncate (章节1.5.5) 深入了分析ext4是如何释放真正存储数据的块的,解决问题3 8 inode本身结构的清理过程 ext4_evict_inode (章节1.5.6.2) 深入了分析ext4是如何释放真正存储数据的块的,解决问题1的另一半) 在正式跟进关键删除流程前,会分析一些主流程中的东西
2 找到待删除的目录项 [ext4_find_entry (章节1.2)](#序号 内容 关键函数 细节 1 ext4中的删除入口函数 ext4_unlink(章节1.1) 在正式跟进关键删除流程前,会分析一些主流程中的东西 2 找到待删除的目录项 ext4_find_entry (章节1.2) 分析是如何查找目录项的 3 哈希索引 ext4_dx_find_entry (章节1.2.3) 分析查找目录项时对哈希索引的使用 4 同步标志 文字描述 (章节1.3) 文字描述EXT4中是如何使用同步的,解决问题2 5 删除目录项 ext4_generic_delete_entry (章节1.4) 分析了两个核心源码,清除了如何是如何删除目录项的,解决问题1的一半 6 inode添加到孤立列表 ext4_orphan_add (章节1.5.3) 分析了孤立列表机制,以及inode如何放入孤立列表的 7 inode对应数据块核心清理过程 ext4_truncate (章节1.5.5) 深入了分析ext4是如何释放真正存储数据的块的,解决问题3 8 inode本身结构的清理过程 ext4_evict_inode (章节1.5.6.2) 深入了分析ext4是如何释放真正存储数据的块的,解决问题1的另一半) 分析是如何查找目录项的
3 哈希索引 [ext4_dx_find_entry (章节1.2.3)](#序号 内容 关键函数 细节 1 ext4中的删除入口函数 ext4_unlink(章节1.1) 在正式跟进关键删除流程前,会分析一些主流程中的东西 2 找到待删除的目录项 ext4_find_entry (章节1.2) 分析是如何查找目录项的 3 哈希索引 ext4_dx_find_entry (章节1.2.3) 分析查找目录项时对哈希索引的使用 4 同步标志 文字描述 (章节1.3) 文字描述EXT4中是如何使用同步的,解决问题2 5 删除目录项 ext4_generic_delete_entry (章节1.4) 分析了两个核心源码,清除了如何是如何删除目录项的,解决问题1的一半 6 inode添加到孤立列表 ext4_orphan_add (章节1.5.3) 分析了孤立列表机制,以及inode如何放入孤立列表的 7 inode对应数据块核心清理过程 ext4_truncate (章节1.5.5) 深入了分析ext4是如何释放真正存储数据的块的,解决问题3 8 inode本身结构的清理过程 ext4_evict_inode (章节1.5.6.2) 深入了分析ext4是如何释放真正存储数据的块的,解决问题1的另一半) 分析查找目录项时对哈希索引的使用
4 同步标志 [文字描述 (章节1.3)](#序号 内容 关键函数 细节 1 ext4中的删除入口函数 ext4_unlink(章节1.1) 在正式跟进关键删除流程前,会分析一些主流程中的东西 2 找到待删除的目录项 ext4_find_entry (章节1.2) 分析是如何查找目录项的 3 哈希索引 ext4_dx_find_entry (章节1.2.3) 分析查找目录项时对哈希索引的使用 4 同步标志 文字描述 (章节1.3) 文字描述EXT4中是如何使用同步的,解决问题2 5 删除目录项 ext4_generic_delete_entry (章节1.4) 分析了两个核心源码,清除了如何是如何删除目录项的,解决问题1的一半 6 inode添加到孤立列表 ext4_orphan_add (章节1.5.3) 分析了孤立列表机制,以及inode如何放入孤立列表的 7 inode对应数据块核心清理过程 ext4_truncate (章节1.5.5) 深入了分析ext4是如何释放真正存储数据的块的,解决问题3 8 inode本身结构的清理过程 ext4_evict_inode (章节1.5.6.2) 深入了分析ext4是如何释放真正存储数据的块的,解决问题1的另一半) 文字描述EXT4中是如何使用同步的,解决问题2
5 删除目录项 [ext4_generic_delete_entry (章节1.4)](#序号 内容 关键函数 细节 1 ext4中的删除入口函数 ext4_unlink(章节1.1) 在正式跟进关键删除流程前,会分析一些主流程中的东西 2 找到待删除的目录项 ext4_find_entry (章节1.2) 分析是如何查找目录项的 3 哈希索引 ext4_dx_find_entry (章节1.2.3) 分析查找目录项时对哈希索引的使用 4 同步标志 文字描述 (章节1.3) 文字描述EXT4中是如何使用同步的,解决问题2 5 删除目录项 ext4_generic_delete_entry (章节1.4) 分析了两个核心源码,清除了如何是如何删除目录项的,解决问题1的一半 6 inode添加到孤立列表 ext4_orphan_add (章节1.5.3) 分析了孤立列表机制,以及inode如何放入孤立列表的 7 inode对应数据块核心清理过程 ext4_truncate (章节1.5.5) 深入了分析ext4是如何释放真正存储数据的块的,解决问题3 8 inode本身结构的清理过程 ext4_evict_inode (章节1.5.6.2) 深入了分析ext4是如何释放真正存储数据的块的,解决问题1的另一半) 分析了两个核心源码,清除了如何是如何删除目录项的,解决问题1的一半
6 inode添加到孤立列表 [ext4_orphan_add (章节1.5.3)](#序号 内容 关键函数 细节 1 ext4中的删除入口函数 ext4_unlink(章节1.1) 在正式跟进关键删除流程前,会分析一些主流程中的东西 2 找到待删除的目录项 ext4_find_entry (章节1.2) 分析是如何查找目录项的 3 哈希索引 ext4_dx_find_entry (章节1.2.3) 分析查找目录项时对哈希索引的使用 4 同步标志 文字描述 (章节1.3) 文字描述EXT4中是如何使用同步的,解决问题2 5 删除目录项 ext4_generic_delete_entry (章节1.4) 分析了两个核心源码,清除了如何是如何删除目录项的,解决问题1的一半 6 inode添加到孤立列表 ext4_orphan_add (章节1.5.3) 分析了孤立列表机制,以及inode如何放入孤立列表的 7 inode对应数据块核心清理过程 ext4_truncate (章节1.5.5) 深入了分析ext4是如何释放真正存储数据的块的,解决问题3 8 inode本身结构的清理过程 ext4_evict_inode (章节1.5.6.2) 深入了分析ext4是如何释放真正存储数据的块的,解决问题1的另一半) 分析了孤立列表机制,以及inode如何放入孤立列表的
7 inode对应数据块核心清理过程 [ext4_truncate (章节1.5.5)](#序号 内容 关键函数 细节 1 ext4中的删除入口函数 ext4_unlink(章节1.1) 在正式跟进关键删除流程前,会分析一些主流程中的东西 2 找到待删除的目录项 ext4_find_entry (章节1.2) 分析是如何查找目录项的 3 哈希索引 ext4_dx_find_entry (章节1.2.3) 分析查找目录项时对哈希索引的使用 4 同步标志 文字描述 (章节1.3) 文字描述EXT4中是如何使用同步的,解决问题2 5 删除目录项 ext4_generic_delete_entry (章节1.4) 分析了两个核心源码,清除了如何是如何删除目录项的,解决问题1的一半 6 inode添加到孤立列表 ext4_orphan_add (章节1.5.3) 分析了孤立列表机制,以及inode如何放入孤立列表的 7 inode对应数据块核心清理过程 ext4_truncate (章节1.5.5) 深入了分析ext4是如何释放真正存储数据的块的,解决问题3 8 inode本身结构的清理过程 ext4_evict_inode (章节1.5.6.2) 深入了分析ext4是如何释放真正存储数据的块的,解决问题1的另一半) 深入了分析ext4是如何释放真正存储数据的块的,解决问题3
8 inode本身结构的清理过程 [ext4_evict_inode (章节1.5.6.2)](#序号 内容 关键函数 细节 1 ext4中的删除入口函数 ext4_unlink(章节1.1) 在正式跟进关键删除流程前,会分析一些主流程中的东西 2 找到待删除的目录项 ext4_find_entry (章节1.2) 分析是如何查找目录项的 3 哈希索引 ext4_dx_find_entry (章节1.2.3) 分析查找目录项时对哈希索引的使用 4 同步标志 文字描述 (章节1.3) 文字描述EXT4中是如何使用同步的,解决问题2 5 删除目录项 ext4_generic_delete_entry (章节1.4) 分析了两个核心源码,清除了如何是如何删除目录项的,解决问题1的一半 6 inode添加到孤立列表 ext4_orphan_add (章节1.5.3) 分析了孤立列表机制,以及inode如何放入孤立列表的 7 inode对应数据块核心清理过程 ext4_truncate (章节1.5.5) 深入了分析ext4是如何释放真正存储数据的块的,解决问题3 8 inode本身结构的清理过程 ext4_evict_inode (章节1.5.6.2) 深入了分析ext4是如何释放真正存储数据的块的,解决问题1的另一半) 深入了分析ext4是如何释放真正存储数据的块的,解决问题1的另一半

同时先把图放在这让读者有大致了解,如果抱着分析的态度去看,应该是分析完源码再看图。


1.源码解析

首先大胆猜测一下,文件系统中文件删除的源码位于fs/ext4/namei.c中(因为文件创建的源码位于这里,大概率删除和创建是在一个地方),至于链路到底是怎么样的,我们后面再去探究。

找到关键函数ext4_unlink

接下来开始进行源码的分析。

c 复制代码
/**
 * ext4_unlink - 删除目录中的文件条目
 * @dir: 文件所在的目录 inode
 * @dentry: 要删除的目录条目 dentry
 *
 * 此函数实现了 ext4 文件系统中删除文件的操作。它负责从目录中删除对应的目录条目,并更新相关的 inode 链接计数。
 *
 * 返回值:
 * 成功时返回 0,失败时返回负错误码。
 */
static int ext4_unlink(struct inode *dir, struct dentry *dentry)
{
    handle_t *handle = NULL; // 定义一个 journaling 句柄
    int retval;

    // 检查文件系统是否被强制关闭
    if (unlikely(ext4_forced_shutdown(EXT4_SB(dir->i_sb))))
        return -EIO;

    // 记录进入 unlink 函数的跟踪信息
    trace_ext4_unlink_enter(dir, dentry);

    // 初始化配额(如果启用)
    retval = dquot_initialize(dir);
    if (retval)
        goto out_trace;
    retval = dquot_initialize(d_inode(dentry));
    if (retval)
        goto out_trace;

    // 启动 journaling 事务
    handle = ext4_journal_start(dir, EXT4_HT_DIR,
                                EXT4_DATA_TRANS_BLOCKS(dir->i_sb));
    if (IS_ERR(handle)) {
        retval = PTR_ERR(handle);
        handle = NULL;
        goto out_trace;
    }

    // 调用核心删除函数,执行实际的删除操作
    retval = __ext4_unlink(handle, dir, &dentry->d_name, d_inode(dentry));
    if (!retval)
        ext4_fc_track_unlink(handle, dentry); // 跟踪删除操作

#if IS_ENABLED(CONFIG_UNICODE)
    /* 
     * 如果目录是大小写折叠的,可能需要使负的 dentry 失效,
     * 以确保编码和大小写不敏感的一致性。
     */
    if (IS_CASEFOLDED(dir))
        d_invalidate(dentry);
#endif

    // 停止 journaling 事务
    if (handle)
        ext4_journal_stop(handle);

out_trace:
    // 记录退出 unlink 函数的跟踪信息
    trace_ext4_unlink_exit(dentry, retval);
    return retval;
}

这个入口函数没什么好说的,主要是调用__ext4_unlink进行实际的删除操作,看这个函数的源码:

c 复制代码
int __ext4_unlink(handle_t *handle, struct inode *dir, const struct qstr *d_name,
    	  struct inode *inode)
{
	int retval = -ENOENT; // 初始化返回值为-ENOENT,表示默认文件未找到
	struct buffer_head *bh; // 定义一个指向buffer_head结构的指针,用于存储目录块的缓冲区
	struct ext4_dir_entry_2 *de; // 定义一个指向ext4_dir_entry_2结构的指针,用于存储目录项
	int skip_remove_dentry = 0; // 标志位,用于决定是否跳过删除目录项

	// 在目录dir中查找名称为d_name的目录项,并将结果存储在de中
	bh = ext4_find_entry(dir, d_name, &de, NULL);
	if (IS_ERR(bh))
		return PTR_ERR(bh); // 如果查找过程中发生错误,返回相应的错误码

	if (!bh)
		return -ENOENT; // 如果未找到目录项,返回-ENOENT错误

	// 检查找到的目录项的inode是否与目标inode相同
	if (le32_to_cpu(de->inode) != inode->i_ino) {
		/*
		 * 如果目录项的inode与目标inode不同,可能是因为目录项已被重命名。
		 * 在文件系统恢复模式下,允许跳过删除目录项。
		 */
		if (EXT4_SB(inode->i_sb)->s_mount_state & EXT4_FC_REPLAY)
			skip_remove_dentry = 1; // 设置标志位,跳过删除目录项
		else
			goto out; // 否则,跳转到结束部分,返回错误
	}

	// 如果目录设置了同步标志,进行同步处理
	if (IS_DIRSYNC(dir))
		ext4_handle_sync(handle);

	// 如果不需要跳过删除目录项,则执行删除操作
	if (!skip_remove_dentry) {
		retval = ext4_delete_entry(handle, dir, de, bh); // 删除目录项
		if (retval)
			goto out; // 如果删除失败,跳转到结束部分,返回错误
		dir->i_ctime = dir->i_mtime = current_time(dir); // 更新目录的修改时间和状态更改时间
		ext4_update_dx_flag(dir); // 更新目录的htree标志
		retval = ext4_mark_inode_dirty(handle, dir); // 标记目录inode为脏,需要写回磁盘
		if (retval)
			goto out; // 如果标记失败,跳转到结束部分,返回错误
	}
	
	// 如果目标inode的链接计数为0,发出警告
	if (inode->i_nlink == 0)
		ext4_warning_inode(inode, "Deleting file '%.*s' with no links",
				   d_name->len, d_name->name);
	else
		drop_nlink(inode); // 否则,减少inode的链接计数

	// 如果链接计数降为0,将inode添加到orphan列表,等待回收
	if (!inode->i_nlink)
		ext4_orphan_add(handle, inode);
	inode->i_ctime = current_time(inode); // 更新inode的状态更改时间
	retval = ext4_mark_inode_dirty(handle, inode); // 标记inode为脏,需要写回磁盘

out:
	brelse(bh); // 释放buffer_head资源
	return retval; // 返回操作结果
}

主要处理流程解释:

  1. 初始化和变量定义:
    • 函数开始时,将返回值retval初始化为-ENOENT,表示默认情况下文件未找到。
    • 定义了指向buffer_headext4_dir_entry_2结构的指针,用于存储目录块和目录项的信息。
    • 定义了一个标志位skip_remove_dentry,用于决定是否跳过删除目录项的操作。
  2. 查找目录项:
    • 调用ext4_find_entry函数在指定的目录dir中查找名称为d_name的目录项。
    • 如果查找过程中发生错误(即bh为错误指针),函数立即返回相应的错误码。
    • 如果未找到目录项(bh为NULL),函数返回-ENOENT错误,表示文件未找到。
  3. 验证目录项的inode:
    • 检查找到的目录项的inode是否与目标inode(即要删除的文件的inode)相同。
    • 如果不同,可能是因为该目录项已被重命名为其他inode。在这种情况下:
    • 如果文件系统处于恢复模式(EXT4_FC_REPLAY),则设置skip_remove_dentry标志为1,跳过删除目录项。
    • 否则,跳转到函数结束部分,返回错误。
  4. 同步处理:
    • 如果目录设置了同步标志(IS_DIRSYNC(dir)),调用ext4_handle_sync函数进行同步处理,确保删除操作的同步性。
  5. 删除目录项:
    • 如果不需要跳过删除目录项,调用ext4_delete_entry函数删除目录项。
    • 如果删除操作失败,跳转到函数结束部分,返回错误。
    • 删除成功后,更新目录的修改时间和状态更改时间为当前时间。
    • 调用ext4_update_dx_flag函数更新目录的htree标志,反映目录结构的变化。
    • 调用ext4_mark_inode_dirty函数标记目录的inode为脏状态,表示需要将其写回磁盘。
    • 如果标记操作失败,跳转到函数结束部分,返回错误。
  6. 处理目标inode的链接计数:
    • 检查目标inode的链接计数i_nlink是否为0:
    • 如果为0,调用ext4_warning_inode函数发出警告,提示正在删除一个没有链接的文件。
    • 否则,调用drop_nlink函数减少inode的链接计数,表示有一个硬链接被删除。
  7. 处理orphan列表:
    • 如果目标inode的链接计数降为0,调用ext4_orphan_add函数将其添加到orphan列表中,等待文件系统在后续操作中回收其数据块和inode。
  8. 更新inode的时间戳和标记为脏:
    • 更新目标inode的状态更改时间i_ctime为当前时间。
    • 调用ext4_mark_inode_dirty函数标记inode为脏状态,表示需要将其写回磁盘。
  9. 清理和返回:
    • 在函数结束部分,调用brelse函数释放buffer_head资源。
    • 返回操作的结果retval,表示删除操作的成功与否。

从这里开始就涉及很多细节了,我们从上到下一一看这些细节。

首先,是如何找到待删除的目录项的?

1.2 找到待删除目录项ext4_find_entry

1.2.1 主体流程

核心是调用了__ext4_find_entry函数,看它的源码:

c 复制代码
/*
 *	__ext4_find_entry()
 *
 * 在指定的目录中查找具有所需名称的目录项。
 * 它返回找到该目录项的缓存缓冲区,并通过参数 `res_dir` 返回该目录项本身。
 * 它不会读取目录项的 inode ------ 如果需要,您需要自行读取。
 *
 * 返回的 buffer_head 的 ->b_count 被提升。调用者应在适当的时候调用 brelse() 释放它。
 */
static struct buffer_head *__ext4_find_entry(struct inode *dir,
					     struct ext4_filename *fname,
					     struct ext4_dir_entry_2 **res_dir,
					     int *inlined)
{
	struct super_block *sb;
	struct buffer_head *bh_use[NAMEI_RA_SIZE];
	struct buffer_head *bh, *ret = NULL;
	ext4_lblk_t start, block;
	const u8 *name = fname->usr_fname->name;
	size_t ra_max = 0;	/* 预读缓冲区 bh_use[] 中的 buffer_head 数量 */
	size_t ra_ptr = 0;	/* 当前预读缓冲区的索引 */
	ext4_lblk_t  nblocks;
	int i, namelen, retval;

	*res_dir = NULL;	/* 初始化输出参数 */
	sb = dir->i_sb;		/* 获取超级块指针 */
	namelen = fname->usr_fname->len;	/* 获取文件名长度 */
	if (namelen > EXT4_NAME_LEN)	/* 检查文件名是否超过最大长度 */
		return NULL;

	/* 如果目录具有内联数据,尝试在内联数据中查找目录项 */
	if (ext4_has_inline_data(dir)) {
		int has_inline_data = 1;
		ret = ext4_find_inline_entry(dir, fname, res_dir,
					     &has_inline_data);
		if (has_inline_data) {	/* 如果在内联数据中找到 */
			if (inlined)
				*inlined = 1;	/* 设置内联标志 */
			goto cleanup_and_exit;	/* 跳转到清理和退出 */
		}
	}

	/* 特殊处理 "." 和 ".." 目录项,这些只会在第一个块中出现 */
	if ((namelen <= 2) && (name[0] == '.') &&
	    (name[1] == '.' || name[1] == '\0')) {
		/*
		 * "." 或 ".." 仅存在于第一个块
		 * NFS 可能会查找 "..";"." 应由 VFS 处理
		 */
		block = start = 0;
		nblocks = 1;
		goto restart;	/* 跳转到重新启动搜索 */
	}

	/* 如果目录使用了 htree 索引,尝试使用 htree 查找目录项 */
	if (is_dx(dir)) {
		ret = ext4_dx_find_entry(dir, fname, res_dir);
		/*
		 * 成功找到,或错误是文件未找到,则返回。
		 * 否则,回退到传统的搜索方式。
		 */
		if (!IS_ERR(ret) || PTR_ERR(ret) != ERR_BAD_DX_DIR)
			goto cleanup_and_exit;
		dxtrace(printk(KERN_DEBUG "ext4_find_entry: dx failed, "
			       "falling back\n"));
		ret = NULL;	/* 重置返回值,准备回退 */
	}

	/* 计算目录的块数 */
	nblocks = dir->i_size >> EXT4_BLOCK_SIZE_BITS(sb);
	if (!nblocks) {	/* 如果没有块,则返回 NULL */
		ret = NULL;
		goto cleanup_and_exit;
	}

	/* 获取上次查找的起始块,如果超出范围,则从头开始 */
	start = EXT4_I(dir)->i_dir_start_lookup;
	if (start >= nblocks)
		start = 0;
	block = start;	/* 设置当前块为起始块 */

restart:
	do {
		/*
		 * 处理预读逻辑
		 */
		cond_resched();	/* 检查是否需要让出 CPU */

		/* 如果预读指针超过了预读最大值,重新填充预读缓冲区 */
		if (ra_ptr >= ra_max) {
			/* 重新填充预读缓冲区 */
			ra_ptr = 0;
			if (block < start)
				ra_max = start - block;
			else
				ra_max = nblocks - block;
			ra_max = min(ra_max, ARRAY_SIZE(bh_use));	/* 限制预读数量 */
			retval = ext4_bread_batch(dir, block, ra_max,
						  false /* wait */, bh_use);
			if (retval) {	/* 如果预读失败,返回错误 */
				ret = ERR_PTR(retval);
				ra_max = 0;
				goto cleanup_and_exit;
			}
		}

		/* 获取当前预读缓冲区的 buffer_head */
		if ((bh = bh_use[ra_ptr++]) == NULL)
			goto next;	/* 如果 buffer_head 为 NULL,跳过当前块 */

		wait_on_buffer(bh);	/* 等待缓冲区准备好 */

		/* 检查缓冲区是否已更新 */
		if (!buffer_uptodate(bh)) {
			EXT4_ERROR_INODE_ERR(dir, EIO,
					     "reading directory lblock %lu",
					     (unsigned long) block);
			brelse(bh);	/* 释放 buffer_head */
			ret = ERR_PTR(-EIO);
			goto cleanup_and_exit;
		}

		/* 如果缓冲区未被验证,且不是 htree 内部节点,验证目录块的校验和 */
		if (!buffer_verified(bh) &&
		    !is_dx_internal_node(dir, block,
					 (struct ext4_dir_entry *)bh->b_data) &&
		    !ext4_dirblock_csum_verify(dir, bh)) {
			EXT4_ERROR_INODE_ERR(dir, EFSBADCRC,
					     "checksumming directory "
					     "block %lu", (unsigned long)block);
			brelse(bh);	/* 释放 buffer_head */
			ret = ERR_PTR(-EFSBADCRC);
			goto cleanup_and_exit;
		}

		set_buffer_verified(bh);	/* 标记缓冲区为已验证 */

		/* 在当前目录块中搜索目录项 */
		i = search_dirblock(bh, dir, fname,
			    block << EXT4_BLOCK_SIZE_BITS(sb), res_dir);
		if (i == 1) {	/* 如果找到目录项 */
			EXT4_I(dir)->i_dir_start_lookup = block;	/* 更新查找起始点 */
			ret = bh;	/* 设置返回值为当前 buffer_head */
			goto cleanup_and_exit;
		} else {	/* 如果未找到,释放 buffer_head 并检查是否有错误 */
			brelse(bh);
			if (i < 0)
				goto cleanup_and_exit;
		}

	next:
		/* 处理下一块,如果超过块数则循环到第一个块 */
		if (++block >= nblocks)
			block = 0;
	} while (block != start);	/* 直到循环回起始块为止 */

	/*
	 * 如果在搜索过程中目录增长了,继续搜索新增的块
	 */
	block = nblocks;
	nblocks = dir->i_size >> EXT4_BLOCK_SIZE_BITS(sb);
	if (block < nblocks) {
		start = 0;	/* 新增的块从头开始搜索 */
		goto restart;	/* 重新启动搜索 */
	}

cleanup_and_exit:
	/* 清理预读缓冲区中的剩余 buffer_head */
	for (; ra_ptr < ra_max; ra_ptr++)
		brelse(bh_use[ra_ptr]);
	return ret;	/* 返回找到的 buffer_head 或 NULL */
}

主要流程解释:

1.初始化与参数检查:

  • 初始化输出参数:将 *res_dir 设置为 NULL,准备存储搜索结果。
  • 取超级块:通过 dir->i_sb 获取超级块指针。
  • 检查文件名长度:如果文件名长度超过 EXT4_NAME_LEN,则返回 NULL,表示未找到。

2.处理内联数据目录:

  • 内联数据目录:如果目录支持内联数据(即目录项直接存储在 inode 中),则调用 ext4_find_inline_entry 函数尝试在内联数据中查找目录项。
  • 找到内联目录项:如果在内联数据中找到目标目录项,并且设置了 inlined 参数,则标记并返回结果。

3.处理特殊目录项 "." 和 "...":

  • 特殊处理 "." 和 "...":如果要查找的文件名是 "." 或 "...",则这些目录项仅存在于第一个块中,直接定位到第一个块并跳转到重新启动搜索的标签 restart。

4.处理使用 htree 索引的目录:

  • htree 索引:如果目录使用 htree 索引(即目录项经过哈希索引优化),则调用 ext4_dx_find_entry 函数尝试通过 htree 查找目录项。
  • 查找结果:
    • 成功找到或文件未找到:如果通过 htree 查找成功找到目录项,或者文件未找到,则直接返回结果。
    • htrie 查找失败:如果 htree 查找失败(例如目录格式损坏),则回退到传统的线性搜索方式。

5.计算目录的块数和起始块:

  • 计算块数:通过 dir->i_size 计算目录包含的块数。
  • 获取上次查找的起始块:通过 EXT4_I(dir)->i_dir_start_lookup 获取上次查找的起始块,如果超出范围则从第一个块开始。

6.重新启动搜索循环 restart:

  • 预读逻辑:
    • 填充预读缓冲区:如果预读指针 ra_ptr 超过预读最大值 ra_max,则调用 ext4_bread_batch 函数批量读取多个块,提升搜索效率。
  • 遍历目录块:
    • 获取当前块的 buffer_head:从预读缓冲区中获取当前块的 buffer_head
    • 等待缓冲区准备好:调用 wait_on_buffer 等待缓冲区数据准备完毕。
    • 检查缓冲区数据的有效性:
      • 缓冲区是否更新:如果缓冲区数据未更新,则记录错误并返回。
      • 校验和验证:如果缓冲区未被验证,且不是 htree 内部节点,则调用 ext4_dirblock_csum_verify 验证目录块的校验和。如果校验失败,则记录错误并返回。
    • 标记缓冲区为已验证:通过 set_buffer_verified 标记缓冲区数据已被验证。
    • 搜索目录项:调用 search_dirblock 函数在当前目录块中搜索目标目录项。
      • 找到目录项:如果找到,则更新 i_dir_start_lookup 并返回当前的 buffer_head
      • 未找到或发生错误:如果未找到,则释放当前 buffer_head 并继续搜索下一个块。如果发生错误,则返回错误。
    • 处理循环结束:
      • 目录块增长处理:如果在搜索过程中目录块数增加(例如有新的目录项被添加),则重新启动搜索以覆盖新增的块。

7.清理与退出:

  • 释放预读缓冲区:通过循环释放预读缓冲区中未使用的 buffer_head
  • 返回结果:返回找到的 buffer_head(指向包含目标目录项的块)或 NULL(表示未找到)。

再来看看这段代码里面的一些细节点。

1.2.2 目录内联

目录内联(Directory Inlining)是一种优化技术,旨在减少文件系统的存储开销并提升性能。具体来说,它将小目录的元数据直接存储在父目录的元数据中,而不是为每个小目录分配单独的磁盘块。这在文件创建的时候就有所体现(__ext4_new_inode 函数中):

1.2.3 htree索引

ext4 文件系统默认情况下会开启 htree(哈希树)索引功能,尤其是在目录包含大量文件时。htree 是 ext4 文件系统用于优化大目录查找性能的一种索引机制,能有效降低磁盘I/O负载。

可以通过查看目录所在的文件系统是否具有 DIR_INDEX 特性来确认 htree 是否启用。使用 tune2fs 工具查看文件系统特性:

bash 复制代码
tune2fs -l /dev/sda | grep "Filesystem features"

核心源代码如下,这里就不做过多解释了。

c 复制代码
/*
 * ext4_dx_find_entry()
 *
 * 在使用 htree 索引的目录中查找指定名称的目录项。
 * 返回包含该目录项的缓冲区头(buffer_head),并通过参数 `res_dir` 返回目录项本身。
 * 如果查找失败,返回 NULL 或相应的错误指针。
 */
static struct buffer_head * ext4_dx_find_entry(struct inode *dir,
    			struct ext4_filename *fname,
    			struct ext4_dir_entry_2 **res_dir)
{
    // 获取超级块指针
	struct super_block * sb = dir->i_sb;

    // 定义用于存储 htree 帧的数组,大小为 EXT4_HTREE_LEVEL
	struct dx_frame frames[EXT4_HTREE_LEVEL], *frame;

    // 定义缓冲区头指针
	struct buffer_head *bh;

    // 定义逻辑块号变量
	ext4_lblk_t block;

    // 定义返回值变量
	int retval;

#ifdef CONFIG_FS_ENCRYPTION
    // 如果启用了文件系统加密,初始化 `res_dir` 为 NULL
	*res_dir = NULL;
#endif

    // 调用 `dx_probe` 函数,探测 htree 路径
	frame = dx_probe(fname, dir, NULL, frames);

    // 检查 `dx_probe` 是否返回错误指针
	if (IS_ERR(frame))
		return (struct buffer_head *) frame;

    // 进入循环,遍历 htree 索引查找目录项
	do {
        // 获取当前帧指向的块号
		block = dx_get_block(frame->at);

        // 读取目录块,类型为 DIRENT_HTREE
		bh = ext4_read_dirblock(dir, block, DIRENT_HTREE);

        // 检查读取是否发生错误
		if (IS_ERR(bh))
			goto errout;

        // 在读取的目录块中搜索目录项
		retval = search_dirblock(bh, dir, fname,
					 block << EXT4_BLOCK_SIZE_BITS(sb),
					 res_dir);

        // 如果找到目录项,跳转到成功处理部分
		if (retval == 1)
			goto success;

        // 如果未找到,释放缓冲区
		brelse(bh);

        // 如果搜索过程中发生错误,设置错误指针并跳转到错误处理部分
		if (retval == -1) {
			bh = ERR_PTR(ERR_BAD_DX_DIR);
			goto errout;
		}

		/* 检查是否应继续搜索下一个块 */
        // 调用 `ext4_htree_next_block` 决定是否继续搜索
		retval = ext4_htree_next_block(dir, fname->hinfo.hash, frame,
					       frames, NULL);

        // 如果在读取下一个索引块时发生错误,记录警告并跳转到错误处理部分
		if (retval < 0) {
			ext4_warning_inode(dir,
				"error %d reading directory index block",
				retval);
			bh = ERR_PTR(retval);
			goto errout;
		}
    } while (retval == 1); // 当 `retval` 为 1 时,继续循环搜索

    // 如果未找到,设置 `bh` 为 NULL
	bh = NULL;

errout:
    // 输出调试信息,表示未找到指定目录项
	dxtrace(printk(KERN_DEBUG "%s not found\n", fname->usr_fname->name));

success:
    // 释放 htree 帧数组中所有缓冲区头的资源
	dx_release(frames);

    // 返回找到的缓冲区头或 NULL
	return bh;
}

如果没有索引,就开始线性扫描,通过批量读取多个块,利用预读机制提升搜索效率,同时还考虑到了扫描过程中目录增长的情况。

注意,fname 仅仅是文件名本身,不包含路径信息。在文件系统内部,路径解析已经在更高层次(如 VFS 层)完成,__ext4_find_entry 函数只负责在特定的目录 inode 中查找单个文件名对应的目录项。

查找目录项的细节就到此结束,接着继续看主函数中的下一个细节。

1.3 同步标志

在 ext4 文件系统中,同步标志(sync flag)用于控制文件操作是否需要同步地将数据和元数据写入磁盘。同步操作确保数据在操作完成后立即持久化,提供更高的数据一致性和安全性,特别是在系统崩溃或断电的情况下。

1.3.1 判断同步标志

同步标志主要通过 inode 的标志位来设置。对于目录(directory inode),IS_DIRSYNC(dir) 宏用于检查该目录是否具有同步标志。这个宏通常会检查 inode 中的某个特定位(例如 EXT4_SYNC_FL)来确定是否需要同步操作。

c 复制代码
#define IS_DIRSYNC(inode) (test_opt((inode)->i_sb, DIRSYNC))

1.3.2 设置同步标志

设置同步标志的方式主要有以下几种:

1.挂载选项(Mount Options):

  • 在挂载文件系统时,可以通过指定 dirsync 选项来默认为所有目录启用同步操作。例如:
c 复制代码
mount -t ext4 -o dirsync /dev/sda /mnt
  • 这会将所有在该挂载点下的目录操作都设置为同步。

2.文件操作标志:

  • 在用户空间,应用程序可以通过在打开文件时使用 O_SYNC 或 O_DSYNC 标志来要求所有对该文件的写操作都是同步的。虽然这是针对文件的,但在某些情况下,可能会影响到目录的操作。

3.系统调用:

  • 某些系统调用或操作可能会隐式地设置同步标志,例如在执行关键的文件操作(如创建、删除、重命名文件)时,为了确保操作的原子性和一致性,可能会设置同步标志。

1.3.3 若是同步

当操作被标记为同步的,文件系统需要确保数据和元数据在操作完成后立即写入磁盘。这时,ext4_handle_sync 函数发挥关键作用。以下是 `ext4_handle_sync 的具体处理流程:

  1. 提交日志事务(Commit Journal Transaction):
    ext4_handle_syn 会触发日志系统提交当前的日志事务。它确保所有在当前事务中记录的变更(如目录项的删除、文件的重命名等)被写入到日志中。
  2. 等待日志写入完成:
    • 提交事务后,ext4_handle_sync 会等待日志数据被实际写入到磁盘。这通常涉及调用底层的块设备驱动程序,确保数据的物理写入。
  3. 同步文件系统状态:
    • 在日志提交并写入完成后,ext4_handle_sync 还会确保文件系统的状态(如超级块的更新)也被同步到磁盘。这进一步确保了文件系统在同步操作完成后处于一致状态。
  4. 错误处理:
    • 如果在同步过程中发生错误(如磁盘故障、I/O 错误等),ext4_handle_sync 会返回相应的错误代码,允许调用者处理这些异常情况。
  5. 性能影响:
    • 由于需要等待数据实际写入磁盘,同步操作通常比异步操作耗时更长。因此,尽管同步操作提供了更高的数据一致性,但在性能敏感的场景下需要谨慎使用。

1.3.4 若不是同步

如果操作不是同步的,则文件系统会采用异步方式处理这些操作。具体流程如下:

  1. 内存中的变更:
    • 文件操作首先在内存中的文件系统结构(如 inode、目录项等)进行修改。
  2. 日志记录(Journaling):
    • 变更会被记录到日志(journal)中,但不会立即写入磁盘。日志系统会在后台批量处理这些记录,提高效率。
  3. 后台写回(Background Writeback):
    • 通过后台线程或定时任务,文件系统会将日志中的变更异步地写入磁盘。这意味着操作在返回用户空间之前,数据可能仍然在内存中,尚未持久化。(这里暂不探讨日志的定时策略)
  4. 延迟一致性:
    • 虽然异步操作提高了性能,但在系统崩溃或断电的情况下,未写入磁盘的变更可能会丢失。因此,异步操作适用于对性能要求高且可以容忍短暂数据不一致的场景。

在这里我们就可以回答我们最开始的第二个疑问了。

1.4 删除目录项ext4_delete_entry

1.4.1 主体流程

再经过一系列前置检查后,终于来到了关键的删除部分,首先是删除目录项,核心函数如下:

c 复制代码
static int ext4_delete_entry(handle_t *handle,
                             struct inode *dir,
                             struct ext4_dir_entry_2 *de_del,
                             struct buffer_head *bh)
{
    int err, csum_size = 0;

    // 检查目录是否具有内联数据(inline data)
    if (ext4_has_inline_data(dir)) {
        int has_inline_data = 1;
        // 尝试删除内联目录项
        err = ext4_delete_inline_entry(handle, dir, de_del, bh,
                                       &has_inline_data);
        // 如果目录项在内联数据中被删除,则直接返回结果
        if (has_inline_data)
            return err;
    }

    // 检查文件系统是否启用了元数据校验和(metadata checksum)
    if (ext4_has_metadata_csum(dir->i_sb))
        csum_size = sizeof(struct ext4_dir_entry_tail);

    // 追踪缓冲区的操作(调试用)
    BUFFER_TRACE(bh, "get_write_access");
    // 获取对目录缓冲区的写访问权限,以便进行修改
    err = ext4_journal_get_write_access(handle, dir->i_sb, bh,
                                        EXT4_JTR_NONE);
    // 如果获取写权限失败,跳转到错误处理部分
    if (unlikely(err))
        goto out;

    // 调用通用删除目录项函数,执行实际的删除操作
    err = ext4_generic_delete_entry(dir, de_del, bh, bh->b_data,
                                    dir->i_sb->s_blocksize, csum_size);
    // 如果删除失败,跳转到错误处理部分
    if (err)
        goto out;

    // 追踪缓冲区的操作(调试用)
    BUFFER_TRACE(bh, "call ext4_handle_dirty_metadata");
    // 标记目录缓冲区为已修改,并将其记录到日志中
    err = ext4_handle_dirty_dirblock(handle, dir, bh);
    // 如果标记失败,跳转到错误处理部分
    if (unlikely(err))
        goto out;

    // 删除成功,返回0
    return 0;

out:
    // 如果错误不是文件未找到(-ENOENT),记录标准错误信息
    if (err != -ENOENT)
        ext4_std_error(dir->i_sb, err);
    // 返回错误码
    return err;
}

ext4_delete_entry 函数负责删除一个指定的目录项。其主要流程如下:

  1. 检查内联数据:
    • 目录可能包含内联数据(即目录项直接存储在 inode 中,而不是独立的块)。如果目录具有内联数据,首先尝试在内联数据中删除目标目录项。
    • 调用 ext4_delete_inline_entry 函数进行删除。如果删除成功(即目录项在内联数据中被删除),函数直接返回删除结果。
  2. 设置校验和大小:
    • 如果文件系统启用了元数据校验和(metadata checksum),则设置 sum_size 为目录项尾部校验和结构的大小。
  3. 获取写访问权限:
    • 为了修改目录项,需要对目录缓冲区获取写访问权限。调用 ext4_journal_get_write_access 函数,确保可以安全地修改目录缓冲区,并将修改记录到日志中(Journaling)。
  4. 调用通用删除函数:
    • 调用 ext4_generic_delete_entry 函数,实际执行目录项的删除操作。该函数会在目录缓冲区中找到并删除指定的目录项。
  5. 标记目录缓冲区为已修改:
    • 调用 ext4_handle_dirty_dirblock 函数,将已修改的目录缓冲区标记为脏数据,并将其写入日志中,以确保文件系统的一致性和可靠性。
  6. 错误处理:
    • 如果在上述任何步骤中发生错误,函数会记录标准错误信息,并返回相应的错误码。

其中通用目录删除函数ext4_generic_delete_entry如下:

c 复制代码
/*
 * ext4_generic_delete_entry 删除目录项的通用函数,通过合并待删除目录项与前一个目录项来实现删除。
 */
int ext4_generic_delete_entry(struct inode *dir,
                              struct ext4_dir_entry_2 *de_del,
                              struct buffer_head *bh,
                              void *entry_buf,
                              int buf_size,
                              int csum_size)
{
    struct ext4_dir_entry_2 *de, *pde;
    unsigned int blocksize = dir->i_sb->s_blocksize;
    int i;

    // 初始化计数器和前一个目录项指针
    i = 0;
    pde = NULL;
    de = entry_buf;

    // 遍历目录缓冲区中的所有目录项,直到达到缓冲区大小减去校验和大小
    while (i < buf_size - csum_size) {
        // 检查目录项的有效性
        if (ext4_check_dir_entry(dir, NULL, de, bh, entry_buf, buf_size, i))
            return -EFSCORRUPTED;

        // 如果当前目录项是待删除的目录项
        if (de == de_del)  {
            if (pde) {
                // 如果有前一个目录项,将待删除目录项的rec_len与前一个目录项的rec_len合并
                pde->rec_len = ext4_rec_len_to_disk(
                    ext4_rec_len_from_disk(pde->rec_len, blocksize) +
                    ext4_rec_len_from_disk(de->rec_len, blocksize),
                    blocksize);

                // 清除待删除目录项的数据,仅保留rec_len字段
                memset(de, 0, ext4_rec_len_from_disk(de->rec_len, blocksize));
            } else {
                // 如果没有前一个目录项,直接清除当前目录项的inode和name_len字段
                de->inode = 0;
                memset(&de->name_len, 0,
                       ext4_rec_len_from_disk(de->rec_len, blocksize) -
                       offsetof(struct ext4_dir_entry_2, name_len));
            }

            // 增加目录的版本号,标记为已修改
            inode_inc_iversion(dir);
            // 返回成功
            return 0;
        }

        // 获取当前目录项的rec_len(记录长度)
        i += ext4_rec_len_from_disk(de->rec_len, blocksize);
        // 更新前一个目录项指针为当前目录项
        pde = de;
        // 移动到下一个目录项
        de = ext4_next_entry(de, blocksize);
    }

    // 如果未找到待删除的目录项,返回-ENOENT
    return -ENOENT;
}

ext4_generic_delete_entry 是一个通用函数,用于在目录缓冲区中删除指定的目录项。其主要流程如下:

  1. 初始化变量:
    • 初始化当前目录项指针 de为目录缓冲区的起始位置,前一个目录项指针 pde 为 NULL,计数器 i 为0。
  2. 遍历目录缓冲区中的目录项:
    • 循环遍历目录缓冲区中的每个目录项,直到遍历完整个缓冲区或找到待删除的目录项。
    • 在每次循环中,首先检查当前目录项的有效性,确保其结构和数据正确。
  3. 查找待删除的目录项:
    • 如果当前目录项是待删除的目录项(即 de == de_del),则执行删除操作:
    • 有前一个目录项:如果存在前一个目录项 pde,将前一个目录项的 rec_len(记录长度)与当前目录项的 rec_len 合并,形成一个较大的连续空间。清除待删除目录项的数据,仅保留 rec_len 字段。
    • 无前一个目录项:如果不存在前一个目录项,直接清除当前目录项的inodename_len 字段,标记为无效。增加目录的版本号,标记目录已被修改。
    • 返回成功(0)。
  4. 继续遍历:
    • 如果当前目录项不是待删除的目录项,则更新前一个目录项指针 pde 为当前目录项,并移动到下一个目录项。
  5. 未找到目录项:
    • 如果遍历完整个目录缓冲区后仍未找到待删除的目录项,返回 -ENOENT 错误码,表示未找到指定的目录项。

1.4.2 关键细节

注意到实际的清除代码在循环内的两个memset处,我们先看一下目录项的结构体,看它存的是什么。目录项(directory entry)用于存储目录中的文件和子目录的信息,其实际结构定义在 struct ext4_dir_entry_2 中,主要包含以下字段:

c 复制代码
struct ext4_dir_entry_2 {
    __le32 inode;        // 文件的 inode 号
    __le16 rec_len;      // 目录项的长度,以字节为单位
    __u8  name_len;      // 文件名的长度
    __u8  file_type;     // 文件类型(例如,普通文件、目录、符号链接等)
    char  name[];        // 文件名,长度为 name_len 字节
} __attribute__((packed));

再来仔细看下两个清除的关键代码:

如果有前一个目录项,清零当前目录项的数据,保留 rec_len 字段以维护目录结构的完整性。此时相当于已经和前一个目录项合并了,当前目录项的的数据完全被清空

c 复制代码
// 清除待删除目录项的数据,仅保留上一个目录项中的rec_len字段
memset(de, 0, ext4_rec_len_from_disk(de->rec_len, blocksize));

如果没有前一个目录项,则直接清零当前目录项的 inode 和 name_len 字段,保留了后面三个字段值

c 复制代码
// 如果没有前一个目录项,直接清除当前目录项的inode和name_len字段
de->inode = 0;
memset(&de->name_len, 0, ext4_rec_len_from_disk(de->rec_len, blocksize) -
                       offsetof(struct ext4_dir_entry_2, name_len));

看下里面用到的ext4_rec_len_from_disk函数,主要是将磁盘上的 rec_len 值(__le16 类型,小端字节序)转换为内存中的无符号整数(unsigned int),并根据文件系统的块大小(blocksize)和页大小(PAGE_SIZE)进行适当的调整。:

c 复制代码
/*
 * 如果我们将来支持文件系统块大小大于页大小(page_size)的情况,
 * 我们需要移除下面两个函数中的 #if 条件编译语句...
 */
static inline unsigned int
ext4_rec_len_from_disk(__le16 dlen, unsigned blocksize)
{
    // 将小端字节序的 dlen 转换为当前 CPU 的字节序(主机字节序)
    unsigned len = le16_to_cpu(dlen);

    // 条件编译:检查系统的页大小是否大于或等于 65536 字节(64 KB)
#if (PAGE_SIZE >= 65536)
    // 如果 len 是 EXT4_MAX_REC_LEN(最大值)或 0,则直接返回 blocksize
    if (len == EXT4_MAX_REC_LEN || len == 0)
        return blocksize;

    // 对 len 进行位操作,重新组合其低 16 位和高 16 位
    // 1. len & 65532:保留低 16 位中除了最低 2 位的部分(65532 的二进制是 1111111111111100)
    // 2. (len & 3) << 16:将最低 2 位左移 16 位,放到高 16 位中
    // 3. 将两部分按位或运算,得到最终结果
    return (len & 65532) | ((len & 3) << 16);
#else
    // 如果页大小小于 64 KB,则直接返回 len,无需特殊处理
    return len;
#endif
}

由此我们可以得到关于目录项被清除的结论,也就是我们最开始的疑问一的一部分:

  • 若待删除的目录项能和之前的目录项进行合并,合并成更大的块,那当前目录项的内容会全部清除掉。
  • 若待删除的目录项不能和之前的目录项合并,则只会清除和inode的关联以及目录项的大小,其余内容不会被清除掉。

1.5 释放inode

1.5.1 为什么只减少引用

从核心源码中可以看出,当文件被删除的时候,目录项会被删除,但是inode只会进行一个减少引用的操作,只有当引用减少到0的时候,才会执行inode的回收操作。

为什么会这样呢?

因为:在 Unix/Linux 文件系统中,硬链接(Hard Link) 允许多个文件名指向同一个 inode 。每个文件名(目录项)都包含一个指向 inode 的指针。通过创建硬链接,可以为同一个文件创建多个不同的路径或名称。

删除文件实际上是删除目录项(文件名)。每删除一个目录项,就相当于减少该 inode 的链接计数 i_nlink。只有当链接计数降为零时,inode 才会被回收,即文件的数据块和 inode 被释放。

drop_nlink函数如下:

c 复制代码
/**
 * drop_nlink - 直接减少 inode 的链接计数
 * @inode: inode 结构体指针
 *
 * 这是一个低级别的文件系统辅助函数,用于替代直接操作 `i_nlink`。在需要跟踪文件系统写操作的情况下,
 * 当链接计数减少到零时,意味着文件即将被截断并在文件系统上实际删除。
 */
void drop_nlink(struct inode *inode)
{
    // 如果 inode 的链接计数已经为 0,则触发警告
    WARN_ON(inode->i_nlink == 0);
    
    // 直接减少 inode 的链接计数
    inode->__i_nlink--;
    
    // 如果链接计数降为 0,则增加超级块中的删除计数
    if (!inode->i_nlink)
        atomic_long_inc(&inode->i_sb->s_remove_count);
}

1.5.2 Orphan机制介绍

在 ext4 文件系统中,Orphan 机制(孤立 inode 机制)用于管理那些已被删除但仍被进程引用的文件。这种机制确保在系统崩溃或断电等异常情况下,文件系统能够正确地回收这些 inode,避免资源泄漏和文件系统不一致。

Orphan 列表主要用于跟踪那些链接计数(i_nlink)已经降为零但仍在使用中的 inode。具体来说:

  • 防止资源泄漏:当文件被删除(即从目录中移除目录项,链接计数减为零)但仍被某些进程打开时,这些 inode 不会立即被回收。Orphan 列表记录这些 inode,确保在所有引用释放后能够正确地回收它们。
  • 文件系统恢复:在系统崩溃或断电后,文件系统恢复时会遍历 Orphan 列表,清理那些未正确回收的 inode,保证文件系统的一致性。

Orphan 列表的使用主要发生在以下两种情况下:

  1. 正常删除文件时:
    • 当文件的链接计数降为零,但文件仍被进程打开时,文件的 inode 会被添加到 Orphan 列表中。
    • 这确保在所有引用释放后,文件系统能够自动回收这些 inode。
  2. 系统崩溃或异常关闭时:
    • 在系统异常关闭后,文件系统恢复过程中会检查 Orphan 列表,处理那些未被正确回收的 inode。
    • 通过遍历 Orphan 列表,删除未链接的 inode 或截断相关文件,恢复文件系统的一致性。

当inode被加入orphan列表后,相关的状态变化如下:

  1. 当一个 inode 被加入到 Orphan 列表后,表示该 inode 已被删除(链接计数降为零),但由于文件仍被打开或其他原因,尚未被完全回收。此时,inode 处于待回收状态。
  2. 当所有引用该 inode 的文件描述符被关闭后,文件系统会检测到 inode 的引用计数已降为零。文件系统的回收机制会自动调用清理函数,释放 inode 和相关的数据块。
  3. 在系统崩溃或异常关闭后,重新挂载文件系统时,文件系统会调用 ext4_orphan_cleanup 函数。ext4_orphan_cleanup 遍历 Orphan 列表,处理未被正确回收的 inode,确保文件系统的一致性。

1.5.3 添加至孤立列表 ext4_orphan_add源码分析

c 复制代码
/*
 * ext4_orphan_add() 将一个未链接或截断的 inode 链接到孤立 inode 列表中,
 * 以防止在文件关闭/删除之前或 inode 截断跨越多个事务且最后一个事务在崩溃后未恢复时,
 * 系统崩溃导致文件无法正确删除。
 *
 * 在文件系统恢复时,我们会遍历此列表,删除未链接的 inode 并在 ext4_orphan_cleanup() 中截断已链接的 inode。
 *
 * 孤立列表的操作必须在获取 i_rwsem(读写信号量)下进行,除非我们仅在创建或删除 inode 时调用。
 */
int ext4_orphan_add(handle_t *handle, struct inode *inode)
{
    struct super_block *sb = inode->i_sb;
    struct ext4_sb_info *sbi = EXT4_SB(sb);
    struct ext4_iloc iloc;
    int err = 0, rc;
    bool dirty = false;

    // 如果没有日志(journal)或 inode 已损坏,直接返回
    if (!sbi->s_journal || is_bad_inode(inode))
        return 0;

    // 如果 inode 不是新建或正在释放,且未上锁,则触发警告
    WARN_ON_ONCE(!(inode->i_state & (I_NEW | I_FREEING)) &&
                 !inode_is_locked(inode));

    /*
     * 检查 inode 是否已在孤立列表中
     * 如果是,直接返回无需重复添加
     */
    if (ext4_test_inode_state(inode, EXT4_STATE_ORPHAN_FILE) ||
        !list_empty(&EXT4_I(inode)->i_orphan))
        return 0;

    /*
     * 仅对具有数据块被截断或被取消链接的文件有效。
     * 确保我们持有 i_rwsem,或者 inode 无法被外部引用,
     * 因此 i_nlink 不会因为竞争而增加。
     */
    ASSERT((S_ISREG(inode->i_mode) || S_ISDIR(inode->i_mode) ||
            S_ISLNK(inode->i_mode)) || inode->i_nlink == 0);

    // 如果孤立信息中有孤立块,则尝试添加到孤立文件中
    if (sbi->s_orphan_info.of_blocks) {
        err = ext4_orphan_file_add(handle, inode);
        /*
         * 如果添加到孤立文件失败且原因不是空间不足,
         * 则直接返回错误。
         */
        if (err != -ENOSPC)
            return err;
    }

    // 获取超级块的写访问权限
    BUFFER_TRACE(sbi->s_sbh, "get_write_access");
    err = ext4_journal_get_write_access(handle, sb, sbi->s_sbh,
                                        EXT4_JTR_NONE);
    if (err)
        goto out;

    // 预留 inode 写入空间
    err = ext4_reserve_inode_write(handle, inode, &iloc);
    if (err)
        goto out;

    // 获取全局孤立锁
    mutex_lock(&sbi->s_orphan_lock);

    /*
     * 由于之前的错误,inode 可能已经是磁盘孤立列表的一部分。
     * 如果是,跳过对磁盘孤立列表的修改。
     */
    if (!NEXT_ORPHAN(inode) || NEXT_ORPHAN(inode) >
        (le32_to_cpu(sbi->s_es->s_inodes_count))) {
        /* 将此 inode 插入到磁盘孤立列表的头部 */
        NEXT_ORPHAN(inode) = le32_to_cpu(sbi->s_es->s_last_orphan);
        lock_buffer(sbi->s_sbh);
        sbi->s_es->s_last_orphan = cpu_to_le32(inode->i_ino);
        ext4_superblock_csum_set(sb);
        unlock_buffer(sbi->s_sbh);
        dirty = true;
    }

    // 将 inode 添加到内存中的孤立列表
    list_add(&EXT4_I(inode)->i_orphan, &sbi->s_orphan);
    mutex_unlock(&sbi->s_orphan_lock);

    // 如果有修改磁盘孤立列表,处理脏元数据
    if (dirty) {
        err = ext4_handle_dirty_metadata(handle, NULL, sbi->s_sbh);
        rc = ext4_mark_iloc_dirty(handle, inode, &iloc);
        if (!err)
            err = rc;
        if (err) {
            /*
             * 如果将 inode 添加到磁盘孤立列表失败,
             * 必须从内存列表中移除 inode,以避免孤立列表中的游离 inode
             */
            mutex_lock(&sbi->s_orphan_lock);
            list_del_init(&EXT4_I(inode)->i_orphan);
            mutex_unlock(&sbi->s_orphan_lock);
        }
    } else
        brelse(iloc.bh); // 释放 inode 缓冲区头

    jbd_debug(4, "superblock will point to %lu\n", inode->i_ino);
    jbd_debug(4, "orphan inode %lu will point to %d\n",
              inode->i_ino, NEXT_ORPHAN(inode));
out:
    // 处理标准错误
    ext4_std_error(sb, err);
    return err;
}

ext4_orphan_add函数执行流程:

  1. 初始检查:
  • 获取超级块 sb 和 ext4 超级块信息 sbi
  • 检查文件系统是否启用了日志(journal)以及 inode 是否已损坏。
  • 如果未启用日志或 inode 已损坏,直接返回,无需处理孤立。

2.状态验证:

  • 通过 WARN_ON_ONCE 宏确保 inode 处于新建 (I_NEW) 或释放 (I_FREEING) 状态,且已经被上锁。
  • 这确保了在操作 inode 时不会发生竞争条件。
  1. 检查是否已在孤立列表中:
  • 如果 inode 已经标记为孤立文件 (EXT4_STATE_ORPHAN_FILE) 或已经在内存中的孤立列表 (sbi->s_orphan) 中,则无需重复添加,直接返回。

4.验证 inode 类型:

  • 使用 ASSERT 宏确保 inode 是常规文件、目录、符号链接,或者链接计数为零。
  • 这确保孤立处理仅针对有效的文件类型。

5.尝试添加到孤立文件:

  • 如果孤立信息 (s_orphan_info) 中配置了孤立块 (of_blocks),调用 ext4_orphan_file_add 函数尝试将 inode 添加到孤立文件中。
  • 如果添加失败且错误不是空间不足 (-ENOSPC),则返回错误。

6.获取超级块的写访问权限:

  • 调用 ext4_journal_get_write_access 获取对超级块缓冲区的写访问权限,以便修改孤立列表。
  • 如果获取失败,跳转到错误处理部分。

7.预留 inode 写入空间:

  • 调用 ext4_reserve_inode_write 函数为 inode 预留写入空间,确保后续修改有足够的空间记录到日志中。
  • 如果预留失败,跳转到错误处理部分。

8.修改孤立列表:

  • 获取全局孤立锁 s_orphan_lock,以确保对孤立列表的修改是原子的。
  • 检查 inode 是否已经在磁盘孤立列表中。如果没有,将 inode 插入到磁盘孤立列表的头部:
    • 设置 NEXT_ORPHAN(inode) 为当前超级块中最后一个孤立 inode 的 inode 号。
    • 更新超级块中的 s_last_orphan 为当前 inode 的 inode 号。
    • 更新超级块的校验和。
    • 标记需要写回日志。
  • 将 inode 添加到内存中的孤立列表 sbi->s_orphan
  • 释放孤立锁。

9.处理脏元数据:

  • 如果修改了磁盘孤立列表(dirty == true),则:
    • 调用 ext4_handle_dirty_metadata 标记超级块为脏数据,确保其被记录到日志中。
    • 调用 ext4_mark_iloc_dirty 标记 inode 的位置 (iloc) 为脏数据。
    • 如果标记失败,则需要从内存孤立列表中移除 inode,避免出现孤立列表中的游离 inode。
  • 如果未修改磁盘孤立列表,则释放 iloc.bh 缓冲区头。

10.日志调试和错误:

  • 通过 jbd_debug 打印调试信息,显示超级块将指向的 inode 号以及孤立 inode 将指向的下一个 inode 号。
  • 调用 ext4_std_error 处理标准错误,并返回错误码。

从源码中可以看出,ext4_orphan_add 首先尝试调用ext4_orphan_file_add 将 inode 添加到孤立文件中。ext4_orphan_file_add 在孤立文件的孤立块中寻找空闲插槽,将 inode 的 i_ino 写入空闲项,并记录其在孤立文件中的索引。如果孤立文件已满(-ENOSPC),则 ext4_orphan_add 继续将 inode 添加到内存中的孤立列表中。

ext4_orphan_add函数的源码如下:

c 复制代码
static int ext4_orphan_file_add(handle_t *handle, struct inode *inode)
{
    int i, j, start;
    struct ext4_orphan_info *oi = &EXT4_SB(inode->i_sb)->s_orphan_info;
    int ret = 0;
    bool found = false;
    __le32 *bdata;
    int inodes_per_ob = ext4_inodes_per_orphan_block(inode->i_sb);
    int looped = 0;

    /*
     * 寻找具有空闲孤立项的块。使用 CPU 编号进行简单哈希,
     * 作为在孤立文件中搜索的起始点。
     */
    start = raw_smp_processor_id()*13 % oi->of_blocks;
    i = start;
    do {
        if (atomic_dec_if_positive(&oi->of_binfo[i].ob_free_entries)
            >= 0) {
            found = true;
            break;
        }
        if (++i >= oi->of_blocks)
            i = 0;
    } while (i != start);

    if (!found) {
        /*
         * 目前我们不扩展或缩减孤立文件。我们只使用在 mke2fs 时
         * 分配的空间。为每个孤立 inode 操作预留额外的空间
         * 显得不划算。
         */
        return -ENOSPC;
    }

    // 获取孤立块缓冲区的写访问权限
    ret = ext4_journal_get_write_access(handle, inode->i_sb,
                oi->of_binfo[i].ob_bh, EXT4_JTR_ORPHAN_FILE);
    if (ret) {
        // 如果获取失败,恢复孤立块的空闲项计数
        atomic_inc(&oi->of_binfo[i].ob_free_entries);
        return ret;
    }

    // 获取孤立块数据
    bdata = (__le32 *)(oi->of_binfo[i].ob_bh->b_data);
    /* 在块中寻找空闲插槽 */
    j = 0;
    do {
        if (looped) {
            /*
             * 如果多次遍历块仍未找到空闲项,可能是由于条目不断分配和释放
             * 或块损坏。避免无限循环并放弃,使用孤立列表。
             */
            if (looped > 3) {
                atomic_inc(&oi->of_binfo[i].ob_free_entries);
                return -ENOSPC;
            }
            cond_resched();
        }
        while (bdata[j]) {
            if (++j >= inodes_per_ob) {
                j = 0;
                looped++;
            }
        }
    } while (cmpxchg(&bdata[j], (__le32)0, cpu_to_le32(inode->i_ino)) !=
             (__le32)0);

    // 记录孤立 inode 在孤立文件中的索引
    EXT4_I(inode)->i_orphan_idx = i * inodes_per_ob + j;
    // 设置 inode 状态为孤立文件
    ext4_set_inode_state(inode, EXT4_STATE_ORPHAN_FILE);

    // 标记孤立块缓冲区为脏数据,记录到日志
    return ext4_handle_dirty_metadata(handle, NULL, oi->of_binfo[i].ob_bh);
}

1.5.4 实际清除 ext4_orphan_cleanup源码分析

c 复制代码
/*
 * ext4_orphan_cleanup() 遍历一个单向链表中的 inodes(从超级块开始),
 * 这些 inodes 是在所有目录中删除后,但在崩溃时仍被进程打开的。
 * 我们遍历这个列表并尝试删除这些 inodes,以恢复文件系统的一致性。
 *
 * 为了在遍历过程中保持孤立 inode 链的完整性(以防止在恢复期间崩溃),
 * 我们将每个 inode 链接到超级块的 orphan list_head,并像正常操作中删除 inode 一样处理它们
 * (这些操作会被日志记录)。
 *
 * 我们只对每个 inode 调用 iget() 和 iput(),这是非常安全的,如果我们错误地指向一个正在使用或已删除的 inode,
 * 最坏的情况下,我们会从 ext4_free_inode() 获取一个 "bit already cleared" 的信息。
 * 指向错误 inode 的唯一原因是如果 e2fsck 已经对这个文件系统运行过,
 * 并且它必须已经为我们清理了 orphan inode,因此我们可以安全地中止而无需进一步操作。
 */
void ext4_orphan_cleanup(struct super_block *sb, struct ext4_super_block *es)
{
    unsigned int s_flags = sb->s_flags;
    int nr_orphans = 0, nr_truncates = 0;
    struct inode *inode;
    int i, j;
#ifdef CONFIG_QUOTA
    int quota_update = 0;
#endif
    __le32 *bdata;
    struct ext4_orphan_info *oi = &EXT4_SB(sb)->s_orphan_info;
    int inodes_per_ob = ext4_inodes_per_orphan_block(sb);

    // 如果没有孤立文件和孤立块,则无需清理
    if (!es->s_last_orphan && !oi->of_blocks) {
        jbd_debug(4, "no orphan inodes to clean up\n");
        return;
    }

    // 如果设备以只读方式挂载,跳过清理
    if (bdev_read_only(sb->s_bdev)) {
        ext4_msg(sb, KERN_ERR, "write access unavailable, skipping orphan cleanup");
        return;
    }

    /* 检查特性集是否允许读写挂载 */
    if (!ext4_feature_set_ok(sb, 0)) {
        ext4_msg(sb, KERN_INFO, "Skipping orphan cleanup due to unknown ROCOMPAT features");
        return;
    }

    if (EXT4_SB(sb)->s_mount_state & EXT4_ERROR_FS) {
        /* 在只读挂载并且有错误时,不清理列表 */
        if (es->s_last_orphan && !(s_flags & SB_RDONLY)) {
            ext4_msg(sb, KERN_INFO, "Errors on filesystem, clearing orphan list.\n");
            es->s_last_orphan = 0;
        }
        jbd_debug(1, "Skipping orphan recovery on fs with errors.\n");
        return;
    }

    // 如果文件系统是只读的,临时关闭只读标志以允许写操作
    if (s_flags & SB_RDONLY) {
        ext4_msg(sb, KERN_INFO, "orphan cleanup on readonly fs");
        sb->s_flags &= ~SB_RDONLY;
    }
#ifdef CONFIG_QUOTA
    /*
     * 打开配额,如果文件系统具有配额特性,并且之前是只读挂载,
     * 以便正确更新配额。
     */
    if (ext4_has_feature_quota(sb) && (s_flags & SB_RDONLY)) {
        int ret = ext4_enable_quotas(sb);

        if (!ret)
            quota_update = 1;
        else
            ext4_msg(sb, KERN_ERR, "Cannot turn on quotas: error %d", ret);
    }

    /* 为旧版配额打开日志化配额 */
    for (i = 0; i < EXT4_MAXQUOTAS; i++) {
        if (EXT4_SB(sb)->s_qf_names[i]) {
            int ret = ext4_quota_on_mount(sb, i);

            if (!ret)
                quota_update = 1;
            else
                ext4_msg(sb, KERN_ERR, "Cannot turn on journaled quota: type %d: error %d", i, ret);
        }
    }
#endif

    // 遍历超级块中的孤立列表
    while (es->s_last_orphan) {
        /*
         * 如果在清理过程中遇到错误,则跳过剩余部分。
         */
        if (EXT4_SB(sb)->s_mount_state & EXT4_ERROR_FS) {
            jbd_debug(1, "Skipping orphan recovery on fs with errors.\n");
            es->s_last_orphan = 0;
            break;
        }

        // 获取孤立 inode
        inode = ext4_orphan_get(sb, le32_to_cpu(es->s_last_orphan));
        if (IS_ERR(inode)) {
            es->s_last_orphan = 0;
            break;
        }

        // 将 inode 添加到内存中的孤立列表中
        list_add(&EXT4_I(inode)->i_orphan, &EXT4_SB(sb)->s_orphan);
        // 处理孤立 inode(截断或删除)
        ext4_process_orphan(inode, &nr_truncates, &nr_orphans);
    }

    // 遍历孤立文件中的所有孤立 inode
    for (i = 0; i < oi->of_blocks; i++) {
        bdata = (__le32 *)(oi->of_binfo[i].ob_bh->b_data);
        for (j = 0; j < inodes_per_ob; j++) {
            if (!bdata[j])
                continue;
            inode = ext4_orphan_get(sb, le32_to_cpu(bdata[j]));
            if (IS_ERR(inode))
                continue;
            // 标记 inode 状态为孤立文件
            ext4_set_inode_state(inode, EXT4_STATE_ORPHAN_FILE);
            EXT4_I(inode)->i_orphan_idx = i * inodes_per_ob + j;
            // 处理孤立 inode(截断或删除)
            ext4_process_orphan(inode, &nr_truncates, &nr_orphans);
        }
    }

#define PLURAL(x) (x), ((x) == 1) ? "" : "s"

    // 记录清理结果
    if (nr_orphans)
        ext4_msg(sb, KERN_INFO, "%d orphan inode%s deleted",
               PLURAL(nr_orphans));
    if (nr_truncates)
        ext4_msg(sb, KERN_INFO, "%d truncate%s cleaned up",
               PLURAL(nr_truncates));
#ifdef CONFIG_QUOTA
    /* 如果启用了配额,并且进行了更新,则关闭配额 */
    if (quota_update) {
        for (i = 0; i < EXT4_MAXQUOTAS; i++) {
            if (sb_dqopt(sb)->files[i])
                dquot_quota_off(sb, i);
        }
    }
#endif
    sb->s_flags = s_flags; /* 恢复只读挂载状态 */
}

ext4_orphan_cleanup函数执行流程如下:

  1. 初始检查:
    • 检查超级块中的 s_last_orphan 是否存在,或者孤立文件(orphan file)中是否有孤立 inode。
    • 如果没有孤立 inode,打印调试信息并返回,无需清理。
  2. 文件系统状态检查:
    • 如果文件系统以只读模式挂载或存在错误(EXT4_ERROR_FS),跳过 Orphan 清理。
    • 确保文件系统挂载为读写模式,以便进行清理操作。
  3. 配额处理(可选):
    • 如果文件系统启用了配额特性,在清理前确保配额正确启用,以便正确更新配额信息。
  4. 遍历超级块孤立列表:
    s_last_orphan 指向第一个孤立 inode 的 inode 号。
    • 使用 ext4_orphan_get 获取该 inode 的 inode 结构。
    • 将 inode 添加到内存中的孤立列表 sbi->s_orphan
    • 调用 xt4_process_orphan 函数,执行截断或删除操作。
  5. 遍历孤立文件中的所有孤立 inode:
    • 孤立文件(orphan file)中记录了更多的孤立 inode。
    • 遍历每个孤立块,获取其中的 inode 号。
    • 将每个 inode 标记为孤立文件状态,并调用 ext4_process_orphan 进行处理。
  6. 处理截断和删除:
    • 在 ext4_process_orphan 函数中:
    • 截断操作:对于仍有数据块的文件,调用 ext4_truncate 截断文件数据,并删除 inode。
    • 删除操作:对于已完全删除的文件,直接删除 inode。
  7. 统计和日志:
    • 记录清理过程中删除的 Orphan inode 数量和截断的文件数量。
    • 输出相关日志信息,供系统管理员参考。
  8. 配额关闭(可选):
    • 如果启用了配额并进行了更新,则在清理完成后关闭配额。
  9. 恢复文件系统状态:
    • 恢复文件系统的只读挂载状态(sb->s_flags)。

其核心释放inode的函数是ext4_process_orphan,其源码如下:

c 复制代码
/**
 * ext4_process_orphan() - 处理加入orphan列表的inode
 * @inode:   需要处理的inode
 * @nr_truncates: 统计需要截断(truncate)的inode计数的指针
 * @nr_orphans:   统计需要彻底删除的inode计数的指针
 *
 * 该函数会根据inode的状态(是否仍有链接引用)来决定截断文件数据块
 * 还是直接删除inode。处理完成后会调用iput(inode),从而触发后续的回收逻辑。
 */
static void ext4_process_orphan(struct inode *inode,
				int *nr_truncates, int *nr_orphans)
{
	struct super_block *sb = inode->i_sb;
	int ret;

	// 初始化配额(如果启用了配额功能)
	dquot_initialize(inode);

	// 如果 inode 仍然有链接(即 i_nlink > 0),说明只是需要截断文件
	if (inode->i_nlink) {
		// 如果挂载带有DEBUG选项,则输出调试信息
		if (test_opt(sb, DEBUG))
			ext4_msg(sb, KERN_DEBUG,
				 "%s: truncating inode %lu to %lld bytes",
				 __func__, inode->i_ino, inode->i_size);
		jbd_debug(2, "truncating inode %lu to %lld bytes\n",
			  inode->i_ino, inode->i_size);

		// 上锁,防止并发修改
		inode_lock(inode);
		// 截断页面缓存至 inode->i_size
		truncate_inode_pages(inode->i_mapping, inode->i_size);
		// 调用ext4_truncate,释放超过inode->i_size部分的数据块
		ret = ext4_truncate(inode);
		if (ret) {
			/*
			 * 如果 ext4_truncate() 在获取事务句柄时失败了,
			 * 我们需要手动将该inode从内存中的orphan列表中删除,
			 * 避免在后续操作中出现问题。
			 */
			ext4_orphan_del(NULL, inode);
			ext4_std_error(inode->i_sb, ret);
		}
		inode_unlock(inode);

		// 截断操作完成,截断计数加1
		(*nr_truncates)++;
	} else {
		// 如果inode没有链接计数(i_nlink == 0),说明彻底删除
		if (test_opt(sb, DEBUG))
			ext4_msg(sb, KERN_DEBUG,
				 "%s: deleting unreferenced inode %lu",
				 __func__, inode->i_ino);
		jbd_debug(2, "deleting unreferenced inode %lu\n", inode->i_ino);

		// 被删除的inode计数加1
		(*nr_orphans)++;
	}

	/*
	 * iput(inode) 是关键的回收触发点:
	 * 如果 i_nlink==0 并且没有其他引用,会触发ext4_evict_inode(),
	 * 进而释放该inode占用的数据块,并回收inode本身。
	 */
	iput(inode);
}

我们直接定位到关键的删除部分逻辑:

  • 当一个inode还有链接时(i_nlink > 0),只需要"截断"到 i_size,释放多余的数据块。ext4_truncate(inode):释放 inode 不再需要的数据块。
  • 当一个inode的 i_nlink == 0 且没有其他引用时,最终会在 iput(inode) 之后触发回收逻辑。

接下来我们就再往深处分析ext4_truncate(inode)iput(inode),看看文件系统到底是怎么处理inode的截断和删除的。

1.5.5 inode截断 ext4_truncate源码分析(块释放核心点)

ext4_truncate的源码位于fs/ext4/inode.c中,其源码如下:

c 复制代码
/**
 * ext4_truncate() - 截断(truncate)文件到 inode->i_size 所指定的大小
 * @inode: 需要被截断的 inode
 *
 * 当文件大小(i_size)被下调时,需要从磁盘上释放超过该大小的文件块。
 * 此函数执行以下操作:
 * 1. 处理 inline data 情况(如果 inode 以内联方式存储数据)。
 * 2. 如果新的文件大小不是块对齐,则零填最后一个块尾部。
 * 3. 将 inode 添加到 orphan(孤立)列表,以便系统崩溃后能够恢复截断操作。
 * 4. 对应于 extents 模式或间接模式,调用相应的截断函数(ext4_ext_truncate / ext4_ind_truncate)。
 * 5. 事务完成后,如果 inode 仍有链接数(即不是删除文件),则从 orphan 列表移除该 inode。
 * 6. 更新 inode 的 mtime/ctime 并标记 inode 为脏。
 *
 * 注:如果文件是通过 unlink 正在删除,那么 i_nlink 会被置为 0,此时不需要从 orphan 列表中删除,
 *     因为后续的 evict_inode 会进行处理。
 */
int ext4_truncate(struct inode *inode)
{
    struct ext4_inode_info *ei = EXT4_I(inode);
    unsigned int credits;
    int err = 0, err2;
    handle_t *handle;
    struct address_space *mapping = inode->i_mapping;

    // 如果 inode 既不是新建也不是正在释放,但却没有上锁,则触发警告
    if (!(inode->i_state & (I_NEW|I_FREEING)))
        WARN_ON(!inode_is_locked(inode));
    trace_ext4_truncate_enter(inode);

    // 如果此 inode 不允许截断(例如一些特殊情形),直接返回
    if (!ext4_can_truncate(inode))
        goto out_trace;

    /*
     * 如果文件大小变为 0,并且未使用no_auto_da_alloc选项,
     * 则将这个inode标记为需要关闭延迟分配(DA)。
     */
    if (inode->i_size == 0 && !test_opt(inode->i_sb, NO_AUTO_DA_ALLOC))
        ext4_set_inode_state(inode, EXT4_STATE_DA_ALLOC_CLOSE);

    /*
     * 处理inline data的情况:
     * 如果inode有内联数据且截断后仍包含内联,那么不需要继续后续的块截断。
     */
    if (ext4_has_inline_data(inode)) {
        int has_inline = 1;

        err = ext4_inline_data_truncate(inode, &has_inline);
        // 如果截断内联数据出现错误,或依旧是内联格式,结束截断
        if (err || has_inline)
            goto out_trace;
    }

    // 如果文件末尾不对齐块大小,需要attach_jinode以支持日志写入零填操作
    if (inode->i_size & (inode->i_sb->s_blocksize - 1)) {
        if (ext4_inode_attach_jinode(inode) < 0)
            goto out_trace;
    }

    /*
     * 计算此次截断所需的事务块数 credits:
     * - 对于extent方式,使用 ext4_writepage_trans_blocks。
     * - 对于间接索引方式,使用 ext4_blocks_for_truncate 估算。
     */
    if (ext4_test_inode_flag(inode, EXT4_INODE_EXTENTS))
        credits = ext4_writepage_trans_blocks(inode);
    else
        credits = ext4_blocks_for_truncate(inode);

    // 启动truncate事务
    handle = ext4_journal_start(inode, EXT4_HT_TRUNCATE, credits);
    if (IS_ERR(handle)) {
        err = PTR_ERR(handle);
        goto out_trace;
    }

    // 如果文件末尾不对齐块大小,需要零填最后一个块的尾部
    if (inode->i_size & (inode->i_sb->s_blocksize - 1))
        ext4_block_truncate_page(handle, mapping, inode->i_size);

    /*
     * 将 inode 添加到 orphan 列表,保证发生崩溃或截断跨多个事务时,
     * 下次挂载/恢复时能够继续截断。
     */
    err = ext4_orphan_add(handle, inode);
    if (err)
        goto out_stop;

    // 加写锁保护元数据操作
    down_write(&EXT4_I(inode)->i_data_sem);

    // 丢弃该 inode 的预分配块(如果有)
    ext4_discard_preallocations(inode, 0);

    // 根据是否是 extent 模式调用不同的截断函数
    if (ext4_test_inode_flag(inode, EXT4_INODE_EXTENTS))
        err = ext4_ext_truncate(handle, inode);
    else
        ext4_ind_truncate(handle, inode);

    // 释放写锁
    up_write(&ei->i_data_sem);
    if (err)
        goto out_stop;

    // 如果 inode 被同步挂载或设置了同步属性,则需要进行事务同步
    if (IS_SYNC(inode))
        ext4_handle_sync(handle);

out_stop:
    /*
     * 如果此 inode 仍然有链接(即文件没有被彻底删除),
     * 则从orphan列表中删除该 inode。若 i_nlink==0,说明是unlink删除场景,
     * orphan列表的清理留给evict_inode过程。
     */
    if (inode->i_nlink)
        ext4_orphan_del(handle, inode);

    // 更新 inode 的时间戳并标记为脏
    inode->i_mtime = inode->i_ctime = current_time(inode);
    err2 = ext4_mark_inode_dirty(handle, inode);
    if (unlikely(err2 && !err))
        err = err2;

    // 停止 truncate 的事务
    ext4_journal_stop(handle);

out_trace:
    trace_ext4_truncate_exit(inode);
    return err;
}

还是直接看关键操作(ext4_ext_truncate / ext4_ind_truncate

1.5.5.1 extent模式下的截断 ext4_ext_truncate源码分析

ext4_ext_truncate函数源码如下:

c 复制代码
/**
 * ext4_ext_truncate() - 截断(truncate)基于 Extent 索引的文件
 * @handle:	日志事务句柄
 * @inode:	需要被截断的 inode
 *
 * 此函数专门处理使用 Extent 方式存储数据的 inode 截断操作,主要包括:
 * 1. 更新 inode 的 i_disksize,保证在崩溃场景下能重启截断。
 * 2. 从 extent 状态缓存(extent status cache)中移除指定范围的记录。
 * 3. 调用 ext4_ext_remove_space() 真正释放超出范围的块(核心块回收逻辑)。
 */
int ext4_ext_truncate(handle_t *handle, struct inode *inode)
{
	struct super_block *sb = inode->i_sb;
	ext4_lblk_t last_block;
	int err = 0;

	/*
	 * TODO: 这里可能存在优化空间;目前会进行完整扫描,
	 * 而实际上 page 截断(page truncation)就足以满足大部分场景。
	 */

	/* 保证崩溃后能够根据 i_disksize 恢复截断 */
	EXT4_I(inode)->i_disksize = inode->i_size;
	err = ext4_mark_inode_dirty(handle, inode);
	if (err)
		return err;

	/*
	 * 计算截断到的逻辑块号(last_block),即根据文件大小得到需要保留的
	 * 最后一个块号(向上对齐)。
	 */
	last_block = (inode->i_size + sb->s_blocksize - 1)
		     >> EXT4_BLOCK_SIZE_BITS(sb);

retry:
	/*
	 * 首先从 extent status cache 中移除 [last_block, EXT_MAX_BLOCKS) 范围的记录;
	 * 如果内存紧张导致 -ENOMEM,则等待一会儿重试。
	 */
	err = ext4_es_remove_extent(inode, last_block,
				    EXT_MAX_BLOCKS - last_block);
	if (err == -ENOMEM) {
		memalloc_retry_wait(GFP_ATOMIC);
		goto retry;
	}
	if (err)
		return err;

retry_remove_space:
	/*
	 * 调用 ext4_ext_remove_space 释放 [last_block, EXT_MAX_BLOCKS - 1] 范围的块;
	 * 这是真正的块回收逻辑所在。
	 */
	err = ext4_ext_remove_space(inode, last_block, EXT_MAX_BLOCKS - 1);
	if (err == -ENOMEM) {
		memalloc_retry_wait(GFP_ATOMIC);
		goto retry_remove_space;
	}
	return err;
}

再看ext4_ext_remove_space函数:

c 复制代码
/**
 * ext4_ext_remove_space() - 从基于 Extent 的 inode 中,移除 [start, end] 范围的块
 * @inode: 需要操作的 inode
 * @start: 需要删除的起始逻辑块号
 * @end:   删除的结束逻辑块号
 *
 * 此函数是真正执行 "在 extent tree 中释放指定区间的块" 的核心逻辑。流程包括:
 * 1. 启动一次 truncate 类的事务 (ext4_journal_start_with_revoke)。
 * 2. 若需要在extent中间打洞(punch hole),会先做必要的split(ext4_force_split_extent_at)。
 * 3. 从右到左(或从高到低逻辑块号)遍历 extent tree,调用 ext4_ext_rm_leaf 等函数释放数据块。
 * 4. 若最后所有 extent 都被清空,更新 root 级 eh_depth。
 */
int ext4_ext_remove_space(struct inode *inode, ext4_lblk_t start,
			  ext4_lblk_t end)
{
	struct ext4_sb_info *sbi = EXT4_SB(inode->i_sb);
	int depth = ext_depth(inode);
	struct ext4_ext_path *path = NULL;
	struct partial_cluster partial;
	handle_t *handle;
	int i = 0, err = 0;

	partial.pclu = 0;
	partial.lblk = 0;
	partial.state = initial;

	ext_debug(inode, "truncate since %u to %u\n", start, end);

	/*
	 * 发起一次 Truncate 日志事务,并为 revoke 分配一定的元数据操作额度
	 */
	handle = ext4_journal_start_with_revoke(inode, EXT4_HT_TRUNCATE,
			depth + 1,
			ext4_free_metadata_revoke_credits(inode->i_sb, depth));
	if (IS_ERR(handle))
		return PTR_ERR(handle);

again:
	trace_ext4_ext_remove_space(inode, start, end, depth);

	/*
	 * 当 end < EXT_MAX_BLOCKS - 1 时,说明需要在 extent tree 中间移除一段,
	 * 这会涉及 "打洞(punch hole)" 的场景,需要先分割 (split) 正在覆盖这一段的extent。
	 */
	if (end < EXT_MAX_BLOCKS - 1) {
		struct ext4_extent *ex;
		ext4_lblk_t ee_block, ex_end, lblk;
		ext4_fsblk_t pblk;

		/* 找到或紧邻 end 的 extent */
		path = ext4_find_extent(inode, end, NULL,
					EXT4_EX_NOCACHE | EXT4_EX_NOFAIL);
		if (IS_ERR(path)) {
			ext4_journal_stop(handle);
			return PTR_ERR(path);
		}
		depth = ext_depth(inode);
		ex = path[depth].p_ext;
		if (!ex) {
			/* inode可能没有任何块 */
			if (depth) {
				EXT4_ERROR_INODE(inode,
						 "path[%d].p_hdr == NULL",
						 depth);
				err = -EFSCORRUPTED;
			}
			goto out;
		}

		ee_block = le32_to_cpu(ex->ee_block);
		ex_end = ee_block + ext4_ext_get_actual_len(ex) - 1;

		/*
		 * 如果 end 在当前ex的范围内,则进行 split:
		 * end+1之后的部分拆分成新的extent,以便后续只删除 [start, end] 范围。
		 */
		if (end >= ee_block && end < ex_end) {
			if (sbi->s_cluster_ratio > 1) {
				pblk = ext4_ext_pblock(ex) + (end - ee_block + 1);
				partial.pclu = EXT4_B2C(sbi, pblk);
				partial.state = nofree;
			}
			// 使用 ext4_force_split_extent_at 做split
			err = ext4_force_split_extent_at(handle, inode, &path,
							 end + 1, 1);
			if (err < 0)
				goto out;
		} else if (sbi->s_cluster_ratio > 1 && end >= ex_end &&
			   partial.state == initial) {
			/*
			 * 如果正在打洞,且 partial还未被设置,
			 * 则设置partial以免随后把不该删的块也删掉。
			 */
			lblk = ex_end + 1;
			err = ext4_ext_search_right(inode, path, &lblk, &pblk,
						    NULL);
			if (err < 0)
				goto out;
			if (pblk) {
				partial.pclu = EXT4_B2C(sbi, pblk);
				partial.state = nofree;
			}
		}
	}

	/*
	 * 从右往左扫描释放所有多余的块。
	 * 先处理leaf级(ext4_ext_rm_leaf),再往上层index级清理。
	 */
	depth = ext_depth(inode);
	if (path) {
		int k = i = depth;
		while (--k > 0)
			path[k].p_block = le16_to_cpu(path[k].p_hdr->eh_entries) + 1;
	} else {
		/* 如果没有现成的path,需要新分配 */
		path = kcalloc(depth + 1, sizeof(struct ext4_ext_path),
			       GFP_NOFS | __GFP_NOFAIL);
		if (path == NULL) {
			ext4_journal_stop(handle);
			return -ENOMEM;
		}
		path[0].p_maxdepth = path[0].p_depth = depth;
		path[0].p_hdr = ext_inode_hdr(inode);
		i = 0;

		if (ext4_ext_check(inode, path[0].p_hdr, depth, 0)) {
			err = -EFSCORRUPTED;
			goto out;
		}
	}
	err = 0;

	/*
	 * 从叶子节点往回走的方式,遍历并删除指定范围的块。
	 */
	while (i >= 0 && err == 0) {
		if (i == depth) {
			/* 这是叶子节点,执行真正的块删除操作 */
			err = ext4_ext_rm_leaf(handle, inode, path,
					       &partial, start, end);
			brelse(path[i].p_bh);
			path[i].p_bh = NULL;
			i--;
			continue;
		}

		/* 以下处理索引节点(index block)的场景 */
		if (!path[i].p_hdr)
			path[i].p_hdr = ext_block_hdr(path[i].p_bh);

		if (!path[i].p_idx) {
			/* 初始化索引指针 */
			path[i].p_idx = EXT_LAST_INDEX(path[i].p_hdr);
			path[i].p_block = le16_to_cpu(path[i].p_hdr->eh_entries)+1;
		} else {
			path[i].p_idx--;
		}

		if (ext4_ext_more_to_rm(path + i)) {
			/* 深入到更下一级 */
			struct buffer_head *bh;
			memset(path + i + 1, 0, sizeof(*path));
			bh = read_extent_tree_block(inode, path[i].p_idx,
						    depth - i - 1,
						    EXT4_EX_NOCACHE);
			if (IS_ERR(bh)) {
				err = PTR_ERR(bh);
				break;
			}
			cond_resched();
			path[i + 1].p_bh = bh;
			path[i + 1].p_block = le16_to_cpu(path[i].p_hdr->eh_entries);
			i++;
		} else {
			/*
			 * 当前索引层处理完毕,若该索引层空了就删除索引,
			 * 否则回退到上一层
			 */
			if (path[i].p_hdr->eh_entries == 0 && i > 0) {
				/* 删除空索引块 */
				err = ext4_ext_rm_idx(handle, inode, path, i);
			}
			brelse(path[i].p_bh);
			path[i].p_bh = NULL;
			i--;
		}
	}

	trace_ext4_ext_remove_space_done(inode, start, end, depth, &partial,
					 path->p_hdr->eh_entries);

	/*
	 * 如果 partial.state == tofree,表示部分 cluster 需要被释放,
	 * 就在此处调用 ext4_free_blocks() 做真正的块释放。
	 */
	if (partial.state == tofree && err == 0) {
		int flags = get_default_free_blocks_flags(inode);

		if (ext4_is_pending(inode, partial.lblk))
			flags |= EXT4_FREE_BLOCKS_RERESERVE_CLUSTER;
		ext4_free_blocks(handle, inode, NULL,
				 EXT4_C2B(sbi, partial.pclu),
				 sbi->s_cluster_ratio, flags);
		if (flags & EXT4_FREE_BLOCKS_RERESERVE_CLUSTER)
			ext4_rereserve_cluster(inode, partial.lblk);
		partial.state = initial;
	}

	/*
	 * 如果整个树都删空了,则需要更新 eh_depth=0、eh_max 等字段
	 * 表示没有 extent。
	 */
	if (path->p_hdr->eh_entries == 0) {
		err = ext4_ext_get_access(handle, inode, path);
		if (err == 0) {
			ext_inode_hdr(inode)->eh_depth = 0;
			ext_inode_hdr(inode)->eh_max =
				cpu_to_le16(ext4_ext_space_root(inode, 0));
			err = ext4_ext_dirty(handle, inode, path);
		}
	}
out:
	ext4_ext_drop_refs(path);
	kfree(path);
	path = NULL;
	if (err == -EAGAIN)
		goto again;
	ext4_journal_stop(handle);

	return err;
}

这里已经是extent模式下释放相关空间的核心代码了,看懂它需要对extent及块分配释放的算法有所了解,这里目前不是本文的重点,本文的重点是分析文件删除的底层,源码摸到这也差不多了,在后续块分配算法分析的地方会专门来分析这个部分。

但是在这里我们仍然需要跟进到最终物理块实际释放的地方,以解答我们在最开始提出的第三个疑问。

接着看一下ext4_ext_rm_leaf函数和ext4_free_blocks函数:

1.5.5.2 核心块释放点 ext4_free_blocks源码分析
c 复制代码
/**
 * ext4_ext_rm_leaf() - 移除给定范围内的物理块,并在 Extent 树的叶子层更新记录
 * @handle:          日志事务句柄
 * @inode:           目标 inode
 * @path:            寻址到该叶子节点的路径信息
 * @partial_cluster: 描述在集群模式下需要特别处理的部分集群信息
 * @start:           要删除的起始逻辑块号
 * @end:             要删除的结束逻辑块号
 *
 * 该函数用于在 Extent 树的叶子层删除 [start, end] 范围的块。要求该范围与叶子中对应的
 * extent 有"完整的逻辑对应关系",否则返回 EIO(或 EFSCORRUPTED)。流程中会调用
 * ext4_remove_blocks() 实现物理块的释放,并在必要时调整或删除 Extent 项。
 */
static int
ext4_ext_rm_leaf(handle_t *handle, struct inode *inode,
		 struct ext4_ext_path *path,
		 struct partial_cluster *partial,
		 ext4_lblk_t start, ext4_lblk_t end)
{
	struct ext4_sb_info *sbi = EXT4_SB(inode->i_sb);
	int err = 0, correct_index = 0;
	int depth = ext_depth(inode), credits, revoke_credits;
	struct ext4_extent_header *eh;
	ext4_lblk_t a, b;
	unsigned num;
	ext4_lblk_t ex_ee_block;
	unsigned short ex_ee_len;
	unsigned unwritten = 0;
	struct ext4_extent *ex;
	ext4_fsblk_t pblk;

	/*
	 * p_hdr 代表当前叶子节点的 Extent Header(已在 ext4_ext_remove_space() 中检查过)。
	 */
	if (!path[depth].p_hdr)
		path[depth].p_hdr = ext_block_hdr(path[depth].p_bh);
	eh = path[depth].p_hdr;
	if (unlikely(path[depth].p_hdr == NULL)) {
		EXT4_ERROR_INODE(inode, "path[%d].p_hdr == NULL", depth);
		return -EFSCORRUPTED;
	}

	// 获取当前要处理的 extent
	ex = path[depth].p_ext;
	if (!ex)
		ex = EXT_LAST_EXTENT(eh); // 取叶子中的最后一个 extent

	ex_ee_block = le32_to_cpu(ex->ee_block);
	ex_ee_len   = ext4_ext_get_actual_len(ex);

	/*
	 * 从最后一个可用的 extent 往前遍历,只要和 [start, end] 区间有重叠,就执行删除操作。
	 */
	while (ex >= EXT_FIRST_EXTENT(eh) &&
	       (ex_ee_block + ex_ee_len > start)) {

		if (ext4_ext_is_unwritten(ex))
			unwritten = 1;
		else
			unwritten = 0;

		path[depth].p_ext = ex;  // 指向当前正在处理的 extent

		// 计算要删除的实际逻辑块区间 [a, b]
		a = (ex_ee_block > start) ? ex_ee_block : start;
		b = (ex_ee_block + ex_ee_len - 1 < end) ?
		    ex_ee_block + ex_ee_len - 1 : end;

		// 如果该 extent 完全在待删除区间之后,跳过并向前移动
		if (end < ex_ee_block) {
			if (sbi->s_cluster_ratio > 1) {
				// 对集群模式,需要标记右侧 extent 以免被误删
				pblk = ext4_ext_pblock(ex);
				partial->pclu = EXT4_B2C(sbi, pblk);
				partial->state = nofree;
			}
			ex--;
			ex_ee_block = le32_to_cpu(ex->ee_block);
			ex_ee_len   = ext4_ext_get_actual_len(ex);
			continue;
		// 如果要删除的区间不完整地覆盖当前 extent,则报错 (代码中是 -EFSCORRUPTED)
		} else if (b != ex_ee_block + ex_ee_len - 1) {
			err = -EFSCORRUPTED;
			goto out;
		} else if (a != ex_ee_block) {
			// 仅删除 extent 的尾部,一部分保留
			num = a - ex_ee_block;
		} else {
			// 该 extent 全部删除
			num = 0;
		}

		/*
		 * 估算当前操作可能需要的事务块数 credits 和 revoke_credits,
		 * 并调用 ext4_datasem_ensure_credits() 来扩展事务。
		 */
		credits = 7 + 2 * (ex_ee_len / EXT4_BLOCKS_PER_GROUP(inode->i_sb));
		if (ex == EXT_FIRST_EXTENT(eh)) {
			correct_index = 1;
			credits += ext_depth(inode) + 1;
		}
		credits += EXT4_MAXQUOTAS_TRANS_BLOCKS(inode->i_sb);

		revoke_credits =
			ext4_free_metadata_revoke_credits(inode->i_sb,
							  ext_depth(inode)) +
			ext4_free_data_revoke_credits(inode, b - a + 1);

		err = ext4_datasem_ensure_credits(handle, inode,
						  credits, credits,
						  revoke_credits);
		if (err) {
			if (err > 0)
				err = -EAGAIN;
			goto out;
		}

		// 准备修改该叶子块
		err = ext4_ext_get_access(handle, inode, path + depth);
		if (err)
			goto out;

		/*
		 * 调用 ext4_remove_blocks() 执行真正的物理块释放操作,
		 * 并同步更新 partial cluster 状态。
		 */
		err = ext4_remove_blocks(handle, inode, ex, partial, a, b);
		if (err)
			goto out;

		/*
		 * 若 num == 0,表示整个 extent 被删除,将 ee_len 置 0 并清空其 pblock。
		 * 之后在下面会把该 extent 条目从数组里移除。
		 */
		if (num == 0)
			ext4_ext_store_pblock(ex, 0);

		ex->ee_len = cpu_to_le16(num);
		// 如果该 extent 之前是 unwritten,但现在只剩下一部分块,则继续标记它为 unwritten
		if (unwritten && num)
			ext4_ext_mark_unwritten(ex);

		/*
		 * 如果该 extent 完全被删除 (num == 0),需要从 leaf 数组中移除该节点,
		 * 并将后续 extent 向前挪动。
		 */
		if (num == 0) {
			if (end != EXT_MAX_BLOCKS - 1) {
				// 对于hole punching,需要把后面的 extents 都往前搬移
				memmove(ex, ex + 1,
					(EXT_LAST_EXTENT(eh) - ex) *
					sizeof(struct ext4_extent));
				// 清空数组末尾的一项
				memset(EXT_LAST_EXTENT(eh), 0,
				       sizeof(struct ext4_extent));
			}
			le16_add_cpu(&eh->eh_entries, -1);
		}

		// 标记该叶子块已经被修改
		err = ext4_ext_dirty(handle, inode, path + depth);
		if (err)
			goto out;

		// 移动到前一个 extent
		ex--;
		ex_ee_block = le32_to_cpu(ex->ee_block);
		ex_ee_len   = ext4_ext_get_actual_len(ex);
	}

	/*
	 * 如果删除第一个extent时需要修正索引,需要调用 ext4_ext_correct_indexes()
	 */
	if (correct_index && eh->eh_entries)
		err = ext4_ext_correct_indexes(handle, inode, path);

	/*
	 * 如果 partial cluster 里还有需要释放的块,同时该叶子仍存在至少一个 extent,
	 * 则继续执行释放操作。
	 */
	if (partial->state == tofree && ex >= EXT_FIRST_EXTENT(eh)) {
		pblk = ext4_ext_pblock(ex) + ex_ee_len - 1;
		if (partial->pclu != EXT4_B2C(sbi, pblk)) {
			int flags = get_default_free_blocks_flags(inode);

			if (ext4_is_pending(inode, partial->lblk))
				flags |= EXT4_FREE_BLOCKS_RERESERVE_CLUSTER;
			ext4_free_blocks(handle, inode, NULL,
					 EXT4_C2B(sbi, partial->pclu),
					 sbi->s_cluster_ratio, flags);
			if (flags & EXT4_FREE_BLOCKS_RERESERVE_CLUSTER)
				ext4_rereserve_cluster(inode, partial->lblk);
		}
		partial->state = initial;
	}

	// 如果本叶子节点已空(eh->eh_entries == 0),则调用 ext4_ext_rm_idx 移除对应的索引块
	if (err == 0 && eh->eh_entries == 0 && path[depth].p_bh != NULL)
		err = ext4_ext_rm_idx(handle, inode, path, depth);

out:
	return err;
}

这个函数的核心也是用ext4_free_blocks处理块释放。

c 复制代码
/**
 * ext4_free_blocks() - 释放物理块到文件系统的空闲块池,并更新相关配额
 * @handle: 日志事务句柄
 * @inode:  对应的 inode
 * @bh:     可选的缓冲区指针,用于单块的 metadata 忘却(forget)
 * @block:  起始物理块号
 * @count:  要释放的块数
 * @flags:  释放块时需要使用的标志位
 *
 * 该函数是 ext4 中释放物理块的通用函数,会调用 ext4_mb_clear_bb()
 * 来实际清除位图并归还块到空闲池。如果需要,也会调用 ext4_forget() 来
 * 遗忘 metadata 块,将其从 page cache 和 buffer cache 中同步删除。
 */
void ext4_free_blocks(handle_t *handle, struct inode *inode,
		      struct buffer_head *bh, ext4_fsblk_t block,
		      unsigned long count, int flags)
{
	struct super_block *sb = inode->i_sb;
	struct ext4_sb_info *sbi = EXT4_SB(sb);
	unsigned int overflow;

	// 如果文件系统处于快速回放(FC Replay),直接使用 ext4_free_blocks_simple() 无需记录到日志
	if (sbi->s_mount_state & EXT4_FC_REPLAY) {
		ext4_free_blocks_simple(inode, block, count);
		return;
	}

	might_sleep();

	// 如果传入了 bh,但 block 尚未设定,则从 bh->b_blocknr 获取物理块号
	if (bh) {
		if (block)
			BUG_ON(block != bh->b_blocknr);
		else
			block = bh->b_blocknr;
	}

	// 再次确认所释放的物理块合法性(除非 EXT4_FREE_BLOCKS_VALIDATED 标志保证合法)
	if (!(flags & EXT4_FREE_BLOCKS_VALIDATED) &&
	    !ext4_inode_block_valid(inode, block, count)) {
		ext4_error(sb, "Freeing blocks not in datazone - "
			   "block = %llu, count = %lu", block, count);
		return;
	}

	// 如果需要forget某个 metadata 块(EXT4_FREE_BLOCKS_FORGET),则调用 ext4_forget()
	if (bh && (flags & EXT4_FREE_BLOCKS_FORGET)) {
		BUG_ON(count > 1);
		ext4_forget(handle, flags & EXT4_FREE_BLOCKS_METADATA,
			    inode, bh, block);
	}

	/*
	 * 考虑到集群模式,一次释放操作可能需要对齐到集群边界。
	 * 若存在 NOFREE_FIRST_CLUSTER 或 NOFREE_LAST_CLUSTER 标志,
	 * 则会调整 block 与 count,以避开首尾的 partial cluster。
	 */
	overflow = EXT4_PBLK_COFF(sbi, block);
	if (overflow) {
		if (flags & EXT4_FREE_BLOCKS_NOFREE_FIRST_CLUSTER) {
			overflow = sbi->s_cluster_ratio - overflow;
			block += overflow;
			if (count > overflow)
				count -= overflow;
			else
				return;
		} else {
			block -= overflow;
			count += overflow;
		}
	}
	overflow = EXT4_LBLK_COFF(sbi, count);
	if (overflow) {
		if (flags & EXT4_FREE_BLOCKS_NOFREE_LAST_CLUSTER) {
			if (count > overflow)
				count -= overflow;
			else
				return;
		} else
			count += sbi->s_cluster_ratio - overflow;
	}

	// 若需要对 [block, count] 区间的所有块执行forget操作,则循环调用 ext4_forget
	if (!bh && (flags & EXT4_FREE_BLOCKS_FORGET)) {
		int i;
		int is_metadata = flags & EXT4_FREE_BLOCKS_METADATA;

		for (i = 0; i < count; i++) {
			cond_resched();
			if (is_metadata)
				bh = sb_find_get_block(inode->i_sb, block + i);
			ext4_forget(handle, is_metadata, inode, bh, block + i);
		}
	}

	// 最终调用 ext4_mb_clear_bb() 更新块位图,释放 [block, block+count-1] 到空闲池
	ext4_mb_clear_bb(handle, inode, block, count, flags);
}

我们仔细看一下这个函数的执行流程:

  1. 合法性检查
    • 如果没有指定 EXT4_FREE_BLOCKS_VALIDATED,则调用 ext4_inode_block_valid() 再次验证物理块范围合法性。
  2. 处理 metadata forget
    • 若需要对 metadata 块执行"忘却(forget)",调用 ext4_forget 使其从 buffer/page cache 中移除。
  3. 对齐集群模式
    • 若启用了大于 1 的 cluster ratio,需要考虑部分集群的首末保留或整 cluster 释放。
  4. 正式释放到位图
    • 最终调用 ext4_mb_clear_bb() 清空块位图并归还到 free block pool,同时更新配额信息(如果启用了配额)。

首先解答一下这里的metadata为什么要进行forget操作。

当我们在执行 ext4_free_blocks() 时,如果指定了标志 EXT4_FREE_BLOCKS_FORGET 且块属于metadata 类型(比如它存储的是索引块、目录块等),就会调用内核函数 ext4_forget() 来 "遗忘" 这些块。这通常意味着:

  1. 从当前的页缓存 / buffer cache 中清除:
    • 将对应的 buffer_head 失效或丢弃,防止后续再访问这个块时还以为它在使用中。
  2. 解除与 inode(或其他结构)的映射关系:
    • 让内核不再将其视为正在使用的元数据块。
  3. 在日志(journal)层面:
    • 确保事务日志对该块的修改不会再被视为"有效"元数据。

换言之,"forget" 是在告诉文件系统与缓存层:"此块不再包含有效的文件系统元数据了"。

接下来我们看看ext4_mb_clear_bb函数:

c 复制代码
/**
 * ext4_mb_clear_bb() -- 释放(free)给定范围的块时的辅助函数
 *			    (被 ext4_free_blocks() 调用)
 * @handle:		事务句柄(journal transaction handle)
 * @inode:		对应的 inode
 * @block:		要释放的起始物理块号
 * @count:		要释放的块个数
 * @flags:		释放块时使用的标志位(ext4_free_blocks传入)
 *
 * 函数职责:
 * 1. 找到对应的块组(block_group)和在组内的偏移量(bit),判断是否跨越多个组。
 * 2. 如果需要分多段处理,则分段循环执行对位图、组描述符的更新。
 * 3. 如果开启了 journaling 并且指定释放的块可能是 metadata(或需要做 "forget"),
 *    则将该块加入到"延迟真正释放"的列表(free cluster list)中,等待事务提交后再进行复用。
 * 4. 更新块位图和组描述符,减少该组的空闲块计数,并进行校验和(checksum)更新。
 *
 * 注意:此函数不会物理地对块进行"零填"或"擦除"数据,而是仅在 ext4 的元数据(位图、组描述符)中标记这些块为可用。
 */
static void ext4_mb_clear_bb(handle_t *handle, struct inode *inode,
			     ext4_fsblk_t block, unsigned long count,
			     int flags)
{
	struct buffer_head *bitmap_bh = NULL;        // 用于读取组内位图的buffer
	struct super_block *sb = inode->i_sb;        // 对应的超级块
	struct ext4_group_desc *gdp;                 // 组描述符指针
	unsigned int overflow;                       // 如果释放范围跨越组边界,用于存储溢出部分
	ext4_grpblk_t bit;                           // 组内块偏移(cluster 粒度)
	struct buffer_head *gd_bh;                   // 组描述符所在的 buffer head
	ext4_group_t block_group;                    // 块组号
	struct ext4_sb_info *sbi;                    // ext4 超级块信息
	struct ext4_buddy e4b;                       // buddy信息结构,用于管理空闲块
	unsigned int count_clusters;                 // 要释放的块数对应的 cluster 数
	int err = 0;                                 // 函数内错误码
	int ret;                                     // 用于记录函数返回值的临时变量

	sbi = EXT4_SB(sb);

do_more:
	overflow = 0;
	// 根据 block 计算其所在的块组 block_group 以及在组内的偏移 bit(cluster 粒度)
	ext4_get_group_no_and_offset(sb, block, &block_group, &bit);

	// 如果该块组已被标记为位图损坏,则直接返回
	if (unlikely(EXT4_MB_GRP_BBITMAP_CORRUPT(ext4_get_group_info(sb, block_group))))
		return;

	/*
	 * 判断当前要释放的块数 (count) 是否会跨越该组的边界:
	 * 如果溢出到下一个块组,则将本组能处理的部分先处理,剩余的丢给下一轮。
	 */
	if (EXT4_C2B(sbi, bit) + count > EXT4_BLOCKS_PER_GROUP(sb)) {
		overflow = EXT4_C2B(sbi, bit) + count - EXT4_BLOCKS_PER_GROUP(sb);
		count -= overflow;
	}
	count_clusters = EXT4_NUM_B2C(sbi, count);

	// 读取该块组的位图
	bitmap_bh = ext4_read_block_bitmap(sb, block_group);
	if (IS_ERR(bitmap_bh)) {
		err = PTR_ERR(bitmap_bh);
		bitmap_bh = NULL;
		goto error_return;
	}

	// 获取组描述符
	gdp = ext4_get_group_desc(sb, block_group, &gd_bh);
	if (!gdp) {
		err = -EIO;
		goto error_return;
	}

	// 确认 [block, block+count-1] 范围落在有效数据区(非保留元数据区域)
	if (!ext4_inode_block_valid(inode, block, count)) {
		ext4_error(sb, "Freeing blocks in system zone - "
			   "Block = %llu, count = %lu", block, count);
		// 不直接返回错误,而是走 error_return 流程进行异常处理
		goto error_return;
	}

	/*
	 * 先获取对bitmap_bh(块位图所在buffer)和 gd_bh(组描述符)的写访问权限
	 * 以便修改后记录到事务日志
	 */
	BUFFER_TRACE(bitmap_bh, "getting write access");
	err = ext4_journal_get_write_access(handle, sb, bitmap_bh, EXT4_JTR_NONE);
	if (err)
		goto error_return;

	BUFFER_TRACE(gd_bh, "get_write_access");
	err = ext4_journal_get_write_access(handle, sb, gd_bh, EXT4_JTR_NONE);
	if (err)
		goto error_return;

#ifdef AGGRESSIVE_CHECK
	// 调试模式下,可校验要释放的块是否全部处于已分配状态
	{
		int i;
		for (i = 0; i < count_clusters; i++)
			BUG_ON(!mb_test_bit(bit + i, bitmap_bh->b_data));
	}
#endif

	trace_ext4_mballoc_free(sb, inode, block_group, bit, count_clusters);

	/*
	 * 加载该块组的 buddy 缓存,用于修改空闲块信息
	 * GFP_NOFS|__GFP_NOFAIL 保证分配内存时不会轻易失败
	 */
	err = ext4_mb_load_buddy_gfp(sb, block_group, &e4b, GFP_NOFS | __GFP_NOFAIL);
	if (err)
		goto error_return;

	/*
	 * 如果是 metadata 块(或需要在事务完成前不复用),
	 * 就把这些块记录到 "延迟释放列表",并在位图上清理(或标记)。
	 * 这样在事务提交前,这些块不会重新分配给其他文件。
	 */
	if (ext4_handle_valid(handle) &&
	    ((flags & EXT4_FREE_BLOCKS_METADATA) ||
	     !ext4_should_writeback_data(inode))) {
		// 分配一个 ext4_free_data 结构,把要释放的块范围记录进去
		struct ext4_free_data *new_entry;

		new_entry = kmem_cache_alloc(ext4_free_data_cachep, GFP_NOFS|__GFP_NOFAIL);
		new_entry->efd_start_cluster = bit;
		new_entry->efd_group = block_group;
		new_entry->efd_count = count_clusters;
		new_entry->efd_tid = handle->h_transaction->t_tid;

		// 上锁后在 bitmap_bh->b_data 中清零对应位(表示这些块不再使用)
		ext4_lock_group(sb, block_group);
		mb_clear_bits(bitmap_bh->b_data, bit, count_clusters);
		// 注册到 buddy 的 free list 中(ext4_mb_free_metadata)
		ext4_mb_free_metadata(handle, &e4b, new_entry);
	} else {
		/*
		 * 否则就是一般情况,不需要延迟释放,可立即更新位图和
		 * 组描述符来释放块,并允许以后立即再次分配。
		 */
		if (test_opt(sb, DISCARD)) {
			// 如果文件系统启用了discard选项,尝试对这些块执行一次TRIM
			err = ext4_issue_discard(sb, block_group, bit, count, NULL);
			if (err && err != -EOPNOTSUPP)
				ext4_msg(sb, KERN_WARNING,
					 "discard request in group:%u block:%d "
					 "count:%lu failed with %d",
					 block_group, bit, count, err);
		} else {
			EXT4_MB_GRP_CLEAR_TRIMMED(e4b.bd_info);
		}

		ext4_lock_group(sb, block_group);
		mb_clear_bits(bitmap_bh->b_data, bit, count_clusters);
		// 在buddy缓存中释放这些块
		mb_free_blocks(inode, &e4b, bit, count_clusters);
	}

	// 更新组描述符中的 free 集群计数
	ret = ext4_free_group_clusters(sb, gdp) + count_clusters;
	ext4_free_group_clusters_set(sb, gdp, ret);

	// 更新块位图和组描述符的校验和
	ext4_block_bitmap_csum_set(sb, block_group, gdp, bitmap_bh);
	ext4_group_desc_csum_set(sb, block_group, gdp);
	ext4_unlock_group(sb, block_group);

	// 如果启用了 flex_bg,需要更新其 free_clusters 计数
	if (sbi->s_log_groups_per_flex) {
		ext4_group_t flex_group = ext4_flex_group(sbi, block_group);
		atomic64_add(count_clusters,
			     &sbi_array_rcu_deref(sbi, s_flex_groups, flex_group)->free_clusters);
	}

	// 卸载 buddy 缓存
	ext4_mb_unload_buddy(&e4b);

	// 标记 bitmap_bh 为脏,用于写回日志
	BUFFER_TRACE(bitmap_bh, "dirtied bitmap block");
	err = ext4_handle_dirty_metadata(handle, NULL, bitmap_bh);

	// 标记组描述符 (gd_bh) 为脏
	BUFFER_TRACE(gd_bh, "dirtied group descriptor block");
	ret = ext4_handle_dirty_metadata(handle, NULL, gd_bh);
	if (!err)
		err = ret;

	// 如果溢出到下一个块组,则调回 do_more 继续处理溢出部分
	if (overflow && !err) {
		block += count;  // 移动到下一组的起始块
		count = overflow;
		put_bh(bitmap_bh);
		goto do_more;
	}

error_return:
	brelse(bitmap_bh);
	// 如果发生错误,记录后续处理
	ext4_std_error(sb, err);
	return;
}

可以看出,ext4_mb_clear_bb(以及上层的 ext4_free_blocks)在释放块时,核心操作是更新元数据(位图、组描述符、可能的 buddy 缓存等)表示这些块已空闲。并不会对物理磁盘块执行零填或覆盖操作。

至此我们完美的找到了一问三的答案!!!

1.5.5.3 间接索引模式下的截断 ext4_ind_truncate源码分析

其实分析完extent模式下的源码后,这里已经没有分析的必要了,因为截断/删除的核心逻辑我们已经找到了,但还是简单看一下这个函数的流程如何。

c 复制代码
/**
 * ext4_ind_truncate - 截断(truncate)一个使用传统间接寻址的 ext4 inode
 * @handle:  日志事务句柄
 * @inode:   需要被截断的 inode
 *
 * 该函数主要应用于传统非-extents 的 inode,处理其 direct block(直接块)、
 * single indirect、double indirect 和 triple indirect block 的回收。
 * 运行流程:
 * 1. 计算出文件要截断后的逻辑块号 last_block。
 * 2. 若 last_block 不是文件系统的最大允许块号 max_block,则调用ext4_block_to_path
 *    找出路径。
 * 3. 调用 ext4_es_remove_extent 移除 inode 在 [last_block, EXT_MAX_BLOCKS) 的
 *    extent 状态缓存,以免后续截断时冲突。
 * 4. 同步更新 i_disksize = i_size,以便在崩溃后能够恢复截断位置。
 * 5. 根据返回的路径 depth,选择性地释放 direct blocks 或按某种方式递归释放
 *    indirect blocks。
 * 6. 最后统一释放 single indirect、double indirect、triple indirect 指针指向的块。
 */
void ext4_ind_truncate(handle_t *handle, struct inode *inode)
{
	struct ext4_inode_info *ei = EXT4_I(inode);
	__le32 *i_data = ei->i_data;    // 指向 inode 中 12个直接块 + 3个间接块的数组
	int addr_per_block = EXT4_ADDR_PER_BLOCK(inode->i_sb);
	ext4_lblk_t offsets[4];         // 存储分解的逻辑块偏移
	Indirect chain[4];             // 存储路径中各级间接块的元信息
	Indirect *partial;             // 指向部分被共享的路径
	__le32 nr = 0;
	int n = 0;
	ext4_lblk_t last_block, max_block;
	unsigned blocksize = inode->i_sb->s_blocksize;

	/*
	 * last_block:根据文件新的i_size,计算出最后一个需要保留的逻辑块号。
	 * max_block:ext4 全局允许的最大块号(受限于s_bitmap_maxbytes)。
	 */
	last_block = (inode->i_size + blocksize - 1)
			>> EXT4_BLOCK_SIZE_BITS(inode->i_sb);
	max_block = (EXT4_SB(inode->i_sb)->s_bitmap_maxbytes + blocksize - 1)
			>> EXT4_BLOCK_SIZE_BITS(inode->i_sb);

	/*
	 * 如果 last_block == max_block,表示截断后的大小达到了ext4传统寻址极限,
	 * 不需要再额外释放数据块(因为所有块都算在有效范围内)。
	 */
	if (last_block != max_block) {
		// 计算从 inode->i_data 出发,到达 last_block 的索引路径(深度为n)
		n = ext4_block_to_path(inode, last_block, offsets, NULL);
		if (n == 0)
			return;
	}

	/*
	 * 移除 [last_block, EXT_MAX_BLOCKS) 范围内的Extent状态缓存,
	 * 避免后续截断与缓存冲突。
	 */
	ext4_es_remove_extent(inode, last_block, EXT_MAX_BLOCKS - last_block);

	/*
	 * 在进入 orphan list保护后,就可以把 i_disksize 更新成新的 i_size,
	 * 这样万一崩溃,ext4_orphan_cleanup 也能正确截断。
	 */
	ei->i_disksize = inode->i_size;

	if (last_block == max_block) {
		/*
		 * 如果要截断到ext4最大寻址限制处,则不需要释放块
		 */
		return;
	} else if (n == 1) {
		/*
		 * 当 n==1,说明要截断的块位于 direct block(直接块)范围内。
		 * offsets[0] 是要释放的起始位置,
		 * 释放 [offsets[0], EXT4_NDIR_BLOCKS) 范围的直接块即可。
		 */
		ext4_free_data(handle, inode, NULL, i_data + offsets[0],
			       i_data + EXT4_NDIR_BLOCKS);
		goto do_indirects;
	}

	/*
	 * ext4_find_shared 查找和其他可能共享的路径部分,并返回 partial 指针。
	 * 同时如果它发现了需要单独处理的块号,会存入nr。
	 */
	partial = ext4_find_shared(inode, n, offsets, chain, &nr);
	// 如果 nr!=0,说明有一个 top-level block 需要单独释放
	if (nr) {
		if (partial == chain) {
			/*
			 * 表示共享的分支直接挂在 inode->i_data 上,这里相当于
			 * "整条分支从 inode 出来,只有最顶的一个block要释放"
			 */
			ext4_free_branches(handle, inode, NULL, &nr, &nr+1,
					   (chain + n - 1) - partial);
			*partial->p = 0;  
		} else {
			/*
			 * 共享的分支挂在一个间接块中,需要先 get_write_access,
			 * 再调用 ext4_free_branches 释放 nr 指向的block
			 */
			BUFFER_TRACE(partial->bh, "get_write_access");
			ext4_free_branches(handle, inode, partial->bh,
				partial->p, partial->p + 1,
				(chain + n - 1) - partial);
		}
	}

	/*
	 * 从 partial 往回一路释放中间节点里 [partial->p+1 .. block结尾] 的数据
	 */
	while (partial > chain) {
		ext4_free_branches(handle, inode, partial->bh,
				   partial->p + 1,
				   (__le32 *)partial->bh->b_data + addr_per_block,
				   (chain + n - 1) - partial);
		BUFFER_TRACE(partial->bh, "call brelse");
		brelse(partial->bh);
		partial--;
	}

do_indirects:
	/*
	 * 经过上面步骤后,凡是需要部分删除的间接节点都已经处理完了,
	 * 剩下的就把对应的 single indirect / double indirect / triple indirect
	 * 全部清空(如果有)。
	 */
	switch (offsets[0]) {
	default:
		// 1) single indirect
		nr = i_data[EXT4_IND_BLOCK];
		if (nr) {
			ext4_free_branches(handle, inode, NULL, &nr, &nr+1, 1);
			i_data[EXT4_IND_BLOCK] = 0;
		}
		fallthrough;
	case EXT4_IND_BLOCK:
		// 2) double indirect
		nr = i_data[EXT4_DIND_BLOCK];
		if (nr) {
			ext4_free_branches(handle, inode, NULL, &nr, &nr+1, 2);
			i_data[EXT4_DIND_BLOCK] = 0;
		}
		fallthrough;
	case EXT4_DIND_BLOCK:
		// 3) triple indirect
		nr = i_data[EXT4_TIND_BLOCK];
		if (nr) {
			ext4_free_branches(handle, inode, NULL, &nr, &nr+1, 3);
			i_data[EXT4_TIND_BLOCK] = 0;
		}
		fallthrough;
	case EXT4_TIND_BLOCK:
		/* nothing more to do */
		;
	}
}

后续就不再分析了,其实最终还是调用的ext4_free_blocks函数执行的实际块清理,不同的地方在于如何找到这些块。

1.5.6 inode删除 iput源码分析

有了上面的分析,我们知道底层的数据块其实并没有真正清空,只是更新了位图表示这些块可用而已。但是还有一个问题没有解决,就是inode本身是否会被清空呢,这也是我们全文最后一个疑问了。其核心逻辑就在上面ext4_process_orphan函数最后调用的iput(inode)里,然我们一起看看吧!

iput(inode) 函数的作用是减少一个 inode 的引用计数,并在引用计数降为 0 时释放该 inode 及其关联的资源。iput 是 "inode put" 的缩写,表示对 inode 的引用计数进行递减操作。

其源码位于fs/inode.c中,我们先看下它的通用逻辑。

1.5.6.1 fs层通用逻辑

iput函数源码如下:

c 复制代码
/**
 * iput - 递减 inode 的引用计数
 * @inode: 要操作的 inode
 *
 * 如果 inode 引用计数减到 0,则调用 iput_final 执行后续的回收流程;
 * 若 inode 还在使用中(引用计数不为 0),则只做一次普通的计数-1 并退出。
 *
 * 注意:iput() 可能会导致 inode 真正被销毁,因此可以引发睡眠(等待 IO 等)。
 */
void iput(struct inode *inode)
{
    if (!inode)
        return;

    BUG_ON(inode->i_state & I_CLEAR); // 确保 inode 未标记为"清理中"
retry:
    // 原子地将 i_count 减一,并且如果减到0则获取 i_lock
    if (atomic_dec_and_lock(&inode->i_count, &inode->i_lock)) {
        // 如果 inode 有链接计数且带有 I_DIRTY_TIME 状态,
        // 说明可能需要先更新一下延迟时间戳,然后重新尝试 iput 流程
        if (inode->i_nlink && (inode->i_state & I_DIRTY_TIME)) {
            atomic_inc(&inode->i_count);
            spin_unlock(&inode->i_lock);
            // 记录到 trace,并将 inode 标记为同步写回
            trace_writeback_lazytime_iput(inode);
            mark_inode_dirty_sync(inode);
            goto retry; // 回到 retry 重新执行
        }
        // 真正进入最后一次 put,调用 iput_final
        iput_final(inode);
    }
}
EXPORT_SYMBOL(iput);

iput_final函数源码如下:

c 复制代码
/*
 * iput_final - 当 inode 最后一次引用被释放时调用
 * @inode: inode
 *
 * 1. 调用 drop_inode / generic_drop_inode 以决定 inode 是否可以被真正释放。
 * 2. 若 drop=0(不删除),且 inode 所在的 super_block 依旧处于活动状态,
 *    则将 inode 加回 inode LRU 列表以便后续重用,并返回。
 * 3. 否则进入 inode 的真正"Freeing"流程。先可能进行写回 (write_inode_now),
 *    然后设置 inode 状态为 I_FREEING 并将其从 LRU 上移除。
 * 4. 最终调用 evict(inode) 完成 inode 回收工作。
 */
static void iput_final(struct inode *inode)
{
    struct super_block *sb = inode->i_sb;
    const struct super_operations *op = inode->i_sb->s_op;
    unsigned long state;
    int drop;

    WARN_ON(inode->i_state & I_NEW); // 不应当在 I_NEW 状态时进入

    // 调用文件系统的 drop_inode()(若存在),否则用 generic_drop_inode()
    if (op->drop_inode)
        drop = op->drop_inode(inode);
    else
        drop = generic_drop_inode(inode);

    // 如果文件系统选择"不删除此 inode",且 inode 未被标记为DONTCACHE,且超级块还在活动,
    // 就把 inode 加回LRU列表,并退出
    if (!drop &&
        !(inode->i_state & I_DONTCACHE) &&
        (sb->s_flags & SB_ACTIVE)) {
        __inode_add_lru(inode, true);
        spin_unlock(&inode->i_lock);
        return;
    }

    // 到这表示需要释放
    state = inode->i_state;
    if (!drop) {
        // 如果 drop=0 但别的原因需要释放(例如系统正准备关机),
        // 先写回 inode
        WRITE_ONCE(inode->i_state, state | I_WILL_FREE);
        spin_unlock(&inode->i_lock);

        write_inode_now(inode, 1); // 强制写回 inode
        spin_lock(&inode->i_lock);
        state = inode->i_state;
        WARN_ON(state & I_NEW);
        state &= ~I_WILL_FREE; // 清除 I_WILL_FREE 标志
    }

    // 最终将 inode 状态标记为 I_FREEING
    WRITE_ONCE(inode->i_state, state | I_FREEING);
    if (!list_empty(&inode->i_lru))
        inode_lru_list_del(inode);
    spin_unlock(&inode->i_lock);

    // 调用 evict(inode) 执行真正的销毁逻辑
    evict(inode);
}

evict函数源码如下:

c 复制代码
/**
 * evict - 真正的 "回收 / 逐出" inode
 * @inode: 要回收的 inode
 *
 * 1. inode 必须已被标记为 I_FREEING。
 * 2. 等待可能正在进行的 writeback 完成,避免并发写回时文件系统出错。
 * 3. 若 super_operations 定义了 evict_inode(),则调用它,否则执行 truncate_inode_pages_final + clear_inode。
 * 4. 移除 inode 的 hash 链接,彻底从全局可见名单中删除。
 * 5. 调用 destroy_inode(inode) 进行最后的结构释放。
 */
static void evict(struct inode *inode)
{
    const struct super_operations *op = inode->i_sb->s_op;

    BUG_ON(!(inode->i_state & I_FREEING)); // 必须标记过
    BUG_ON(!list_empty(&inode->i_lru));    // 应该已经从 LRU 移除

    if (!list_empty(&inode->i_io_list))
        inode_io_list_del(inode);

    inode_sb_list_del(inode);

    /*
     * 等待 flusher 线程结束对该 inode 的写回工作。
     */
    inode_wait_for_writeback(inode);

    /*
     * 如果文件系统定义了 evict_inode,则调用它做文件系统特定的删除逻辑;
     * 否则使用默认的 truncate + clear_inode。
     */
    if (op->evict_inode) {
        op->evict_inode(inode);
    } else {
        truncate_inode_pages_final(&inode->i_data);
        clear_inode(inode);
    }

    // 若是字符设备 inode,则需要 cd_forget
    if (S_ISCHR(inode->i_mode) && inode->i_cdev)
        cd_forget(inode);

    // 从 inode 全局 hash 中移除
    remove_inode_hash(inode);

    // 释放锁并确保 inode->i_state 正确
    spin_lock(&inode->i_lock);
    wake_up_bit(&inode->i_state, __I_NEW);
    BUG_ON(inode->i_state != (I_FREEING | I_CLEAR));
    spin_unlock(&inode->i_lock);

    // 最后调用 destroy_inode(inode) 释放其内存
    destroy_inode(inode);
}

在这里可以看到,通用逻辑开始调用到ext4层的释放inode的逻辑evict_inode函数了,接下来我们分析下EXT4文件系统中是怎么做的。

1.5.6.2 EXT4中的evict_inode源码分析

我们可以看到在ext4中的evict_inode实质为ext4_evict_inode函数。

其源码如下:

c 复制代码
/**
 * ext4_evict_inode - 在最后一次 iput() 且 i_nlink=0 时调用,执行 ext4 中 inode 的释放
 * @inode: 需要被回收的 inode
 *
 * 该函数由 VFS 层的 evict() 回调调用。当一个 ext4 inode 的引用计数归零 (i_count=0) 且链接计数 (i_nlink) 为 0 时,
 * 表示该 inode 可以从磁盘结构中删除。主要过程包括:
 *
 * 1. 对于启用 journaling data 并且是常规文件的 inode,需要先将相关脏页写回并等待提交完毕,以避免数据丢失。
 * 2. 如果 inode 还未被标记坏 (is_bad_inode() 为 false),执行额外的截断操作(如 ordered data 模式下).
 * 3. 启动一个 truncate 的 journal 事务,释放 inode 占用的所有块,移除 xattr,最后调用 ext4_free_inode 释放该 inode 元数据。
 * 4. 如果在流程中出现错误,会把 inode 从 orphan 列表移除,然后仅进行必要的内存清理 (ext4_clear_inode)。
 */
void ext4_evict_inode(struct inode *inode)
{
    handle_t *handle;
    int err;
    /*
     * extra_credits: 计算在最后释放 inode 时需要的 journal 事务日志额度,
     * 例如涉及 sb、inode 自身、bitmap、group descriptor、xattr 块等。
     */
    int extra_credits = 6;
    struct ext4_xattr_inode_array *ea_inode_array = NULL;
    bool freeze_protected = false;

    trace_ext4_evict_inode(inode);

    // 如果 i_nlink != 0,说明还不能删除该 inode,只做截断页面缓存等操作
    if (inode->i_nlink) {
        /*
         * 对于启用了 journaling data 的常规文件,需要确保其 page cache
         * 中的数据都写回并提交到磁盘,防止截断后丢失数据。
         */
        if (inode->i_ino != EXT4_JOURNAL_INO &&
            ext4_should_journal_data(inode) &&
            S_ISREG(inode->i_mode) && inode->i_data.nrpages) {
            journal_t *journal = EXT4_SB(inode->i_sb)->s_journal;
            tid_t commit_tid = EXT4_I(inode)->i_datasync_tid;

            jbd2_complete_transaction(journal, commit_tid);
            filemap_write_and_wait(&inode->i_data);
        }
        truncate_inode_pages_final(&inode->i_data);
        goto no_delete;
    }

    // 若 inode 标记为坏,不做删除处理,仅执行 no_delete 路径
    if (is_bad_inode(inode))
        goto no_delete;

    dquot_initialize(inode);

    // 如果是 ordered data 模式,需开始截断
    if (ext4_should_order_data(inode))
        ext4_begin_ordered_truncate(inode, 0);
    truncate_inode_pages_final(&inode->i_data);

    /*
     * 对于带 journaling data 的 inode,可能因事务提交导致 inode 又变脏;
     * 这里先确保从写回队列中移除。
     */
    if (!list_empty_careful(&inode->i_io_list)) {
        WARN_ON_ONCE(!ext4_should_journal_data(inode));
        inode_io_list_del(inode);
    }

    /*
     * 若当前不处于一个已开启的 ext4_journal handle 中,需要对文件系统加写保护
     * 防止被冻结 (sb_start_intwrite)。
     */
    if (!ext4_journal_current_handle()) {
        sb_start_intwrite(inode->i_sb);
        freeze_protected = true;
    }

    if (!IS_NOQUOTA(inode))
        extra_credits += EXT4_MAXQUOTAS_DEL_BLOCKS(inode->i_sb);

    /*
     * 由于截断操作中也要更新 block bitmap、group descriptor、inode 等,
     * extra_credits 中已包括一些内容,需要将重复的 3 个 credits 减去。
     */
    handle = ext4_journal_start(inode, EXT4_HT_TRUNCATE,
                ext4_blocks_for_truncate(inode) + extra_credits - 3);
    if (IS_ERR(handle)) {
        ext4_std_error(inode->i_sb, PTR_ERR(handle));
        // 即使启动事务失败,也要从 orphan 列表中移除
        ext4_orphan_del(NULL, inode);
        if (freeze_protected)
            sb_end_intwrite(inode->i_sb);
        goto no_delete;
    }

    if (IS_SYNC(inode))
        ext4_handle_sync(handle);

    /*
     * 若是快速符号链接,需要先清除 i_data;然后设置 i_size=0 以便后续的 ext4_truncate() 释放块。
     */
    if (ext4_inode_is_fast_symlink(inode))
        memset(EXT4_I(inode)->i_data, 0, sizeof(EXT4_I(inode)->i_data));
    inode->i_size = 0;
    err = ext4_mark_inode_dirty(handle, inode);
    if (err) {
        ext4_warning(inode->i_sb,
                     "couldn't mark inode dirty (err %d)", err);
        goto stop_handle;
    }

    // 若该 inode 仍占用块 (i_blocks != 0),执行 ext4_truncate 释放
    if (inode->i_blocks) {
        err = ext4_truncate(inode);
        if (err) {
            ext4_error_err(inode->i_sb, -err,
               "couldn't truncate inode %lu (err %d)",
               inode->i_ino, err);
            goto stop_handle;
        }
    }

    // 删除该 inode 可能存在的所有 xattr,返回 ea_inode_array 用于后续释放
    err = ext4_xattr_delete_inode(handle, inode, &ea_inode_array,
                                  extra_credits);
    if (err) {
        ext4_warning(inode->i_sb, "xattr delete (err %d)", err);
        goto stop_handle;
    }

    // 从 orphan 列表中删除该 inode
    ext4_orphan_del(handle, inode);

    // 将 dtime 设为当前时间
    EXT4_I(inode)->i_dtime = (__u32)ktime_get_real_seconds();

    // 尝试再次将 inode 标记为脏,用于更新 i_dtime
    if (ext4_mark_inode_dirty(handle, inode))
        // 如果标记失败,则只做一个 in-core 的 clear,没法做完整释放
        ext4_clear_inode(inode);
    else
        // 否则进行 ext4_free_inode,把 inode 结构从磁盘结构中彻底释放
        ext4_free_inode(handle, inode);

    ext4_journal_stop(handle);
    if (freeze_protected)
        sb_end_intwrite(inode->i_sb);
    ext4_xattr_inode_array_free(ea_inode_array);
    return;

stop_handle:
    // 如果出错,停止 journal,并从 orphan 中移除,然后清理
    ext4_journal_stop(handle);
    ext4_orphan_del(NULL, inode);
    if (freeze_protected)
        sb_end_intwrite(inode->i_sb);
    ext4_xattr_inode_array_free(ea_inode_array);
no_delete:
    /*
     * 如果 inode 无法正常删除,也要保证清理其缓存信息,比如
     * orphan 链接、预分配、extent 状态等。
     */
    if (!list_empty(&EXT4_I(inode)->i_fc_list))
        ext4_fc_mark_ineligible(inode->i_sb, EXT4_FC_REASON_NOMEM, NULL);

    ext4_clear_inode(inode);
}

从源码中可以发现大致流程:

  1. i_nlink!=0:仅截断 page cache,并 不 做真正删除;
  2. 引用计数=0 且非坏 inode:开始真正删除:
    • 处理 journaling data 情况,写回脏页
    • 开启 truncate 事务,i_size=0 → ext4_truncate(inode) 释放块。
    • 删除 xattr,移除 orphan 链表,设置 dtime。
    • 调用 ext4_free_inode把 inode 结构从磁盘结构中彻底释放。
    • 如果出现错误,则仅在内存层面执行 ext4_clear_inode

ext4_free_inode函数源码如下:

c 复制代码
/**
 * ext4_free_inode - 释放一个已从文件系统引用中分离的 inode
 * @handle: 当前进行中的 journaling 事务句柄
 * @inode:  要被删除/回收的 inode
 *
 * 注意:
 * 1. 进入本函数时,VFS 保证这个 inode 已经没有任何目录项引用(i_nlink == 0),且
 *    在内核内部也无其它引用(i_count <= 1),因此不会出现竞态条件。
 * 2. 要先调用 ext4_clear_inode(inode) 清理内存态信息,再把 inode 对应的位(bitmap)清零
 *    来表示磁盘上不再使用该 inode。
 * 3. 如果 inode bitmap 出现校验错误或者事务中出现错误(fatal),函数只会进行必要的清理并返回。
 */
void ext4_free_inode(handle_t *handle, struct inode *inode)
{
    struct super_block *sb = inode->i_sb;      // 对应超级块
    int is_directory;                          // 是否目录
    unsigned long ino;                         // inode 编号
    struct buffer_head *bitmap_bh = NULL;      // inode bitmap 的 buffer_head
    struct buffer_head *bh2;                   // 对应 group descriptor 的 buffer_head
    ext4_group_t block_group;                  // inode 所在的块组号
    unsigned long bit;                         // 在组内的 inode 位偏移
    struct ext4_group_desc *gdp;               // 组描述符指针
    struct ext4_super_block *es;               // 超级块信息
    struct ext4_sb_info *sbi;                  // ext4 私有超级块信息
    int fatal = 0, err, count, cleared;        // 一些临时变量记录错误码、统计等
    struct ext4_group_info *grp;               // 保存组的额外信息结构

    // 保护: 若 sb 不存在,或者 inode 仍有引用计数/硬链接计数 > 0,则不该进入这里
    if (!sb) {
        printk(KERN_ERR "EXT4-fs: %s:%d: inode on nonexistent device\n",
               __func__, __LINE__);
        return;
    }
    if (atomic_read(&inode->i_count) > 1) {
        ext4_msg(sb, KERN_ERR, "%s:%d: inode #%lu: count=%d",
                 __func__, __LINE__, inode->i_ino,
                 atomic_read(&inode->i_count));
        return;
    }
    if (inode->i_nlink) {
        ext4_msg(sb, KERN_ERR, "%s:%d: inode #%lu: nlink=%d\n",
                 __func__, __LINE__, inode->i_ino, inode->i_nlink);
        return;
    }

    sbi = EXT4_SB(sb);
    ino = inode->i_ino;
    ext4_debug("freeing inode %lu\n", ino);
    trace_ext4_free_inode(inode);

    /*
     * 首先初始化并释放配额的引用计数等,防止后续回收时配额系统出现不一致。
     */
    dquot_initialize(inode);
    dquot_free_inode(inode);

    is_directory = S_ISDIR(inode->i_mode);

    /*
     * 关键:**先**调用 ext4_clear_inode(inode),保证内存态 inode 不再使用/关联任何缓冲,
     * 以防之后同一 inode 号被再次分配时在内存中形成"别名"冲突。
     */
    ext4_clear_inode(inode);

    es = sbi->s_es;
    // 检查 inode 编号是否有效
    if (ino < EXT4_FIRST_INO(sb) || ino > le32_to_cpu(es->s_inodes_count)) {
        ext4_error(sb, "reserved or nonexistent inode %lu", ino);
        goto error_return;
    }

    // 计算该 inode 所在的块组 (block_group) 及其在组内的下标 (bit)
    block_group = (ino - 1) / EXT4_INODES_PER_GROUP(sb);
    bit = (ino - 1) % EXT4_INODES_PER_GROUP(sb);

    // 读取该块组的 inode bitmap,如果bitmap损坏则错误返回
    bitmap_bh = ext4_read_inode_bitmap(sb, block_group);
    if (IS_ERR(bitmap_bh)) {
        fatal = PTR_ERR(bitmap_bh);
        bitmap_bh = NULL;
        goto error_return;
    }

    // 如果不是快速回放模式,再检查该组 bitmap 是否可用
    if (!(sbi->s_mount_state & EXT4_FC_REPLAY)) {
        grp = ext4_get_group_info(sb, block_group);
        if (unlikely(EXT4_MB_GRP_IBITMAP_CORRUPT(grp))) {
            fatal = -EFSCORRUPTED;
            goto error_return;
        }
    }

    /*
     * 获取对 inode bitmap 的写访问权限,以便更新位图
     */
    BUFFER_TRACE(bitmap_bh, "get_write_access");
    fatal = ext4_journal_get_write_access(handle, sb, bitmap_bh,
                                          EXT4_JTR_NONE);
    if (fatal)
        goto error_return;

    /*
     * 同时获取并锁定 group descriptor,以便更新 free_inodes_count 等字段
     */
    fatal = -ESRCH;
    gdp = ext4_get_group_desc(sb, block_group, &bh2);
    if (gdp) {
        BUFFER_TRACE(bh2, "get_write_access");
        fatal = ext4_journal_get_write_access(handle, sb, bh2,
                                              EXT4_JTR_NONE);
    }

    // 在更新前先加锁 group
    ext4_lock_group(sb, block_group);
    // cleared=1 表示我们成功地从 bitmap 中清除了该 inode 的 bit
    cleared = ext4_test_and_clear_bit(bit, bitmap_bh->b_data);
    if (fatal || !cleared) {
        // 如果写访问出错或位图本来就已清除,则直接退出
        ext4_unlock_group(sb, block_group);
        goto out;
    }

    // 更新该组的 free_inodes_count
    count = ext4_free_inodes_count(sb, gdp) + 1;
    ext4_free_inodes_set(sb, gdp, count);

    // 如果是目录 inode,还要更新 used_dirs_count
    if (is_directory) {
        count = ext4_used_dirs_count(sb, gdp) - 1;
        ext4_used_dirs_set(sb, gdp, count);
        if (percpu_counter_initialized(&sbi->s_dirs_counter))
            percpu_counter_dec(&sbi->s_dirs_counter);
    }

    // 更新 inode bitmap 校验和 / group desc 校验和
    ext4_inode_bitmap_csum_set(sb, block_group, gdp, bitmap_bh,
                               EXT4_INODES_PER_GROUP(sb) / 8);
    ext4_group_desc_csum_set(sb, block_group, gdp);

    // 解锁 group
    ext4_unlock_group(sb, block_group);

    // s_freeinodes_counter++,表明系统内空闲 inode 数量增加
    if (percpu_counter_initialized(&sbi->s_freeinodes_counter))
        percpu_counter_inc(&sbi->s_freeinodes_counter);

    // 如果是 flex_bg 模式,还要增加对应 flex_group 的 free_inodes
    if (sbi->s_log_groups_per_flex) {
        struct flex_groups *fg;
        fg = sbi_array_rcu_deref(sbi, s_flex_groups,
                                 ext4_flex_group(sbi, block_group));
        atomic_inc(&fg->free_inodes);
        if (is_directory)
            atomic_dec(&fg->used_dirs);
    }

    /*
     * 将 group_desc buffer 标记脏
     */
    BUFFER_TRACE(bh2, "call ext4_handle_dirty_metadata");
    fatal = ext4_handle_dirty_metadata(handle, NULL, bh2);

out:
    // 如果成功清除该 inode bit,就需要将 bitmap 也标记为 dirty
    if (cleared) {
        BUFFER_TRACE(bitmap_bh, "call ext4_handle_dirty_metadata");
        err = ext4_handle_dirty_metadata(handle, NULL, bitmap_bh);
        if (!fatal)
            fatal = err;
    } else {
        // 如果 cleared=0,说明位已经被清除过,可能存在位图损坏
        ext4_error(sb, "bit already cleared for inode %lu", ino);
        ext4_mark_group_bitmap_corrupted(sb, block_group,
                                         EXT4_GROUP_INFO_IBITMAP_CORRUPT);
    }

error_return:
    // 释放 bitmap buffer
    brelse(bitmap_bh);
    // 用 ext4_std_error 最终处理可能出现的 fatal 错误
    ext4_std_error(sb, fatal);
}

执行流程概览:

  1. 安全检查:确认 sb 存在、i_count==1i_nlink==0
  2. 释放配额引用:调用 dquot_initialize / dquot_free_inode
  3. 调用 ext4_clear_inode(inode):先清除内存态的 inode 信息;
  4. 确定所在块组,读取 inode bitmap;
  5. 清除 bitmap 中对应的位,把 inode 计为"已空闲"**;更新该组的 free_inodes_count
  6. 若是目录,还要更新 used_dirs_count
  7. 标记 bitmap block、group descriptor block为脏并提交日志,增加全局计数 s_freeinodes_counter`;
  8. 出错时仅进行基本处理并返回。

这就是最终清理inode的代码了,不难看出,同样只修改了inode的位图表示该inode是空闲的,并未实际的将磁盘中的inode清除掉!!!

至此,艺术已成,我们理清了在ext4这层是如何删除一个文件的,同时也梳理到了清理这些结构的关键细节!!!

源码分析到此结束,在章节2中会有一张大图用于概括上述的核心流程,在总结中会总结关键细节及设计的优雅之处。


2.文件删除一览流程图

核心流程图如下所示:(这里未包括实际的数据块清理过程,因为数据块的实际清理伴随着inode的变化会同步进行)

  1. 用户层发起 unlink(path)
    • 用户代码调用 C 库的 unlink() 系统调用,进入内核后在 VFS 层对应为 vfs_unlink()
    2. VFS 层解析路径并调用 ext4_unlink()
    • 找到目标文件的 dentry 和 inode 后,VFS 调 inode->i_op->unlink(dentry)。对 ext4 来说即 ext4_unlink()
    ext4_unlink() 内部通过 __ext4_unlink() 删除目录项、更新目标 inode 的 i_nlink--,并将 inode 加入 orphan 列表或其它逻辑处理。
  2. drop_nlink() 使 i_nlink 减 1
    • 若 i_nlink 减为 0,则将 inode 视为无硬链接存在,即可以真正回收。
  3. VFS 回收 inode:iput()
    • 当内核引用计数 i_count 也降为 0 时,VFS 调 iput(inode) iput_final(inode)evict(inode),准备彻底清理 inode。
  4. evict() 回调到 ext4_evict_inode()
    • 在evict(inode)中,如超级块操作 s_op->evict_inode 存在,则调用 ext4_evict_inode()
    • 在 ext4_evict_inode() 中执行最后的块截断、删除 xattr、从 orphan 列表移除,然后调用 ext4_free_inode()
  5. ext4_free_inode() 释放 inode
    • 先调用 ext4_clear_inode(),清理内存态信息(预分配、buffer、加密等)。
    • 后对 inode bitmap 清除对应位、更新组描述符 free_inodes_count 等,从磁盘结构视角把该 inode 号标记为可再次分配。
  6. ext4_clear_inode()
    • 主要做内存级别的擦除,不直接更新磁盘位图,但保证不会再使用该 inode 的内存缓存或 journaling 记录。

通过上述核心交互和关键步骤,ext4 文件删除操作得以完成,从用户发起删除到 inode 占用块被释放并可再次分配。

3.总结

先基于上面的源码分析,再次回答我们最开始提出的三个问题。

1. 释放inode和目录项,是否清空了这里面的数据?

在 ext4 中,清除目录项的操作,会按照以下逻辑处理:

  • 若待删除的目录项能和之前的目录项进行合并,合并成更大的块,那当前目录项的内容会全部清除掉。
  • 若待删除的目录项不能和之前的目录项合并,则只会清除和inode的关联以及目录项的大小,其余内容不会被清除掉。

在 ext4 中,释放 inode 的操作,并不会物理清零其数据内容;系统只会从目录结构和位图/元数据上将该 inode 标记为"未使用",在内存态也会通过 ext4_clear_inode() 等函数清除和失效缓存。原有的数据块内容在磁盘上并不会被自动覆写。

2. 这些操作什么时候会同步到磁盘上?

ext4 采用 journaling 机制,文件删除和 inode 释放等操作先记录在日志中;在事务提交(commit)或同步写回(如 sync、fsync、挂载选项 sync)时,会将更新同步到磁盘。

3. 释放文件所占用的块后,这些块会被清空吗?

不会自动清零;在 ext4 中,释放块仅在位图中标记为"可用"。物理数据仍然存在,直至新写入覆盖它或使用其他机制(如 discard、安全擦除工具)进行实际清除。

然后简单的列举一些EXT4中关于文件删除设计精巧的地方:

  1. Orphan 列表保障一致性
    • 巧思:将 i_nlink = 0 但仍被打开(或尚未彻底删除)的 inode 放入 Orphan 列表,以防在系统崩溃或断电后出现无主数据块。
    • 精妙之处:即使删除操作未完成,Orphan 列表可在恢复时识别并继续处置这些 inode,防止数据泄漏与资源泄漏。
  2. Journaling 与多阶段删除
    • 巧思:将目录项删除、inode 释放、块回收分为多个阶段,各自加入日志事务,一次提交或多次提交都保持一致性。
    • 精妙之处:减少"原子大操作"的复杂度,保证每个步骤在日志中可回放或中止,崩溃后能精准定位到尚未完成的操作。
  3. ext4_clear_inode() 先行策略
    • 巧思:在释放 inode 前优先调用 ext4_clear_inode,将 inode 的内存态(buffer、预分配信息、加密态信息)彻底清理。
    • 精妙之处:避免新分配的 inode 与旧 inode 在内存中"别名"冲突,保证同一 inode 号不会重复使用缓存,提升安全与稳定性。
  4. 有序数据与同步机制
    • 巧思:针对不同挂载模式(data=ordered / journal / writeback),搭配 sync、dirsync 等挂载选项,灵活决定何时把删除操作同步到磁盘。
    • 精妙之处:在性能与可靠性间做出平衡,让用户可自主选择是高实时性的同步删除,还是高吞吐的延迟提交。
  5. 快速符号链接数据的特殊处理
    • 巧思:对 inode 里的 i_data 存储软链接的情形,删除前先清空该部分,避免处理为正常数据块而导致不必要的步骤。
    • 精妙之处:减少对符号链接和普通文件公用逻辑的冲突,提高实现的简洁度。
  6. 多级安全校验(bitmap校验、group校验、CSUM)
    • 巧思:删除时会反复验证组描述符及 inode bitmap 的校验和,若出现异常立刻标识为"损坏"并拒绝继续操作。
    • 精妙之处:从源头避免写入错误的位图或组信息导致文件系统再分配失误,体现出防御式编程与冗余校验的可靠性思路。
  7. 延迟清零的数据块
    • 巧思:仅在位图和元数据中将块标记为可用,并非物理清除;有需要可选择 discard 或定期运行 fstrim 等操作。
    • 精妙之处:将"安全彻底擦除"的问题独立出去,不强行拉低删除性能;用户可灵活决定是否进行物理级别的安全擦除。

4.参考

内核源代码:


ATFWUS 2025-01-05

相关推荐
南郁4 天前
001-监控你的文件-FSWatch-C++开源库108杰
c++·开源·文件系统·文件监控·fswatch·文件变动信息·libfswatch
【 STM32开发 】12 天前
【CubeMX+STM32】SD卡 文件系统读写 FatFs+SDIO+DMA
stm32·文件系统·sd·fatfs·sdio
HIT_Weston1 个月前
32、【OS】【Nuttx】OSTest分析(1):stdio测试(二)
文件系统·os·nuttx
刘争Stanley1 个月前
Android系统开发(六):从Linux到Android:模块化开发,GKI内核的硬核科普
android·linux·运维·内核·镜像·gki·kmi
一只搬砖的猹1 个月前
小米vela系统(基于开源nuttx内核)——如何使用信号量进行PV操作
嵌入式硬件·内核·小米·rtos·信号量·线程通信·vela系统
极客代码1 个月前
【Linux】设备驱动中的ioctl详解
linux·内核·驱动·设备驱动·iocto
令狐掌门2 个月前
linux ext4文件系统
linux·ext4
看星猩的柴狗2 个月前
GhostRace: Exploiting and Mitigating Speculative Race Conditions-记录
内核
极客先躯2 个月前
mysql 架构详解
数据库·mysql·架构·文件系统·半同步复制·高可用·主从复制