做视频网站需要什么证书,上海做公司网站,小清新个人网站,北京市昌平网站建设EXT4源码分析之“文件删除”原理【七万字超长合并版】#xff08;源码关键细节分析#xff09;#xff0c;详细的跟踪了ext4文件删除的核心调用链#xff0c;分析关键函数的细节#xff0c;解答了开篇中提出的三个核心疑问。 文章目录 提示前言全文重点索引1.源码解析1.1 … EXT4源码分析之“文件删除”原理【七万字超长合并版】源码关键细节分析详细的跟踪了ext4文件删除的核心调用链分析关键函数的细节解答了开篇中提出的三个核心疑问。 文章目录 提示前言全文重点索引1.源码解析1.1 入口函数ext4_unlink1.2 找到待删除目录项ext4_find_entry1.2.1 主体流程1.2.2 目录内联1.2.3 htree索引 1.3 同步标志1.3.1 判断同步标志1.3.2 设置同步标志1.3.3 若是同步1.3.4 若不是同步 1.4 删除目录项ext4_delete_entry1.4.1 主体流程1.4.2 关键细节 1.5 释放inode1.5.1 为什么只减少引用1.5.2 Orphan机制介绍1.5.3 添加至孤立列表 ext4_orphan_add源码分析1.5.4 实际清除 ext4_orphan_cleanup源码分析1.5.5 inode截断 ext4_truncate源码分析块释放核心点1.5.5.1 extent模式下的截断 ext4_ext_truncate源码分析1.5.5.2 核心块释放点 ext4_free_blocks源码分析1.5.5.3 间接索引模式下的截断 ext4_ind_truncate源码分析 1.5.6 inode删除 iput源码分析1.5.6.1 fs层通用逻辑1.5.6.2 EXT4中的evict_inode源码分析 2.文件删除一览流程图3.总结4.参考 提示
本文是超长合并版全文7.2万字左右包括源码分析请参照索引按需阅读或阅读小节版本
前言
这是系列的第4篇文章在前面的几篇文章中我们研究了文件的创建、文件的写入今天主要分析文件的删除当我们删除了一个文件底层到底发生了什么样的事情让我们一起分析源码来看看吧
在开始前我们可以利用现有的知识前面几篇文章的内容猜测一下文件删除的主要逻辑
释放相关inode从目录中移除。释放文件所占用的块。
大致思路肯定跑不开这几步但是有一些细节值得我们去源码中寻找
1. 释放inode和目录项是否清空了这里面的数据 2. 这些操作什么时候会同步到磁盘上 3. 释放文件所占用的块后这些块会被清空吗
让我们带着疑问去源码中一探究竟吧
注这篇文章我们换个思路先去研究源码再探讨里面的细节最后画出文件删除的整体流程图。
全文重点索引
由于本文从入口开始跟踪源码并逐个分析函数所以内容非常多为了方便读者迅速从中得到重要信息做了这个重点索引。
章节1的整体流程是从入口函数开始一直跟踪到最终的删除逻辑。
序号内容关键函数细节1ext4中的删除入口函数ext4_unlink章节1.1在正式跟进关键删除流程前会分析一些主流程中的东西2找到待删除的目录项ext4_find_entry 章节1.2分析是如何查找目录项的3哈希索引ext4_dx_find_entry 章节1.2.3分析查找目录项时对哈希索引的使用4同步标志文字描述 章节1.3文字描述EXT4中是如何使用同步的解决问题25删除目录项ext4_generic_delete_entry 章节1.4分析了两个核心源码清除了如何是如何删除目录项的解决问题1的一半6inode添加到孤立列表ext4_orphan_add 章节1.5.3分析了孤立列表机制以及inode如何放入孤立列表的7inode对应数据块核心清理过程ext4_truncate 章节1.5.5深入了分析ext4是如何释放真正存储数据的块的解决问题38inode本身结构的清理过程ext4_evict_inode 章节1.5.6.2深入了分析ext4是如何释放真正存储数据的块的解决问题1的另一半
同时先把图放在这让读者有大致了解如果抱着分析的态度去看应该是分析完源码再看图。 1.源码解析
首先大胆猜测一下文件系统中文件删除的源码位于fs/ext4/namei.c中因为文件创建的源码位于这里大概率删除和创建是在一个地方至于链路到底是怎么样的我们后面再去探究。
找到关键函数ext4_unlink 接下来开始进行源码的分析。
1.1 入口函数ext4_unlink
/*** 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进行实际的删除操作看这个函数的源码
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; // 设置标志位跳过删除目录项elsegoto 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);elsedrop_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函数看它的源码
/** __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;elsera_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 工具查看文件系统特性
tune2fs -l /dev/sda | grep Filesystem features核心源代码如下这里就不做过多解释了。
/** 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_LEVELstruct 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_HTREEbh 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 为 NULLbh NULL;errout:// 输出调试信息表示未找到指定目录项dxtrace(printk(KERN_DEBUG %s not found\n, fname-usr_fname-name));success:// 释放 htree 帧数组中所有缓冲区头的资源dx_release(frames);// 返回找到的缓冲区头或 NULLreturn bh;
}如果没有索引就开始线性扫描通过批量读取多个块利用预读机制提升搜索效率同时还考虑到了扫描过程中目录增长的情况。
注意fname 仅仅是文件名本身不包含路径信息。在文件系统内部路径解析已经在更高层次如 VFS 层完成__ext4_find_entry 函数只负责在特定的目录 inode 中查找单个文件名对应的目录项。
查找目录项的细节就到此结束接着继续看主函数中的下一个细节。
1.3 同步标志
在 ext4 文件系统中同步标志sync flag用于控制文件操作是否需要同步地将数据和元数据写入磁盘。同步操作确保数据在操作完成后立即持久化提供更高的数据一致性和安全性特别是在系统崩溃或断电的情况下。
1.3.1 判断同步标志
同步标志主要通过 inode 的标志位来设置。对于目录directory inodeIS_DIRSYNC(dir) 宏用于检查该目录是否具有同步标志。这个宏通常会检查 inode 中的某个特定位例如 EXT4_SYNC_FL来确定是否需要同步操作。
#define IS_DIRSYNC(inode) (test_opt((inode)-i_sb, DIRSYNC))1.3.2 设置同步标志
设置同步标志的方式主要有以下几种
1.挂载选项Mount Options
在挂载文件系统时可以通过指定 dirsync 选项来默认为所有目录启用同步操作。例如
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 主体流程
再经过一系列前置检查后终于来到了关键的删除部分首先是删除目录项核心函数如下
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 dataif (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 checksumif (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;// 删除成功返回0return 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如下
/** 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);}// 如果未找到待删除的目录项返回-ENOENTreturn -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 中主要包含以下字段
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 字段以维护目录结构的完整性。此时相当于已经和前一个目录项合并了当前目录项的的数据完全被清空。
// 清除待删除目录项的数据仅保留上一个目录项中的rec_len字段
memset(de, 0, ext4_rec_len_from_disk(de-rec_len, blocksize));如果没有前一个目录项则直接清零当前目录项的 inode 和 name_len 字段保留了后面三个字段值。
// 如果没有前一个目录项直接清除当前目录项的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进行适当的调整。
/** 如果我们将来支持文件系统块大小大于页大小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则直接返回 blocksizeif (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函数如下
/*** 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源码分析
/** 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);}} elsebrelse(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函数的源码如下
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源码分析
/** 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_QUOTAint 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;elseext4_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;elseext4_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;}// 获取孤立 inodeinode 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);}// 遍历孤立文件中的所有孤立 inodefor (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);}}
#endifsb-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_get 获取该 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其源码如下
/*** 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_sizetruncate_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_nlink0 并且没有其他引用会触发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中其源码如下
/*** 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);elsecredits 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);elseext4_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_nlink0说明是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函数源码如下
/*** 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函数
/*** 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会先做必要的splitext4_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* end1之后的部分拆分成新的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 做spliterr 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_depth0、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源码分析
/*** 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;}// 获取当前要处理的 extentex path[depth].p_ext;if (!ex)ex EXT_LAST_EXTENT(eh); // 取叶子中的最后一个 extentex_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;elseunwritten 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但现在只剩下一部分块则继续标记它为 unwrittenif (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;// 移动到前一个 extentex--;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处理块释放。
/*** 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);elseblock 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;elsereturn;} 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;elsereturn;} elsecount sbi-s_cluster_ratio - overflow;}// 若需要对 [block, count] 区间的所有块执行forget操作则循环调用 ext4_forgetif (!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, blockcount-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函数
/*** 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; // 用于读取组内位图的bufferstruct 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 headext4_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 以及在组内的偏移 bitcluster 粒度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, blockcount-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));}
#endiftrace_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_metadataext4_mb_free_metadata(handle, e4b, new_entry);} else {/** 否则就是一般情况不需要延迟释放可立即更新位图和* 组描述符来释放块并允许以后立即再次分配。*/if (test_opt(sb, DISCARD)) {// 如果文件系统启用了discard选项尝试对这些块执行一次TRIMerr 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模式下的源码后这里已经没有分析的必要了因为截断/删除的核心逻辑我们已经找到了但还是简单看一下这个函数的流程如何。
/*** 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_blockext4 全局允许的最大块号受限于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) {/** 当 n1说明要截断的块位于 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, nr1,(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-p1 .. 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 indirectnr i_data[EXT4_IND_BLOCK];if (nr) {ext4_free_branches(handle, inode, NULL, nr, nr1, 1);i_data[EXT4_IND_BLOCK] 0;}fallthrough;case EXT4_IND_BLOCK:// 2) double indirectnr i_data[EXT4_DIND_BLOCK];if (nr) {ext4_free_branches(handle, inode, NULL, nr, nr1, 2);i_data[EXT4_DIND_BLOCK] 0;}fallthrough;case EXT4_DIND_BLOCK:// 3) triple indirectnr i_data[EXT4_TIND_BLOCK];if (nr) {ext4_free_branches(handle, inode, NULL, nr, nr1, 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函数源码如下
/*** 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_lockif (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_finaliput_final(inode);}
}
EXPORT_SYMBOL(iput);iput_final函数源码如下
/** iput_final - 当 inode 最后一次引用被释放时调用* inode: inode** 1. 调用 drop_inode / generic_drop_inode 以决定 inode 是否可以被真正释放。* 2. 若 drop0不删除且 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);elsedrop 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) {// 如果 drop0 但别的原因需要释放(例如系统正准备关机),// 先写回 inodeWRITE_ONCE(inode-i_state, state | I_WILL_FREE);spin_unlock(inode-i_lock);write_inode_now(inode, 1); // 强制写回 inodespin_lock(inode-i_lock);state inode-i_state;WARN_ON(state I_NEW);state ~I_WILL_FREE; // 清除 I_WILL_FREE 标志}// 最终将 inode 状态标记为 I_FREEINGWRITE_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函数源码如下
/*** 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_forgetif (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函数。 其源码如下
/*** ext4_evict_inode - 在最后一次 iput() 且 i_nlink0 时调用执行 ext4 中 inode 的释放* inode: 需要被回收的 inode** 该函数由 VFS 层的 evict() 回调调用。当一个 ext4 inode 的引用计数归零 (i_count0) 且链接计数 (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_size0 以便后续的 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,couldnt 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,couldnt 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 列表中删除该 inodeext4_orphan_del(handle, inode);// 将 dtime 设为当前时间EXT4_I(inode)-i_dtime (__u32)ktime_get_real_seconds();// 尝试再次将 inode 标记为脏用于更新 i_dtimeif (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_size0 → ext4_truncate(inode) 释放块。 • 删除 xattr移除 orphan 链表设置 dtime。 • 调用 ext4_free_inode把 inode 结构从磁盘结构中彻底释放。 • 如果出现错误则仅在内存层面执行 ext4_clear_inode。
ext4_free_inode函数源码如下
/*** 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_headstruct buffer_head *bh2; // 对应 group descriptor 的 buffer_headext4_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);}// 在更新前先加锁 groupext4_lock_group(sb, block_group);// cleared1 表示我们成功地从 bitmap 中清除了该 inode 的 bitcleared ext4_test_and_clear_bit(bit, bitmap_bh-b_data);if (fatal || !cleared) {// 如果写访问出错或位图本来就已清除则直接退出ext4_unlock_group(sb, block_group);goto out;}// 更新该组的 free_inodes_countcount ext4_free_inodes_count(sb, gdp) 1;ext4_free_inodes_set(sb, gdp, count);// 如果是目录 inode还要更新 used_dirs_countif (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);// 解锁 groupext4_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_inodesif (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 也标记为 dirtyif (cleared) {BUFFER_TRACE(bitmap_bh, call ext4_handle_dirty_metadata);err ext4_handle_dirty_metadata(handle, NULL, bitmap_bh);if (!fatal)fatal err;} else {// 如果 cleared0说明位已经被清除过可能存在位图损坏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 bufferbrelse(bitmap_bh);// 用 ext4_std_error 最终处理可能出现的 fatal 错误ext4_std_error(sb, fatal);
}执行流程概览
安全检查确认 sb 存在、i_count1、i_nlink0释放配额引用调用 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 回收 inodeiput() • 当内核引用计数 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 号不会重复使用缓存提升安全与稳定性。有序数据与同步机制 • 巧思针对不同挂载模式dataordered / 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.chttps://github.com/torvalds/linux/blob/v5.19/fs/ext4/inode.chttps://github.com/torvalds/linux/blob/v5.19/fs/ext4/super.chttps://github.com/torvalds/linux/blob/v5.19/fs/ext4/mballoc.chttps://github.com/torvalds/linux/blob/v5.19/fs/ext4/mballoc.chttps://github.com/torvalds/linux/blob/v5.19/fs/ext4/ext4.hhttps://github.com/torvalds/linux/blob/v5.19/fs/ext4/extents.chttps://github.com/torvalds/linux/blob/v5.19/fs/ext4/ialloc.chttps://github.com/torvalds/linux/blob/v5.19/fs/inode.c ATFWUS 2025-01-05