操作系统笔记——储存系统、文件系统(王道408)
前言
属实是极限冲刺了,距离考研还有10天,我还有俩本书没学完(乐),昨天一下午一晚上学完进程,今天再接再厉,直接学完储存和文件系统
IO部分参见我的计组笔记,非常详细
储存系统
我不喜欢重复造轮子,这一章我会比较简略,尽量写高层次的思想,具体内容我的另一篇笔记里面记录的很详细,如果基础不是很好,可以对照看。
地址转换
关于物理地址:
- 逻辑地址:从源程序到汇编语言程序的这些阶段,都用逻辑地址
- 逻辑地址默认0为地址起点,不考虑和其他程序之间的相互作用
- 因此,后续几步,直到把程序装入内存的整个过程,肯定是要将逻辑地址变为物理地址的
- 后续的步骤为编译链接为目标模块,装入内存。如何变,就形成了3种不同的方法
- 绝对装入(很low,没OS才这么做):在编译链接阶段形成物理地址
- 静态重定位(可重定位装入):在装入的过程中,将指令内容修改,形成物理地址
- 动态重定位(动态运行时装入):指令内容一直是逻辑地址,使用
重定位寄存器
辅助地址偏移,在程序真正运行访存的时候才形成物理地址
(才发现我们OS老师上课用的那张图是从王道这里来的,我就说风格怎么不一样)
我们前面讨论的是如何形成物理地址,其实形成如何把多个.o文件的逻辑地址统一起来,也是一个需要注意的点,这个技术就是链接
- 静态链接:链接阶段一次性组合
- 动态链接
- 装入时动态链接:装入的时候,一次性组合
- 运行时动态链接:调用的时候,才针对性的装入对应的模块(.dll动态链接库)
联系前面的物理地址生成,很显然,绝对装入方法只能搭配静态链接使用,而动态链接只能和重定位方法结合使用
视角抬高,内存管理除了负责部分地址转换以外,还有很多功能。
内存保护的两种思路:
- 上下限寄存器:直接记录程序物理地址的上下线
- 重定位寄存器+界地址寄存器:界地址寄存器规定了逻辑地址的上限
内存扩展
覆盖
覆盖,就是让互斥的程序段公用一片内存,有两种可能:
- 固定区:互斥程序段只有一个,那么这片区域就是独占
- 一般来说,只有一个固定区(main函数)
- 覆盖区:有多个程序公用,每一个覆盖区都由当前覆盖段里占内存最大的模块决定。
- 比如B先用内存,C要用,就把B的部分直接覆盖就行,这也是这个名字的来源
这个方法的缺点就是需要人为指定覆盖结构(计算机不会分析),不方便。
交换
交换就是把暂时不用的程序换出,腾出空间给其他程序运行。
结合第二章,交换其实就对应着中级调度
因此换出的程序首选被挂起的程序,其次就是低优先级的,总之尽可能减小换出的副作用。
交换区要频繁读写,因此单独划出。
为了加快读写,采用连续分配的方式管理磁盘(IO更快)
储存器分配——连续分配
所谓连续分配,就是程序要放就是一整段全放进去,不可以拆开。
固定大小分区
说白了,单一连续分配
就是只有一个应用分区
因此没有外部碎片,只有内部碎片
下面的固定分区分配
,其实就是把这一个区,拆分成多个固定的区,只分配,不改变大小。
既然思想一致,只是分区数量的差异,那么碎片的逻辑也就一样了。
多个分区还要进行管理,需要一个固定分区表,这个表能修改的只有分配标记
如果最大的那个分区都满足不了当前程序,就上覆盖技术。
动态分区分配
动态分区就是固定分区加强版,除了可以修改标记以外,还可以修改区域的大小。
数据结构有两种:
- 分区表
- 沿用固定分区的思路
- 空闲分区链。这里注意一下其结构
- 这是一个双向链表,有首尾两侧链域
- 中间部分,可以存放分区的描述信息
分配和回收的过程中,要涉及到分区的拆分和回收合并:
- 拆分:动态分配算法
- 回收:会涉及到表项/节点的修改或者删除,要具体讨论
动态分区的思路,可以保证新分的区是满的,所以没有内部碎片
代价就是会产生外部碎片,内存中有一些地方因为太小是怎么也用不到的
解决方法也很直接,就是把分区挪一下,挤一挤,即紧凑技术。
很明显,程序在这个过程中浮动了,因此只能搭配 动态运行时装入(动态重定位)技术使用。
动态分区分配算法
- 首次适应
- 遍历空闲分区表/链,第一个能用的就直接用,同时进行修改
- 优点:快
- 最佳适应(最小适应)
- 一种粗暴的思路是遍历全部空闲分区链
- 另一种更好一点的思路是维持空闲分区链的有序性
- 在修改后重新排序,因为分配只会导致减小,所以我们只需要对着前半截进行一次插入排序即可
- 优点:保证大空间
- 缺点:产生小碎片,慢
- 最坏适应(最大适应)
- 与2反其道而行之
- 优点:减少小碎片
- 缺点:破坏大空间,慢
- 邻近适应
- 对1的修改
- 从上一次停下的位置开始查找,这样可以跳过前面因为分配而产生的小空间,快速用到后面的大空间
- 缺点是破坏大空间
- 优点是比首次适应还快
储存器分配——非连续分配
页式管理
基本思想
页式管理其实是分区的进化版,将分区粒度变得非常细,同时用页表建立索引,因此可以分散储存,大大提高空间利用率。
页表负责索引功能,将逻辑页号转为物理页号,这里区分一下名词:
- 逻辑页:对应程序,叫页,页面
- 物理页:对应内存,叫页框,页帧,物理块,物理页面,内存块
因为逻辑页是连续递增的,因此直接隐含在偏移地址里了,不在页表项里,而页表项的长度一定是要对齐的(k字节)
如何转换呢?
- 逻辑到物理:
- 说白了就是用索引表的页号查找对应页框号,然后拼接就可以
- 注意,页框号要
乘系数
才是页起始地址
- 物理到逻辑:
- 1的逆过程,在二进制下其实很简单,直接截取地址,后半段就是页内偏移,前半段就是页框号
- 本质在于,页框大小固定,因此两部分都是定长
地址变换硬件
学过汇编的话,这个过程非常熟悉。
因为页表位置可以浮动,我们干脆就用一个页表寄存器储存地址(PTR)
考虑到安全性检验,还要再存页表长度,这两个是分成两节存在一个寄存器里的
需要注意,既然是寄存器,那其实也是程序上下文,所以随着进程切换,肯定也会有装入和保存的过程
这个转换流程,用字母描述:
- P页号
- W页内偏移
- 需要注意的是越界验证,因为PTR存的是
页表长度
,所以是虚高1位的,因此只要P等于
M,就算越界 - 我们都是手算,实际上计算机直接拼接就行
前面说到页表项大小要对齐到k字节,实际上不仅仅如此。
3B情况下,会产生页框内碎片,那么我如果要访问这个碎片地址上的页表项呢?那只能+1偏移,这样做很麻烦,而且容易出bug
所以干脆进行二次对齐,对齐到能够被页框大小整除,所以一般是用4字节,做题的时候要考虑这两种对齐。
快表(TLB)
参考cache原理,TLB其实就是页表的cache,材料也都是SRAM,只不过TLB的等级还要在cache之上,是最紧贴CPU的
TLB是一种cache,更具体的说,应该是全相联方式储存的模式。
因此快表不能像页表那样,把页号隐藏在地址里,而是多加一个字段,且每次要遍历快表。
查找过程有两种:
- 先查TLB,再查页表
- 同时查询
进而衍生出不同耗时·的计算结果
TLB和cache的区别:
- cache会缓存一整个内存块
- TLB只cache页表项
- 从这个角度来看,其实TLB就是比cache更细,TLB是内存块的cache,而cache是整个内存的cache
多级页表
当一个页表存不下页表项,就需要用二级。
一般来说,只有二级页表,实际上可以多层
区分一下名称:
- 二级页表
- 外层页表,或顶层页表,页目录表
- 每一行:页目录项,页表描述符
- 一级页表
- 每一行:页表项,页描述符
转换过程无非就是前N次确定最终页号,最后1次进行访存,即N+1次
页表具体分几级,要根据地址长度来定,先抛去页内偏移,之后看看能拆几节页号地址。
段式管理
首先要明确,段式管理和页式管理是并列的,都是非连续的分配。
段式管理很像动态分区,但领域不一样:
- 动态分区是给内存进行分区,分区表是针对内存的,每个分区对应一个进程
- 段式管理是给进程空间进行分区,段表是针对一个进程的,每个分区对应程序的一个内存段
段表和页式管理类似,每个段表项都是等长的,段号都是隐含的(但是段不等长)
寻址过程也很类似,都是两次+越界检测
越界也是同理,这个段长是具体长度,虚高,所以只要满足W=C就代表越界了
从设计理念上来说,段页还是不同的,如下:
- 页式管理完全是为了系统服务的
- 是物理性的,纯粹按照地址切分的
- 用户不可见
- 段式管理更多的是为了用户服务
- 是逻辑性的,分模块的
- 用户可见
由设计理念来引申,共享与保护:
- 因为段是逻辑的,我们共享的时候也是按照模块共享的,逻辑上非常直观
- 比如我可以专门为可重入代码,或者共享数据建立一个段,这个段直接整体共享即可(不可重入代码不可共享)
- 而页并不具备这种逻辑的整体性,一页里面可能啥都有
- 同理,段也更有利于保护,整个模块一起保护很方便
- 页的内容很复杂错乱,所以共享管理很麻烦
定不定长也是一个区分点:
- 页式管理定长,因此给定一个逻辑地址,就可以直接通过除法运算锁定页号
- 页式管理一维,给地址直接上线性地址
- 段式管理不定长,给一个逻辑地址,只能截取段号,而不是除法运算
- 因此段式管理是二维的,给地址的时候要给两部分,段命(对应段号)和段内地址
段页式管理
终于到了段页式管理了,这才是版本真神。
段页式管理是页+段的综合,底层用页,高层用段。
另一种理解就是把二级页表爆改成段表了
段页式是两级的,所以访存次数是2+1=3
要进行两次越界判断,由此可得,其实二级页表也得进行两次越界判断。
注意,这个TLB是把段号和页号一起作为一个tag的,而不是弄两个TLB
虚拟储存器——基于交换的内存扩充技术
基本概念
虚拟内存的特征:
- 多次性:针对装入过程来说
- 对换性:内外交换
- 虚拟性:针对空间视图来说,看到的很大,但是是虚拟的
因为虚拟内存是把进程的内存空间拆分了,所以必须使用非连续性内存分配技术。
在此基础上,增添两个功能:
- 请求调入
- 置换
后面以页举例,更复杂的也是类似逻辑。
请求分页
请求分页逻辑可以参考cache来,其实是一个思想
但是具体还是不太一样:cache仅仅是缓存,管理能力很弱,而虚拟内存的管理能力很强,除了页框内容的缓存外,还专门有页表来管理页框,我们研究的其实是页表的管理。
请求页表结构:
- 首先,虚拟页表的管控对象是内存+外存
- 管控对象到底在内存还是外存?因此要用状态位+内存块号+外存地址进行区分和寻址
- 其次,考虑置换过程
- 置换哪一个?因此要有访问字段,辅助置换算法
- 换出的时候是否要写回?因此有修改位,需要考虑是否被修改(类似cache脏位)
如果目标页的有效位=0,说明在外存,发生缺页中断。
注意,缺页中断并不是外中断,而是广义的中断,实际上是异常。
之后研究一下请求分页管理中的细节,其实和基本分页的区别无非就是两点:
- 额外的检查
- 状态位
- 额外的修改
- 外存:置换前是否写回外存
- 页表:置换后页表的标志位要刷新
- TLB:快表的有效位恒等于1,因此换出的时候,要TLB删除(否则出错),换入的时候也可以根据局部性原理将这个页表项复制到TLB
不过不得不说,这个过程真的挺复杂的,后面做题继续细化吧,你且知道相关联的三个部分就可以:外存,页表项(以及对应的页框),TLB
页面置换算法
这几个方法在我另一篇笔记里已经有详细的描述了,这里进行细化。
注意,页面置换次数≠缺页次数,缺页是要更加广泛的,注意题目问的是哪个。
首先是OPT
具体做的时候,就是从发生缺页的位置开始,查看后面要调用的页,在这里面找我们当前物理块里装的页,排在最后一个的就是要置换出去的。
然后是FIFO
和LRU
,具体过程很简单:
- FIFO,有两种理解方式,效果相同,做题的时候自己看着办
- 新进来的页会把原来的页推下去,末位淘汰,直观
- 另一种理解方式是用一个指针指向即将要替换的位置,每次替换都让指针挪一位
- LRU,也是两种理解方式
- 类似于FIFO的下推+末位淘汰,但是如果命中,就把这个块提到最上面(刷新存在感)
- 另一种理解方式是逆向遍历访问序列,类似于OPT,最后一个出现的就是要淘汰的(只不过方向相反)
- 效果对比
- FIFO有Belady异常,而LRU就没有
- LRU效果是最接近OPT的,但是开销太大,需要硬件计时器(参考cache替换),要求的数量还不少。
再说时钟置换算法CLOCK(NRU)
思想很简单:
- 排成循环队列
- 命中,刷新访问位=1
- 注意,命中不需要转时钟,指针不变
- 不命中,按照时钟方式扫描,进行替换
- 1置0,访问位=1,相当于免死金牌
- 0置换,访问位=0,则受斩
- 置换后要将指针后移,防止这个新的页面在下一轮扫描的一开始就掉血
极限情况是进行1轮+1次扫描,也就是两轮扫描,这个方法兼顾了效率和效果。
改进NRU还考虑到了写回的IO损耗,尽可能避免IO(替换修改位=0的页面),同时还要维持原本NRU的原则,于是根据(访问位,修改位),可以分成4个优先级:
- 0,0,既没用,又没修改过,直接换
- 0,1,没用,但是被修改过,换的成本大点,但是造成的影响不大
- 1,0,用过,不得不换,只能找个换的成本小点的
- 1,1,成本最大,不得已的办法
具体如何去扫描呢?分4轮:
- 先在没访问过的里面扫两轮
- 第一轮扫(0,0),
- 第二轮扫(0,1),同时置零访问位
- 第二轮才会像NRU一样置零访问位,因为这两轮整体并做对访问位的检查,所以只置零一次
- 之后在访问过的里面扫两轮
- 注意,这两轮本来是(1,0),(1,1)的专长,但是因为第一组操作已经把访问位置0,所以走到这里的,肯定在第一组操作之前全部都是(1,x)的情况
- 第三轮扫(0,0)
- 第四轮扫(0,1),走到这一步一定会有一个页被置换出去
- 这一组操作其实是针对修改位而来的
改进NRU非常的完美:
- 两组操作继承自NRU,对访问位的置0也和NRU完全一致
- 而在在两组操作内部,又加入了对修改位的考察
虽然改进NRU最多进行4轮考察,但是这点内存中的消耗和降低IO损耗带来的收益相比,微不足道
页面分配策略、抖动、工作集
之后介绍三种分配+置换的搭配:
- 固定分配+局部置换
- 其实就是我们前面做题的时候用的思路
- 当前进程和外存进行交换
- 可变分配+全局置换
- 只要缺页,就增加物理块
- 当前进程
不直接
和外存进行交换,而是直接用空闲的,或者从其他进程抢一个(未锁定)的页框过来 - 之所以不直接,是因为抢夺其他进程页框,也会间接导致其他进程的交换,实际上还是要交换
- 这个方法反而还不如局部置换稳定
- 可变分配+局部置换
- 在1的前提下,如果系统察觉到1的缺页率比较高,就分配空闲块
- 当然,3方法也存在抢夺物理块的情况,但是频率比2低多了
- 请求调页
- 就是缺页中断,精确度很高,IO开销大
- 预调页策略
- 目标是减少IO开销
- 就是一种预测,因为其效果一般,所以只是在程序刚启动才这么干,这个时候调入不需要置换,就算翻车也无所谓。
再论从何处调页:
- 普通系统
- 对换区大,那就全在对换区操作就行,因此要先复制到对换区再调入
- 对换区小,因此要尽可能精细化,只把要修改的,可能反复IO的数据写回到对换区
- Unix系统
- 介于普通系统的两个策略之间,精细度居中
- 第一次是从文件区调入
- 之后换出的页面,不管是否被修改过,都放到对换区
内存映射文件
传统文件读写,要进行内存文件的多级索引,比较麻烦,如果你不是一次性读入,那么每读一个块都要多级索引一次。
内存映射文件直接把文件索引一次性读到内存里,分出一些页表项直接把文件地址记录进去
出于效率考虑,这里只是分配了页表项,并没有将文件读入,但是后续的读入已经很简单了,不需要多级索引,只需要IO就可以,效率高多了。
修改只需要在内存中,这进一步减少了IO损耗,最后进程关闭文件的时候,才将文件一次性写回,非常方便。
总之,内存映射,既可以减少索引损耗,又可以减少IO损耗
文件映射还有另一个好处,就是便于共享文件。
注意区分页表项和物理页框,实际上读入后的文件是放在物理页框里的,我们说的共享只是让不同进程的页表项指向同一个页框。
文件管理
文件系统复杂之处在于非常庞杂,需要一个良好的整体观,明确区分各种概念,接下来直接简要的把整个文件系统简单概括一下:
文件这一章整体都比较乱,因为文件系统确实是比较庞大,我在看我以前笔记的时候也同样有此感觉,因此我在这篇笔记里要尽可能让宏观逻辑顺畅。
因为文件系统庞大,所以我会自上而下的写(1,2,3),从逻辑逐渐过渡到物理,最后再拔高统筹(4,5):
- 先说目录结构,这是和用户最直接关联的
- 这一部分会着重讲解目录树的分支节点,即
目录文件
- 这一部分会着重讲解目录树的分支节点,即
- 这一节讲目录树叶节点上的,
普通文件
- 先讲文件的逻辑结构
- 再往下讲文件的物理结构,这一节尤为重要,决定了你访问文件的IO成本
- 这一节加深对物理层面的理解,文件之外的地方(空闲块)如何管理
到此为止,你已经可以从上到下,找到一个文件的所有磁盘块了,并且你也知道一个文件的空间从哪来,到哪去了,文件管理最基本的功能已经有了
- 视角开始拔高,补充一些文件的管理服务
- 最后用文件系统统筹,从最底层的磁盘分区到高层的VFS顺一遍
概述
区分:
- 标识符vs文件名
- 前者是OS内部用,后者给用户
- 外存地址vs文件目录
- 前者给OS内部用,后者给用户
文件内部,和文件之间,都需要组织。
目录结构
文件目录的概念
文件目录离我们很近,Windows文件夹就是一个文件目录的GUI
文件夹本身就是一个文件,现实中你装文件的袋子肯定也是有实体的,通过文件夹,就可以找到文件夹里面的文件,文件夹里面,可能有文件夹,也可能有文件,这叫嵌套,和你电脑里的逻辑是一样的。
虽然他们都是文件,但是性质不一样:
- 文件:
- 文件分为两部分,数据本身(文件体)以及FCB
- 一个文件对应一个FCB,记录其元数据,包括名称和物理地址等,实现了按名访问。对文件修改的同时也要修改FCB
- 文件夹也是文件,所以文件夹本身也有数据+FCB
- 文件夹(文件目录):
- 文件夹记录了这个文件夹下面的所有文件,包括下级目录和本层文件
- 文件夹是一种特殊文件,内含多个FCB,而不是普通数据
文件目录结构
文件是要给人用的,所以肯定会有一个名字,也就是说文件和文件之间是要有区分的,而内存里的东西就不需要,这就是文件系统和内存的本质区别。
既然有名字,在一个文件夹下就不能重名,因此文件目录结构是一个重要问题。
最开始叫一级目录
,说白了就是整个系统只有一个文件夹(MFD,Master File Directory),很显然,如果文件太多,则需要遍历文件目录,耗时很多
之后升级二级目录
,多出来的一级代表用户(UFD,User FD),此时有一点区分度,但是不太够
多级目录就是我们现在见到的目录结构,层层嵌套
比如上图的路径,要读3次目录,才能找到文件,之后你还得再读文件,所以消耗是读目录+读文件
当然,其灵活性的好处远远大于这点IO损耗
索引节点
回顾一下B树和B+树,这两者的区别在于B树节点本身就储存着数据,而B+树很精简,索引节点只是索引,叶节点只储存指针。
当时还说了文件系统用B+树,一个重大原因就是其可以在一个磁盘块有限的空间里尽可能塞入多的索引项。
越精简,储存一个文件目录需要的磁盘块就越少,从而减少在遍历索引表时的IO次数。
因此,直接把FCB中文件名字以外的元数据都剥离出去,构成一个索引节点(inode?)
文件结构
这一章开始讲一个文件如何组织,FCB和文件体两部分要统筹着看。
文件结构分类
流式文件和有结构文件的本质区别在于对齐。
- 无结构文件(流式文件)
- 基本单位很小,比如txt,每个字符是一个字节,字节流压根就不需要考虑对齐,直接切割到不同磁盘块里肯定是齐的
- 结构文件(记录式文件)
- 基本单位类似结构体,你不能把结构体从中间切开吧,所以要考虑对齐
- 定长记录vs可变长记录,可变长记录复杂,但是利用率高,定长记录直观简单
逻辑结构
类似于数据结构,逻辑结构主要讨论逻辑,不讨论具体实现
这里说的地址,通通指代逻辑地址
,与物理无关
顺序文件
顺序文件对应顺序表,顺序表又可以分为链表和数组。
- 链式储存,只能顺序,优点是可以分散储存
- 顺序储存
- 不定长,只能顺序,没有优点
- 定长,其实就相当于数组了,优点是可以随机存取
- 定长+乱序(串结构),普通数组
- 定长+有序(顺序结构),可以上特殊算法了,代价就是维护成本大
总结一下:
- 但凡用了可变长记录,就会破坏数组特性,顺序文件就只能顺序查找了。
- FCB记录顺序表的首地址和长度
- 注意,题目会模糊逻辑结构和物理结构,比如顺序文件(储存),其实暗含了物理结构,
默认的是顺序文件+顺序分配
索引文件
前面说了,在顺序文件的前提下,一旦引入可变长记录,就啥也不是了,但是索引表可以完美解决这个问题。
索引表本身是定长的,因此可以随机访问索引表,找到对应项目后,再根据指针找到对应的数据记录
虽然分两次查找速度会慢一点,但是总比从头遍历数据记录好。
数据库的索引原理就是这样,索引的思想就在于,给非随机存取的东西,附加上随机存取的性质
,代价就是多走一层,更占空间
总结:
- 索引文件可以实现间接的随机存取
- FCB指向一个索引文件,通过索引文件再找到文件所有的数据记录
索引顺序文件
索引文件有个缺点,索引和数据记录一一对应,也就是说其消耗是比例性的。
如果数据记录本身太小了,那么这个占比就很大,类似于用链表节点存一个字符,浪费率高达8/9
索引顺序文件双管齐下:
- 将数据记录分组,内部为顺序结构,每一组对应一个索引,原来是一个数据,很显然可以降低浪费率
- 组之间分散,分组后,数据记录组的数量变少,索引表项就少了
如果索引表还是大,那么还可拆分多级,这个时候你就会发现,已经有了B+树的感觉了,唯一细节的地方就在于,这棵树的叶节点指向的是一组数据记录,而不是一个数据记录。
所以查数据也很简单,先逐层查索引,之后再到组内进行顺序查找,把整体切分为组后,顺序存储查找时间太长的缺点就被消除了,而且组内存不定长记录也是没问题的。
总之就是一个完美。
物理结构
物理结构和逻辑结构无关,物理结构要解决的问题是,给定一片连续的逻辑地址,如何将其分配到物理空间
,并组织起来
磁盘块
如果磁盘块和内存块大小不一样呢?扇区的大小一般为512B,而我们之前学页式管理,一页经常是4KB的,这显然不对劲。
我们姑且不讨论这些区别,我们就默认磁盘块大小=内存块大小,在内外存交换的时候很方便,直接以块为单位,再联系一下cache和内存块的交换,也是这个单位,所以整个数据运行过程都非常流畅。
类似于内存,外存同样采用逻辑地址+物理地址的思想。
连续分配(顺序分配)
顺序文件+连续分配方式,说白了就是串结构,类似数组,因此可以随机(直接)+顺序访问
注意,磁盘和内存不一样,磁盘(特指HDD)本身是无法进行随机读写的,严格来说只能说叫直接读写
,即DAM,介于纯粹顺序和纯粹随机之间,磁盘本身的特性就决定了,再快也快不到哪去。
因此访问分散的磁盘块还是很耗时的,这种顺序分配读起来是最快的。
啥都好,就是不够灵活,扩展性很差,而且会产生大量磁盘碎片(终于知道以前windows上面说的磁盘碎片是啥了),而且紧凑也很费时间,这是致命缺点
链接分配
链式分配即链表。链表的优缺点,隐式分配都有。
最大的缺点就是只能顺序访问,是纯粹的顺序访问
读一个文件必须从头遍历,这个过程中要反复IO,每个块都要IO一次,消耗非常大
优点在于拓展是很简单的,只需要改一下链域,然后把FCB中的尾磁盘块指针改一下就行。
这里有个问题,为什么不像链表那样用NULL呢?可能是磁盘里面没有NULL这个概念,所以只能用块号限制
相比而言,下面的显式链接就是在内存中的链表,因此是通过NULL机制(-1)来实现的收尾,不需要尾块号
显式链接的区别在于,把磁盘中的连接结构提取到了内存中,以FAT(File Allocation Table)的形式保存,就是数组形式的链表,一个磁盘只需要一个超大的FAT统一管理即可。
虽然还是链表,性能有显著提升,本质区别在于,隐式链接的遍历需要在内存和磁盘之间反复横跳,而显式链接的链表遍历都可以在内存中操作,而内存速度快的很,就算按照链表的方式遍历,速度也比反复IO快多了
也就是说,遍历的方式其实并没有太大变化,只是n次内存操作比n次IO速度快多了,此外还要做一些区分:
- 显式链表可以实现随机访问。
- 在磁盘读写中,一切随机访问都不是严格的随机访问
- 这里的随机,指的是可以跳过前面的磁盘块
- 但是并不意味着可以跳过前面的显式链接,因为你还是个链表嘛
- 优缺点分析
- 优点:具有媲美顺序结构的性能(随机访问)
- 优点:具有媲美隐式链接的扩展性(链表本质)
- 缺点:内存驻留FAT消耗大,外存空间也会有占用,统称为储存空间
索引分配
显式链接比较优秀,是以前系统常用的方案,但是随着文件越来越多,FAT逐渐变大,负荷就太大了
同时CPU性能之类的也提升了,可以考虑通过若干次IO,多次索引的方式来把压力分摊到IO上了
索引分配的一个目标就是给FAT瘦身,曾经的FAT是所有文件一张,而现在是一个文件一张索引表,然后逐级构成B+树结构(但是并不等同于数据结构学的那个B+树)
类似于页表,属实是万法归宗了,我可以这么说,凡是大规模的数据组织,储存,使用B树或者B+都是很好的方案,内存,外存,不约而同地使用了这种方法。
在这里,磁盘块可以被分成两类:
- 索引块,存放索引表
- 数据块
单级索引非常简单,就是先读索引,然后通过索引把逻辑块号映射为物理块号,再读物理块即可。
如果一个索引块放不下一个文件的索引项,就要扩展,方案如下:
- 使用隐式链接方案链接索引块
- 缺点同隐式链接,如果索引块多了,IO次数太多
- 多层索引
- 经典的B+树思想(但是并不等同数据结构那个B+树)
- 其中涉及到逻辑地址到物理地址的转换,需要根据情况进行除和模的计算,尤其是多级情况下,计算还是有一点复杂的
- 具体来说,除以一个子节点可以容纳的最大索引数( m k m^{k} mk,m为下图的256,k为剩余索引层数),比如上图中两层索引,k=1,因此除以256,如果是三层索引,那么计算第一层的索引下标就得除以 25 6 2 256^2 2562,然后求模,用这个模再除以 25 6 1 256^1 2561,再求模,总之就是除一下,求个模,循环往复,直到最后一层
- 混合索引
- 多层索引比较死板,无论多小的文件,都需要固定的索引层数,浪费,现代操作系统中采用混合索引的方式,根据文件的大小灵活扩充索引的级数
- 以下图为例,一个文件的
顶级索引节点
比较特殊,是顶级索引表,项目并不多。如果用到的块比较少,那么就只用直接地址,如果要多一些,就要用一级间接索引,从间接索引开始的索引节点,这种索引节点就是填满的,如果再放不下,就启用二级间接。 - 在Linux里面的顶级索引表是12+1+1+1个索引项,最高开三级索引
- 注意,因为FCB是改进版本,所以根据指针读顶级索引表也算一次读内存,所以不要被“二级索引”误导,严格来看其实要高一级的,IO次数计算的时候要注意。反过来,如果题目告诉你顶级索引表已经读入了,那么就不用修正了
逻辑结构vs物理结构(难点)
关键词区分:
- xx
文件
=xx储存
=逻辑结构 - xx
分配
=物理结构
先区分一下逻辑地址和物理地址,基础要牢固:
- 逻辑地址,每个文件默认是从0开始的
- 物理地址,就是真实的磁盘块号
从逻辑地址到物理地址的转换,就是文件的物理结构来负责的,与逻辑结构毫无关系。
比如是用xx文件+索引分配方式,那么无论你是什么逻辑结构,你都要通过索引表进行映射,而如果是链式分配,那么就需要给定物理首地址+顺序遍历的方式去映射,说白了这些分配方式都是逻辑到物理的映射方式
进一步辨析,文件/储存≠分配
直观来看,似乎就应该是连续文件配连续分配,链式文件配链式分配,但是其实不是这样的。
无论你逻辑上文件是什么样的,总之都是一片连续的逻辑地址
,思考一下C语言创建文件的过程,你最终都是要write到这个文件里的,无论你用什么逻辑组织,最后你都要调用write函数逻辑上连续地写入文件,这个write函数就决定了逻辑地址一定是连续的。
因为逻辑空间必然连续,所以OS会一视同仁,自行决定分配方式,比如下图,逻辑结构是链式存储,但是物理分配采用的是连续分配。
总之,逻辑结构和物理结构一点关系都没,只不过是思想互相借鉴,并不能对应,不然为啥要分开讲呢?
从头查找一个文件的过程
学到这里,从头盘一下吧,如何从文件系统里查一个文件的一项数据记录呢?
- 找到文件的目录:逐级查找目录
- 这个过程本身就是用FCB找目录文件,再从中挑出FCB继续找目录文件的过程。
- 目录通常都比较小,所以没有复杂的物理结构,获取目录每次IO一次即可
- 找到目标文件的FCB:遍历最下级文件目录
- 注意,目录里面的改进版FCB为索引结构,只有名字和索引节点指针两项
- 找到目标文件的索引节点,解析节点
- 从这里开始,就要区分文件的物理结构了
- 假设文件本身采用k级索引分配的逻辑结构储存文件
- 在文件内查找数据记录
- 从索引节点层层索引,索引k次
- 最后读取1次,成功获得目标磁盘块
完全跑下来,IO成本=查找目录的成本+读文件的成本
查找目录的成本就是目录级数(斜杠个数)
读文件的成本要根据物理结构而定
文件储存空间管理(空闲部分)
前面说的目录结构,文件结构,都是对非空闲块进行管理,还有空闲的部分,在这一章集中讲解
分区可以理解为一个特殊的文件夹
- 目录区,存放FCB以及索引节点,以及各种分区元数据
- 文件区,放数据体
FCB,索引,以及文件体,这些我们都在前面讲过了,属于非空闲部分的管理
接下来讲解一下,如何管理空闲部分(其实类似于内存管理,思路都是类似的)
空闲表
空闲表,参考内存分区里面的空闲分区表,同样的记录了起始地址+长度
分配和回收都是一模一样的
具体到文件的物理结构来说,这种空闲管理方式比较适合连续分配方式。
空闲链表
两种方式,空闲盘区链,类似于分区里面的空闲区链表,重点都在“区”,而空闲盘块链粒度更细,区分:
- 空闲盘区链同内存中空闲区链表
- 分配使用适应算法
- 回收要检测被回收区两侧是否有空闲区相邻
- 空闲盘块链,这是一个全新的概念
- OS视角下,相当于把所有空闲的磁盘块变成了一个
队列
- 分配和回收,分别是出队和入队的过程
- OS视角下,相当于把所有空闲的磁盘块变成了一个
两种方式都适用于离散或者连续分配,只是效率不同:
连续分配,或者一次分多个,空闲区块链快,而如果是离散少量,则空闲盘块链快
位示图
位示图很经典,这里额外讲了字
注意,这里的字针对的是位示图的一行,如果一行有kbit,那么一个字对应k个磁盘块。
还需要注意的是从0开始还是从1开始,非典型情况可以参考矩阵压缩那一块,思路一模一样。
之所以要引入字,是因为这样便于定位。
分配的时候,要将字号位号转化为盘块号,同时将0置1,回收的时候是逆向过程
成组链表
略,0.1%的几率考,我就当他0%,主打一个效率
文件管理服务
文件基本操作
create,创建文件两件事
- 占用空闲磁盘块,放文件体
- 添加FCB到目录
delete是逆向过程
open,两件事:
- 找到,并检查权限
- 打开,此时FCB作为进程资源复制到进程的“打开文件表里”
- 还会把文件
索引号
(文件描述符)返回给用户(程序),其实就是一个key,用于快速锁定内存中的这个FCB
- 还会把文件
考虑到文件的读写共享,还需要在系统中维持一张系统打开文件表,并且计数(类似于硬链接的思想)
删除是逆过程,删除的是进程本身的打开文件表项,计数-1,同样类似于硬链接,如果计数为0,则在系统打开文件表里删除对应项
读文件:
- 从哪里读:打开文件表中的读写指针
- 读多少
- 读到哪里:内存中的位置
写文件类似
文件共享
树结构的特点在于分支之间隔离。
既然如此,如何通过一个目录,访问另一个目录呢?分为软硬两种链接方法
上图为硬链接
,文件和索引节点一一对应,硬链接直接让当前目录里的FCB指向对应索引节点即可,或者说在两个目录里,存在一样的FCB。
索引节点会维护一个计数器,只有在全部硬链接都失效的时候(count=0),才删除文件体和索引节点。
还有一种软链接
(符号链接)方式,是Windows的方式,即快捷方式.ink文件,link本身就是一个文件,当OS判断其为链接文件时,会读取里面记录的目标文件的路径,用这个路径找到对应文件目录下的文件,而不是直接用FCB指向。
软链接的本身只是快捷方式,指向的文件删了就删了,快捷方式本身不会受到影响,只是没用了罢了。
文件保护
口令
就是密码,密码本身在文件之中(FCB或者索引节点),因此可以用技术手段去逆向分析破解。
加密
本质上也是把密码放在文件之中,但是加密是把密码“分散”在整个文件之中了,加密后的文件本身就是密码,我们用密码去和加密后的文件比对(解密),就可以得到源文件。比较费事
ACL最基本的形式如上,我们Linux里面用的chmod 777之类的指令,变成二进制其实就是111 111 111
如果每个用户对每个文件都有这么一行,实际上在列方向上是有冗余的,在MySQL里面,采用角色的思路来进行权限控制,下面这个思路也是一样,文件给每个角色(分组)整体分配权限,然后某个用户访问的时候,直接检查其归属于哪些分组即可。
这样,无论用户有多少,一个文件ACL的长度也只有分组的个数。
下图中,组就是用户组
一个文件,可以针对每一个组配置对应的权限。
文件系统
文件系统的布局
本节,从最底层一个空磁盘开始,逐步构建文件系统,与内存接轨:
- 物理格式化(低级格式化)
- 磁盘刚生产出来,空白一片,厂商会首先划分扇区,进行编号(固化在磁盘中),这是最低级的格式化,和用户的那个格式化不同
- 有坏扇区,物理上是无法挪动的,可以将其信息固化,编号的时候跳过,顺延
- 高级格式化后会形成MBR(统筹所有分区)+若干磁盘分区,下面列出分区内部结构:
- 如果安装有系统,则会有一个引导块,用于拉起系统
- 超级块和空闲空间管理区功能类似,互相补充
- i节点区,存放inode即索引节点,这片空间可以看做数组(注意,如果是混合索引分配,那么i节点区不储存非顶层索引,因为长度不对)
- 根目录
- 规划好物理结构,逻辑结构后,文件系统基本构建完毕,接下来是与内存的接轨
- 接轨之前是文件的访问,要先访问目录,然后根据物理结构找到具体的磁盘块
- 打开文件后,进程以及系统会在内存中保持对文件的引用
- 目录缓存和系统打开文件表归属于系统,自然在内核空间
- 进程打开文件表包含在PCB中,而PCB又是在内核空间里
- 打开流程如下,需要注意下面三张表
虚拟文件系统
如果没有VFS,不同文件系统的规范完全不同,OS对于外接文件系统的兼容性就非常差。
VFS的宏观功能:
- 向上提供统一的文件标准接口,比如POSIX接口
- 向下规定协议,对接入此VFS的文件系统一点硬性规定
具体到细节上,VFS通过若干思路实现接口的统一化:
- 统一不同文件系统的目录项
- 统一用vnode保存,只存在于内存中
- 相对来说,其他文件系统的目录项会同时存在于外存和内存中
函数功能指针
的意义,在于区分不同v节点的文件系统类型,毕竟上层OS下发的函数调用,最终还是要落到具体的系统中的,通过函数功能指针,就可以找到这个文件对应的系统类型的函数接口,进行专属于这个文件系统的操作
文件系统挂载
所谓的挂载,就是把异种文件系统接入宿主机的VFS中,日常中插U盘其实就是挂载,要做三件事:
- 注册:
- VFS首先要知道这是个什么系统,要记入挂载表里
- VFS还要知道这个系统如何操作,即把其提供的函数接口保存下来
- 接入
- 把文件夹挂载到挂载点上
一般来说,Windows挂载点在磁盘根目录
用过虚拟机的应该都有过挂载的经历,比如说你是一个Linux的虚拟机,但是你想要将主系统(Windows)的文件夹共享到这个Linux中,需要执行一个挂载命令才可以在hgfs文件夹里面看到共享文件夹,这其实也是一个挂载的过程。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!