文件系统简述

0X00 文件

** ‘文件’是进程创建的逻辑单元。 ** —《现代操作系统(原书第三版)》
文件我们再熟悉不过了,电脑磁盘上存的都是文件。在Windows里和Unix系列系统里,表面看上去文件之间还是有一点点小的区别。比如,在Windows里主要还是以文件的后缀名来标识文件具体是什么类型的,图片还是视频;在Unix系列里文件的后缀名就没那么重要,主要后缀名是用来帮助人们识别文件类型的,操作系统并不很关心。
** 真正的文件类型 ** 是文件的本质类型,不是我们常说的exe类型、doc类型、更不是什么图片类型和视频类型。在Windows下有常见的普通文件和目录。没错,目录其实是文件的。在Unix里,还有一些叫字符特殊文件和块特殊文件的。

0X01 文件的元数据

文件里最重要的东西肯定是文件内容了,但是文件存在磁盘里是还有一些其他的相关数据也被存进去了的,那些数据被称之为元数据。想一下文件的相关信息,在Windows里右键一个文件选择属性或者在Linux里使用ls -l看到的文件的详细信息,这些几乎全部都是文件的元数据,都存到了磁盘中。常见的元数据有下面这些:创建者、拥有者、权限标志、文件大小、锁等等。比如说我们在Linux下输入ls -l能看到文件的大小、权限、所属者,这些都是文件的元数据。

0X02 MBR-主引导记录

经常装系统的话应该比较熟悉这个词‘主引导记录’,在以前的磁盘上常用的就是这种称为MBR的磁盘分区方式,其实现在还有好多在用MBR的,不过由于MBR的原理导致不支持2TB以上的磁盘且不支持4个以上的主分区,所以用的越来越少了,取而代之的是GPT。不过由于MBR比较简单,就先介绍一下MBR。
计算机在启动的时候,BIOS会读取MBR的分区表来找到引导分区并引导操作系统。可以启动的分区称为活动分区,必须要是活动的分区才可以引导系统启动;MBR的分区表只能容纳四个分区,如果需要更多的分区就需要创建扩展分区。可以在一个MBR的分区表中创建三个主分区,在最后一个位置创建一个扩展分区。实际上最后一个扩展分区是不能直接使用的,相当于扩展分区在磁盘上花了一块当另一个磁盘用、在扩展分区头部还有一个扩展分区的分区表,里面保存着逻辑分区的分区信息,且这个分区表的空间比较大,所以逻辑分区可以创建好多个。

0X03 文件存储

文件存储在磁盘中有好多种分配方案,这些方案各有利弊。随着存储介质、CPU等设备的发展和人们需求的变化,出现了下面这些比较好的方案。

连续分配

首先我们把磁盘想象成一个超长的条形存储设备,这样就比较好理解(然而实际上现在常见的磁盘是区分盘面、磁道、柱面、扇区的)。
早期的磁盘和现代的CD-ROM是使用这种连续分配方式存储数据的。连续分配,由字面可知是把文件连续的从头到尾得存到磁盘里,这种方式读写都非常快,但是却非常不适合日常使用。考虑下面这种情况,我有下面这些文件
[ A ][ B ][ C ][ D ][ E ][ F ]整个磁盘大小为6GB,每个文件都有1GB,刚刚好用完整个磁盘。但是当我删除了B和E两个文件的时候就会变成下面这样,空余两个1GB的位置出来,但是这两个空间不连续
[ A ][ ][ C ][ D ][ ][ F ]现在我有2GB的空间。系统需要维护一个空闲空间列表来让后来的文件放在这些空闲的地方,因为如果不维护这张表的话,当磁盘写满过一次就再也不能写入新的数据了。虽然我们维护了一张这样的表也并不能很让人满意,比如有一个1.5GB的文件想存到磁盘里,系统就会查找连续的空余空间,但是并没有一个连续的高达1.5GB的空间,所以并不能把文件存进去,显然这并不能让人满意。而且,这些还都是建立在一个前提之下的,就是说“存储文件之前必须知道文件的大小”,然而事实上很多时候是不知道的,比如我们打开了AE(一款渲染视频的软件)来制作一段视频特效,然而在生成视频的时候没有人知道这个文件最后是多大的,所以就并不适用于这种情况。但是这种分配方案就没有优点了吗?也不是的。比如我们需要将数据刻录到CD-ROM上,因为CD-ROM是只读设备,所以在第一次刻录之后就没有修改的可能了,那么我们就可以通过这种方案直接将已有的数据顺序刻录到光盘里,这样以后的读取就会变得很快了。然后针对磁盘有了下面的‘链表分配方案’

链表分配

使用链表分配方案时,目录下的每一个文件都只保留文件的头指针,每个文件都是一个链表,这样我们就可以顺着指针的指向把整个文件从文件系统中遍历出来。虽然链表分配方案成功的利用起来了空闲空间,但是还是有下面两个比较严重的问题:

  1. 每次想要访问文件的第n个节点时候,都要从文件头开始访问,有n-1次磁盘的访问是无效的,所以这种方案对随机读取非常慢;
  2. 因为每个磁盘块的大小都是2的n次幂,保存的大小也就是2的n次幂,但是因为文件头被指针占去了一定的字节,就导致实际存储的文件并不是2的n次幂。虽然这个问题并不是致命的,但是确实会让系统变慢,也会让面向系统的编程变得困难很多。

内存链表解决了链表分配的一些问题。

内存链表分配

内存链表分配是将磁盘里所有文件的所有块都做成链表,依旧是每个文件一个链表。但是这次将链表整个存放到内存中,这样在随机访问的时候因为链表全都在内存中就会非常快。但是由于要对每一个文件建立存储,且存放在内存中,所以这种文件系统并不适合用于小文件大磁盘。对于一个200GB的磁盘,里面充满了1KB的块,那么根据系统优化之后这张表需要600~800MB的内存,然而现在动辄TB级的磁盘,则非常不适用。这种内存链表分配方案中维护的表称之为‘文件分配表’英文也就是我们熟悉的‘File Allocation Table-----FAT’
为了克服内存链表分配的内存占用大的问题,有了i-Node方案。

i-Node

i-Node方案在磁盘头部预留一段空间用来存放i-Node,这里的i-Node是一种数据结构,里面包含了文件的一些元数据和文件所有块的相关信息,所以根据一个i-Node就可以找到着整个文件。因为每个i-Node的预留空间都是固定的,如果文件太大太分散就会导致一个i-Node并不能存储完所有信息,那么i-Node中最后一段就保存了另一个i-Node的地址,然后在另一个i-Node中继续保存信息。因为i-Node是保存在磁盘里的,所以不会影响到内存,只有当文件真正打开的时候才会将数据加载到内存。所以内存占用是核同时打开的文件数量相关的。在Linux中我们使用ls -l -i就可以看到每个文件的i-Node编号。现在的大多数文件系统都采用这种方案了,比如EXT、NTFS、XFS等。

0X04 文件共享

首先明确两点:1. 这里说的文件共享并不是说将一个文件通过网络传输给他人的那种文件共享; 2. 系统中的文件结构不是树状,而是图。(当Windows中我们给一个文件建立了一个快捷方式并放在了另一个目录里的时候,就形成了图解构)
这里的文件共享主要就是链接的问题,关于链接的内容可以在我博客里找到。Linux软连接/硬链接 理解Linux链接
每个文件会保存指向自己的链接数,当只想自己的链接数为0的时候,那么这块数据就抛弃掉了。

0X05 文件系统

日志结构文件系统

因为现在的CPU运算能力和磁盘容量、内存容量等都有了非常大的进步,所以在不实际访问磁盘只在高速缓存上就能访问到很多需要的数据,所以根据这种情况,就出现了日志结构文件系统(Log-structred File System)。这种文件系统将文件操作结构化成日志。在这种文件系统中每次将数据读写缓存到内存,然后定时定量地将数据从内存写入磁盘。

日志文件系统

日志文件系统比日志结构文件系统有更强的鲁棒性(Roubst 也就是健壮性)。在这种文件系统中进行文件操作时,先记录下要干什么,然后再开始操作。这样不管什么时候出了错误,都可以根据日志来恢复操作。比如在Unix中删除一个文件分成三个步骤:1.在目录中删除文件 2.释放i-Node到空间i-Node节点池 3.强磁盘块归还到操作系统。 如果完成了第一步,就死机了,那么由于日志的存在就可以在知道这个操作究竟要干什么,在恢复开机的时候就可以继续完成这次操作。当所有的任务项都完成了的时候就删除这个日志。

虚拟文件系统

在Windows里用的是多根目录的方式,也就是有多个根目录,比如C盘D盘E盘等,但是在Linux中我们使用的是单根目录形式,如果要同时使用几种文件系统比如:根目录使用XFS、/home使用ext4、/usr目录使用ext3、那么久需要使用一种叫做虚拟文件系统的技术。两个不同的文件系统之间需要连接的话需要使用VFS(虚拟文件系统)接口来将两个文件系统连起来。使用这种虚拟文件系统的技术就可以让同一个根目录下面挂载有不同文件系统的设备。

0X06 磁盘分块

已知文件在磁盘里是按照块存储的,那么每个块分配多大就成为了一个问题。因为在磁盘底层,每个文件占用的块都是整数,比如我一个块是1kb,那么我有一个2.5kb的文件也要占用3个块,甚至是1字节的文件也要占用1kb,每个块中剩余的部分是不能存储其他文件的。从这方面看来分块越大就越浪费空间,块越小磁盘空间利用率越高。那么我们把块都分成最小,这样就行了吗?显然没有这么简单。因为磁盘在读写数据的时候是按照块来的,所以分的块越大读写的速度越快,因为磁盘里的总块数少,块越小越慢。总结下来是这样:随着块大小的提升,磁盘读写效率会提高,但是空间利用率会降低。统计所得,分块大小为4kb最容易获得最佳性能。

0X07 缓冲区

现在的文件系统都支持缓冲区写入。缓冲区写入对应的另一种是‘同步写入’。缓冲区写入:程序生成的或者用户的数据首先写入到内存中,当达到一定时间或者一定量的时候一次性写入磁盘;同步写入:将程序和用户产生的数据实时写入磁盘里。下面对比一下这两种的优缺点

同步写入优点:数据实时同步,出现数据丢失的可能性很小
同步写入缺点:由于数据产生很慢,所以磁盘利用率不高,且长期占据磁盘
缓冲写入优点:数据首先写入比磁盘快得多的内存中,再统一写入磁盘只会短时间占用磁盘,且占用磁盘时利用率高
缓冲写入缺点:当数据在缓冲区没有写入磁盘时系统发生异常或者崩溃,数据非常容易丢失

这里有两段Python的代码,展示了缓冲区写入和同步写入的速度差异,首先是使用同步写入方式写入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/usr/bin/python
#coding=utf-8

from time import ctime

f = open('D:/list.txt', 'r+', 0) #不使用缓冲
start_time = ctime() #开始时间
for i in range(3000000):
f.write('hello,world\n')
f.close()
end_time = ctime() #结束时间

print start_time
print end_time

运行结果是这样的,我们看到写入300W行’hello,world’用了8秒,最后生成的数据量是37.1MB

1
2
Thu Nov 17 11:12:21 2016
Thu Nov 17 11:12:29 2016

然后使用Python默认大小的缓冲区试试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/usr/bin/python
#coding=utf-8

from time import ctime

f = open('D:/list.txt', 'r+', -1) #使用默认缓冲区大小
start_time = ctime() #开始时间
for i in range(3000000):
f.write('hello,world\n')
f.close()
end_time = ctime() #结束时间

print start_time
print end_time

运行结果是这样的,我们看到这次写入相同的数据只用了1秒

1
2
Thu Nov 17 11:18:04 2016
Thu Nov 17 11:18:05 2016

0X08 坏块屏蔽

在磁盘这种物理结构里出现错误是比较正常的,尤其是机械磁盘磁臂在旋转的时候与磁道的摩擦会产生部分坏块。这些坏块上的数据会发生丢失或者错误,那么怎么屏蔽这些坏块呢?之前在那些分区软件里看到过坏块屏蔽,感觉非常高端,其实原理是很简单的。比如磁盘里有23块坏块,那么修复程序就创建一个文件,指定这个文件就存储在这23个坏块上,且对操作系统不可见,那么操作系统虽然知道这里有文件,但是不会去管他,这样就相当于屏蔽了磁盘里的坏块。

0X09 提升文件系统性能 高速缓存

高速缓存就是将即将需要的文件和经常使用的文件放在磁盘的高速缓存里,因为高速缓存的速度比磁盘要快得多,所以就可以通过这种方式来提高I/O效率。
在Unix里有一个系统调用sync,在Windows里有一个FlushFileBuffers,是用来将高速缓存里的数据同步写入到磁盘里的。在Unix系列系统中每隔30秒就执行一次sync将数据写入,在Windows中则是实时的。这两种方案并没有谁好谁坏之分,各有优劣。

0X0A 磁盘碎片整理

因为绝大多数的现代文件系统都采用了链表存储的方案,所以在使用磁盘一段时间之后文件都是分散的放在磁盘的各个角落的,这样的话读写文件就会变得比较慢,文件越零散读写就越慢。那么我们可以手动将磁盘进行整理,将分散的文件数据聚合到一起,当然也只能是尽量,因为某些数据是不能被移动的,比如页文件、休眠文件、日志。当零散的文件变成连续的文件的时候读写的效率就会有大幅度提升。但是由于各个操作系统采用的文件系统的内部实现不同,导致几乎只有Windows需要对磁盘进行手动整理。当然所谓的手动整理也是有软件支持的,不需要用户自己去操作磁盘。但是因为Windows的发展,也几乎不再需要手动进行整理了。