Ø EXT2中块组的划分
块组是非常重要的概念,首先请朋友们弄清楚分区和块组是完全不同的概念,分区至多只能有4个(3个主分区和1个扩展分区,如果4个分区不够用,只能用逻辑分区),分区的作用就是对磁盘进行分割,分区之后才能用文件系统对分区进行格式化;块组是基于文件系统的概念,EXT2在分区的基础上格式化后会形成至少1个以上的块组。
那么在EXT2中是用什么规则来划分块组的呢?由于EXT2规定,块位图能且只能占一个逻辑块,因此块位图事实上成为了块组划分的一个标准(或者说是限制)。举个例子:
用EXT2格式化一个1G大小的分区,逻辑块的大小为4096字节,求此分区被格式化后有多少个块组?
一个逻辑块的位图大小为:4096 * 8 = 32768,也就说一个块组最多只能有32768个逻辑块(因为块位图只能管32768个逻辑块)
32768个逻辑块的大小为:32768 * 4096 = 134217728 Bytes = 131072K Bytes = 128M Bytes
1024M Bytes / 128M Bytes = 8
因此被格式化后有8个块组,每个块组的大小为128M Bytes,编号从0开始。当然利用格式化工具,如mke2fs是可以指定块组的个数的。
Ø Inode是什么
终于要讨论Inode了,在之前都是一笔带过,实际上正是Inode的存在才使得EXT2能实现高速存储和读取。
首先Inode是在EXT2中指向数据块的“指针”,然后Inode保存着文件的属性;在EXT2中,文件的数据和属性是分开保存的,文件的属性保存在Inode表中,文件的数据保存在数据块中。以下是Inode的结构体定义:
struct ext2_inode { __le16 i_mode; /* File mode 文件模式:普通文件、目录、字符设备等等*/ __le16 i_uid; /* Low 16 bits of Owner Uid 拥有者ID*/ __le32 i_size; /* Size in bytes 文件大小*/ __le32 i_atime; /* Access time 最近访问时间*/ __le32 i_ctime; /* Creation time 创建时间*/ __le32 i_mtime; /* Modification time 修改时间*/ __le32 i_dtime; /* Deletion Time 删除时间*/ __le16 i_gid; /* Low 16 bits of Group Id 用户组ID*/ __le16 i_links_count; /* Links count 连接数*/ __le32 i_blocks; /* Blocks count 物理块的数量*/ __le32 i_flags; /* File flags */ union { struct { __le32 l_i_reserved1; } linux1; struct { __le32 h_i_translator; } hurd1; struct { __le32 m_i_reserved1; } masix1; } osd1; /* OS dependent 1 所属操作系统*/ __le32 i_block[EXT2_N_BLOCKS];/* Pointers to blocks 至多可以有15个“指针” 指向真正存放文件数据的地方*/ __le32 i_generation; /* File version (for NFS) 文件版本 */ __le32 i_file_acl; /* File ACL 文件访问权限*/ __le32 i_dir_acl; /* Directory ACL 目录访问权限*/ __le32 i_faddr; /* Fragment address 片地址*/ union { struct { __u8 l_i_frag; /* Fragment number */ __u8 l_i_fsize; /* Fragment size */ __u16 i_pad1; __le16 l_i_uid_high; /* these 2 fields */ __le16 l_i_gid_high; /* were reserved2[0] */ __u32 l_i_reserved2; } linux2; struct { __u8 h_i_frag; /* Fragment number */ __u8 h_i_fsize; /* Fragment size */ __le16 h_i_mode_high; __le16 h_i_uid_high; __le16 h_i_gid_high; __le32 h_i_author; } hurd2; struct { __u8 m_i_frag; /* Fragment number */ __u8 m_i_fsize; /* Fragment size */ __u16 m_pad1; __u32 m_i_reserved2[2]; } masix2; } osd2; /* OS dependent 2 */ }; |
用sizeof(ext2_inode)查看这里的结构体ext2_inode大小为120,可是N多文章里面均提到Inode size的默认值为128,究竟谁才是正确的呢?打开ext2_fs.h这个文件可以发现,这里有这么句话:
#define EXT2_GOOD_OLD_INODE_SIZE 128 |
首先,先明显一个概念,Inode size不一定为128,因为可以通过格式化工具mke2fs显示指定Inode size(具体请查看man 8 mke2fs)。;然后查看格式化工具mke2fs的源代码(下载地址:http://e2fsprogs.sourceforge.net/)打开e2fsprogs-1.39/misc/mke2fs.c,可以注意到
while ((c = getopt (argc, argv, "b:cf:g:i:jl:m:no:qr:s:tvE:FI:J:L:M:N:O:R:ST:V")) != EOF) { switch (c) { ... case 'I': inode_size = strtoul(optarg, &tmp, 0); /**-I 可以显示的改变Inode的大小*/ ... if (inode_size) { /**如果用户定义的Inode size小于128*/ if (inode_size < EXT2_GOOD_OLD_INODE_SIZE || /** 或者如果用户定义的Inode size大于逻辑块的大小 # define EXT2_BLOCK_SIZE(s) ((s)->s_blocksize)*/ inode_size > EXT2_BLOCK_SIZE(&fs_param) || /** 或者如果用户定义的Inode size不是128的倍数*/ inode_size & (inode_size - 1)) { com_err(program_name, 0, _(”invalid inode size %d (min %d/max %d)”), inode_size, EXT2_GOOD_OLD_INODE_SIZE, blocksize); exit(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中定义的:
[defaults] base_features = sparse_super,filetype,resize_inode,dir_index,ext_attr blocksize = 4096 inode_size = 256 /**通过查看源代码,发现256才是真正的默认值*/ inode_ratio = 16384 |
明确了一个Inode自身所占空间以后,我们来看这么个问题:在一个块组中有Inode的数量是如何规划的呢?EXT2的默认规则是每16K空间分配一个Inode(关于此参数请查看上表,其实在程序中还有一个参数就是8K不过是要当读取mke2fs.conf文件失败时才启用。这里先以inode_ratio = 16384为默认值)
让我们用这种规则计算下Inode的数量(1G磁盘空间,逻辑块为4096Bytes):
Inode的个数:128M Bytes / 16K Bytes = 8192
Inode所占空间:8192 * 256bytes = 2048K Bytes
当然如果我们打算只在这1G的空间里放一些电影或者照片等比较大的文件,那么每16K空间分配一个Inode的这种策略也是浪费空间的,因此格式化工具mke2fs允许用户通过-i(小写)自定义每N空间分配一个Inode,
….. case 'i': inode_ratio = strtoul(optarg, &tmp, 0); /**如果inode的个数小于1024*/ if (inode_ratio < EXT2_MIN_BLOCK_SIZE || /**或者如果inode的个数大于4096*1024*/ 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_SIZE * 1024;
设定最小值的含义是:每1024个字节就需要一个Inode,也就是说在这个磁盘空间里用户预计放的是很多小于1K的文件。
设定最大值的含义是:每EXT2_MAX_BLOCK_SIZE * 1024个字节 (= 4M)才需要一个Inode,也就是说在这个磁盘空间里用户预计放的是大于等于4M的文件。
Ø 直接寻址和间接寻址
前文说过Inode的作用有两个,一是数据的“指针”,二是保存文件的属性。关于这两个作用,是通过在Inode这个结构体中保存各种文件属性(或数据指针)的值实现的。查看Inode的结构体和ext2_fs.h文件,可以发现一个Inode至多可以保存15个指针,如下:。
__le32 i_block[EXT2_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) #define 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为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
通过这种方式,就算是读取大文件的次数也只需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 (2^32 - 1) 512-byte sectors in i_blocks. * We need to be 1 filesystem block less than the 2^32 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 * sectors in the file, including data and all indirect blocks, * does not exceed 2^32 -1 * __u32 i_blocks representing the total number of * 512 bytes blocks of the file */ upper_limit = (1LL << 32) - 1;
/* total blocks in file system block size */ upper_limit >>= (bits - 9);
/* indirect blocks */ meta_blocks = 1; /* double indirect blocks */ meta_blocks += 1 + (1LL << (bits-2)); /* tripple indirect blocks */ meta_blocks += 1 + (1LL << (bits-2)) + (1LL << (2*(bits-2)));
upper_limit -= meta_blocks; upper_limit <<= bits;
res += 1LL << (bits-2); res += 1LL << (2*(bits-2)); res += 1LL << (3*(bits-2)); res <<= bits; if (res > upper_limit) res = upper_limit;
if (res > MAX_LFS_FILESIZE) res = MAX_LFS_FILESIZE;
return res; } |
可见,函数ext2_max_size的计算结果受两个条件的限制:
1. 小于等于 (1LL << 32) – 1
2. 小于等于 MAX_LFS_FILESIZE
Ø 文件读取与Inode
简单地说,Inode是找到文件的“钥匙”,一个文件对一个Inode。那么利用Inode到底是如何找到我们需要的文件数据的呢?这里设定一个场景:假设有用户在shell中输入more /usr/test.txt后,内核的运作步骤如下:
1. 找到块组描述表的第一个块组描述符,并获得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性能。
参考资料:
[ULK] 深入理解Linux内核第三版. DanielP. BovetMarcoCesati
Linux 编程一站式学习
[UNIX] The Art of UNIX Programming. EricRaymond