其实硬盘上的文件和内存中的还是有几分相像的,刚开始看的时候复杂程度严重的查处了想象,看完之后很多的操作都是比较经典并且看起来理所当然。硬盘不像内存,随机访问是需要相当大的代价的,所以在存放数据的时候就需要把顺序访问的数据放在一起,而一些经常用到的数据,比如ext2_super_block、ext2_group_desc则每个组中都保存一份,不然每次在访问组的时候要去一个固定的位置去查找超级块的信息,那就疯掉了。
以前问过别人为什么要由组块这一层结构。因为它并没有保存什么比较有用的信息,更或者说是组块开起来并没有设么必要存在。为了了解这个结构的必要性,先想象它为Ext2做了些什么:把一个很大的空间分成了一块块比较小的空间。那这样有什么好处呢?每个块中都由自己数据块的bitmap,这个经常访问到的数据结构总是在附近,而且同一个文件在分配的时候总是尝试在相同的组块中,这样会避免磁盘碎片。
如果没有简介块的话,在磁盘上找一个文件中的一块是很容易的事情。因为有间接块的存在给我们带来了很多的麻烦:用到了几个间接块?间接块存在么?等等。但是如果没有间接块的话在我们要保存大文件的时候浪费的空间将是灾难性的。好了,现在来看在找块的时候发生了什么:
static int ext2_get_blocks(struct inode *inode,
sector_t iblock, unsigned long maxblocks,
struct buffer_head *bh_result,
int create)
其中的iblock是inode中的第几块(不然我们也不需要费这么大劲了:))。磁盘文件可能用到了间接块,那我们怎么知道各个间接块中的offset?调用下面这个函数就可以了,比内存中分页麻烦的地方在于如果第一层够用了就不用地二层了。但是如果第一层不够用我们要把第一层用完,然后把剩下的部分再放到第二层去。最后把目标在各层中的偏移量保存在offset数组中,这些工作由下面的函数完成。
static int ext2_block_to_path(struct inode *inode,
long i_block, int offsets[4], int *boundary)
到目前为止,我们还没有和硬盘打交道。不和硬盘打交道怎么能读取到块呢?不过这也是麻烦的开始!下面的函数沿着刚才计算出来的偏移量来一次读取块,并且把结果保存在chain数组中。如果最后读到了数据块,那么我们可能已经成功了(so easy)。
static Indirect *ext2_get_branch(struct inode *inode,
int depth,int *offsets,
Indirect chain[4],int *err)
如果返回的结果是NULL,那么说明读到的最后一个就是想要的数据块。但是就算如此也不能简单地认为成功了,因为可能有别的线程在我们读取的时候已经对这个文件进行了截断操作。所以还要检查一下。
if (!partial) {
first_block = le32_to_cpu(chain[depth - 1].key);
clear_buffer_new(bh_result); /* What's this do? */
count++;
while (count < maxblocks && count <= blocks_to_boundary) {
ext2_fsblk_t blk;
if (!verify_chain(chain, partial)) {
count = 0;
goto changed;
}
blk = le32_to_cpu(*(chain[depth-1].p + count));
if (blk == first_block + count)
count++;
else
break;
}
goto got_it;
}
至于这里count的计算感觉一些翻译的书上面有点问题的,建议大家在看的时候注意一下。这里的first_block是最后一个块的key(也就是偏移量),而blk是p向后移动count个位置后的地址上保存的值。那p到底指向什么地方?
static inline void add_chain(Indirect *p, struct buffer_head *bh, __le32 *v)
{
p->key = *(p->p = v);
p->bh = bh;
}
可以看到p保存的是指向bh中相对偏移量的那个位置的指针,而key只是保存偏移量。在count++的时候再次比较的话,bh中的位置在向后移动,如果他们的值还相等,那么这些逻辑上连续的数据块是不是在物理上面也是连续的?好了,言归正传。到了下面的代码中就说明需要分配了。那么要对分配做好准备。如果是普通文件但此时该文件是没有预分配的统计的结构,那么就需要分配了。
if (S_ISREG(inode->i_mode) && (!ei->i_block_alloc_info))
ext2_init_block_alloc_info(inode);
既然要分配块,那这个块分配在哪里好?如果刚才分配过一个块,和我们现在要访问的数据块在逻辑上是相邻的,那现在分配在它的物理块的后面就好了(当然它后面的这个物理块不一定是空闲的)。能有这么一个位置当然是非常好的事情,没有怎么办呢?在这个间接块中找到前一块的物理块的位置,如果还是不行,那么我们就之后把它离间接块近一点,因为在访问数据块之前总是要访问间接块的,把它们放的近一点总是没错的吧?最后,还是不行,那就把它们随机一下,保证在同一个柱面中就可以了。既然是再计划着分配块,那么有多少块需要分配?如下:
indirect_blks = (chain + depth) - partial - 1;
count = ext2_blks_to_allocate(partial, indirect_blks,
maxblocks, blocks_to_boundary);
要分配的块包括两部分,一部分用于建立间接块,另一部分是数据块。它们总是不满足于只是分配一个数据块(这是最低要求),而是尝试分配数据块把该间接块中的其他部分也搞定了。现在得到的信息都是不确定的(确定的信息总是在位图中),下面进入真正的分配的过程:
static int ext2_alloc_branch(struct inode *inode,
int indirect_blks, int *blks, ext2_fsblk_t goal,
int *offsets, Indirect *branch)
也许令大家失望了,在这个函数中并没有看到具体分配的过程(这毕竟是比较复杂的),其中只是简单的调用函数搞定了,剩下的工作是干什么的?下面是具体的代码:
num = ext2_alloc_blocks(inode, goal, indirect_blks,
*blks, new_blocks, &err);
if (err)
return err;
branch[0].key = cpu_to_le32(new_blocks[0]);
for (n = 1; n <= indirect_blks; n++) {
bh = sb_getblk(inode->i_sb, new_blocks[n-1]);
branch[n].bh = bh;
lock_buffer(bh);
memset(bh->b_data, 0, blocksize);
branch[n].p = (__le32 *) bh->b_data + offsets[n];
branch[n].key = cpu_to_le32(new_blocks[n]);
*branch[n].p = branch[n].key;
if ( n == indirect_blks) {
current_block = new_blocks[n];
for (i=1; i < num; i++)
*(branch[n].p + i) = cpu_to_le32(++current_block);
}
set_buffer_uptodate(bh);
unlock_buffer(bh);
mark_buffer_dirty_inode(bh, inode);
if (S_ISDIR(inode->i_mode) && IS_DIRSYNC(inode))
sync_dirty_buffer(bh);
}
*blks = num;
好了,现在分析一下这段代码做了些什么事情?ext2_alloc_blocks一次性地就把我们需要的数据块都申请到了(至于它是怎么做的在后面会讲)。branch[0].key保存了分配到的第一个块的位置,开始处理所有的间接块。取得bh,然后把bh中的所有的值(除了要这次要用到的位置)设置为0。如果处理到了最后一个间接块(再要加进来的都是数据块了),那么把剩余的current_block后面的块都弄进来,多多益善。说了这么多,关键还是怎么分配,在函数ext2_alloc_blocks中:
static int ext2_alloc_blocks(struct inode *inode,
ext2_fsblk_t goal, int indirect_blks, int blks,
ext2_fsblk_t new_blocks[4], int *err)
到了这里感觉和网络编程的时候很像,看似有一个函数我们调用以此就可以完成了传输,但是,下面却有一个循环不停地传不停地尝试直到全部传完。下面的代码也在做类似的事情:
target = blks + indirect_blks;
while (1) {
count = target;
current_block = ext2_new_blocks(inode,goal,&count,err);
if (*err)
goto failed_out;
target -= count;
while (index < indirect_blks && count) {
new_blocks[index++] = current_block++;
count--;
}
if (count > 0)
break;
}
new_blocks[index] = current_block;
到最后了,会发现很多创建的工作都是再ext2_new_blocks中完成的,也就是处理一下预留窗口或者是查看位图,情况很多但不怎么有趣。改天有空再补充。