alloc_new_reservation解析

来源:互联网 发布:巫师3优化补丁 编辑:程序博客网 时间:2024/06/06 17:50

        前一篇博客中我们主要分析了ext2_try_to_allocate_with_rsv()函数的实现原理,在分析过程中我们提到,对于某个文件数据块的分配可能会涉及到预留窗口的分配(在那篇博客中我有着较为详细的原理分析),而为文件分配预留窗口的实现在那时我们并没有提及,留到这里专门来写,也表明,该功能不但很重要,其实现也是比较复杂的。

        既然说到文件预留窗口的分配,我们不得不首先提及下预留窗口及其相关背景知识,顾名思义,预留窗口即初始时预分配一个较大的窗口(如申请一块,但分配八块),于是,预留窗口中剩余的空闲块可以作为下次块分配请求时的响应,这样的好处是可以让文件的数据块尽量地连续,而且也能减少磁盘碎片的发生。另外,每个文件在其一次打开的生命周期内只有一个预留窗口,该预留窗口被创建,并且可能扩大或者缩小,在close的时候被删除。整体上,ext2文件系统使用一颗RB树来组织文件系统中所有的预留窗口(最开始我猜是链表组织的,因为代码的注释中处处散发着双向链表的气息),大概结构如下图所示:


        我们接下来的任务就是搞清楚怎么从现有的这么多预留窗口的缝隙中分配一个全新的符合我们需要的预留窗口,并且添加到该全局的RB树中。同样,我们还是尽量通过举例子来加深对代码的理解,空想是基本不可能的。

static int alloc_new_reservation(struct ext2_reserve_window_node *my_rsv,ext2_grpblk_t grp_goal, struct super_block *sb,unsigned int group, struct buffer_head *bitmap_bh){struct ext2_reserve_window_node *search_head;ext2_fsblk_t group_first_block, group_end_block, start_block;ext2_grpblk_t first_free_block;struct rb_root *fs_rsv_root = &EXT2_SB(sb)->s_rsv_window_root;unsigned long size;int ret;spinlock_t *rsv_lock = &EXT2_SB(sb)->s_rsv_window_lock;group_first_block = ext2_group_first_block_no(sb, group);group_end_block = group_first_block + (EXT2_BLOCKS_PER_GROUP(sb) - 1);if (grp_goal < 0)start_block = group_first_block;//@grp_goal is relative to @groupelsestart_block = grp_goal + group_first_block;//default size is 8 blockssize = my_rsv->rsv_goal_size;//如果原来的预留窗口不为空,那么应该怎么处理?if (!rsv_is_empty(&my_rsv->rsv_window)) {//如果原来的预留窗口跨了当前group,//并且goal是落在该预留窗口之中的// 那么这时候其实是意味着根本无需去//分配新的预留窗口的//那我就疑惑了:像这种无需分配新的预留窗口的//上层调用者应该就不会调用这个函数了啊if ((my_rsv->rsv_start <= group_end_block) &&(my_rsv->rsv_end > group_end_block) &&(start_block >= my_rsv->rsv_start))return -1;if ((my_rsv->rsv_alloc_hit >     (my_rsv->rsv_end - my_rsv->rsv_start + 1) / 2)) {/* * if the previously allocation hit ratio is * greater than 1/2, then we double the size of * the reservation window the next time, * otherwise we keep the same size window */size = size * 2;if (size > EXT2_MAX_RESERVE_BLOCKS)size = EXT2_MAX_RESERVE_BLOCKS;my_rsv->rsv_goal_size= size;}}spin_lock(rsv_lock);/* * shift the search start to the window near the goal block */search_head = search_reserve_window(fs_rsv_root, start_block);retry:ret = find_next_reservable_window(search_head, my_rsv, sb,start_block, group_end_block);if (ret == -1) {if (!rsv_is_empty(&my_rsv->rsv_window))rsv_window_remove(sb, my_rsv);spin_unlock(rsv_lock);return -1;}spin_unlock(rsv_lock);first_free_block = bitmap_search_next_usable_block(my_rsv->rsv_start - group_first_block,bitmap_bh, group_end_block - group_first_block + 1);if (first_free_block < 0) {/* * no free block left on the bitmap, no point * to reserve the space. return failed. */spin_lock(rsv_lock);if (!rsv_is_empty(&my_rsv->rsv_window))rsv_window_remove(sb, my_rsv);spin_unlock(rsv_lock);return -1;/* failed */}start_block = first_free_block + group_first_block;/* * check if the first free block is within the * free space we just reserved */if (start_block >= my_rsv->rsv_start && start_block <= my_rsv->rsv_end)return 0;/* success *//* * if the first free bit we found is out of the reservable space * continue search for next reservable space, * start from where the free block is, * we also shift the list head to where we stopped last time */search_head = my_rsv;spin_lock(rsv_lock);goto retry;}
        我在阅读代码的时候,会习惯性地将长的代码分成几个段来考虑,并会特别注意段之间的衔接,尤其的分段之间的内在逻辑,即为何要写成这样,这样是充分考虑了各种意外情况么,因为我们知道可能大多数的代码都在处理异常情况。

        好,我将上述代码照例划分成我所理解的几个大段,第一段是if判断,第二段search_reserve_window(),第三段find_next_reserveable_window(),接下来的以bitmap_search_next_uasble_window()为主的作为第四段。

        首先,我们来理解第一段,这个if理解起来着实有些费力,源代码中的注释也不少。关于这个还得结合调用者(目前发现主要是ext2_try_to_allocate_with_rsv)的行为来分析,因为if判断的内容my_rsv就是调用者传入的参数,调用者ext2_try_to_allocate_with_rsv()会在两种情况下为文件分配一个新的预留窗口:1. 文件尚无预留窗口;2.文件当前的预留窗口无效了(即本次要分配的块不落在当前的预留窗口了)。if判断的就是文件原来是没有预留窗口呢还是原来有预留窗口但本次需要分配新的?

        如果,是原来有预留窗口但需要丢弃的,那我们得考虑本次为文件新分配的预留窗口得分配多大。预留窗口的分配默认是8个磁盘块,但是,如果文件原来预留窗口的命中率达到50%以上,我们有必要扩大这个数值,具体扩大方法是在原来的预留窗口上再翻倍,但也是有上限的(1027,1024个直接块+3个间接数据块)。另外,该部分代码中我还有点不太明白的就是if里面的第一段:判断了文件之前的预留窗口是否跨越group,如果是,并且本次要分配的建议块号落在预留窗口之内,那么返回失败,这个作何理解?我代码的注释中也标出了我的疑惑,按照道理来说,如果目标块号落在预留窗口之内,是不会重新去分配新的预留窗口的,这个应该是在调用者ext2_try_to_allocate_with_rsv()来保证的,第一部分代码如下所示:

if (!rsv_is_empty(&my_rsv->rsv_window)) {//如果原来的预留窗口跨了当前group,//并且goal是落在该预留窗口之中的// 那么这时候其实是意味着根本无需去//分配新的预留窗口的//那我就疑惑了:像这种无需分配新的预留窗口的//上层调用者应该就不会调用这个函数了啊if ((my_rsv->rsv_start <= group_end_block) &&(my_rsv->rsv_end > group_end_block) &&(start_block >= my_rsv->rsv_start))return -1;if ((my_rsv->rsv_alloc_hit >     (my_rsv->rsv_end - my_rsv->rsv_start + 1) / 2)) {/* * if the previously allocation hit ratio is * greater than 1/2, then we double the size of * the reservation window the next time, * otherwise we keep the same size window */size = size * 2;if (size > EXT2_MAX_RESERVE_BLOCKS)size = EXT2_MAX_RESERVE_BLOCKS;my_rsv->rsv_goal_size= size;}}

        第一部分执行完成后,要么成功,要么失败,失败了就直接返回-1,成功了也就意味着本次需要新分配的预留窗口的参数已经设置完毕(主要是当次分配的预留窗口大小应该是多少),接下来就是真正的分配过程了。让我们拭目以待。

        第一部分成功后,接下来的主要工作就是为文件分配预留窗口了,但即便是这样一个功能,内核在实现的时候也是分成几个部分来完成的,我想,主要可能是为了代码结构更加清晰吧。首先第一个部分是调用函数search_reserve_window,让我们进入该函数:

static struct ext2_reserve_window_node *search_reserve_window(struct rb_root *root, ext2_fsblk_t goal){struct rb_node *n = root->rb_node;struct ext2_reserve_window_node *rsv;if (!n)return NULL;do {rsv = rb_entry(n, struct ext2_reserve_window_node, rsv_node);if (goal < rsv->rsv_start)n = n->rb_left;else if (goal > rsv->rsv_end)n = n->rb_right;elsereturn rsv;} while (n);/* * We've fallen off the end of the tree: the goal wasn't inside * any particular node.  OK, the previous node must be to one * side of the interval containing the goal.  If it's the RHS, * we need to back up one. *//* 如果发现搜索到最后,路径上的最后一个节点的start > goal,那么我们就返回该节点的prev,会在后面的实例中说明** */if (rsv->rsv_start > goal) {n = rb_prev(&rsv->rsv_node);rsv = rb_entry(n, struct ext2_reserve_window_node, rsv_node);}return rsv;}
        首先,我们要弄明白这个函数到底在做什么,这个函数只是我们分配预留窗口路上的助手,它帮助我们确定我们想要分配的预留窗口应该从哪开始预留。因为我们知道,当前的文件系统可能已经有众多文件分配了预留窗口,而我们分配的预留窗口有如下原则:1. 绝对不能和其他文件的预留窗口有交集;2.该预留窗口应该从建议块号开始,或者在建议块号附近。让我们继续以上面的图为例:


这张图是当前ext2文件系统的所有文件的预留窗口组成的RB树,假如我们现在有如下两个需求:

  1. 需要分配一个预留窗口:窗口大小为6,建议起始块号为61;
  2. 需要分配一个预留窗口:窗口大小为16,建议起始块号为70;
  3. 需要分配一个预留窗口:窗口大小为32,建议起始块号为130;
        让我们看看对上面这三种需求,该函数怎么为我找到可以分配的那段区间。总的搜寻过程(也是函数的执行流)用我的语言概括来说:
        从红黑树的根开始查找并判断目标块号和当前节点的预留窗口数据块范围的关系,如果goal < start(当前预留窗口的起始块号),那么说明我们所要找寻的分配区间应该在当前节点的左子树上,转到左子树上查找;如果goal > end(当前预留窗口的结束块号),则意味着我们所要寻找的分配区间应该在当前节点的右子树上,则转到右子树上继续寻找;如果goal 落在[start, end]区间中,那么返回当前节点即可。该函数结束无非出现三种情况:1. goal落在某个文件的预留窗口内,此时返回该预留窗口;2. 到树的某个分支结束循环(即已经到某个查找路径的叶子节点了还未找到这样一个预留窗口)且此时goal < 叶子节点的起始块号(start),那么这时候返回该叶子节点的prev;3. 到树的某个分支结束循环(即已经到某个查找路径的叶子节点了还未找到这样一个预留窗口)且此时goal > 叶子节点的结束块号(end),那么这时候返回该叶子节点即可。
        上面的叙述过程未免太过抽象,看起来也很累,不妨结合上面的三个需求实例来理解上面的过程。
  1. 对于实例1,我们首先从根节点开始匹配,goal = 61,大于当前预留窗口的end,所以继续朝右子树搜索,右孩子的预留窗口为[80, 85],于是,继续转左孩子,预留窗口为[68,76],还是得继续朝左孩子走,发现左孩子此时已经为空了,怎么办?(目前关于这点的理解还不是很明白,因为这里和RB的实现相关),这时候返回的是当前预留窗口节点[68,76]的prev,我猜应该是根节点[50,60];
  2. 对于实例2,同样从根节点开始匹配,goal = 70,大于当前预留窗口的end,所以朝右孩子搜索,右孩子预留窗口为[80,85],其start > goal,于是朝左孩子节点搜寻,来到预留窗口[68,76]处,惊喜地发现goal正好落在这个预留窗口内,因此,我们直接将这个预留窗口返回给调用者;
  3. 对于实例3,按照上面的搜寻流程一直走到最底层的[100,120]都没有找到合适的预留窗口,因此,我们返回最后一个搜寻的预留窗口,即[100,120]。
        于是,对于上面的三种情况,我们都返回了一个预留窗口,即告诉调用者,你应该从该预留的end + 1开始往后搜索,找到一个合适的未被其他文件使用的预留窗口。该函数的使命就算完成了。

        接下来,我们看整个分配预留窗口的第三段代码,上面的search_reserve_window()已经给了我们一个搜寻的起点,接下来就让我们看看怎么去搜寻一个合适的预留窗口。而搜寻成功的条件是:找到一段连续物理磁盘块,且这些磁盘块未被其它文件用作预留窗口,而我们不在乎这段物理磁盘块是否已经被占用了,因为这中情况会在后面处理,我们这里只是想搜寻一段未被其它文件当做预留窗口使用的连续磁盘块即可。该函数的实现find_next_reservable_window如下:

static int find_next_reservable_window(struct ext2_reserve_window_node *search_head,struct ext2_reserve_window_node *my_rsv,struct super_block * sb,ext2_fsblk_t start_block,ext2_fsblk_t last_block){struct rb_node *next;struct ext2_reserve_window_node *rsv, *prev;ext2_fsblk_t cur;int size = my_rsv->rsv_goal_size;/* TODO: make the start of the reservation window byte-aligned *//* cur = *start_block & ~7;*/cur = start_block;rsv = search_head;if (!rsv)return -1;while (1) {//cur <= rsv->rsv_end意味着当前搜寻的//落在当前某个文件的预留窗口内//那么就从该预留窗口的下一块继续搜寻if (cur <= rsv->rsv_end)cur = rsv->rsv_end + 1; //如果一直搜寻到该块组末尾都没 //找到一个合适大小的可 预留窗口 //那只能返回失败if (cur > last_block)return -1;/* fail */prev = rsv;next = rb_next(&rsv->rsv_node);rsv = rb_entry(next,struct ext2_reserve_window_node,rsv_node);/* * Reached the last reservation, we can just append to the * previous one. */ //如果一直找到最后一个节点了 //那么我们就使用最后一个节点 //之后的那部分空间作为预留空间即可if (!next)break;//判断前一个预留窗口和下一个//预留窗口之间是否有足够的预留空间//如果有,那最好不过了//就使用这些未被使用的预留空间//作为需要分配的预留窗口if (cur + size <= rsv->rsv_start) {/* * Found a reserveable space big enough.  We could * have a reservation across the group boundary here  */break;}}if ((prev != my_rsv) && (!rsv_is_empty(&my_rsv->rsv_window)))rsv_window_remove(sb, my_rsv);my_rsv->rsv_start = cur;my_rsv->rsv_end = cur + size - 1;my_rsv->rsv_alloc_hit = 0;//添加到预留窗口组成的RB树中if (prev != my_rsv)ext2_rsv_window_add(sb, my_rsv);return 0;}
        首先,我们得明白这个函数的功能:从前面我们分析的函数提供的预留窗口开始,向后搜寻,找到一个未被别的文件当做预留窗口使用的一段连续的磁盘块。而且,我们得意识到,我们查找的范围必须局限于块组内,而不是无限制地找寻,让我们先看看参数的意义:

  1. search_head:即前面一个函数的返回值,指引我们应该从哪开始往后查找;
  2. my_rsv:存储我们最终分配到的预留窗口信息;
  3. start_block:我们查找的起始块号,即goal;
  4. last_block:我们查找的最大块号,不能超过这个限制。
        接下来就让我们看看这个函数的逻辑:
        从指引节点开始,我们判断我们所要查找的目标是否和当前的节点的预留窗口有交集,如果有,我们只能调整目标,因为我们的原则是不能使用别人预留窗口中的磁盘块,我们只能调整到该预留窗口中的end的下一个磁盘块作为目标块,重复上述搜索流程,这样到最后无非出现以下三种情形:
  1. 一直判断不成立,知道目标块号超出了最大的限制,此时返回-1,标志失败;
  2. 到某一个区间的时候,突然发现前后两个预留窗口有一个较大的空隙(如上面实例1那样,[50,60]和[68,76]这两个预留窗口之间有8个磁盘块,满足我们需要的6个大小),那此时我们就将这段磁盘块作为新分配的预留窗口;
  3. 一直搜寻到最后一个预留窗口仍然不存在两个预留窗口之间有较大的空隙满足我们的需求,但是发现最后一个预留窗口之后的空间够我们使用(这就是上面实例3的情况,要分配的起始块号130落在了最大预留窗口的后面,这后面所有的数据块都没被其他文件使用,我们当然直接使用即可)。
        产生了上面的3种结果,1会失败,2和3都满意地分配到了想要的预留窗口,我们将这个预留窗口添加到RB树中,然后返回即可。而且my_rsv中保存了我们分配到的预留窗口的详细信息。
        你以为到这里就算完事了么?非也,因为上面的分配我们虽能保证分配到的预留窗口不和其他文件的预留窗口有重叠,但我们无法保证里面的数据块一定是空闲的,即其有可能已经被分配了,这是一件很可怕的事,不是嘛?因为好不容易辛辛苦苦分配到的预留窗口有可能压根没用,因此,我们得作个有效性地检查,看看我们刚刚分配的预留窗口是否可用(只要有一个空闲块就为可用,真悲哀),如果不可用,我们还得继续想办法往下搜索。这就是接下来的第四部分所要完成的任务了。
spin_unlock(rsv_lock);first_free_block = bitmap_search_next_usable_block(my_rsv->rsv_start - group_first_block,bitmap_bh, group_end_block - group_first_block + 1);if (first_free_block < 0) {/* * no free block left on the bitmap, no point * to reserve the space. return failed. */spin_lock(rsv_lock);if (!rsv_is_empty(&my_rsv->rsv_window))rsv_window_remove(sb, my_rsv);spin_unlock(rsv_lock);return -1;/* failed */}start_block = first_free_block + group_first_block;/* * check if the first free block is within the * free space we just reserved */if (start_block >= my_rsv->rsv_start && start_block <= my_rsv->rsv_end)return 0;/* success *//* * if the first free bit we found is out of the reservable space * continue search for next reservable space, * start from where the free block is, * we also shift the list head to where we stopped last time */search_head = my_rsv;spin_lock(rsv_lock);goto retry;
        这部分的代码相对来说是比较简单的,即找找看我们的预留窗口中是否有至少一个空闲的磁盘块,我们需要注意的就是:如果失败,即辛辛苦苦分配而来的预留窗口中一个空闲磁盘块都没有,该怎么处理,我们的处理办法也是比较简单的,从当前这个预留窗口继续往后搜索,即代码中的goto retry。直到找到一个或者彻底失败。
        至此,我们就把这个函数算是较为彻底的交代了,非常的复杂,但理解深刻以后又显得是那么自然。接下来的任务就是在分配到的预留窗口中如何去分配磁盘块了,留待下一篇博客吧。
原创粉丝点击