ext2文件系统采用了直接和间接映射的方式来保存逻辑块至物理块的映射关系。因此,在每次读或者写某个逻辑块之前,需要查找这种映射关系,将逻辑块号转化为物理块号。这就是ext2_get_block的主要作用,另外,如果要访问的逻辑块尚未分配物理磁盘块,也会在该函数中为该逻辑块分配数据块以及可能所需的间接块,然后建立好映射关系。这一般出现在写情况中。
关于ext2文件系统如何通过直接块和间接块来建立逻辑块至物理块之间的映射可参考之前的博客,今天我们所要描述的函数是ext2_get_branch,从字面上来看,该函数是完成从逻辑块号到物理块号的主战场,如果一切正常,从该函数返回时,这种映射关系就已经建立了,但是需要注意的是:如果该逻辑块尚未分配物理块,该函数是不负责分配的,它只负责查找到已经建立好映射的地方,至于未映射的关系会在后面处理。
我们举个例子来描述该代码的具体实现逻辑。假设我们需要读一个起始块号为281的逻辑块,读的块数目为2,根据ext2文件系统映射方式,直接映射可映射的数据块数量为12,假设文件系统块大小为1k,那么0~12K大小的文件可通过直接映射方式来索引;一级间接块映射的方式最多可索引的块大小为1K * (1K/4)= 256K,因此直接映射+一级间接映射的方式最多可索引文件大小为268K,而现在我们需要读取的逻辑块号为281K,必须采用二级间接映射的方式才可索引该逻辑块。如下图所示:
从上图可以看到,逻辑块号为281的物理块号存储在上图所示的一级间接块的第13项(第一项存储的逻辑块号是268对应的物理块号),而该一级间接块所在的物理块号又被存储在二级间接块的偏移为0 的地方(间接块的每一项存储的是下一级索引或者数据块的物理块号,每项占用4个字节)。而该二级间接块的物理块号则是存储在EXT2_I(inode)->i_data[13]中,这也是我们查找的起点,因此,整的来看,我们的主要目的就是从起点开始,一步一步定位到最终找到逻辑块号对应的物理块号。
以本例来说,我们首先确定281该逻辑块号采用的是二级映射,然后我们从i_data[13]中获取到二级间接块的物理块号,假如为199,读出该块内容(如果尚未读出到内存),再计算该块在二级间接块中的偏移(本例中偏移为0),然后读出该偏移处的物理块号,假如为200,这就是一级间接块的物理块号,重复上述过程,读出一级间接块内容,再来计算该逻辑块号在一级间接块中的偏移(该间接块映射的逻辑块号范围是268 ~ 523,因此逻辑块281的偏移为13,所以一级间接块的第13项存储的即是逻辑块号281的物理块号),至此,我们便一步步地查找了逻辑块对应的物理块号。
其实内核在实现的时候并不是按照我上述的流程来的,文件系统首先会计算好该逻辑块号所需的映射深度(直接映射or一级间接映射or二级间接映射or三级间接映射),并计算好每级映射块的物理块号在上级映射块中的存储位置,将这些位置存储在一个偏移量数组offsets[]中。如上例中,281该逻辑块的物理块号在一级间接块的偏移量为13,而一级间接块其物理块号存储在二级间接块的偏移量为0处,二级间接块的物理块号存储在i_data[13]中。因此,整个offsets[]的便是{13, 0, 13}。
好了,言归正传,我们来看看ext2_get_branch()的实现:
static Indirect *ext2_get_branch(struct inode *inode, int depth, int *offsets, Indirect chain[4], int *err) { struct super_block *sb = inode->i_sb; Indirect *p = chain; struct buffer_head *bh; *err = 0; /* i_data is not going away, no lock needed */ add_chain (chain, NULL, EXT2_I(inode)->i_data + *offsets); //如果p->key为0,说明尚未映射 if (!p->key) goto no_block; while (--depth) { bh = sb_bread(sb, le32_to_cpu(p->key)); if (!bh) goto failure; read_lock(&EXT2_I(inode)->i_meta_lock); if (!verify_chain(chain, p)) goto changed; add_chain(++p, bh, (__le32*)bh->b_data + *++offsets); read_unlock(&EXT2_I(inode)->i_meta_lock); if (!p->key) goto no_block; } return NULL; changed: read_unlock(&EXT2_I(inode)->i_meta_lock); brelse(bh); *err = -EAGAIN; goto no_block; failure: *err = -EIO; no_block: return p; }该函数的实现,基本上就是按照我前面所说的那样,从梦开始的地方一级级地查找,直到最终找到我们想要的或者是到某个地方映射关系被打断。这里需要仔细看看的可能就是内核实现的时候使用了一个数据结构Indirect。
typedef struct { __le32 *p; __le32 key; struct buffer_head *bh; } Indirect;该数据结构记录了映射链中每一级的索引信息。p是索引指针,指向记录下一级索引物理块号(或者数据块)的存储位置,key记录p指针里面的内容,key=0意味着映射链的断裂,bh则指向该当前间接块被从磁盘读出保存在内存中的数据结构(即与磁盘块一一对应的buffer_head结构)。还是以上面的例子来说,如下图:
上图中的左下角的数据结构中完整地记录了二级间接块时当前的索引全景,branch->p是指向存储一级间接块物理块号的指针,branch->key记录该指针处的内容,即物理块号,branch->bh则指向的是该二级间接块被从磁盘中读出并存储在内存中的数据结构(buffer_head)。
让我们在回头看看ext2_get_branch()的代码,首先看看该函数的参数:
中,作为映射链的最开始,add_chain (chain, NULL, EXT2_I(inode)->i_data + *offsets);其实现也非常简单,如下:
static inline void add_chain(Indirect *p, struct buffer_head *bh, __le32 *v) { p->key = *(p->p = v); p->bh = bh;//初始的时候,bh=NULL,因为此时不是通过间接块来存储索引,而是i_data[]; }接下来,我们从映射链的起始地方开始一级一级地往下查找,代码如下:
while (--depth) { bh = sb_bread(sb, le32_to_cpu(p->key)); if (!bh) goto failure; read_lock(&EXT2_I(inode)->i_meta_lock); if (!verify_chain(chain, p)) goto changed; add_chain(++p, bh, (__le32*)bh->b_data + *++offsets); read_unlock(&EXT2_I(inode)->i_meta_lock); if (!p->key) goto no_block; } return NULL;查找结束条件是已经到了最后一级间接映射或者映射链断裂(p->key = 0,这对应着写情况),到每一级的时候将从其到下一级的映射关系记录在映射链中,以便可继续往下查找(add_chain(++p, bh, (__le32*)bh->b_data + *++offsets))。
接下来让我们看看程序结束时的返回值状况,通过代码我们知道,如果一直找到了最后一级映射(可以通过逻辑块找到其物理块号),并且其中没有断链,而且该逻辑块业已分配物理块,那么此时大功告成,返回值为NULL,如果在其中某个地方断链了,即此时逻辑块对应的物理块和建立映射需要的间接块可能未分配,那么返回值就为断链处的详细信息p(Indirect结构)。