前一篇博客中我们主要分析了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 @group else start_block = grp_goal + group_first_block; //default size is 8 blocks size = 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; else return 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树,假如我们现在有如下两个需求:
接下来,我们看整个分配预留窗口的第三段代码,上面的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; }首先,我们得明白这个函数的功能:从前面我们分析的函数提供的预留窗口开始,向后搜寻,找到一个未被别的文件当做预留窗口使用的一段连续的磁盘块。而且,我们得意识到,我们查找的范围必须局限于块组内,而不是无限制地找寻,让我们先看看参数的意义:
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。直到找到一个或者彻底失败。