本文将从内核源码、实例演示等角度详解ext4 extent B+树的前世今生,希望看过本文的读者能理解ext4 extent的工作原理。内核版本3.10.96,详细内核源码注释见GitHub - dongzhiyan-stack/kernel-code-comment: 3.10.96 内核源代码注释。
1 ext4 extent由来介绍
ext4之前的文件系统(ext2、ext3)由文件逻辑地址寻址到物理块地址,采用的是直接+间接寻址的方式,间接寻址又细化为1级间接寻址、2级间接寻址、3级间接寻址。
图1.1
按照我的理解画了如上示意图,最左边的是ext4_inode的i_block[15]数组,它的前12个成员保存是保存文件数据的物理块号。第13个成员即i_block12,保存的是个索引号(也是个物理块号),根据它找到的物理块里的4K数据才是保存文件数据的物理块号,这就是1级间接寻址,或者叫1级间接映射。第14个成员即i_block13,保存的是个索引号,根据它找到的物理块,该物理块里边的4K数据还是索引号。根据这个索引号找到物理块,里边的4K数据才是保存文件数据的物理块号,这就是2级间接寻址,或者叫2级间接映射。第15个成员即i_block14,保存的是个索引号,它指向的物理块里还是索引数据,这个索引数据指向的物理块里保存的数据还是索引数据,这个索引数据指向的物理块里保存的数据才是保存文件数据的物理块号,这就是3级间接寻址,或者叫3级间接映射。
ext2/ext3 这种直接+间接的寻址方式,缺点比较多,比如浪费物理块,大文件时寻址浪费磁盘性能和效率低,还有说容易碎片化。改良方案是引入extent,由一个extent结构就可以描述大文件的逻辑地址和物理地址的映射关系,可以节省磁盘空间,提升寻址性能,还能减少碎片化。
2 ext4 extent简单举例演示
extent结构内核用” struct ext4_extent”表示,如下所示:
- struct ext4_extent {
- //起始逻辑块地址
- __le32 ee_block;
- //逻辑块映射的连续物理块个数
- __le16 ee_len;
- //由ee_start_hi和ee_start_lo一起计算出起始逻辑块地址映射的起始物理块地址
- __le16 ee_start_hi;
- __le32 ee_start_lo;
- };
成员ee_block是起始逻辑块地址,成员ee_len是映射的连续物理块个数,成员ee_start_hi和ee_start_lo一起计算出起始逻辑块地址映射的起始物理块地址,我们这里假设这里计算出来的起始物理块地址是p_block。则这个ext4_extent结构表示文件逻辑块地址ee_block~(ee_block+ee_len)与物理块地址范围p_block~(p_block+ee_len)构成映射。进一步说,通过ext4_extent结构我们就知道了文件的逻辑块地址ee_block~(ee_block+ee_len)映射的物理块号,文件逻辑块地址ee_block~(ee_block+ee_len)与物理块地址范围p_block~(p_block+ee_len)一一对应。
一个ext4_extent可以表示文件一段逻辑块地址与物理块地址的映射关系,一个庞大的文件一般会有有多段逻辑块,此时需要用多个ext4_extent结构表示每段逻辑块映射的物理块。当有非常多ext4_extent结构时,太乱了,需要想办法把这么多的ext4_extent结构组织起来!内核用的是B+树,我们这里称为ext4 extent B+树,如下演示了这个B+树:
演示是3层B+树,第1层是根节点、第2层索引节点、第3层是叶子节点。B+树的根节点比较好理解,叶子节点主要保存数据结构就是ext4_extent。索引节点是什么?索引节点像是起着指引作用,根节点通过索引节点可以找到叶子节点。这里出现了两个新的数据结构ext4_extent_header和ext4_extent_idx。ext4_extent_header是头部结构,主要保存叶子节点或者索引节点的统计信息,ext4_extent_idx主要包含ext4_extent的索引信息,通过ext4_extent_idx可以找到ext4_extent结构。如下是两个结构体的定义:
- //索引节点或叶子节点头结构体信息
- struct ext4_extent_header {
- __le16 eh_magic;
- //索引节点或叶子节点目前有效的ext4_extent_idx或ext4_extent个数
- __le16 eh_entries;
- //索引节点或叶子节点最多可以保存多少个ext4_extent_idx或ext4_extent
- __le16 eh_max;
- //当前叶子节点或者索引节点所处B+数的层数
- __le16 eh_depth;
- __le32 eh_generation;=
- };
- struct ext4_extent_idx {
- //起始逻辑块地址
- __le32 ei_block;
- //由ei_leaf_lo和ei_leaf_hi组成起始逻辑块地址对应的物理块地址
- __le32 ei_leaf_lo;
- __le16 ei_leaf_hi;
- __u16 ei_unused;
- };
ext4 extent B+树第一层的根节点由1个ext4_extent_header+ 4个ext4_extent_idx组成,根节点的ext4_extent_idx指向第2层的叶子节点。B+树第2层中,索引节点由1个ext4_extent_header+N个ext4_extent_idx组成,第2层索引节点的ext4_extent_idx指向了第3层的叶子节点。B+树第3层中,叶子节点由1个ext4_extent_header+N个ext4_extent组成。
需要说明,根节点最对可以有4个ext4_extent_idx,它们每个都指向1个第2层的索引节点,就是说第2层最多有4个索引节点,为了演示方便示意图中只画了两个索引节点。第2层的索引节点中的每个ext4_extent_idx都指向一个第3层的叶子节点,为了演示方便示意图中只画了3个叶子节点。
ext4 extent B+树最核心的作用是通过它可以找到文件逻辑块地址与物理块地址的映射关系,我们可以通过ext4 extent B+树可以找到文件逻辑块地址映射的物理块地址,下边我们做个演示。先把上文的示意图简单改造下,标记上根节点/索引节点的ext4_extent_idx和叶子节点的ext4_extent对应的逻辑块地址。
假设我们想知道文件逻辑地址0映射的物理块地址是什么?首先找到根节点的第一个ext4_extent_idx,它的起始逻辑块号是0。然后找到它指向的索引节点,再找到该索引节点的第一个ext4_extent_idx,它的起始逻辑块号也是0。继续,找到当前索引节点第一个ext4_extent_idx指向的叶子节点。因为该叶子节点的第一个ext4_extent对应的逻辑块地址范围是0~10,我们要查找逻辑块地址0正好在它的逻辑块地址范围内。好的,每一个有效的ext4_extent数据结构都保存了其代表的逻辑块地址映射的物理块地址,理所应当,通过逻辑块地址范围0~10这个ext4_extent就可以直到逻辑块地址0映射的物理块地址。
提醒一下,只有叶子节点或者根节点的ext4_extent才会保存文件逻辑块地址与物理块地址的映射关系,索引节点或者根节点的ext4_extent_idx只保存了它代表的起始逻辑块地址和起始物理块地址,ext4_extent_idx只是起了一个索引作用,只是通过ext4_extent_idx的起始逻辑块地址找到它指向的ext4_extent。
再说明一下,ext4文件系统的一个物理块经测试是4K大小,一个内存page也是4K大小,内核里文件的逻辑块看代码也是以4K大小为单位,如下示意图演示了这个关系:
后续我们介绍文件逻辑块地址与物理块地址时,默认逻辑块和物理块都是以4K大小为单位。
3 简单演示ext4 extent B+树的形成过程
第2节的示意图演示了文件逻辑块地址与物理块地址的关系,ext4 extent B+树有根节点、索引节点、叶子节点。刚开始读写文件时,文件逻辑块地址和物理块地址映射关系比较简单,此时只是把保存文件逻辑块地址和物理块地址映射关系的ext4_extent存储到ext4 extent B+的根节点。后续随着文件逻辑块地址和物理块地址映射关系越来越复杂,需要的ext4_extent越来越多,便会出现叶子节点、索引节点。我们下边演示这个过程:
3.1 根节点4个extent插入过程
最开始,ext4 extent B+树是空的
好的,现在经过复杂的查找,我们知道了文件逻辑块地址0~10映射物理块地址是10000~10010(文件逻辑块地址映射的物理块地址是连续的),我们把这个映射的关系保存到第一个ext4_extent结构,如下简单演示一下:
- struct ext4_extent {
- __le32 ee_block = 0
- __le16 ee_len = 10
- //由ee_start_hi和ee_start_lo一起计算出起始物理块地址是10000
- __le16 ee_start_hi;
- __le32 ee_start_lo;
- };
好的,现在我们把这个ext4_extent插入到ext4 extent B+树,如下所示:
ext4 extent B+树的第一个位置保存了逻辑地址范围是0~10的ext4_extent。图中标出了ext4_extent代表的逻辑地址是0~10范围,没有标出映射的物理块地址。
好的,随着文件读写,文件逻辑块地址与物理块地址的映射关系越来越复杂,现在又多了3段映射关系,如下所示:
- 逻辑块地址 20~30 映射的物理块地址 12000~12010
- 逻辑块地址 50~60 映射的物理块地址 13000~13010
- 逻辑块地址 80~90 映射的物理块地址 18000~12010
好的,现在当然需要3个ext4_extent保存这3段逻辑块地址与物理块地址的映射关系,并插入到ext4 extent B+树,全插入后如下所示:
示意图每个ext4_extent下边的数字都是他们代表的逻辑块地址范围,ext4_extent上边的a0、a20、a50、a80是我对他们的编号,为了后续叙述方便,字母a后边的数字是他们的起始逻辑块号,后边叙述中也经常用到。
3.2 根节点下的叶子节点extent插入过程
继续,现在来了一个新的文件逻辑块地址与物理块地址的映射:逻辑块地址100~130映射了物理块地址19000~19010,于是分配一个新的ext4_extent结构保存这个映射关系,但是把把这个ext4_extent插入ext4 extent B+树时遇到问题了,根节点空间用完了。此时会创建一个叶子节点缓解尴尬局面,如下所示:
先把根节点原有的4个ext4_extent移动到了叶子节点前4个,然后把逻辑块地址100~130映射了物理块地址19000~19010的ext4_extent插入到叶子节点第5个ext4_extent位置。还有一个重点,根节点的原有的4个ext4_extent结构全清空,然后变成ext4_extent_idx。第一个ext4_extent_idx是有效的,它的起始逻辑块地址是原来该位置的ext4_extent的起始逻辑块地址(就是0),后3个ext4_extent_idx是无效的(就是没有使用)。
这里说明一点:从这里开始,根节点、索引节点有效的ext4_extent_idx圈了红色边框,叶子节点有效的ext4_extent也圈了红色边框(根节点的第一个ext4_extent_idx和叶子节点的前5个ext4_extent)。无效的ext4_extent_idx和ext4_extent红色边框都是原始黑色的,他们都还没用用来标识逻辑地址与物理地址的映射关系。
好的,我们继续。随着继续读写文件,新的文件逻辑块地址与物理块地址映射陆续产生,因此又产生了很多新的ext4_extent,最后把叶子节点所有的的ext4_extent全占满了,叶子节点最后一个ext4_extent的逻辑块地址是280~290,如下图所示。(后续文章为了叙述方便,我们大部分情况只说明ext4_extent的逻辑块地址,不再提逻辑块地址映射的物理块地址。)
好的,现在又来了一个新的逻辑块地址与物理块地址的映射关系:逻辑块地址 300~320 映射的物理块地址 28000~28020。新建一个ext4_extent保存这个映射关系后,该怎么把ext4_extent插入ext4 extent B+树呢?此时需要先在根节点第2个ext4_extent_idx (该ext4_extent_idx此时空闲,并未使用) 位置处,创建新的ext4_extent_idx,它的起始逻辑块地址是300,编号b300。然后创建b300这个ext4_extent_idx指向的叶子节点,该叶子节点的第一个ext4_extent就保存逻辑块地址 300~320 映射与物理块地址 28000~28020的映射关系,如下所示:
根节点的第2个ext4_extent_idx起始逻辑块地址正是300(图中标号是b300),它指向的叶子节点是新创建的,该叶子节点的第一个ext4_extent的逻辑块地址是 300~320。
好的,继续读写文件,有了新的逻辑块地址与物理块地址映射关系,把它们对应的ext4_extent添加到b300那个ext4_extent_idx指向的叶子节点,直到占满这个叶子节点,如下图所示:
继续,读写文件,再次产生新的逻辑块地址与物理块地址映射关系(逻辑块地址都大于>=600),只能把它们对应的ext4_extent添加到b300后边的ext4_extent_idx指向的叶子节点,但是这个叶子节点还没有,需要创建新的叶子节点………...下图直接来个最终演示结果,把根节点的4个ext4_extent_idx指向叶子节点的ext4_extent全占满了。
如图,根节点的这4个ext4_extent_idx编号一次是b0、b300、b500、b900。在b300指向叶子节点的ext4_extent全占满后,此时新添加的ext4_extent逻辑块地址是600~620,则创建b600指向的叶子节点,然后把逻辑块地址是600~620的ext4_extent插入到该叶子节点第一个ext4_extent位置处。后续又把逻辑块地址是680~690的ext4_extent插入到该叶子节点第2个ext4_extent位置处………. 把逻辑块地址是880~890的ext4_extent插入到该叶子节点最后一个ext4_extent位置处。
好的,b600指向的叶子节点ext4_extent也全占满了。此时来了一个新的ext4_extent,它代表的逻辑块地址是900~920,该怎么插入?老方法,创建b900指向的叶子节点,把它插入到该叶子节点第一个ext4_extent位置处。后续又把逻辑块地址是980~990的ext4_extent插入到该叶子节点第2个ext4_extent位置处………. 把逻辑块地址是1290~1290的ext4_extent插入到该叶子节点最后一个ext4_extent位置处。这个过程上边的示意图都演示了!
Ok,本小节完整介绍了根节点的4个ext4_extent_idx指向的叶子节点添加ext4_extent的过程,包括逻辑块地址与ext4_extent怎么建立联系、叶子节点的创建、叶子节点与ext4_extent_idx的关系。
3.3 根节点下索引节点的创建
这里发出疑问,上小节最后ext4 extent B+树根节点的4个ext4_extent_idx指向的叶子节点的ext4_extent全占满了(如图3.2.5所示),如果此时向B+树添加逻辑块地址是1300~1320的ext4_extent,会发生什么?我们直接在示意图中演示(高清大图查看方法:鼠标右键点击图片后,点击"在新标签页中打开图片",然后在新窗口点击图片即可查看高清大图):
如图所示,新增了一层索引节点,把根节点的原有的4个ext4_extent_idx(b0、b300、b600、b900)移动到了该索引节点的前4个ext4_extent_idx位置处。在索引节点的第5个ext4_extent_idx(该ext4_extent_idx此时空闲,并未使用)位置处,创建新的ext4_extent_idx(编号是b1300),令它的起始逻辑块地址是1300。接着创建b1300指向的叶子节点,最后把逻辑块地址是1300~1320的ext4_extent插入到b1300指向的叶子节点的第一个ext4_extent位置。
Ok,继续向b13000指向的叶子节点添加了ext4_extent,直到把该叶子节点的所有位置的ext4_extent全占满,如下图所示:
继续,ext4 extent B+树第2层的索引节点前5个ext4_extent_idx(b0、b300、b600、b900、b1300)指向的叶子节点的ext4_extent全占满了,此时如果向ext4 extent B+树插入逻辑块地址是1600~1620的ext4_extent该怎么办?下边的示意图演示了:
显然,就是在第2层的索引节点的第6个ext4_extent_idx(该ext4_extent_idx此时空闲,并未使用)位置处,创建新的ext4_extent_idx,它的起始逻辑块地址1600,我们给它编号b1600。然后创建b1600指向的叶子节点,把逻辑块地址是1600~1620的ext4_extent插入到该叶子节点第一个ext4_extent位置处。
接下来,继续向b1600这个ext4_extent_idx指向的叶子节点的插入ext4_extent,最后把该叶子节点所有的ext4_extent全占满了。再插入新的ext4_extent时,则在索引节点第7个ext4_extent_idx位置处(b1600后边的那个ext4_extent_idx, 该ext4_extent_idx此时空闲,并未使用)创建新的ext4_extent_idx,然后为这个新的ext4_extent_idx创建叶子节点,把新的ext4_extent插入到该叶子节点第一个ext4_extent位置处。这个过程跟前边b1300那个ext4_extent_idx指向的叶子节点的ext4_extent全占满时,向ext4 extent B+树插入逻辑块地址是1600~1620的ext4_extent的过程是类似的(图3.3.3)。
加大力度,随着不断向向ext4 extent B+树新的ext4_extent,第2层的索引节点的所有ext4_extent_idx全部被创建,这些ext4_extent_idx指向的叶子节点的ext4_extent也全占满,如下图所示:
说明一下,为了节省空间,把第2层的索引节点中b1300和b1600这两个ext4_extent_idx及其指向的叶子节点省略了,实际上索引节点的所有ext4_extent_idx都创建了,并且它们的叶子节点也都有创建。图中只显示了索引节点最后一个ext4_extent_idx,它的起始逻辑块地址是2000,标号b2000,它指向的叶子节点的ext4_extent全占满了。
继续,如果此时我们继续向ext4_extent B+树添加逻辑块地址是5000~5020的ext4_extent,怎么办?这里情况就有点特殊了,我们详细说下:第2层的索引节点的ext4_extent_idx全用完了,只能回到上一层的根节点,找到c0这ext4_extent_idx后边第2个ext4_extent_idx(该ext4_extent_idx此时空闲,并未使用)位置处,创建新的ext4_extent_idx,它的起始逻辑块地址是5000,然后创建它指向的索引节点。注意,是创建索引节点!然后在新创建的索引节点的第一个ext4_extent_idx位置处,创建新的ext4_extent_idx,令它的起始逻辑块地址是5000。这个过程用下图演示:
如图,在根节点第2个ext4_extent_idx位置处创建了起始逻辑块地市是5000的ext4_extent_idx,编号c5000。然后创建c5000这个ext4_extent_idx指向的索引节点,在该索引节点第一个ext4_extent_idx位置处创建起始逻辑块地址是5000的ext4_extent_idx,编号c5000_2。
继续,创建c5000_2这个ext4_extent_idx指向的叶子节点,并且把逻辑块地址是5000~5020的ext4_extent插入到该叶子节点第一个ext4_extent位置处,如下图所示:
继续向c5000_2这个ext4_extent_idx指向的叶子节点插入ext4_extent,直到把这个叶子节点的ext4_extent占满。然后再插入ext4_extent时,会在c5000_2后边的ext4_extent_idx位置处创建新的ext4_extent_idx,再创建该ext4_extent_idx指向的叶子节点,最后再把这个叶子节点的ext4_extent占满………..一直不停的插入ext4_extent,直到把c5000指向的索引节点上的所有ext4_extent_idx全用上,并且把这些ext4_extent_idx指向叶子节点的ext4_extent全占满,此时是如下状态:
为了画图方便,只把c5000_2和c8200这两个ext4_extent_idx指向叶子节点的ext4_extent显示了出来,其实二者之间的ext4_extent_idx指向叶子节点的ext4_extent也是被占满状态。
好的,现在演示了根节点c5000指向的索引节点被占满的情况,后续再插入ext4_extent,需要考虑向它后边的ext4_extent_idx位置处创建新的ext4_extent_idx,再创建该ext4_extent_idx指向的索引节点,再创建叶子节点,再插入新的ext4_extent………..这个过程跟图3.3.5上边的过程一致。最最后,在又插入了非常多的ext4_extent后,把目前整个ext4 extent B+树全占满了,如下图所示(高清大图查看方法:鼠标右键点击图片后,点击"在新标签页中打开图片",然后在新窗口点击图片即可查看高清大图):
显示空间有限,部分ext4_extent_idx和ext4_extent没有显示出来,实际是每个ext4 extent B+树每个索引节点ext4_extent_idx、每个叶子节点的ext4_extent全用满了!
Ok,再来最后一击,如果此时我们向该ext4 extent B+树插入逻辑块地址是1500~1520的ext4_extent该怎么办?
首先需要创建一层索引节点,把原根节点的c0、c5000、c9000、c13000这4个ext4_extent_idx移动到该索引节点的前4个ext4_extent_idx位置处,如下图所示:
继续,在新创建的索引节点第5个ext4_extent_idx(该ext4_extent_idx此时空闲,并未使用)位置处创建起始逻辑块地址是15000的ext4_extent_idx,编号c15000。并且,还创建了c15000指向的索引节点,并且在该索引节点的第一个ext4_extent_idx(该ext4_extent_idx此时空闲,并未使用)位置处,也创建起始逻辑块地址是150000的ext4_extent_idx,编号c15000_2。最后,创建c15000_2指向的叶子节点,把逻辑块地址是15000~15020的ext4_extent插入到该叶子节点的第一个ext4_extent位置处。需注意,c5000、c9000、c13000索引节点ext4_extent_idx指向的索引节点及下边的叶子节点与c0是类似的,这些索引节点和叶子节点全占满,只是空间限制没有画出来。
好的,经过以上详细甚至有点繁琐的总结,我想大家对ext4_extent插入ext4_extent B+树的过程有了详细了解,中间涉及到索引节点和叶子节点的创建。本文举例时,ext4_extent的逻辑块块地址非常零碎,比如0~10、20~30、50~60,600~620、680~690,2000~2020、2030~2090等等,没有大片连续的逻辑块地址。这是故意的,如果这些逻辑块地址彼此连续的,那估计只用一个或者很少的ext4_extent结构就可以表示了。这样ext4_extent B+树的索引节点和叶子节点就会非常少,无法演示叶子节点ext4_extent占满和索引节点ext4_extent_idx占满,然后创建新的索引节点和叶子节点,无法演示ext4_extent B+树的深度慢慢增加。
一个ext4_extent表示一段连续的逻辑块地址和物理块地址的映射关系,尤其注意一个ext4_extent逻辑块地址映射的物理块地址必须是连续的。另外,ext4_extent B+树中索引节点和叶子节点是怎么建立彼此的联系呢?涉及到哪些函数呢?函数流程是什么呢?下篇文章详细讲解。