【linux内核分析-存储】EXT4源码分析之“文件删除”原理【七万字超长合并版】(源码+关键细节分析)
EXT4源码分析之“文件删除”原理【七万字超长合并版】(源码+关键细节分析),详细的跟踪了ext4文件删除的核心调用链,分析关键函数的细节,解答了开篇中提出的三个核心疑问。
文章目录
- 提示
- 前言
- 全文重点索引
- 1.源码解析
- 1.1 入口函数ext4_unlink
- 1.2 找到待删除目录项ext4_find_entry
- 1.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_entry
- 1.4.1 主体流程
- 1.4.2 关键细节
- 1.5 释放inode
- 1.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的整体流程是从入口函数开始,一直跟踪到最终的删除逻辑。
序号 | 内容 | 关键函数 | 细节 |
---|---|---|---|
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.源码解析
首先大胆猜测一下,文件系统中文件删除的源码位于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
函数批量读取多个块,提升搜索效率。
- 填充预读缓冲区:如果预读指针 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 工具查看文件系统特性:
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 inode),IS_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 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;// 删除成功,返回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_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
,其源码如下:
/*** 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_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
中,其源码如下:
/*** 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_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
函数源码如下:
/*** 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),会先做必要的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 做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_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源码分析
/*** 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, 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
函数:
/*** 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 以及在组内的偏移 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));}
#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_metadata)ext4_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_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 indirectnr = 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 indirectnr = 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 indirectnr = 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
函数源码如下:
/*** 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. 若 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);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) {// 如果 drop=0 但别的原因需要释放(例如系统正准备关机),// 先写回 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_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 列表中删除该 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_size=0 →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);// cleared=1 表示我们成功地从 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 {// 如果 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 bufferbrelse(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
相关文章:
【linux内核分析-存储】EXT4源码分析之“文件删除”原理【七万字超长合并版】(源码+关键细节分析)
EXT4源码分析之“文件删除”原理【七万字超长合并版】(源码关键细节分析),详细的跟踪了ext4文件删除的核心调用链,分析关键函数的细节,解答了开篇中提出的三个核心疑问。 文章目录 提示前言全文重点索引1.源码解析1.1 …...
一个在ios当中采用ObjectC和opencv来显示图片的实例
前言 在ios中采用ObjectC编程利用opencv来显示一张图片,并简单绘图。听上去似乎不难,但是实际操作下来,却不是非常的容易的。本文较为详细的描述了这个过程,供后续参考。 一、创建ios工程 1.1、选择ios工程类型 1.2、选择接口模…...
使用Python实现基于强化学习的游戏AI:打造智能化游戏体验
友友们好! 我的新专栏《Python进阶》正式启动啦!这是一个专为那些渴望提升Python技能的朋友们量身打造的专栏,无论你是已经有一定基础的开发者,还是希望深入挖掘Python潜力的爱好者,这里都将是你不可错过的宝藏。 在这个专栏中,你将会找到: ● 深入解析:每一篇文章都将…...
STM32G0B1 can Error_Handler 解决方法
问题现象 MCU上电,发送0x13帧数据固定进入 Error_Handler 硬件介绍 MCU :STM32G0B1 can:NSI1042 tx 接TX RX 接RX 折腾了一下午,无解,问题依旧; 对比测试 STM32G431 手头有块G431 官方评估版CAN 模块; 同样的…...
洛谷 P2511 [HAOI2008] 木棍分割
第一问很简单,第二问 d p dp dp。 (真是哪都能混个 d p dp dp) 参考题解 #include <bits/stdc.h>using namespace std;int read() {int x 0, f 1; char c getchar();while (c < 0 || c > 9) {if (c -) f -1; c getcha…...
二极管钳位电路分享
二极管钳位(I/O的过压/浪涌保护等) 如果我们的电路环境接收外部输入信号容易受到噪声影响,那我们必须采取过压和浪涌保护措施,其中一个方式就是二极管钳位保护。 像上图,从INPUT输入的电压被钳位在-Vf与VCCVf之间&…...
guestfish/libguestfs镜像管理工具简介
文章目录 简介guestfishlibguestfs项目 例子原理代码libguestfs架构参考 简介 guestfish Guestfish 是libguestfs项目中的一个工具软件,提供修改虚机镜像内部配置的功能。它不需要把虚机镜像挂接到本地,而是为你提供一个shell接口,你可以查…...
AutoSar架构学习笔记
1.AUTOSAR(Automotive Open System Architecture,汽车开放系统架构)是一个针对汽车行业的软件架构标准,旨在提升汽车电子系统的模块化、可扩展性、可重用性和互操作性。AUTOSAR的目标是为汽车电子控制单元(ECU…...
Scade pragma: separate_io
概述 在 Scade 语言中,支持对用户自定义算子使用 separate_io pragma 进行修饰。其形式如: function #pragma kcg separate_io #end N(x: int8) returns (y,z: int8) let y x;z x; tel在上例中,算子N 就被 pragma #pragma kcg separate_i…...
三天速成微服务
微服务技术栈 总结 微服务技术对比 技术栈 SpringCloud SpringCloud是目前国内使用最广泛的微服务框架。官网地址:https://spring.io/projects/spring-cloud Springboot和SpringCould兼容性 代码目录结构如下 用于远程调用Bean 代码 package cn.itcast.order.config;//import …...
【MySQL】九、表的内外连接
文章目录 前言Ⅰ. 内连接案例:显示SMITH的名字和部门名称 Ⅱ. 外连接1、左外连接案例:查询所有学生的成绩,如果这个学生没有成绩,也要将学生的个人信息显示出来 2、右外连接案例:对stu表和exam表联合查询,把…...
GitLab 创建项目、删除项目
1、创建项目 点击左上角图标,回到首页 点击 Create a project 点击 Create blank project 输入项目名称,点击Create Project 创建成功 2、删除项目 进入项目列表 点击对应项目,进入项目 进入Settings页面 拖到页面底部,展开Adva…...
python学opencv|读取图像(二十六)使用cv2.putText()绘制文字进阶-在图像上写文字
【1】引言 前序已经学会了在画布上绘制文字的大部分技巧,相关文章链接为: python学opencv|读取图像(二十三)使用cv2.putText()绘制文字-CSDN博客 python学opencv|读取图像(二十四)使用cv2.putText()绘制…...
Apache HTTPD 多后缀解析漏洞
目录 漏洞简介 漏洞环境 漏洞复现 漏洞防御 漏洞简介 Apache HTTPD 支持一个文件拥有多个后缀,并为不同后缀执行不同的指令。比如,如下配置文件: AddType text/html .html AddLanguage zh-CN .cn 以上就是Apache多后缀的特性。如果运维…...
(二)当人工智能是一个函数,函数形式怎么选择?ChatGPT的函数又是什么?
在上一篇文章中,我们通过二次函数的例子,讲解了如何训练人工智能。今天,让我们进一步探讨:面对不同的实际问题,应该如何选择合适的函数形式? 一、广告推荐系统中的函数选择 1. 业务目标 想象一下&#x…...
JavaScript学习-入门篇
JavaScript的运行环境 开发环境就是开发JavaScript代码所需的环境,一般建议新手刚刚开始使用一些记事本工具(如sublime、editPlus、VScode),锻炼代码的手感。等学习到一定阶段,就可以使用集成开发工具IDE࿰…...
今日头条ip属地根据什么显示?不准确怎么办
在今日头条这样的社交媒体平台上,用户的IP属地信息对于维护网络环境的健康与秩序至关重要。然而,不少用户发现自己的IP属地显示与实际位置不符,这引发了广泛的关注和讨论。本文将深入探讨今日头条IP属地的显示依据,并提供解决IP属…...
python之移动端测试---appium
Appium Appium介绍环境准备新版本appium的用法介绍元素定位函数被封装,统一使用By.xxx(定位方式):通过文本定位的写法 一个简单的请求示例APP操作api基础apk安装卸载发送,拉取文件uiautomatorviewer工具使用获取页面元素及属性模拟事件操作模…...
【网络安全实验室】基础关实战详情
须知少时凌云志,曾许人间第一流 1.key在哪里 url:http://rdyx0/base1_4a4d993ed7bd7d467b27af52d2aaa800/index.php 查看网页源代码的方式有4种,分别是:1、鼠标右击会看到”查看源代码“,这个网页的源代码就出现在你眼前了&…...
在DJI无人机上运行VINS-FUISON(PSDK 转 ROS)
安装ceres出现以下报错,将2版本的ceres换成1版本的ceres CMake did not find one.Could not find a package configuration file provided by "absl" with any ofthe following names:abslConfig.cmakeabsl-config.cmakeAdd the installation prefix of …...
MarkDown怎么转pdf;Mark Text怎么使用;
MarkDown怎么转pdf 目录 MarkDown怎么转pdf先用CSDN进行编辑,能双向看版式;标题最后直接导出pdfMark Text怎么使用一、界面介绍二、基本操作三、视图模式四、其他功能先用CSDN进行编辑,能双向看版式; 标题最后直接导出pdf Mark Text怎么使用 Mark Text是一款简洁的开源Mar…...
代码实战:基于InvSR对视频进行超分辨率重建
Diffusion Models专栏文章汇总:入门与实战 前言:上一篇博客《使用Diffusion Models进行图像超分辩重建》中讲解了InvSR的原理,博主实测的效果是非常不错的,和PASD基本持平。这篇博客就讲解如何利用InvSR对视频进行超分辨率重建。 目录 环境准备 代码讲解 环境准备...
解决HBuilderX报错:未安装内置终端插件,是否下载?或使用外部命令行打开。
版权声明 本文原创作者:谷哥的小弟作者博客地址:http://blog.csdn.net/lfdfhl 错误描述 在HBuilderX中执行npm run build总是提醒下载插件;图示如下: 但是,下载总是失败。运行项目时候依然弹出上述提醒。 解决方案 …...
邻接表深度优先遍历和广度优先遍历计算方法
DFS和BFS 一、 深度优先遍历次序(DFS)二、 深度优先遍历生成树三、 广度优先遍历次序(BFS)四、 广度优先遍历生成树示例说明1. DFS遍历2. BFS遍历 在图的遍历中,常用的两种算法是深度优先遍历(DFSÿ…...
计算机网络-数据链路层(CSMA/CD协议,CSMA/CA协议)
2.2 ppp协议 点对点协议ppp是目前使用最广泛的点对点数据链路层协议。 2.3 媒体接入控制基本概念 共享信道要着重考虑的一个问题就是如何协调多个发送和接收站点对一个共享传输媒体的占用,即媒体接入控制MAC。 2.3.1 静态划分信道 频分复用 时分复用 波分复用 码分复…...
网络安全的学习与实践经验(附资料合集)
学习资源 在线学习平台: Hack This Site:提供从初学者到高级难度的挑战任务,适合练习各种网络安全技术。XCTF_OJ:由XCTF组委会开发的免费在线网络安全网站,提供丰富的培训材料和资源。SecurityTube:提供丰…...
Navicat 17 for Mac 数据库管理软件
Mac分享吧 文章目录 效果一、准备工作二、开始安装1. 双击运行软件,将其从左侧拖入右侧文件夹中,等待安装完毕。2. 应用程序/启动台显示Navicat图标,表示安装成功。 二、运行测试运行后提示:“Navicat Premium.pp”已损坏&#x…...
Json与jsoncpp
目录 一、关于Json 1.数据类型 2.语法规则 二、写Json相关API 1.Json::Value类 2.append方法 3.toStyledString方法 三、读Json相关API 1.Json::Reader类 2.parse方法 3.类型判断方法 4.类型转换方法 5.getMemberNames方法 四、完整代码 一、关于Json Json是一种轻…...
Webpack、Vite区别知多少?
前端的项目打包,我们常用的构建工具有Webpack和Vite,那么Webpack和Vite是两种不同的前端构建工具,那么你们又是否了解它们的区别呢?我们在做项目时要如何选择呢? 一、工具定义 1、Webpack:是一个强大的静态模块打包工…...
以太网ICMP协议(ping指令)——FPGA学习笔记25
--素材来源原子哥 一、IP协议 1、IP简介 IP是Internet Protocol(网际互连协议)的缩写。IP 协议是 TCP/IP 协议簇中的核心协议,它为上层协议提供无状态、无连接、不可靠的服务。IP 协议规定了数据传输时的基本单元和格式 。 IP协议是 OSI 参考模型中网络层…...
ESP32自动下载电路分享
下面是一个ESP32系列或者ESP8266等电路的一个自动下载电路 在ESP32等模块需要烧写程序的时候,需要通过将EN引脚更改为低电平并将IO0引脚设置为低电平来切换到烧写模式。 有时候也会采用先将IO接到一个按键上,按住按键拉低IO0的同时重新上电的方式进入烧写…...
数据结构(ing)
学习内容 指针 指针的定义: 指针是一种变量,它的值为另一个变量的地址,即内存地址。 指针在内存中也是要占据位置的。 指针类型: 指针的值用来存储内存地址,指针的类型表示该地址所指向的数据类型并告诉编译器如何解…...
STM32-笔记32-ESP8266作为服务端
esp8266作为服务器的时候,这时候网络助手以客户端的模式连接到esp8266,其中IP地址写的是esp8266作为服务器时的IP地址,可以使用ATCIFSR查询esp8266的ip地址,端口号默认写333。 当esp8266作为服务器的时候,需要完成哪些…...
[Day 12]904.水果成篮
今天给带来的题目是滑动窗口的另一种题目,之前我们讲了滑动窗口题目中长度最小的子数组,今天这个题目实际上是求长度最长的子数组 题目描述:力扣链接 904.水果成篮 你正在探访一家农场,农场从左到右种植了一排果树。这些树用一个整…...
检查字符是否相同
给你一个字符串 s ,如果 s 是一个 好 字符串,请你返回 true ,否则请返回 false 。 如果 s 中出现过的 所有 字符的出现次数 相同 ,那么我们称字符串 s 是 好 字符串。 输入:s "abacbc" 输出:t…...
专家混合(MoE)大语言模型:免费的嵌入模型新宠
专家混合(MoE)大语言模型:免费的嵌入模型新宠 今天,我们深入探讨一种备受瞩目的架构——专家混合(Mixture-of-Experts,MoE)大语言模型,它在嵌入模型领域展现出了独特的魅力。 一、M…...
CSS3 框大小
CSS3 框大小 CSS3 是网页设计和开发中不可或缺的一部分,它为开发者提供了更多样化、更灵活的样式和布局选择。在 CSS3 中,框大小(Box Sizing)是一个重要的概念,它决定了元素内容的宽度和高度以及元素整体的大小。本文将详细介绍 CSS3 框大小的概念、用法以及最佳实践。 …...
Vue动态控制disabled属性
参考:https://blog.csdn.net/guhanfengdu/article/details/126082781 在Vue中disabled:的值是受布尔值影响的,false为关闭禁用,true为开启禁用效果。 结果就是true会让按钮禁用 相反false会让按钮重新可以使用 那如果想要通过id属性值来判断是否禁用…...
Python入门教程 —— 列表
1.列表的基本使用 列表的介绍 前面学习的字符串可以用来存储一串信息,那么想一想,怎样存储咱们班所有同学的名字呢? 定义100个变量,每个变量存放一个学生的姓名可行吗?有更好的办法吗? 列表 列表的格式 定义列的格式:[元素1, 元素2, 元素3, ..., 元素n] 变量tmp的类型…...
CSS2笔记
一、CSS基础 1.CSS简介 2.CSS的编写位置 2.1 行内样式 2.2 内部样式 2.3 外部样式 3.样式表的优先级 4.CSS语法规范 5.CSS代码风格 二、CSS选择器 1.CSS基本选择器 通配选择器元素选择器类选择器id选择器 1.1 通配选择器 1.2 元素选择器 1.3 类选择器 1.4 ID选择器 1.5 基…...
对一个双向链表,从尾部遍历找到第一个值为x的点,将node p插入这个点之前,如果找不到,则插在末尾。使用C语言实现
以下是一个用C语言实现的双向链表(Doubly Linked List)插入操作的代码。该代码从尾部遍历找到第一个值为x的节点,并在其前插入新节点p,或者在未找到时将其插入链表末尾。 #include <stdio.h> #include <stdlib.h>// 定…...
C语言string函数库补充之strstr
这次讲解一个函数strstr 它的功能是在一个字符串(称为“主字符串”)中查找另一个字符串(称为“子字符串”)的第一个出现位置。如果找到了子字符串,strstr 函数会返回一个指向子字符串在主字符串中首次出现位置的指针&…...
SpringBoot整合Mapstruct转换器使用教程(提供Gitee源码)
前言:MapStruct 主要是为了简化 Java 应用程序中不同对象之间(特别是 DTO(Data Transfer Object)、VO(Value Object)、BO(Business Object)和数据库实体类等)数据转换的过程。 目录 一、什么是Mapstruct 二、导入Maven依赖 三、创建数据模型 四、创建Mapper接口 …...
vue cli更新遇到的问题(vue -V查询版本号不变的问题)
1.镜像地址选择 npm会去默认的registry远程仓库中下载指定内容 该过程可能十分缓慢 因此我们可以切换默认仓库为镜像地址 npm config set registry https://registry.npmmirror.com 通过该指令可以从最新的镜像地址下载指定内容(镜像地址可能会有变 有变请重新查询) 2.下载 …...
CSP初赛知识学习计划
CSP初赛知识学习计划 学习目标 在20天内系统掌握CSP初赛所需的计算机基础知识、编程概念、数据结构、算法等内容,为初赛取得优异成绩奠定坚实基础。 资料收集 整理的CSP知识点文档。相关教材,如《信息学奥赛一本通》等。在线编程学习平台,…...
Python 中常见的数据结构之二推导式
Python 中常见的数据结构之二推导式 使用推异式列表推导式字典推导式集合推导式 使用推异式 推导式是一种从已存在的序列中快速构建列表(list)、集合(set) 和 字典(dictionary)方式。Python 支持 3 种不同类型的推导式: 列表推导式;字典推导式…...
java.lang.Error: FFmpegKit failed to start on brand:
如果你使用FFmpegKit的时候遇到了这个问题: java.lang.Error: FFmpegKit failed to start on brand: Xiaomi, model: MI 8, device: dipper, api level: 29, abis: arm64-v8a armeabi-v7a armeabi, 32bit abis: armeabi-v7a armeabi, 64bit abis: arm64-v8a.at c…...
Gateway服务网关
一、初识Gateway服务网关 1.为什么需要网关? 在微服务中,各个模块之间的调用,也可以称其为远程调用!但是,如果是外部(用户)对微服务进行访问时,发的请求能不加处理的直接访问微服务…...
windows终端conda activate命令行不显示环境名
问题: 始终不显示环境名 解决 首先需要配置conda的环境变量 确保conda --version能显示版本 然后对cmd进行初始化,如果用的是vscode中的终端,那需要对powershell进行初始化 Windows CMD conda init cmd.exeWindows PowerShell conda …...
【网络】ARP表、MAC表、路由表
ARP表 网络设备存储IP-MAC映射关系的表,便于快速查找和转发数据包 ARP协议工作原理 ARP(Address Resolution Protocol),地址解析协议,能够将网络层的IP地址解析为数据链路层的MAC地址。 1.主机在自己的ARP缓冲区中建立…...