简介

该篇blog只是存储系列科普文章中的第二篇,所有文章请参考:

​​博客所有文章​​

在工程架构领域里,存储是一个非常重要的方向,这个方向从底至上,我分成了如下几个层次来介绍:

  1. 硬件层:讲解磁盘,SSD,SAS, NAS, RAID等硬件层的基本原理,以及其为操作系统提供的存储界面;
  2. 操作系统层:即文件系统,操作系统如何将各个硬件管理并对上提供更高层次接口;
  3. 单机引擎层:常见存储系统对应单机引擎原理大概介绍,利用文件系统接口提供更高级别的存储系统接口;
  4. 分布式层:如何将多个单机引擎组合成一个分布式存储系统;
  5. 查询层:用户典型的查询语义表达以及解析;

文件系统磁盘分配

在存储知识栈上提到文件系统, 大家首先就关心他是如何管理好硬件层提供的磁盘空间的。

本文开篇就先描述典型的几种文件系统管理磁盘的技术方案: 连续分配, 链式分配, 索引分配。

连续分配

顾名思义, 就是创建文件的时候, 给分配一组连续的块。在单独拿出一块地方存储各个文件的meta信息, meta信息也很简单, 包含文件名, 起始位置和长度即可, 这个存放meta信息的地方也叫做FAT(文档分配表)。

如下图:

存储系统 (二)_位图

该方案的优点:

  1. 简便, 适用于一次性写入操作;
  2. 所需磁盘寻道次数和寻道时间最少;

缺点也很明显:

  1. 文件不能动态增长, 因为后面的块可能已经分配给别人了;
  2. 不利于文件的插入和删除, 插入文件之前需要声明文件大小;
  3. 外部碎片问题;

为了克服连续分配的问题, 又进化出来了链式分配方案。

链式分配

链式分配即将文件块像链表一样管理起来, 每个块中放一个指针, 指针指向下一个文件块所在的位置。这样在FAT中存储也很简单, 只需要存储文件名, 起始块号和结束块号(都可以不存)。

如下图:

存储系统 (二)_ext_02

该方案的优点:

  1. 提高磁盘利用率, 没有碎片问题
  2. 有利于文件的插入, 删除

缺点:

  1. 存取速度慢, 需要移动的磁道次数多, 寻道时间长;
  2. 读写任何一个地方都需要沿着指针前进直到找到对应块为止;
  3. 链接占据了一定的磁盘空间, 数据不是严格按照块大小分配;

索引分配

这是一个折衷方案, 也是现在大部分文件系统采用的方案, 他综合了连续分配和链式分配的好处。

该方案会在FAT中保存所有文件块的位置, 各文件系统都有一套自己对应的细节分配策略, 会保证一个文件尽量连续的同时, 又避免出现大量的磁盘碎片。

如下图:

存储系统 (二)_描述符_03

该方案的优点是:

  1. 能够保持好大部分文件的局部性
  2. 满足文件插入, 删除的高效
  3. 随机读写不需要沿着指针前进

缺点:

  1. 会有较多的磁盘寻道次数
  2. 索引表本身管理复杂, 也会带来额外的系统开销

ext文件系统

基本概念

  1. 块. 即Block, 数据存储的最小单元, 每个块都有一个唯一的地址, 从0开始, 起源于第一个可以用来存储数据的扇区;
  2. 超级块. 超级块保存了各种文件系统的meta信息, 比如块大小信息。他位于文件系统的2号和3号扇区(物理地址), 占用两个扇区大小;
  3. 块组. 所有的块会划分成多个块组, 每个块组包含同样多个块, 但是可能整个文件系统整个块数不是块组的整数倍, 所以最后一个块组包含的个数可能会小于其他块;

总体结构

一个ext文件系统分成多个块组, 每个块组的结构基本相同, 如下:

存储系统 (二)_文件系统_04

解释如下:

  1. 0~1号扇区: 引导扇区. 如果没有引导代码, 则这两个扇区为空, 全部用0填充;
  2. 2~3号扇区: 超级块, 超级块包含各种meta信息:
  1. 块大小, 每个块组包含块数, 总块数, 第一个块前保留块数, inode节点数, 每个块组的inode节点数
  2. 卷名, 最后挂载时间, 挂载路径, 文件系统是否干净, 是否要调用一致性检查标识
  3. 空间inode节点和空闲块的记录信息, 在分配inode节点和新块的时候使用
  1. 组描述符表:
    位于超级块后面的一个块, 注意在超级块之前只占用了4个扇区, 如果一个块大小超过4个扇区的话, 这里就会有空的扇区。
    组描述符表中包含了所有块组的描述信息, 每个块组占据32个字节(差不多是8个整数的大小)。如果格式化使用默认参数, 那么组描述符表不会超过一块块组。
    组描述符和超级块在每个块组中都有一个备份, 但是激活了​​稀疏超级块​​特征的情况除外。
    ​​​稀疏超级块​​即不是在所有块组中都存储超级块和组描述符的备份, 默认该策略被激活, 其策略类似当块组号是3,5,7的幂的块组才存副本。
  2. 块位图表
    每个块对应一个bit, 只包含了本块组的信息。而文件系统创建的时候, 默认每个块组包含的块数跟每个块包含的bit数是一样的, 这种情况下每个块位图表正好等于一个块的大小。
    而该位图表的块的起始位置会在组描述符表中指定, 大小则为块组中包含块数个bit。
  3. inode位图块
    跟块位图表类似, 通常情况他也只占用一个块。通常一个块组中的inode节点数会小于block数量, 所以其不会超过一个块大小。
  4. inode节点表
    每个inode都占用128位的一个描述信息, 起始位置在组描述符中表示, 大小为inode节点数*128。
  5. 数据区
    这就是真正存储数据的区域。

块大小为4096的时候, 各部分和扇区对应关系如下图:

存储系统 (二)_描述符_05

块大小为2048的时候, 各部分和扇区对应关系如下图:

存储系统 (二)_ext_06

超级块详细信息

超级块总是位于文件系统的第1024~2048字节处。

超级块中包含的信息如下:

偏移(16进制)

字节数

含义&解释

00~03

4

文件系统中总inode节点数

04~07

4

文件系统中总块数

08~0B

4

为文件系统预保留的块数

0C~0F

4

空闲块数

10~13

4

空间inode节点数

14~17

4

0号块组起始块号

18~1B

4

块大小(1024左移位数)

1C~1F

4

片段大小, 跟块大小一模一样

20~23

4

每个块组中包含的块数量

24~27

4

每个块组中包含的片段数量, 跟包含的快数量一致

28~2B

4

每个块组中包含的inode节点数量

2C~2F

4

文件系统最后挂载时间

30~33

4

文件系统最后写入时间

34~35

2

当前挂载数

36~37

2

最大挂载数

38~39

2

文件系统签名标识 53EF

3A~3B

2

文件系统状态, 正常, 错误, 恢复了孤立的inode节点

3C~3D

2

错误处理方式, 继续, 以只读方式挂载

3E~3F

2

辅版本号

40~43

4

最后进行一致性检查的时间

44~47

4

一致性检查的间隔时间

48~4B

4

创建本文件系统的操作系统

4C~4F

4

主版本号, 只有该值为1的时候, 偏移54之后的扩展超级块的一些动态属性值才是有意义的

50~51

2

默认为UID的保留块

52~53

2

默认为GID的保留块

54~57

2

第一个非保留的inode节点号, 即用户可以使用的第一个inode节点号

58~59

2

每个inode节点的大小字节数

5A~5B

2

本超级块所在的块组号

5C~5F

4

兼容标识特征

60~63

4

非兼容标识特征

64~67

4

只读兼容特征标识

68~77

16

文件系统ID号

78~87

16

卷名

88~C7

64

最后的挂载路径

C8~CB

4

位图使用的运算法则

CC~CC

1

文件再分配的块数

CD~CD

1

目录再分配的块数

CE~CF

2

未使用

D0~DF

16

日志ID

E0~E3

4

日志inode节点

E4~E7

4

日志设备

E8~EB

4

孤立的inode节点表

EC~3FF

788

未使用

块组描述符详细信息

每个块组描述符占用32个字节, 其数据结构如下:

偏移(16进制)

字节数

含义&解释

00~03

4

块位图起始地址(块号)

04~07

4

inode节点位图起始地址(块号)

08~0B

4

inode节点表起始地址(块号)

0C~0D

2

块组中空闲块数

0E~0F

2

块组中空闲inode节点数

10~11

2

块组中的目录数

12~1F

-

未使用

inode信息

每个inode会被分配给一个目录或者一个文件, 每个inode包含了128个字节, 里面包含了这个文件的各种meta信息。

在所有inode中, 1~10号会用作保留给内核使用, 在这些保留节点中, 2号节点用于存储根目录信息, 1号表示坏块, 8号表示日志文件信息。

第一个用户可见的都是从11号inode开始, 11号节点一般用作​​lost+found​​​目录, 当检查程序发现一个inode节点已经被分配, 但是没有文件名指向他的时候, 就会把他添加到​​lost+found​​目录中并赋予一个新的文件名。

inode通过三级指针的方式来找到文件数据最终存储的数据块, 见下图:

存储系统 (二)_描述符_07

这种方式能保证小文件的读取效率的同时, 也支持大文件的读取。

目录项

在ext文件系统中, 每个目录也对应一个inode节点, 该inode节点对应的数据块里面会存储该目录下所有文件/目录的信息。

每个文件/目录对应的信息就叫​​目录项​​, 目录项包含的内容也很简单, 主要就是文件名和指向该文件名的inode指针, 详细信息如下:

偏移(16进制)

字节数

含义&解释

00~03

4

inode节点号

04~05

2

本目录项的长度字节数

06~06

1

名字长度字节数

07~07

1

文件类型

08~

不定长度

名字的ascii码

当文件删除的时候, 不会真正删除文件, 会将该文件所属目录对应的目录项给删除, 同时将对应数据块在快位图表中标记为0。

链接

  1. 硬链接是指在另外一个目录对应的目录项中增加一个目录项。硬链接建立之后, 用户无法通过文件名来判断到底哪个是原文件名, 哪个是链接名;
  2. 软连接是一种文件类型, 该文件里面就存储源文件的完整路径, 如果源文件完整路径长度小于60个字节, 那么就将该值直接存储在inode节点表中, 避免浪费数据块;

分配策略

  1. 首先判断应该在哪个块组中分配
  1. 如果是为文件创建节点
  1. 默认会在其父目录所在的组中为其创建节点, 这样可以确保一个目录中所有文件都位于一个大致的区域中
  2. 如果父目录所在组没有空闲的节点或者空闲的块了, 就到别的块组中为该文件分配节点, 找寻别的块组的算法如下
  1. 每次讲当前组号加上2^N次方再求hash
  2. 如果上面的算法没找到, 就线性查找
  1. 如果是为目录创建节点
  1. 会将其分配到可用空间较多的块组中, 分配算法如下:
  1. 首先利用超级块中的剩余inode和剩余块数字算出每个块组的平均剩余数字, 然后依次找到一个大于平均值的块组
  2. 如果没找到, 就利用块组描述符表中的信息, 找到一个最空闲的块组
  1. 当一个数据块分配给某文件之后
  1. 该块原来的内容会被请出, inode节点对应的各种元信息会跟着做修改
  2. 如果是文件, 节点对应链接数会设置为1, 如果是目录, 链接数会被设置为2

实际创建/删除文件过程

我们创建一个​​/xuanku/file.txt​​, 该文件大小为10000字节, 文件块大小为4096, 那么下面来看一下过程:

  1. 读取文件系统的1024字节~2048字节, 即超级块信息。通过超级块得到快大小为4096, 每个块组含有8192个块以及2008个inode节点
  2. 读取文件系统中第1个块(即组描述符表), 得到所有块组布局信息
  3. 访问2号inode节点(即根节点), 通过读取根节点数据块信息, 假设读到其位于5号块
  4. 在第5号块所有目录项中从前往后遍历, 直到找到文件名为​​xuanku​​的目录项, 读到该inode节点为4724
  5. 每个块组有2008个inode, 用4724针对2008取模, 得到的结果是2号块组
  6. 通过组描述符表中得到第2个块组的inode节点表起始于16378号块
  7. 从16378号块读取inode节点表中查找4724号对应的inode节点(708号表项), 查询到该​​xuanku​​的目录内容位于17216号块
  8. 从17216块读取​​xuanku​​​目录项内容, 将​​file.txt​​相关信息更新到该块的目录项当中
  9. 然后开始为文件分配inode节点, 默认会放到父目录所在块组, 即2号块组, 再次2号块组的inode节点表16378, 开始从4724往后找到第一个可用的inode节点分配给该文件, 假设找到的是4850号inode。
  10. 然后就给4850号的inode位图表设置为1, 组描述符的空闲inode节点数和超级块的总空闲inode节点数都减1, 将inode节点的地址写入​​file.txt​​对应的目录项当中, 然后写各种时间, 记录日志
  11. file.txt需要3个块的存储空间, 通过2号块组描述符找到块位图对应的块, 并从前往后找到可用的块。然后将对应的位设置为1, 并更新到inode节点当中
  12. 然后将文件的内容写入到对应的块中

下面再演示一下将该文件删除的过程:

  1. 读取文件系统的1024字节~2048字节, 即超级块信息。通过超级块得到快大小为4096, 每个块组含有8192个块以及2008个inode节点
  2. 读取文件系统中第1个块(即组描述符表), 得到所有块组布局信息
  3. 访问2号inode节点(即根节点), 通过读取根节点数据块信息, 假设读到其位于5号块
  4. 在第5号块所有目录项中从前往后遍历, 直到找到文件名为​​xuanku​​的目录项, 读到该inode节点为4724
  5. 每个块组有2008个inode, 用4724针对2008取模, 得到的结果是2号块组
  6. 通过组描述符表中得到第2个块组的inode节点表起始于16378号块
  7. 从16378号块读取inode节点表中查找4724号对应的inode节点(708号表项), 查询到该​​xuanku​​的目录内容位于17216号块
  8. 从17216块读取​​xuanku​​​目录项内容, 读到​​file.txt​​的inode节点为4850
  9. 然后取消​​file.txt​​​的目录项分配, 修改系列​​xuanku​​目录对应的目录项信息
  10. 取消inode节点信息, 修改2号块组对应的inode节点表信息, 将inode节点位设置为0, 更新块组描述符和超级块的空闲inode节点数加1
  11. 还要回收文件内容对应的6个块空间, 将块位图表中的bit设置为0, 清除inode节点的块指针, 更新块组描述符和超级块的空闲块数加1

参考

  1. 数据重新 文件系统原理精解与数据恢复最佳实践