第四部分 存储管理 文件系统

2020-01-09  本文已影响0人  CocoAdapter

第十章 文件系统

这部分主要是一些较高层的策略问题,主要是面向上层开发人员,而不涉及太多具体实现。

1. 文件操作

1.1 基本操作集合

文件为抽象数据类型。操作系统提供 6 个系统调用构成操作文件的最小集合,包括

读写操作可以使用相同的指针。称为 当前文件位置指针。这个指针的初始化受文件访问模式影响,APPEND 会置于末尾,否则是文件头。

1.2 打开文件表

以上操作大多涉及搜索目录,得到文件的相关条目,为了避免这种搜索,OS 一般要求访问文件前需要先 open()。OS 有个打开文件表,维护所有打开文件的信息。如果不用了再 close()。create/delete 操作的是未 open() 的。
open() 函数返回一个文件句柄 (Windows) 或者称为文件描述符 (UNIX-like),后续操作都以这个值作为指针,避免重复搜索。同时,open() 一般也支持访问模式信息,如 CREATE, APPEND 等。OS 会根据文件系统的权限子系统检查 open() 是否合法。

文件系统的设计哲学显然是一种 “有状态” 通信。有状态和无状态的区别主要是是否单次操作需要携带所有必要信息。如果是无状态,那么每次都要指定文件名、访问模式、当前文件指针等等,NFS 就是这么设计的;有状态就得把状态维护起来,放在一个数据结构里,每次操作只需要携带一个指针以让系统知道操作的是哪个项。这两种设计哲学广泛分布于计算机领域,如 TCP 和 UDP,TCP 有状态,每条链接的状态信息由 OS 维护,而 UDP 每次读写数据互不干扰,OS 并没有 UDP 的状态信息(可能需要维护其它信息,如端口信息)

通常,OS 采用两级内部表:单独进程表系统总表

1.3 同步

OS 可以提供强制 (Windows) 或建议(UNIX-like) 的文件锁定机制。强制锁会要求文件操作的所有 API 都检查锁,也即一个进程加了锁,OS 会确保其它进程调用 open() 这类函数的时候会被阻塞。这是一个内核级别的实现。而建议锁是指,一个进程加了锁,其它进程需要主动去检查这个锁的状态,再决定是否操作文件。Linux 文件锁 的实现可以参考这篇文章。
Java 实现文件互斥访问时通过要访问文件的 FileChannel 的 lock() 取得 FileLock 来做得。FileLock 支持共享和非共享,同时支持字节级别的锁定。

public abstract FileLock lock(long position, long size, boolean shared)
        throws IOException;

2. 文件类型与结构

实现文件类型的常见技术是将类型作为文件名的一部分,即放在扩展名里。UNIX 系统采用位于某些文件开始部分的魔数(magic number)大致表明文件类型,但不是所有文件都有魔数。Linux 的可执行文件是依赖于其执行权限而不是拓展名。所以综上,文件类型的区分方法千奇百怪。
文件类型决定文件结构。OS 必须支持至少一种结构,即可执行文件的结构,以便系统能加载和运行程序。但是,OS 一般也不应该规定太多文件结构,谁支持谁就得负责,这个让 OS 来做 OS 就很亏。

Linux 大致有这些文件类型:普通文件、目录、字符设备、块设备、套接字、符号链接

3. 访问方法

4. 目录与磁盘的结构

文件系统底层是磁盘,向上提供前面提到的 API,那么必须维护目录来组织这些文件。

4.1 一些概念

  1. 磁盘 磁盘是一块单独的物理设备
  2. 分区 磁盘格式化后才能使用,格式化实际上就是对磁盘进行分区,分为不同的部分,每个部分有些元数据表明物理存储的一些信息,也就是文件系统的元数据。按传统分区技术,分区一般分为主分区和扩展分区;分区表格式有主引导分区(Master Boot Record, MBR,已逐渐淘汰) 和 GPT(GUID Partition Table) 分区。比如 Windows 的 C 盘、D 盘就是分完区后的结果。详见鸟哥的 Linux 私房菜磁盘部分
  3. 文件系统 文件系统和分区不是一个概念,分区一般说的是物理存储设备,文件系统是描述存储设备的内容格式。
  4. LVM Logical Volume Manager Linux 下的逻辑卷管理器,向上屏蔽掉底层物理磁盘的细节。实际上就是让 OS 能操作多块磁盘以及支持扩容、缩容等操作。传统分区有很多限制。
LVM

PV(Physical Volume):物理卷,处于LVM最底层,可以是物理硬盘或者分区。
PE(Physical Extend):物理区域,PV中可以用于分配的最小存储单元,可以在创建PV的时候制定(默认为4MB),如1M, 2M, 4M, 8M, 32M, 64M…组成同一VG中所有PV的PE大小应该相同。
VG(Volume Group):卷组,建立在PV之上,可以含有一个到多个PV。
LV(Logical Volume):逻辑卷,建立在VG之上,相当于原来分区的概念。不过大小可以动态改变。

  1. VFS Linux 支持多种文件系统,为了屏蔽底层细节,VFS 扮演了中间层的角色

4.2 目录

目录(directory)可以理解为一个字典,记录的是文件名到磁盘存储位置的映射。目录可以分为单级目录、两级目录、树形目录(主流目录结构)、无环图目录(为了共享子目录)。

UNIX-Like 既支持基于软链接的共享,这个时候文件系统并不把软连解当作实际节点,删了之后也依然保留,需要程序员自己意识到被删了;也支持基于硬链接的共享,后者修改了 inode 的内容,会由文件系统管理节点。详见相关博客。

4.3 文件系统挂载

文件系统在使用前得先挂在到某个目录,挂载位置称为挂载点,被挂载的称为设备。比如, Linux 中,/home 一般是个单独的分区,内核会在启动的时候将其挂载到根目录下的 /home 处。然后我们才可以通过 /home/test.c 直接访问 test.c 文件。

Linux 默认只有一个分区,即 /,这个分区上运行着发行版默认的文件系统。这个 / 里面可以装文件,也可以挂载其它文件系统。这里首先得认识到 OS 目录树和文件系统不是一个概念。你可以在 /test1 挂载一个 ext3 文件系统,可以在 /test2 挂载一个 xfs 文件系统,可以在 /test3 放一个普通文件。挂载可以看作根目录的某个节点被另外一颗树替代。Windows 会在启动时自动安装所有文件系统(卷 C, D...),如果我们插入 U 盘,Windows 会识别并把 U 盘挂载到目录树上,只不过这个目录树是两级的。Linux 的挂载点一般可以是任何位置,Windows 的新版本似乎也可以,但不清楚。

4.4 保护

UNIX-like 文件系统的一般权限 和 SUID, SGID, SBIT等

第十一章 文件系统实现

1. 文件系统的结构

文件系统一般分层实现,从上往下:

内存和磁盘之间的 I/O 以块 (block) 为单位,块的大小与磁盘扇区大小无关。同时,上述模块在实际实现中可能揉在一起。

2. 文件系统的实现

2.1 元数据

首先考虑一下文件系统需要维护哪些信息。分别从磁盘上和内存中来考虑(下述不区分卷和分区的概念,因为卷实际上就是个逻辑分区):

2.1.1 磁盘上

BIOS/UEFI 是启动检测程序。他们运行后,会去读 MBR/GPT 分区,然后 MBR/GPT 分区可以直接加载内核文件来启动 OS,也可以将控制流转到其它引导控制块。

2.1.2 内存中

2.2 操作函数

2.2.1 创建文件/文件夹

创建文件时,应用程序调用逻辑文件系统,由其分配一个新的 FCB。如果文件系统的实现在文件系统创建时(格式化的时候)已经创建了所有 FCB,如 ext 文件系统,则可从空闲的 FCB 集合中分配一个可用的 FCB。然后逻辑文件系统用文件名和新的 FCB 去更新其对应的目录文件,并写回磁盘,这个过程会调用下面的文件组织模块。
UNIX 不区分文件和文件目录的处理,而 Windows 提供两套系统调用。

2.2.2 打开文件

文件使用前需要先打开。open() 首先需要搜索系统打开文件表查询是否已经打开,如是,直接在进程打开文件表中新建条目并指向系统打开表中的条目,同时更新计数;否则,需要搜索文件所在目录,这个通常缓存在内存中,找到文件后,将其 FCB 复制到系统打开文件表,并在进程打开文件表中重复前面相同的动作。
进程打开文件表中的条目一般包含一个指针指向系统表,所有文件操作通过这个指针进行,所以文件表不需要包含文件名,因为可以通过指针访问 FCB。这个在 UNIX 中称为 文件描述符(file descriptor),在 Windows 中称为 文件句柄 (file handle)。

2.2.3 读写文件

读文件需要指定文件描述符,然后内核访问进程的打开文件表,获取系统打开文件表的索引,再根据系统文件表中存储的 FCB(可能在内存中也可能是一个指向磁盘的指针),然后获取文件的元数据,包括数据存储位置、当前位置指针等。然后向下发送读取指令,最后由 I/O 设备完成读取。读到的内容放入指定的缓冲中。
写文件类似,只不过一般都是写缓冲,落盘的策略一般由文件系统自行安排或者应用程序手动调用 fsync。

2.2.4 关闭文件

关闭文件需要删除进程打开文件表中的条目,并递减系统打开文件表中的计数。当所有打开该文件的用户关闭它时,任何更新的元数据落盘,并且删除系统表中的条目。

2.3 分区与挂载

分区并不一定得有文件系统,没有格式化的分区也可以使用,称为裸盘,原始磁盘 (raw disk)。例如,UNIX 的交换空间就是使用的裸盘,数据库也经常使用。
根分区,包括 OS 内核和其它系统文件在启动时挂载。其它卷可以在引导时自动挂载或在启动后手动挂载,取决于 OS。
挂载的实现一般时标记某个 FCB 为挂载点,同时在其里写入安装表中条目的指针,即完成挂载。

2.4 虚拟文件系统

VFS 实际上就是 OOP 技术的一种体现,通过中间层来屏蔽底层多种实现。

3. 目录实现

4. 分配方法

4.1 连续分配

连续分配的好处是,简单,支持顺序访问和直接访问,对于磁盘结构,读取效率高。缺点是,本质上是一个动态存储分配问题,也就存在这类型问题的所有问题,如外部碎片,如何快速找到一个最好的可用分配,文件增大怎么做。
外部碎片可以通过复制-整理算法来做,这个技术在垃圾回收算法中常用。存在 STW 问题,而且需要额外存储空间来做整理。文件增大一般通过扩展来解决,即一开始先分配一块,如果增大,再分配一块,第一块链接到后一块上。

4.2 链接分配

链接分配解决了上述所有问题,但是不能有效支持文件的随机访问。顺序访问的情况下,与索引分配一样,对于磁盘结构,可能效率低下,因为要不停旋转磁盘头。同时,因为链接指针需要空间,所以有效存储率变低了。
链接分配的一个重要变种就是文件分配表(File Allocation Table, FAT),广泛用于 MS-DOS。每个卷的开头部分的磁盘存储一张表,表里存储了卷上的所有块的指针,从而解决随机访问的问题。

4.3 索引分配

索引分配将所有指针放在一起形成索引块,解决随机访问的问题。每个 FCB 里存储了各自的索引块。
UNIX 文件系统使用一种组合策略来优化索引的存储,inode 的前12 个指针是直接索引,直接指向文件的数据块的地址;接下来的 3 个指针指向间接块,分别是一级间接块、二级间接块、三级间接块,对应二级、三级、四级索引。

5. 空闲空间管理

为了跟踪空闲磁盘空间,需要维护一个空闲空间列表。创建和删除文件块都需要修改这个空闲空间列表。一般通过位图来实现,例如 UNIX;也可以通过链表等。

后面还包括对效率、性能、恢复、一致性、NFS 的讨论,这里不再总结。

上一篇 下一篇

猜你喜欢

热点阅读