本文系统性介绍一下【ext文件系统】。
文章不深入细节,而从更宏观的角度去解释理解文件系统的逻辑运作,可加强对Linux文件系统的逻辑理解。
一、为什么需要一个文件系统
存储是一种有限的资源,存储的如何使用跟分配,需要一个管理者去协调。文件系统就是这个管理者。File System ≈ Storage Manager。
二、硬盘的分区
关于硬盘分区,第一反应可能是很多分区的细节技术。
在这些细节之前可以有一个思考:为什么硬盘需要分区?
因为分区是文件系统在存储层的最小单位(lvm 跟 k8s 类的虚拟文件系统暂不讨论)。一块硬盘分区之后可以解决两个问题:
- 同一块硬盘可以支持多个文件系统。比如分区1刷NTFS,分区2刷FAT,很自由。
- 对于硬盘的物理盘容量大于文件系统的情况,可以不浪费。像FAT32最大只支持到2TB。如果一块盘容量有10T又想刷FAT32,就可以划分成5个分区,每个都刷上FAT32。
以上是我个人认为分区的真正直接解决的问题,欢迎补充。至于分区后带来的隔离性、条理性等好处,我反而觉得是“副作用下的好处了”。
三、资源管理的策略
磁盘跟内存很像,都是一种可随机读写的有限资源。管理这种资源的时候,很容易就能联想到内存的管理。事实上磁盘跟内存的管理确有相似之处。
3.1 管理的对象本质
从需求分析,明确文件系统要管理的对象有两个——文件 以及 存储。其他的派生策略都是围绕它们而展开的。
文件系统要做的,就是对存储资源进行管理,利用更小的存储空间、更有效率的存储组织、更健壮的数据结构来表达出所谓的 “文件的实体”。
下面话分两头。
3.2 文件
3.2.1 文件是什么?
直觉上,应该是我们打开某个文件夹之后,在下面能看到的一系列东西。
但反直觉的,这里并不是真正的“文件”,而仅仅是目录文件下的目录项信息。
举个例子来解释这种关系,这里就像是餐馆里的餐牌,餐牌(目录)上你看到的是能吃的东西(目录项),而真正能吃的东西(目录项对应的文件)并不在餐牌上。
真正能吃的东西才是文件,同一个能吃的东西可以出现在不同的餐牌(目录)里,而且取名可能不一样。因而文件名本身不存在于文件中,仅仅是目录中“报菜名”的字符而已
从需求思考,文件应当分为两部分信息:
一部分是文件本体存储的内容。比如照片的二进制、文本文件的字符串等,这部分的内容在所有不同的文件系统中都是一致的,例如同一张照片从 macOS 拷贝到 Windows,它还是会长原来的样子;
另一部分是文件的管理数据,meta 部分。这部分包含了文件的属性比如修改日期、所有者、权限、存储的分布等等,这部分显然在不同文件系统下会很不一样(毕竟它们的理念跟原理会有所差异)。其中的存储分布会在后面的章节展开。
也就是说,文件本体 + 文件元信息 = 文件。
上面的元信息在 ext 中就是 inode。它们的关系如图:
综上,我们在哲学上来总结一下文件在 Linux 中是什么。
从使用上看,是目录充当展示作用、文件系统组织文件元信息+文件内容一起,给用户造成的使用概念。从狭义上看,“文件”仅仅是这里的“文件内容数据”部分。
3.2.2 文件inode的管理
文件有多种多样的内容,它们的形式、大小不一。跟它们的内容不同,它们的管理信息却是基本一致的——都需要处理权限信息、类型信息、所有者、大小、存储分布、修改时间戳等等。
功能的一致意味着数据结构的一致,自然而然能想到数组。**于是 inode 被 ext 以类似数组的形式,摆在了一块进行管理。**只要知道某个 inode 的编号,直接换算数组地址即可快速定位。
而又由于 inode 也是存在硬盘上,跟文件的存储内容区需要一个清晰的、固定的界限(边界一旦变动就要发生巨量的移动,这显然不现实),所以它必须在一开始就决定好大小、数量。
决定它大小数量也很讲究。上文说到,文件跟 inode 是一一对应的,如果分配给 inode 的空间少了,文件数一多就不够表达新文件了;如果分配给 inode 的空间多了,系统却又没有这么多的文件要存(比如一些视频服务器上,大部分都是大的视频文件),inode 的空间就会造成硬盘的浪费。
一般来说文件系统都会根据硬盘的大小,以每多少的容量分配多少的 inode 的标准来自动动态设定 inode 数量。
由于一开始就需要规划好 inode 数量,于是 inode 也是一种有限资源。
ext 文件系统通过位图来实现 inode 的资源管理。亦即用一段连续空间的bit位来表示某一个inode是否已经使用,1 为使用,0 为空闲。
这连续空间在 ext 中就叫 imap(inodemap)。
3.3 存储
在 3.2 中介绍了文件的本质,而文件内容的落地终究是要到具体的存储上去的。ext 通过一定的算法,分配了一定的硬盘区域以承载文件内容。这些承载内容的存储区域记录在文件的 inode 里,结合 inode 里的文件元信息构成了在 ext 下的文件的完成体。
3.3.1 分配单位
磁盘的管理策略跟内存的页表管理方式类似,都有一个 “最小分配单位” 来减少资源的碎片问题和提升信息组合的效率。
试想一下不使用最小分配单位的设定的情况下的分配策略:
1、要么只能连续分配
2、要么面临不等大小的字节块链表。
第一种策略,例子里A、C之间的小空隙会有大量的碎片产生;第二种策略,B如果是一个大文件,会有不规则的链表的遍历代价。
提出最小分配单位之后,可以综合上面两个的优缺点来达到平衡:
1、可以让大的文件见缝插针,存放更自由,大大减少碎片问题;
2、由于分配单位是固定的,文件的组合从链表类管理变成了数组类管理,可维护性跟效率都有提升(同大小元素的设定,非常符合 “数组”)。
(忽略上图的图形比例)
ext 文件系统提出的最小叫 Block,亦即“块”(Windows里叫簇 Cluster,其实是一个东西)。
块并不会直接让空间浪费问题直接消失,只是一种最小存储粒度到全连续的一种分配策略的取舍。块的空间浪费问题如下:
所以,块的大小怎么定也是一门学问,定小了,就会让大文件切的太细,组合的过程变长性能变慢;定大了,小文件就容易造成存储空间的浪费。
ext文件系统和windows的NTFS文件系统,一般都会针对硬盘的大小来自动决策默认值。典型的块大小定在 4KB。用户也可以根据自己对硬盘空间的感觉来手动修改。比如觉得自己的硬盘不容易装满,又想获得一定程度的性能提升,就可以把 4KB 这个值设的更大。
一旦块的大小被设定好,整块存储空间的块数量、块编号也就随之确定了。
3.3.2 分配管理信息
一个存储块分配之后,要标记为 “已分配” 表明已经占用状态,而文件删除之后的存储块随之删除,也要标记为 “解除分配” ,以响应后续的分配需求。
类似于上面的 inode,ext 对块的管理也使用了位图。
例如开辟一个 4KB 大小的空间来维护块区,第一个 bit 表示第一个块,第N个bit表示第N个块,那么这段空间总共可以维护 4 * 1024 * 8 = 32768 个块。假设块大小设置为 4KB,那么总共可以维护 32768 * 4KB = 128MB。
这连续空间在 ext 中就叫 bmap(blockmap)。
3.3.3 块分配之后与inode的关联
ext4 用了更先进也更复杂的形式来处理这个问题。为了方便,这里讲讲 ext2 跟 ext3 的策略。
inode 里有15个固定指针来关联分配的块。
其中:
前12个指针都是是直接指针,指向的是文件所在的块。
第13个指针是一级间接寻址指针,指向一个块,块里顺序放慢了分配的块指针。为了方便说明,我们称这种块叫指针块a。
第14个指针指向的是二级间接寻址指针,指向的是一个块,块里顺序放满了指向上面的指针块a的指针。称之指针块b。
第15个指针指向的是三级间接寻址指针,指向的是一个块,块里顺序放满了指向上面的指针块b的指针。称之为指针c。
我们可以一下这种方式能表示的存储大小。
假设块大小设置为4KB,指针大小设置为4B(INT32)。
前12个指针能表达的大小:12 * 4KB = 48KB;
第13个指针能表达的大小:4KB(块大小)/ 4(指针大小) * 4KB = 4MB;
第14个指针能表达的大小:4KB / 4 * 4MB = 4GB;
第15个指针能表达的大小:4KB / 4 * 4GB = 4T;
4T左右的总表达大小已经能满足绝大部分的存储需求了。
结合 inode 里的文件大小信息即可知道应该去哪里取多少的块来进行数据的组合。
3.4 分而治之
3.4.1 还需要分级管理
其实讲完上面的结构,大体的管理结构其实已经完备。这个时候看硬盘的结构,会类似于下面的结构:
但这个结构对于非常大的存储空间来说还是不够的。
假设我们的硬盘分区有1T,而我们的块大小设定为 4KB。那么就会有 1T / 4KB = 268435456 个块。我们在找空闲的块进行组合存储文件的时候,只能用遍历的形式去找空闲块。这样的遍历复杂度是 O(268435456) ,代价巨大。
同样的,按照容量大小间隔固定分配数量的 inode 也会面临这个问题。
为了解决 bmap 跟 imap 过大问题,我们需要继续分级,向上再提出一个分而治之的概念——块组(block group)。
说是块组,其实是切割了小的、包含完整的上述内容在里头的系统,而不只是块。指定数量的块组会再抽出来空闲块数、空闲容量、空闲 inode 数的统计概念对外提供。
文件系统在搜寻合适的空间的时候,就现在块组里快速查找一次,再进入块组中进行对块跟 inode 的处理。这样能提高效率。
3.4.2 块组的大小、数量
如同 inode 数量,块组的数量是一开始就初始化好的。依据是限制每个 bmap 的大小最多只能占满一个块来划分整个文件系统的块组。
比如,我们有一个1T的分区,块大小设置为 4KB。
4KB能表示 4 * 1024 * 8 = 32768 个块,也就是 32768 * 4KB = 128M 左右的空间(实际上还要更小一些,因为 bmap、imap、itable 等元数据区占用的块也需要记入 bmap)。
那么 1T 的分区就会划分 Math.ceil(1T / 128M) = 8192 个块组。
3.4.3 0号块组(Group 0)
有分割这些块组的信息,就需要有一个向上的管理者。这个管理者就是0号块组。0号块组存储了超级块(super block)跟块组描述表(GDT)。
超级块中存储了文件系统的一些元信息,比如创建时间、剩余容量、自检状况等等。我们用 df 指令进行显示的时候,其实就是读的超级块信息,所以速度非常的快,而不用等待统计。
块组的描述表,则是类似 inodeTable 的块组目录,可以看作是块组的 inode 信息。里面有块组的剩余容量、剩余块数、空闲inode数等等信息,文件系统以此来快速定位到合适的块组做逻辑。
从GDT也能看出来 ext 的设计模式还是有很强的统一性的,inode 如此,GDT 也是如此。
Group 0 过于重要,它如果毁了整个文件系统也就毁了。所以它是有备份的。比如 ext2 在一些特定的 group 号中,会存入 super block 跟 GDT的备份,检测到 group 0 的异常会启用等等,具体的策略就不深究了。
3.5 文件系统大合照(ext4)
其中,第一个内容只有超级块跟GDT所在的块组或者备份块组才会有,第二个内容所有块组都有。
3.6 多文件系统的配合
ext 支持多文件系统。
方式是将其他的文件系统挂载到某个文件系统的路径上,打开挂载的路径之后其实就无缝切入到对应的文件系统中。
如果此时原来的文件系统有同名的文件(可能是目录也可能是普通文件),文件都会被隐藏起来。隐藏起来的方式,是通过原来文件系统的目录项的 inode 号,关联到新挂载的文件夹系统上。
整个过程比较简洁简单,下面来在 centos7上实践一下。
1、首先使用 df -lh 指令,来查看当前的文件系统状态。
[natsusao@VM-20-16-centos ~]$ df -lh
Filesystem Size Used Avail Use% Mounted on
devtmpfs 1.9G 0 1.9G 0% /dev
tmpfs 1.9G 24K 1.9G 1% /dev/shm
tmpfs 1.9G 928K 1.9G 1% /run
tmpfs 1.9G 0 1.9G 0% /sys/fs/cgroup
/dev/vda1 79G 16G 61G 21% /
tmpfs 379M 0 379M 0% /run/user/0
tmpfs 379M 0 379M 0% /run/user/1002
这是一台用腾讯云虚拟出来的centos7系统。注意到根文件系统挂载的是虚拟云盘 vda1 文件系统。其他信息先无视,有兴趣可以搜索 tmpfs 、tty 有关的细节。
2、接着用 dd 指令生成一个1G大小的块文件备用。这一步可以看作是把存储介质在物理上准备好。
[natsusao@VM-20-16-centos ~]$ dd if=/dev/zero of=mydisk.img bs=1G count=1
1+0 records in
1+0 records out
1073741824 bytes (1.1 GB) copied, 6.24218 s, 172 MB/s
[natsusao@VM-20-16-centos ~]$ ls -lh mydisk.img
-rw-rw-r-- 1 natsusao natsusao 1.0G Apr 6 17:55 mydisk.img
3、使用 losetup 指令,将块文件关联循环设备。这一步可以看作是把存储介质插入系统。
[natsusao@VM-20-16-centos ~]$ sudo losetup /dev/loop0 mydisk.img
4、用 mke2fs 格式化这个设备。
[natsusao@VM-20-16-centos ~]$ sudo mke2fs /dev/loop0
mke2fs 1.42.9 (28-Dec-2013)
Discarding device blocks: done
Filesystem label=
OS type: Linux
Block size=4096 (log=2)
Fragment size=4096 (log=2)
Stride=0 blocks, Stripe width=0 blocks
65536 inodes, 262144 blocks
13107 blocks (5.00%) reserved for the super user
First data block=0
Maximum filesystem blocks=268435456
8 block groups
32768 blocks per group, 32768 fragments per group
8192 inodes per group
Superblock backups stored on blocks:
32768, 98304, 163840, 229376
Allocating group tables: done
Writing inode tables: done
Writing superblocks and filesystem accounting information: done
可以看到这个 “分区” 已经被格式化的信息,块大小默认是 4KB,8192 个块为一个块组,超级块的备份位置等等。
5、在 home目录下有一个 loop0 文件夹,里面有普通的文件。先看一下当前的信息:
[natsusao@VM-20-16-centos ~]$ stat loop0
File: ‘loop0’
Size: 4096 Blocks: 8 IO Block: 4096 directory
Device: fd01h/64769d Inode: 789702 Links: 2
Access: (0775/drwxrwxr-x) Uid: ( 1002/natsusao) Gid: ( 1002/natsusao)
Access: 2023-04-06 18:07:43.467643515 +0800
Modify: 2023-04-06 18:07:43.467643515 +0800
Change: 2023-04-06 18:07:43.467643515 +0800
Birth: -
注意看 inode 号,789702
6、现在用 mount 指令把循环设备挂上去此文件目录:
[natsusao@VM-20-16-centos ~]$ sudo mount /dev/loop0 /home/natsusao/loop0
再查看此文件的信息:
[natsusao@VM-20-16-centos ~]$ stat loop0
File: ‘loop0’
Size: 4096 Blocks: 8 IO Block: 4096 directory
Device: 700h/1792d Inode: 2 Links: 3
Access: (0755/drwxr-xr-x) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2023-04-06 17:59:19.000000000 +0800
Modify: 2023-04-06 18:12:25.695262951 +0800
Change: 2023-04-06 18:12:25.695262951 +0800
Birth: -
发现 inode 已经发生改变。inode 为 2 其实已经是挂载上去的文件系统的根 inode 号。
cd 进去之后可以看到典型的 ext2 初始化文件结构:
[natsusao@VM-20-16-centos ~]$ cd loop0/
[natsusao@VM-20-16-centos loop0]$ ls
lost+found
在这下面就可以自由操作了。
7、恢复原状
[natsusao@VM-20-16-centos ~]$ sudo umount /home/natsusao/loop0
[natsusao@VM-20-16-centos ~]$ sudo losetup -d /dev/loop0
确认一下循环设备已经 detach
[natsusao@VM-20-16-centos ~]$ sudo losetup -f
/dev/loop0
此时再去看路径文件
[natsusao@VM-20-16-centos ~]$ stat loop0
File: ‘loop0’
Size: 4096 Blocks: 8 IO Block: 4096 directory
Device: fd01h/64769d Inode: 789702 Links: 2
Access: (0775/drwxrwxr-x) Uid: ( 1002/natsusao) Gid: ( 1002/natsusao)
Access: 2023-04-06 18:07:43.467643515 +0800
Modify: 2023-04-06 18:07:43.467643515 +0800
Change: 2023-04-06 18:07:43.467643515 +0800
Birth: -
注意 inode 号,熟悉的文件就又回来了。
四、一些问题的解答
经过上面,相信很多问题都能思考来得出答案了。
下面举几个问题,来验证一下自己的思路。
4.1 一旦一个文件系统初始化好,它还能动态扩容吗?
不可以。因为文件系统的 inode、block、group 等都是一开始就初始化好的,不会动态发生变化。
这种情况在玩虚拟机的时候经常出现。比如一开始只给硬盘分配了100G,装好系统之后用着用着发现不够用了,就在宿主机将分配好的硬盘扩容到200G。但到虚拟机里面会发现,并没有生效。
这种情况相当于硬盘突然多了100G未初始化的存储区。此时只能将新增的存储区划分一个新的分区,并将分区格式化为某个文件系统,再挂载上具体的路径使用。
当然这种做法不理想,因为使用的时候,具体的功能使用某个路径是相对固定的,而往往扩容的需求又是具体功能引发的,这显然没能无感解决问题。centos 用了 lvm 文件系统,可以很好的解决这种情况。有兴趣可另行了解。
4.2 不同文件系统的 inode 是共通的吗?
不共通,每个文件系统的 inode 都是独立的,文件系统在格式化一段分区之后各自初始化好了自己的 meta 数据,不依赖其他文件系统。
因此,不能跨文件系统做硬连接。
但是软连接是可以的,因为软连接本质上存储的只是一个路径信息。
4.3 在一台机器上把一个文件从 pathA mv 到 pathB,过程一样吗?
不一定。
如果 pathA 跟 pathB 在同一个文件系统下,那此时的 mv,其实做的只是对旧目录项的删除,新目录项的建立并关联 inode,这中间没有数据拷贝发生。所以速度很快。
但如果 pathA 跟 pathB 不在同一个文件系统下,mv 指令发生在不同的文件系统之间,情况就不一样了。文件需要从一个文件系统拷贝到另一个文件系统,才能重新关联。这个代价已经是 cp 的代价。
4.4 du 跟 df 的大小为什么会不一致?
df = disk filesystem ,读取的是文件系统的元信息(也就是在 group 0 的那些)。此指令是直接读取显示的,所以速度特别快;
du = disk usage , 是具体到某个目录下的统计用量。
df、du 的指令含义不一致,导致它们不同的原因有很多。
1、文件系统A本来的、在文件系统B挂在上来之后,被隐藏的文件;
2、假设文件系统B挂载到了文件系统A上,那么 df 指令下的文件系统A是不会统计文件系统B的。但 du 指令会遍历过去一起统计;
3、空洞文件。空洞文件的支持,导致 df 看到的逻辑容量跟 du 计算的实际容量不一致;
4、其他等等。