1. 知识扫盲
1.1. 磁盘介绍
磁盘(disk):如上(网图)
磁道(Track):将磁道划分为若干个小的区段,就是扇区。
柱面(cylinder):磁片中半径相同的同心磁道构成“柱面",意思是这一系列的磁道垂直叠在一起,就形成一个柱面的形状。磁道数=柱面数
扇区(sector):一个扇区512字节
磁头(header):每张磁片的正反两面各有一个磁头,一个磁头对应一张磁片的一个面。
磁盘空间:512 * Track * Cylinder * header
1.2. 寻址方式
Chs寻址:有上述提到的三个参数决定,C代表Cylinder,H代表Header,S代表Sector。
举例说明:
磁头数最大为255 (用 8 个二进制位存储)。从0开始编号。
柱面数最大为1023(用 10 个二进制位存储)。从0开始编号。
扇区数最大数 63(用 6个二进制位存储)。从1始编号。
所以CHS寻址方式的最大寻址范围为:
255 * 1023 * 63 * 512 / 1048576 = 7.837 GB ( 1M =1048576 Bytes )
或硬盘厂商常用的单位:
255 * 1023 * 63 * 512 / 1000000 = 8.414 GB ( 1M =1000000 Bytes )
缺点:外圈磁盘空间利用率不足
LBA寻址:LBA编址方式将 CHS这种三维寻址方式转变为一维的线性寻址,它把硬盘所有的物理扇区的C/H/S编号通过一定的规则转变为一线性的编号。
备注:有兴趣自己研究。
1.3. 设备
分区:切蛋糕,把一块磁盘分成多个部分使用。
裸设备:又叫裸分区,未经过格式化,不用通过文件系统读取的特殊字符设备。
文件系统:文件的系统是操作系统用于明确磁盘或分区上的文件的方法和数据结构;即在磁盘上组织文件的方法。
块设备:系统中能够随机(不需要按顺序)访问固定大小数据片(chunks)的设备被称作块设备。通常块设备有缓存。
字符设备:字符设备按照字符流的方式被有序访问。
2. 文件系统Ext4的解析
2.1. 文件系统的组织结构
如上图:文件系统中存在四种类型的资源,分别是目录、文件、软连接、硬链接;我们将在本文中讲清楚这几种类型资源的存储形式。
2.2. 磁盘文件系统解析
当在Linux系统中插入一块没有经过格式化磁盘的时候,我们可以通过
fdisk -l命令,查看格式化和非格式化分区。
假设现在系统装插入了一块未格式化的硬盘:/dev/vdb,通过fdisk /dev/vdb命令将磁盘划分为分区:/dev/vdb;
然后通过mkfs.ext4 /dev/vdb1命令将分区格式化成Ext4文件系统。
2.2.1. inode与数据块
Ext4文件系统是以块(Block)的方式管理文件的,默认单位为4KB一块。
每个文件包含两部分:
第一部分是文件的属性,如:名称、大小、时间、权限等,我们给了它一个高端的名字叫元数据,这部分数据是以inode的结构存储在数据块上;i是index的意思。
第二部分是文件内容,这部分内容直接存储在数据块上。
现在有一个问题是这两部分如何组成一个文件?答案在inode结构体里:
- struct ext4_inode {
- __le16 i_mode; /* File mode */
- __le16 i_uid; /* Low 16 bits of Owner Uid */
- __le32 i_size_lo; /* Size in bytes */
- __le32 i_atime; /* Access time */
- __le32 i_ctime; /* Inode Change time */
- __le32 i_mtime; /* Modification time */
- __le32 i_dtime; /* Deletion Time */
- __le16 i_gid; /* Low 16 bits of Group Id */
- __le16 i_links_count; /* Links count */
- __le32 i_blocks_lo; /* Blocks count */
- __le32 i_flags; /* File flags */
- union {
- struct {
- __le32 l_i_version;
- } linux1;
- struct {
- __u32 h_i_translator;
- } hurd1;
- struct {
- __u32 m_i_reserved1;
- } masix1;
- } osd1; /* OS dependent 1 */
- __le32 i_block[EXT4_N_BLOCKS];/* Pointers to blocks */
- __le32 i_generation; /* File version (for NFS) */
- __le32 i_file_acl_lo; /* File ACL */
- __le32 i_size_high;
- __le32 i_obso_faddr; /* Obsoleted fragment address */
- union {
- struct {
- __le16 l_i_blocks_high; /* were l_i_reserved1 */
- __le16 l_i_file_acl_high;
- __le16 l_i_uid_high; /* these 2 fields */
- __le16 l_i_gid_high; /* were reserved2[0] */
- __le16 l_i_checksum_lo;/* crc32c(uuid+inum+inode) LE */
- __le16 l_i_reserved;
- } linux2;
- struct {
- __le16 h_i_reserved1; /* Obsoleted fragment number/size which are removed in ext4 */
- __u16 h_i_mode_high;
- __u16 h_i_uid_high;
- __u16 h_i_gid_high;
- __u32 h_i_author;
- } hurd2;
- struct {
- __le16 h_i_reserved1; /* Obsoleted fragment number/size which are removed in ext4 */
- __le16 m_i_file_acl_high;
- __u32 m_i_reserved2[2];
- } masix2;
- } osd2; /* OS dependent 2 */
- __le16 i_extra_isize;
- __le16 i_checksum_hi; /* crc32c(uuid+inum+inode) BE */
- __le32 i_ctime_extra; /* extra Change time (nsec << 2 | epoch) */
- __le32 i_mtime_extra; /* extra Modification time(nsec << 2 | epoch) */
- __le32 i_atime_extra; /* extra Access time (nsec << 2 | epoch) */
- __le32 i_crtime; /* File Creation time */
- __le32 i_crtime_extra; /* extra FileCreationtime (nsec << 2 | epoch) */
- __le32 i_version_hi; /* high 32 bits for 64-bit version */
- __le32 i_projid; /* Project ID */
- };
|
在结构体中我标红了一项__le32 i_block[EXT4_N_BLOCKS]; 就是通过这个结构,文件系统将文件的属性和数据关联了起来。EXT4_N_BLOCKS定义如下:
- #define EXT4_NDIR_BLOCKS 12
- #define EXT4_IND_BLOCK EXT4_NDIR_BLOCKS
- #define EXT4_DIND_BLOCK (EXT4_IND_BLOCK + 1)
- #define EXT4_TIND_BLOCK (EXT4_DIND_BLOCK + 1)
- #define EXT4_N_BLOCKS (EXT4_TIND_BLOCK + 1)
|
可以得出EXT4_N_BLOCKS=15, 这个结构体共占用4byte * 15 = 60byte,我们知道一个文件小的几KB,大的有几G甚至几十G,这60个字节究竟是怎么关联起所有的数据块的?
EXT4文件系统为此引入了一个概念:Extents,把存储数据块地址的内容做成了树状结构,在inode节点中的60byte存放1个Header和4个Entry:
Header结构如下:
- /*
- * Each block (leaves and indexes), even inode-stored has header.
- */
- struct ext4_extent_header {
- __le16 eh_magic; /* probably will support different formats */
- __le16 eh_entries; /* number of valid entries */
- __le16 eh_max; /* capacity of store in entries */
- __le16 eh_depth; /* has tree real underlying blocks? */
- __le32 eh_generation; /* generation of the tree */
- };
|
eh_entries 表示这个节点里面有多少项。这里的项分两种,如果是叶子节点,这一项会直接指向硬盘上的连续块的地址,我们称为数据节点 ext4_extent;如果是分支节点,这一项会指向下一层的分支节点或者叶子节点,我们称为索引节点 ext4_extent_idx。这两种类型的项的大小都是 12 个 byte。
数据节点结构如下:
- /*
- * This is the extent on-disk structure.
- * It's used at the bottom of the tree.
- */
- struct ext4_extent {
- __le32 ee_block; /* first logical block extent covers */
- __le16 ee_len; /* number of blocks covered by extent */
- __le16 ee_start_hi; /* high 16 bits of physical block */
- __le32 ee_start_lo; /* low 32 bits of physical block */
- };
|
ee_len的长度是16位,所以个extent最多可表示 215 * 4KB = 128M,i_block共占用60byte,除去Header部分12byte,剩下的48byte可容纳4个extent,所以文件未超过 128M * 4 = 512M的情况下,inode中的i_block就够用了。
索引节点结构如下:
- /*
- * This is index on-disk structure.
- * It's used at all the levels except the bottom.
- */
- struct ext4_extent_idx {
- __le32 ei_block; /* index covers logical blocks from 'block' */
- __le32 ei_leaf_lo; /* pointer to the physical block of the next *
- * level. leaf or next index could be there */
- __le16 ei_leaf_hi; /* high 16 bits of physical block */
- __u16 ei_unused;
- };
|
当文件超过512M后,i_block中的4个ext4_extent就会变成索引节点ext4_extent_idx, i_block的ext4_header中的值eh_depth的值由0变成1,此时需要一个额外的块存储叶子节点。一个块占用4KB,除去12byte的Header其他全都用来存储ext4_extent,共可以存储:
(4096 - 12)/ 12 = 340 个ext4_extent,算下来可以存储 340 * 128M = 42.5G,当树第一次分裂占用一个块去存储ext4_extent的情况下,文件就已经非常大了。
到这里我们已经知道了一个文件是如何存储在磁盘上的了。
2.2.2. inode与数据块位图
回到实际场景上来,假如现在要创建一个新文件,文件系统上有那么多的inode和block,难道我每次创建文件时都要遍历所有inode块看系统中是否有空闲的inode给新文件?如果这样效率会打大折扣,所以文件系统中引入了inode位图和block位图。
使用一块存储inode位图,每个bit位一个inode,0表示inode未使用,1表示inode已经被使用。数据块也是同样的原理。
我们以一个创建文件的实例分析一下inode位图的使用,这里直接给出文件创建流程(以后再详细分析创建文件流程):
- // 文件创建流程:
- open
- do_sys_open
- do_filp_open
- path_openat
- do_last
- lookup_open
- dir_inode->i_op->create
- // 最后调用inode操作的create函数,实际上就是ext4_create
- const struct inode_operations ext4_dir_inode_operations = {
- .create = ext4_create,
- .lookup = ext4_lookup,
- .link = ext4_link,
- .unlink = ext4_unlink,
- .symlink = ext4_symlink,
- .mkdir = ext4_mkdir,
- .rmdir = ext4_rmdir,
- .mknod = ext4_mknod,
- .tmpfile = ext4_tmpfile,
- .rename = ext4_rename2,
- .setattr = ext4_setattr,
- .getattr = ext4_getattr,
- .listxattr = ext4_listxattr,
- .get_acl = ext4_get_acl,
- .set_acl = ext4_set_acl,
- .fiemap = ext4_fiemap,
- };
|
文件创建流程由用户调用open开始,最后调用的文件系统的ext4_create函数,调用链:ext4_create->ext4_new_inode_start_handle->__ext4_new_inode,
在__ext4_new_inode中,调用ext4_find_next_zero_bit,从文件系统中读取bitmap位图,找到一个空闲的inode作为新文件的inode。
到这里我们就知道了文件系统是如何提高效率的了。
2.2.3. 块组
实际上这里还有一个问题没有解决,一个分区内多少块作为位图块,多少作为数据块?文件系统在这里引入了块组的概念。其结构如下:
一块空间为4KB,那么一块block位图共计有:4KB * 8 = 32K 个位。所以一个组最大可容纳数据量是:32K * 4KB = 128M。
块组描述符表存储的是文件系统中所有的块组描述符列表。每个块组描述符包含块位图、inode位图,空闲块、空闲inode等信息,结构如下:
- /*
- * Structure of a blocks group descriptor
- */
- struct ext4_group_desc
- {
- __le32 bg_block_bitmap_lo; /* Blocks bitmap block */
- __le32 bg_inode_bitmap_lo; /* Inodes bitmap block */
- __le32 bg_inode_table_lo; /* Inodes table block */
- __le16 bg_free_blocks_count_lo;/* Free blocks count */
- __le16 bg_free_inodes_count_lo;/* Free inodes count */
- __le16 bg_used_dirs_count_lo; /* Directories count */
- __le16 bg_flags; /* EXT4_BG_flags (INODE_UNINIT, etc) */
- __le32 bg_exclude_bitmap_lo; /* Exclude bitmap for snapshots */
- __le16 bg_block_bitmap_csum_lo;/* crc32c(s_uuid+grp_num+bbitmap) LE */
- __le16 bg_inode_bitmap_csum_lo;/* crc32c(s_uuid+grp_num+ibitmap) LE */
- __le16 bg_itable_unused_lo; /* Unused inodes count */
- __le16 bg_checksum; /* crc16(sb_uuid+group+desc) */
- __le32 bg_block_bitmap_hi; /* Blocks bitmap block MSB */
- __le32 bg_inode_bitmap_hi; /* Inodes bitmap block MSB */
- __le32 bg_inode_table_hi; /* Inodes table block MSB */
- __le16 bg_free_blocks_count_hi;/* Free blocks count MSB */
- __le16 bg_free_inodes_count_hi;/* Free inodes count MSB */
- __le16 bg_used_dirs_count_hi; /* Directories count MSB */
- __le16 bg_itable_unused_hi; /* Unused inodes count MSB */
- __le32 bg_exclude_bitmap_hi; /* Exclude bitmap block MSB */
- __le16 bg_block_bitmap_csum_hi;/* crc32c(s_uuid+grp_num+bbitmap) BE */
- __le16 bg_inode_bitmap_csum_hi;/* crc32c(s_uuid+grp_num+ibitmap) BE */
- __u32 bg_reserved;
- };
|
同时还有一个超级块Ext4_super_block。这里面有整个文件系统一共有多少 inode,s_inodes_count;一共有多少块,s_blocks_count_lo,每个块组有多少 inode,s_inodes_per_group,每个块组有多少块,s_blocks_per_group 等。这些都是这类的全局信息。
默认情况下,超级块和块组描述符表在每个副本中都存在,但这样着实有点浪费空间,所以文件系统增加了sparse_super 选项,如果开起了sparse_super,超级块和块组描述符表的副本只会保存在块组索引为 0、3、5、7 的整数幂里。
到此文件系统Ext4的存储格式已经介绍完了。
2.2.4. 目录结构
分析完了文件的存储格式,再来看一下目录是如何存储的,其实目录的存储形式与文件的形式一样的!一个inode + 数据块,只不过数据块里存储的内容有了目录自己的格式,这个结构称之为:ext4_dir_entry:
- /*
- * Structure of a directory entry
- */
- #define EXT4_NAME_LEN 255
- struct ext4_dir_entry {
- __le32 inode; /* Inode number */
- __le16 rec_len; /* Directory entry length */
- __le16 name_len; /* Name length */
- char name[EXT4_NAME_LEN]; /* File name */
- };
- /*
- * The new version of the directory entry. Since EXT4 structures are
- * stored in intel byte order, and the name_len field could never be
- * bigger than 255 chars, it's safe to reclaim the extra byte for the
- * file_type field.
- */
- struct ext4_dir_entry_2 {
- __le32 inode; /* Inode number */
- __le16 rec_len; /* Directory entry length */
- __u8 name_len; /* Name length */
- __u8 file_type;
- char name[EXT4_NAME_LEN]; /* File name */
- };
|
按照这种格式,一个目录的数据块的存储形式如下图:
同样这里存在一个效率问题,目录下文件多了之后,我们在一个目录下遍历去查找就太慢了,于是就增加了索引模式。在inode中如果开启了EXT4_INDEX_FL 标志,则目录的组织形式将发生变化:
- struct dx_root
- {
- struct fake_dirent dot;
- char dot_name[4];
- struct fake_dirent dotdot;
- char dotdot_name[4];
- struct dx_root_info
- {
- __le32 reserved_zero;
- u8 hash_version;
- u8 info_length; /* 8 */
- u8 indirect_levels;
- u8 unused_flags;
- }
- info;
- struct dx_entry entries[0];
- };
|
该结构是一个边长结构体,前两项内容是目录 . 和 .. 表示当前目录和上级目录;中间有一个dx_root_info用来标识一些元数据(忽略);最后一部分是ex_entry:
- struct dx_entry{
- __le32 hash;
- __le32 block;
- };
|
其内容是一个hash值和一个block地址的映射,当我们要搜索一个文件名时,可以通过名称取哈希。如果哈希能够匹配上,就说明这个文件的信息在相应的块里面。然后打开这个块,如果里面不再是索引,而是索引树的叶子节点的话,那里面还是 ext4_dir_entry_2 的列表,我们只要一项一项找文件名就行。
2.2.5. 软硬链接
在文件系统中还有一种特殊的的文件叫链接,分为软连接和硬链接,软连接通过:ln -s src_file dest_file创建,硬链接通过:ln src_file, dest_file创建,同样为文件,所以它们也包含inode和数据块,那他们的区别是什么呢?
硬链接与原始文件共用一个 inode 的,但是 inode 是不跨文件系统的,每个文件系统都有自己的 inode 列表,因而硬链接是没有办法跨文件系统的。
软链接相当于重新创建了一个文件。这个文件也有独立的 inode,只不过打开这个文件看里面内容的时候,内容指向另外的一个文件。
3. 实操
讲了这么多肯定就会有人质疑,你讲的到底对不对?实践出真知,下面我们实操一下。
3.1. 准备工作
在2.2 中我已经创建好了一个ext4的文件系统,我将它挂载在虚拟机的/home/vdb目录下,并创建了一个文件test,内容为“hello world”:
3.2. 查看文件系统属性
可以通过dumpe2fs /dev/vdb查看 vdb上的文件系统详细信息:
last mounted on: 挂载点
filesystem features: 文件系统的特性
sparse_super:表示开启了稀疏超级块
inode count: inode总数量
block count:块总数量
Block size:块大小4096
Blocks per group:32768
每组容量:32768 * 4K = 128M
inode size:inode占用空间
3.3. 查看test文件
在3.1中我们看到 test文件的inode编号是13,文件大小是12字节,现在看一下这个13号inode落在了哪个组,还是通过dumpe2fs查看:
可以看出test文件的inode落在了第0组。
inode table at:1062-1573表示inode存储第1062-1573块上。每个块4k,每个inode大小为256字节,那么一块可以存储 4096 / 256 = 16个inode。所以第13号inode在1062的块上面。
那么inode=13的偏移量计算公式如下:
4096 * 1062 + (13 - 1)* 256 = 4353024
查看inode
我们通过hexdump查看磁盘偏移位置的值(即inode的内容),可以看到
5个字节处的值为12,通过查看inode的定义可知它代表文件大小,与test文件大小吻合。
查看数据块
通过inode的定义可知,i_block在inode第48个字节的位置上,而的i_block的前12个字节固定为header信息,所以extent_entry是从第60个字节开始的,第60个字节的值为0x8600 = 34304,计算如下:
34304 * 4096 = 140509184
通过dexdump将140509184偏移处的内容打印出来:
“hello world”,完美!!!
3.4. 文件是否被“删”了
往往我们在删除文件的时候,通过rm -f xxx就把文件删了,但实际上文件并没有真的删除。数据还存储在磁盘块上,删的只是inode而已:
通过上面的操作可以看到我在rm -f test后将原数据块的内容dump出来,“hello world”还在哪里,而13号inode已经被回收了。