当内核要分配一个新的数据块来保存Ext2普通文件的数据时,就调用ext2_get_block( )函数。这个函数依次处理在“数据块寻址”部分所描述的那些数据结构,并在必要时调用ext2_alloc_block( )函数在Ext2分区实际搜索一个空闲的块。
为了减少文件的碎片,Ext2文件系统尽力在已分配给文件的最后一个块附近找一个新块分配给该文件。如果失败,Ext2文件系统又在包含这个文件索引节点的块组中搜寻一个新的块。作为最后一个办法,可以从其它一个块组中获得空闲块。
Ext2文件系统使用数据块的预分配策略。文件并不仅仅获得所需要的块,而是获得一组多达8个邻接的块。ext2_inode_info结构的i_prealloc_count域存放预分配给某一文件但还没有使用的数据块数,而i_prealloc_block域存放下一次要使用的预分配块的逻辑块号。当下列情况发生时,即文件被关闭时,文件被删除时,或关于引发块预分配的写操作而言,有一个写操作不是顺序的时候,就释放预分配但一直没有使用的块
下面我们来看一下ext2_get_block( )函数,其代码在fs/ext2/inode.c中:
/*
* Allocation strategy is simple: if we have to allocate something, we will
* have to go the whole way to leaf. So let's do it before attaching anything
* to tree, set linkage between the newborn blocks, write them if sync is
* required, recheck the path, free and repeat if check fails, otherwise
* set the last missing link (that will protect us from any truncate-generated
* removals - all blocks on the path are immune now) and possibly force the
* write on the parent block.
* That has a nice additional property: no special recovery from the failed
* allocations is needed - we simply release blocks and do not touch anything
* reachable from inode.
*/
static int ext2_get_block(struct inode *inode, long iblock, struct buffer_head *bh_result, int create)
{
int err = -EIO;
int offsets[4];
Indirect chain[4];
Indirect *partial;
unsigned long goal;
int left;
int depth = ext2_block_to_path(inode, iblock, offsets);
if (depth == 0)
goto out;
lock_kernel();
reread:
partial = ext2_get_branch(inode, depth, offsets, chain, &err);
/* Simplest case - block found, no allocation needed */
if (!partial) {
got_it:
bh_result->b_dev = inode->i_dev;
bh_result->b_blocknr = le32_to_cpu(chain[depth-1].key);
bh_result->b_state |= (1UL << BH_Mapped);
/* Clean up and exit */
partial = chain+depth-1; /* the whole chain */
goto cleanup;
}
/* Next simple case - plain lookup or failed read of indirect block */
if (!create || err == -EIO) {
cleanup:
while (partial > chain) {
brelse(partial->bh);
partial--;
}
unlock_kernel();
out:
return err;
}
/*
* Indirect block might be removed by truncate while we were
* reading it. Handling of that case (forget what we've got and
* reread) is taken out of the main path.
*/
if (err == -EAGAIN)
goto changed;
if (ext2_find_goal(inode, iblock, chain, partial, &goal) < 0)
goto changed;
left = (chain + depth) - partial;
err = ext2_alloc_branch(inode, left, goal,
offsets+(partial-chain), partial);
if (err)
goto cleanup;
if (ext2_splice_branch(inode, iblock, chain, partial, left) < 0)
goto changed;
bh_result->b_state |= (1UL << BH_New);
goto got_it;
changed:
while (partial > chain) {
brelse(partial->bh);
partial--;
}
goto reread;
}
对这个函数,源代码的作者对分配策略给出了简单的注释,相信读者从会中领略一二。函数的参数inode指向文件的inode结构;参数iblock表示文件中的逻辑块号;参数bh_result为指向缓冲区首部的指针,buffer_head结构已在上一章做了介绍;参数create表示是否需要创建。
其中Indirect结构在同一文件中定义如下:
typedef struct {
u32 *p;
u32 key;
struct buffer_head *bh;
} Indirect
用数组chain[4]描述四种不同的索引,即直接索引、一级间接索引、二级间接索引、三级间接索引。举例说明这个结构各个域的含义。如果文件内的块号为8,则不需要间接索引,所以只用chain[0]一个Indirect结构,p指向直接索引表下标为8处,即&inode->u.ext2_i.i_data[8];而key则持有该表项的内容,即文件块号所对应的设备上的块号(类似于逻辑页面号与物理页面号的对应关系);bh为NULL,因为没有用于间接索引的块。如果文件内的块号为20,则需要一次间接索引,索引要用chian[0]和chain[1]两个表项。第一个表项chian[0] 中,指针bh仍为NULL,因为这一层没有用于间接索引的数据块;指针p指向&inode->u.ext2_i.i_data[12],即间接索引的表项;而key持有该项的内容,即对应设备的块号。chain[1]中的指针bh则指向进行间接索引的块所在的缓冲区,这个缓冲区的内容就是用作间接索引的一个整数数组,而p指向这个数组中下标为8处,而key则持有该项的内容。这样,根据具体索引的深度depth,数组chain[]中的最后一个元素,即chain[depth-1].key,总是持有目标数据块的物理块号。而从chain[]中第一个元素chain[0]到具体索引的最后一个元素chain[depth-1],则提供了具体索引的整个路径,构成了一条索引链,这也是数据名chain的由来。
了解了以上基本内容后,我们来看ext2_get_block()函数的具体实现代码:
· 首先调用ext2_block_to_path()函数,根据文件内的逻辑块号iblock计算出这个数据块落在哪个索引区间,要采用几重索引(1表示直接)。如果返回值为0,表示出错,因为文件内块号与设备上块号之间至少也得有一次索引。出错的原因可能是文件内块号太大或为负值。
· ext2_get_branch()函数深化从ext2_block_to_path()所取得的结果,而这合在一起基本上完成了从文件内块号到设备上块号的映射。从ext2_get_branch()返回的值有两种可能。一是,如果顺利完成了映射则返回值为NULL。二是,如果在某一索引级发现索引表内的相应表项为0,则说明这个数据块原来并不存在,现在因为写操作而需要扩充文件的大小。此时,返回指向Indirect结构的指针,表示映射在此断裂。此外,如果映射的过程中出错,例如,读数据块失败,则通过err返回一个出错代码。
· 如果顺利完成了映射,就把所得结果填入缓冲区结构bh_result中,然后把映射过程中读入的缓冲区(用于间接索引)全部释放。
· 可是,如果ext2_get_branch()返回一个非0指针,那就说明映射在某一索引级上断裂了。根据映射的深度和断裂的位置,这个数据块也许是个用于间接索引的数据块,也许是最终的数据块。不管怎样,此时都应该为相应的数据块分配空间。
· 要分配空间,首先应该确定从物理设备上何处读取目标块。根据分配算法,所分配的数据块应该与上一次已分配的数据块在设备上连续存放。为此目的,在ext2_inode_info结构中设置了两个域i_next_alloc_block和i_next_alloc_goal。前者用来记录下一次要分配的文件内块号,而后者则用来记录希望下一次能分配的设备上的块号。在正常情况下,对文件的扩充是顺序的,因此,每次所分配的文件内块号都与前一次的连续,而理想上来说,设备上的块号也同样连续,二者平行地向前推进。这种理想的“建议块号”就是由ext2_find_goal()函数来找的。
· 设备上具体物理块的分配,以及文件内数据块与物理块之间映射的建立,都是调用ext2_alloc_branch()函数完成的。调用之前,先要算出还有几级索引需要建立。
· 从ext2_alloc_branch()返回以后,我们已经从设备上分配了所需的数据块,包括用于间接索引的中间数据块。但是,原先映射开始断开的最高层上所分配的数据块号只是记录了其Indirect结构中的key域,却并没有写入相应的索引表中。现在,就要把断开的“树枝”接到整个索引树上,同时,还需要对文件所属inode结构中的有关内容做一些调整。这些操作都是由ext2_splice_branch()函数完成。
到此为止,万事具备,则转到标号got_it处,把映射后的数据块连同设备号置入bh_result所指的缓冲区结构中,这就完成了数据块的分配。