一.前言
本节开始将分析Linux的文件系统。Linux一切皆文件的思想可谓众所周知,而其文件系统又是字符设备、块设备、管道、进程间通信、网络等等的必备知识,因此其重要性可想而知。本文将先介绍文件系统基础知识,然后介绍最重要的结构体inode以及构建于其上的一层层的文件系统。
二.文件系统基础知识
一切设计均是为了实现需求,因此我们从文件系统需要的基本功能来看看其该如何设计。首先,一个文件系统需要有以下基本要求
- 文件需要让人易于读写,并避免名字冲突等
- 文件需要易于查找、整理归类
- 操作系统需要有文档记录功能以便管理
由此,文件系统设计了如下特性:
- 采取树形结构、文件夹设计
- 对热点文件进行缓存,便于读写
- 采用索引结构,便于查找分类
- 维护一套数据结构用于记录哪些文档正在被哪些任务使用
依此基本设计,我们可以开始慢慢展开看看Linux博大而精神的文件系统。
三.inode结构体和文件系统
3.1块存储的表示
硬盘中我们以块为存储单元,而在文件系统中,我们需要有一个存储块信息的基本结构体,这就是文件系统的基石inode,其源码如下。inode意为indexnode,即索引节点。从这个数据结构中我们可以看出,inode里面有文件的读写权限i_mode,属于哪个用户i_uid,哪个组i_gid,大小是多少i_size_lo,占用多少个块i_blocks_lo。另外,这里面还有几个与文件相关的时间。i_atime即accesstime,是最近一次访问文件的时间;i_ctime即changetime,是最近一次更改inode的时间;i_mtime即modifytime,是最近一次更改文件的时间。
/**Structureofaninodeonthedisk*/structext4_inode{__le16i_mode;/*Filemode*/__le16i_uid;/*Low16bitsofOwnerUid*/__le32i_size_lo;/*Sizeinbytes*/__le32i_atime;/*Accesstime*/__le32i_ctime;/*InodeChangetime*/__le32i_mtime;/*Modificationtime*/__le32i_dtime;/*DeletionTime*/__le16i_gid;/*Low16bitsofGroupId*/__le16i_links_count;/*Linkscount*/__le32i_blocks_lo;/*Blockscount*/__le32i_flags;/*Fileflags*/......__le32i_block[EXT4_N_BLOCKS];/*Pointerstoblocks*/......};#defineEXT4_NDIR_BLOCKS12#defineEXT4_IND_BLOCKEXT4_NDIR_BLOCKS#defineEXT4_DIND_BLOCK(EXT4_IND_BLOCK+1)#defineEXT4_TIND_BLOCK(EXT4_DIND_BLOCK+1)#defineEXT4_N_BLOCKS(EXT4_TIND_BLOCK+1)
这里我们需要重点关注一下i_block,该成员变量实际存储了文件内容的每一个块。在ext2和ext3格式的文件系统中,我们用前12个块存放对应的文件数据,每个块4KB,如果文件较大放不下,则需要使用后面几个间接存储块来保存数据,下图很形象的表示了其存储原理。
该存储结构带来的问题是对于大型文件,我们需要多次调用才可以访问对应块的内容,因此访问速度较慢。为此,ext4提出了新的解决方案:Extents。简单的说,Extents以一个树形结构来连续存储文件块,从而提高访问速度,大致结构如下图所示。
主要结构体为节点ext4_extent_header,eh_entries表示这个节点里面有多少项。这里的项分两种:
- 如果是叶子节点,这一项会直接指向硬盘上的连续块的地址,我们称为数据节点ext4_extent;
- 如果是分支节点,这一项会指向下一层的分支节点或者叶子节点,我们称为索引节点ext4_extent_idx。这两种类型的项的大小都是12个byte。
如果文件不大,inode里面的i_block中,可以放得下一个ext4_extent_header和4项ext4_extent。所以这个时候,eh_depth为0,也即inode里面的就是叶子节点,树高度为0。如果文件比较大,4个extent放不下,就要分裂成为一棵树,eh_depth>0的节点就是索引节点,其中根节点深度最大,在inode中。最底层eh_depth=0的是叶子节点。除了根节点,其他的节点都保存在一个块4k里面,4k扣除ext4_extent_header的12个byte,剩下的能够放340项,每个extent最大能表示128MB的数据,340个extent会使你表示的文件达到42.5GB。这已经非常大了,如果再大,我们可以增加树的深度。
/**Eachblock(leavesandindexes),eveninode-storedhasheader.*/structext4_extent_header{__le16eh_magic;/*probablywillsupportdifferentformats*/__le16eh_entries;/*numberofvalidentries*/__le16eh_max;/*capacityofstoreinentries*/__le16eh_depth;/*hastreerealunderlyingblocks?*/__le32eh_generation;/*generationofthetree*/};/**Thisistheextenton-diskstructure.*It'susedatthebottomofthetree.*/structext4_extent{__le32ee_block;/*firstlogicalblockextentcovers*/__le16ee_len;/*numberofblockscoveredbyextent*/__le16ee_start_hi;/*high16bitsofphysicalblock*/__le32ee_start_lo;/*low32bitsofphysicalblock*/};/**Thisisindexon-diskstructure.*It'susedatallthelevelsexceptthebottom.*/structext4_extent_idx{__le32ei_block;/*indexcoverslogicalblocksfrom'block'*/__le32ei_leaf_lo;/*pointertothephysicalblockofthenext**level.leafornextindexcouldbethere*/__le16ei_leaf_hi;/*high16bitsofphysicalblock*/__u16ei_unused;};
由此,我们可以通过inode来表示一系列地块,从而构成了一个文件。在硬盘上,通过一系列的inode,我们可以存储大量的文件。但是我们尚需要一种方式去存储和管理inode,这就是位图。同样的,我们会用块位图去管理块的信息。如下所示为创建inode的过程中对位图的访问,我们需要找出下一个0位所在,即空闲inode的位置。
structinode*__ext4_new_inode(handle_t*handle,structinode*dir,umode_tmode,conststructqstr*qstr,__u32goal,uid_t*owner,__u32i_flags,inthandle_type,unsignedintline_no,intnblocks){......inode_bitmap_bh=ext4_read_inode_bitmap(sb,group);......ino=ext4_find_next_zero_bit((unsignedlong*)inode_bitmap_bh->b_data,EXT4_INODES_PER_GROUP(sb),ino);......}
3.2文件系统的格式
inode和块是文件系统的最小组成单元,在此之上还有多级系统,大致有如下这些:
- 块组:存储一块数据的组成单元,数据结构为ext4_group_desc。这里面对于一个块组里的inode位图bg_inode_bitmap_lo、块位图bg_block_bitmap_lo、inode列表bg_inode_table_lo均有相应的定义。一个个块组,就基本构成了我们整个文件系统的结构。
- 块组描述符表:多个块组的描述符构成的表
- 超级块:对整个文件系统的情况进行描述,即ext4_super_block,存储全局信息,如整个文件系统一共有多少inode:s_inodes_count;一共有多少块:s_blocks_count_lo,每个块组有多少inode:s_inodes_per_group,每个块组有多少块:s_blocks_per_group等。
- 引导块:对于整个文件系统,我们需要预留一块区域作为引导区用于操作系统的启动,所以第一个块组的前面要留1K,用于启动引导区。
超级块和块组描述符表都是全局信息,而且这些数据很重要。如果这些数据丢失了,整个文件系统都打不开了,这比一个文件的一个块损坏更严重。所以,这两部分我们都需要备份,但是采取不同的策略。
- 默认策略:在每个块中均保存一份超级块和块组描述表的备份
- sparse_super策略:采取稀疏存储的方式,仅在块组索引为0、3、5、7的整数幂里存储。
- MetaBlockGroups策略:我们将块组分为多个元块组(MetaBlockGroups),每个元块组里面的块组描述符表仅仅包括自己的内容,一个元块组包含64个块组,这样一个元块组中的块组描述符表最多64项。这种做法类似于merkletree,可以在很大程度上优化空间。
3.3目录的存储格式
为了便于文件的查找,我们必须要有索引,即文件目录。其实目录本身也是个文件,也有inode。inode里面也是指向一些块。和普通文件不同的是,普通文件的块里面保存的是文件数据,而目录文件的块里面保存的是目录里面一项一项的文件信息。这些信息我们称为ext4_dir_entry。这里有两个版本,第二个版本ext4_dir_entry_2是将一个16位的name_len,变成了一个8位的name_len和8位的file_type。
structext4_dir_entry{__le32inode;/*Inodenumber*/__le16rec_len;/*Directoryentrylength*/__le16name_len;/*Namelength*/charname[EXT4_NAME_LEN];/*Filename*/};structext4_dir_entry_2{__le32inode;/*Inodenumber*/__le16rec_len;/*Directoryentrylength*/__u8name_len;/*Namelength*/__u8file_type;charname[EXT4_NAME_LEN];/*Filename*/};
在目录文件的块中,最简单的保存格式是列表,就是一项一项地将ext4_dir_entry_2列在哪里。每一项都会保存这个目录的下一级的文件的文件名和对应的inode,通过这个inode,就能找到真正的文件。第一项是“.”,表示当前目录,第二项是“…”,表示上一级目录,接下来就是一项一项的文件名和inode。有时候,如果一个目录下面的文件太多的时候,我们想在这个目录下找一个文件,按照列表一个个去找太慢了,于是我们就添加了索引的模式。如果在inode中设置EXT4_INDEX_FL标志,则目录文件的块的组织形式将发生变化,变成了下面定义的这个样子:
structdx_root{structfake_direntdot;chardot_name[4];structfake_direntdotdot;chardotdot_name[4];structdx_root_info{__le32reserved_zero;u8hash_version;u8info_length;/*8*/u8indirect_levels;u8unused_flags;}info;structdx_entryentries[0];};
当前目录和上级目录不变,文件列表改用dx_root_info结构体,其中最重要的成员变量是indirect_levels,表示间接索引的层数。索引项由结构体dx_entry表示,本质上是文件名的哈希值和数据块的一个映射关系。
structdx_entry{__le32hash;__le32block;};
如果我们要查找一个目录下面的文件名,可以通过名称取哈希。如果哈希能够匹配上,就说明这个文件的信息在相应的块里面。然后打开这个块,如果里面不再是索引,而是索引树的叶子节点的话,那里面还是ext4_dir_entry_2的列表,我们只要一项一项找文件名就行。通过索引树,我们可以将一个目录下面的N多的文件分散到很多的块里面,可以很快地进行查找。
3.4软链接和硬链接的存储格式
软链接和硬链接也是文件的一种,可以通过如下命令创建。ln-s创建的是软链接,不带-s创建的是硬链接。
ln[参数][源文件或目录][目标文件或目录]
硬链接与原始文件共用一个inode,但是inode是不跨文件系统的,每个文件系统都有自己的inode列表,因而硬链接是没有办法跨文件系统的。而软链接不同,软链接相当于重新创建了一个文件。这个文件也有独立的inode,只不过打开这个文件看里面内容的时候,内容指向另外的一个文件。这就很灵活了。我们可以跨文件系统,甚至目标文件被删除了链接文件也依然存在,只不过指向的文件找不到了而已。
四.总结
本文主要从文件系统的设计角度出发,逐步分析了inode和基于inode的ext4文件系统结构和主要组成部分,下面引用极客时间中的一张图作为总结。