因为要给给文件系统增加数据索引功能,所以对文件系统进行了不少的修改(主要是仿照i_zone[]的方式进行索引数据的管理和存储),但是考虑到文件还有删除或者截断操作,我们就必须面对这个问题,看看文件系统是如何处理的。今天我们来看看ext2和minix是如何截断文件的:即,如何删除那些我们不需要的数据块了。
所谓的截断文件指的是:一个特定长度的文件,我们从某个位置开始丢弃后面的数据,之前的数据依然保留。对具体文件系统来说,截断数据主要意味着两件事情:1. 文件大小发生变化;2. 文件被截断部分之前占用的数据块(对ext2和minix来说可能还包含间接块)释放,让其他文件可以使用。
看看我们今天要分析函数的名字就能知道,该函数主要处理如何截断文件数据。前面我们说过,所谓的截断,其主要工作就是释放文件占用的磁盘块,但这里并不是释放文件所有的磁盘块,我们只释放从被截断位置处至文件结束这部分数据块,当然由于ext2和minix特殊的映射方式,我们不仅需要截断数据块,还可能包括映射这些数据块的间接索引块。
__ext2_truncate_blocks解析
同样,在下面的解析过程中,我们通过举一个事例来理解该函数的实现,照例,我们将函数的实现罗列如下:
//这里要注意,有很多文件系统的truncate函数的名字为truncate,且参数只有inode一个。因为没有offset这个参数,结果理解起来非常费劲。
static void __ext2_truncate_blocks(struct inode *inode, loff_t offset)
{
__le32 *i_data = EXT2_I(inode)->i_data;
struct ext2_inode_info *ei = EXT2_I(inode);
int addr_per_block = EXT2_ADDR_PER_BLOCK(inode->i_sb);
int offsets[4];
Indirect chain[4];
Indirect *partial;
__le32 nr = 0;
int n;
long iblock;
unsigned blocksize;
blocksize = inode->i_sb->s_blocksize;
//这里是有点儿讲究的
//offset + blocksize - 1主要是考虑到从offset所在的下一块开始
//释放,因为如果offset != 0意味着offset 所在的块是不能
//被释放的,只能从下一个块开始释放
iblock = (offset + blocksize-1) >> EXT2_BLOCK_SIZE_BITS(inode->i_sb);
n = ext2_block_to_path(inode, iblock, offsets, NULL);
if (n == 0)
return;
/*
* From here we block out all ext2_get_block() callers who want to
* modify the block allocation tree.
*/
mutex_lock(&ei->truncate_mutex);
//如果截断处从直接映射块开始
//那么我们首先释放直接块
//再来释放间接块映射的那部分
if (n == 1) {
ext2_free_data(inode, i_data+offsets[0],
i_data + EXT2_NDIR_BLOCKS);
goto do_indirects;
}
partial = ext2_find_shared(inode, n, offsets, chain, &nr);
/* Kill the top of shared branch (already detached) */
if (nr) {
if (partial == chain)
mark_inode_dirty(inode);
else
mark_buffer_dirty_inode(partial->bh, inode);
ext2_free_branches(inode, &nr, &nr+1, (chain+n-1) - partial);
}
/* Clear the ends of indirect blocks on the shared branch */
while (partial > chain) {
ext2_free_branches(inode,
partial->p + 1,
(__le32*)partial->bh->b_data+addr_per_block,
(chain+n-1) - partial);
mark_buffer_dirty_inode(partial->bh, inode);
brelse (partial->bh);
partial--;
}
do_indirects:
/* Kill the remaining (whole) subtrees */
switch (offsets[0]) {
default:
nr = i_data[EXT2_IND_BLOCK];
if (nr) {
i_data[EXT2_IND_BLOCK] = 0;
mark_inode_dirty(inode);
ext2_free_branches(inode, &nr, &nr+1, 1);
}
case EXT2_IND_BLOCK:
nr = i_data[EXT2_DIND_BLOCK];
if (nr) {
i_data[EXT2_DIND_BLOCK] = 0;
mark_inode_dirty(inode);
ext2_free_branches(inode, &nr, &nr+1, 2);
}
case EXT2_DIND_BLOCK:
nr = i_data[EXT2_TIND_BLOCK];
if (nr) {
i_data[EXT2_TIND_BLOCK] = 0;
mark_inode_dirty(inode);
ext2_free_branches(inode, &nr, &nr+1, 3);
}
case EXT2_TIND_BLOCK:
;
}
ext2_discard_reservation(inode);
mutex_unlock(&ei->truncate_mutex);
}
让我们首先看看这个函数的参数:
- inode:代表被截断的文件;
- offset:从offset该位置开始截断,注意offset以字节为单位,后面必须根据offset计算其所在块号。
举例来理解这个函数的执行流程,我们将这部分代码分为几个大的段落,首先来看看第一部分在干啥:
//这里是有点儿讲究的
//offset + blocksize - 1主要是考虑到从offset所在的下一块开始
//释放,因为如果offset != 0意味着offset 所在的块是不能
//被释放的,只能从下一个块开始释放
iblock = (offset + blocksize-1) >> EXT2_BLOCK_SIZE_BITS(inode->i_sb);
n = ext2_block_to_path(inode, iblock, offsets, NULL);
if (n == 0)
return;
/*
* From here we block out all ext2_get_block() callers who want to
* modify the block allocation tree.
*/
mutex_lock(&ei->truncate_mutex);
//如果截断处从直接映射块开始
//那么我们首先释放直接块
//再来释放间接块映射的那部分
if (n == 1) {
ext2_free_data(inode, i_data+offsets[0],
i_data + EXT2_NDIR_BLOCKS);
goto do_indirects;
}
这是第一种情况:被截断文件偏移offset处于直接映射处,这种截断的处理方式应该算是比较简单的,我们只需要首先释放直接映射的数据块,然后再释放所有的间接映射方式所映射的数据块以及间接映射块即可。所以在代码中有个判断:
(if(n == 1)) ext2_free_data();goto do_indirects;
这个判断即是处理上述情形,如果映射深度为1,首先释放那些被直接映射方式映射的直接数据块。处理完成以后,我们就可以释放被间接块映射的数据块和间接块了(如果文件足够大,需要使用间接映射才能索引),所以一个goto转到了do_indirects。下面的一个事例说明了这种情况。
假如当前文件大小为300KB,文件系统块大小为1KB。因此,我们知道必须使用二级间接映射才可以索引整个文件,其中,直接映射方式能索引12KB大小文件数据,一级间接索引可索引256KB文件数据,剩下的(300KB-12KB - 256KB = 32KB)数据必须通过二级间接索引方式来索引,整个映射关系如下图所示(右下角的那个绿色的一级间接块不应该存在,因为文件当前只有300个块,根本就没有用到那个一级间接块,所以并没有分配):
上图是当前文件的映射方式,我们可以清楚地看到,只有使用二级间接索引的方式才能索引到所有文件数据块。现在假如我们要将文件截断成4KB大小,应该怎么做?
根据上面的代码,在本例中,我们很容易知道我们需要回收哪些磁盘块:5-300这296个数据块和1个一级间接映射的映射块和二间接映射中的1个一级间接映射和1个二级间接映射块,共299个block。按照另外一种划分方式,就是:1. 直接块Block5 ~ Block12共8个,这些是直接映射方式使用的磁盘块;2. 一级间接索引方式索引的数据块和间接索引快,上图中为Block13 ~ Block268,同时还有一级间接索引块,共257个;3. 二级间接索引块索引的数据块Block269 ~ Block300,加上1个二级间接索引块和1个一级间接索引块。 总的来说,我们一共需要回收的数据块+索引块有296+1+1+1 = 299个。下图中用X代表了我们需要释放的磁盘块。(下面的图不是非常准确,因为只有前Block1~ Block4是被保留的,Block5开始就要被释放了,但由于空间关系,没有画出来,同时右下角的那个绿色一级间接块并不存在,所以不需要删除,特此注释)
那假如说我们要截断的部分不是直接映射方式能索引到的,该怎么办呢?我们会直接进入下面这个分支:
partial = ext2_find_shared(inode, n, offsets, chain, &nr);
/* Kill the top of shared branch (already detached) */
if (nr) {
if (partial == chain)
mark_inode_dirty(inode);
else
mark_buffer_dirty_inode(partial->bh, inode);
ext2_free_branches(inode, &nr, &nr+1, (chain+n-1) - partial);
}
/* Clear the ends of indirect blocks on the shared branch */
while (partial > chain) {
ext2_free_branches(inode,
partial->p + 1,
(__le32*)partial->bh->b_data+addr_per_block,
(chain+n-1) - partial);
mark_buffer_dirty_inode(partial->bh, inode);
brelse (partial->bh);
partial--;
}
首先我们来以一个例子思考下对于这种情况我们该怎么处理,一旦明白了怎么处理,代码我们也就懂了一半(实际上,没有示例的介绍,find_shared()这个函数,我看了一个下午,也没看懂!!!可见理论和实践相结合是多么的重要!!!)。
假如当前文件大小为1MB,也就是1024个block,此时必须使用二级间接索引方式,如下图所示(由于画图空间的考虑,我们只画出了二级索引的映射情况,将直接映射方式和一级间接映射没有画出,实际的offsets[3]={13,2,243},特此说明)
现在假如我要从882KB处开始截断,即从Block882处开始(offsets[3]={13,2,102}),以后(包括block882)所有的文件数据块(Block882 ~ Block1023)都将被释放,在前面的函数中我们计算了Block882的映射深度为3(用到了二级间接映射),offsets[0]是13。因此,我们需要释放的磁盘块索引路径一定是从i_data[13]处开始。一级映射和直接块映射路径上面的索引块和直接数据块我们无需释放(都在block882之前)。而且我们需要明白,该二级映射路径上也不是所有的块都需要被释放(因为882之前的一部分block,也就是(1)从780开始的那102个block和882到1023的这142个block共用了二级间接块和一级间接块;(2)从block268到block779这512个block和block882-block1023这142个block共用了二级间接块),本例中,只有上图标X的索引块或者数据块才需要被释放,也就是只有这。所以我想我们首先第一步得确认我们需要回收哪些索引块和数据块。
其实释放可以按照如下思路来进行:首先我们释放二级映射路径上的需要释放的磁盘块,对本例来说,释放Block882 ~ Block1023,然后释放该路径上必须释放的间接块,本例中因为我们二级间接映射路径上的一级间接块只分配了三个,而且我们截断的起始块(882)正好是位于最后一个间接块映射范围,因此,我们不必释放任何一个一级间接块,同样,二级间接块我们也不能释放。
因此,以上的函数段的作用总结起来就是:将被截断块所在的映射路径上需要被释放的直接数据块或者间接块回收。但是注意:这里我们只回收某一级映射路径上磁盘块(截断起始块所在的映射路径,如本例中的二级映射路径)。
但是请注意:我们上面只是处理了某一级映射路径上的磁盘块,如上面代码中的第一段中的直接映射块处理,第二段中的二级映射路径上磁盘块的释放。但如果文件足够大,那么我们还得需要处理接下来映射路径上的磁盘块释放,如上面的第二个例子,假如文件为1G,那么我们还需要三级映射,而上面的代码只处理了二级映射路径上的磁盘块,所以我们还得处理三级映射路径上的磁盘块释放。
接下来的第三部分代码就是干这个事情的,需要注意的是:这部分代码处理的是某个映射路径上所有被分配的磁盘块,也意味着该路径上的所有分配的磁盘块均将被释放。其实现如下:
do_indirects:
/* Kill the remaining (whole) subtrees */
switch (offsets[0]) {
default:
nr = i_data[EXT2_IND_BLOCK];
if (nr) {
i_data[EXT2_IND_BLOCK] = 0;
mark_inode_dirty(inode);
ext2_free_branches(inode, &nr, &nr+1, 1);
}
case EXT2_IND_BLOCK:
nr = i_data[EXT2_DIND_BLOCK];
if (nr) {
i_data[EXT2_DIND_BLOCK] = 0;
mark_inode_dirty(inode);
ext2_free_branches(inode, &nr, &nr+1, 2);
}
case EXT2_DIND_BLOCK:
nr = i_data[EXT2_TIND_BLOCK];
if (nr) {
i_data[EXT2_TIND_BLOCK] = 0;
mark_inode_dirty(inode);
ext2_free_branches(inode, &nr, &nr+1, 3);
}
case EXT2_TIND_BLOCK:
;
}
ext2_discard_reservation(inode);
mutex_unlock(&ei->truncate_mutex);
}
这段代码的主要作用是释放某个映射路径上的所有已分配磁盘块。offsets[0]记录了该映射路径的起始块号,但是我们需要注意一点:我们要映射的路径是位于offsets[0]的下一级映射,因为offsets[0]所在的映射路径上的需要被删除的磁盘块已经被释放了。如上面的事例2,offsets[0]=13也即二级映射路径上的磁盘块已经处理过了,我们需要处理的是三级映射路径上被分配磁盘块的释放。所以你看上面代码的时候一定需要注意这点。
其实,这段代码实现也比较简单了,判断如果这一级映射路径存在(即nr != 0),那么就调用ext2_free_branches()来释放这一级映射路径上分配过的磁盘块。而且可以事先透露下,ext2_free_branches()是用递归实现的。即从该映射路径起始一直会释放到映射路径结束的地方。关于该函数的实现,我觉得是没必要再专门写一篇博客来阐述了,理解了整个流程以后,读起来其实算是比较容易的。
至此,我们算是理解了ext2文件系统的截断流程,在阅读代码的时候,一定得事先考虑清楚其中需要处理的问题,有了截断的逻辑,我们才能更容易地理解这部分代码。
minix的truncate()
/****
* 这个函数主要处理如何截断文件文件数据,解决如何删除那些我们不需要的数据块的问题。所谓的截断文件指的是:一个特定长度的文件,我们从某个位置开始丢弃后面的数据,之前的数据依然保留。对具体文件系统来说,截断数据主要意味着两件事情:1. 文件大小发生变化;2. 文件被截断部分之前占用的数据块(包括存储blockid的那些i_zone[]管理的元数据块)释放,让其他文件可以使用。对于我们的索引系统来说,应该还要删除i_index管理的那些block。这个函数放到itree.h中,不要放在这里了。另外,这个函数本身没有带什么注释,所以我当初理解的时候浪费了很多时间。这里,很让人迷惑的是iblock的计算方法,它使用的是inode->i_size的位置为起点计算要删除的block的起点。因为我们的理解是inode中的i_size字段表示的是文件当前的长度,也就是文件的终止位置,如果从这个地方开始截断,那不相当于什么都不做吗?如果将本函数理解为删除所有的数据,那后面 根据n的数值进行删除的操作又有问题,比如n=1时,它删除的是idata+offset[0]--idata+DIRECT所管理的那些block。后来,经过不断探索,我发现,不能把i_size当成文件的真实长度,而是在文件写入失败或者文件长度超过限制的时候,我们是要先修改i_size的,将之设定为预期的长度,然后调用truncate()将超出预期长度的内容及对应的元数据删掉。其实这个函数让人难以理解是因为它实际上将需要截断的位置这个参数给隐藏掉了,如果再加1个截断位置的变了pos,来替换inode—>i_size,估计就容易理解了。
*/
static inline void truncate (struct inode * inode)
{
struct super_block *sb = inode->i_sb;
indexblock_t *idata = i_data(inode);
int offsets[DEPTH];
Indirect chain[DEPTH];
Indirect *partial;
indexblock_t nr = 0;
int n;
int first_whole;
long iblock;
//这里不要
iblock = (inode->i_size-> + sb->s_blocksize -1) >> sb->s_blocksize_bits;//inode数据占用多少个block
block_truncate_page(inode->i_mapping, inode->i_size, get_block);//这一步应该是删除page cache中的内容(根据基树进行查找,然后清理)
//后面应该是删除i_zone管理的那些block,也就是元数据占用的block
n = block_to_path(inode, iblock, offsets);//找到截断开始的第一个数据块的路径,填写到offsets[],则offsets[]记录的偏移量为需要删除的文件内容的起始位置,如offsets[]={9,1,10,2}则从{9,1,10,2}表示的block开始,之后的这些存储数据的block以及存储这些blockid的block,都需要清理掉
if (!n)
return;
//如果截断的起始位置位于直接寻址区
if (n == 1) {//先处理直接寻址管理的那部分元数据
free_data(inode, idata+offsets[0], idata + DIRECT);
first_whole = 0;
goto do_indirects;//再处理间接寻址管理的那些元数据
}
first_whole = offsets[0] + 1 - DIRECT;
partial = find_shared(inode, n, offsets, chain, &nr);
if (nr) {
if (partial == chain)
mark_inode_dirty(inode);
else
mark_buffer_dirty_inode(partial->bh, inode);
free_branches(inode, &nr, &nr+1, (chain+n-1) - partial);
}
/* Clear the ends of indirect blocks on the shared branch */
while (partial > chain) {
free_branches(inode, partial->p + 1, block_end(partial->bh),
(chain+n-1) - partial);
mark_buffer_dirty_inode(partial->bh, inode);
brelse (partial->bh);
partial--;
}
do_indirects:
/* Kill the remaining (whole) subtrees */
while (first_whole < DEPTH-1) {
nr = idata[DIRECT+first_whole];
if (nr) {
idata[DIRECT+first_whole] = 0;
mark_inode_dirty(inode);
free_branches(inode, &nr, &nr+1, first_whole+1);
}
first_whole++;
}
inode->i_mtime = inode->i_ctime = current_time(inode);
mark_inode_dirty(inode);
}