当前位置: 首页 > news >正文

已知 inode 号,如何操作文件?Ext 文件系统增删查改底层逻辑拆解

前言

在 Linux Ext 系列文件系统(Ext2/Ext3/Ext4)中,inode 是文件的 “身份证”—— 它记录了文件的元数据(权限、大小、数据块位置等),是连接 “文件名” 与 “实际数据” 的核心桥梁。我们通常通过文件名(如/home/test.txt)操作文件,但这背后其实是 “文件名→目录项→inode→数据块” 的查找流程。

那如果跳过目录查找,直接已知 inode 号和指定分区,对文件的 “增、删、查、改” 本质是在做什么?这不仅能帮我们理解文件系统的底层逻辑,更能搞懂 “inode 为何是文件的核心索引”。

本文将以 Ext2 文件系统为例,从 “inode 号定位 inode 结构体” 的基础步骤切入,逐一拆解 “查、改、删、增” 四大操作的底层细节 —— 包括元数据如何读写、数据块如何分配、目录项如何关联,让你从 “使用者” 视角转变为 “设计者” 视角,彻底吃透文件操作的本质。

一、前提:先搞定 “从 inode 号到 inode 结构体” 的定位

在解释任何操作前,必须先明确:已知 inode 号和指定分区时,如何找到对应的 inode 结构体?这是所有操作的 “入场券”,核心依赖 Ext 文件系统的 “分组式存储” 设计。

1. 先读超级块:获取 “全局配置参数”

指定分区挂载后,内核首先读取分区的超级块(struct ext2_super_block) —— 它是分区的 “总配置表”,存储了定位 inode 所需的 3 个关键参数:

  • s_inodes_per_group:每个块组包含的 inode 总数(比如 1024 个 / 组);
  • s_inode_size:每个 inode 结构体的大小(Ext2 默认 128 字节,Ext4 可配置为 256 字节);
  • s_blocks_per_group:每个块组的总数据块数(辅助定位块组位置)。

超级块的位置固定:原始副本在块组 0(第一个块组)的第 1 个数据块(块号 1),同时在 2^n 编号的块组(1、2、4、8...)中备份,防止损坏。

2. 计算块组编号:确定 inode 在哪个 “存储单元”

Ext 文件系统将分区划分为多个大小相等的 “块组(Block Group)”,每个块组自带一套 “inode 表 + 数据块 + 块组描述符”。通过 inode 号计算块组编号的公式为:

块组编号 = (inode号 - 1) / s_inodes_per_group  

(减 1 是因为 inode 号从 1 开始,而块组索引从 0 开始,避免整除时多算一组)

举个例子:若 inode 号 = 1234,s_inodes_per_group=1024,则块组编号 =(1234-1)/1024=1233/1024=1(整除取商),即 inode 在第 2 个块组(索引 1)。

3. 定位 inode 结构体:找到块组内的 “具体位置”

确定块组后,需进一步计算 inode 在该块组 “inode 表” 中的偏移位置:

组内偏移 = (inode号 - 1) % s_inodes_per_group  
inode在磁盘的偏移量 = 块组的inode表起始块号 × 块大小 + 组内偏移 × s_inode_size  

  • 块组的 inode 表起始块号:从 “块组描述符(Group Descriptor)” 中获取 —— 每个块组描述符记录了该组 inode 表、数据块的起始位置;
  • 块大小:由超级块s_log_block_size计算(块大小 = 1024×2^s_log_block_size,如s_log_block_size=2则块大小 = 4096 字节)。

最终,内核通过 “磁盘偏移量” 读取到目标 inode 结构体 —— 这是后续所有操作的 “元数据入口”。

二、“查”:读取文件信息,本质是 “解析 inode + 读取数据块”

“查” 是最基础的操作,分为 “查元数据” 和 “查内容” 两类,核心是 “读” 而非 “改”。

1. 查元数据:直接解析 inode 结构体

inode 结构体(struct ext2_inode)存储了文件的所有元数据,已知 inode 结构体后,直接提取字段即可获取信息,无需操作数据块。关键字段与对应查询场景如下:

元数据类型inode 结构体字段查询场景示例
文件类型与权限i_models -l 查看权限(如-rw-r--r--
所有者与组i_uidi_gidls -l 查看用户(如user:group
文件大小i_sizedu -h 查看文件占用空间
时间戳i_atime(访问)、i_mtime(修改)、i_ctime(元数据变更)stat 查看文件时间信息
数据块映射i_block数组定位文件实际数据存储位置

比如执行stat /home/test.txt,若已知其 inode 号,内核会直接定位 inode 结构体,提取i_atimei_size等字段返回给用户 —— 这比通过文件名查找快得多。

2. 查文件内容:通过 inode 的i_block数组定位数据块

文件内容存储在 “数据块(Data Block)” 中,inode 的i_block数组是 “数据块的索引表”,通过它才能找到具体的内容。整个流程分为 “解析i_block数组” 和 “读取数据块” 两步:

(1)i_block数组的结构:4 种指针类型

Ext2 的i_block是一个长度为 15 的数组(__u32 i_block[15]),包含 4 种指针,支持不同大小的文件:

  • 直接指针(前 12 个)i_block[0]~i_block[11],直接指向存储文件内容的数据块。适合小文件(如 12×4KB=48KB 以内,块大小 4KB 时),访问速度最快(一次定位);
  • 一级间接指针i_block[12],指向一个 “一级间接块”—— 该块不存内容,而是存储多个 “数据块的编号”(如 4KB 块可存 1024 个 4 字节编号)。适合中等文件(48KB~48KB+4MB=4144KB);
  • 二级间接指针i_block[13],指向 “二级间接块”—— 该块存储 “一级间接块的编号”,一级间接块再存 “数据块编号”。适合大文件(4144KB~4144KB+4GB=4096.1MB);
  • 三级间接指针i_block[14],指向 “三级间接块”—— 通过 “三级→二级→一级→数据块” 的层级,支持超大文件(最大 4TB,块大小 4KB 时)。
(2)读取内容的具体流程(以 “读取文件偏移 5KB” 为例)

假设块大小 = 4KB,inode 号 = 1234,目标偏移 = 5KB:

  1. 计算目标数据块序号:偏移 5KB ÷ 块大小 4KB = 1(商为块序号,从 0 开始),即需要读取第 2 个数据块;
  2. 解析i_block数组:块序号 1 < 12(直接指针数量),直接取i_block[1]的值 —— 这是目标数据块的编号(如块号 = 567);
  3. 读取数据块:根据块编号 567,计算其在分区的物理位置(块号 × 块大小 = 567×4KB=2268KB),读取该块的 4KB 数据;
  4. 提取目标内容:偏移 5KB 的 “块内偏移”=5KB - 1×4KB=1KB,从读取的 4KB 数据中提取第 1KB~5KB 的内容,返回给用户。

如果是大文件(如偏移 10MB),则需要通过一级间接块:先读i_block[12]指向的间接块,从间接块中找到第(10MB÷4KB -12)=2560-12=2548 个数据块编号,再读对应的数据块 —— 本质是多了一次 “间接块读取”,但逻辑一致。

三、“改”:修改文件,本质是 “更新 inode 元数据 + 重写数据块”

“改” 分为 “改元数据” 和 “改内容”,核心是 “更新 inode 或数据块,并同步磁盘”,需保证文件系统的一致性(如时间戳更新、块位图同步)。

1. 改元数据:直接修改 inode 结构体字段

元数据修改不涉及文件内容,仅需更新 inode 结构体的对应字段,并将修改同步到磁盘的 inode 表中。常见场景如下:

修改场景操作逻辑
修改权限(chmod 7551. 定位 inode 结构体;2. 将i_mode字段从0100644(rw-r--r--)改为0100755(rwxr-xr-x);3. 更新i_ctime(元数据变更时间)为当前时间;4. 将修改后的 inode 结构体写回磁盘 inode 表。
修改所有者(chown1. 定位 inode 结构体;2. 更新i_uid(用户 ID)和i_gid(组 ID);3. 更新i_ctime;4. 同步磁盘。
截断文件(truncate1. 定位 inode 结构体;2. 若目标大小(如 10KB)<原大小(如 20KB):计算需释放的块(块序号 3~4),将这些块的编号在 “块位图” 中标记为 “空闲”;3. 更新i_size为 10KB,更新i_ctimei_mtime(内容修改时间);4. 同步 inode 表和块位图到磁盘。

这类修改速度极快 —— 因为仅操作 inode 结构体(128/256 字节),无需处理数据块。

2. 改内容:重写或追加数据块,同步 inode 指针

内容修改涉及数据块的读写,需分 “覆盖已有内容” 和 “追加新内容” 两种场景,核心是 “保证数据块与 inode 指针的一致性”。

(1)场景 1:覆盖已有内容(如修改文件中间 1KB)

假设文件路径/home/test.txt,inode 号 = 1234,目标是将偏移 5KB~6KB 的内容改为 “new data”:

  1. 定位数据块:同 “查内容” 逻辑,计算偏移 5KB 对应块序号 1,通过i_block[1]找到块号 567;
  2. 重写数据块:读取块 567 的 4KB 数据,将 “块内偏移 1KB~2KB” 的内容替换为 “new data”,再将修改后的 4KB 数据写回块 567;
  3. 更新 inode 时间戳:定位 inode 结构体,将i_mtime(内容修改时间)和i_ctime(元数据间接变更)更新为当前时间;
  4. 同步磁盘:将修改后的 inode 结构体和数据块写回磁盘,避免掉电丢失。
(2)场景 2:追加新内容(如echo "new line" >> test.txt

假设原文件大小 = 10KB(块序号 0~2,用了 3 个直接指针),追加内容大小 = 2KB,块大小 = 4KB:

  1. 检查最后一个数据块是否有空闲空间:原文件最后一个块是块序号 2(i_block[2]指向块号 569),该块已用 10KB - 2×4KB=2KB,剩余 2KB 空间,刚好容纳追加的 2KB 内容;
  2. 追加内容到数据块:读取块 569 的 4KB 数据,在 “块内偏移 2KB” 处追加 “new line”,再写回块 569;
  3. 更新 inode 大小和时间戳:将i_size从 10KB 改为 12KB,更新i_mtimei_ctime
  4. 同步磁盘:写回 inode 结构体和数据块。

如果追加内容超出最后一块的空闲空间(如追加 3KB,剩余 2KB 不够),则需要分配新数据块

  • 从块组的 “块位图” 中找到第一个空闲块(如块号 570),标记为 “已使用”;
  • 将追加内容写入块 570;
  • 更新 inode 的i_block[3](第 4 个直接指针)为块号 570,i_size改为 10KB+3KB=13KB;
  • 同步块位图、inode 表和新数据块。

如果直接指针已用完(如用了 12 个直接块,追加内容需第 13 个块),则需要分配 “一级间接块”:

  • 分配一个空闲块作为一级间接块(如块号 571),标记为 “已使用”;
  • 将新数据块的编号(如 572)写入间接块 571 的第一个位置;
  • 更新 inode 的i_block[12]为间接块号 571,i_size相应增加;
  • 同步间接块、inode 表和新数据块 —— 这就是大文件追加的底层逻辑。

四、“删”:删除文件,本质是 “释放 inode 和数据块,断开目录关联”

很多人以为 “删除文件” 是 “清空数据块内容”,但实际上 Ext 文件系统的删除是 “释放索引”—— 数据块内容仍在磁盘,只是 inode 和块的 “占用标记” 被清除,后续可被新数据覆盖。

已知 inode 号和指定分区时,删除流程分为 “断开目录关联”“递减引用计数”“释放资源” 三步:

1. 第一步:断开目录项与 inode 的关联

目录项(dentry)是内存中的 “文件名→inode 号” 映射,存储在目录项高速缓存(dcache)中。每个文件的目录项都属于其父目录(如/home/test.txt的目录项属于/home目录)。

  • 定位父目录的 inode(如/home的 inode 号 = 456),读取其父目录的数据块(目录的数据块存储 “目录项列表”,每个目录项包含 “文件名、inode 号、类型”);
  • 在父目录的目录项列表中,找到 “文件名 = test.txt,inode 号 = 1234” 的目录项,将其标记为 “无效”(或直接删除该条目);
  • 更新父目录 inode 的i_mtime(目录内容修改时间)和i_ctime,同步父目录 inode 到磁盘。

这一步的作用是:让用户无法通过原文件名找到该 inode—— 但 inode 和数据块仍未释放,若有其他硬链接(i_nlink>1),仍可通过硬链接访问。

2. 第二步:递减 inode 的引用计数(i_nlink

inode 结构体的i_nlink字段记录 “硬链接数”—— 即多少个目录项指向该 inode。删除时需先递减该计数:

  • 定位目标 inode 结构体,将i_nlink -= 1
  • i_nlink > 0(存在其他硬链接):仅完成 “断开目录关联”,不释放 inode 和数据块(如ln a.txt b.txt后删除 a.txt,b.txt 仍可访问);
  • i_nlink == 0(无任何硬链接):进入 “彻底释放资源” 流程。

3. 第三步:彻底释放 inode 和数据块

这是删除的核心步骤,需释放 inode 和所有关联的数据块,将其标记为 “空闲”,供其他文件使用:

(1)释放数据块

遍历 inode 的i_block数组,释放所有关联的数据块(包括直接块、间接块):

  1. 释放直接块:遍历i_block[0]~i_block[11],若块编号非 0(表示已分配),则在块组的 “块位图” 中找到该块编号,标记为 “空闲”;
  2. 释放一级间接块:若i_block[12]非 0(存在一级间接块):
    • 读取该间接块,遍历其中存储的所有数据块编号,将这些块在块位图中标记为 “空闲”;
    • 再将一级间接块本身在块位图中标记为 “空闲”;
  3. 释放二级 / 三级间接块:逻辑同上,先释放下一级间接块中的数据块,再释放当前间接块(如二级间接块→一级间接块→数据块);
  4. 更新超级块:将超级块的s_free_blocks_count(空闲数据块数)加上 “释放的块总数”,同步超级块到磁盘。
(2)释放 inode
  1. 在块组的 “inode 位图” 中,找到目标 inode 号的位置,将其标记为 “空闲”(表示该 inode 号可被新文件重新分配);

     2. 清空 inode 结构体的关键字段(如i_mode设为 0、i_block数组置空、i_size设为 0),避      免残留数据干扰新文件;
3. 更新超级块的s_free_inodes_count(空闲 inode 数),使其加 1,同步超级块到磁盘。

至此,文件的 “索引信息”(inode 和数据块标记)已完全释放 —— 虽然磁盘上的数据块内容未被 “擦除”,但系统已认为这些空间是空闲的,后续新文件写入时会覆盖旧数据,这也是数据恢复工具能找回删除文件的原理(需在数据被覆盖前操作)。

五、“增”:创建文件,本质是 “分配 inode + 分配数据块 + 建立目录映射”

这里需先澄清:“创建文件” 时,我们通常不知道 inode 号(inode 号是创建过程中分配的),但 “已知指定分区 + 父目录 inode 号” 是创建的前提 —— 因为新文件的目录项必须存储在父目录的数据块中。整个流程可拆解为 “分配 inode”“初始化 inode”“分配数据块(可选)”“建立目录关联” 四步:

1. 第一步:在分区中分配空闲 inode

创建文件的核心是先拿到一个 “未被使用” 的 inode,作为文件的元数据载体:

  1. 遍历块组找空闲 inode:从块组 0 开始,依次检查每个块组的 “inode 位图”,找到第一个标记为 “空闲” 的 inode 号(记为new_inode_num);
  2. 标记 inode 为已使用:在该块组的 inode 位图中,将new_inode_num对应的位标记为 “已使用”,防止被其他文件重复分配;
  3. 初始化 inode 结构体:根据新文件的类型(如正则文件、目录),填充 inode 结构体字段:
    • i_mode:设为正则文件(0100644,默认权限,受 umask 影响)或目录(0040755);
    • i_uid/i_gid:设为当前用户的 ID 和组 ID(如uid=1000gid=1000);
    • i_size:初始设为 0(空文件);
    • i_atime/i_mtime/i_ctime:均设为当前时间戳(创建时间);
    • i_nlink:设为 1(初始只有父目录的一个目录项指向该 inode);
    • i_block:数组置空(暂无数据块关联)。
  4. 同步 inode 到磁盘:将初始化后的 inode 结构体写入该块组的 inode 表中,确保数据持久化。

2. 第二步:分配初始数据块(可选,取决于是否写入初始内容)

  • 若创建空文件(如touch test.txt):无需分配数据块,i_block数组保持空,i_size仍为 0;
  • 若创建文件时直接写入内容(如echo "hello" > test.txt):需分配 1 个空闲数据块,流程如下:
    1. 遍历块组的 “块位图”,找到第一个 “空闲” 的数据块编号(记为new_block_num);
    2. new_block_num在块位图中标记为 “已使用”;
    3. 将 “hello\n”(共 6 字节)写入new_block_num对应的数据块;
    4. 更新 inode 的i_block[0](第一个直接指针)为new_block_numi_size设为 6 字节。

3. 第三步:建立目录项与 inode 的关联

新文件的 inode 和数据块已准备好,但用户需要通过 “文件名” 访问文件 —— 这就需要在父目录中添加一条 “文件名→inode 号” 的目录项:

  1. 定位父目录的 inode 和数据块:已知父目录的 inode 号(如/home的 inode 号 = 456),通过前文的 “inode 定位逻辑” 找到其父目录的 inode 结构体,再从i_block数组中读取父目录的数据块(目录的数据块存储所有子文件的目录项);
  2. 在父目录数据块中添加目录项:目录项的结构通常包含 “文件名长度、文件名、inode 号、文件类型”,例如:
    • 文件名:test.txt(长度 8 字节);
    • inode 号:new_inode_num(如 1234);
    • 文件类型:正则文件(标记为0x8);
      将这条目录项写入父目录数据块的空闲位置(若父目录数据块已满,则需为父目录分配新数据块);
  3. 更新父目录 inode:将父目录 inode 的i_mtime(目录内容修改时间)和i_ctime更新为当前时间,同步父目录 inode 和数据块到磁盘。

4. 第四步:更新超级块的全局统计信息

最后,更新分区超级块的空闲资源计数,反映 “创建文件” 对资源的消耗:

  • 若未分配数据块:仅将s_free_inodes_count减 1(空闲 inode 数减少 1);
  • 若分配了数据块:将s_free_inodes_count减 1,同时将s_free_blocks_count减 1(空闲数据块数减少 1);
  • 同步超级块到磁盘,确保整个文件系统的状态一致性。

至此,文件创建完成 —— 用户后续可通过 “父目录路径 + 文件名”(如/home/test.txt),经目录项找到 inode 号,再通过 inode 访问数据块。

六、总结:已知 inode 号的文件操作,到底在 “操作什么”?

梳理完 “增删查改” 四大操作,我们可以用一张表总结其核心逻辑 —— 本质上,所有操作都是围绕 “inode 元数据” 和 “数据块” 的组合管理,已知 inode 号只是跳过了 “目录项→inode 号” 的查找步骤,直接切入文件系统的核心索引层:

文件操作核心操作对象底层本质动作
inode 结构体、数据块读取 inode 元数据(权限、大小等),解析i_block指针定位数据块并读取内容
inode 结构体、数据块、位图更新 inode 字段(元数据修改),或重写 / 追加数据块(内容修改),同步时间戳和位图
inode、数据块、位图、目录项断开目录关联→递减 inode 引用计数→释放数据块(块位图置空闲)→释放 inode(inode 位图置空闲)
父目录 inode、新 inode、数据块分配空闲 inode→初始化 inode→(可选)分配数据块→在父目录添加目录项→更新超级块

理解这些逻辑,不仅能帮你搞懂 “文件操作为何有时快有时慢”(如改元数据比改内容快、小文件比大文件操作快),更能在遇到文件系统问题时(如 inode 耗尽、数据块损坏)快速定位原因 —— 毕竟,所有文件系统工具(如df -i查看 inode 使用、fsck修复磁盘)的底层逻辑,都源于对这些操作的封装。


文章转载自:

http://P6nGV4EI.dgcLy.cn
http://pysCjBPC.dgcLy.cn
http://Q1zTIGOP.dgcLy.cn
http://RyXESARO.dgcLy.cn
http://49jcBuqj.dgcLy.cn
http://5mCUSv2P.dgcLy.cn
http://4Lcnvxag.dgcLy.cn
http://NrNMspdJ.dgcLy.cn
http://Btt3YuvI.dgcLy.cn
http://bxUK1OEP.dgcLy.cn
http://G9xv3Qac.dgcLy.cn
http://j49Vzgnx.dgcLy.cn
http://IOXQO9EK.dgcLy.cn
http://aa19BCIj.dgcLy.cn
http://1yDNml1k.dgcLy.cn
http://rdoxdOKF.dgcLy.cn
http://mo5ZyjGy.dgcLy.cn
http://bSsmrbHn.dgcLy.cn
http://t7Vn5J8H.dgcLy.cn
http://fZwk7dUL.dgcLy.cn
http://i7dGyojR.dgcLy.cn
http://5LSS1HOQ.dgcLy.cn
http://sToHe5H1.dgcLy.cn
http://uE5Q9zEb.dgcLy.cn
http://3KJ3PaMA.dgcLy.cn
http://Lc3ULZF2.dgcLy.cn
http://lEdDTFYT.dgcLy.cn
http://KScEBiAd.dgcLy.cn
http://GhuwX81G.dgcLy.cn
http://mgGJBP2C.dgcLy.cn
http://www.dtcms.com/a/374700.html

相关文章:

  • 论文阅读,Plug-and-Play Latent Diffusion,Brain Imaging
  • C#(/unity)中的闭包
  • 概率论第六讲—数理统计
  • Oracle RAC共享存储核心技术
  • C++, ffmpeg, libavcodec-RTSP拉流,opencv实时预览
  • 全网首发!Realsense 全新 D555 相机开箱记录与 D435i、L515、D456 横向测评!
  • 基于 Django 与 Bootstrap 构建的现代化设备管理平台
  • 图像金字塔---图像上采样下采样
  • 【ARM】ULINK Pro如何和SWD接口进行连接调试
  • 使用 Apollo TransformWrapper 生成相机到各坐标系的变换矩阵
  • 苹果用户速更新!macOS存严重漏洞,用户隐私数据面临泄露风险
  • 认识CPU (六):缓存与内存——芯片里的多级智能仓库
  • C++设计模式原理与实战(视频教程)
  • 苍穹外卖项目实战(day7-1)-缓存菜品和缓存套餐功能-记录实战教程、问题的解决方法以及完整代码
  • 51.不可变基础设施:云原生时代的「乐高城堡」建造法
  • Redis小白入门
  • 分层-三层架构
  • 实战:HarmonyOS 中 HEIF 图像开发全流程(图处理篇)
  • 深入 Kubernetes:从零到生产的工程实践与原理洞察
  • 在Ubuntu上修改Nginx的默认端口(例如从80端口改为其他端口,如8080)
  • 《用 Pandas 和 Matplotlib 绘制柱状图:从数据读取到可视化表达的实战指南》
  • python之socket网络编程
  • 【用与非门设计一个七段显示译码器,要求显示Y, E, S 三个符号+门电路符号逻辑式】2022-12-5
  • 解决 Ubuntu 25.04 下 make menuconfig 报 ncurses 错误的问题
  • (49)es容器化部署启动报错-RBAC权限问题
  • MacOS 运行CosyVoice
  • Adam优化算法:深度学习的自适应动量估计方法
  • macos deepctr_torch虚拟环境配置
  • react的filber架构
  • Spring框架事件驱动架构核心注解之@EventListener