本文描述了ReiserFS文件系统的磁盘结构,是我在为Windows NT平台写一个ReiserFS文件系统浏览器的时候写的。我对于官方网站www.namesys.com所提供的文档并不满意,所以自己写了一份。但是,因为这是我第一次接触ReiserFS,所以错误在所难免,如果有怀疑的地方,请先参考原始说明。
本文版权归Gerson Kurz所有并遵循GPL许可证
在你开始之前需要知道的
假定你已经知道分区的布局,参数如下:
1. 分区以字节为单位的起始位置
2. 分区以字节为单位的大小
3. 分区确实是一个ReiserFS的分区
如下类型定义将被使用:
__u16 - 16-bit unsigned integer
__u32 - 32-bit unsigned integer
__u64 - 64-bit unsigned integer
超级块
超级块包含了ReiserFS文件系统的基本信息。它位于分区起始位置的固定偏移位置,
REISERFS_DISK_OFFSET_IN_BYTES = (64*1024) = 65536
超级块数据结构在reiserfs_fs_sb.h文件中定义,如下:
struct reiserfs_super_block
{
__u32 s_block_count;
__u32 s_free_blocks; /* free blocks count */
__u32 s_root_block; /* root block number */
__u32 s_journal_block; /* journal block number */
__u32 s_journal_dev; /* journal device number */
/* Since journal size is currently a #define in a header file, if
** someone creates a disk with a 16MB journal and moves it to a
** system with 32MB journal default, they will overflow their journal
** when they mount the disk. s_orig_journal_size, plus some checks
** while mounting (inside journal_init) prevent that from happening
*/
/* great comment Chris. Thanks. -Hans */
__u32 s_orig_journal_size;
__u32 s_journal_trans_max ; /* max number of blocks in a transaction. */
__u32 s_journal_block_count ; /* total size of the journal. can change over time */
__u32 s_journal_max_batch ; /* max number of blocks to batch into a trans */
__u32 s_journal_max_commit_age ; /* in seconds, how old can an async commit be */
__u32 s_journal_max_trans_age ; /* in seconds, how old can a transaction be */
__u16 s_blocksize; /* block size */
__u16 s_oid_maxsize; /* max size of object id array, see get_objectid() commentary */
__u16 s_oid_cursize; /* current size of object id array */
__u16 s_state; /* valid or error */
char s_magic[12]; /* reiserfs magic string indicates that file system is reiserfs */
__u32 s_hash_function_code; /* indicate, what hash function is being use to sort names in a directory*/
__u16 s_tree_height; /* height of disk tree */
__u16 s_bmap_nr; /* amount of bitmap blocks needed to address each block of file system */
__u16 s_version; /* I'd prefer it if this was a string,
something like "3.6.4", and maybe
16 bytes long mostly unused. We
don't need to save bytes in the
superblock. -Hans */
__u16 s_reserved;
__u32 s_inode_generation;
char s_unused[124] ; /* zero filled by mkreiserfs */
} __attribute__ ((__packed__));
(不熟悉GCC编译器的朋友,”__attribute__((__packed__))”表示按一个字节对齐)。
s_blocksize字段至关重要。所有ReiserFS文件系统的数据都是按块组织的,每一个块都是相同的这个尺寸。在本文的剩余部分中,当我提到“块”,那我指的就是这种块(而不是任何如你的磁盘信息中所谓的块尺寸中的块)。另外我假定REISERFS_BLOCKSIZE宏指向这个字段。
注:s_magic包括字符串”ReIsErFs”或”ReIsEr2Fs”
使用/空闲块位图
文件系统需要知道那些块已经被使用而那些还是空闲的。空闲块通常是未格式化的,也就是说:块头结构(后面介绍)无效或丢失。ReiserFS使用位图来标识已使用或空闲。位图占用若干块,其中每一个字节标识8个使用/空闲位。比如,如下的字节:
FF FF FF C7
其中0xC7的二进制表达为11000111,这样就表示开始的8+8+8+3个块已经被使用(“1”),而后是三个块空闲块(”0”),最后两个为已使用块(”1”)。注意位图的解析因你系统的大小端字节序而异。在linux系统上,你可以使用如下命令来查询reiserfs是如何解析位图的。
debugreiserfs -b <name of reiserfs partition>
位图块总数
你可以从超级块结构中找到位图块的总数。
reiserfs_super_block.s_bmap_nr - amount of bitmap blocks
查找第一个位图块
第一个位图块是紧接着超级块的一个块。所以,计算第一个位图块的位置,可以用如下公式:
REISERFS_DISK_OFFSET_IN_BYTES + REISERFS_BLOCKSIZE
查找后续位图块
第一个之外的其他位图块的位置可以如下公式计算:
REISERFS_BLOCKSIZE * REISERFS_BLOCKSIZE * REISERFS_BLOCKSIZE * 8 * n
其中n >= 1。不要问我为什么是这样。所以,例如块大小为4096,第二个位图块的位置就是4096 * 4096 * 8 * 1 = 134217728字节,即第32768个块。
B树
注:本节仅作简要介绍,如果想了解B树的详细细节请在网上搜索“btree”。
B树是一种连接性的数据结构就像规则树一样。存在父块和子块,子块也依次拥有其自己的子块,依次类推。B树与“普通”树之间的区别在于,任何B树中的块都以一个key来标识。Key可以是任何形式的,但必须是唯一的、可比的。在我们的例子中key只是一个整数,而在ReiserFS中它是由四个整型组成的结构。重要的是你必须有一个定义好的排序方式-没有任何两个key是一样的,任何两个key必须是可比和可排序的。下面是一个例子。让我们假设key就是数字,他们可以按典型的数字大小来比较。其中根块具有如下key:
这就意味着,它需要三个子块,一个容纳范围为0..99的子块,一个容纳100..199,最后一个容纳200及以上。让我们看一下一个假设中的子块组:
0 |
23 |
87 |
105 |
130 |
170 |
175 |
180 |
205 |
209
|
这里,开始的三个元素是块100的子块,接着的五个是块200的子块,最后三个是块200之上的。你应该已经注意到不是所有的key都被使用到(译注:这里指数字)-在例子中对于0..99范围内的key只用到了三个。这种形式的排序会导致很多的树层次(我认为reiserfs默认是四层树结构)。
叶子节点是指没有子节点的节点。在ReiserFS中,他们的数据结构与内部节点是不同(参考下文“叶子节点”一节)
怎样用key查找文件项
这里依然没有什么算法,只是一些简单的介绍。你从顶层节点开始,查找大于或等于目标key的部分。如果你找到了,再进入它对应的子节点,依次类推,直到你到达叶子节点。
ReiserFS中的key
注:这一部分有点补足,因为我并不是十分理解其中的概念。
目前存在两种类型的key,每一种都包含如下四个元素:
( directory-id, object-id, offset, type )
其中任何一个都只是一个数字。二者主要区别在于“普通”key,四个元素都是一个32位的整数;但是有一些key他们的“offset”字段是一个60位的数字,而“type”字段是一个4位数字。因为我到现在还没有发现“offset”字段的主要用途,所以我还不能告诉你为什么有这种区别存在。
Key结构定义如下:
struct offset_v1 {
__u32 k_offset;
__u32 k_uniqueness;
};
struct offset_v2 {
__u64 k_offset: 60;
__u64 k_type: 4;
};
struct key {
__u32 k_dir_id; /* packing locality: by default parent directory object id */
__u32 k_objectid; /* object identifier */
union {
struct offset_v1 k_offset_v1;
struct offset_v2 k_offset_v2;
} u;
} ;
所以,按版本1来解释该结构时,你会得到4个32位整数:
( k_dir_id, k_object_id, u.k_offset_v1.k_offset, u.k_offset_v1.k_uniqueness )
如果按版本2来解释,你会得到如下4个整数:
( k_dir_id, k_object_id, u.k_offset_v2.k_offset, u.k_offset_v2.k_type )
将key扩展为内存中的“cpu key”是一个很好的做法,它扩展后的样子如下(我的定义):
typedef struct reiserfs_cpu_key
{
__u32 k_dir_id;
__u32 k_objectid;
__u64 k_offset;
__u32 k_type;
} REISERFS_CPU_KEY, *LPREISERFS_CPU_KEY;
这是一个兼容两种类型key的简单方法。
如何区分key的类型
好的,我必须承认,我不是很有把握。但是它似乎看起来是这样的:通常所有的都是类型1。而对于ih_version 2的文件项头指定的key是类型2。我会在后续“叶子节点”节中对此作更多解释,现在你只要认为所有的在磁盘上的key都是第一种类型的就可以了,而在内存中将总是被扩展为一个cpu-key。
怎样进行key比较
为了在B树中搜索,你必须能进行key的比较。好的,把key想象成一个整型数组。依次检查每一个整数。并使用普通的“小于”关系。下面是一个用于在两个cpu key中进行比较的C函数,它很清晰的说明了比较的过程:
int CompareCpuKeys(REISERFS_CPU_KEY* a, REISERFS_CPU_KEY *b)
{
// compare 1. integer
if( a->k_dir_id < b->k_dir_id )
return -1;
if( a->k_dir_id > b->k_dir_id )
return 1;
// compare 2. integer
if( a->k_objectid < b->k_objectid )
return -1;
if( a->k_objectid > b->k_objectid )
return 1;
// compare 3. integer
if( a->k_offset < b->k_offset )
return -1;
if( a->k_offset > b->k_offset )
return 1;
// compare 4. integer
if( a->k_type < b->k_type )
return -1;
if( a->k_type > b->k_type )
return 1;
return 0;
}
块处理
遍历ReiserFS所有磁盘上数据的概念是基于块的。ReiserFS中的块与“块/扇区/簇”中的块没有必然的联系。ReiserFS中的块大小由超级块指定。
共有三种类型的块:
已格式的内部块:这种块由B树数据使用。它包括key以及相关联的磁盘块。它将在后续“内部节点”部分详细描述。
已格式的叶子块:这种块用来描述文件信息。它将在后续“叶子节点”部分详细描述。
未格式化的块:未格式化的块仅包含原始的文件数据。你不可能通过观察而识别出这种块,--未格式化的块没有被ReiserFS B树数据结构使用。
严格的说,存在另外一种块类型,那就是超级块,但那只是一个特例。(见上)
标准块头
每一个格式化的块都包含一个块头。它的定义如下:
struct block_head {
__u16 blk_level; /* Level of a block in the tree. 1: leaf; 2 and higher: internal; */
__u16 blk_nr_item; /* Number of keys/items in a block. */
__u16 blk_free_space; /* Block free space in bytes. */
__u16 blk_reserved; /* dump this in v4/planA */
struct key blk_right_delim_key; /* used for Leaf nodes only */
};
你可以通过检查blk_level字段来判断一个格式化的块是否是一个叶子节点。
内部节点
一个内部节点是B树中的一个元素。这样,它包含了指向其子节点的的指针。
Block Header Key1 Key2 … Keyn Ptr.1 Ptr.2 … Ptr.n+1 Free Space
Key队列包含了普通(V1)版本的ReiserFS key。指针队列包含的元素结构如下:
struct disk_child {
__u32 dc_block_number; /* Disk child's block number. */
__u16 dc_size; /* Disk child's used space. */
__u16 dc_reserved;
};
可见,这就是指向子节点的指针。注意disk_child的数量比key要多一个,与前文中B树的简短介绍中所述的相似。
块头的blk_level字段值大于1。Blk_nr_item字段指定了key队列中key的个数。
如下代码段可以用来枚举所有的key以及他们相关联的的“disk-child”。
// assume bMemory is the block data
// get a pointer to the array of keys
LPBYTE lpbHeaderData = bMemory+sizeof(REISERFS_BLOCK_HEAD);
// get a pointer to the array of disk childs
LPBYTE lpbPointerData = bMemory+sizeof(REISERFS_BLOCK_HEAD)+(pH->blk_nr_item*sizeof(REISERFS_KEY));
// enumerate array
for( int i = 0; i < pH->blk_nr_item; i++ )
{
REISERFS_KEY* key = (REISERFS_KEY*)(lpbHeaderData+i*sizeof(REISERFS_KEY));
REISERFS_DISK_KEY* pointer = (REISERFS_DISK_KEY*) (lpbPointerData+i*sizeof(REISERFS_DISK_KEY));
// TODO: add evaluation
}
// this is the last pointer (note: no key!)
REISERFS_DISK_KEY* pointer = (REISERFS_DISK_KEY*) (lpbPointerData+i*sizeof(REISERFS_DISK_KEY));
TODO: add evaluation for last pointer
叶子节点
叶子节点的分布图如下:
Block Header Header 1 Header 2 … Header n Free Space Data1 Data2 … Data n
块头中blk_level字段的值为1。文件项的数量由块头中的blk_nr_item字段指定。
文件项头的结构如下:
struct item_head
{
struct key ih_key; /* Everything in the tree is found by searching for it based on its key.*/
union {
__u16 ih_free_space_reserved; /* The free space in the last unformatted node of an indirect item if this
is an indirect item. This equals 0xFFFF iff this is a direct item or
stat data item. Note that the key, not this field, is used to determine
the item type, and thus which field this union contains. */
__u16 ih_entry_count; /* Iff this is a directory item, this field equals the number of directory
entries in the directory item. */
} u;
__u16 ih_item_len; /* total size of the item body */
__u16 ih_item_location; /* an offset to the item body within the block */
__u16 ih_version; /* 0 for all old items, 2 for new
ones. Highest bit is set by fsck
temporary, cleaned after all done */
} ;
其中ih_item_location和ih_item_len字段指定了文件项数据的位置(在当前块中的位置)。在后面的内容中,将详细的介绍文件项的类型,并利用这两个字段来分析数据的位置。
下面的代码段演示了如何枚举叶子节点中所有的文件项:
// assume: bMemory is the data of the current block
// assume: pH is a pointer to the block head.
// find start of item header array
LPBYTE lpbHeaderData = bMemory+sizeof(REISERFS_BLOCK_HEAD);
for( int i = 0; i < pH->blk_nr_item; i++ )
{
// this is the item header
LPREISERFS_ITEM_HEAD iH = (LPREISERFS_ITEM_HEAD)lpbHeaderData;
// this is the item data
LPBYTE lpbItemData = bMemory + iH->ih_item_location;
DWORD dwItemSize = iH->ih_item_len
// TODO: add implementation
// skip to next item
lpbHeaderData += sizeof(REISERFS_ITEM_HEAD);
}
叶子节点中共有四种类型的文件项:
目录文件项:用于表示目录中的文件名和key。
统计文件项:用于表示文件的信息。
直接文件项:用于在块中直接装载小文件的内容。
间接文件项:指向大文件内容的块指针号队列。
下面将对上述四种类型做更详细的介绍。文件项的类型由ih_key.k_type字段决定。注意,以我所知,这是RerserFS中V2类型key会出现的唯一地方。使用以下代码段可以区别V1和V2版本的key。
// assume: iH is the pointer to the item header
#define ITEM_VERSION_1 0
#define ITEM_VERSION_2 1
REISERFS_KEY* key = &(iH->ih_key);
REISERFS_CPU_KEY cpukey;
cpukey.k_dir_id = key->k_dir_id;
cpukey.k_objectid = key->k_objectid;
if( iH->ih_version == ITEM_VERSION_1 )
{
cpukey.k_type = iH->ih_key.u.k_offset_v1.k_uniqueness;
cpukey.k_offset = iH->ih_key.u.k_offset_v1.k_offset;
}
else if ( iH->ih_version == ITEM_VERSION_2 )
{
cpukey.k_type = (int) iH->ih_key.u.k_offset_v2.k_type;
cpukey.k_offset = iH->ih_key.u.k_offset_v2.k_offset;
}
else assert(false);
这段代码可以根据磁盘上的key产生合适的cpu-key。
目录项
一个目录就是叶子节点中的一个项,它的k_type值为500。它代表了一个目录的所有文件入口,也就是一个目录列表。(但是它不包含文件的统计信息,你必须另外读取)。目录项的数据样子如下:
Dir Entry 1 Dir Entry 2 … Dir Entry N FileName N … Filename 2 Filename 1
每一个目录入口使用如下的结构:
struct reiserfs_de_head
{
__u32 deh_offset; /* third component of the directory entry key */
__u32 deh_dir_id; /* objectid of the parent directory of the object, that is referenced by directory entry */
__u32 deh_objectid; /* objectid of the object, that is referenced by directory entry */
__u16 deh_location; /* offset of name in the whole item */
__u16 deh_state; /* whether 1) entry contains stat data (for future), and 2) whether
entry is hidden (unlinked) */
} __attribute__ ((__packed__));
文件名并非是以0结尾的,你必须自己计算文件名的长度。第一个文件名开始于第一个reiserfs_de_head的偏移位置deh_location处,并且结束于项数据的结尾处。第二个文件名开始于它的de_head的偏移量deh_location处,结束于第一个de_head的deh_location处,以此类推。要找出有多少个reiserfs_de_head入口,你必须枚举所有的入口头,直到下一个头的开始位置等于最后一个已知deh_location.(译者:此处似乎并不正确)
如果以上内容一开始听起来有点困惑,那么下面是一段简单的代码枚举一个磁盘上的目录项中所有的文件名:
// given:
// SIZE_OF_BLOCK size of the item on disk.
// DATA_OF_BLOCK data of the directory item on disk. You should allocate one
// byte more and make sure the buffer is zero-terminated for the code below to work.
int dh_offset = 0; // offset in the file
int dh_strpos = SIZE_OF_BLOCK; // size
while( dh_offset < dh_strpos )
{
// get a pointer to the current item
REISERFS_DIRECTORY_HEAD* pDH = (REISERFS_DIRECTORY_HEAD*) (DATA_OF_BLOCK+dh_offset);
// the filename starts at the deh_location and is zeroterminated automatically
printf("name='%s'\n",bMemory+pDH->deh_location);
// make the next string zero-terminated, too.
(bMemory+pDH->deh_location)[0] = 0;
// this is the max sized, used for the loop ending criteria
dh_strpos = pDH->deh_location;
// increase array offset.
dh_offset += sizeof(REISERFS_DIRECTORY_HEAD);
}
下面是这样一个目录块的十六进制数据附带ASCII对照:
0000000: 01000000 | 05000000 | efa40100 | 66000400 ............f... <-- direntry #1
0000010: 02000000 | 04000000 | 05000000 | 64000400 ............d... <-- direntry #2
0000020: 805e2900 | efa40100 | f2a40100 | 61000400 .^).........a... <-- direntry #3
0000030: 8032e44f | efa40100 | f0a40100 | 58000400 .2.O........X... <-- direntry #4
0000040: 80eeb365 | efa40100 | f1a40100 | 50000400 ...e........P... <-- direntry #5
0000050: 70726f66 | 696c6573 | 64656663 | 6f6e6669 profilesdefconfi
0000060: 67746d70 | 2e2e2e gtmp...
你可以看到文件名确实是非0结尾的。经过合适的解析,这个结构可以读出:
Deh_offset deh_dir_id deh_objectid deh_location deh_state filename
1 5 107759 102 4 ‘.’
2 4 5 100 4 ‘..’
2711168 107759 107762 97 4 ‘tmp’
1340355200 107759 107760 88 4 ‘defconfig’
1706290816 107759 107761 80 4 ‘profiles’
你可以看到,当前目录(“.”)的object id为107759,它被其子文件使用为他们的directory id。
(持续翻译中...)