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篇文章,在前面的几篇文章中,我们研究了文件的创建、文件的写入,今天主要分析文件的删除,当我们删除了一个文件,底层到底发生了什么样的事情,让我们一起分析源码来看看吧!
在开始前,我们可以利用现有的知识(前面几篇文章的内容)猜测一下文件删除的主要逻辑:
- 释放相关inode,从目录中移除。
- 释放文件所占用的块。
大致思路肯定跑不开这几步,但是有一些细节值得我们去源码中寻找:
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
:

接下来开始进行源码的分析。
1.1 入口函数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; // 返回操作结果
}
主要处理流程解释:
- 初始化和变量定义:
• 函数开始时,将返回值retval初始化为-ENOENT,表示默认情况下文件未找到。
• 定义了指向buffer_head
和ext4_dir_entry_2
结构的指针,用于存储目录块和目录项的信息。
• 定义了一个标志位skip_remove_dentry
,用于决定是否跳过删除目录项的操作。 - 查找目录项:
• 调用ext4_find_entry
函数在指定的目录dir中查找名称为d_name的目录项。
• 如果查找过程中发生错误(即bh为错误指针),函数立即返回相应的错误码。
• 如果未找到目录项(bh为NULL),函数返回-ENOENT错误,表示文件未找到。 - 验证目录项的inode:
• 检查找到的目录项的inode是否与目标inode(即要删除的文件的inode)相同。
• 如果不同,可能是因为该目录项已被重命名为其他inode。在这种情况下:
• 如果文件系统处于恢复模式(EXT4_FC_REPLAY),则设置skip_remove_dentry
标志为1,跳过删除目录项。
• 否则,跳转到函数结束部分,返回错误。 - 同步处理:
• 如果目录设置了同步标志(IS_DIRSYNC(dir)),调用ext4_handle_sync
函数进行同步处理,确保删除操作的同步性。 - 删除目录项:
• 如果不需要跳过删除目录项,调用ext4_delete_entry
函数删除目录项。
• 如果删除操作失败,跳转到函数结束部分,返回错误。
• 删除成功后,更新目录的修改时间和状态更改时间为当前时间。
• 调用ext4_update_dx_flag
函数更新目录的htree标志,反映目录结构的变化。
• 调用ext4_mark_inode_dirty
函数标记目录的inode为脏状态,表示需要将其写回磁盘。
• 如果标记操作失败,跳转到函数结束部分,返回错误。 - 处理目标inode的链接计数:
• 检查目标inode的链接计数i_nlink是否为0:
• 如果为0,调用ext4_warning_inode
函数发出警告,提示正在删除一个没有链接的文件。
• 否则,调用drop_nlink
函数减少inode的链接计数,表示有一个硬链接被删除。 - 处理orphan列表:
• 如果目标inode的链接计数降为0,调用ext4_orphan_add
函数将其添加到orphan列表中,等待文件系统在后续操作中回收其数据块和inode。 - 更新inode的时间戳和标记为脏:
• 更新目标inode的状态更改时间i_ctime为当前时间。
• 调用ext4_mark_inode_dirty
函数标记inode为脏状态,表示需要将其写回磁盘。 - 清理和返回:
• 在函数结束部分,调用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
函数批量读取多个块,提升搜索效率。
- 填充预读缓冲区:如果预读指针 ra_ptr 超过预读最大值 ra_max,则调用
- 遍历目录块:
- 获取当前块的
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 的具体处理流程:
- 提交日志事务(Commit Journal Transaction):
•ext4_handle_syn
会触发日志系统提交当前的日志事务。它确保所有在当前事务中记录的变更(如目录项的删除、文件的重命名等)被写入到日志中。 - 等待日志写入完成:
• 提交事务后,ext4_handle_sync
会等待日志数据被实际写入到磁盘。这通常涉及调用底层的块设备驱动程序,确保数据的物理写入。 - 同步文件系统状态:
• 在日志提交并写入完成后,ext4_handle_sync
还会确保文件系统的状态(如超级块的更新)也被同步到磁盘。这进一步确保了文件系统在同步操作完成后处于一致状态。 - 错误处理:
• 如果在同步过程中发生错误(如磁盘故障、I/O 错误等),ext4_handle_sync
会返回相应的错误代码,允许调用者处理这些异常情况。 - 性能影响:
• 由于需要等待数据实际写入磁盘,同步操作通常比异步操作耗时更长。因此,尽管同步操作提供了更高的数据一致性,但在性能敏感的场景下需要谨慎使用。
1.3.4 若不是同步
如果操作不是同步的,则文件系统会采用异步方式处理这些操作。具体流程如下:
- 内存中的变更:
• 文件操作首先在内存中的文件系统结构(如 inode、目录项等)进行修改。 - 日志记录(Journaling):
• 变更会被记录到日志(journal)中,但不会立即写入磁盘。日志系统会在后台批量处理这些记录,提高效率。 - 后台写回(Background Writeback):
• 通过后台线程或定时任务,文件系统会将日志中的变更异步地写入磁盘。这意味着操作在返回用户空间之前,数据可能仍然在内存中,尚未持久化。(这里暂不探讨日志的定时策略) - 延迟一致性:
• 虽然异步操作提高了性能,但在系统崩溃或断电的情况下,未写入磁盘的变更可能会丢失。因此,异步操作适用于对性能要求高且可以容忍短暂数据不一致的场景。
在这里我们就可以回答我们最开始的第二个疑问了。
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
函数负责删除一个指定的目录项。其主要流程如下:
- 检查内联数据:
• 目录可能包含内联数据(即目录项直接存储在 inode 中,而不是独立的块)。如果目录具有内联数据,首先尝试在内联数据中删除目标目录项。
• 调用ext4_delete_inline_entry
函数进行删除。如果删除成功(即目录项在内联数据中被删除),函数直接返回删除结果。 - 设置校验和大小:
• 如果文件系统启用了元数据校验和(metadata checksum),则设置sum_size
为目录项尾部校验和结构的大小。 - 获取写访问权限:
• 为了修改目录项,需要对目录缓冲区获取写访问权限。调用ext4_journal_get_write_access
函数,确保可以安全地修改目录缓冲区,并将修改记录到日志中(Journaling)。 - 调用通用删除函数:
• 调用ext4_generic_delete_entry
函数,实际执行目录项的删除操作。该函数会在目录缓冲区中找到并删除指定的目录项。 - 标记目录缓冲区为已修改:
• 调用ext4_handle_dirty_dirblock
函数,将已修改的目录缓冲区标记为脏数据,并将其写入日志中,以确保文件系统的一致性和可靠性。 - 错误处理:
• 如果在上述任何步骤中发生错误,函数会记录标准错误信息,并返回相应的错误码。
其中通用目录删除函数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
是一个通用函数,用于在目录缓冲区中删除指定的目录项。其主要流程如下:
- 初始化变量:
• 初始化当前目录项指针de
为目录缓冲区的起始位置,前一个目录项指针pde
为 NULL,计数器 i 为0。 - 遍历目录缓冲区中的目录项:
• 循环遍历目录缓冲区中的每个目录项,直到遍历完整个缓冲区或找到待删除的目录项。
• 在每次循环中,首先检查当前目录项的有效性,确保其结构和数据正确。 - 查找待删除的目录项:
• 如果当前目录项是待删除的目录项(即de == de_del
),则执行删除操作:
• 有前一个目录项:如果存在前一个目录项pde
,将前一个目录项的rec_len
(记录长度)与当前目录项的rec_len
合并,形成一个较大的连续空间。清除待删除目录项的数据,仅保留rec_len
字段。
• 无前一个目录项:如果不存在前一个目录项,直接清除当前目录项的inode
和name_len
字段,标记为无效。增加目录的版本号,标记目录已被修改。
• 返回成功(0)。 - 继续遍历:
• 如果当前目录项不是待删除的目录项,则更新前一个目录项指针 pde 为当前目录项,并移动到下一个目录项。 - 未找到目录项:
• 如果遍历完整个目录缓冲区后仍未找到待删除的目录项,返回 -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 列表的使用主要发生在以下两种情况下:
- 正常删除文件时:
• 当文件的链接计数降为零,但文件仍被进程打开时,文件的 inode 会被添加到 Orphan 列表中。
• 这确保在所有引用释放后,文件系统能够自动回收这些 inode。 - 系统崩溃或异常关闭时:
• 在系统异常关闭后,文件系统恢复过程中会检查 Orphan 列表,处理那些未被正确回收的 inode。
• 通过遍历 Orphan 列表,删除未链接的 inode 或截断相关文件,恢复文件系统的一致性。
当inode被加入orphan列表后,相关的状态变化如下:
- 当一个 inode 被加入到 Orphan 列表后,表示该 inode 已被删除(链接计数降为零),但由于文件仍被打开或其他原因,尚未被完全回收。此时,inode 处于待回收状态。
- 当所有引用该 inode 的文件描述符被关闭后,文件系统会检测到 inode 的引用计数已降为零。文件系统的回收机制会自动调用清理函数,释放 inode 和相关的数据块。
- 在系统崩溃或异常关闭后,重新挂载文件系统时,文件系统会调用
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
函数执行流程:
- 初始检查:
- 获取超级块
sb
和 ext4 超级块信息sbi
。 - 检查文件系统是否启用了日志(journal)以及 inode 是否已损坏。
- 如果未启用日志或 inode 已损坏,直接返回,无需处理孤立。
2.状态验证:
- 通过 WARN_ON_ONCE 宏确保 inode 处于新建 (I_NEW) 或释放 (I_FREEING) 状态,且已经被上锁。
- 这确保了在操作 inode 时不会发生竞争条件。
- 检查是否已在孤立列表中:
- 如果 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
函数执行流程如下:
- 初始检查:
• 检查超级块中的s_last_orphan
是否存在,或者孤立文件(orphan file)中是否有孤立 inode。
• 如果没有孤立 inode,打印调试信息并返回,无需清理。 - 文件系统状态检查:
• 如果文件系统以只读模式挂载或存在错误(EXT4_ERROR_FS),跳过 Orphan 清理。
• 确保文件系统挂载为读写模式,以便进行清理操作。 - 配额处理(可选):
• 如果文件系统启用了配额特性,在清理前确保配额正确启用,以便正确更新配额信息。 - 遍历超级块孤立列表:
•s_last_orphan
指向第一个孤立 inode 的 inode 号。
• 使用ext4_orphan_ge
t 获取该 inode 的 inode 结构。
• 将 inode 添加到内存中的孤立列表sbi->s_orphan
。
• 调用xt4_process_orphan
函数,执行截断或删除操作。 - 遍历孤立文件中的所有孤立 inode:
• 孤立文件(orphan file)中记录了更多的孤立 inode。
• 遍历每个孤立块,获取其中的 inode 号。
• 将每个 inode 标记为孤立文件状态,并调用ext4_process_orphan
进行处理。 - 处理截断和删除:
• 在ext4_process_orphan
函数中:
• 截断操作:对于仍有数据块的文件,调用ext4_truncate
截断文件数据,并删除 inode。
• 删除操作:对于已完全删除的文件,直接删除 inode。 - 统计和日志:
• 记录清理过程中删除的 Orphan inode 数量和截断的文件数量。
• 输出相关日志信息,供系统管理员参考。 - 配额关闭(可选):
• 如果启用了配额并进行了更新,则在清理完成后关闭配额。 - 恢复文件系统状态:
• 恢复文件系统的只读挂载状态(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);
}
我们仔细看一下这个函数的执行流程:
- 合法性检查
• 如果没有指定 EXT4_FREE_BLOCKS_VALIDATED,则调用ext4_inode_block_valid()
再次验证物理块范围合法性。 - 处理 metadata forget
• 若需要对 metadata 块执行"忘却(forget)",调用 ext4_forget 使其从 buffer/page cache 中移除。 - 对齐集群模式
• 若启用了大于 1 的 cluster ratio,需要考虑部分集群的首末保留或整 cluster 释放。 - 正式释放到位图
• 最终调用ext4_mb_clear_bb()
清空块位图并归还到 free block pool,同时更新配额信息(如果启用了配额)。
首先解答一下这里的metadata为什么要进行forget操作。
当我们在执行 ext4_free_blocks()
时,如果指定了标志 EXT4_FREE_BLOCKS_FORGET 且块属于metadata 类型(比如它存储的是索引块、目录块等),就会调用内核函数 ext4_forget()
来 "遗忘" 这些块。这通常意味着:
- 从当前的页缓存 / buffer cache 中清除:
• 将对应的 buffer_head 失效或丢弃,防止后续再访问这个块时还以为它在使用中。 - 解除与 inode(或其他结构)的映射关系:
• 让内核不再将其视为正在使用的元数据块。 - 在日志(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);
}
从源码中可以发现大致流程:
i_nlink!=0
:仅截断 page cache,并 不 做真正删除;- 引用计数=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);
}
执行流程概览:
- 安全检查:确认
sb
存在、i_count==1
、i_nlink==0
; - 释放配额引用:调用
dquot_initialize
/dquot_free_inode
; - 调用
ext4_clear_inode(inode)
:先清除内存态的 inode 信息; - 确定所在块组,读取 inode bitmap;
- 清除 bitmap 中对应的位,把 inode 计为"已空闲"**;更新该组的
free_inodes_count
; - 若是目录,还要更新
used_dirs_count
; - 标记 bitmap block、group descriptor block
为脏并提交日志,增加全局计数
s_freeinodes_counter`; - 出错时仅进行基本处理并返回。
这就是最终清理inode
的代码了,不难看出,同样只修改了inode的位图表示该inode是空闲的,并未实际的将磁盘中的inode清除掉!!!
至此,艺术已成,我们理清了在ext4这层是如何删除一个文件的,同时也梳理到了清理这些结构的关键细节!!!
源码分析到此结束,在章节2中会有一张大图用于概括上述的核心流程,在总结中会总结关键细节及设计的优雅之处。
2.文件删除一览流程图
核心流程图如下所示:(这里未包括实际的数据块清理过程,因为数据块的实际清理伴随着inode的变化会同步进行)
- 用户层发起
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 列表或其它逻辑处理。 drop_nlink()
使 i_nlink 减 1
• 若i_nlink
减为 0,则将 inode 视为无硬链接存在,即可以真正回收。- VFS 回收 inode:
iput()
• 当内核引用计数 i_count 也降为 0 时,VFS 调iput(inode)
→iput_final(inode)
→evict(inode)
,准备彻底清理 inode。 - evict() 回调到
ext4_evict_inode()
• 在evict(inode)
中,如超级块操作s_op->evict_inode
存在,则调用ext4_evict_inode()
。
• 在ext4_evict_inode()
中执行最后的块截断、删除 xattr、从 orphan 列表移除,然后调用ext4_free_inode()
。 ext4_free_inode()
释放 inode
• 先调用ext4_clear_inode()
,清理内存态信息(预分配、buffer、加密等)。
• 后对 inode bitmap 清除对应位、更新组描述符free_inodes_count
等,从磁盘结构视角把该 inode 号标记为可再次分配。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中关于文件删除设计精巧的地方:
- Orphan 列表保障一致性
• 巧思:将i_nlink = 0
但仍被打开(或尚未彻底删除)的 inode 放入 Orphan 列表,以防在系统崩溃或断电后出现无主数据块。
• 精妙之处:即使删除操作未完成,Orphan 列表可在恢复时识别并继续处置这些 inode,防止数据泄漏与资源泄漏。 - Journaling 与多阶段删除
• 巧思:将目录项删除、inode 释放、块回收分为多个阶段,各自加入日志事务,一次提交或多次提交都保持一致性。
• 精妙之处:减少"原子大操作"的复杂度,保证每个步骤在日志中可回放或中止,崩溃后能精准定位到尚未完成的操作。 ext4_clear_inode()
先行策略
• 巧思:在释放 inode 前优先调用ext4_clear_inode
,将 inode 的内存态(buffer、预分配信息、加密态信息)彻底清理。
• 精妙之处:避免新分配的 inode 与旧 inode 在内存中"别名"冲突,保证同一 inode 号不会重复使用缓存,提升安全与稳定性。- 有序数据与同步机制
• 巧思:针对不同挂载模式(data=ordered / journal / writeback),搭配 sync、dirsync 等挂载选项,灵活决定何时把删除操作同步到磁盘。
• 精妙之处:在性能与可靠性间做出平衡,让用户可自主选择是高实时性的同步删除,还是高吞吐的延迟提交。 - 快速符号链接数据的特殊处理
• 巧思:对 inode 里的 i_data 存储软链接的情形,删除前先清空该部分,避免处理为正常数据块而导致不必要的步骤。
• 精妙之处:减少对符号链接和普通文件公用逻辑的冲突,提高实现的简洁度。 - 多级安全校验(bitmap校验、group校验、CSUM)
• 巧思:删除时会反复验证组描述符及 inode bitmap 的校验和,若出现异常立刻标识为"损坏"并拒绝继续操作。
• 精妙之处:从源头避免写入错误的位图或组信息导致文件系统再分配失误,体现出防御式编程与冗余校验的可靠性思路。 - 延迟清零的数据块
• 巧思:仅在位图和元数据中将块标记为可用,并非物理清除;有需要可选择 discard 或定期运行 fstrim 等操作。
• 精妙之处:将"安全彻底擦除"的问题独立出去,不强行拉低删除性能;用户可灵活决定是否进行物理级别的安全擦除。
4.参考
内核源代码:
- https://github.com/torvalds/linux/blob/v5.19/fs/ext4/namei.c
- https://github.com/torvalds/linux/blob/v5.19/fs/ext4/inode.c
- https://github.com/torvalds/linux/blob/v5.19/fs/ext4/super.c
- https://github.com/torvalds/linux/blob/v5.19/fs/ext4/mballoc.c
- https://github.com/torvalds/linux/blob/v5.19/fs/ext4/mballoc.c
- https://github.com/torvalds/linux/blob/v5.19/fs/ext4/ext4.h
- https://github.com/torvalds/linux/blob/v5.19/fs/ext4/extents.c
- https://github.com/torvalds/linux/blob/v5.19/fs/ext4/ialloc.c
- https://github.com/torvalds/linux/blob/v5.19/fs/inode.c
ATFWUS 2025-01-05