在之前的多篇博客中我们都比较详细地阐述了ext2_get_block()路径中各个函数的实现原理,今天我们来关注下ext2_allocate_branch()的实现细节,这个函数是非常重要的,而且理解起来也没那么简单,虽然说函数的功能一句话就可能说完:为数据块建立映射路径。

要理解这个函数,我们首先得理解ext2文件系统的逻辑块至物理块的映射方法(这个在之前已经作了比较仔细的描述)。同样,在分析这个函数的时候,我们也通过举例来说明,这样理解起来会更具体。

让我们假设这样一种场景,假如目前文件的大小是2KB,但我们现在seek到了268KB位置处,写入2个数据块。这样是可以的,即产生了所谓的文件空洞。至于为什么seek到这样一个特殊的位置,也是有代表性的,下面我会说明。

回想ext2文件系统的逻辑块至物理块的映射关系我们知道,269KB此处需要采用2级间接映射,而且正好就从这个地放起开始需要采用二级映射的方式(如果文件系统块大小为1KB的话,计算很简单,直接索引方式可管理12KB数据,一级索引方式可管理256KB数据)。在这种场景下进入该函数,让我们看看会发生点什么。

static int ext2_alloc_branch(struct inode *inode,
            int indirect_blks, int *blks, ext2_fsblk_t goal,
            int *offsets, Indirect *branch)
{
    int blocksize = inode->i_sb->s_blocksize;
    int i, n = 0;
    int err = 0;
    struct buffer_head *bh;
    int num;
    ext2_fsblk_t new_blocks[4];
    ext2_fsblk_t current_block;

    /* 首先分配直接块和间接块*/
    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]);
    /*
     * metadata blocks and data blocks are allocated.
     */
    for (n = 1; n <= indirect_blks;  n++) {
        /*
         * Get buffer_head for parent block, zero it out
         * and set the pointer to new one, then send
         * parent to disk.
         */
        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];
            /*
             * End of chain, update the last new metablock of
             * the chain to point to the new allocated
             * data blocks numbers
             */
            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);
        /* We used to sync bh here if IS_SYNC(inode).
         * But we now rely upon generic_write_sync()
         * and b_inode_buffers.  But not for directories.
         */
        if (S_ISDIR(inode->i_mode) && IS_DIRSYNC(inode))
            sync_dirty_buffer(bh);
    }
    *blks = num;
    return err;
}

我们首先不妨看看这个函数的参数代表何意义:

  • inode:自然代表了该文件;
  • indirect_blocks:需要分配多少个间接块,本例中需要采用二级映射,而且该映射路径上的所有间接块均未分配,因此,需要分配2个间接块;
  • blks:需要分配的数据块数量,本例中也为2;
  • goal:调用者计算的建议最佳分配块号,这个的计算已经在之前的博客中说明过;
  • offsets[]:保存了每一级映射路径上的偏移;
  • branch[]:保存了映射关系的数据结构。

让我们看看函数主要流程:

  1. 首先自然要分配间接块和数据块,在函数ext2_allocate_blocks()中完成,这个我们等到下一篇博客中再详述;
    2.然后需要建立映射关系,其实就是将映射路径上的映射关系进行填充,主要过程在for循环中完成,这也是我们关注的重点。

再回到我们前面举的例子上来,现在我们间接块和数据块都分配好了,假如都分配成功,但此时映射尚未建立,如下图所示:

ext2_allocate_branch()解析

这就是我们目前所处的状态,间接块和数据块也已分配,但这种连接尚未建立起来。文件在i_data[13](即二级索引的起始之处)断链了,接下来我们要做的是就是创建这种连接关系。
让我们看看for循环的执行流程:
首先在for循环尚未开始的地方,branch[0].key = new_blocks[0],new_blocks[]数组中存储了ext2_allocate_blocks()分配的间接块和直接块的索引块号(new_blocks[]只有4项,如果分配的块数过多,它如何能记下,其实它的使用有点讲究,因为最多需要三个间接块,所以做多使用前三项来保存间接块号,而该数组并不记录所有的数据块号,只记录数据块的起始块号,因为它好像保证分配的数据块连续),因此,new_blocks[0]中保存的就是第一个间接块的物理块号,经过这一步,第一个链接就此建立起来,如下图:

ext2_allocate_branch()解析

至此,第一个链接就建立起来了,接下来进入for循环,建立剩余的链接,建立链接的过程相对简单:

for (n = 1; n <= indirect_blks;  n++) {
        /*
         * Get buffer_head for parent block, zero it out
         * and set the pointer to new one, then send
         * parent to disk.
         */
        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];
            /*
             * End of chain, update the last new metablock of
             * the chain to point to the new allocated
             * data blocks numbers
             */
            for (i=1; i < num; i++)
                *(branch[n].p + i) = cpu_to_le32(++current_block);//分配的数据块连续
        }
}

建立链接也很简单,对于每个间接块,从磁盘读出其内容,然后将在相应的偏移处记录下一个间接块的物理块号

branch[n].p = (__le32 *)bh->b_data + offsets[n] //将指针指向间接块的相应偏移处
branch[n].key = cpu_to_le32(new_blocks[n])//设置key,即下一级映射的间接块物理块号
*branch[n].p = branch[n].key//将内容写入间接块的相应位置

最后,在间接块映射完成以后,需要将数据块的物理块号记录在一级间接块中。这就是for循环里面的if判断的作用,因为分配的数据块是连续的,所以只需要知道起始数据块号和块数即可。

至此,所有的链接都已经完成了,形成的效果如下所示:

ext2_allocate_branch()解析

至此,整个建立连接的过程就已经完成了。理解这个过程非常重要,还好,我们已经把它搞透彻了。