1、Ext2 文件系统的硬盘布局本文主要讲述 Linux 上比较流行的 ext2 文件系统在硬盘分区上的详细布局情况。Ext2 文件系统加上日志支持的下一个版本是 ext3 文件系统,它和 ext2 文件系统在硬盘布局上是一样的,其差别仅仅是 ext3 文件系统在硬盘上多出了一个特殊的 inode(可以理解为一个特殊文件),用来记录文件系统的日志,也即所谓的 journal。由于本文并不讨论日志文件,所以本文的内容对于 ext2 和 ext3 都是适用的。粗略的描述对于 ext2 文件系统来说,硬盘分区首先被划分为一个个的 block,一个 ext2 文件系统上的每个 block 都是一样大小的,
2、但是对于不同的 ext2 文件系统,block 的大小可以有区别。典型的 block 大小是 1024 bytes 或者 4096 bytes。这个大小在创建 ext2 文件系统的时候被决定,它可以由系统管理员指定,也可以由文件系统的创建程序根据硬盘分区的大小,自动选择一个较合理的值。这些 blocks 被聚在一起分成几个大的 block group。每个 block group 中有多少个 block 是固定的。每个 block group 都相对应一个 group descriptor,这些 group descriptor 被聚在一起放在硬盘分区的开头部分,跟在 super block
3、的后面。所谓 super block,我们下面还要讲到。在这个 descriptor 当中有几个重要的 block 指针。我们这里所说的 block 指针,就是指硬盘分区上的 block 号数,比如,指针的值为 0,我们就说它是指向硬盘分区上的 block 0;指针的值为 1023,我们就说它是指向硬盘分区上的 block 1023。我们注意到,一个硬盘分区上的 block 计数是从 0 开始的,并且这个计数对于这个硬盘分区来说是全局性质的。在 block group 的 group descriptor 中,其中有一个 block 指针指向这个 block group 的 block bit
4、map,block bitmap 中的每个 bit 表示一个 block,如果该 bit 为 0,表示该 block 中有数据,如果 bit 为 1,则表示该 block 是空闲的。注意,这个 block bitmap 本身也正好只有一个 block 那么大小。假设 block 大小为 S bytes,那么 block bitmap 当中只能记载 8*S 个 block 的情况(因为一个 byte 等于 8 个 bits,而一个 bit 对应一个 block)。这也就是说,一个 block group 最多只能有 8*S*S bytes 这么大。在 block group 的 group de
5、scriptor 中另有一个 block 指针指向 inode bitmap,这个 bitmap 同样也是正好有一个 block 那么大,里面的每一个 bit 相对应一个 inode。硬盘上的一个 inode 大体上相对应于文件系统上的一个文件或者目录。关于 inode,我们下面还要进一步讲到。在 block group 的 descriptor 中另一个重要的 block 指针,是指向所谓的 inode table。这个 inode table 就不止一个 block 那么大了。这个 inode table 就是这个 block group 中所聚集到的全部 inode 放在一起形成的。一个
6、 inode 当中记载的最关键的信息,是这个 inode 中的用户数据存放在什么地方。我们在前面提到,一个 inode 大体上相对应于文件系统中的一个文件,那么用户文件的内容存放在什么地方,这就是一个 inode 要回答的问题。一个 inode 通过提供一系列的 block 指针,来回答这个问题。这些 block 指针指向的 block,里面就存放了用户文件的内容。2.1 回顾现在我们回顾一下。硬盘分区首先被分为好多个 block。这些 block 聚在一起,被分成几组,也就是 block group。每个 block group 都有一个 group descriptor。所有这些 desc
7、riptor 被聚在一起,放在硬盘分区的开头部分,跟在 super block 的后面。从 group descriptor 我们可以通过 block 指针,找到这个 block group 的 inode table 和 block bitmap 等等。从 inode table 里面,我们就可以看到一个个的 inode 了。从一个 inode,我们通过它里面的 block 指针,就可以进而找到存放用户数据的那些 block。我们还要提一下,block 指针不是可以到处乱指的。一个 block group 的 block bitmap 和 inode bitmap 以及 inode tabl
8、e,都依次存放在这个 block group 的开头部分,而那些存放用户数据的 block 就紧跟在它们的后面。一个 block group 结束后,另一个 block group 又跟着开始。详细的布局情况3.1 Super Block所谓 ext2 文件系统的 super block,就是硬盘分区开头(开头的第一个 byte 是 byte 0)从 byte 1024 开始往后的一部分数据。由于 block size 最小是 1024 bytes,所以 super block 可能是在 block 1 中(此时 block 的大小正好是 1024 bytes),也可能是在 block 0 中
9、。硬盘分区上 ext3 文件系统的 super block 的详细情况如下。其中 _u32 是表示 unsigned 不带符号的 32 bits 的数据类型,其余类推。这是 Linux 内核中所用到的数据类型,如果是开发用户空间(user-space)的程序,可以根据具体计算机平台的情况,用 unsigned long 等等来代替。下面列表中关于 fragments 的部分可以忽略,Linux 上的 ext3 文件系统并没有实现 fragments 这个特性。另外要注意,ext3 文件系统在硬盘分区上的数据是按照 Intel 的 Little-endian 格式存放的,如果是在 PC 以外的平
10、台上开发 ext3 相关的程序,要特别注意这一点。如果只是在 PC 上做开发,倒不用特别注意。struct ext3_super_block /*00*/ _u32 s_inodes_count; /* inodes 计数 */_u32 s_blocks_count; /* blocks 计数 */_u32 s_r_blocks_count; /* 保留的 blocks 计数 */_u32 s_free_blocks_count; /* 空闲的 blocks 计数 */*10*/ _u32 s_free_inodes_count; /* 空闲的 inodes 计数 */_u32 s_first
11、_data_block; /* 第一个数据 block */_u32 s_log_block_size; /* block 的大小 */_s32 s_log_frag_size; /* 可以忽略 */*20*/ _u32 s_blocks_per_group; /* 每 block group 的 block 数量 */_u32 s_frags_per_group; /* 可以忽略 */_u32 s_inodes_per_group; /* 每 block group 的 inode 数量 */_u32 s_mtime; /* Mount time */*30*/ _u32 s_wtime;
12、/* Write time */_u16 s_mnt_count; /* Mount count */_s16 s_max_mnt_count; /* Maximal mount count */_u16 s_magic; /* Magic 签名 */_u16 s_state; /* File system state */_u16 s_errors; /* Behaviour when detecting errors */_u16 s_minor_rev_level; /* minor revision level */*40*/ _u32 s_lastcheck; /* time of
13、last check */_u32 s_checkinterval; /* max. time between checks */_u32 s_creator_os; /* 可以忽略 */_u32 s_rev_level; /* Revision level */*50*/ _u16 s_def_resuid; /* Default uid for reserved blocks */_u16 s_def_resgid; /* Default gid for reserved blocks */_u32 s_first_ino; /* First non-reserved inode */_u
14、16 s_inode_size; /* size of inode structure */_u16 s_block_group_nr; /* block group # of this superblock */_u32 s_feature_compat; /* compatible feature set */*60*/ _u32 s_feature_incompat; /* incompatible feature set */_u32 s_feature_ro_compat; /* readonly-compatible feature set */*68*/ _u8 s_uuid16
15、; /* 128-bit uuid for volume */*78*/ char s_volume_name16; /* volume name */*88*/ char s_last_mounted64; /* directory where last mounted */*C8*/ _u32 s_algorithm_usage_bitmap; /* 可以忽略 */_u8 s_prealloc_blocks; /* 可以忽略 */_u8 s_prealloc_dir_blocks; /* 可以忽略 */_u16 s_padding1; /* 可以忽略 */*D0*/ _u8 s_journ
16、al_uuid16; /* uuid of journal superblock */*E0*/ _u32 s_journal_inum; /* 日志文件的 inode 号数 */_u32 s_journal_dev; /* 日志文件的设备号 */_u32 s_last_orphan; /* start of list of inodes to delete */*EC*/ _u32 s_reserved197; /* 可以忽略 */;我们可以看到,super block 一共有 1024 bytes 那么大。在 super block 中,我们第一个要关心的字段是 magic 签名,对于 e
17、xt2 和 ext3 文件系统来说,这个字段的值应该正好等于 0xEF53。如果不等的话,那么这个硬盘分区上肯定不是一个正常的 ext2 或 ext3 文件系统。从这里,我们也可以估计到,ext2 和 ext3 的兼容性一定是很强的,不然的话,Linux 内核的开发者应该会为 ext3 文件系统另选一个 magic 签名才对。在 super block 中另一个重要的字段是 s_log_block_size。从这个字段,我们可以得出真正的 block 的大小。我们把真正 block 的大小记作 B,B = 1 s_blocksize)*/inode_size EXT2_BLOCK_SIZE(e
18、xit(1);if (inode_size != EXT2_GOOD_OLD_INODE_SIZE)fprintf(stderr, _(”Warning: %d-byte inodes not usable ”“on most systemsn”),inode_size);fs_param.s_inode_size = inode_size;OK,通过源码我们可以清楚地发现就算用户可以通过-I 选项自定义 Inode size,但是必须遵循以下三个条件:1.必须大于等于 128;2.必须小于逻辑块的大小;3.必须是 128 的倍数;笔者认为真正的默认值是在 mke2fs.conf 中定义的:d
19、efaultsbase_features = sparse_super,filetype,resize_inode,dir_index,ext_attrblocksize = 4096inode_size = 256 /*通过查看源代码,发现 256 才是真正的默认值*/inode_ratio = 16384明确了一个 Inode 自身所占空间以后,我们来看这么个问题:在一个块组中有 Inode 的数量是如何规划的呢?EXT2 的默认规则是每 16K 空间分配一个Inode(关于此参数请查看上表,其实在程序中还有一个参数就是 8K 不过是要当读取 mke2fs.conf 文件失败时才启用。这里
20、先以 inode_ratio = 16384 为默认值)让我们用这种规则计算下 Inode 的数量(1G 磁盘空间,逻辑块为 4096Bytes):Inode 的个数:128M Bytes / 16K Bytes = 8192Inode 所占空间:8192 * 256bytes = 2048K Bytes当然如果我们打算只在这 1G 的空间里放一些电影或者照片等比较大的文件,那么每 16K 空间分配一个 Inode 的这种策略也是浪费空间的,因此格式化工具mke2fs 允许用户通过 -i(小写) 自定义每 N 空间分配一个 Inode,case i:inode_ratio = strtoul(
21、optarg, /*如果 inode 的个数小于 1024*/if (inode_ratio EXT2_MAX_BLOCK_SIZE * 1024 |/*或者用户输入出错*/*tmp) com_err(program_name, 0,_(“invalid inode ratio %s (min %d/max %d)“),optarg, EXT2_MIN_BLOCK_SIZE,EXT2_MAX_BLOCK_SIZE);exit(1);break;在用户输入有效数字的基础上,遵循以下两个条件:1.必须大于等于 EXT2_MIN_BLOCK_SIZE;2.必须小于等于 EXT2_MAX_BLOCK_
22、SIZE * 1024;设定最小值的含义是:每 1024 个字节就需要一个 Inode,也就是说在这个磁盘空间里用户预计放的是很多小于 1K 的文件。设定最大值的含义是:每 EXT2_MAX_BLOCK_SIZE * 1024 个字节 (= 4M)才需要一个 Inode,也就是说在这个磁盘空间里用户预计放的是大于等于 4M 的文件。 直接寻址和间接寻址前文说过 Inode 的作用有两个,一是数据的“指针”,二是保存文件的属性。关于这两个作用,是通过在 Inode 这个结构体中保存各种文件属性(或数据指针)的值实现的。查看 Inode 的结构体和 ext2_fs.h 文件,可以发现一个 Inod
23、e至多可以保存 15 个指针,如下:。_le32 i_blockEXT2_N_BLOCKS; /* Pointers to blocks 至多可以有 15 个“指针” 指向真正存放文件数据的地方*/* Constants relative to the data blocks*/#define EXT2_NDIR_BLOCKS 12#define EXT2_IND_BLOCK EXT2_NDIR_BLOCKS#define EXT2_DIND_BLOCK (EXT2_IND_BLOCK + 1)#define EXT2_TIND_BLOCK (EXT2_DIND_BLOCK + 1)#defi
24、ne EXT2_N_BLOCKS (EXT2_TIND_BLOCK + 1)不难看出,对于一个 Inode 来说,其指针数组结构如下:如果 15 个指针都是直接指向(直接寻址)数据块,而每个数据块的大小而4K,那么一个 Inode 最大能指向的数据仅为 14*5 = 60K,很显然这种直接寻址的方案是不可用的,因此在 EXT2 规定 0-11的指针采用直接寻址的方式,而 12-14 的指针采用间接寻址的方式,12 号指针采用一级间接寻址,13 号指针采用二级间接寻址,14 号指针采用三级间接寻址,示意图如下: 一个逻辑块最多可以保存 BlockSize/4 个指针,如 BlockSize 为
25、4096,就一级间接寻址而言,可表示的最大空间为:( (4096 / 4) + 12 )*4K = 4144K Bytes;就二级间接寻址而言,可表示的最大空间为:( (4096 / 4)2 +(4096 / 4) + 12 )* 4K = 1049648K Bytes = 1025.04M Bytes ;就三级间接寻址而言,可表示的最大空间为:( (4096 / 4)3+(4096 / 4)2 +(4096 / 4) + 12 )* 4K = 4299165744 K Bytes = 4198404.046M Bytes = 4100G Bytes = 4T Bytes通过这种方式,就算是读
26、取大文件的次数也只需 4 次操作(三级寻址),因此存取性能是很好的。心细的朋友一定发现了,这里是用 EXT2 最大允许的 BlockSize 来计算 Inode最能表示的最大单个文件,计算的结果约等于是 4T,那是不是就是说 EXT2 允许最大单文件的大小为 4T 呢?答案是否定的,关于这点,还是通过查看源代码的方式比较清晰,打开/fs/ext2/super.c,可以看到:/* Maximal file size. There is a direct, and ,double-,triple-indirect* block limit, and also a limit of (232 - 1
27、) 512-byte sectors in i_blocks.* We need to be 1 filesystem block less than the 232 sector limit.*/static loff_t ext2_max_size(int bits)loff_t res = EXT2_NDIR_BLOCKS;int meta_blocks;loff_t upper_limit;/* This is calculated to be the largest file size for a* dense, file such that the total number of*
28、 sectors in the file, including data and all indirect blocks,* does not exceed 232 -1* _u32 i_blocks representing the total number of* 512 bytes blocks of the file*/upper_limit = (1LL = (bits - 9);/* indirect blocks */meta_blocks = 1;/* double indirect blocks */meta_blocks += 1 + (1LL upper_limit)re
29、s = upper_limit;if (res MAX_LFS_FILESIZE)res = MAX_LFS_FILESIZE;return res;可见,函数 ext2_max_size 的计算结果受两个条件的限制:1. 小于等于 (1LL 32) 12. 小于等于 MAX_LFS_FILESIZE 文件读取与 Inode简单地说,Inode 是找到文件的“钥匙”,一个文件对一个 Inode。那么利用 Inode 到底是如何找到我们需要的文件数据的呢?这里设定一个场景:假设有用户在 shell 中输入 more /usr/test.txt 后,内核的运作步骤如下:1. 找到块组描述表的第一个
30、块组描述符,并获得 Inode 表的起始块号;2. 找到 Inode 表所在的这个块,根据预先定义的 Inode size 偏移到第二个Inode 结构体的首地址,EXT2 规定第二个 Inode 才属于根目录的;3. 根据根目录的 Inode 所标志的数据块号进行地址偏移,获得根目录的数据(EXT2 规定,目录才是特殊的文件,只不过其在数据块中保存的是目录下文件和 Inode 的信息。)找到目录 etc 的 Inode 号;4. 通过 Inode 表找到目录 usr 的 Inode 结构体的首地址;5. 通过目录 usr 的 Inode 所标志的数据块号进行地址偏移,获得目录 usr的数据块,并获得目录 usr 的数据,找到目录 samba 的 Inode 号;6. 通过 Inode 表找到文件 test.txt 的 Inode 结构体的首地址;通过文件 test.txt 的 Inode 所标志的数据块号进行地址偏移,获得数据块。另外为了进一步地提高 IO 性能,EXT2 还通过缓冲区高速缓存等手段来提高IO 性能。