1 基本概念
1.1 文件系统和文件
1.1.1 简述
文件系统: 一种用于持久性存储的系统抽象
- 在存储器上: 组织, 控制, 导航, 访问和检索数据
- 大多数计算机系统包含文件系统
- 个人电脑, 服务器, 笔记本电脑
- ipod, Tivo/机顶盒, 手机/掌上电脑
- google可能是由一个文件系统构成的
文件: 文件系统中一个单元的相关数据在操作系统中的抽象
1.1.2 文件系统的功能
- 分配文件磁盘空间
- 管理文件块(哪一块属于那一个文件)
- 管理空闲空间(哪一块是空闲的)
- 分配算法(策略)
- 管理文件集合
- 定位文件及其内容
- 命名: 通过名字找到文件的接口
- 最常见: 分层文件系统
- 文件系统类型(组织文件的不同方式)
- 提供的便利及特征
- 保护: 分层来保护数据安全
- 可靠性/持久性: 保持文件的持久即便发生崩溃, 媒体错误, 攻击等
1.1.3 文件
文件属性:
名称, 类型, 位置, 大小, 保护, 创建者, 创建时间, 最近修改时间...
文件头:
- 在存储元数据中保存了每个文件的信息
- 保存文件的属性
- 跟踪哪一块存储块属于逻辑上文件结构的哪个偏移
1.2 文件描述符
1.2.1 对于文件操作的一些背景
文件使用方式:
使用程序必须在使用前先"打开"文件
f = open(name, flag);...
close(f);
内核跟踪每个进程打开的文件:
- 操作系统为每个进程维护一个打开文件表
- 一个打开文件描述符是这个表中的索引
操作系统需要一些元数据来管理打开文件:
- 文件指针: 指向最近的一次读写位置, 每个打开了这个文件的进程都这个指针
- 文件打开计数: 记录文件打开的次数 --- 当最后一个进程关闭了文件时, 允许将其从打开文件表中移除
- 文件磁盘位置: 缓存数据访问信息
- 访问权限: 每个进程访问模式信息
用户视图:
持久的数据结构
系统访问接口:
- 字节的集合(UNIX)
- 系统不会关心你想存储在磁盘上的任何的数据结构
操作系统内部视角:
- 块的集合(块是逻辑转换单元, 而扇区是物理转换单元)
- 块大小 <==> 扇区大小: 在UNIX中, 块的大小是4KB
举例:
当一个用户的需求为, 访问2 - 12字节空间会发生以下事情:
- 文件系统获取字节所在的块
- 返回块的对应部分
如果是要写2 - 12字节的话:
- 获取块
- 修改块内对应部分
- 写回块
在文件系统中的所有操作都是在整个块空间上进行的: 即使每次只访问一个字节, 也会缓存目标数据所属的整个块, 也就是4096字节.
用户的文件访问方式(在系统层面需要知道用户的访问模式):
- 顺序访问: 按字节依次读取(几乎所有的访问都是这种方式)
- 随机访问: 从中间读写
- 不常用, 但是仍然重要. 例如, 虚拟内存支持文件: 内存也存储在文件中
- 更加快速 - 不希望获取文件中间的内容的时候也必须先获取块内所有字节
- 基于内容访问: 通过特征
- 许多系统不提供此种访问方式. 相反, 数据库是建立在索引内容的磁盘访问上(需要高效的随机访问)
1.2.2 文件
文件内部结构:
- 无结构
- 单词, 比特的队列
- 简单记录结构
- 列
- 固定长度
- 可变长度
- 复杂结构
- 格式化的文档(如, MS Word, PDF)
- 可执行的文件
这些结构, 对于相应的应用程序可以理解, 但是站在操作系统的角度上, 只会知道这个文件的字节流, 不会知道这个文件具体的内容.
访问权限:
多用户系统中的文件共享是很必要的, 所以需要有访问控制:
- 谁能够获得哪些文件的哪些访问权限
- 访问模式: 读, 写, 执行, 删除, 列举等
文件访问控制列表(ACL)可以实现以上需求
- <文件实体, 权限>
Unix模式
- <用户 | 组 | 所有人, 读 | 写 | 可执行>
- 用户ID识别用户, 表明每个用户所允许的权限及保护模式
- 组ID允许用户组成组, 并制定了组访问权限
多用户同时访问时:
- 指定多用户/客户如何同时访问共享文件
- 和过程同步算法相似
- 因磁盘I/O和网络延迟而设计简单
- Unix文件系统(UFS)语义
- 对打开文件的写入内容立即对其他打开同一文件的其他用户可见
- 共享文件指针允许多用户同时读取和写入文件
- 会话语义
- 写入内容只有当文件关闭时可见
- 锁
- 一些操作系统和文件系统提供该功能
1.3 目录
文件以目录的方式组织起来, 目录是一类特殊的文件, 每个目录都包含了一张表<name, pointer to file header>.
目录和文件是成树形结构(早期的文件系统是扁平的, 只有一层目录)
层次命名空间
目录的典型操作:
- 搜索文件
- 创建文件
- 删除文件
- 枚举目录
- 重命名文件
- 在文件系统中遍历一个路径
操作系统应该只允许内核模式修改目录:
- 确保映射的完整性
- 应用程序能够读目录(如ls)
文件名列表:
- 用文件名的线性列表实现, 包含了指向数据块的指针
- 编程简单
- 执行耗时
- Hash表 - hash数据结构的线性表
- 减少目录搜索时间
- 碰撞 - 两个文件名的hash值相同
- 固定大小
路径遍历:
- 名字解析: 逻辑名字转换成物理资源(如文件)的过程
- 在文件系统中: 到实际文件的文件名(路径)
- 遍历文件目录直到找到目标文件
- 举例: 解析"/bin/ls"
- 读取root的文件头(在磁盘固定位置)
- 读取root的数据块: 搜索"bin"项
- 读取bin的大小
- 读取bin的数据块: 搜索"ls"项
- 读取ls的文件头
- 当前工作目录(可以提高效率, 因为已经对当前目录作了缓存)
- 每个进程都会指向一个文件目录用于解析文件名
- 允许用户指定相对路径来代替绝对路径
文件系统的挂载:
- 一个文件系统需要先挂载才能被访问
- 一个未被挂载的文件系统被挂载在挂载点上
1.4 文件别名
有可能会两个或多个文件名关联同一个文件:
- 硬链接: 多个文件项指向一个文件, 这个多个文件的文件内容都是同一文件
- 软链接: 以"快捷方式"指向其他文件, 这个文件的文件内容是另一个文件的文件路径
- 通过存储真实文件的逻辑名称来实现
如果删除一个有别名的文件会如何:
- 如果用的软链接方式, 这个别名将成为一个"悬空指针"
- 如果用的硬链接方式, 只是引用计数减少了1
Backpointers方案:
- 每个文件有一个包含多个backpointers的列表, 所以删除所有的backpointers
- Backpointers使用菊花链管理
添加一个间接层: 目录项数据结构
- 链接 - 已存在文件的另外一个名字(指针)
- 链接处理 - 跟随指针来定位文件
这个别名机制会存在潜在的问题 -- 循环, 也就是有可能树状结构变成环状, 需要保证没有环:
- 只允许到文件的链接, 不允许在子目录的链接
- 每增加一个新的链接都要用循环检测算法确定是否合理
更多实践:
- 限制路径可遍历文件目录的数量
1.5 文件系统种类
- 磁盘文件系统
- 文件存储在数据存储设备上, 如磁盘
- 例如: FAT, NTFS, ext2/3, ISO9660, 等
- 数据库文件系统
- 文件根据其特征是可被寻址(辨识)的
- 例如: WinFS
- 日志文件系统
- 记录文件系统的修改/事件
- 例如: Journaling file system
- 网络/分布式文件系统
例如: NFS, SMB, AFS, GFS
分布式文件系统:
分布式文件系统一大特点就是文件可以通过网络被共享:
- 文件位于远程服务器
- 客户端远程挂载服务器文件系统
- 标准系统文件访问被转换成远程访问
- 标准文件共享协议: NFS for Unix, CIFS for Windows
分布式文件系统的问题:
- 客户端和客户端上的用户辨别起来很复杂
- 例如, NFS是不安全的
- 一致性问题
- 特殊/虚拟文件系统
2 虚拟文件系统
文件系统是分层结构的:
- 上层: 虚拟(逻辑)文件系统
- 底层: 特定文件系统模块
设计虚拟文件系统的目的:
- 对所有不同文件系统的抽象
虚拟文件系统功能:
- 提供相同的文件和文件系统接口
- 管理所有文件和文件系统关联的数据结构
- 高效查询例程, 遍历文件系统
- 与特定文件系统模块的交互
抛开差异性, 一个基本的文件系统应该包含的数据结构有:
- 卷控制块(Unix: "superblock")
- 每个文件系统一个
- 文件系统详细信息
- 块, 块大小, 空余块, 计数/指针等
- 文件控制块(Unix: "vnode" or "inode")
- 每个文件一个
- 文件详细信息
- 许可, 拥有者, 大小, 数据库位置等
- 目录节点(Linux: "dentry")
- 每个目录项一个(目录和文件)
- 将目录项数据结构及树型布局编码或树型数据结构
- 指向文件控制块, 父节点, 项目列表等
3 数据块缓存
数据缓存有以下几种情况:
- 数据块按需读入内存
- 提供read()操作
- 预读: 预选读取后面的数据块
- 数据块使用后被缓存
- 假设数据将会再次被使用
- 写操作可能被缓存和延迟写入
- 两种数据块缓存方式
- 普通缓冲区缓存
- 页缓存: 统一缓存数据块和内存页
操作系统希望将硬盘的缓存同内存的页式管理结合在一起, 实现一个基于分页的缓存机制, 使得硬盘的数据可以更好的被上层应用所使用:
- 分页要求
- 当需要一个页时才将其载入内存
- 支持存储
- 一个页(在虚拟地址空间中)可以映射到一个本地文件中(在二级存储中)
文件数据块的页缓存:
- 在虚拟内存中文件数据块被映射成页
- 文件的读/写操作被转换成对内存的访问
- 可能导致缺页和/或设置成脏页
- 问题: 页置换 - 从进程或文件页缓存中?
4 打开文件的数据结构
打开文件就是把硬盘里面相应文件的文件控制块的内容读到内存中来, 然后把相关信息放入打开文件列表中, 其中专门设置了一个index, 把index作为fd返回给应用程序. 接下来应用程序都是基于这个fd对文件进行操作, 所以有以下.
打开文件描述:
- 每个被打开的文件一个
- 文件状态信息
- 目录项, 当前文件指针, 文件操作设置等
打开文件表:
- 一个进程一个
- 一个系统级的
- 每个卷控制块也会保存一个列表
- 所以如果有文件被打开将不能被卸载
因为文件是存在硬盘中, 所有对该文件有操作权限的进程都可以进行该文件的相应操作, 所以可能会出现多进程同时访问一个文件的情况. 所以会存在用锁来进行同步互斥操作:
- 一些操作系统和文件系统提供该功能
- 调节对文件的访问
- 强制和劝告
- 强制: 根据锁保持情况和需求拒绝访问
- 劝告: 进程可以查找锁的状态来决定怎么做
5 文件分配
大多数文件都很小, 所以需要对小文见提供强力的支持, 所以块的空间不能太大. 然而部分文件非常大, 所以也必须支持大文件(64 - bit偏移), 大文件访问需要相当高效.
5.1 为文件分配数据块
分配方式:
- 连续分配
- 链式分配
- 索引分配
评价分配方式指标:
- 高效: 如存储利用(外部碎片)
- 表现: 如访问速度
5.2 分配数据块方式
5.2.1 连续分配
文件头(文件控制块)指定起始块和长度.
分配策略(与连续内存分配类似):
- 最先匹配
- 最佳匹配
- ...
优势:
- 文件读取表现好
- 高效的顺序和随机访问
劣势:
- 碎片
- 文件增长问题
5.2.2 链式分配
文件以数据块链表方式存储, 文件头包含了到第一块和最后一块的指针
优势:
- 创建, 增大, 缩小很容易
- 没有碎片
劣势:
- 不可能进行真正的随机访问
- 可靠性(破坏了一个链)
5.2.3 索引分配
为每个文件创建一个名为索引数据块的非数据数据块, 到文件数据块的指针列表, 文件头包含了索引数据块.
优势:
- 创建, 增大, 缩小很容易
- 没有碎片
- 支持直接访问
劣势:
- 当文件很小时, 存储索引的开销
- 如何处理大文件?
针对大文件的索引块分配:
- 链式索引块
- 多级索引块
大文件文件头包含13个指针:
- 10个指针指向数据块
- 第11个指针指向间接数据块
- 第12个指针指向二重间接数据块
- 第13个指针指向三重间接数据块
这样做的影响:
- 提高了文件大小限制阈值
- 动态分配数据块, 文件扩展很容易
- 小文件开销小
- 只为大文件分配间接数据块, 大文件在访问间接数据块是需要大量的查询
6 空闲空间列表
空闲空间列表用来跟踪在存储中的所有未分配的数据块, 空闲空间列表存储在哪里? 空闲空间列表的最佳数据结构是什么样的?
6.1 空闲空间列表的形式
位图:
可以用位图代表空闲数据块列表(例如1111000010010101011101), 如果位图的第i位为0, 表明数据块i是空闲的, 反之则已分配.
位图的方式使用简单但是可能会是一个big vector:
- 160GB disk -> 40M blocks -> 5MB worth of bits
- 然而, 如果空闲空间在磁盘中均匀分布, 那么在找到"0"之前需要扫描: 表示磁盘上数据块的总数, 表示空闲块的数目
用这种方式, 需要保证一致性, 也就是说, 要使用位图形式的空闲空间列表的话, 需要先把其读入内存, 但是如果位图发生变化, 需要保证内存中和硬盘中位图的一致性, 即需要保护:
- 指向空闲列表的指针
- 位图
- 必须保存在磁盘上
- 在内存和磁盘拷贝可能有所不同
- 不允许在内存中的状态为而在磁盘中
- 解决
- 在磁盘上设置
- 分配
- 在内存中设置
6.2 其他方式
- 链式列表
- 分组列表
- ...
7 多磁盘管理 - RAID
分区: 硬件磁盘的一种适合操作系统指定格式的划分
卷: 一个拥有一个文件系统实例的可访问的存储空间
- 通常常驻在磁盘的单个分区上
单磁盘, 读写速度较慢, 使用多个并行磁盘来增加:
- 吞吐量(通过并行)
- 可靠性和可用性(通过冗余)
RAID - 冗余磁盘阵列
- 各种磁盘管理技术
- RAID levels: 不同RAID分类(如RAID - 0, RAID - 1, RAID - 5)
实现:
- 在操作系统内核: 存储/卷管理
- RAID硬件控制器(I/O)
RAID优势:
- 数据块分成多个子块, 存储在独立的磁盘中(和内存交叉类似)
- 通过更大的有效块大小来提供更大的磁盘带宽
- 可靠性成倍增长
- 读取性能线性增加(向两个磁盘写入, 从任何一个读取)
8 磁盘调度
磁盘调度是在OS层面通过重新组织I/O请求顺序来有效减少磁盘访问开销.
磁盘访问的开销是很大的, 和内存访问的速度差了好几个量级:
- 读取或写入时, 磁头必须被定位在期望的磁道, 并从所期望的扇区开始.
- 寻道时间: 定位到期望的磁道所花费的时间
- 旋转延迟: 从扇区的开始处到到达目的处所花费的时间
- 平均旋转延迟时间 = 磁盘旋转一周时间的一半
计算磁盘的I/O传输时间有以下几个参数:
- = 访问时间
- = 寻道时间
- = 旋转延迟
- = 传输时间
- = 传输的比特数
- = 磁道上的比特数
- = 磁盘转数
有:
其中, 寻道时间是性能上区别的原因, 对单个磁盘, 会有一个I/O请求数目, 如果请求是随机的, 那么表现会很差.
具体的调度策略有:
- FIFO, 先进先出
- 按顺序处理请求
- 公平对待所有进程
- 在有很多进程的情况下, 接近随机调度的性能
- 最短服务优先
- 选择从磁臂当前位置需要移动最少的I/O请求
- 总是选择最短寻道时间
- 容易使得进程饥饿
- SCAN方法
- 磁臂在一个方向上移动, 满足所有未完成的请求, 知道磁臂到达该方向上最后的磁道
- 调换方向
- 有时被成为elevator algorithm
优化1(C-SCAN):
- 限制了仅在一个方向上扫描
- 当最后一个磁道也被访问过了后, 磁臂返回到磁盘的另外一端再次进行扫描
优化2(C-LOOK):
- C-SCAN的改进版本
- 磁臂先到达该方向上最后一个请求处, 然后立即反转
- N-Step-SCAN
- 在SSTF, SCAN及CSCAN几种调度算法中, 都可能出现磁臂停留在某处不动的情况, 例如进程反复请求对某一磁道的I/O操作. 我们把这一现象成为"磁臂粘着".
- N-Step-SCAN算法是将磁盘请求队列分成若干个长度为N的子队列, 磁盘调度将按照FCFS算法依次处理这些子队列. 而每处理一个队列时又是按SCAN算法, 对一个队列处理完后, 再处理其他队列
- 当正在处理某子队列时, 如果又出现新的磁盘I/O请求, 便将新请求进程放入其他队列, 这样就可避免出现粘着现象
FSCAN:
- FSCAN算法实质上是N步SCAN算法的简化, 即FSCAN只将磁盘请求队列分成两个子队列.
- 一个是由当前所有请求磁盘I/O的进程形成的队列. 由磁盘调度按SCAN算法进行处理. 在处理某队列期间, 将新出现的所有请求磁盘I/O的进程, 放入另一个等待处理的队列. 这样, 所有的新请求都将被推迟到下一次扫描时处理.