文件系统
文件
文件命名
对于MS-DOS来说,它是不区分大小写的,大多数现代操作系统是区分的。
在Unix系统中,文件的扩展名更多是给用户看看的,实际上操作系统可以忽略它,因为文件都是字节组成的,所以怎么解释这一堆字节实际上取决于用户程序;而在Win中,扩展名有强制性的意义。
文件结构
文件组成形式有三种:纯粹的字节序列,固定长度的记录组成的序列,块树。
第一种就是纯字节,一个字节一个字节连着的,拼成了整个文件,好处就是相当灵活,可以在任意地方添加,删除,修改,这是Win和Unix使用的形式。
第二种就是一些长度固定的记录拼在一起组成了文件,这些记录由固定数量的字节组成。
第三种就是把文件分成大小不一的块,然后组织成树的形式并排序,这个在处理商业数据的计算机比较常见。
文件类型
文件类型主要有这几种:
普通文件
目录
字符特殊文件
块特殊文件
目录是用来管理文件系统结构的系统文件。 字符特殊文件和输入输出有关。
普通文件分为ASCII文件和二进制文件。ASCII文件最大的优势就是可以打印,还可以使用任何文本编辑器进行编辑,同时还可以把ASCII文件作为输入输出。
二进制文件有一定的结构,无法直接打印,一般来说,只有使用此文件的程序在了解这个结构。来看两个二进制文件例子:
其中魔数用于说明这是一个可执行文件。
文件访问
文件访问有两种形式:顺序访问,随机访问。
顺序访问就如其字面意思,没法跳过,一个接着一个访问。
而随机访问则可以访问任意位置;对于随机访问,有两种方式,一是每次read操作都给出开始读文件的位置,而seek则是用来把当前位置设置到需要读的位置,然后连续地读就可以了,后者是Unix和Win使用的方式。
文件属性
用来指出一个文件可以进行的操作,修改时间,创建时间,所有者等。
文件操作
create:创建一个不包含任何数据的文件,此调用表明文件建立并设置一些文件属性。
delete:删除文件释放空间。
open:在使用一个文件之前,应该先打开文件,为的是把文件属性和磁盘地址表放入内存以便后续调用的快速访问。
close:当访问结束后,不再需要文件属性和磁盘地址,此时应从内存释放它们,关闭文件出触发对文件的完整写入。
read:读取文件中的数据,默认从当前位置开始读,调用者指明读取的数据量并读入缓冲区。
write:向文件写入数据,一般是从当前位置开始,如果当前位置是末尾。那么文件长度会增加,如果不是那么就会发生覆盖,造成原数据永久性的丢失。
append:在文件末尾添加数据。
seek:对于随机访问的文件,可以用seek把位置指针移到需要的文件位置去,然后开始相应的操作。
get attributes:读取文件属性。
set attributes:设置文件属性。
rename:对文件重命名。
目录
目录大多用来记录文件的位置,在很多系统中,目录也是文件的一种。
一级目录结构
在这种目录下,只有一个根目录,所有的文件都在一个文件夹下。
层次目录结构
现代文件系统多采用这种方式,从根目录像树一样展开,延伸出不同的目录和文件,并可以继续下去。
路径名
路径一般有两种:绝对路径,相对路径。
绝对路径指的是从根目录开始的路径,在Unix里是从'/'开始的。
相对路径指的是相对于当前工作目录的路径(也就是相对于当前程序所在位置的路径)
大多数支持层次结构的文件系统,都有两个特殊的字符:'.'和'..';前者表示当前目录,后者表示当前目录的父目录,在根目录中,'..'也表示当前目录,毕竟根目录可没父目录了。
目录操作
create:创建目录,当然操作系统会自动添加两个目录项:'.'和'..',这一过程也可由调用mkdir指令完成。
delete:删除目录,只有空目录才可以删除,只包含'.'和'..'也算空目录。
opendir:为了读取目录内容,必须先打开目录,这点和文件一样。
closedir:读取目录结束后,应该关闭目录释放内存。
readdir:总是返回打开目录的下一个目录项。
rename:重命名操作
link:允许同一文件出现在多个目录下,这种技术又名“硬链接”,它增加了i节点的计数器计数。
unlink:删除目录项,如果被删除的文件出现在一个目录里。那么直接删除,如果不是,那就只删除指定路径名的连接(把i节点计数器减一),在Unix中,删除文件其实就是调用unlink。
还有一种用于链接文件的方式,叫做“符号链接”,最常见的就是Win的快捷方式。这种方式大概原理是在链接目录下创建一个小文件,它指向真实的共享文件,当使用这个小文件时,文件系统会沿着路径最终找到实际的文件。
文件系统的实现
文件系统布局
多数磁盘划分为一个或多个分区,每个分区有一个独立的文件系统,磁盘的0号扇区称为主引导记录(MBR),后面是分区表,包含每个分区的起始地址和结束地址,表中的第一个分区被标记为活动分区,MBR会读入活动分区的第一个块(引导块),引导块中的程序会装载该分区中的操作系统。当然,分区里还有其他的东西,比如超级块,包含系统信息以及确定系统可执行二进制文件的魔数,块的数量等。
文件的实现
文件有四种存储形式:连续分配,链表分配,采用内存中的表进行链表分配,i节点。
连续分配仅需要文件起始地址和文件需要的磁盘块数,这样实现简单且读写性能好。但是缺点在于会造成磁盘碎片化,维护空闲分区是个比较大的开销。而且时间久了,每次写入文件都需要提前获取文件大小。
链表分配把文件存在链表块里,每个块包含指向下一个块的指针,每次仅需记录文件第一个块的地址即可,但是这样的缺点是,随机访问效率低下,每次都需要从头遍历,而且每个块记录的并不是全部文件数据,还有一个指针,空间利用率不高。
采用内存中的表进行链表分配把磁盘块做成一张大表,放在内存里,每个文件仅需记录起始块号,每个表里放的是下一块的地址,最后用特殊数字(-1)做终止。缺点是需要把整个硬盘放到表里面。这就是FAT格式。
i节点是给每个文件一个i节点,里面记录了文件的属性,以及每个磁盘块的地址,如果节点不够记录文件了,那么让其中几个记录磁盘地址的项记录另一个包含更多磁盘块地址的磁盘块的地址。
目录的实现
有两种方法实现目录,一种是固定大小的目录项,一种是只包含文件名和i节点指针的目录项,其余属性由i节点记录。
对于固定大小的实现,除了文件名就是一个文件属性结构体,以及用于说明磁盘块位置的一个或多个磁盘地址。
对于长文件名,有两种实现,两种方法区别看图,但是后者在删除添加文件时需要维护目录的堆,前者可能造成空间利用不充分,和连续分配的缺点一样,后面加入的文件名可能找不到合适的大小。
查找名字是一个费时的操作,可以使用散列表,把散列值相同连接在一起,表的键是散列值,表的值是目录项。
共享文件
对于共享文件,有两种常用的实现(同时也解决了文件在进程间的同步问题)。i节点(硬链接)和符号链接(软链接)。
对于i节点,大概原理是这样的:假设文件为A所有,B试图共享,就会增加一次节点计数,表明有另一个用户(线程)在共享它,但是如果A把文件删了,然后此i节点又分配给了另一个文件,那么就会发生错乱。或者只删除A的目录项,节点计数器减1(只要不为0就不会被删除),但是文件所有者依旧是A,这就会造成混乱,A得继续为B服务,直到B想删了文件为止。
对于软链接,就如快捷方式一样,软链接的实现方法是,创建一个Link类型的小文件,放在B目录下,然后让这个小文件只包含A中准备被链接的文件的路径名,当B读取小文件时,操作系统检查小文件类型然后去读真正的文件。是不是和Windows的快捷方式一样???!
对于符号链接,只有所有者才能删除文件,B删除的只能是快捷方式;而且在文件被删除之后,B访问将导致失败。
对于链接,有一个就是,在进行文件复制时,要能分辨出链接文件和真文件,不然会造成对同一文件的多次复制。
日志结构文件系统
大概说一下工作原理,就是为了解决过于零碎的写操作问题。
每次把准备写入的数据,放到缓冲区里,然后每隔一段时间,写入硬盘,怎么写呢?把它们作为一个段,拼接在日志末尾,也就是写入到硬盘的日志文件的末尾。LFS把磁盘当成一个日志来处理了。
作为一个单独的段写入到日志末尾,那么这个段可能包含数据,包含i节点等等信息。所以在LFS系统,i节点遍布于磁盘而不是像普通文件系统那样集中于某处。所以就要求能快速查找i节点,所以可以建立一张索引表,并存到内存里去。至于之后的操作,就像平时那样,读取i节点,找到文件磁盘块,属性等等。
时间久了,可能磁盘会满,LFS有一个清理线程,会定期回收空间和i节点,它也会压缩空间,来获得新的空间,比方说,它会检查段的摘要,找到那些i节点和块还在用,不用的直接丢弃,用的i节点和块就放到内存,准备重新写入,最后,把磁盘维护成了一个环形链表,前面的写线程写入,后面的清理线程清理空间,读线程读取。
日志文件系统
日志文件系统主要用来处理崩溃发生后的情况,对于一个操作,日志系统会先把操作放到日志里,然后执行,如果操作全部成功完成,再删除日志记录,这样可以防止在死机崩溃时丢失操作。
但是,日志系统要求操作必须是幂等的,就是可以被执行任意次且不会产生影响。
当然,还可以通过原子事务操作来确保操作的完成。
虚拟文件系统
一个操作系统,可能有多个文件系统。在Windows中,通过盘符来确定不同的文件系统,然后发送不同的请求。不过,对面的Unix阵营就很认真了,它们尝试把不同的文件系统整合到一起。
介于历史原,绝大多数的Unix都引用一个成为虚拟文件系统(VFS)的概念把多种文件系统组织成一个有序的结构。来看看它是怎么做到的:
首先VFS对上暴露一个接口,所有的用和线程不知道VFS的存在,他们想处理文件只管调用此接口,这就是著名的POSIX接口。
然后VFS对下暴露一个接口,这是给真正的文件系统使用的,所有注册于VFS的文件系统必须实现这些接口,这个接口里面包含了很多的功能调用(函数),由实际文件系统实现。所以VFS实际上有两个不同的接口,对上和对下。
大多数VFS都是面对对象的,这和编写它们的语言无关。有几种通常支持的主要的对象类型:超块(用来描述文件系统),v节点(描述文件,或者说支持更多功能的i节点)和目录。它们的每一个都需要实际的文件系统提供相关的操作,当然,VFS也有它自己的数据结构用于处理额外的操作。
假设进程发起一个打开操作,那么实际的工作原理如下:
首先根文件系统在VFS里注册,无论是在启动时还是运行中,当装载其他系统时,也会把它们注册在VFS里。然后提供一个函数列表指针,这个函数列表包含了基本的操作。这个指针供VFS调用。
此时开始读,VFS确定文件系统,从已注册的文件系统的超块组成的表里找到超块然后解析获取系统信息。找到文件,创建v节点,调用实际的文件系统,返回对应文件的i节点的信息,把这个信息和其他信息一起复制到v节点,这些其他信息包括一个指向函数表的指针,这个函数表包含了可对v节点执行的所有操作,比如read,write,close等。
此时v节点被创建,VFS在文件描述符表里新建一个表项,让它指向某个数据结构,这个数据结构包含v节点和当前文件位置,最后VFS把文件描述符返回给调用者,调用者用这个文件描述符来读,写,关闭文件。
当调用者使用文件描述符进行操作时,VFS通过进程表和文件描述符表来确定v节点的位置,并跟随指针指向函数表(这个函数表包含了一些可以用来操作实际文件系统的函数调用),然后由实际的文件系统来完成相应的操作,此时一个处理完成。
所以对于新加入的文件系统,设计者应获得VFS要求的功能调用的列表,并实现。然后注册到VFS中去。
文件系统的管理和优化
磁盘空间管理
块大小是一个很重要的指标,要知道,块大了,会浪费空间,块小了,同一个文件会产生更多次寻址操作,浪费时间。所以合理的设计块大小很重要。
记录空闲块也是一个很重要的需要考量的事情。有两种方法,一种是空闲链表法,一种是位图法。
对于空闲链表法,通常情况下,链表存在硬盘里,每个块包含许多个记录空闲块编号的小空间,然后还有一个指向下一个空闲块记录块的指针。
对于位图法,已使用用0表示,未使用用1表示,n个块需要n位位图。
当然,如果磁盘普遍比较空的话,可以记录空闲块首地址以及空闲块连续的长度。
接着来看链表分配,此时删除文件,触发对内存里的空闲表的写入,在快满了的情况下,写满,就会触发写回硬盘操作,但是接下来可能很快就开始写入,那么此时就需要用掉空闲表,如果内存里的空闲表不够可能还要从硬盘读入,这样来回的操作很容易产生不必要的IO,所以可以尝试在快满了的时候,拆分内存里的空闲表,把它拆成一半半,一半写入磁盘,一半留内存。
在位图中,这就没必要,因为位图是按块序号排序的,所以分配的结果是可以把文件比较紧凑地聚集在一起,这样就减少了磁盘臂的运动。
磁盘配额限制用户的文件数量和可用磁盘空间。
文件系统备份
增量转储在一次全备份后,每次只备份发生了更新的内容。
物理转储把整个硬盘进行备份。
逻辑转储递归的备份自给定日期后发生更改的文件。
当进行逻辑转储时,还要把通向备份文件或目录的路径上的全部文件和目录进行备份,为的是方便整体迁移和对单个文件进行增量恢复,这样可以获取需要恢复文件的一些属性。
逻辑转储算法要维持一个以i节点为索引的位图,每个i节点包含了几位,随着算法的执行,位图中的标志位会被设置或清楚。算法的执行分为四个过程。
这儿有四个阶段,第一阶段检查所有目录项,算法还会递归的检查所有目录,对于修改过的,设置标记位。
第一阶段结束,所有修改过的节点都被标记了;第二阶段遍历,清除不包含任何标志位为已修改的文件或子目录的目录的标志位。
第三阶段转储标记位为已修改的目录。
第四阶段,转储已修改的文件。
对于转储,空闲列表不是文件,所以每次备份都需要重新构造,另一个问题就是链接文件,记得只恢复一次(避免多次复制同一个文件),以及原有链接的建立。
文件系统的一致性
一致性的检查分为两种:块一致性和文件一致性。
程序构造两张表,一记录每个块在文件中出现的次数,另一个记录块在空闲区中出现的次数,空闲块计数直接读取空闲列表就能做到,已使用计数,则需要读取全部i节点,然后把i节点包含的数据块在已使用表里面+1;所以有可能出现四种情况:
第二种,块丢失,仅需重新加入空闲表即可。
第三种,空闲表出现重复块,重新建立空闲表即可。
第四种,数据块重复,也就是已使用的计数>1,此时把重复的块的内容复制到空闲块,然后把它插入到其中一个文件(因为此时表明有两个或多个文件同时使用了这个块来放数据)。并报告用户。
接下来是文件计数,对于每个目录中的每个文件,将文件计数器+1,硬链接也计数,但是符号链接不计数。然后,与i节点保存的计数相比较,如果i节点的计数大(说明文件未正确释放,或硬链接未正确-1),此时设置正确的i节点即可;若i节点小于计数(说明文件可能要被误删了,得及时制止,等一下,这就要求对i节点的操作是原子性的),则重置i节点为正确的值即可,这两个情况都是重置i节点的计数。
文件系统的性能
高速缓存就是在硬盘里放一个缓冲区,用来存放被访问的块。
高速缓冲区一般使用双向链表实现,最近最常使用的放后面,最不常用的排前面,每次需要载入新的块就从前面移走。
但是对于可能经常访问的数据,尽量放后面;i节点等和文件系统一致性有关的,应立即写入磁盘,而不论它在哪里。
最后,应该定期刷新缓冲区,把数据写入到磁盘。在Unix中,sync系统调用每30秒刷新一次,而Windows则是每次写入高速缓冲区都写入磁盘。原因在于Unix多用于不可移动的磁盘,如服务器,而Windows则是个人PC居多。
块提前读策略仅适用于顺序读取,随机读取反而会降低性能。
减少磁臂运动是说尽量把数据写在柱面,或者说,把i节点写在磁盘中部或与文件在同一个柱面组里。
磁盘碎片整理
用来减少磁盘空穴,压缩磁盘空间,但是对SSD没用甚至会减少其寿命。
文件系统实例
MS-DOS
Unix V7
来看看Unix V7的文件空间分配: