我们要想弄明白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;
}
一、基础概念
- 直接块:inode里直接存放磁盘块号,不用绕路查找,速度最快
- 一级间接块:先查间接块,再取数据块
- 二级间接块:两层跳转查找
- 三级间接块:三层跳转查找
- 高效计算方式,位移运算效率更高:
- 除法替换为位移 :比如
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
步骤:
- 先判断i_block < direct_blocks,5 < 12成立,则进入这个分支
- 赋值:
- 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(逻辑块号)
步骤:
- 先判断
i_block < direct_blocks,20 < 12 不成立,跳过直接块分支。 - 进入本分支,
i_block -= direct_blocks,即i_block = 20 - 12 = 8。 - 判断
8 < 256,成立,说明第20号块属于一级间接块。 - 赋值:
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)就是"列号"(一级间接块里的第几个数据块)- 翻译成除法运算:
- 先减去前面所有占用块号,得到纯净二级区间块号
- 行号 = 块号 ÷ 256
- 列号 = 块号 % 256
示例
假设:
direct_blocks = 12indirect_blocks = ptrs = 256double_blocks = 65536ptrs_bits = 8- 逻辑块号
i_block = 3000
步骤:
-
跳过直接块和一级间接块
i_block < direct_blocks?否(3000 > 12)i_block -= direct_blocks,i_block = 3000 - 12 = 2988i_block < indirect_blocks?否(2988 > 256)i_block -= indirect_blocks,i_block = 2988 - 256 = 2732i_block < double_blocks?是(2732 < 65536),进入本分支
-
路径分解
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个数据块
-
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
步骤:
- i_block -= double_blocks → i_block = 100000 - 65536 = 34464
- i_block >> 16(ptrs_bits * 2 = 16)→ 34464 >> 16 = 0,说明在第0个二级间接块
- (i_block >> 8) & 255 → (34464 >> 8) & 255 = 134 & 255 = 134,在第134个一级间接块
- i_block & 255 → 34464 & 255 = 64,在该一级间接块的第64个数据块
结果:
- inode 的三级间接块指针
- 第0个二级间接块
- 第134个一级间接块
- 第64个数据块
具象的理解
可以把三级间接比作一个超级电影院,这个超级电影院有如下设计:
- 第一层(影院大门):有256个放映厅,编号从0到255
- 第二层(放映厅里):每个放映厅里,有256个看台,编号从0到255
- 第三层(看台上):每个看台上,有256个座位,编号从0到255
找 块号 = 70000 号的座位,我们可以通过整除、取余数方式来计算:
-
第一步:找放映厅 公式:
块号 ÷ (256×256)作用:确定属于哪一个放映厅。 计算:70000 ÷ 65536商是 1,余数是 70000 - 65536 = 4464。 结论:在第1号放映厅里。 -
第二步:找看台 公式:
(块号 ÷ 256) % 256作用:确定放映厅内第几个看台。 计算:`70000 ÷ 256 = 273',273代表从整个电影院的第0号座位算起,70000号座位,在所有"看台序列"里,排第273个。第1号放映厅里,只装第256 ~ 511号看台(因为第0号放映厅装了0~255号看台)。所以我们要算: 273 % 256 = 17。 结论:在第17号看台上。 -
第三层步:找座位 公式:
块号 % 256作用:确定看台上具体座位 计算:第1号放映厅的第17号看台,看台上有256个座位,编号0~255。所以我们要算:70000 % 256 = 112。 结论:在第17号看台上的第112号座位。
6、边界值 boundary 变量通俗讲解
c
*boundary = final - 1 - (i_block & (ptrs - 1));
计算当前这一层还剩余多少空闲位置,预判距离当前层级装满还有多少余量,用于文件写入时判断是否需要开辟新间接块。
四、核心总结
- 整个
ext2_block_to_path函数,本质就是分层拆分块号 ,分层依据:块号从小到大,依次走直接块→一级→二级→三级间接。 2 所有分层拆分逻辑,基本的逻辑全是整除向上分层,余数定位内部位置,位运算只是为了内核性能,换成除法余数更好理解。
五、结尾
学会这个内核经典函数后,我们就能彻底明白:为什么小文件读写快、大文件读写存在间接寻址开销。
后续会继续分享Ext2超级块、inode、日志文件系统等底层干货,喜欢存储与Linux内核底层知识可以点个关注~~