文件系统的树(File System Tree)
Reiser文件系统采用平衡树结构(B+树或叫S+树),由内部节点(internal nodes)和叶子节点(leaf nodes)组成,每个节点都是一个磁盘块。每个对象(item),比如文件,目录或状态项(stat items)都有一个唯一的key,通过比较key可以定位到一个inode节点。内部节点由key和指向子节点的指针组成。指针的数目比key的数目多1。P0包含的指针指向所有小于K0的子节点,P1的指针指向[K0,K1)之间的子节点,以此类推。最后的指针指向最后key之后的子节点。每个节点都有等级,等级1表示叶子节点,2和更高的等级表示内部节点。
以我们的分区示例,S+树的一部分如下:(假定key是一个128 bits的数值)
内部节点用矩形表示,叶子节点用圆表示。注意块编号和key没有直接的联系。Reiser文件系统会把key相近的对象分配给编号相近的块,这不影响我们。
(译注:上面的图中,每个矩形里的4个整数表示一个key,比如Block 16514的key是{1398,1449,161,2}。key的比较按照数值的顺序进行,比如左边Block 8482里的2个key,{3,4,1,500}和{3,1182,0,0}的大小顺序就是这样比较出来的。)
块头信息(Block headers)
内部或叶子节点所在的块有头信息。只有无格式的块没有头信息。块头信息总是24字节大小,包含如下信息:
Name | Size | Description |
Level | 2 | 块在树中的等级 |
Nr. of items | 2 | 块中的对象数目 |
Free space | 2 | 块中未使用的字节数 |
Reserved | 2 | 保留 |
Right key | 16 | 块的最后的分界key |
“Right key”域最初用于叶子节点,现在只是为了兼容以前的版本。
示例:
下面是块8416的头信息,索引树最左边的叶子节点。
00000000 01 00 06 00 e4 04 00 00 00 00 00 00 00 00 00 00 ....ä...........
00000010 00 00 00 00 00 00 00 00
Level: 1
Items: 6
Free space: 1252 bytes
Keys
Reiser文件系统使用key不仅唯一的标识每个对象,还用它来定位索引树里的位置和分组相似的对象。key包含4个域:目录id(directory id),对象id(object id),对象内偏移(offset),类型(type)。注意,对象id只是key的一个部分。由于有目录id,同一个目录下的文件会被组合到一起,并且很有可能处于同一个子树里。offset主要用于间接对象(indirect item),因为它可以包含(块大小 – 4)/ 4个指向无格式块的指针。如果块大小为4096的话,那么一个文件最大可以有4048KB。要处理更大的文件,必须使用多个key,这些key的其他域都相同,除了偏移,表示对应数据在整个文件中的偏移量。这样整个文件还需要用一个特殊的key来表示。我暂时不知道为什么对象的类型也是key的一部分。
在Reiser文件系统3.5版本及以前,offset和type域都是4个字节,这意味着一个文件的最大字节数是232,即4GB(实际上还要减去一点儿)。为了支持更大的文件,3.6版本的offset域增加到了60 bits,而type域缩减为4 bits,这样理论上可以支持260字节大小的文件。但是由于块数最多只有232个,块大小最大为216字节,所以文件系统其实只支持248字节的文件。
为了和前面的版本兼容,现在有2种不同的key,而key自己并没有版本信息,于是容易让人混淆。为了解决这个问题,以前对象头信息里保留的16 bits现在用来保存版本号了。这样叶子节点里的key的版本就清楚了,但是如果你想知道内部节点(非叶子节点)里的key的版本,你必须找到某个叶子节点里去(译注:因为对象的key是存在叶子节点里的)。Reiser文件系统的库代码就是使用这个“拙劣”的方法:
static inline int is_key_format_1 (int type) {
return ( (type == 0 || type == 15) ? 1 : 0);
}
/* old keys (on i386) have k_offset_v2.k_type == 15 (direct and
indirect) or == 0 (dir items and stat data) */
/* */
int key_format (const struct key * key)
{
int type;
type = get_key_type_v2 (key);
if (is_key_format_1 (type))
return KEY_FORMAT_1;
return KEY_FORMAT_2;
}
从这段代码可以看出,状态项(stat items)总是KEY_FORMAT_1的,因为在版本2里它们的类型域是0。
版本1的key:
Name | Size | Description |
Directory ID | 4 | 对象所在的目录id |
Object ID | 4 | 对象的id (“inode编号”) |
Offset | 4 | 当前key对应的数据在整个文件中的偏移 |
Type | 4 | 对象的类型,可能的值有: 状态项(Stat):0 间接项(Indirect):0xfffffffe 直接项(Direct):0xffffffff 目录项(Directory):500 其他:555 |
版本2的key:
Name | Size | Description |
Directory ID | 4 | 对象所在的目录id |
Object ID | 4 | 对象的id (“inode编号”) |
Offset | 60 bits | 当前key对应的数据在整个文件中的偏移 |
Type | 4 bits | 对象的类型,可能的值有: 状态项(Stat):0 间接项(Indirect):1 直接项(Direct):2 目录项(Directory):3 其他:15 |
除了状态项的offset域是0,文件(无论直接或间接项)和目录的offset域都从1开始,这样在叶子节点里它们就被排序到状态项的后面。对于目录项(见下面),offset域保存的是最左边项的头信息(Header0)里的offset域,而不是偏移量。(译注:后面会讲到,目录项主要包括一个目录下的所有文件的头信息和文件名,其中第一个文件的头信息里的offset域就是此处目录key里offset域保存的内容。这样做的目的是,如果目录下的文件数太多,导致一个目录项放不下,可以分成多个目录项,并以offset域来区分)
示例:
下面是块8482包含的内部节点里最开始的2个key,第一个key是版本2,第二个是版本1:
00000000 02 00 00 00 0e 00 00 00 00 00 00 00 00 00 00 00 ................
Directory id: 2
Object id: 14
Offset: 0
Type: Stat item (0)
00000000 03 00 00 00 04 00 00 00 01 00 00 00 f4 01 00 00 ............ô...
Directory id: 3
Object id: 4
Offset: 1
Type: Directory item (500)
key之间比较时,先比较目录id,如果相等就比较对象id,以此类推。通过查看Reiser文件系统的源码可以发现,当需要比较type域的时候,代码里出现了一个警告。这说明type域从数据结构的角度来说并不是key的比较标准,唯一需要比较这个域的时候是“尾转化(tail conversion)”的时候,即一个直接项转化成一个间接项的时候。
内部节点(Internal nodes)
内部节点块由头信息,key,和指向子节点的指针组成。与我们前面解释S+树的图不同,内部节点实际上先列出所有排好序的key,然后是所有的指针。前面说过,P0所指的子树的所有key是小于K0的。
内部节点头信息里的level域总是大于1。“Nr. of items”域表示key的数目,而不是(key,Ptr)对的数目,因为Ptr的数目总是比key的数目多1。下面的图显示了指针的结构:
给定n号key(位置是24 + n×16字节),并假定key的总数是k,那么和n号key对应的左指针位于24 + k×16 + n×8。
示例:
00000000 02 00 a0 00 e0 00 00 00 00 00 00 00 00 00 00 00 .. .à...........
00000010 00 00 00 00 00 00 00 00 02 00 00 00 0e 00 00 00 ................
00000020 00 00 00 00 00 00 00 00 03 00 00 00 04 00 00 00 ................
00000030 01 00 00 00 f4 01 00 00 03 00 00 00 9e 04 00 00 ....ô...........
00000040 00 00 00 00 00 00 00 00 04 00 00 00 05 00 00 00 ................
...
00000a10 01 10 00 00 00 00 00 20 e0 20 00 00 04 0b b4 cc ....... à ....´Ì
00000a20 03 21 00 00 94 0d 54 c5 0b 21 00 00 e0 0f 2f c5 .!....TÅ.!..à./Å
00000a30 5e 23 00 00 b4 0f f4 ff 60 23 00 00 38 07 a9 ff ^#..´.ôÿ`#..8.©ÿ
...
Level: 2
Nr. items: 160
Free space: 224 bytes
Key 0: {2, 14, 0, 0}
Key 1: {3, 4, 1, 500}
Key 2: {3, 1182, 0, 0}
...
Ptr 0: {8416, 2820}
Ptr 1: {8451, 3479}
Ptr 2: {8459, 4064}
Ptr 3: {9054, 4020}
...
上面的例子是块8482的一部分,在前面S+树的图里也显示过它。0号key始于24字节(0x18),由于共有160个对象,0号指针始于2584字节(0xA18)。注意指针里的保留域里可能含有垃圾数据。块的空闲区域始于3872字节(0xF20),也可能包含垃圾数据。