在实际写数据到磁盘之前需要分配磁盘上的空间。这里的写数据包括写文件数据、在目录中创建文件和添加扩展属性等等。但凡需要存储新数据的场景都需要分配磁盘空间。分配磁盘空间的主要功能在函数ext2_get_blocks
中实现,该函数的原型如下所示:
static int ext2_get_blocks(struct inode *inode,
sector_t iblock, unsigned long maxblocks,
struct buffer_head *bh_result,
int create)
该函数原型中,重点需要说明的是iblock参数,该参数表示文件的逻辑位置,位置以文件系统的块大小为单位,值为文件系统的以0为起始位置逻辑地址。举个简单的例子,加入文件系统格式化时块大小是2K,而此时写入数据的偏移为4K,那么此时iblock就是2。也就是说,该函数通过数据在文件中的逻辑位置计算需要分配多少磁盘空间。
函数ext2_get_blocks的主流程如图1所示,该函数主要完成三方面的工作:
- 计算并获取存储路径,我们知道文件数据是通过间接块的方式存储的,因此这里主要是根据数据逻辑地址计算出其存储的路径情况,并获得该路径。
- 计算需要分配的块的数量和期望的磁盘物理位置。
- 分配磁盘空间,计算出需要的磁盘空间的数量后,最后该函数调用ext2_alloc_branch来分配需要的磁盘空间,具体就是将空间管理的位图置位。
为了容易理解磁盘整个分配磁盘空间的流程,我们先回顾一下Ext2的间接块结构,如图2所示(具体解释请参考本号历史文章)。为了理解更清楚一些,我们假设请求的数据位置需要二级间接块,因此,我们将该关系放大,如图3所示。我们知道对于Ext2来说,其地址是32位的,因此在间接块中的数据其实可以理解为无符号整型的大数组。其中数组中的每一项就是一个下一级磁盘数据块的地址。这样我们根据数据在文件中的逻辑地址就可以计算出来需要几级间接块及位置数据在该间接块中存放的位置。
有了间接块需要的数据,我们在结合数据本身需要的磁盘空间,就可以计算出本次请求需要申请的磁盘空间。最后就是根据这些来分配磁盘空间了。下面我们分布详细介绍一下各个流程的实现细节。
计算存储路径
所谓计算存储路径是指计文件数据逻辑地址在间接块中存储的物理地址表示。仍然以上述图3为例,对于二级间接块可存储的情况,在分配磁盘空间之前需要计算出其在一级间接块和二级间接块中的位置。这样,在后续的数据查找流程,根据这些间接块中存储的地址信息就可以找到文件某个位置的数据在磁盘的物理地址。
函数ext2_block_to_path
用于计算数据在间接块数组中存放的路径。该函数的功能主要是根据逻辑地址计算出在深度及每一层的位置。前文我们已经知道文件数据的放置方式,结合图3以比较清楚的理解本函数的代码。这里根据数据的逻辑地址分为4种情况,分别如下:
- ** 不需要间接块 **: 也就是数据目的位置(以文件块大小为单位)在12以内,则说明是直接引用,不需要间接块,此时在数组的前12个元素中的一个。
- ** 一级间接块 **: 数据范围在一级间接块可表示的范围内,此时表示路径的数组的第一个元素是inode数组中的第12个元素,而第二个元素则是在间接块中的具体位置。比如i_block是18,此时通过直接寻址无法满足要求,因此需要一级间接块。这样,offsets中第一个元素的值是12,表示是一级间接块;offsets的第二个元素是6,因为直接索引可以表示12个数据块,因此在间接块中的分别可以存储从第13到256+12的数据范围,对于位置为18的数据在间接块的位置就是6。
- ** 二三级间接块 **: 以此类推,根据逻辑地址的大小可能会需要二级甚至三级间接块,依照这种算法可以计算出每一级间接块的位置。这里不在赘述。
下面是本函数的代码,本文加了一些注释,代码本身比较简单,本文不在赘述。这里需要注意的是,除了返回深度和每一层的位置外,还会返回在最后的间接块上可容纳的地址的数量。比如计算出来在最后一级间接块的位置是250,那么最多可容纳6个地址。
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;
}
获取存储路径
前面逻辑计算出了深度和每一级间接块应该存储的信息,但具体的间接块目前处于什么情况并不清楚。仍然以上述二级间接块为例,可能会出现如下几种情况:
- 用户访问的数据位置所需要的间接块已经全部分配了
- 一级间接块已经具备了,但二级间接块不具备
- 所有间接块都不具备
因此,这里的工作就是根据当前信息及上一步计算出的信息进行综合判断,确定已经具备的间接块,并返回关键信息,为后续流程分配磁盘空间做准备。具体实现在函数ext2_get_branch中。该函数的输入是上一个函数的计算结果,输出是需要分配的间接块的信息(chain)。
static Indirect *ext2_get_branch(struct inode *inode,
int depth,
int *offsets,
Indirect chain[4],
int *err)
分配磁盘空间
完成间接块情况分析之后,再经过简单的计算,就可以计算出总共需要分配的磁盘块的数量。然后就可以调用ext2_alloc_branch函数分配磁盘空间了。该函数主要调用了2个函数,具体如图3所示。其中ext2_alloc_blocks用户分配磁盘块,本质是将管理磁盘空间的位图的对应位进行置位操作;另外一个函数sb_getblk用于从磁盘读取该块的数据,并进行初始化。
初始化的目的比较明确,因为间接块用来存储地址信息,如果是从磁盘读取的新间接块数据可能是未知值,因此需要清零操作,并且完成本请求地址的初始化操作。
至此,磁盘空间分配的主要流程执行完成,但仍然有一些小的处理流程,比如更新inode中的记录最后一次分配位置、更新时间和将inode变脏等等。这些细节读者可以自行阅读代码理解。
另外,为了简化,本文有些内容没有介绍,比如预留窗口,还有就是跨越间接块的处理等问题。这些问题相对复杂一些,我们后续文章会继续介绍。