聊聊磁盘文件系统(二)

存储 存储设备
如果一个文件比较大,inode的块号不足以标识所有的数据块,就会使用间接块。文件系统会在硬盘上分配一个数据块,不存储文件数据,专门用来存储块号。

[[406814]]

数据的存放

  • 在 ext2 和 ext3 中,其中前 12 项直接保存了块的位置,也就是说,我们可以通过 i_block[0-11],直接得到保存文件内容的块。

但是,如果一个文件比较大,inode的块号不足以标识所有的数据块,就会使用间接块。文件系统会在硬盘上分配一个数据块,不存储文件数据,专门用来存储块号。该块被称为间接块。inode的长度是固定的。间接块占用的空间对于大文件来说是必要的。但是对于小文件不会带来额外的开销。当我们用到 i_block[12]的时候,就不能直接放数据块的位置了,要不然 i_block 很快就会用完了。这该怎么办呢?我们需要想个办法。我们可以让 i_block[12]指向间接块。也就是说,我们在 i_block[12]里面放间接块的位置,通过 i_block[12]找到间接块后,间接块里面放数据块的位置,通过间接块可以找到数据块。如果文件再大一些,i_block[13]会指向一个块,我们可以用二次间接块。二次间接块里面存放了间接块的位置,间接块里面存放了数据块的位置,数据块里面存放的是真正的数据。如果文件再大一些,i_block[14]会指向三次间接块。

  • ext4文件系统的Extents一棵树:

解释一下 Extents。比方说,一个文件大小为 128M,如果使用 4k 大小的块进行存储,需要 32k 个块。Extents 可以用于存放连续的块,也就是说,我们可以把 128M 放在一个 Extents 里面。这样的话,对大文件的读写性能提高了,文件碎片也减少了。如下图所示:

索引节点区,用来存储索引节点。Inode存储了文件系统对象的一些元信息,如所有者、访问权限(读、写、执行)、类型(是文件还是目录)、内容修改时间、inode修改时间、上次访问时间、对应的文件系统存储块的地址,等等。知道了1个文件的inode号码,就可以在inode元数据中查出文件内容数据的存储地址。对于EXT4的默认情况,一个inode的大小是256字节,inode是EXT4最重要的元数据信息。注意Inode没有文件名称,将在下文中讲述。

  1. struct ext4_inode { 
  2.  __le16 i_mode;  /* File mode */ 
  3.  __le16 i_uid;  /* Low 16 bits of Owner Uid */ 
  4.  __le32 i_size_lo; /* Size in bytes */ 
  5.  __le32 i_atime; /* Access time */ 
  6.  __le32 i_ctime; /* Inode Change time */ 
  7.  __le32 i_mtime; /* Modification time */ 
  8.  __le32 i_dtime; /* Deletion Time */ 
  9.  __le16 i_gid;  /* Low 16 bits of Group Id */ 
  10.  __le16 i_links_count; /* Links count */ 
  11.  __le32 i_blocks_lo; /* Blocks count */ 
  12.  __le32 i_flags; /* File flags */ 
  13.  union { 
  14.   struct { 
  15.    __le32  l_i_version; 
  16.   } linux1; 
  17.   struct { 
  18.    __u32  h_i_translator; 
  19.   } hurd1; 
  20.   struct { 
  21.    __u32  m_i_reserved1; 
  22.   } masix1; 
  23.  } osd1;    /* OS dependent 1 */ 
  24.  __le32 i_block[EXT4_N_BLOCKS];/* Pointers to blocks */ 
  25.  __le32 i_generation; /* File version (for NFS) */ 
  26.  __le32 i_file_acl_lo; /* File ACL */ 
  27.  __le32 i_size_high; 
  28.  __le32 i_obso_faddr; /* Obsoleted fragment address */ 
  29.  union { 
  30.   struct { 
  31.    __le16 l_i_blocks_high; /* were l_i_reserved1 */ 
  32.    __le16 l_i_file_acl_high; 
  33.    __le16 l_i_uid_high; /* these 2 fields */ 
  34.    __le16 l_i_gid_high; /* were reserved2[0] */ 
  35.    __le16 l_i_checksum_lo;/* crc32c(uuid+inum+inode) LE */ 
  36.    __le16 l_i_reserved; 
  37.   } linux2; 
  38.   struct { 
  39.    __le16 h_i_reserved1; /* Obsoleted fragment number/size which are removed in ext4 */ 
  40.    __u16 h_i_mode_high; 
  41.    __u16 h_i_uid_high; 
  42.    __u16 h_i_gid_high; 
  43.    __u32 h_i_author; 
  44.   } hurd2; 
  45.   struct { 
  46.    __le16 h_i_reserved1; /* Obsoleted fragment number/size which are removed in ext4 */ 
  47.    __le16 m_i_file_acl_high; 
  48.    __u32 m_i_reserved2[2]; 
  49.   } masix2; 
  50.  } osd2;    /* OS dependent 2 */ 
  51.  __le16 i_extra_isize; 
  52.  __le16 i_checksum_hi; /* crc32c(uuid+inum+inode) BE */ 
  53.  __le32  i_ctime_extra;  /* extra Change time      (nsec << 2 | epoch) */ 
  54.  __le32  i_mtime_extra;  /* extra Modification time(nsec << 2 | epoch) */ 
  55.  __le32  i_atime_extra;  /* extra Access time      (nsec << 2 | epoch) */ 
  56.  __le32  i_crtime;       /* File Creation time */ 
  57.  __le32  i_crtime_extra; /* extra FileCreationtime (nsec << 2 | epoch) */ 
  58.  __le32  i_version_hi; /* high 32 bits for 64-bit version */ 
  59.  __le32 i_projid; /* Project ID */ 
  60. }; 

普通文件的存储格式

数据块区,则用来存储文件数据。i_block,我们来看看EXT4_N_BLOCKS的具体定义:

  1. #define  EXT4_NDIR_BLOCKS    12 
  2. #define  EXT4_IND_BLOCK      EXT4_NDIR_BLOCKS 
  3. #define  EXT4_DIND_BLOCK      (EXT4_IND_BLOCK + 1) 
  4. #define  EXT4_TIND_BLOCK      (EXT4_DIND_BLOCK + 1) 
  5. #define  EXT4_N_BLOCKS      (EXT4_TIND_BLOCK + 1) 

inode 里面的 i_block 中,可以放得下一个 ext4_extent_header 和 4 项 ext4_extent。

  1. struct ext4_extent_header {  
  2.     __le16 eh_magic; /* probably will support different formats */  
  3.      
  4.     __le16 eh_entries; /* number of valid entries */  
  5.                             
  6.     __le16 eh_max; /* capacity of store in entries */  
  7.                            
  8.     __le16 eh_depth; /* has tree real underlying blocks? */  
  9.                             
  10.     __le32 eh_generation; /* generation of the tree */ 
  11. }; 
  1. /* 
  2.  * This is the extent on-disk structure. 
  3.  * It's used at the bottom of the tree. 
  4.  */ 
  5. struct ext4_extent { 
  6.   __le32  ee_block;  /* first logical block extent covers */ 
  7.   __le16  ee_len;    /* number of blocks covered by extent */ 
  8.   __le16  ee_start_hi;  /* high 16 bits of physical block */ 
  9.   __le32  ee_start_lo;  /* low 32 bits of physical block */ 
  10. }; 
  11. /* 
  12.  * This is index on-disk structure. 
  13.  * It's used at all the levels except the bottom. 
  14.  */ 
  15. struct ext4_extent_idx { 
  16.   __le32  ei_block;  /* index covers logical blocks from 'block' */ 
  17.   __le32  ei_leaf_lo;  /* pointer to the physical block of the next * 
  18.          * level. leaf or next index could be there */ 
  19.   __le16  ei_leaf_hi;  /* high 16 bits of physical block */ 
  20.   __u16  ei_unused; 
  21. }; 

如果文件不大,inode 里面的 i_block 中,可以放得下一个 ext4_extent_header 和 4 项 ext4_extent。所以这个时候,eh_depth 为 0,也即 inode 里面的就是叶子节点,树高度为 0。如果文件比较大,4 个 extent 放不下,就要分裂成为一棵树,eh_depth>0 的节点就是索引节点,其中根节点深度最大,在 inode 中。最底层 eh_depth=0 的是叶子节点。

目录与文件名的存储格式

目录下文件比较少的情况下:目录本身也是个文件,也有 inode。inode 里面也是指向一些块。和普通文件不同的是,普通文件的块里面保存的是文件数据,而目录文件的块里面保存的是目录里面一项一项的文件信息。这些信息我们称为 ext4_dir_entry。从代码来看,有两个版本,在成员来讲几乎没有差别,只不过第二个版本 ext4_dir_entry_2 是将一个 16 位的 name_len,变成了一个 8 位的 name_len 和 8 位的 file_type。即该目录项的数据所在inode编号、文件名长度与类型、文件名字三部分组成。

  1. struct ext4_dir_entry { 
  2.   __le32  inode;      /* Inode number */ 
  3.   __le16  rec_len;    /* Directory entry length */ 
  4.   __le16  name_len;    /* Name length */ 
  5.   char  name[EXT4_NAME_LEN];  /* File name */ 
  6. }; 
  7. struct ext4_dir_entry_2 { 
  8.   __le32  inode;      /* Inode number */ 
  9.   __le16  rec_len;    /* Directory entry length */ 
  10.   __u8  name_len;    /* Name length */ 
  11.   __u8  file_type;   /* File type */ 
  12.   char  name[EXT4_NAME_LEN];  /* File name */ 
  13. }; 

file_type指定了目录项的类型。改变量的可能值,由以下枚举类型定义:

  1. enum{ 
  2.     EXT4_FT_UNKNOWN, 
  3.     EXT4_FT_REG_FILE, 
  4.     EXT4_FT_DIR, 
  5.     EXT4_FT_CHRDEV, 
  6.     EXT4_FT_BLKDEV, 
  7.     EXT4_FT_FIFO, 
  8.     EXT4_FT_SOCK, 
  9.     EXT4_FT_SYMLINK, 
  10.     EXT4_FT_MAX 

ls列出的目录内容如下:

  1. [root@localhost ~]# ls -la 
  2. 总用量 37536 
  3. dr-xr-x---.  7 root root     4096 5月  26 16:54 . 
  4. dr-xr-xr-x. 19 root root      288 6月  10 14:51 .. 
  5. -rw-------.  1 root root     1260 1月  11 2014 anaconda-ks.cfg 

每一项都会保存这个目录的下一级的文件的文件名和对应的 inode,通过这个 inode,就能找到真正的文件。第一项是“.”,表示当前目录,第二项是“..”,表示上一级目录,接下来就是一项一项的文件名和 inode。**目录下文件比较多的情况下:如果一个目录下有几万几十万个条目,这个方法就比较慢了。原因在于线性扫描,而且,1个block(4096字节),基本只能放下几十~200个条目,一旦需要几十几百个block,那么为了获取子文件的inode,这个DISK IO的消耗是不能忍受的。因此开发了dir_index的功能。dir_index使用dx_entry来对目录文件的block进行管理,一个dx_entry对象对应一个block。dx_entry.hash记录的是其对应block内所有目录项的最小hash值,dx_entry.block记录的是目录文件的逻辑块号。从/etc/mke2fs.conf中也可以看出,这个是格式化文件系统的默认选项:

  1. [defaults] 
  2.     base_features = sparse_super,filetype,resize_inode,dir_index,ext_attr 
  3.     default_mntopts = acl,user_xattr 
  4.     enable_periodic_fsck = 0 
  5.     blocksize = 4096 
  6.     inode_size = 256 
  7.     inode_ratio = 16384 
  8.  
  9. [fs_types] 
  10.     ext3 = { 
  11.         features = has_journal 
  12.     } 
  13.     ext4 = { 
  14.         features = has_journal,extent,huge_file,flex_bg,uninit_bg,dir_nlink,extra_isize 
  15.         auto_64-bit_support = 1 
  16.         inode_size = 256 
  17.     } 

如果在 inode 中设置 dir_index 标志,则目录文件的块的组织形式将发生变化,变成了下面定义的这个样子:

  1. struct dx_root 
  2.   struct fake_dirent dot; 
  3.   char dot_name[4]; 
  4.   struct fake_dirent dotdot; 
  5.   char dotdot_name[4]; 
  6.   struct dx_root_info 
  7.   { 
  8.     __le32 reserved_zero; 
  9.     u8 hash_version; 
  10.     u8 info_length; /* 8 */ 
  11.     u8 indirect_levels; 
  12.     u8 unused_flags; 
  13.   } 
  14.   info; 
  15.   struct dx_entry  entries[0]; 
  16. }; 

当然,首先出现的还是差不多的,第一项是“.”,表示当前目录;第二项是“..”,表示上一级目录,这两个不变。接下来就开始发生改变了。是一个 dx_root_info 的结构,其中最重要的成员变量是 indirect_levels,表示间接索引的层数。接下来我们来看索引项 dx_entry。这个也很简单,其实就是文件名的哈希值和数据块的一个映射关系。

  1. struct dx_entry 
  2.   __le32 hash; 
  3.   __le32 block; 
  4. }; 

那么,找到一个子文件需要如下步骤。1)根据待查找子文件名计算出hash值 2)在当前的全部dx_entry中采用二分查找的方式找到对应的dx_entry 3)根据dx_entry.block记录值读取目录文件对应的逻辑块内容 4)在读取到的block内容中遍历查找匹配的子文件目录项 不难发现,之前的需要读取N + 1个block的困境被简化为只需要读取一个block的内容即可,问题迎刃而解

为了表示图中上半部分的那个简单的树形结构,在文件系统上的布局就像图的下半部分一样。无论是文件夹还是文件,都有一个 inode。inode 里面会指向数据块,对于文件夹的数据块,里面是一个表,是下一层的文件名和 inode 的对应关系,文件的数据块里面存放的才是真正的数据。

ext类文件系统的缺点

最大的缺点是它在创建文件系统的时候就划分好一切需要划分的东西,以后用到的时候可以直接进行分配,也就是说它不支持动态划分和动态分配。对于较小的分区来说速度还好,但是对于一个超大的磁盘,速度是极慢极慢的。例如将一个几十T的磁盘阵列格式化为ext4文件系统,可能你会因此而失去一切耐心。除了格式化速度超慢以外,ext4文件系统还是非常可取的。当然,不同公司开发的文件系统都各有特色,最主要的还是根据需求选择合适的文件系统类型。

本文转载自微信公众号「运维开发故事」,可以通过以下二维码关注。转载本文请联系运维开发故事公众号。

 

责任编辑:姜华 来源: 运维开发故事
相关推荐

2021-06-28 06:24:12

磁盘存储VFS

2021-06-21 14:52:45

磁盘机械磁盘固态磁盘

2018-01-10 12:42:09

Linux磁盘文件系统

2009-10-12 11:14:51

LinuxLinux磁盘文件系统管理

2010-04-07 18:42:42

Unix命令

2010-03-02 15:09:26

Linux mount

2016-12-27 10:48:59

Linux命令磁盘与文件系统

2021-06-29 07:47:22

文件系统磁盘

2009-10-13 14:31:26

:Linux系统磁盘系统管理

2017-08-17 10:03:06

磁盘系统实例

2020-07-22 14:53:06

Linux系统虚拟文件

2010-03-05 17:43:00

Linux XFS文件

2023-09-05 15:17:48

LinuxLUN磁盘

2023-09-03 17:09:58

LinuxSAN LUN磁盘

2024-03-11 10:30:31

Linux文件系统

2011-01-13 14:10:30

Linux文件系统

2018-08-24 10:10:25

Linux文件系统技术

2019-09-20 10:04:45

Linux系统虚拟文件

2020-07-28 08:00:03

存储数据技术

2021-05-31 06:10:14

Btrfs文件系统Linux
点赞
收藏

51CTO技术栈公众号