Ext2数据块寻址原理:直接块、间接块到三级间接块

我们要想弄明白ext2文件系统中是如何寻址的,需要看懂下面的这个函数的实现逻辑

c 复制代码
static int ext2_block_to_path(struct inode *inode,
			long i_block, int offsets[4], int *boundary)
{
	int ptrs = EXT2_ADDR_PER_BLOCK(inode->i_sb);
	int ptrs_bits = EXT2_ADDR_PER_BLOCK_BITS(inode->i_sb);
	const long direct_blocks = EXT2_NDIR_BLOCKS,
		indirect_blocks = ptrs,
		double_blocks = (1 << (ptrs_bits * 2));
	int n = 0;
	int final = 0;

	if (i_block < 0) {
		ext2_msg(inode->i_sb, KERN_WARNING,
			"warning: %s: block < 0", __func__);
	} else if (i_block < direct_blocks) {
		offsets[n++] = i_block;
		final = direct_blocks;
	} else if ( (i_block -= direct_blocks) < indirect_blocks) {
		offsets[n++] = EXT2_IND_BLOCK;
		offsets[n++] = i_block;
		final = ptrs;
	} else if ((i_block -= indirect_blocks) < double_blocks) {
		offsets[n++] = EXT2_DIND_BLOCK;
		offsets[n++] = i_block >> ptrs_bits;
		offsets[n++] = i_block & (ptrs - 1);
		final = ptrs;
	} else if (((i_block -= double_blocks) >> (ptrs_bits * 2)) < ptrs) {
		offsets[n++] = EXT2_TIND_BLOCK;
		offsets[n++] = i_block >> (ptrs_bits * 2);
		offsets[n++] = (i_block >> ptrs_bits) & (ptrs - 1);
		offsets[n++] = i_block & (ptrs - 1);
		final = ptrs;
	} else {
		ext2_msg(inode->i_sb, KERN_WARNING,
			"warning: %s: block is too big", __func__);
	}
	if (boundary)
		*boundary = final - 1 - (i_block & (ptrs - 1));

	return n;
}

一、基础概念

  1. 直接块:inode里直接存放磁盘块号,不用绕路查找,速度最快
  2. 一级间接块:先查间接块,再取数据块
  3. 二级间接块:两层跳转查找
  4. 三级间接块:三层跳转查找
  5. 高效计算方式,位移运算效率更高:
    • 除法替换为位移 :比如 i_block / 256 可以写成 i_block >> 8,效率更高。
    • 取模替换为位运算 :比如 i_block % 256 可以写成 i_block & (256-1),也就是 i_block & 0xff

二、该函数作用

ext2_block_to_path 核心功能:传入文件内偏移块号 i_block,自动判断走哪一种寻址方式,拆分出一层层查找下标,最终告诉系统去哪一级间接块里找真实磁盘数据块。

它的入参含义如下:

  • struct inode *inode

    目标文件的 inode。这个函数主要用它拿到 inode->i_sb(超级块)里的块大小等参数,从而计算"一个间接块能放多少指针"。

  • long i_block

    文件的"逻辑块号"(第几个数据块,从0开始)。

    例如块大小1KB时,文件偏移对应的逻辑块号通常是 i_block = pos / 1024

  • int offsets[4](输出参数)

    输出参数:用来返回在ext2块指针树里走的路径。每个元素表示在某一层的数组下标/指针槽位置。

  • int *boundary()

    可选输出参数,可传 NULL,用来告诉调用者:从 i_block 开始,后面还有多少个连续逻辑块在同一个"指针块/区间"里,没跨边界。

    这个信息常用于读写合并/预读/一次映射多块时的"到边界为止"。

三、函数核心逻辑拆解

1. ptrs与ptrs_bits 底层含义

c 复制代码
static int ext2_block_to_path(struct inode *inode,
			long i_block, int offsets[4], int *boundary)
{
	int ptrs = EXT2_ADDR_PER_BLOCK(inode->i_sb);
	int ptrs_bits = EXT2_ADDR_PER_BLOCK_BITS(inode->i_sb);
        
	const long direct_blocks = EXT2_NDIR_BLOCKS,
		indirect_blocks = ptrs,
		double_blocks = (1 << (ptrs_bits * 2));
}
  • ptrs = 一个间接块(1 个 block)里能存多少个 32-bit 块号。EXT2_ADDR_PER_BLOCK(s) = EXT2_BLOCK_SIZE(s) / sizeof(__u32)

    例如块大小4KB:4096/4=1024 个指针,块大小1KB:1024/4 = 256个指针。

  • ptrs_bits = EXT2_ADDR_PER_BLOCK_BITS(inode->i_sb);
    ptrs_bits = log2(ptrs)该变量主要用位移代替除法/乘法,来提高程序的运行性能。

    例如 ptrs=1024,2^10=1024,也即是 ptrs_bits=10

2. 直接寻址

c 复制代码
else if (i_block < direct_blocks) {
	offsets[n++] = i_block;
	final = direct_blocks;
}

代码含义

这个块号很小,在inode直接管理范围内,不需要任何间接跳转,直接把块号存入路径数组,直接读取即可。

示例

假设:

  • i_block = 5
  • direct_blocks = 12

步骤:

  1. 先判断i_block < direct_blocks,5 < 12成立,则进入这个分支
  2. 赋值:
    • offsets[0] = 5
    • final = 12

3. 一级间接寻址

c 复制代码
else if ( (i_block -= direct_blocks) < indirect_blocks) {
	offsets[n++] = EXT2_IND_BLOCK;
	offsets[n++] = i_block;
	final = ptrs;
}

代码含义

  • 这一分支处理一级间接块的情况。
  • 逻辑块号 i_block 先减去直接块数量 direct_blocks,如果结果小于 indirect_blocks(即一级间接块能寻址的块数),说明这个块号属于一级间接块范围。
  • offsets[n++] = EXT2_IND_BLOCK;
    记录一级间接块在 inode 指针数组中的索引(通常是 12)。
  • offsets[n++] = i_block;
    记录在一级间接块中具体的偏移。
  • final = ptrs;
    记录本层级(一级间接块)能容纳的最大块数,便于后续边界计算。

示例:

假设:

  • direct_blocks = 12(直接块数量)
  • indirect_blocks = ptrs = 256(一级间接块能寻址的块数)
  • i_block = 20(逻辑块号)

步骤:

  1. 先判断 i_block < direct_blocks,20 < 12 不成立,跳过直接块分支。
  2. 进入本分支,i_block -= direct_blocks,即 i_block = 20 - 12 = 8
  3. 判断 8 < 256,成立,说明第20号块属于一级间接块。
  4. 赋值:
    • offsets[0] = EXT2_IND_BLOCK(通常是12)
    • offsets[1] = 8(第20号逻辑块在inode的一级间接块指针指向的块中,是第8个数据块)
    • final = 256(表示一级间接块最多能容纳256个数据块)

4. 二级间接区间寻址

c 复制代码
else if ((i_block -= indirect_blocks) < double_blocks) {
	offsets[n++] = EXT2_DIND_BLOCK;
	offsets[n++] = i_block >> ptrs_bits;
	offsets[n++] = i_block & (ptrs - 1);
	final = ptrs;
}

代码含义

把二级间接块看成一个二维数组:

erlang 复制代码
二级间接块
  ├─ 一级间接块0 ─ 数据块0, 数据块1, ..., 数据块255
  ├─ 一级间接块1 ─ 数据块256, ..., 数据块511
  ├─ ...
  └─ 一级间接块10 ─ 数据块2560, ..., 数据块2815
  • i_block >> ptrs_bits 就是"行号"(第几个一级间接块)
  • i_block & (ptrs - 1) 就是"列号"(一级间接块里的第几个数据块)
  • 翻译成除法运算:
    1. 先减去前面所有占用块号,得到纯净二级区间块号
    2. 行号 = 块号 ÷ 256
    3. 列号 = 块号 % 256

示例

假设:

  • direct_blocks = 12
  • indirect_blocks = ptrs = 256
  • double_blocks = 65536
  • ptrs_bits = 8
  • 逻辑块号 i_block = 3000

步骤:

  1. 跳过直接块和一级间接块

    • i_block < direct_blocks?否(3000 > 12)
    • i_block -= direct_blocksi_block = 3000 - 12 = 2988
    • i_block < indirect_blocks?否(2988 > 256)
    • i_block -= indirect_blocksi_block = 2988 - 256 = 2732
    • i_block < double_blocks?是(2732 < 65536),进入本分支
  2. 路径分解

    • offsets[n++] = EXT2_DIND_BLOCK;(通常是13,表示二级间接块指针在inode数组中的位置)
    • offsets[n++] = i_block >> ptrs_bits;2732 >> 8 = 10,表示在二级间接块的第10个一级间接块
    • offsets[n++] = i_block & (ptrs - 1);2732 & 255 = 172,表示在该一级间接块的第172个数据块
  3. final 赋值

    • final = ptrs = 256
    • 表示本层级(一级间接块)能容纳的最大块数

5. 三级间接寻址

c 复制代码
else if (((i_block -= double_blocks) >> (ptrs_bits * 2)) < ptrs) {
	offsets[n++] = EXT2_TIND_BLOCK;
	offsets[n++] = i_block >> (ptrs_bits * 2);
	offsets[n++] = (i_block >> ptrs_bits) & (ptrs - 1);
	offsets[n++] = i_block & (ptrs - 1);
	final = ptrs;
}

代码含义

  • 先把 i_block 减去所有直接块、一级间接块、二级间接块能寻址的块数。-
  • 再右移 (ptrs_bits * 2) 位,等价于除以 ptrs * ptrs,判断结果是否小于 ptrs。
  • 以此来判断i_block是否落在三级间接块能寻址的范围内。
  • EXT2_TIND_BLOCK:inode 里三级间接块指针的位置(通常是14)。
  • i_block >> (ptrs_bits * 2):第几个二级间接块(行号)。
  • (i_block >> ptrs_bits) & (ptrs - 1):在该二级间接块的第几个一级间接块(列号)。
  • i_block & (ptrs - 1):在该一级间接块的第几个数据块(最底层的列号)。

示例

假设:

  • ptrs = 256,ptrs_bits = 8
  • double_blocks = 65536
  • 逻辑块号 i_block = 100000

步骤:

  1. i_block -= double_blocks → i_block = 100000 - 65536 = 34464
  2. i_block >> 16(ptrs_bits * 2 = 16)→ 34464 >> 16 = 0,说明在第0个二级间接块
  3. (i_block >> 8) & 255 → (34464 >> 8) & 255 = 134 & 255 = 134,在第134个一级间接块
  4. i_block & 255 → 34464 & 255 = 64,在该一级间接块的第64个数据块

结果:

  • inode 的三级间接块指针
  • 第0个二级间接块
  • 第134个一级间接块
  • 第64个数据块

具象的理解

可以把三级间接比作一个超级电影院,这个超级电影院有如下设计:

  • 第一层(影院大门):有256个放映厅,编号从0到255
  • 第二层(放映厅里):每个放映厅里,有256个看台,编号从0到255
  • 第三层(看台上):每个看台上,有256个座位,编号从0到255

块号 = 70000 号的座位,我们可以通过整除、取余数方式来计算:

  1. 第一步:找放映厅 公式:块号 ÷ (256×256) 作用:确定属于哪一个放映厅。 计算:70000 ÷ 65536商是 1,余数是 70000 - 65536 = 4464。 结论:在第1号放映厅里。

  2. 第二步:找看台 公式:(块号 ÷ 256) % 256 作用:确定放映厅内第几个看台。 计算:`70000 ÷ 256 = 273',273代表从整个电影院的第0号座位算起,70000号座位,在所有"看台序列"里,排第273个。第1号放映厅里,只装第256 ~ 511号看台(因为第0号放映厅装了0~255号看台)。所以我们要算: 273 % 256 = 17。 结论:在第17号看台上。

  3. 第三层步:找座位 公式:块号 % 256 作用:确定看台上具体座位 计算:第1号放映厅的第17号看台,看台上有256个座位,编号0~255。所以我们要算:70000 % 256 = 112。 结论:在第17号看台上的第112号座位。

6、边界值 boundary 变量通俗讲解

c 复制代码
*boundary = final - 1 - (i_block & (ptrs - 1));

计算当前这一层还剩余多少空闲位置,预判距离当前层级装满还有多少余量,用于文件写入时判断是否需要开辟新间接块。

四、核心总结

  1. 整个ext2_block_to_path函数,本质就是分层拆分块号 ,分层依据:块号从小到大,依次走直接块→一级→二级→三级间接。 2 所有分层拆分逻辑,基本的逻辑全是整除向上分层,余数定位内部位置,位运算只是为了内核性能,换成除法余数更好理解。

五、结尾

学会这个内核经典函数后,我们就能彻底明白:为什么小文件读写快、大文件读写存在间接寻址开销。

后续会继续分享Ext2超级块、inode、日志文件系统等底层干货,喜欢存储与Linux内核底层知识可以点个关注~~

相关推荐
Irene19912 小时前
nano 和 vim(Linux 默认安装)的区别(文本编辑器 vs 专业编辑器)
linux·vim·nano
量子炒饭大师2 小时前
【Linux系统编程】——【从0构建第一个Linux系统-进度条】从0到1分阶段构建动态进度条
linux·运维·服务器·进度条
.千余2 小时前
【Linux】网络基础2---Socket编程预备
linux·网络·php
曦月合一2 小时前
在CentOS 6.5系统中OpenJDK 1.7升级更新 OpenJDK 1.8,并部署
linux·centos·jdk1.8
小小ken2 小时前
virtualbox中的ubuntu虚拟机登录到桌面后出现屏幕闪烁现象解决办法
linux·运维·ubuntu
tianyuanwo2 小时前
Linux社区ISO制作底层探秘:从mkisofs到xorriso的全面解析
linux·mkisofs·xorriso
xiaoye-duck2 小时前
《Linux系统编程》Linux基础开发工具 (三):从零实现动态进度条(附回车、换行与缓冲区详解)
linux
cui_ruicheng2 小时前
Linux网络编程(四):UDP Socket基础编程
linux·服务器·网络·udp
用户2367829801682 小时前
Linux more 命令详解:从基础分页到高级文本查看技巧
linux