一、基本概念
文件系统和文件
文件系统:一种用于持久性存储的系统抽象(持久性存储比如硬盘)
文件:文件系统中一个单元的相关数据在操作系统中的抽象
文件系统的功能
文件描述符
操作系统内核会跟踪每个进程所打开的文件:操作系统为每一个进程都维护了一个打开文件表,一个打开文件描述符就是这个表中的索引。
操作系统需要通过以下这些元数据信息来管理这些打开文件:
- 文件指针:指向最近的一次读写位置
- 文件打开计数:记录打开文件的次数,最后一个进程关闭了文件时,允许将其从打开文件表中移除
- 文件磁盘位置:缓存数据访问信息
- 访问权限:每个程序访问模式信息
除了这些元数据之外,站在系统访问的角度来看还需要有:
- 用户视图:一种持久的数据结构
- 系统访问接口:高层系统或者用户角度不会关心你想存储在磁盘上的任何数据结构
- 操作系统内部视角:块的集合(块是逻辑转换单元,而扇区是物理转换单元);块的大小不等于扇区(磁盘访问基本大小)的大小
文件的结构有多种,而对于文件内容的解析是交给应用层去完成的,操作系统只通过字节流来对文件进行读写。所以应用程序可以在文件中定义很复杂的文件格式,这样可以简化操作系统的实现。
文件时可以共享的,因此在多用户系统中的文件共享是很必要的,因此就要对文件进行访问控制,操作系统就给文件定制了文件访问控制列表(ACL),表中存放格式:文件实体 - 权限。访问模式被分为:读、写、执行、删除、列举等。UNIX模式中,指定了不同的用户对应的不同的权限,记录格式为:用户|用户组|所有人 - 读|写|可执行
用户ID:识别用户,表明每个用户所允许的权限以及保护模式
组ID:允许用户组成组,并指定了组的访问权限
多个用户如何同时访问共享文件,对此Unix文件系统语义中表明:
- 对打开文件的写入内容立即对其他打开同一文件的其他用户可见
- 共享文件指针允许多用户同时读取和写入文件
**会话语义:**写入内容只有当前文件关闭时可见,也就是关闭时才会进行回写数据
**锁:**一些操作系统和文件系统需要提供该功能文件锁,来提高执行效率
目录
文件是以目录的方式组织起来,目录是一类特殊的文件,目录和文件的基础结构类似都为树形结构(早期的文件系统是扁平的只有一层目录)。
操作系统只允许内核模式来修改目录,应用程序只能发出修改目录的请求,这样确保了映射的完整性。
一个文件系统需要先被挂载在挂载点上才能被访问,一个挂载点可以挂载不同的文件系统。我们通常可以称呼一个可被挂载的数据为一个文件系统而不是一个分区。
挂载点一定是目录,该目录为进入该文件系统的入口。如果使用CentOS 7.x的话,那么就应该会有: /、/boot、/home 三个挂载点。
文件别名
两个或多个文件名关联同一个文件。这种关联有多种实现方式:
- 硬链接:多个文件项指向一个文件(使用inode链接来产生新的文件名而不是文件)
- 软链接:以快捷方式指向其他文件
- 通过存储真实文件的逻辑名称来实现
如果删除一个有别名的文件:
- 软链接:那么这个别名会成为一个悬空指针,指向空
- 硬链接:每个文件有一个包含多个backpointers的列表,相当于计数器,每删除一次都不会真正删除文件,而是删除一个backpointers,直到所有的列表元素全部被删除才真正去删除这个文件
如果不对别名关联方式加以限制就有可能会出现循环,别名指向自己上层的文件的情况发生,对应的解决方式为:
- 只允许到文件的链接,不允许在子目录上进行链接
- 每增加一个新的链接都用循环检测算法确定是否合理
- 限制路径可以遍历文件目录的数量
文件系统种类
- 磁盘文件系统:文件存储在数据存储设备上比如磁盘;例如:FAT(Windows、U盘)、minix、ext2(Linux【索引式文件系统】)、ISO9660等
补充小记:
Linux的正统文件系统为ext2,通常会将文件权限(rwx)、文件属性(拥有者、用户组、时间参数等)与实际文件数据分别存储在不同的区块,权限和属性放到inode中,数据文件则存放到数据区块中。其中包含三中区块:
- 超级区块:记录此文件系统的整体信息、包括inode与数据区块的总量、使用量、剩余量,以及文件系统的格式与相关信息等
- inode:记录文件的属性,一个文件占用一个inode,同时记录此文件的数据所在的区块号码
- 数据区块:实际记录文件的内容,若文件太大,会占用多个区块
而在FAT文件系统中则不是通过inode这种方式来进行数据存储的,则是通过在区块中给出下一个链接区块的地址信息,从而形成了一种类似于链表的调用方式。所以FAT没有办法一开始就将文件的所有区块全部读出,而ext2则可以通过inode中记录的所有区块信息,一次性全部读出。文件系统中难免会有碎片整理的问题,就是文件写入的区块太过于离散,导致了文件读取的性能降低,因此对于FAT来说需要频繁进行碎片整理,将同一个文件所属的区块集合在一起,对于ext2来说则不是很需要进行碎片整理。
对于ext2来说一开始就将inode与数据区块划分好了,除非重新格式化或者利用resize2fs命令改变大小,否则就会固定不变。
inode中记录的数据至少有:
- 文件的读写属性
- 文件的拥有者与用户组
- 文件大小
- 文件建立或状态改变的时间
- 最近一次的读取时间
- 最近的修改时间
- 定义文件特性的标识
- 文件真正内容的指向
每个inode(128B大小,ext4则为256B)拥有12个直接指向区块号码的对照,这12个记录可以直接获取区块号码,除此之外还有间接、双间接、三间接区域记录各一个用来适用于需要指向大量区块的场景,拥有了间接类的区域则可以将inode支持大小从12K增加到16GB。
- 数据库文件系统:文件根据其特征是可以被寻址辨识的;例如:WinFS
- 日志文件系统:记录文件系统的修改/事件(早期的文件系统,开销很大,类似于数据库日志);例如:journaling file system、ext3/4、NTFS(Windows)
- 网络/分布式文件系统:通过网络访问另一台机子上的文件;例如:NFS、SMB、AFS、GFS
- 特殊/虚拟文件系统:目的是以文件的形式提供接口来访问内核
二、虚拟文件系统
建立于不同的特定文件系统之上,将不同的文件系统进行抽象,使其对上层提供统一的文件系统接口。虚拟文件系统管理所有文件和文件系统关联的数据结构,可以高效查询例程,遍历文件系统。可以与特定的文件系统模块进行交互。构建一个虚拟文件系统至少需要以下模块:
不同模块的数据载入内存的时机:
- 卷控制模块:当文件系统挂载时进入内存
- 文件控制块:当文件被访问时进入内存
- 目录节点:在遍历一个文件路径时进入内存
三、数据缓存
在内存中开启一块空间用于存放在硬盘中的常用数据,原因还是因为内存和硬盘的读取速度相差好几个数量级。
通过减少对硬盘的读写次数来提高执行效率。
四、打开文件的数据结构
打开文件首先需要查找文件在硬盘中的什么地方,打开其实就是将存在硬盘中的文件块加载到内存块中。然后把相关的关键信息放到相应的打开文件表中,在打开文件表中建立对应的项,然后把这个项的索引index返回给应用程序。应用程序就只需要通过这个index来进行读写就好了。
打开文件表:
- 一个进程一个
- 是系统级别的
- 每个卷控制块也会保存一个列表
- 如果有文件被打开,那么这个文件就不能被卸载
打开文件时可能文件被加锁了,所以会有两种打开文件的方式,可以通过参数来告诉操作系统来进行区分:
- 强制:根据锁保持情况和需求拒绝访问,比如我打开了文件别人就不能再打开这个文件
- 劝告:进程可以查找锁的状态来决定怎么做
五、文件分配
对文件进行修改会造成文件的数据空间增加或者减少,那操作系统如何来对文件块进行分配大小呢?
所以我们需要一种中间的通用的数据块分配方式来处理这种情况:
最常见的分配方式:
- 基于连续的分配
- 基于链式的分配
- 基于索引的分配
判断分配方法的维度:高效(存储利用、有无外部碎片),访问速度
连续分配
这种方式文件和文件相邻放置,那么如果文件大小发生变化那么性能就会很低。
链式分配
这种方式几乎没有碎片,如果有也是在最后的文件块中对应的扇区没有用满导致的部分碎片。但是只能串行访问不能随机访问,且如果有一个链丢失,那么后续的所有文件就都丢了。
索引分配
只需要把索引块放入到内存块中就可以获得所有文件的位置信息。因为索引块的大小是固定的,所以处理大文件时是通过上述inode多级方式来进行处理的:
六、空闲空间列表
分配空间的时候我们需要知道哪里还有空闲的空间,所以需要跟踪在存储中的所有未分配的数据块。应对策略为:
用位图代表空闲数据块列表
这里有一个数据一致性的问题:位图是存在硬盘上的,如果一个文件在分配空间后,没有及时把信息回写到硬盘上的位图中,那么操作系统就仍然会认为这块空间是空闲状态的。
关键步骤为:分配空间的时候是优先把磁盘中的位图置为1后再进行空间的分配
不同的文件系统拥有不同实现方式,有些文件系统中可能不是通过位图的方式来进行空闲列表的管理的:
七、多磁盘管理-RAID
如果实现方式是通过RAID硬件控制器的方式,那么RAID硬件控制器来管理底层的硬盘类型,从而向操作系统提供了一种抽象的统一的硬盘存储能力,使得操作系统不需要关心底层存储的硬盘类型。
RAID0
对于RAID0这种存储方式来说,对于读取文件就有可能是并行读取,提高了文件的读取效率,但是其中的任何一个硬盘失效或者故障就会影响到所有的数据。
RAID1
磁盘起到了镜像的作用,如果一个磁盘坏了,那么还有另外一个磁盘可以读取,仍然可以正常工作。但是读取和写入都需要做两遍,可能是两倍的耗时。
RAID4
需要注意的是,只能应对1个盘出现异常的情况,可以通过奇偶校验磁盘中进行反推恢复数据。如果出现过多的异常情况,那么数据是无法进行恢复的。
这种情况下,如果对1-4的磁盘中任意一个磁盘写入数据,那么对应的校验磁盘需要进行数据的写入操作,这样会导致校验磁盘的操作过于频繁,会成为这种方式的性能瓶颈。
RAID5
把校验块均匀分布在不同的磁盘中,使得每个块的操作频率均衡了。但是这种方式还是只能使用于一个磁盘异常的情况,两个及以上的磁盘出现异常情况还是会寄。
按照bit来做奇偶校验,粒度太细,使用上来说不如按Byte来做奇偶校验来的方便。
RAID6
RAID6支持了更多容错性:
RAID01&RAID10
通过将RAID0和RAID1进行嵌套组合来实现的简单的可用的阵列方式:
前一位的是底层使用的阵列方式,后一位是构建上层的磁盘阵列方式。
八、磁盘调度
磁盘调度是在操作系统层面重新组织IO请求的方式来减少对磁盘的开销。
寻道时间:定位到期望的磁道所花费的时间
旋转延迟:从扇区开始处到到达目的处花费的时间
平均旋转延迟时间 = 磁盘旋转一周时间的一半
如果请求是随机的,那么读取的性能会很差。所以最好来说,IO请求是随机的。因此我们设计了对应的调度算法:
先来先处理
最短服务优先
这种方式会有个问题:会在一块小区域中打转,距离比较远的区域就不会去到。可能甚至会导致较远区域的“饥饿”。
scan方法
类似于电梯,先上到头再下到尾。当然,可以在此基础上进行优化【C-SCAN方法】:
类似于单向电梯,一个方向扫描后,立刻回到原起始点再次扫描。这种方式效率更高,对处理请求来说更加公平。当然还可以继续优化【C-LOOK方法】:
N-Step-SCAN
为了更加的实用,我们把这个N设定为2,就有了FSCAN算法: