操作系统概念逻辑线(下)

书接上回,上次我们讲逻辑线讲到了进程和线程的管理调度。

上文说到,只有一个程序被装入和内存才能够执行,并被称为进程,可问题来了,内存就这么大,我们如何讲一个程序装入内存呢?

这里,就是上文我们所讲到的长期调度程序的作用了,他会选择将哪一个进程放入到内存。可如何放入呢?

我们首先想到,我们可以将进程的全部代码与数据放入内存中,这样,在运行的时候其本身的速度会快很多,这就是我们的第八章:内存管理。

但是,我们如何安排进程的存放呢?最简单的就是将整个进程连续的放入一段大小足够容纳进程的内存中,即所谓的多分区分配方法。

但是这个时候有一个问题,我们的代码写的是,是按照进程被放在了地址为0的位置的,即代码中所有的地址都是相对于开口的相对地址,或者逻辑地址,而我们不可能将进程只放在内存的开头,所以,这就有了逻辑地址与物理地址的冲突,即需要重定位。

通过重定位,我们就区分了逻辑地址空间和物理地址空间,逻辑地址空间我们的进程,或者用户所看到的,是一段连续的内存;而物理地址空间可以是连续的,也可以是分开的,他所代表着进程在内存中实际的存储位置,而逻辑地址与物理地址一般是不同的,这就需要我们的重定位寄存器MMU来建立起两者之间的联系。

随着进程进入系统,将会被加入到输入队列,等待被分配到内存中进行执行,os根据所有进程的内存需要与当前可用来决定哪些进程可以进入内存(长期调度)。当进程分配到内存空间时,装入内存,并开始竞争CPU(短期调度),当进程终止时,内存释放

这个时候,我们如何找到可以放入进程的“孔”呢?这就有了三种适应:
①首次适应:分配第一个足够大的孔,找到即停
②最佳适应:分配最小的足够大的空,全部找完
③最差适应:分配最大的空,全部找完

但这个方法问题很大,就是在我们连续运行的时候,进程进进出出,又大小不一,很容易会将内存分割成千疮百孔的样子,这就是外部碎片,所以并不是一个很好的方法。

那么,怎么解决外部碎片的问题呢?我们想到,能否将进程人为地分成好几段?每一段的大小一定,这样就可以实现非连续的内存分配,可以大大减少外部碎片呢?

这样,我们自然而然的就想到了一种叫**“分页”**的内存管理方法,反正进程的地址是可以被重定位的,所以,哪怕我把进程分成好几段,然后每一段放在不同的位置,我在找的时候,只需要修改我的重定位寄存器的基址,就一样可以找到,而在用户的严重,我们所分配的内存空间仍旧是连续的。

所以,分页,就是把物理内存分成固定大小的块,称为帧;将逻辑内存分为同样大小的块,称为页,跟踪所有的空闲帧。当需要执行进程时,其页从备份存储中调入到可用的内存帧中。

但是,由于每一页之间并不一定是连续的,所以我们需要一个数据结构,来存储每一页的首地址,然后作为基址给到重定位寄存器,而这个数据结构,就是我们的“页表”。此时,我们是通过页表来将逻辑地址转换成物理地址,相当于用页表代替了MMU的工作。
在这里插入图片描述
CPU生成的地址分成以下两部分:页号P:页号作为页表中的索引。页表中包含每页所在物理内存的基地址。页偏移(d):与页的基地址组合就形成了物理地址,就可送交物理单元。f=页号*页大小+基地址。

每个进程有一个页表,页的大小与cache无关,有大页和小页之分(需要能够兼容),但页的大小不能随着进程随意改变。

页表这个数据结构时属于OS的,每一个进程的PCB中会有一个指针指向页表。此时,每次数据/指令的访问需要访问两次内存。一次访问页表,另一次访问数据/指令

什么意思呢,就是我们的每一个进程都会被OS赋予一个自己的页表,虽然占据了一定的内存空间,但通过这个页表,我们进程可以实现在物理空间中的不连续的存储,而这个页表可以看作是一大堆基址的集合,用来构成一个连续的逻辑地址空间。

那么,进程是在什么时候被分页,并构成页表的呢?自然是在被转入内存的时候,即长期调度时。

这种分页技术,不会产生外部碎片,但是会有内部碎片,即我们没有办法保证每一个进程正好是我们每一帧的大小的整数倍

但是,每一次都要访问页表,尤其是对于某些很长的代码构成的进程,这样子会很慢的,所以我们有了一个叫TLB的硬件,它类似于我们的高速缓存,可以同时对其中的所有项进行一个比较,我们将页表中的某些常用的项放在其中,这样,在我们要访问页表之前,可以先访问TLB,如果TLB中有,直接就可以找到,速度会快得多。
在这里插入图片描述
既然我们有了页表这个数据结构,我们自然要考虑,我们如何设计这个结构?当然,最简单的就是构成一个一维数组的样子,每一位对应一个物理地址。

但是,如果我们进程的逻辑地址空间很大,那么页表很可能会又臭又长,这个时候,我们要查找页表中的某一项所花费的时间将会很长。由此,我们自然而然地想到了我们的老方法,将页表再进行划分,就类似于字典,我们先根据首字母划分,然后再根据第二个字母划分,以此类推,这样,就有了我们的层次页表:
在这里插入图片描述
层次页表已经很好用了,但是呢,还是有可能会有很多层,这个时候怎么办呢?
还记不记得我们算法的时候学排序学到过一个哈希表的结构?现在就来了。虚拟地址中的虚拟页号被放入hash页表中。hash页表的每一条目都包括一个链接组的元素,这些元素hash成同一位置(碰撞)。每个元素有3个域,
①虚拟页码
②所映射的帧号
③指向下一个节点的指针

虚拟页号与链表中的每 一个元素的第一个域相比较。如果匹配,那么对应的帧码就用来形成位置地址。如果不匹配,那么就对链表中的下一个域进行页码比较。
在这里插入图片描述
实际上,我们相当于限制死了我们的页表有多少项,但这个时候,我们的每一项存储的不再是基址,而是一个基址链表,相当于我需要再查找链表才能找到我想要的基址。

当然,还有人比较擅长于逆向思维,想到了能不能不通过逻辑地址去找物理地址,而是通过物理地址去找相应的逻辑地址?这就是反向页表。说句实话,我个人并不是很看好这种方法,在这里就不说了。

分页基本上就是这样,也是比较好的方法了,但是呢,我们可以发现,计算机可以很容易的理解分页,找到每一页的内容与数据,但我们人不行啊,我们不知道这一页讲的是什么,有什么联系。

为了让人能够看懂,我们就将分页进行一点点改动,每一页的大小不再固定,而是可变,为什么可变,是因为我们从我们的程序的角度上将这个程序进行了大小不一的划分。比方说,主程序一段,子函数一段,变量一段,堆栈一段等等。每个段都有名称和长度,地址制定了段名称和段内偏移。

由于我们同样对一个程序进行了划分,所以我们也需要一个类似于页表的段表,用来将二维的用户定义地址映射为一维物理地址。段表的每个条目都有段基地址和段界限。
在这里插入图片描述
至此,我们基本上解决了进程如何装入内存的问题。

但是,问题又来了,内存太贵了,进程太多了,全都放进去放不下,怎么办呢?这时候我们看到了我们的磁盘,原本我们的程序就是放在磁盘上的文本文件,是在经过编译生成可执行文件之后,再放入内存的,也就是说,磁盘上本就有我们完整的可执行文件,那么,我们自然而然的想到了:

我们能不能将进程仍旧放在磁盘上,而只是将我们需要运行的那一部分(一小部分代码和数据)放入内存,然后通过不断地换入换出就可以了啊。

这就是我们虚拟内存的基本假设:进程的代码与数据部分装入内存

而想要实现这个,我们就需要实现“请求分页”,即在需要的时候才调入相应的页。

想要实现这个,首先,我们需要一个位来表示当前某个页是否在内存中,即有没有放入,这里,就是有效无效位,1:合法且在内存中;0:无效或者可能在磁盘上,当有效位无效的时候,不会对MMU进行地址转换

然后和之前一样,我们在长期调度的时候判断哪些页可以放入内存,哪些放在磁盘。

那么,整体是怎么工作的呢?是这样的:
①检查进程的内部页表(一般与PCB一起保存),以确定该引用是合法还是非法的地址访问。在中断程序中进行
②如果引用非法,那么终止进程。如果引用有效但是尚未调入页面,那么现在应调入。
③找到一个空闲帧(从空闲帧链表中取一个,可能引发页面置换)
④调度一个磁盘操作,以便将所需要的页调入刚分配的帧。是磁盘I/O采用DMA方式进行,会将进程放入等待队列
⑤当磁盘读操作完成后,修改进程的内部表和页表,以表示该页已在内存中。I/O完成
⑥重新开始因非法地址陷阱而中断的指令。进程现在能访问所需的页,就好像它似乎总在内存中。进程状态改变
在这里插入图片描述
相当于如果我们的页没有放入内存,将会有两次中断,通过第一次中断陷入OS,OS启动I/O操作,I/O操作完成引发第二次中断,这里的I/O操作实际上就是磁盘放入内存的过程

这个时候,貌似我们实现了请求分页,但有一个很严重的问题没有考虑,就是,当我们想从磁盘中拿一个页放入内存的时候,如果内存已经满了怎么办?

这里,就需要解决页面置换的问题,而正是页面置换这一步,真正意义上的区分了逻辑内存与物理内存。

实际上,页面置换的步骤很简单:
①查找所需页在磁盘上的位置
②查找一个空闲帧,否则用页置换算法选择一个牺牲帧,将其写入磁盘,并改变页表与帧表
③将所需页写入空闲帧,改变页表与帧表
④重启进程,阻塞到就绪

而问题就在于,我们如何选择一个牺牲帧。这里,就和之前的进程调度一样,有很多种不同的算法了。

最正常最简单最直接的就是先进先出式方法FIFO,即记录每个页进入内存的时间,并选择最老的页,但这种方法不一定是最好的,比方说我们可能需要不时调用一个全局变量

最优的是什么呢?是我们最优置换方法OPT,这个方法想的是置换最长时间内不会被使用的页,这是一种很难实现的算法,一种需要未来的向前看的算法。

但为了接近最优,我们有了一个LRU页置换方法,这个方法就是找到最近最少使用的页进行置换,置换最长时间内没有被使用的页,是一种向后看的算法

而LRU页置换如何实现,有很多种方式,比方说通过附加引用位,我们可以记录在最近的多少个周期内某一个页的使用情况,或者二次机会算法,这些都是后话。

然后呢,我们也可以使用基于计数的页置换方法,为每个页保留一个用于记录其引用次数的计数器,有两种算法:
①最不经常使用页置换算法
②最常使用页置换算法
这些,基本上就是我们对于页面置换的实现方法了。

在我们以为进程内存分配已经结束的时候,我们发现了虚拟内存导致的另一个问题。

我们知道,虚拟内存是因为我们不把进程的全部,而仅仅所需要运行的一部分放入内存,这样我们的虚拟内存的大小完全可以大于实际内存,我们可以运行比实际更多的进程。但这引来了另一个问题。我们如何确定某一个进程在执行的时候需要分配多少内存(页)才能够保证他正常运行。

就比方说,一个全部大小为10页的进程,我们最理想的是他每次只需要一页的空间,这样当这一页运行完,将当前页换出,而换入下一页,这是最好的。但是,由于有全局变量之类的存在,可能他在执行第1页的代码,却要引用到第5页的变量,以及第8页的数组,这个时候,如果我们只给了他一页的空间,他就需要频繁的换入换出,甚至都不能够执行完毕,这样,就导致了颠簸。

所以,虚拟内存自身会有一个“帧分配”的问题。同样有两种主流的方法:
①固定分配,有两种,平均分配和按比例分配,每个进程所分配的数量会随着多道程序的级别而改变,多道程序的程度增加(内存中进程的数量增加),那么每个进程会失去一些帧来给新的进程

②优先级分配,按优先级比例而不是进城大小来分配,如果一个进程产生了一个页错误,那么可以从自身的帧中选择用于替换,或者从比自身优先级低的进程中选取帧用于替换

同样的,分配有两种思想,静态分配和动态分配,静态分配是从一开始进程被固定了能够使用多少帧,那么在整个运行过程中,就只能从这些帧中进行选用,而动态分配就是能够使用的帧的大小是不固定的,可以随着运行进行分配。两种思想就表现为全局置换和局部置换:
①全局置换:允许一个进程从所有帧集合中选择一个置换帧,而不管该帧是否已分配给其他进程;一个进程可以从另一个进程中取帧。
②局部置换:要求每个进程仅从其自己的分配帧中进行选择

上文简单地提到了一点颠簸,这里就讲一下颠簸是什么,如果一个进程没有足够的页,那么会一直忙于将页面换进换出,页错误率就会非常高。这会导致CPU使用率低,新的进程会加入到系统中来。OS发现CPU使用率低,会加大多道程序程度,使更多进程加入到内存,使页错误率更高,最终系统无法完成工作

局部置换可以限制颠簸在进程之间传递与扩散,但不能够解决。

当进程执行时,它从一个局部移向另一个局部。局部是一个经常使用页的集合。

一个程序通常由多个不同局部组成,它们可能重叠。

当一个子程序(函数)调用的时候,就定义了一个新局部

能够解决颠簸问题,最好的就是对页错误率进行实时的检测与计算,可以灵活地控制颠簸,如果实际的页错误频率太高,则分配更多的帧,如果太低,就可以从进程中拿走帧。如果太高而又没有可用的空闲帧,那么就选择一个进程进行暂停,将其的帧进行释放,并分配给错误率高的进程

现在,我们对于进程在内存中的处理已经全部解决。

接下来,我们自然而然的就想到了,内存结束了,就是外存了。如何解决外存的问题。

首先,我们的外存是什么?目前最常用的自然是磁盘,通过在磁片上进行磁记录可以保存信息。每一盘分两个磁面,每个磁面分为多个圆形磁道,每个磁道分为多个扇区,同一个磁臂位置所涵盖的磁道集合构成了柱面

可以看到,磁盘也一样,被分成了很多个小区域,我们可以看做一个一维的逻辑块的数组,逻辑块是最小的传输单位,一位逻辑块数组an顺序映射到磁盘的扇区,扇区0是最外面柱面的第一个磁道的第一个扇区。先按磁道内扇区顺序,再按照柱面内磁道顺序,再按照从外向内的柱面顺序

既然进行了划分,那么自然而然地,我们就不得不考虑对他的访问问题。有没有觉得熟悉?是的,和之前的CPU调度(进程调度)很像,而这里我们的评价要求不再是平均等待时间最短,而是磁头的移动距离最小。这里我个人感觉是,磁头会在半径上进行一个来回的搜索,而我们想要的就是让磁头不要太经常的来回摆动。

同样的,最开始便是先进先出调度,即FCFS调度,可以解决饥饿问题,但很明显,磁头的来回摆动太多,不是最优化算法。

既然进程调度中有一个最短作业优先调度STF,那么同样,我也可以有一个最短距离有限的SSTF调度,去寻找当前距离最近的磁头。这个同样不是最优算法,只是一种局部最优算法,可能导致饥饿。

然后呢,就是我们的扫描算法SCAN调度。磁臂在磁盘上来回移动,磁头来回扫描,如果当前位置恰好有请求,则执行该请求。需要知道磁头当前位置以及移动方向。

可以改善饥饿,但是仍然可能发生饥饿(书上这里说是不可能导致饥饿,但经过思考之后,发现还是有可能的。考虑到请求不一定是同时到达的,如果磁盘读写慢于请求到达的速度,就有可能一直停留在一个磁头位置。举个例子:食堂打饭的时候,一个阿姨负责三个窗口,并且来回扫描进行打饭,如果一个窗口的人非常多,且一直往前挤,那么,阿姨就不得不一直给这个窗口的人打饭)

对SCAN调度方法进行一个小小的优化,我们就有了C-SCAN调度,是SCAN的一种变种,磁头从磁盘的一段移动到另一端,处理请求,但返回的时候不处理请求,相当于把柱面看做一个环形链表

由于SCAN是一个来回的扫描,那我能不能做一个一趟的扫描呢?这便有了LOOK调度,与SCAN类似,但是不会运行到磁头的另一端,即不会走过磁盘的整个宽度,而是只移动到一个方向上最远的请求为止

同样的,对C-SCAN进行处理,我们也可以得到C-LOOK调度,与C-SCAN类似,但是不会运行到磁头的另一端,即不会走过磁盘的整个宽度,而是只移动到一个方向上最远的请求为止

讲完了磁盘调度,下面会有什么问题呢?

首先,我们拿到一个磁盘,要做什么?自然是格式化,清空,而这里,格式化有两种:
①低级格式化:也叫物理格式化,将磁盘分成磁盘控制器能读和能写的扇区,为每一个扇区采用特别的数据结构。
②逻辑格式化(高级格式化,用来创建文件系统)

为了能存储文件,OS必须在磁盘上记录一定的数据结构,有两步,首先是进行分区,然后进行逻辑格式化(高级格式化,用来创建文件系统)

在逻辑格式化的时候,OS将初始的文件系统数据结构存储到磁盘上,包括空闲与已分配的空间和一个初始为空的目录

这里,我们需要扯回之前提到过的,我们在开机的时候,会在磁盘的0柱面0扇区找一个引导程序,这里既然在讲磁盘,那么就再进行讲解一下:

个人认为:在加电后,首先执行加电自检程序(POST程序),这个程序是在BIOS上的(BIOS是在ROM芯片上),如果有错误将会终止开机;否则执行自举程序(bootstrap)这个程序是在0扇区那400个字节上的。如果没有二级引导程序(很久以前的老系统,OS不大)会直接将OS内核装入内存,并跳转到OS的第一条指令。BIOS包括了上面的上电自检程序、自举程序(初始化)、引导程序,由硬件基本输入输出程序存储在ROM芯片上,程序中有中断例程、系统设置、POST程序、自检程序,这个自举程序并不是上面的自举程序

绝大多是PC,自举程序保存在ROM中,因为ROM不用初始化且位于固定位置,而且是制度的,不会受到计算机病毒的影响。但只是一小部分,完整的自举程序在磁盘的启动快上,位于固定位置,称为启动磁盘或系统磁盘。小的自举程序的作用时进一步从磁盘上调入更为完整的自举程序。这一段我更倾向于将自举程序看成引导程序。

格式化好了,也建立了文件系统了,那么我们就可以对这一个磁盘进行使用了。
但是,在使用的时候,我们肯定会出现错误的情况,某一个磁盘坏掉了怎么办?有人说换一个,但数据怎么办,这里,就需要我们养成时不时备份的习惯。而这个备份,就是我们的“镜像”,即复制每一个磁盘

但是,复制一整个磁盘代价有点大,能不能对我们坏掉的数据进行一个修复呢?这里,就有了我们的校验位。
而且,我们为了保险起见,不能把所有的鸡蛋放在一个篮子里是不是,这就有了“分散”的概念,可以保险,也可以通过并行处理的方式改变性能,有两种方法:
①位级分散:多个磁盘上分散每个字节的各个位,8个磁盘可以合并为单个磁盘使用,每秒能够处理的访问和单个磁盘一致,但可接触的数据更多了,能访问的数据为8倍,相当于磁盘合并
②块级分散:一个文件的块可以分散在多个磁盘上

而通过不同的校验位和分散可以组成不同的方案,这些方案就是我们的RAID级别(或者说结构),关于这个我不想多说,很麻烦,但并不是太难理解,自己看看书吧。

接下来,我们就要考虑,如何实现在内存与磁盘之间、以及磁盘与设备之间的数据传输,也就是我们的I/O子系统。这一章我们之前有说过,什么轮询啊、中断啊、缓冲啊,基本上都是一些换汤不换药的东西,只不过讲的比较详细,比方说DMA直接内存访问的步骤是什么什么的,在这里不进行多说,因为并不是很难,之前也有说明。

下面才是重点。

上文我们提到过对磁盘进行一个管理,格式化的时候用到了一个文件系统的东西。这个是干什么的呢?简单地将,我们现在的电脑,打开F盘,里面密密麻麻的是什么?是文件夹,而这些文件夹是如何存储的,如何实现的?就是我们的文件系统的工作。

想要搞定文件系统,首先要明白,文件系统所处理的文件是什么东西,需要实现什么。

文件是记录在外存上的相关信息的具有名称的集合,操作系统对存储设备的各种属性加以抽象并且定义了逻辑存储单元(文件),再将文件映射到物理设备上。通常,文件表示程序和数据

文件的属性有很多,名称类型位置大小保护等等,这里要注意,文件的信息被保存在目录结构中,而目录结构也保存在外存上

至于目录是什么,我们下文再说。

先继续说一下,文件所需要的操作是什么。实际上都知道,创建、读、写、删除、截短等等

打开文件需要有如下信息:
①文件指针:系统必须跟踪上次读写操作的位置作为当前文件位置指针
②文件打开计数器:跟踪打开和关闭文件的数量,在最后关闭的时候计数器为0,此时系统可删除该条目
③文件磁盘位置:用于定位磁盘上文件位置的信息保存在内存中以避免每个操作从磁盘中读取信息
④访问权限:每个进程用一个访问模式打开文件,保存在单个进程打开的文件表中,以便操作系统能允许或拒绝以后的I/O请求

访问文件的方法也有几种,如顺序访问,被称为磁带模型,一个记录接着一个进行处理;也可以直接访问,被称为磁盘模型,被允许程序按照任意顺序进行快速读与写。

讲完了文件的基本信息,我们就可以开始写文件系统了。

首先呢,我们需要一个数据结构,来存储我们的文件信息,这里,我们可以把每一个文件的文件信息看成一个节点,那么,问题就变成了我们通过什么样的结构来组合这些节点。而这个结构,就是我们的目录结构。

目录是包含所有文件信息的节点的集合

目录可以看做符号表,他能将文件名称转换成目录条目,相关操作有:
①搜索文件:能够搜索目录结构以查找特定文件的条目
②创建文件:可以创建新文件并加入到目录中
③删除文件
④遍历条目:能遍历目录内所有文件以及其目录中每个文件条目的内容
⑤跟踪文件系统:访问每个目录和每个目录的每个文件

首先呢,是单层结构目录,简单地讲,就是线性表的形式,所有文件都包含在同意目录中,便于支持和理解,但存在命名问题与分组问题,每个文件必须拥有唯一名称,不可重名,文件条目过多时,难以记住所有文件的名称

然后呢,为了能够为不同的用户建立不同的目录,我们有了双层结构目录,即第一层是用户名构成的线性表,每一个用户名指向他自己的线性表,其中存储着他自己的文件

最后呢,便是我们现在最常用的树形目录,允许用户创建自己的子目录,相应地组织文件。系统内每个文件有唯一的路径名

路径名有两种方式:
①绝对路径:从根目录开始给出路径上的目录名直到所指定的文件
②相对路径:从当前目录开始定义路径

个人理解是这样的,单层结构目录是我们写操作系统的时候,最开始的只是将所有的文件摆在一起,这个时候文件名自然不能一样;双层结构实际上也差不多,结伴上相当于我们将我们所写的文件按照不同的属性(功能也好名称也好)分成了多个文件夹,这个时候每个文件夹的文件互不干扰;而树形结构就是我们现在的操作系统对文件进行分类的情况,有多个盘,每个盘下面有多个文件夹,文件夹也可以嵌套文件夹等。

当然,为了实现共享,还可以有无环图目录,以及通用图目录等等。

个人理解:我们需要区别安装与格式化。所谓安装,就是将一个磁盘通过数据线连接到PC端,这叫安装。我们在安装文件系统的时候,是两个文件系统进行链接的过程(比方说,磁盘的文件系统与U盘的文件系统的目录在某个节点进行链接),而操作系统,也只不过是根据文件系统在内存中的一部分的缓存数据而去寻找到相应文件的引导块,再通过引导块将文件以DMA的形式传入到内存中;

而格式化则不一样,格式化是操作系统根据磁盘生成一个合理的分区并创建一个新的文件系统的软件实现(目录文件),此后OS就可以根据目录文件去访问这个硬件设备

个人理解:文件系统包括硬件实现和软件实现,硬件部分包括设备(磁盘等存储装置)与设备控制器,软件部分包括设备驱动程序、中断处理程序、文件组织模块(就是地址转换器)以及目录(我个人还是倾向于将其看做一个指针文件,也就是上文的逻辑文件系统)

简单地讲,由于现在,操作系统需要同时支持多个文件系统类型,这就牵扯到如何将多个文件系统合并成一个目录结构?这就有了虚拟文件系统,实际上,个人感觉并不难,只要指针到位,合并目录并不是一件很难的事情,难点在于,如何实现使用。如果不同的文件系统有着不同的调用函数,这个时候就会很难办。所以,这就是虚拟文件系统的作用,统一了调用函数,允许在不同类型的文件系统上采用相同的API,这样,我们在编写程序之后,编译器在生成可执行程序的时候,就不需要区分好几套不同的API来调用硬件设备,而是用一套就可以了

那么目录如何进行实现呢?最为简单的方法是使用存储文件名和数据块指针的线性列表(数组、链表等),当然,不嫌麻烦也可以用哈希表。

可能有人会问,上文不是已经提到过目录的实现了么,这里又是什么呢?实际上,之前提到的目录的结构,包括单层、双层、树形、无环图等等,都是一种理论逻辑上的结构,这里讲的是采取什么样子进行实现,包括链表、数组等等,哈希表也是一种方式。

现在,我们有了文件系统,我们可以找到我们的文件,但是,问题又来了,我们如何为文件分配磁盘块,或者如何存储文件?

由于文件很多,而且牵扯到各种操作,所以就需要对文件进行一个磁盘的分配,类似于进程在内存上的分配。

首先呢,自然是连续分配,这里实际上就是进程分配中的单区间分配方法,这样也一样会有外部碎片的问题

然后呢,是链接分配,什么意思呢?每个文件是磁盘块的链表;磁盘块分布在磁盘的任何地方。实际上,最简单的讲就是,我们在磁盘中创建了一个很长的文件链表

最后,还有一个索引分配,将所有的数据块指针集中到索引块中
①索引块中的第i个条目指向文件的第i块。
②目录条目包括索引块的地址

索引分配支持直接访问,且没有外部碎片问题,普通的索引分配与内存分配中的分页(注意,普通的是分页)很像,因为磁盘实际上就是大小确定的块,因为大小确定,所以也会有一个类似于页表的情况。而拓展的索引分配与内存分配中的分段(注意,不是分页,分页是分成了大小一致的块,分段是分成了大小不一的段!!!)有些类似,同样是有一个类似于段表的结构,然后通过段表的指针去找到文件,这是因为拓展的索引分配允许块与块之间进行合并与组合

为了记录空闲磁盘空间,系统需要维护一个空闲空间链表,它记录了所有空闲磁盘空间,即未分配给文件或目录的空间。(不一定以链表的方式实现)
悄悄说一句,有没有感觉,外存的分配好像没有内存那么讲究?我个人感觉,这是因为,我们的内存很小,我们要尽可能的使用它的一切资源,效率优先;而外存不一样,如果我们想的话,外存可以像多大要多大(钱要够),所以,我们存储的要点并不是效率,而是人性化,这也就是为什么我们会有一颗树的形式进行存储,就是为了满足我们整理的强迫症。

如果你能看到这里,那么恭喜你,你成功的看完了《操作系统概念》前十三章的内容,可以看到,内容虽然复杂繁多,但还是存在一条比较清晰的逻辑线的,推荐使用我的其他关于操作系统的博客,进行更加系统的复习。

发布了205 篇原创文章 · 获赞 110 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/qq_40851744/article/details/103652983