三. 内存管理1. 内存管理的概念2 内存空间的扩充2.1 覆盖技术2.2 交换技术3 连续分配方式3.1 分配方式单一连续分配固定分区分配动态分区分配3.2 动态分区分配算法首次适应算法最佳适应算法最坏适应算法邻近适应算法4. 非连续分配的管理方式4.1 基本分页存储管理基本地址变换机构具有快表的地址变换机构4.2 两级页表解决单级页表第一个问题解决单级页表第二个问题4.3 基本分段存储管理方式分段概念地址表示分段与分页管理的对比4.4 段页式管理方式段表、页表地址转换过程5. 虚拟内存5.1 请求分页管理方式页表机制对比缺页中断机制5.2 页面置换算法最佳置换算法(OPT)先进先出置换算法(FIFO)最近最久未使用置换算法(LRU)时钟置换算法(CLOCK)改进型的时钟置换算法5.3 页面分配策略页面分配、置换策略调入页面时机及何处调入抖动现象与工作集6. 内存映射文件四. 文件管理1. 文件的逻辑结构1.1 顺序文件1.2 索引文件1.3 索引顺序文件2. 文件目录2.1 文件控制块2.2 文件的目录结构单级目录结构两级目录结构多级目录结构无环图目录结构索引结点3. 文件的物理结构3.1 连续分配方式3.2 链接分配方式隐式链接显示连接3.3 索引分配4. 文件的逻辑结构和物理结构区别4.1 无结构文件4.2 顺序文件4.3 索引文件5. 文件存储空间管理5.1 存储空间的划分与初始化空闲表法空闲链表法位示图法成组链接法6. 文件的基本操作7. 文件共享与保护7.1 基于索引结点的共享方式(硬链接)7.2 基于符号链的共享方式(软链接)7.3 文件保护8. 文件系统8.1 文件系统的全局结构文件系统在外存中的结构文件系统在内存中的结构8.2 虚拟文件系统8.3 文件系统的挂载五. 设备管理1.
内存可存放数据。程序执行前需要先放到内存中才能被CPU处理,目的是为了缓和CPU与硬盘之间的速度矛盾。
由于在计算机组成原理知识可知指令中的地址是逻辑地址,而如何将逻辑地址转换为物理地址这里给出三种策略:绝对装入、可重定位装入(静态重定位)和动态运行时装入(动态重定位)。
绝对装入
在编译时,如果知道程序将放到内存中的哪个位置,编译程序将产生绝对地址的目标代码。装入程序按照装入模块中的地址,将程序和数据装入内存。
如:如果知道装入模块要从内存地址为
前提是需要知道装入模块起始地址,而换一台电脑之后不能保证模块的起始地址还是从
可重定位装入(静态重定位)
采用这种方式,编译、链接后的装入模块的地址都是从
如果一个模块的起始地址是
静态重定位的特点是在一个作业装入内存时,必须分配其要求的全部内存空间(连续空间),如果没有足够的内存,就不能装入该作业。作业一旦进入内存后,在运行期间就不能再移动,也不能再申请内存空间。
这种方式用于用于早期的多道批处理操作系统。
动态运行时装入(动态重定位)
编译、链接后的装入模块的地址都是从
有一个专门用于存放模块起始位置的寄存器,即重定位寄存器。当一个模块装入内存后,模块在内存中的起始地址会放入重定位寄存器。之后指令中的逻辑地址(形式地址)
采用动态重定位时允许程序在内存中发生移动。并且可将程序分配到不连续的存储区中。在程序运行前只需装入它的部分代码即可投入运行,然后在程序运行期间,根据需要动态申请分配内存,便于程序段的共享,可以向用户提供一个比存储空间大得多的地址空间。
这种方式用于现代操作系统。
程序的运行过程如下:
开发人员编写完代码之后,需要进行编译,由编译程序将用户源代码编译成若干个目标模块(编译就是把高级语言翻译为机器语言)。这些模块已经包含目标代码所对应的指令,指令编址都是从逻辑地址
接着把目标模块都组装起来形成一个完整的装入模块。在Windows中装入模块就是exe可执行文件。此时装入模块有一个完整的链接地址。
最后把装入模块装入内存中即可。当装入内存中就确定了进程所对应的实际物理地址是多少。
上面已经介绍了三种装入方式,但是链接的方式也有三种:
静态链接方式
在程序运行之前,先将备目标模块及它们所需的库函数连接成一个完整的可执行文件(装入模块)之后不再拆开。就是上面提到的方式。
装入时动态链接
将各目标模块装入内存时,边装入边链接的链接方式。一边装入一边形成逻辑地址。
运行时动态链接
在程序执行中需要该目标模块时,才对它进行链接。其优点是便于修改和更新,便于实现对目标模块的共享。
假如运行main()
函数时用到a()
函数,只需要将目标模块b()
即目标模块
内存基础知识总结:
操作系统作为系统资源的管理者,当然也需要对内存进行管理。操作系统需要对内存进行以下几种管理:
操作系统负责内存空间的分配与回收。
程序装入内存后,操作系统要怎么记录哪些内存区域已经被分配出去了,哪些区域还处于空闲状体。当某个进程运行结束之后,要将进程占用的内存空间回收。当一个新的进程需要装入内存时操作系统应该指出放到哪片区域。
操作系统需要提供某种技术从逻辑上对内存空间进行扩充
某个游戏的大小超过60GB,按理来说这个游戏程序运行之前需要把60GB数据全部放入内存。然而,实际我的电脑内存才4GB,但操作系统使用虚拟技术(操作系统的虚拟性),就可以运行。把物理上很小得内存拓展为逻辑上很大的内存。
操作系统需要提供地址转换功能,负责程序的逻辑地址与物理地址的转换。
为了使编程更方便,程序员写程序时应该只需要关注指令、数据的逻辑地址。而逻辑地址到物理地址的转换(这个过程称为地址重定位)应该由操作系统负责,这样就保证了程序员写程序时不需要关注物理内存的实际情况。
装入的方式有三种:绝对装入(编译时产生绝对地址)、可重定位装入(装入时将逻辑地址转换为物理地址)、动态运行时装入(运行时将逻辑地址转换为物理地址,需设置重定位寄存器)。
操作系统需要提供内存保护功能。保证各进程在各自存储空间内运行,互不干扰
让各个进程只能访问自己的内存空间。
假设进程
方法一:在CPU中设置一对上、下限寄存器,存放进程的上、下限地址。进程的指令要访问某个地址时,CPU检查是否越界。
方法二:采用重定位寄存器(又称基址寄存器)和界地址寄存器(又称限长寄存器)进行越界检查。重定位寄存器中存放的是进程的起始物理地址。界地址寄存器中存放的是进程的最大逻辑地址。
假如进程当前要访问一个逻辑地址为
内存空间扩充技术有三种:覆盖技术、交换技术和虚拟存储技术(之后会介绍)
早期的计算机内存很小,比如IBM推出的第一台PC机最大只支持1MB大小的内存。因此经常会出现内存大小不够的情况。后来人们引入了覆盖技术,用来解决"程序大小超过物理内存总和"的问题。
覆盖技术的思想:将程序分为多个段(多个模块)。常用的段常驻内存,不常用的段在需要时调入内存。
具体的做法是内存中分为一个"固定区"和若干个"覆盖区"。需要常驻内存的段放在"固定区"中,调入后就不再调出(除非运行结束)不常用的段放在"覆盖区",需要用到时调入内存,用不到时调出内存。
如上图,A函数会依次调用B和C函数(注意是依次调用而不是同时调用)。B函数又会调用D函数,而C函数会依次调用E和F函数。
可以把A函数(包含main()函数)模块放到内存的固定去里,大小就是A模块的大小,即
这样只用
这种方式也有很明显的缺点:必须由程序员声明覆盖结构,操作系统完成自动覆盖。所以对用户不透明,增加了用户编程负担。覆盖技术只用于早期的操作系统中,现在已成为历史。
交换(对换)技术的设计思想:内存空间紧张时,系统将内存中某些进程暂时换出外存,把外存中某些已具备运行条件的进程换入内存(进程在内存与磁盘间动态调度)。
之前学到的中级调度(内存调度),就是要决定将哪个处于挂起状态的进程重新调入内存。
暂时换出外存等待的进程状态为挂起状态(挂起态,suspend)。挂起态又可以进一步细分为就绪挂起、阻塞挂起两种状态。在加入这两种状态后就有了七状态模型。
而此时会产生第一个问题,应该在外存(磁盘)的什么位置保存被换出的进程?
具有对换功能的操作系统中,通常把磁盘空间分为文件区和对换区两部分。文件区主要用于存放文件,主要追求存储空间的利用率,因此对文件区空间的管理采用离散分配方式。对换区空间只占磁盘空间的小部分,被换出的进程数据就存放在对换区。由于对换的速度直接影响到系统的整体速度,因此对换区空间的管理主要追求换入换出速度,因此通常对换区采用连续分配方式(学过文件管理章节后即可理解)。总之,对换区的
第二个问题是什么时候应该交换?
交换通常在许多进程运行且内存吃紧时进行,而系统负荷降低就暂停。例如:在发现许多进程运行时经常发生缺页,就说明内存紧张,此时可以换出一些进程。如果缺页率明显下降,就可以暂停换出。
第三个问题是应该换出哪些进程?
可优先换出阻塞进程。可换出优先级低的进程。为了防止优先级低的进程在被调入内存后很快又被换出,有的系统还会考虑进程在内存的驻留时间。
注意:PCB会常驻内存,不会被换出外存。所以操作系统换出进程并不是将所有的数据都放入外存,操作系统为了保持对换出进程的管理,进程的PCB信息还是会放在内存中进行管理。
覆盖与交换总结:
内存空间的分配与回收有两种方式:连续分配方式和非连续分配方式。连续分配方式又可以进一步细分为:单一连续分配、固定分区分配、动态分区分配。
连续分配:指为用户进程分配的必须是一个连续的内存空间。非连续分配:为用户进程分配的是一个离散的内存空间。
在单一连续分配方式中,内存被分为系统区和用户区。系统区通常位于内存的低地址部分,用于存放操作系统相关数据。用户区用于存放用户进程相关数据。内存中只能有一道用户程序,用户程序独占整个用户区空间,并不支持多道程序并发运行。
当一个用户进程所需内存空间很小,但放入内存的用户区之后,用户去中其他的空闲区间,也不会分配给别的用户程序。所以说是整个用户程序独占整个用户区的存储空间。
所以优点:实现简单。无外部碎片。可以采用覆盖技术扩充内存。不一定需要采取内存保护(如:早期的PC操作系统MS-DOS)
缺点:只能用于单用户、单任务的操作系统中。有内部碎片。存储器利用率极低。
内部碎片是指分配给某进程的内存区域中,如果有些部分没有用就是"内部碎片"。
20世纪60年代出现了支持多道程序的系统,为了能在内存中装入多道程序,且这些程序之间又不会相互干扰,于是将整个用户空间划分为若干个固定大小的分区,在每个分区中只装入一道作业,这样就形成了最早的、最简单的一种可运行多道程序的内存管理方式。
固定分区分配策略有两种:分区大小相等和分区大小不等。
如果采用分区大小相当这种策略,系统会把用户区整片的内存区间分割为若干个固定大小相等的区域。
如果采用分区大小相当这种策略,系统会把用户区整片的内存区间分割为大小固定,但各个分区大小不相等的区域。
分区大小相等:缺乏灵活性,但是很适合用于用一台计算机控制多个相同对象的场合(比如:钢铁厂有
而分区大小不等:增加了灵活性,可以满足不同大小的进程需求。根据常在系统中运行的作业大小情况进行划分(比如:划分多个小分区、适量中等分区、少量大分区)。
另外操作系统需要建立一个数据结构,即分区说明表,来实现各个分区的分配与回收。每个表项对应一个分区,通常按分区大小排列。每个表项包括对应分区的大小、起始地址、状态(是否已分配)。
如果某个系统分区如下图所示,则对应的分区表如下:
用数据结构的数组(或链表)即可表示这个表。当某用户程序要装入内存时,由操作系统内核程序根据用户程序大小检索该表,从中找到一个能满足大小的、未分配的分区,将之分配给该程序,然后修改状态为"已分配"。
优点:实现简单,无外部碎片。
缺点:当用户程序太大时,可能所有的分区都不能满足需求,此时不得不采用覆盖技术来解决,但这又会降低性能。同时会产生内部碎片,内存利用率低。
动态分区分配又称为可变分区分配。这种分配方式不会预先划分内存分区,而是在进程装入内存时,根据进程的大小动态地建立分区,并使分区的大小正好适合进程的需要。因此系统分区的大小和数目是可变的。
如:假设某计算机内存大小为64MB,系统区8MB,用户区共56 MB。则进程
这样会产生三个问题:
系统要用什么样的数据结构记录内存的使用情况?
系统中的分区的大小和数量会变的,并且有的分区已经分配,而有的没有分配,所以需要用一个数据结构表示。
当很多个空闲分区都能满足需求时,应该选择哪个分区进行分配?
如上图,此时进程
如何进行分区的分配与回收操作?
假设进程
接下来会详细介绍这三个问题。
系统要用什么样的数据结构记录内存的使用情况?
会采用两种常用的数据结构:空闲分区表和空闲分区链。
上图如果建立一个空闲分区表,则每个空闲分区对应一个表项。表项中包含分区号、分区大小、分区起始地址等信息。
如果建立一个分区链,则每个分区的起始部分和末尾部分分别设置前向指针和后向指针。起始部分处还可记录分区大小等信息。
当很多个空闲分区都能满足需求时,应该选择哪个分区进行分配?
假如现在有一个进程
把一个新作业装入内存时,须按照一定的动态分区分配算法,从空闲分区表(或空闲分区链)中选出一个分区分配给该作业。由于分配算法算法对系统性能有很大的影响,因此人们对它进行了广泛的研究。
如何进行分区的分配与回收操作?
这里以数据表结构为例演示分配操作,数据链原理类似。
情况一:
如上图所示,假设当前有一个进程
分配完毕后需要修改分区表,修改好的分区表如下:
情况二:
同样,如果采用某种算法确定进程
分配完毕后需要修改分区表,这里直接删除分区
回收也有几种情况:
情况一:回收区的后面有一个相邻的空闲分区
假设进程
此时这块回收区域的后面有一个10MB的空闲分区,因此在这块内存分区回收后,需要将这块空间与10MB的合并:
合并需要修改分区表:
所以两个相邻的空闲分区合并为一个。
情况二:回收区的前面有一个相邻的空闲分区。解决方法同情况一,都是这及将两个空闲区合并为一个。
情况三:回收区的前、后各有一个相邻的空闲分区
假设进程
合并分区表如下:
情况四:回收区的前、后都没有相邻的空闲分区
此时进程
注:新增一个表项时,各表项的顺序不一定按照上面地址递增顺序排列,具体的排列方式需要依据动态分区分配算法来确定。
动态分区分配没有内部碎片,但是有外部碎片。
内部碎片:分配给某进程的内存区域中,如果有些部分没有用上就是内部碎片。 外部碎片:是指内存中的某些空闲分区由于太小而难以利用。
如上图所示,假设此时有一个进程
如上图,虽然每个空闲区都不够进程
前面介绍的三种装入方式当中,动态重定位方式显然最方便实现这些进程在内存中移动位置这件事。其实就是将进程起始地址放入重定位寄存器中。
连续分配管理总结:
用于解决在动态分区分配方式中,当很多个空闲分区都能满足需求时,应该选择哪个分区进行分配的问题。
动态分区分配算法有四种:首次适应算法(First Fit)、最佳适应算法(Best Fit)、最坏适应算法(Worst Fit)、邻近适应算法(Next Fit)。
算法思想:每次都从低地址开始查找,找到第一个能满足大小的空闲分区。
实现方式:空闲分区以地址递增的次序排列。每次分配内存时顺序查找空闲分区链(或空闲分区表),找到大小能满足要求的第一个空闲分区。
某个系统中内存分配如上图所示,采用首次适应算法对应的空闲分区表如下:
上面是按照起始地址由低到高排列。对应的空闲分区链如下:
以空闲分区链为例,当有一个进程
此时这里剩余
如果再来一个进程
算法思想:由于动态分区分配是一种连续分配方式,为各进程分配的空间必须是连续的一整片区域。因此为了保证当"大进程"到来时能有连续的大片空间,可以尽可能多地留下大片的空闲区,即,优先使用更小的空闲区。
实现方式:空闲分区按容量递增次序链接。每次分配内存时顺序查找空闲分区链(或空闲分区表),找到大小能满足要求的第一个空闲分区。即找到的第一个空闲分区一定是能满足进程所需空间,并且最小的空闲分区。
如果系统中的内存使用情况如上图所示。对应的空闲分区链和空闲分区表如下:
是按照空闲分区从小到大递增的顺序排列。假如当前有一个新进程
由于最佳适应算法的空闲分区链和空闲分区表需要按照空闲大小的递增次序排列,所以这里新进程占据空闲分区大小为
缺点:每次都选最小的分区进行分配,会留下越来越多的、很小的、难以利用的内存块。因此这种方法会产生很多的外部碎片。
又称最大适应算法(Largest Fit),与最佳适应算法相反,算法思想是为了解决最佳适应算法的问题,即留下太多难以利用的小碎片,可以在每次分配时优先使用最大的连续空闲区,这样分配后剩余的空闲区就不会太小,更方便使用。
实现方式:空闲分区按容量递减次序链接。每次分配内存时顺序查找空闲分区链(或空闲分区表),找到大小能满足要求的第一个空闲分区。
如果系统中的内存使用情况如上图所示。对应的空闲分区链和空闲分区表如下:
此时有个新进行
同时修改分区链和分区表:
如果有另一个新进程
同时修改空闲分区链,修改后第一个空闲分区变为
缺点:每次都选最大的分区进行分配,虽然可以让分配后留下的空闲区更大,更可用,但是这种方式会导致较大的连续空闲区被迅速用完。如果之,后有"大进程"到达,就没有内存分区可用了。
算法思想:首次适应算法每次都从链头开始查找的。这可能会导致低地址部分出现很多小的空闲分区,而每次分配查找时,都要经过这些分区,因此也增加了查找的开销。如果每次都从上次查找结束的位置开始检索,就能解决上述问题。
实现方式:空闲分区以地址递增的顺序排列(可排成一个循环链表)。每次分配内存时从上次查找结束的位置开始查找空闲分区链(或空闲分区表),找到大小能满足要求的第一个空闲分区。
如果系统中的内存使用情况如上图所示。对应的空闲分区链表如下:
此时有一个新进程
同时修改空闲分区链
可以看到采用邻近适应算法和首次适应算法,只需要按照地址递增的次序进行排列即可。所以这里发生内存空闲分区变化,也不用对整个链表进行重现排列。这也是邻近适应算法和首次适应算法比最佳适应算法、最坏适应算法更优秀的地方。
假如又有一个进程
修改空闲分区链
可以看出邻近适应算法不需要从链头位置重新遍历,所以比首次适应算法更快。但这也并不代表比首次适应算法更优秀。
首次适应算法每次都要从头查找,每次都需要检索低地址的小分区。但是这种规则也决定了当低地址部分有更小的分区可以满足需求时,会更有可能用到低地址部分的小分区,也会更有可能把高地址部分的大分区保留下来(所以首次适应算法包含了最佳适应算法的优点)。
邻近适应算法的规则可能会导致无论低地址、高地址部分的空闲分区都有相同的概率被使用,也就导致了与首次适应算法相比,高地址部分的大分区更可能被使用,划分为小分区,最后导致无大分区可用(邻近适应算法拥有最坏适应算法的缺点)。
四种算法中,首次适应算法的效果最好。
四种动态分区分配算法总结:
非连续分配:为用户进程分配的可以是一些分散的内存空间。
如果一个系统支持分页存储的话,那么系统会把内存分为一个一个大小相等的区域。
如上图所示每个区域是4KB。每个分区就是一个"页框"(页框
为了把进程数据和指令放入页框中,操作系统会把各个进程的逻辑地址空间分为与页框大小相等的单元。
某个进程逻辑地址空间如上图所示,可以看到该进程是一个大小为
上面将进程的逻辑地址空间也分为与页框大小相等的一个个部分,每个部分称为一个"页"或"页面",每个页面也有一个编号,即"页号"。页号也是从0开始。如上图系统给进程的各个进行编号,这个编号就是"页号"(如进程A_0
)或者叫页编号。进程的各个页会被放到内存的各个页框当中,即操作系统以页框为单位为各个进程分配内存空间,所以进程的每个页面分别放入一个页框中。也就是说,进程的页面与内存的页框有一一对应的关系。
同时各个页面不必连续存放,可以放到不相邻的各个页框中。
这种一一对应的关系需要用数据结构"页表"来记录。即为了能知道进程的每个页面在内存中存放的位置,操作系统要为每个进程建立一张页表。而页表通常存在PCB(进程控制块)中。
上面提到过,进程的逻辑地址空间会被分为一个个的页面。每一个页面就会对应页表当中的一个页表项(一行)。而每个页表项由"页号"和"块号"组成。
所以这样一个页表就可以记录下来进程各个页对应的实际存放的内存块之间的映射关系。
问题一
假设某系统物理内存大小为4GB,页面大小为4KB,则每个页表项至少应该为多少字节?
内存块大小
而对于页号,页表项是连续存放的,因此页号可以是隐含的,不占存储空间(类比数组)。假设页表中的各页表项从内存地址为
注意:页表记录的只是内存块号,而不是内存块的起始地址。
问题二
将进程地址空间分页之后,操作系统该如何实现逻辑地址到物理地址的转换?
特点:虽然进程的各个页面是离散存放的,但是页面内部是连续存放的。
如果要访问逻辑地址
因为页面内部是连续存放的,所以
如:在某计算机系统中,页面大小是
计算方法:
所以
在计算机内部,地址是用二进制表示的,如果页面大小刚好是
假设某计算机用
后
结论
假设通过查询页表得知
结论
总结:页面大小刚好是
所以分页存储管理的逻辑地址结构如下所示:
如果有
所谓的基本地址变换机构就是在基本分页存储管理当中,用于实现逻辑地址到物理地址转换的一组硬件机构。这里要掌握原理和流程。
在分页存储管理当中,如果要把逻辑地址转换成物理地址,需要知道逻辑地址对应的页号、逻辑地址对应的页内偏移量、逻辑地址对应的页面在内存中存放的位置、最后根据页面在内存当中的起始位置和页内偏移量就可以得到最终的物理地址。
为了实现这个地址转换的功能,系统中会设置一个页表寄存器,用来存放页表在内存中的起始地址
设页面大小为
例:若页面大小L为
等价描述:某系统按字节寻址,逻辑地址结构中,页内偏移量占
计算页号、页内偏移量
页号P
根据题中条件可知,页号
物理地址
在分页存储管理(页式管理)的系统中,只要确定了每个页面的大小,逻辑地址结构就确定了。因此,页式管理中地址是一维的。即,只要给出一个逻辑地址,系统就可以自动地算出页号、页内偏移量两个部分,并不需要显式地告诉系统这个逻辑地址中,页内偏移量占多少位。
上小节知道每个页表项的长度是相同的,页号是"隐含"的。假设某系统物理内存大小为4GB,页面大小为4KB,内存总共会被分为
各页表项会按顺序连续地存放在内存中。如果该页表在内存中存放的起始地址为
但如果每个页表项占
结论:理论上,页表项长度为
上面例子也可以看出进程页表通常是装在连续的内存块中的。
基本地址变换结构总结:
可以看出在根据逻辑地址计算出物理地址访问内存单元的整个过程中,总共需要两次访问内存的操作。第一次访问内存是在查询页表的时候,第二次访问内存是在是在实际访问内存单元的时候进行的。
是基本地址变换机构的改进版本。在基本地址变换机构的基础上引入快表,就可以让地址变换的过程更快。
快表,又称联想寄存器(TLB,translation lookaside buffer ),是一种访问速度比内存快很多的高速缓存(TLB不是内存),用来存放最近访问的页表项的副本,可以加速地址变换的速度。与此对应,内存中的页表常称为慢表。
引入快表后,地址的变换过程:
由于查询快表的速度比查询页表的速度快很多,因此只要快表命中,就可以节省很多时间。因为缓存的局部性原理,一般来说快表的命中率可以达到
例:某系统使用基本分页存储管理,并采用了具有快表的地址变换机构。访问一次快表耗时
访问依次命中后还需要花
而有的系统支持快表和慢表同时查找,如果是这样,平均耗时应该是
快表慢表同时查找的甘特图如下:
可以很直观看到,当快表查询同时,慢表也在查询,经过
具有快表的地址变换机构与基本地址变换机构对比:
TLB和普通Cache的区别:TLB中只有页表项的副本,而普通Cache中可能会有其他各种数据的副本。
某计算机系统按字节寻址,支持
根据页号查询页表的方法:
根据局部性原理可知,很多时候,进程在一段时间内只需要访问某几个页面就可以正常运行了。因此没有必要让整个页表都常驻内存。
所以单级页表页表有以下问题:
问题一:页表必须连续存放,因此当页表很大时,需要占用很多个连续的页框。
解决第一个问题可以参考解决进程在内存中必须连续存储的问题,做法是将进程地址空间分页,并为其建立一张页表,记录各页面的存放位置。同样的思路也可用于解决"页表必须连续存放"的问题,把必须连续存放的页表再分页。
具体的做法是:
可将长长的页表进行分组,使每个内存块刚好可以放入一个分组(比如上个例子中,页面大小4KB,每个页表项4B,每个页面可存放1K个页表项,因此每1K个连续的页表项为一组, 每组刚好占一个内存块,再将各组离散地放到各个内存块中)另外,要为离散分配的页表再建立一张页表,称为页目录表,或称外层页表,或称顶层页表。
假如:某计算机系统按字节寻址,支持
由于单级页表的长度过大,所以可以将这个页表拆分成一个个的小分组,每个小分组的大小刚好可以装入一个内存块。由于每个页面的大小是
另外可以对这些小页表进行编号(如
在把大页表拆分成小页表之后,由于每一个页表的大小都是
为了记录这些小页表之间的相对顺序,以及在内存中存放的块号位置,需要为这些小页表再建立更上一级的页表,及页目录表(或者叫顶级页表、处层页表)。相应的这层小页表可以统称为二级页表。
从这个图中可以直观看出,页目录表其实是建立了,二级页表的页号,及二级页表在内存中存放的块号之间的映射关系。
所以此时如果想找
采用这样的两级页表结构后逻辑地址的结构也需要发生变化。可以把以前
例:将逻辑地址
按照地址结构将逻辑地址拆分成三部分
从PCB中读出页目录表始址,再根据一级页号查页目录表,找到下一级页表在内存中的存放位置
假设页目录表如下所示:
则其一级页号转化为十进制是
根据二级页号查表,找到最终想访问的内存块号
可以从这个位置读出二级页表,再用二级页号进行查询。
二级页号转换为十进制是
结合页内偏移量得到物理地址
最终要访问的内存块号为
问题二:没有必要让整个页表常驻内存,因为进程在一段时间内可能只需要访问某几个特定的页面。
可以在需要访问页面时才把页面调入内存(虚拟存储技术)。可以在页表项中增加一个标志位,用于表示该页面是否已经调入内存。
若想访问的页面不在内存中,则产生缺页中断(内中断),然后将目标页面从外存调入内存。之后会有更详细介绍。
需要注意的细节:
若采用多级页表机制,则各级页表的大小不能超过一个页面
例:某系统按字节编址,采用
页面大小
页面大小
如果只分为两级页表,则一级页号占
两级页表的访存次数分析(假设没有快表机构)
第一次访存:访问内存中的页目录表
第二次访存:访问内存中的二级页表
第三次访存:访问目标内存单元
在没有快表机构的情况下,
两级页表总结:
与"分页存储"最大的区别就是,离散分配时所分配地址空间的基本单位不同。
进程的地址空间会按照程序自身的逻辑关系划分为若干个段,每个段都有一个段名(在低级语言中,程序员使用段名来编程),每段从
假如一个进程是
可以看到每一个段都会有一个段名,是开发人员在编程的时候使用,另外每个段的地址都是从
操作系统为用户进程分配内存规则:以段为单位进行分配,每个段在内存中占据连续空间,但各段之间可以不相邻。
由于分段存储管理当中是按逻辑功能模块划分,用户编程更方便,程序的可读性更高。
1LOAD 1, [D] | <A>;//将分段D中A单元内的值读入寄存器1
2STORE 1, [X]| <B>;//将寄存器1的内容存入x分段的B单元中
由于各个分段按逻辑功能模块划分,并且上面的段名也是用户自己定义的,所以用户在读这段程序的时候可读性很高。
在用户编程的时候使用段名表示,而编译程序会将段名转换为与之对应的段号。CPU根据这些段号来区分各个段。
采用这种分段机制后逻辑地址变为:段号(段名)和段内地址(段内偏移量)所组成。
注意:段号的位数决定了每个进程最多可以分几个段,段内地址位数决定了每个段的最大长度是多少
在上图中,若系统是按字节寻址的,则段号占
而上面的汇编代码,在写程序时使用的段名[D]、[X] 会被编译程序翻译成对应段号。而像<A>
单元、<B>
单元会被编译程序翻译成段内地址。
程序分多个段,各段离散地装入内存,为了保证程序能正常运行,就必须能从物理内存中找到各个逻辑段的存放位置。为此,需为每个进程建立一张段映射表,简称"段表"。段表和之前的页表作用类似。
之前学习的页表是建立了各个逻辑页面到实际的物理页框之间的映射关系。而段表是记录了各个逻辑段到实际的五路内存存放位置之间的映射关系。可以看到每个段表由段号、段长、基址组成。
每个段对应一个段表项,其中记录了该段在内存中的起始位置(又称基址)和段的长度。
需要注意的是各个段表项的长度是相同的。例如:某系统按字节寻址,采用分段存储管理,逻辑地址结构为(段号
有以下一段汇编代码:
xxxxxxxxxx
11LOAD 1,[D]| <A>;//将分段D中A单元内的值读入寄存器1
经过编译程序编译后,形成等价的机器指令:取出段号为
CPU执行指令时需要将逻辑地址变换为物理地址。上面逻辑地址机器指令用二进制表示:
逻辑地址转换为物理地址详细过程:
在内存的系统区当中存放着很多用于管理系统当中软硬件的数据结构,包括进程控制模块PCB也在系统区当中。当一个进程要上处理机运行之前,进程切换相关的内核程序负责恢复进程运行环境,其中包括一个很重要的段表寄存器硬件。其值是进程的段表在进程当中的起始地址和段表长度。因此当进程还没有运行时,段表起始地址和段表长度是存放在进程的PCB当中,当进程上处理机运行时,这两个信息会放到速度很快的段表寄存器当中。
当知道段表的起始地址后,就可以知道段表存放在内存中哪个位置。接下来这个进程在运行过程中避免不了要访问一些逻辑地址,如访问逻辑地址
确定段号是合法的没有越界之后,就会根据段号
在找到了段号对应的段表项之后,系统还会对逻辑地址当中的段内地址
此时已经找到目标段的段表项,所以就知道目标段在内存中存放的位置了。最后根据这个段基址
假设逻辑地址<A>
存放的位置。
页是信息的物理单位。分页的主要目的是为了实现离散分配,提高内存利用率。分页仅仅是系统管理上的需要,完全是系统行为,对用户是不可见的。
段是信息的逻辑单位。分段的主要目的是更好地满足用户需求。一个段通常包含着一组属于一个逻辑模块的信息。分段对用户是可见的,用户编程时需要显式地给出段名。
页的大小固定且由系统决定。段的长度却不固定,决定于用户编写的程序。
从地址空间的角度来说,分页的用户进程地址空间是一维的,程序员只需给出一个记忆符即可表示一个地址。
如上图所示,用户可以用记忆符<A>
来表示某个页面当中的内存单元。
而用分段的用户进程地址空间是二维的,程序员在标识一个地址时,既要给出段名,也要给出段内地址。
如上图所示,用户需要显示给出段名[D]
和段内地址<A>
。因此在分页管理当中,在用户自己看来的进程地址空间是连续的。但是在分段存储管理当中用户知道自己的进程地址空间是被分为一个个段,并且每个段会占据一连串的连续地址空间。
分段比分页更容易实现信息的共享和保护。
假如有一个生产者进程(
假设当前的这个生产者进程有一个段表,它的
如果此时有一个消费者进程想要和生产者进程共享使用一号段,可以让消费者进程的段表项同样指向
所以,如果要实现共享,只需要让进程的某一个段表项指向同一个内存段即可。
而不能被修改的代码称为纯代码或可重入代码(不属于临界资源),这样的代码是可以共享的。可修改的代码是不能共享的(比如,有一个代码段中有很多变量,各进程并发地同时访问可能造成数据不一致)。假如,有一个代码段只是简单的输出"Hello World !",那么所有进程并发访问这段代码并没有问题。
而如果采用分页管理方式:
如果让消费者进程的某个页表项指向这个页面,显然不合理,因为这个页面中的橙色部分是不允许共享的,只有绿色部分可以。可以看出由于分页管理方式的页面不是按逻辑模块划分的。这就很难实现共享。
保护的原理也类似,对于分段式存储方式,可以在段表中增加一个"是否允许其他进程访问"的标记列即可。
而对于分页式管理,一块的内存块中有的可以被访问,有的不能被访问,这要是由于各个页面不是按逻辑划分造成的。因此很难用页表实现信息保护。
再来看看访问一个逻辑地址需要几次访存:
对于分页(单级页表):第一次访存,要查内存中的页表。第二次访存,访问目标内存单元。总共两次访存
分段:第一次访存,查内存中的段表。第二次访存,访问目标内存单元。总共两次访存。同时与分页系统类似,分段系统中也可以引入快表机构,将近期访问过的段表项放到快表中,这样可以少一次访问,加快地址变换速度。
基本分段式存储管理总结:
段页式管理方式是分段、分页这两种管理方式的结合。
分页、分段的优缺点分析:
分段管理中产生的外部碎片同样可以参考动态分区分配解决,所以也可以用"紧凑"来解决,只是需要付出较大的时间代价
为了解决上述缺点,可以采用段页式管理方式。
在采用段页式管理方式的系统当中,一个进程会按照逻辑模块进行分段,再将各段分页(如每个页面
对于内存来说,内存空间可以分为大小相同的内存块。每个内存块的大小和系统当中页面的大小是一样的,也就是
段页式系统的逻辑地址结构由段号、页号、页内地址(页内偏移量)组成。如:
这个地方的页号和页内偏移量就是,分段管理当中的段内地址进行再拆分的一个结果。需要注意的是:段号的位数决定了每个进程最多可以分几个段;页号位数决定了每个段最大有多少页;页内偏移量决定了页面大小、内存块大小是多少。
在上述例子中,若系统是按字节寻址的,则段号占
"分段"过程对用户是可见的,程序员编程时需要显式地给出段号、段内地址。而将各段"分页"的过程对用户是不可见的。系统会根据段内地址自动划分页号和页内偏移量。因此段页式管理的地址结构是二维的。
与之前的分页式和分段式管理类似,对进程分段再分页之后需要段表和页表。
所以进程先分段,分段后系统会为各个段建立一个段表。进程当中的各个段会对应段表中的各个段表项。而每个段表项由段号、页表长度、页表存放块号(页表起始地址)组成。每个段表项长度相等,段号是隐含的。
可以根据块号即可算出页表存放的内存地址。如
于是可以从
相应的这两个页面,会依次对应页表当中的一个页表项。每个页表项记录了每个页面存放的内存块号位置。
可以通过两个页号查询对应页表找到内存中的位置。
所以可以看到,段式管理当中的段表记录的式段号、段长度、段的起始地址。而段页式管理当中记录的是段号、页表长度、页表存放块号,这三个信息,即后面两个不一样。而对于页表来说结构式相同的,都是记录了页号到物理块号的映射关系。
从上面分析当中可以知道,一个进程只会对应一个段表,而一个段表会对应多个页表。
首先需要知道的是系统中也会有一个段表寄存器这个硬件。在进程上处理机运行之前会从PCB当中读出段表起始地址和段表长度这些信息。之后放到段表寄存器当中。在进行地址转换的时候第一步需要根据逻辑地址得到段号、页号、页内偏移量。第二步需要将段号和段表长度进行一个对比,检查段号是否越界。如果段号合法,接下来可以根据段号和段表起始地址来计算出这个段号对应的段表项在内存当中的位置。这样就找到了想要找的段表项。
接下里需要注意的是由于各个段的长度是不一样的,所以各个段分页之后可以分为数量不等的页面。所以这里需要对页号合法性进行检查,看是否越界。如果页号没有超出页表长度的话就可以继续往下执行。通过这个段表项知道页表存放的位置,于是就可以读出页表。之后就可以根据逻辑地址中的页号找到来找到相应的页表项。找到页表项之后就可以知道这个页面在内存中的存放位置。最后可以根据内存块号、页内偏移量得到最终的物理地址。
因此在段页式管理当中,进行地址转换的过程总共需要三次访存。第一次访存是访问内存当中的段表,第二次访存是访问内存当中的页表。第三次访存才是访问最终的目标内存单元。同样的也可引入快表机制,用段号和页号作为查谢快表的关键字。若快表命中则仅需一次访存。
段式存储管理总结:
在传统存储管理方式(连续分配
虚拟内存是基于高速缓存技术的思想提出的内存管理方案。传统存储方式如下:
系统使用的如果是上面的管理方案,很多暂时用不到的数据也会长期占用内存,导致内存利用率不高。所以传统的存储管理方式有两个很明显的缺点:
一次性:
作业必须一次性全部装入内存后才能开始运行。这会造成两个问题:
驻留性
一旦作业被装入内存,就会一直驻留在内存中,直至作业运行结束。事实上,在一个时间段内,只需要访问作业的一小部分数据即可正常运行,这就导致了内存中会驻留大量的、暂时用不到的数据,浪费了宝贵的内存资源。
这些缺点可以用虚拟存储技术解决问题。虚拟内存技术遵循局部性原理:
时间局部性:如果执行了程序中的某条指令,那么不久后这条指令很有可能再次执行。如果某个数据被访问过,不久之后该数据很可能再次被访问。(因为程序中存在大量的循环)。
空间局部性:一旦程序访问了某个存储单元,在不久之后,其附近的存储单元也很有可能被访问。(因为很多数据在内存中都是连续存放的,并且程序的指令也是顺序地在内存中存放的)。
基于局部性原理,在程序装入时,可以将程序中很快会用到的部分装入内存,暂时用不到的部分留在外存,就可以让程序开始执行。在程序执行过程中,当所访问的信息不在内存时,由操作系统负责将所需信息从外存调入内存,然后继续执行程序。若内存空间不够,由操作系统负责将内存中暂时用不到的信息换出到外存。
在操作系统的管理下,在用户看来似乎有一个比实际内存大得多的内存,这就是虚拟内存。
虚拟内存是操作系统虚拟性的一个体现,实际的物理内存大小没有变,只是在逻辑上进行了扩充。
虚拟内存有一下三个主要特征:
多次性:无需在作业运行时-次性全部装入内存,而是允许被分成多次调入内存。
对换性:在作业运行时无需一直常驻内存,而是允许在作业运行过程中,将作业换入、换出。
虚拟性:从逻辑上扩充了内存的容量,使用户看到的内存容量,远大于实际的容量。
虚拟内存技术,允许一个作业分多次调入内存。如果采用连续分配方式,会不方便实现。因此,虚拟内存的实现需要建立在离散分配的内存管理方式基础上,即传统的非连续分配存储管理技术:
在传统的非连续分配存储的三种管理技术之上运用虚拟内存技术,就形成了与之相对应的请求分页存储管理、请求分段存储管理、请求段页式存储管理。
传统内存管理方式和虚拟内存的管理方式最主要区别是在采用虚拟内存技术,程序在执行的过程中,当所访问的信息不在内存时,由操作系统负责将所需信息从外存调入内存,然后继续执行程序。若内存空间不够,由操作系统负责将内存中暂时用不到的信息换出到外存。
所以为了满足这两个全新的需求,操作系统需要在基本的存储管理方式的基础上再增加两个主要的功能:
请求调页(请求调段)功能
是指在请求分页存储管理当中,如果所需要的页面暂时还不在内存当中,那么操作系统需要负责把这个页面从外存调入内存,并且完善一系列的处理。
页面置换(或段置换)功能
当内存空间不够时,操作系统需要通过置换功能把暂时用不到的分页(分段)先换出到外存当中。
虚拟内存的基本概念总结:
请求分页存储管理是在基本分页存储管理方式的基础上进行拓展,从而实现的一种虚拟内存管理技术。
程序在执行的过程中,当所访问的信息不在内存时,由操作系统负责将所需信息从外存调入内存,然后继续执行程序。若内存空间不够,由操作系统负责将内存中暂时用不到的信息换出到外存。
所以为了满足这两个全新的需求,操作系统需要在基本的存储管理方式的基础上再增加两个主要的功能:
请求调页(请求调段)功能
是指在请求分页存储管理当中,如果所需要的页面暂时还不在内存当中,那么操作系统需要负责把这个页面从外存调入内存,并且完善一系列的处理。
页面置换(或段置换)功能
当内存空间不够时,操作系统需要通过置换功能把暂时用不到的分页(分段)先换出到外存当中。
针对这两个功能如何实现会介绍请求管理方式中页表机制和基本分页存储管理方式种的区别。另外为了实现请求调页功能请求分页管理系统引入了缺页中断机构。
与基本分页管理相比,请求分页管理中,为了实现"请求调页",操作系统需要知道每个页面是否已经调入内存。如果还没调入,那么也需要知道该页面在外存中存放的位置。所以为了知道这些信息,肯定需要把这些信息记录在页表这种数据结构中。
另外当内存空间不够时,要实现"页面置换",操作系统需要通过某些指标来决定到底换出哪个页面。有的页面没有被修改过,就不用再浪费时间写回外存。有的页面修改过,就需要将外存中的旧数据覆盖,因此,操作系统也需要记录各个页面是否被修改的信息。
所以相对于基本分页存储管理的页表来说,请求分页存储管理的页表增加了:
状态位
表示这个页面是不是已经调入内存了
访问字段
操作系统在置换页面的时候可以根据访问字段的数据来决定到底要换出哪个页面。所以可以在访问字段当中记录每个页面最近被访问过几次。可以选择把访问次数更少的换出外存。也可以记录上次访问时间,这样就可以换出很久没有访问的页面。
修改位
用来标记这个页面在调入内存后是否被修改过。如果没有修改,那么就不需要写回内存可以节省时间。
外存地址
各个页面在外存中存放的地址
例子:假设此时要访问逻辑地址
为了访问这个逻辑地址需要查询页表,缺页中断机构会根据对应的页表项来判断此时这个页面是否已经在内存中。如果没有在内存当中,即状态位为
如果内存中空闲块,如上图的
这是第一种情况,就是有空闲内存块的情况。下面是第二种情况,即无空闲内存块的情况:
如果内存无内存块,需要用页面置换算法,通过某种规则来选择要淘汰一个页面。如页面置换算法选中了要置换
缺页中断是因为当前执行的指令想要访问的目标页面未调入内存而产生的,因此属于内中断。内外中断分类如下:
另外需要注意的是一条指令在执行期间,可能产生多次缺页中断。(如:copy A to B
,即将逻辑地址
请求分页存储管理与基本分页存储管理的在地址变换时候需要新增哪些步骤:
请求调页( 查到页表项时进行判断)
在查找到页面对应的页表项时候,一定需要对页面是否在内存这个状态进行判断。
页面置换(需要调入页面,但没有空闲内存块时进行)
在地址变换过程中,如果发现此时想要访问的页面暂时没有调入到内存,但此时内存中没有空闲的内存块时,那么在这个地址变化的过程中也需要进行页面置换的工作。
要修改请求页表中新增的表项
当页面调入、调出或者被访问时,需要对与之对应的页表项进行数据修改。
请求分页管理方式执行过程与之前的基本分页管理类似:
需要注意的是:在找到对应页表项后,若对应页面未调入内存,则产生缺页中断,之后由操作系统的缺页中断处理程序进行处理。
另外快表中有的页面一定是在内存中的。若某个页面被换出外存,则快表中的相应表项也要删除,否则可能访问错误的页面。
执行过程流程图如下:
补充细节:
①中只有"写指令"才需要修改修改位。并且,一般来说只需修改快表中的数据,只有要将快表项删除时才需要写回内存中的慢表。这样可以减少访存次数。
②和普通的中断处理一样,缺页中断处理依然需要保留CPU现场。
③需要用某种"页面置换算法"来决定一个换出页面(下节内容)
④换入
⑤页面调入内存后,需要修改慢表,同时也需要将表项复制到快表中
在具有快表机构的请求分页系统中,访问一个逻辑地址时,若发生缺页,则地址变换步骤是:
查快表(未命中)
请求分页管理方式总结:
通过之前的学习可以知道,在请求分页存储管理当中如果内存空间不够的话,操作系统会负责将内存中用不到信息换出到外存。
而页面置换算法就是由于选择到底要把哪个页面换出到外存。通过之前的学习知道页面的换入、换出需要磁盘
页面置换算法有五种:最佳置换算法(OPT)、先进先出置换算法(FIFO)、最近最久未使用置换算法(LRU)、时钟置换算法(CLOCK)、改进型的时钟置换算法。
最佳置换算法(OPT,Optimal):即每次选择淘汰的页面将是以后永不使用,或者在最长时间内不再被访问的页面,这样可以保证最低的缺页率。
例:假设系统为某进程分配了三个内存块,并考虑到有一下页面号引用串(会依次访问这些页面):
由于有三个内存块,所以前三次访问直接将不同的页面放入内存块即可。第四次访问
所以整个过程缺页中断发生了
注意:缺页时未必发生页面置换。若还有可用的空闲内存块,就不用进行页面置换。
综上所述最佳置换算法可以保证最低的缺页率,但实际上,只有在进程执行的过程中才能知道接下来会访问到的是哪个页面。操作系统无法提前预判页面访问序列。因此,最佳置换算法是无法实现的。
先进先出置换算法(FIFO):每次选择淘汰的页面是最早进入内存的页面。
实现方法:把调入内存的页面根据调入的先后顺序排成一个队列,需要换出页面时选择队头页面即可。队列的最大长度取决于系统为进程分配了多少个内存块。
例:假设系统为某进程分配了三个内存块,并考虑到有以下页面号引用串:
由于有三个内存块,所以前三次访问直接将不同的页面放入内存块即可,同时将放入内存块的页面放在队列的队尾。
第四次访问
之后要访问的页面替换原理类似。可以看出分配三个内存块时,缺页次数为
如果修改一下题目,加入系统为某个进程分配的是四个内存块。则访问情况如下:
可以看出分配三个内存块时,缺页次数为
只有FIFO算法会产生Belady异常。另外,FIFO算法虽然实现简单,但是该算法与进程实际运行时的规律不适应,因为先进入的页面也有可能最经常被访问。因此,算法性能差。
最近最久未使用置换算法(LRU,leastrecentlyused):每次淘汰的页面是最近最久未使用的页面。
实现方法:赋予每个页面对应的页表项中,用访问字段记录该页面自上次被访问以来所经历的时间
例:假设系统为某进程分配了四个内存块,并考虑到有以下页面号引用串
由于有四个内存块,所以前几次访问直接将不同的页面放入内存块即可。直到访问
后面的
该算法的实现需要专门的硬件支持,虽然算法性能好,但是实现困难,开销大
最佳置换算法性能最好,但无法实现。先进先出置换算法实现简单,但算法性能差,会出现Belady异常。最近最久未使用置换算法性能好,是最接近OPT算法性能的,但是实现起来需要专门的硬件支持,算法开销大。
时钟置换算法是一种性能和开销较均衡的算法,又称CLOCK算法,或最近未用算法(NRU,Not Recently Used)。
简单的CLOCK算法实现方法:为每个页面设置一个访问位,再将内存中的页面都通过链接指针链接成一个循环队列。当某页被访问时,其访问位置为
例:假设系统为某进程分配了五个内存块,并考虑到有以下页面号引用串:
由于有五个内存块,所以前五个访问的页号
在访问到
进行第二轮扫描时,
之后的访问的替换原理类似。
简单的时钟置换算法仅考虑到一个页面最近是否被访问过。事实上,如果被淘汰的页面没有被修改过,就不需要执行
实现方法:增加一个修改位。修改位
为方便讨论,用(访问位,修改位)的形式表示各页面状态。如(1,1)表示一个页面近期被访问过,且被修改过。
规则:
第一轮:从当前位置开始扫描到第一个
第二轮:若第一轮扫描失败,则重新扫描,查找第一个
第三轮:若第二轮扫描失败,则重新扫描,查找第一个
第四轮:若第三轮扫描失败,则重新扫描,查找第一个
由于第二轮已将所有帧的访问位设为
例:假设系统为某进程分配了五个内存块。内存中五个页面会通过链接指针的方式链接成一个循环队列。
此时如果要淘汰一个页面,需要从队列的队头位置
根据算法规则,第一轮扫描只需要找到访问位和修改位都为
接着假设循环队列如下所示
如果要淘汰一个页面,需要从队列的队头位置
所以进行第二轮查找。第二轮查找第一个
综上所述,替换的优先级是:
第一优先级:最近没访问,且没修改的页面
第二优先级:最近没访问,但修改过的页面
第三优先级:最近访问过,但没修改的页面
第四优先级:最近访问过,且修改过的页面
页面置换算法总结:
首先要了解驻留集:指请求分页存储管理中给进程分配的物理块的集合。
在采用了虚拟存储技术的系统中,驻留集大小一般小于进程的总大小。考虑一个极端情况,若某进程共有
所以若驻留集太小,会导致缺页频繁,系统要花大量的时间来处理缺页,实际用于进程推进的时间很少。而驻留集太大,权会导致多道程序并发度下降,资源利用率降低。所以应该选择一个合适的驻留集大小。
针对驻留集大小是否可变问题,提出了两种分配策略:
另外当页面置换的时候,页面置换的范围是什么,根据这个问题提出了置换范围策略:
将两种分配和置换策略两两结合,可以得到:固定分配局部置换、可变分配局部置换和可变分配全局置换。
没有固定分配全局置换的概念。因为全局置换意味着一个进程拥有的物理块数量必然会改变,因此不可能是固定分配。
三种策略介绍:
固定分配局部置换
系统为每个进程分配一定数量的物理块,在整个运行期间都不改变。若进程在运行中发生缺页,则只能从该进程在内存中的页面中选出一-页换出,然后再调入需要的页面。
这种策略的缺点是:很难在刚开始就确定应为每个进程分配多少个物理块才算合理。(采用这种策略的系统可以根据进程大小、优先级、或是根据程序员给出的参数来确定为一个进程分配的内存块数)。因此灵活性差。
可变分配全局置换
刚开始会为每个进程分配一定数量的物理块。操作系统会保持一个空闲物理块队列。当某进程发生缺页时,从空闲物理块中取出一块分配给该进程;若已无空闲物理块,则可选择一个未锁定的页面换出外存,再将该物理块分配给缺页的进程。
可以看出采用这种策略时,只要某进程发生缺页,都将获得新的物理块,仅当空闲物理块用完时,系统才选择一个未锁定的页面调出。被选择调出的页可能是系统中任何一个进程中的页,因此这个被选中的进程拥有的物理块会减少,缺页率会增加。
上面未锁定的页面是指系统会锁定一些页面,这些页面中的内容不能置换出外存(如:重要的内核数据可以设为"锁定")
显然只要进程缺页就分配一个新的物理块,这种方式也不太合理。
可变分配局部置换
刚开始会为每个进程分配一定数量的物理块。当某进程发生缺页时,只允许从该进程自己的物理块中选出一个进行换出外存。如果进程在运行中频繁地缺页,系统会为该进程多分配几个物理块,直至该进程缺页率趋势适当程度。反之,如果进程在运行中缺页率特别低,则可适当减少分配给该进程的物理块。
这样就可以让系统的多道程序并发度,也保持在一个相对理想的位置。
所以可变分配全局置换:只要缺页就给分配新物理块。可变分配局部置换:要根据发生缺页的频率来动态地增加或减少进程的物理块。
在什么要调入所需要的页面:
预调页策略
根据局部性原理(特别是空间局部性),一次调入若干个相邻的页面可能比一次调入一个页面更高效。但如果提前调入的页面中大多数都没被访问过,则又是低效的。因此可以预测不久之后可能访问到的页面,将它们预先调入内存,但目前预测成功率只有
所以该策略是在运行前就进行调入的。
请求调页策略
进程在运行期间发现缺页时才将所缺页面调入内存。由这种策略调入的页面一定会被访问到,但由于每次只能调入一页,而每次调页都要磁盘
所以该策略是在运行时就进行调入的。
从什么地方调入页面:
之前介绍过磁盘中存储区域分为对换区和文件区两个部分。其中对换区采用连续分配的方式,读写速度更快。而文件区采用的是离散分配方式,读写速度较慢。
方式一:由于对换区读写速度更快,如果系统拥有足够的对换区空间:页面的调入、调出都是在内存与对换区之间进行,这样可以保证页面的调入、调出速度很快。在进程运行前,需将进程相关的数据从文件区复制到对换区。之后再调入内存中,如果内存已满,会将内存中的页面调出到对换区。
方式二:如果系统中缺少足够的对换区空间,凡是不会被修改的数据都直接从文件区调入内存,由于这些页面不会被修改,因此换出时不必写回磁盘,下次需要时再从文件区调入即可。对于可能被修改的部分,换出时需写回磁盘对换区,下次需要时再从对换区调入。
方式三:UNIX方式:运行之前进程有关的数据全部放在文件区,故未使用过的页面,第一次都会从文件区调入内存。若内存不够,使用过的页面需要换出,则写回对换区,下次需要时再从对换区调入。
又称颠簸现象。指刚刚换出的页面马上又要换入内存,刚刚换入的页面马上又要换出外存,这种频繁的页面调度行为。产生抖动的主要原因是进程频繁访问的页面数目高于可用的物理块数(分配给进程的物理块不够)。
所以如果为进程分配的物理块太少,会使进程发生抖动现象。为进程分配的物理块太多,又会降低系统整体的并发度,降低某些资源的利用率。所以为进程分配多少物理块是值得思考的问题。
为了研究为应该为每个进程分配多少个物理块,Denning 提出了进程"工作集"的概念。
工作集:指在某段时间间隔里,进程实际访问页面的集合。
操作系统会根据"窗口尺寸"来算出工作集。例:某进程的页面访问序列如下,窗口尺寸为
由于窗口尺寸的大小是
所以可以看出工作集大小可能小于窗口尺寸,实际应用中,操作系统可以统计进程的工作集大小,根据工作集大小给进程分配若干内存块。如:窗口尺寸为
一般来说, 驻留集大小不能小于工作集大小,否则进程运行过程中将频繁缺页。
拓展:基于局部性原理可知,进程在一段时间内访问的页面与不久之后会访问的页面是有相关性的。因此,可以根据进程近期访问的页面集合(工作集)来设计一种页面置换算法:如果一个进程需要置换出某个页面,可以选择一个不在工作集中的页面进行淘汰。
页面分配策略总结:
内存映射文件指的是操作系统向上层程序员提供的功能(系统调用)。通过这个功能好处:
开发人员可以很方便访问文件数据。
传统的访问方式是:如果当前有一个文件需要被放入内存,这个文件会被拆分成几个大小相等的块,每个块刚好可以放入磁盘块中。这些块有可能被离散存放再磁盘的各个位置。
如果一个进程想要访问数据,传统做法是首先每个进程都有自己的虚拟地址空间,如果一个进程想要访问这个文件的数据:
而采用内存映射方式,那么一个进程访问文件的方式将变得简单:
首先使用open系统调用,指明打开这个文件
接着使用mmap系统调用,让操作系统把文件映射到进程的虚拟地址空间当中。
这个系统调用会返回一个指针,这个指针会指向刚才映射区域的起始地址。
接着就可以以访问内存的方式访问文件数据
如C语言中给一个指针,可以通过指针再加上某一个地址的偏移量,去访问这个指针后面的某些区域。所以只要mmap返回一个指针,那么就可以访问这个文件的任何数据。
注意这里操作系统只是建立了文件数据和内存间的一个映射关系,但并没有把文件数据直接读入内存。这就相当于一个缺页的状态。
如果要访问第一块数据,那么操作系统会发现这块数据还没有调入主存(缺页),此时操作系统会自动把这一块的数据读入内存中。所以开发人员不需要再主动调用read函数,读入数据的过程由操作系统自动完成。
加入此时要修改
因此可以看到采用内存映射文件之后开发人员对文件数据的访问都方便多了。只需要直到文件在内存当中的起始地址,接着按照访问内存的方式去访问这个文件当中的数据即可。文件数据的读入和写出都是由操作系统自动完成的。
方便多个进程共享同一个文件
通过上面介绍可以直到,一个文件可以通过系统调用方式映射到自己的虚拟地址空间中。同样的道理,另一个进程也可以把这个文件映射到自己的虚拟地址空间中。此时两个进程的虚拟地址空间是相互独立的,但是操作系统会把这两块虚拟地址空间映射到相同的物理内存上。
在物理内存中,一个文件对应同一份数据,当一个进程修改文件数据时,另一个进程可以立马"看到"。
内存映射文件总结:
文件就是一组有意义的信息
一个文件有以下属性:
文件名
由创建文件的用户决定文件名,主要是为了方便用户找到文件,同一目录下不允许有重名文件。
标识符
文件并不能确定唯一文件。所以一个系统内的各文件标识符是唯一确定的,对用户来说毫无可读性,因此标识符只是操作系统用于区分各个文件的一种内部名称。
类型
指明文件的类型。指明文件的类型有很多好处,如操作系统可以为不同类型的文件设置默认打开方式。
位置
文件存放的路径(让用户使用)、同时操作系统还需要关心在外存中的地址(操作系统使用,对用户不可见)
大小
指明文件大小
创建时间
上次修改时间及文件所有者信息
保护信息
对文件进行保护的访问控制信息(如:可读可写等操作)
文件内部的数据组织方式:
无结构文件(如文本文件)
由一些二进制或字符流组成,又称"流式文件"。
有结构文件(如数据库表):
由一组相似的记录组成,又称"记录式文件"。
记录是一组相干的数据项集合。
文件组织结构:
这些记录如何组织的问题,如:应该使用顺序存放、还是索引表来表示记录间的顺序。这是文件的逻辑结构重点要探讨的问题。所以文件的逻辑结构要探讨的问题其实就是文件内部这些记录,这些数据应该被怎么组织起来的问题。
文件之间的组织方式:
这种平时常见的文件组织形式是树状的组织形式。这里所谓的目录就是文件夹。用户可以自己创建一层层的目录,各层目录中存放相应的文件。系统中的各个文件就通过一层一层的目录合理有序的组织起来了。目录其实也是一种特殊的有结构文件(由记录组成),如何实现文件目录是之后会重点探讨的问题。
操作系统向上提供的功能:
创建文件
可以"创建文件",(点击新建后,图形化交互进程在背后调用了create系统调用。
读文件
可以"读文件",将文件数据读入内存,才能让CPU处理(双击后,"记事本"应用程序通过操作系统提供的"读文件"功能,即read系统调用,将文件数据从外存读入内存,并显示在屏幕上)。
写文件
可以"写文件",将更改过的文件数据写回外存(我们在"记事本"应用程序中编辑文件内容,点击"保存"后,"记事本"应用程序通过操作系统提供的"写文件"功能,即write系统调用,将文件数据从内存写回外存)
删除文件
可以"删除文件"(点了"删除"之后,图形化交互进程通过操作系统提供的"删除文件"功能,即delete系统调用,将文件数据从外存中删除)
可用几个基本操作完成更复杂的操作,比如:"复制文件",可以先创建一个新的空文件,再把源文件读入内存,再将内存中的数据写到新文件中。
文件如何存放在外存:
与内存一样,外存也是由一个个存储单元组成的,每个存储单元可以存储一定量的数据(如
类似于内存分为一个个"内存块",外存会分为一个个"块
操作系统以"块"为单位为文件分配存储空间,因此即使一个文件大小只有
所以其实文件的物理结构探讨的是文件这些数据在物理上应该是怎么存放怎么组织的问题。而上面提到的文件的逻辑结构是文件的各个记录在逻辑上应该是什么样的组织关系问题。
其他需要由操作系统实现的文件管理功能:
初始文件管理总结:
所谓的"逻辑结构"就是指在用户看来,文件内部的数据应该是如何组织起来的。而"物理结构"指的是在操作系统看来,文件的数据是如何存放在外存中的。文件逻辑结构可以分为无结构文件和有结构文件。有结构文件逻辑结构是:顺序文件、索引文件和索引顺序文件。
算法的具体实现与逻辑结构、物理结构都有关(文件也一样,文件操作的具体实现与文件的逻辑结构、物理结构都有关)
无结构文件:文件内部的数据就是一系列二进制流或字符流组成。又称"流式文件"。如:Windows操作系统中的.txt
文件。
文件内部的数据其实就是一系列字符流,没有明显的结构特性。因此也不用探讨无结构文件的"逻辑结构"问题。所以重点探讨有结构文件。
有结构文件:由一组相似的记录组成,又称"记录式文件"。每条记录又若干个数据项组成。如:数据库表文件。一般来说,每条记录有一个数据项可作为关键字(作为识别不同记录的ID)。
上面例子中,"学号"即可作为各个记录的关键字。每个学生对应一条记录,每条记录由若干个数据项组成。而根据各条记录的长度(占用的存储空间)是否相等,又可分为定长记录和可变长记录两种。
如上图所示,学号、姓名和性别都是定长记录。而特长长度不确定,所以是可变长记录。
之前例子中专业可以给
接下来会讨论这些记录应该在逻辑上怎么被组织起来的问题。根据有结构文件中的各条记录在逻辑上如何组织,可以分为三类:顺序文件、索引文件和索引顺序文件。
顺序文件:文件中的记录一个接一个地顺序排列(逻辑上),记录可以是定长的或可变长的。各个记录在物理上可以顺序存储或链式存储。
如果是顺序存储,那么逻辑上相邻的记录物理上也相邻(类似于顺序表)
而如果采用链式存储结构,逻辑上相邻的记录物理上不一定相邻(类似于链表)
根据记录是否按照关键字顺序排列,又可以把顺序文件分为:串结构和顺序结构。
串结构
记录之间的顺序与关键字无关。通常按照记录存入的时间决定记录的顺序。
顺序结构
记录之间的顺序按关键字顺序排列
显然记录是定长的还是可变长的,记录是否按照关键字有序排列,另外这些记录在物理上是顺序存储还是链式存储,所有的区别都能影响顺序文件能不能实现一些操作的功能。
假设:已经知道了文件的起始地址(也就是第一个记录存放的位置)
思考一:能否快速找到第
思考二:能否快速找到某个关键字对应的记录存放的位置?
如果一个顺序文件采用链式存储方式,那么无论是定长
如果采用顺序存储方式,且是可变长记录,无法实现随机存取。每次只能从第一个记录开始依次往后查找。
由于文件的记录是可变长的,所以必须要在每个记录之前用一定的存储空间来表示这个记录的长度。假设用
由于记录长度不同,所以并不会呈现出规律性,所以只能从第一条开始往后找。
如果采用顺序存储方式,且是定长记录,则可实现随机存取。记录长度为
结论:定长记录的顺序文件,若物理上采用顺序存储,则可实现随机存取。若能再保证记录的顺序结构,则可实现快速检索(即根据关键字快速找到对应记录)。
注意:一般来说,考试题目中所说的"顺序文件"指的是物理上顺序存储的顺序文件,而不是链式存储。之后的讲解中提到的顺序文件也默认如此。并且顺序文件的缺点是:增加
在实际应用过程中,为了减少
对于可变长记录文件,要找到第
基于这种需求提出索引文件这种逻辑结构。每个文件会建立一张索引表,并且索引表的表项会对应文件的一条记录。文件的记录在物理内存中可以离散存放,但是索引表的表项在物理上需要连续存放。
另外每一个索引表的表项大小都是相等的。如:索引号、长度、指针都占
所以索引表本身是定长记录的顺序文件。因此可以快速找到第
可将关键字作为索引号内容,若按关键字顺序排列,则还可以支持按照关键字折半查找。每当要增加
另外,可以用不同的数据项建立多个索引表。如:学生信息表中,可用关键字"学号"建立一张索引表。也可用"姓名"建立一张索引表。这样就可以根据"姓名"快速地检索文件了。(如:SQL就支持根据某个数据项建立索引的功能)
索引文件的缺点:每个记录对应一个索引表项,因此索引表可能会很大。比如:文件的每个记录平均只占
索引顺序文件是索引文件和顺序文件思想的结合。索引顺序文件中,同样会为文件建立张索引表,但不同的是:并不是每个记录对应一个索引表项,而是一组记录对应一个索引表项。
在上图中,学生记录按照学生姓名的开头字母进行分组。每个分组就是一个顺序文件,且每一个分组对应一个索引顺序文件的索引项,分组内的记录不需要按关键字排序。
每个索引项记录了名字和存放的位置。索引顺序文件的索引项也不需要按关键字顺序排列,这样可以极大地方便新表项的插入。
也就是说索引顺序文件的索引表其实是一个定长记录的串结构的顺序文件。可以看到索引表的表项少了很多。
若一个顺序文件有
若采用索引顺序文件结构,可把
同理,若文件共有
为了进一步提高检索效率,可以为顺序文件建立多级索引表。例如,对于一个含
此时,检索一个记录,平均检索顶级索引表
有结构文件总结:
层级目录结构(Windows资源管理器),文件之间的组织结构清晰,易于查找。编程时也可以很方便的用文件路径找到一个文件。如:FILE *fp;fp=fopen("F:\data\myfile.dat");
用户可以轻松实现"按名存取"。
从操作系统角度来探讨这些目录结构应该如何实现。所谓的文件目录就是熟悉的Windows操作系统的文件夹。要实现文件目录的功能需要有很关键的数据结构"文件控制块"。另外随着计算机的发展,目录的结构也出现了不同的变化。对文件控制块的优化也很重要,所以还要了解索引结点知识。
对于某个根目录来说,对应的目录文件如下:
其实就是用上图的目录表来表示根目录下存放哪些文件。所以每一个文件夹,每一个文件都会对应一个表项。故目录本身就是一种有结构文件,由一条条记录组成。每条记录对应一个在该放在该目录下的文件。
注意上图目录表中还保存了文件的物理位置。假如当我们双击"照片"后,操作系统会在这个目录表中找到关键字"照片"对应的目录项(也就是记录),然后从外存中将"照片"目录的信息读入内存,于是,"照片"目录中的内容就可以显示出来了。
目录文件中的一条记录就是一个"文件控制块(FCB)"。FCB的有序集合称为"文件目录",一个FCB就是一个文件目录项。同时由上图还可以知道FCB中包含了文件的基本信息(文件名、物理地址、逻辑结构、物理结构等),存取控制信息( 是否可读
对文件控制块的操作:
文件控制块的有序集合就组成了文件的目录。
早期操作系统并不支持多级目录,整个系统中只建立一张目录表,每个文件占一个目录项。
单级目录实现了"按名存取",但是不允许文件重名。
在创建一个文件时,需要先检查目录表中有没有重名文件,确定不重名后才能允许建立文件,并将新文件对应的目录项插入目录表中,显然,单级目录结构不适用于多用户操作系统。
为了解决单级目录结构不适用于多用户操作系统的问题,后来提出了两级目录结构。
早期的多用户操作系统,采用两级目录结构。分为主文件目录(MFD,Master File Directory)和用户文件目录(UFD,User Flie Directory)。
一个主文件目录记录用户名及相应用户文件目录的存放位置,而用户文件目录由该用户的文件FCB组成。
由于不同的用户文件是存放在不同的用户文件目录下,所以允许不同用户的文件重名。文件名虽然相同,但是对应的其实是不同的文件。
两级目录结构允许不同用户的文件重名,也可以在目录上实现实现访问限制(检查此时登录的用户名是否匹配,用户
但是这种结构缺点就是不可以把自己的文件进行分类。
多级目录结构又称树形目录结构。这是现在常用的目录结构。
每个目录下可以有更低一级别的目录,同时在各个目录下面还有一些文件。并且不同目录下的文件是可以重名的。
用户(或用户进程)要访问某个文件时要用文件路径名标识文件,文件路径名是个字符串。各级目录之间用"1"隔开。从根目录出发的路径称为绝对路径。例如:自拍.jpg
的绝对路径是/照片/2015-08/自拍jpg
。
系统根据绝对路径一层一层地找到下一级目录。刚开始从外存读入根目录的目录表。找到"照片"目录的存放位置后,从外存读入对应的目录表。再找到"2015-08"目录的存放位置,再从外存读入对应目录表。最后才找到文件"自拍.jpg"的存放位置。整个过程需要
很多时候,用户会连续访问同一目录内的多个文件(比如:接连查看2015-08
目录内的多个照片文件),显然,每次都从根目录开始查找,是很低效的。因此可以设置一个"当前目录"。例如:此时已经打开了照片
的目录文件,也就是说,这张目录表已调入内存,那么可以把它设置为当前目录
。当用户想要访问某个文件时,可以使用从当前目录出发的相对路径。
在Linux中,"."表示当前目录,因此如果照片
是当前目录,则自拍.jpg
的相对路径为:./2015-08/自拍.jpg
。从当前路径出发,只需要查询内存中的照片
目录表,即可知道2015-08
目录表的存放位置,从外存调入该目录,即可知道自拍.jpg
存放的位置了。所以只用访存一次
可见,引入"当前目录"和"相对路径"后,磁盘
无环图目录结构在树形目录结构的基础上,增加一些指向同一节点的有向边,使整个目录成为一个有向无环图。可以更方便地实现多个用户间的文件共享。
可以用不同的文件名指向同一个文件,甚至可以指向同一个目录(共享同一目录下的所有内容)。在引入共享目录之后对于文件的删除就不能向之前一样简答,只要一个文件有可能是被多个用户同时使用。当一个用户提出删除文件时候还需要判断这个文件当前是否有其他用户正在访问。所以需要为每个共享结点设置一个共享计数器,用于记录此时有多少个地方在共享该结点。用户提出删除结点的请求时,只是删除该用户的FCB、并使共享计数器减
如上图所示两个用户User1
和User2
同时访问一个文件,所以当前这个文件的共享计数器User1
用户提出删除该文件,操作系统会把该文件的目录项删除,并让共享计数器
只有共享计数器减为
注意:共享文件不同于复制文件。在共享文件中,由于各用户指向的是同一个文件,因此只要其中一个用户修改了文件数据,那么所有用户都可以看到文件数据的变化。如果是某个用户复制的共享文件,那么该用户修改副本文件,并不会影响原本的共享文件。
是对FCB的改进。通过之前的学习知道,由一些列的文件控制块组成了一个一个文件目录,但是操作系统在查找各级目录的过程中只需要使用文件名这个信息就可以,其他信息就暂时不需要。只有当文件匹配的时候才会去关心文件的存放位置。所以可以对目录表进行一个简化,来提升搜索的效率。
因为按照文件名来搜索目录的时候,并不需要关心除了文件名之外其他的信息,因此可以把其他信息放到索引结点当中。即除了文件名 之外的文件描述信息都放到索引结点中。每一个文件都会有一个唯一的索引结点。
而采用了索引结点机制后目录中只包含文件名和索引结点指针,这个目录表所需要占用的空间会小很多。
假设一个FCB是
但若使用索引结点机制,文件名占
当找到文件名对应的目录项时,才需要将索引结点调入内存,索引结点中记录了文件的各种信息,包括文件在外存中的存放位置,根据"存放位置"即可找到文件。而存放在外存中的索引结点称为"磁盘索引结点",当索引结点放入内存后称为"内存索引结点"。相比之下内存索引结点中需要增加一些信息,比如:文件是否被修改、此时有几个进程正在访问该文件等。
文件目录总结:
由之前的学习知道操作系统作为最接近硬件的软件层次,需要对硬件进行管理,包括外存(磁盘)。操作系统对磁盘的管理主要是对非空闲磁盘块进行管理(存放文件数据的磁盘块)、对空闲磁盘块的管理。对非空闲磁盘块的管理是探讨文件的物理结构
这里重点介绍文件的物理结构(文件分配方式)。也即文件数据应该怎样存放在外存中。分为三种方式:连续分配、链接分配(进一步细分为:隐式链接、显式链接)、索引分配。
补充:在内存管理中,进程的逻辑地址空间被分为一个一个页面。同样的,在外存管理中,为了方便对文件数据的管理,文件的逻辑地址空间也被分为了一个一个的文件"块"。于是文件的逻辑地址也可以表示为(逻辑块号,块内地址)的形式。若块的大小是
文件的连续分配方式要求每个文件在磁盘上占有一组连续的块。
如上图所示,文件aaa
被分为
用户通过逻辑地址来操作自己的文件,操作系统负责实现从逻辑地址转换为物理地址的映射方式。为了实现这个地址映射的功能,在文件的目录表中必须记录两个文件属性:文件的起始块号和文件的长度。
如文件aaa
,它的起始块号是物理内存中的aaa
文件。
所以用户给出要访问的逻辑块号,操作系统找到该文件对应的目录项(FCB),之后物理块号aaa
文件逻辑块号为aaa
文件的起始地址
可以直接算出逻辑块号对应的物理块号,因此连续分配支持顺序访问和直接访问(即随机访问)。
另外磁盘读取某个磁盘块时,需要移动磁头。访问的两个磁盘块相隔越远,移动磁头所需时间就越长。而采用连续分配方式,文件的块在物理内存上是连续存放的,所以访问一个文件移动的磁头所需时间就很短。
结论:连续分配的文件在顺序读
如上图,若此时文件
结论:物理上采用连续分配的文件不方便拓展。
若橙色区域为非空闲块,绿色区域为空闲磁盘块。
若此时创建的新文件大小为
结论:物理上采用连续分配,存储空间利用率低,会产生难以利用的磁盘碎片可以用紧凑来处理碎片,但是需要耗费很大的时间代价。
总结:
文件的链接分配采取离散分配的方式,可以为文件分配离徽的磁盘块。分为隐式链接和显式链接两种。
如果采用这种方式,在文件的目录项,也就是文件对应的FCB中需要记录这个文件的起始块号和结束块号。另外各个块之间都会一定的存储空间存储指向下一块的链接指针,当然最后一个块是没有链接指针的。
这些链接对于用户来说是透明的。采用这种方式实现文件的逻辑块号到物理块号的转变方法如下:
用户给出要访问的逻辑块号
结论:采用链式分配(隐式链接)方式的文件,只支持顺序访问,不支持随机访问,查找效率低。另外,指向下一个盘块的指针也需要耗费少量的存储空间。
采用这种方式很适合拓展文件:
假设此时这个文件aaa
需要拓展,也就是需要再分一个新的磁盘块,由于这些文件的块是离散分配的,因此只需要从文件空闲地方随便找一个空闲的位置,再将这个空闲块写入数据后挂到aaa
文件链尾即可。假如这里挑选空闲块号是
结论:采用隐式链接的链接分配方式,很方便文件拓展。另外,所有的空闲磁盘块都可以被利用,不会有碎片问题,外存利用率高。
总结:
把用于链接文件各物理块的指针显式地存放在一张表中。即文件分配表(FAT,File Allocntion Table)。
磁盘当中的各个块的先后顺序都是统一记录在文件分配表当中的。所以一个磁盘有多少个块在文件分配表(FAT)中就相对应的有多少个表项。
假设某个新创建的文件aaa
依次存放在磁盘块
在aaa
的FCB,也就是目录项当中,需要记录文件aaa
存放的起始块号。另外在文件分配表中会显示的记录,文件aaa
这几个物理块的链接关系。由于
接着依次将FAT对应块号的下一块设置为对应的值。当最后一个
注意:一个磁盘仅设置一张FAT。在开机时,将FAT读入内存,并常驻内存。FAT 的各个表项在物理上连续存储,且每一个表项长度相同,因此"物理块号"字段可以是隐含的。
实现文件逻辑块号到物理块号的转换:
用户给出要访问的逻辑块号
如要找到文件aaa
的
这里文件aaa
的
结论:采用链式分配(显式链接)方式的文件,支持顺序访问,也支持随机访问(想访问
显然,显式链接也不会产生外部碎片,也可以很方便地对文件进行拓展。
总结:
考试题目中遇到未指明隐式
索引分配允许文件离散地分配在各个磁盘块中,系统会为每个文件建立一张索引表,索引表中记录了文件的各个逻辑块对应的物理块(索引表的功能类似于内存管理中的页表。建立逻辑页面到物理页之间的映射关系)。索引表存放的磁盘块称为索引块。文件数据存放的磁盘块称为数据块。
假设某个新创建的文件aaa
的数据依次存放在磁盘块aaa
,这个文件建立一张索引表。这个索引表记录了aaa
这些逻辑块和物理块之间一一对应的映射关系。
如果aaa
文件的索引表是存放在aaa
的索引块。而aaa
文件的数据块。采用索引分配方式的文件,需要在自己的目录项FCB中记录自己的索引块到底是哪一块。而索引块中存放的是索引表。因此系统可以根据索引块的块号找到对应的索引表,接着就可以找到各个逻辑块对应的块号了。
如上图,aaa
文件的索引表存放在
注:在显式链接的链式分配方式中,文件分配表FAT是一个磁盘对应一张。而索引分配方式中,索引表是一个文件对应一张。
可以用固定的长度表示物理块号( 如:假设磁盘总容量为
采用索引分配方式,如何实现文件的逻辑地址到物理地址的转换:
假如用户给出要访问的逻辑块号
可见,索引分配方式可以支持随机访问。文件拓展也很容易实现(只需要给文件分配一个空闲块,并增加一个索引表项即可)。但是索引表需要占用一定的存储空间。
假设每个磁盘块
链接方案:如果索引表太大,一个索引块装不下,那么可以将多个索引块链接起来存放。
如果一个文件的大小超过了
假设要找到aaa
的逻辑块号为aaa
的第二个索引块,而各个索引块之间是用链接的方式连起来的,所以为了找到第二个索引块的块号,操作系统首先需要把第一个索引块读入内存,然后才能根据这个索引块中的指针,找到第二个索引块块号,并且把第二个索引块读入内存。只有这样才能找到
若一个文件大小为
这显然是很低效的。为了解决这个问题,提出了多级索引的方案。
多层索引
建立多层索引(原理类似于多级页表)。使第一层索引块指向第二层的索引块。还可根据文件大小的要求再建立第三层、第四层索引块。
同样若一个文件大小为
若采用多层索引,则各层索引表大小不能超过一个磁盘块。可根据逻辑块号算出应该查找索引表中的哪个表项。
如:要访问
若采用三层索引,则文件的最大长度为
故采用
这种方式也存在问题,假如一个文件本来很小,数据块只有
混合索引
多种索引分配方式的结合。
例如,一个文件的顶级索引表中,既包含直接地址索引(直接指向数据块),又包含一级间接索引(指向单层索引表)、还包含两级间接索引(指向两层索引表)。
如上图,如果直接地址有
若顶级索引表还没读入内存,则访问
所以采用混合索引方式好处是对于小文件,只需较少的读磁盘次数就可以访问目标数据块(一般计算机中小文件更多)。
总结:
索引分配允许文件离散地分配在各个磁盘块中,系统会为每个文件建立一张索引表, 索引表中记录了文件的各个逻辑块对应的物理块(索引表的功能类似于内存管理中的页表。建立逻辑页面到物理页之间的映射关系)。索引表存放的磁盘块称为索引块。文件数据存放的磁盘块称为数据块。但若文件太大,索引表项太多,可以采取以下三种方法解决:
链接方案:如果索引表太大,一个索引块装不下,那么可以将多个索引块链接起来存放。
缺点:若文件很大,索引表很长,就需要将很多个索引块链接起来。想要找到
多层索引:建立多层索引(原理类似于多级页表)。使第一层索引块指向第二层的索引块。还可根据文件大小的要求再建立第三层、第四层索引块。采用K层索引结构,且顶级索引表未调入内存,则访问一个数据块只需要
缺点:即使是小文件,访问一个数据块依然需要
混合索引:多种索引分配方式的结合。
例如,一个文件的顶级索引表中,既包含直接地址索引(直接指向数据块),又包含一级间接索引(指向单层索引表)、还包含两级间接索引(指向两层索引表)。
优点:对于小文件来说,访问一个数据块所需的读磁盘次数更少。
重要考点:
①要会根据多层索引、混合索引的结构计算出文件的最大长度(注意:各级索引表最大不能超过一个块)。
②要能自己分析访问某个数据块所需要的读磁盘次数(注意:FCB中会存有指向顶级索引块的指针,因此可以根据FCB读入顶级索引块。每次读入下一级的索引块都需要一次读磁盘操作。另外,要注意题目条件,即顶级索引块是否已调入内存)。
文件的物理结构总结:
所谓的"逻辑结构"就是指在用户看来,文件内部的数据应该是如何组织起来的。而"物理结构"指的是在操作系统看来,文件的数据是如何存放在外存中的。
例子:用C语言创建一个无结构文件
x1FILE *fp = lfopen("test.txt", "W"); //打开文件
2if( fp == NULL ){
3 printf("打开文件失败!");
4 exit(0) ;
5}
6
7//写入1W个Hello world
8for (int i=0; i<10000; i++)
9 fputs("Hello world!",fp);
10fclose(fp);//关闭文件
执行这段代码后会创建一个文本文件(无结构文件),里面会写入一万个"Hello world!"。
每个字符
如果此时想读入第
xxxxxxxxxx
91FILE *fp = fopen("test.txt","r"); //以"读"方式打开文件
2if( fp == NULL ){
3 puts("Fail to open file!") ;
4 exit(0) ;
5}
6fseek(fp,16,SEEK_SET);//读写指针指向16
7char C = fgetc(fp);//从读写指针所指位置读出1个字符
8printf("字符: %C",c);//打印从文件读出的字符
9fclose(fp);//关闭文件
运行结果是o
。总之在用户看来,似乎所有的字符都是连续存放的,所以只需要提供想访问那个字符在文件中的逻辑地址(
接着操作系统会根据文件管理的策略来决定是用连续分配还是连接分配或者是索引分配方式来存放在磁盘中。
所以从用户角度看就做了以下事情:
使用C语言库函数fseek
,将文件读写指针指向位置
使用C语言库函数fgetc
,从读写指针所指位置读出
fgetc
底层使用了Read
系统调用,总之操作系统总能将(逻辑块号,块内偏移量)转换为(物理块号,块内偏移量)。
用C语言创建顺序文件
x
1typedef struct {
2 int number;//学号
3 char name[30];//姓名
4 char major[30];//专业
5}Student_info;
6//以"写"方式打开文件
7FILE *fp = fopen("students.info", "w");
8if(fp == NULL) {
9 printf("打开文件失败!");
10 exit(0) ;
11}
12Student_info student[N];//用数组保存N个学生信
13for(int i = 0; i<N; i++){//生成 N个学生信息
14 student[i].number=i;
15 student[i].name[0]='?';
16 student[i].major[0]='?';
17}
18//将N个学生的信息写入文件
19fwrite (student,sizeof(Student_info),N,fp);//参数分别是:首地址,每条记录大小,要写入几条记录,要存放数据的文件
20fclose(fp);
上面程序运行后,学生信息就会存放在这个文件中。
每个学生记录占
xxxxxxxxxx
131//以"读"方式打开文件
2FILE *fp =fopen("students.info", "r");
3if(fp == NULL) {
4 printf("打开文件失败!");
5 exit(0);
6}
7//文件读写指针指向编号为5的学生记录
8fseek(fp,5*sizeof(Student_info),SEEK_SET);
9Student_info stu;
10//从文件读出1条记录,记录大小为sizeof(Student_ info)
11fread(&stu,sizeof(Student_info), 1, fp);
12printf("学生编号: %d\n",stu.number);
13fclose(fp);
这样就可以读出
上面是用顺序存储方式,同样用户也可以采用链式存储方式:
对于用户来说
顺序存储,各条记录相邻这存放。
支持随机访问:指可以直接确定第
链式存储,各条记录离散着存放,用指针表示先后关系。
所以用户可以自己决定采用什么方式。但是无论如何在用户看来这些记录都是占有一整片连续空间。
从操作系统视角来看,学生信息就是一堆二进制数据,每个磁盘块可存储
此时操作系统可以采用连续分配、链接分配或者索引分配方式存放这些数据。至于哪一种分配方式作为用户不需要关心。
所以文件内部各条记录链式存储是由创建文件的用户自己设计的。而文件整体用链接分配方式是由操作系统决定。
C语言实现索引结构:
xxxxxxxxxx
111//存放学生索引
2typedef struct {
3 int number;//学号
4 int addr;//学生记录的逻辑地址
5} IndexTable;
6
7//存放学生信息
8typedef struct {
9 char name[30];//姓名
10 char major[30];//专业
11}Student_info;
这样一个IndexTable
结构体就对应了一个索引项,每个索引项记录就学生的学号和学生记录的地址。之后的空间可以定义一个Student_info
存放学生信息。之后索引项会建立起学号和各个学生的映射关系。
从用户视角来看,索引文件中的数据项依然是连续存放的。如:前
从操作系统视角来看,这些信息就是一堆二进制数据,每个磁盘块可存储
此时操作系统可以采用连续分配、链接分配或者索引分配方式存放这些数据。至于哪一种分配方式作为用户不需要关心。
所以索引文件的索引表是用户自己建立的,映射是由关键字
逻辑结构和物理结构总结:
之前的文件物理结构
文件的存储空间管理内容如下:
安装Windows操作系统的时候,一个必经步骤是为磁盘分区(C:盘、D: 盘、E: 盘等)。其本质是存储空间的划分,将物理磁盘划分为一个个文件卷(逻辑卷、逻辑盘)。
在存储空间的初始化过程中,需要把文件卷进行进一步的划分。分目录区和文件区。:
目录区
目录区主要存放文件目录信息(FCB)、用于磁盘存储空间管理的信息。
文件区
文件区用于存放文件数据。
另外有的系统支持超大型文件,可支持由多个物理磁盘组成个文件卷。
下面会介绍几种存储空间管理:空闲表法、空闲链表法、位示图法、成组链接法。
假设一个磁盘情况如下:
上图绿色的为空闲块,橙色是非空闲快。假设用的是空闲表法,系统对应的空闲盘快表如下:
表中记录了每个空闲块的起始位置(第一个空闲盘块号)和空闲区间的长度(空闲盘块数)。这种方法适用于适用于"连续分配方式"。
采用这种方法如何分配磁盘块:与内存管理中的动态分区分配很类似,为一个文件分配连续的存储空间。同样可采用首次适应、最佳适应、最坏适应等算法来决定要为文件分配哪个区间。
假如:新创建的文件请求
则系统会因此检查空闲盘快表中的表项,然后找到第一个能够满足
之后再修改空闲盘块表。
采用这种方式如何回收磁盘块:与内存管理中的动态分区分配很类似,当回收某个存储区时需要有四种情况:
回收区的前后都没有相邻空闲区。
会再空闲盘块表中新增一个表项。
回收区的前后都是空闲区。
需要把前后空闲区和新的空闲区进行合并。发生这种情况表项的数量会减少一个。
回收区前面是空闲区。
新的空闲区和前面的空闲区进行合并,不会导致表项的数量改变。
回收区后面是空闲区。
新的空闲区和前面的空闲区进行合并,不会导致表项的数量改变。
总之,回收时需要注意表项的合并问题。
空闲链表法可以进一步划分为空闲盘块链和空闲盘区链。区别在于:
空闲盘块链
是以盘块为单位组成一条空闲链。空闲盘块中存储着下一个空闲盘块的指针。
操作系统会保存着链头、链尾指针。
如何分配:若某文件申请
如何回收:回收的盘块依次挂到链尾,并修改空闲链的链尾指针。
这种方式适用于离散分配的物理结构。为文件分配多个盘块时可能要重复多次操作。
空闲盘区链
是以盘区为单位组成一条空闲链。连续的空闲盘块组成一个空闲盘区。
空闲盘区中的第一个盘块内记录了盘区的长度、下一个盘区的指针。
操作系统保存着链头、链尾指针。
如何分配:若某文件申请
如何回收:若回收区和某个空闲盘区相邻,则需要将回收区合并到空闲盘区中。若回收区没有和任何空闲区相邻,将回收区作为单独的一个空闲盘区挂到链尾。如上图假如当前
这种方式离散分配、连续分配都适用。为个文件分配多个盘块时效率更高。
比起空闲盘块链来说,空闲盘区链效率要更高。因为空闲盘块链只能从链当中一个一个把这些磁盘块划分出来。而空闲盘区链可以一次性划分出一大片连续的空闲区间。所以在分配多个盘块时效率更高。
位示图:每个二进制位对应一个盘块。 在本例中,"0"代表盘块空闲,"1"代表盘块已分配。位示图一般用连续的"字"来表示,如本例中一个字的字长是
如上图,如果绿色表示空闲盘块,橙色的表示已分配盘块。则第一行四个盘块在位示图中的表示是
上图一个字的字长是
要能自己推出盘块号与(字号,位号)相互转换的公式。
要注意题目条件:盘块号、字号、位号到底是从
则
如何分配:若文件需要
如何回收:
采用位示图法连续分配、离散分配都适用。
空闲表法、空闲链表法不适用于大型文件系统,因为空闲表或空闲链表可能过大。UNIX系统中采用了成组链接法对磁盘空闲块进行管理。
文件卷的目录区中专门用一个磁盘块作为"超级块",当系统启动时需要将超级块读入内存。并且要保证内存与外存中的"超级块"数据一致。
超级块作用:在超级块中记录了下一组空闲盘块数。然后还要记录对应一组空闲盘块的块号。
通过这些盘块号就可以依次找到下一个分组的各个盘块了。
可以看到每组的盘号对应的第一个盘块会记录下一个分组的信息,即超级块信息。而注若已经没有下一组空闲快,则第一个盘块处设为某特殊值,如
同时还需要注一个分组中的块号不需要连续,此处只是为了让大家更方便看出各个分组的数量。并且最后一组空闲块的数量一般会少
如何分配:
例1:如需要
检查第一个分组的块数是否足够。
分配第一个分组中的
会选择一组空闲块的最后一个空闲块,放入数据后删除该空闲块,之后修改对应的超级块信息。
例2:如需要
检查第一个分组的块数是否足够。此时
分配第一个分组中的
如何回收:
假设每个分组最多为
需要将超级块中的数据复制到新回收的块中,并修改超级块的内容,让新回收的块成为第一个分组。
文件存储空间管理总结:
操作系统向上提供了几个文件管理最基本的功能:创建文件(create 系统调用)、删除文件(delete系统调用)、读文件(read 系统调用)、写文件(write系统调用)、打开文件(open系统调用)、关闭文件(close 系统调用)。
创建文件
可以"创建文件",(点击新建后,图形化交互进程在背后调用了create
系统调用。进行Create系统调用时,需要提供的几个主要参数:
D:/Demo
)新建文本文档.txt
)操作系统在处理Create系统调用时,主要做了两件事:
D:/Demo
目录),在目录中创建该文件对应的目录项。目录项中包含了文件名、文件在外存中的存放位置等信息。删除文件
可以"删除文件"(点了"删除"之后,图形化交互进程通过操作系统提供的"删除文件"功能,即delete
系统调用,将文件数据从外存中删除)。进行Delete
系统调用时,需要提供的几个主要参数:
D:/Demo
)test.txt
)操作系统在处理Delete
系统调用时,主要做了几件事:
打开文件
在很多操作系统中,在对文件进行操作之前,要求用户先使用open
系统调用"打开文件",需要提供的几个主要参数:
D:/Demo
)test.txt
)r
只读;rw
读写等)操作系统在处理open系统调用时,主要做了几件事:
根据文件存放路径找到相应的目录文件,从目录中找到文件名对应的的目录项,并检查该用户是否有指定的操作权限。
目录对应的目录表:
操作系统会将文件对应的目录项复制到内存中的"打开文件表"中。并将对应表的编号返回给用户。之后用户使用打开文件表的编号来指明要操作的文件。也就是说当用户打开一个文件后,这个文件相关的信息就已经放入内存当中。之后用户再操作这个文件只需要根据打开文件表的编号就可以找到自己要操作的一切信息。
之后用户进程
另外需要注意的是有两种打开文件表:一个是系统的,其中会记录所有正在被其他进程使用的文件。一个是每个进程也会有一个自己的打开文件表,这张表中记录了当前进程此时已经打开的文件是哪些。
进程的打开文件表中有一个系统表索引号,如test.txt
文件在系统中编号是
用户进程的打开文件表中读写指针项记录了该进程对文件的读
系统打开文件表的打开计数器字段可以方便实现某些文件管理的功能。例如:在Windows系统中,我们尝试删除某个txt
文件,如果此时该文件已被某个"记事本"进程打开,则系统会提示我们"暂时无法删除该文件"。其实系统在背后做的事就是先检查了系统打开文件表,确认此时是否有进程正在使用该文件。
关闭文件
进程使用完文件后,要"关闭文件",操作系统在处理Close
系统调用时,主要做了几件事:
count
减1
,若count=0
,则删除对应表项。读文件
可以"读文件",将文件数据读入内存,才能让CPU处理(双击后,"记事本"应用程序通过操作系统提供的"读文件"功能,即read
系统调用,将文件数据从外存读入内存,并显示在屏幕上)
通过之前的讲解可以知道,在对文件读操作之前,要先打开文件。所以在开始读的时候,进程的打开文件表中已经有了这个被读文件对应的表项了。
总之进程使用read
系统调用完成写操作。需要指明是哪个文件(在支持"打开文件"操作的系统中,只需要提供文件在打开文件表中的索引号即可),还需要指明要读入多少数据(如:读入
操作系统在处理read
系统调用时,会从读指针指向的外存中,将用户指定大小的数据读入用户指定的内存区域中。
写文件
写文件的操作与读文件类似。可以"写文件",将更改过的文件数据写回外存(我们在"记事本"应用程序中编辑文件内容,点击"保存"后,"记事本"应用程序通过操作系统提供的"写文件"功能,即write
系统调用,将文件数据从内存写回外存)。
进程使用write
系统调用完成写操作,需要指明是哪个文件(在支持"打开文件"操作的系统中,只需要提供文件在打开文件表中的索引号即可),还需要指明要写出多少数据(如:写出
操作系统在处理write
系统调用时,会从用户指定的内存区域中,将指定大小的数据写回写指针指向的外存。
文件基本操作总结:
操作系统为用户提供文件共享功能,可以让多个用户共享地使用同一个文件。
文件共享方式有:基于索引结点的共享方式(硬链接)、基于符号链的共享方式(软链接)
注意:多个用户共享同一个文件,意味着系统中只有"一份"文件数据。并且只要某个用户修改了该文件的数据,其他用户也可以看到文件数据的变化。
如果是多个用户都"复制"了同一个文件,那么系统中会有"好几份"文件数据。其中一个用户修改了自己的那份文件数据,对其他用户的文件数据并没有影响。
知识回顾:索引结点,是一种文件目录瘦身策略。由于检索文件时只需用到文件名,因此可以将除了文件名之外的其他信息放到索引结点中。这样目录项就只需要包含文件名、索引结点指针。
假设用户User1
创建一个文件aaa
,这个文件会对应一个索引结点,并且索引结点中会包含文件的物理地址及其他属性。
索引结点中设置一个链接计数变量count
,用于表示链接到本索引结点上的用户目录项数。
若
若某个用户决定"删除"该文件,则只是要把用户目录中与该文件对应的目录项删除,且索引结点的count
值减1
。
若count>0
, 说明还有别的用户要使用该文件,暂时不能把文件数据删除,否则会导致指针悬空。当count=0
时系统负责删除文件。
软连接是Link类型的文件,记录了文件1
的存放路径C:/User1/aaa
。类似于Windows操作系统的"快捷方式"。
当User3访问ccc
时,操作系统判断文件ccc
属于Link类型文件,于是会根据其中记录的路径层层查找目录,最终找到User1的目录表中的aaa
表项,于是就找到了文件1
的索引结点。
如果文件1
已删除,但是文件2
(软连接)依然存在,只是通过C:/User1/aaa
这个路径已经找不到文件1
了。
由于用软链接的方式访问共享文件时要查询多级目录,会有多次磁盘
文件共享总结:
操作系统需要保护文件数据的安全。保护的方式一般有三种:口令保护、加密保护、访问控制。
注意:这里的口令是指日常生活俗称的密码,这里的密码是指直接对文件内容的加解密。
口令保护
为文件设置一个"口令"(如:abc112233
),用户请求访问该文件时必须提供"口令"。
口令一般存放在文件对应的FCB或索引结点中。用户访问文件前需要先输入"口令",操作系统会将用户提供的口令与FCB中存储的口令进行对比,如果正确,则允许该用户访问文件。
优点:保存口令的空间开销不多,验证口令的时间开销也很小。
缺点:正确的"口令"存放在系统内部,不够安全。
加密保护
使用某个"密码"对文件进行加密,在访问文件时需要提供正确的"密码"才能对文件进行正确的解密。
如:一个最简单的加密算法,即异或加密,假设用于加密
加密结果如下:
同样的用
优点:保密性强,不需要在系统中存储"密码"。
缺点:编码
访问控制
在每个文件的FCB(或索引结点)中增加一个访问控制列表(Access-Control List, ACL),该表中记录了各个用户可以对该文件执行哪些操作。
用户最文件操作可以分为:读(从文件中读数据)、写(向文件中写数据)、执行(将文件装入内存并执行)、添加(将新信息添加到文件结尾部分)、删除(删除文件,释放空间)、列表清单(列出文件名和文件属性)。
如果一个系统用户是father
、mothor
、son
这三个用户对应的某文件的访问控制权限列表如下:
这里的
有的计算机可能会有很多个用户,因此访问控制列表可能会很大,可以用精简的访问列表解决这个问题。
精简的访问列表:以"组"为单位,标记各"组"用户可以对文件执行哪些操作。如:分为系统管理员、文件主、文件主的伙伴、其他用户几个分组。当某用户想要访问文件时,系统会检查该用户所属的分组是否有相应的访问权限。所以操作系统需要管理分组的信息。
精简访问列表:
若想要让某个用户能够读取文件,只需要把该用户放入"文件主的伙伴"这个分组即可。
优点:实现灵活,可以实现复杂的文件保护功能
注意:如果对某个目录进行了访问权限的控制,那也要对目录下的所有文件进行相同的访问权限控制。
文件保护总结:
文件系统层次结构如下:
下面会对每一层进行分析:
用户接口
用户接口这一层需要向上层的用户提供些简单易用的功能接口。这层就是用于处理用户发出的系统调用请求(Read、Write、 Open、Close等系统调用)
所以用户接口要做的事情就是之前学习的文件基本操作这一小节内容。
文件目录系统
用户是通过文件路径来访问文件的,因此这一层需要根据用户给出的文件路径找到相应的FCB或索引结点。所有和目录、目录项相关的管理工作都在本层完成。如:管理活跃的文件目录表、管理打开文件表等。
所以文件目录系统要做的事情就是之前学习的文件目录这一小节内容。
存取控制模块
为了保证文件数据的安全,还需要验证用户是否有访问权限。这一层主要完成了文件保护相关功能。
所以存取控制模块要做的事情就是之前学习的文件保护这一小节内容。
逻辑文件系统与文件信息缓冲区
用户指明想要访问文件记录号,这一层需要将记录号转换为对应的逻辑地址。
如果采用索引文件这种逻辑结构,会位文件当中的各个记录建立一个索引表。索引表会指向文件的物理地址。而在查询索引表之前就需要将索引表调入文件信息缓冲区中。
所以逻辑文件系统与文件信息缓冲区要做的事情就是之前学习的文件逻辑结构这一小节内容。
物理文件系统
这一层需要把上一层提供的文件逻辑地址转换为实际的物理地址。
所以物理文件系统要做的事情就是之前学习的文件物理结构这一小节内容。
辅助分配模块
负责文件存储空间的管理,即负责分配和回收存储空间
所以辅助分配模块要做的事情就是之前学习的文件存储空间管理这一小节内容。
设备管理模块
直接与硬件交互,负责和硬件直接相关的一些管理工作。如:分配设备、分配设备缓冲区、磁盘调度、启动设备、释放设备等。
所以设备管理模块要做的事情就是之后学习的磁盘管理这一小节内容。
用一个例子来辅助记忆文件系统的层次结构:
假设某用户请求删除文件D:/工作目录/学生信息.xIsx
的最后
从一个磁盘出厂,物理格式化,逻辑格式化来一步一步认识文件系统在外存中是如何被建立的。
物理格式化
物理格式化,即低级格式化:划分扇区,检测坏扇区,并用备用扇区替换坏扇区
磁盘出厂时候第一件要做的事情就是物理格式化。会把磁盘分为一个个扇区,同时也会检测磁盘有没有坏扇区存在。如果有坏扇区就会用备用扇区替换。
坏扇区存在对于操作系统来说是透明的。假设操作系统要访问某一个坏扇区,磁盘驱动器在物理格式化后知道这是一个坏扇区,此时操作系统会用一个备用扇区来替代这个坏扇区。
逻辑格式化
逻辑格式化后,即高级格式化:磁盘分区(分卷Volume),完成各分区的文件系统初始化
在低级格式化后会进行高级格式化,会把磁盘分为一个个分区(卷)
注:逻辑格式化后,灰色部分就有实际数据了,白色部分还没有数据。
一个卷的起始位置、大小、结束位置记录在分区表中。每一个分区(卷)中可以建立各自独立的文件系统。如上面
内存结构如下:
内核区中有个重要的目录缓存,即最近访问过的目录文件数据会被暂时缓存在内存当中。这样做可以加快目录检索的速度。
还有两个重要的数据结构:系统打开文件表和进程打开文件表。
系统打开文件表整个系统之有一张,而进程打开文件表每一个进程都有一张。这个进程打开文件表是包含在每个进程的PCB中的,记录了某一个进程当前打开了哪些文件。
下面介绍open系统调用打开文件过程:
通过open系统调用打开目录M下的A文件,即目录/M/A
。假设已经找到了文件A存放的目录M,接下来会把目录M的数据读入到主存,并缓存。读入主存后就可以检查目录项,一个一个对比是否有没有要找的文件A。找到后会把文件A的FCB复制到系统打开文件表中,表示这个文件被打开,同时设置打开计数为
普通文件系统:
计算机会有很多外部设备。不同外部设备里面的文件系统是各不相同的,所以就导致各个外部设备的函数接口不同,如上图的open函数各个外部设备都不相同。这对上层的开发人员来说很麻烦,因此操作系统内核听该向上层用户进程提供一个统一标准的函数调用接口。所以在操作系统中就引入了虚拟文件系统(VFS)。
有了虚拟文件系统之后,用户在打开一个文件进程的时候,只需要根据虚拟文件系统制定的标准来调用函数就可以。
所以虚拟文件系统向上层用户进程提供统一标准的系统调用接口,屏蔽底层具体文件系统的实现差异。
虚拟问价系统可以处理上层用户发来的标准系统调用请求,之后虚拟文件系统会负责操作底层一个具体的文件系统(如UFS文件系统)。但是每个文件系统的函数名或者参数都不一样,所以为了解决这个问题,虚拟文件系统会要求底层的文件系统必须实现虚拟文件系统规定好的函数接口。
所以VFS要求下层的文件系统必须实现某些规定的函数功能,如:open/read/write等。一个新的文件系统想要在某操作系统上被使用,就必须满足该操作系统VFS的要求。
另外还存在问题:不同的文件系统,表示文件数据结构各不相同。打开文件后,其在内存中的表示就不同。
UFS和FAT文件系统结构:
所以VFS在内存中要根据不同的文件系统结构建立不同的数据结构,这显然不合理。为了解决这个问题在VFS每当打开一个文件之后,虚拟文件系统就会主存中建立一个vnode
(v结点)的数据结构。
这个V结点中包含的就是文件的信息,所以无论打开的文件属于哪个文件系统,都会把文件相关的信息赋值到这个V结点中。这样的话VFS就可以用一个统一的数据结构V结点,来表示任何一个文件的信息,无论文件存放在哪一个文件系统中。
即每打开一个文件,VFS就在主存中新建一个vnode,用统一的数据结构表示文件,无论该文件存储在哪个文件系统。
注意:vnode只存在于主存中,而UFS文件系统目录项的结点记录信息inode既会被调入主存,也会在外存中存储。如果此时要打开的文件是在UFS文件系统中,那么在找到文件所对应的目录项之后,会把文件的inode从外存先读入主存,然后在主存中新建一个vnode,inode的各种信息会被复制到vnode中。
可以看到vnode结点中的函数功能指针
,之前说过不同的文件系统需要实现文件系统规定的函数功能。而每个文件系统背后的具体代码是各不相同的。所以vnode中的函数功能指针其实是指向对应文件系统的函数功能列表。这样的话只要open打开一个文件,之后要对文件进行的任何操作,都可以先找到vnode中的函数功能指针,根据这个指针再找到具体对应的文件系统的函数功能列表,之后再去执行具体的函数。这样就可以实现从上到下一层一层的调用。
所以在打开文件后,创建vnode,并将文件信息复制到vnode中,vnode 的功能指针指向具体文件系统的函数功能。
虚拟文件系统(VFS)特点:
文件系统挂载(mounting),即文件系统安装
挂载功能经常用到,如U盘插入电脑后,U盘的文件系统就需要挂载到操作系统上。具体来说就是要挂载到操作系统的虚拟文件系统上。
文件系统挂载要做的事情:
在VFS中注册新挂载的文件系统。
在内存中VFS会管理一个数据结构,即挂载表( mount table)。其中包含每个文件系统的相关信息,包括文件系统类型、容量大小等。
如上图三个外部设备对应的文件系统挂载到VFS中,此时VFS的挂载表中会有三个表项,三个表项分别指向UFS、NTFS和FAT三个文件系统。当要挂载一个新的文件系统的时候,新挂载的文件系统也会给他新建一个表项,这就完成一个文件系统的注册。
新挂载的文件系统,要向VFS提供一个函数地址列表。即函数功能列表存放在哪个位置。
将新文件系统加到挂载点(mount point),也就是将新文件系统挂载在某个父目录下。如:对于Windows操作系统而言,新挂载的文件系统挂载点就是可移动磁盘(H)
。
通过之前的学习知道,操作系统作为系统资源的管理者,即需要对上层的软件进行管理,也需要对下层的硬件进行管理。这一章节会探讨操作系统对外部设备的管理。
外部设备又可以称为
输入过程无非就是把设备准备好的数据读入计算机当中。而输出的过程就是把计算机当中准备好的数据写出到输出设备中。因此UNIX系统将外部设备抽象为一种特殊的文件,用户可以使用与文件操作相同的方式对外部设备进行操作。
如:
人机交互类外部设备
鼠标、键盘、打印机等。数据传输速度慢。
存储设备
移动硬盘、光盘等。用于存储数据。数据传输速度快。
网络通信设备
路由器、光猫等。用于网络通信。数据传输速度介于上述二者之间。
按传输速率分类:
低速设备
鼠标、键盘等,传输速率为每秒几个到几百字节。
中速设备
如激光打印机等,传输速率为每秒数千至上万个字节。
高速设备
如磁盘等,传输速率为每秒数千字节至千兆字节的设备
按信息交换的单位分类:
块设备
如磁盘等,数据传输的基本单位是"块"。传输速率较高,可寻址,即对它可随机地读
字符设备
鼠标、键盘等。数据传输的基本单位是字符。传输速率较慢,不可寻址,在输入
当
CPU可控制
接受和识别CPU发出的命令
如CPU发来的read
向CPU报告设备的状态
数据交换
地址识别。
类似于内存的地址,为了区分设备控制器中的各个寄存器,也需要给各个寄存器设置一个特定的"地址"。
一个
CPU首先会通过控制线,向
另外设备接口除了状态线路保存设备状态之外,还需要有一个控制线路,可以向外部设备发出控制信息。
值得注意的小细节:
所以对于寄存器编址方式有两种:内存映像
内存映射
寄存器独立编址:控制器中的寄存器使用单独的地址。这种方式缺点是需要设置专门的指令来实现对控制器的操作,不仅要指明寄存器的地址,还要指明控制器的编号。
随着计算机发展
常见的
完成一次读
CPU首先通过控制线向
所以其实数据输入的过程本来应该从设备输入到内存,但是这个过程必须先经过CPU寄存器然后再由于寄存器转存当内存中。最后若还要继续读入数据,则CPU继续发出读指令。
直接控制方式流程图:
CPU需要不断轮询检查这个
数据传送的单位:每次读入或者写出的数据量是一个字。
数据流向:读操作:
优点:实现简单。在读
缺点:CPU和
引入中断机制。由于
注意:
每次
所以引入中断之后才实现CPU和
数据传送的单位:每次读
数据流向:读操作:
优点:与"程序直接控制方式"相比,在"中断驱动方式"中,
缺点:每个字在
与"中断驱动方式"相比,DMA方式(Direct Memory Access,直接存储器存取。主要用于块设备的
所以DMA方式流程如下:
首先CPU会给
这里的DMA控制器也是一种
上面部件是最重要的,而在DMA控制器和块设备之间也有一个相对应的接口,通过这个接口可以实现控制器对于外部设备的通信控制。同时系统总线还会把DMA控制器和内存连接在一起。所以DMA控制器和内存之间可以直接进行数据的读写不再需要经过CPU。
假如CPU在刚开始指明这次要读入的数据是存放在磁盘什么位置。这个位置信息是存放在MAR中的。并且还会说明此时要读入的数据量是多少。接着DMA控制器会根据CPU提供的一系列参数,从磁盘的相应位置读入数据,然后写到内存里。而这个过程就不需要CPU介入。只有DMA控制器完成这一系列指定的任务之后才会向CPU发出一个中断信号,让CPU进行后序处理。
这里需要注意其实DMA控制器并不是每次直接读入一整块的数据,在读入数据的过程中也是一个字一个字读入的。每次读入的字会先存放在DR中。再从DR写入到内存中,用这种方式一个字一个字读入最终就可以完成一整块数据的读入工作。
仅在传送一个或多个数据块的开始和结束时,才需要CPU干预。
数据传送的单位:每次读
数据的流向(不再需要经过CPU),读操作(数据输入):
优点:数据传输以"块"为单位,CPU介入频率进一步降低。数据的传输不再需要先经过CPU再写入内存,数据传输效率进一步增加。CPU和
缺点:CPU每发出一条
通道:是一种硬件,可以理解为是"弱鸡版的CPU"。通道可以识别并执行一系列通道指令。
首先CPU向通道发出
通道被称为弱鸡版CPU是因为与CPU相比,通道可以执行的指令很单一,并且通道程序是放在主机内存中的,也就是说通道与CPU共享内存。
CPU的干预频率变得极低,通道会根据CPU的指示执行相应的通道程序,只有完成一组数据块的读
数据传送的单位:每次读
数据的流向(不再需要经过CPU),读操作(数据输入):
缺点:实现复杂,需要专门的通道硬件支持。
优点:CPU、通道、
四种控制方式总结:
设备独立性软件、设备驱动程序、中断处理程序属于操作系统的内核部分。即"
可以看出越上面的层次越接近用户。越下面的层次越接近硬件。各个层次都会使用下面一层提供的功能,并且向上层的软件提供一些服务。像这种每一层会利用其下层提供的服务,实现某些功能,并屏蔽实现的具体细节,向高层提供服务("封装思想")。
用户层
是最接近用户的一个层次,会向用户提供一些简单易用的接口。一般来说用户层软件会向用户提供一些与
用户层软件将用户请求,翻译成格式化的
Windows操作系统向外提供的一系列系统调用,但是由于系统调用的格式严格,使用麻烦,因此在用户层上封装了系列更方便的库函数接口供用户使用(Windows API)。
设备独立性软件
设备独立性软件,又称设备无关性软件。与设备的硬件特性无关的功能几乎都在这一层实现。
主要实现的功能:
向上层提供统一的调用接口( 如read
实现设备的保护
原理类似于对文件保护。设备被看做是一种特殊的文件,不同用户对各个文件的访问权限是不一样的,同理,对设备的访问权限也不一样。
差错处理
设备独立性软件需要对一些设备的错误进行处理
设备分配与回收
因为很多设备其实是一种临界资源,不可以同时分配给多个进程使用,所以操作系统需要对设备这种资源进行分配与回收。
数据缓冲区管理
可以通过缓冲技术屏蔽设备之间数据交换单位大小和传输速度的差异。
建立逻辑设备名到物理设备名的映射关系。根据设备类型选择调用相应的驱动程序。
用户或用户层软件发出
设备独立性软件需要通过"逻辑设备表(LUT, Logical Unit Table)"来确定逻辑设备对应的物理设备,并找到该设备对应的设备驱动程序。
可以看到逻辑设备名类似于文件路径,这是因为很多操作系统都会把设备当作是一种特殊的文件。每一个表项记录了逻辑设备名到物理设备名的映射关系,并且还记录了这个设备对应驱动程序的入口地址。
操作系统系统可以采用两种方式管理逻辑设备表(LUT):
第一种方式,整个系统只设置一张LUT,这就意味着所有用户不能使用相同的逻辑设备名,因此这种方式只适用于单用户操作系统。
第二种方式,为每个用户设置一张LUT,各个用户使用的逻辑设备名可以重复,适用于多用户操作系统。系统会在用户登录时为其建立一个用户管理进程,而LUT就存放在用户管理进程的PCB中。
这两种方式类似于文件管理中的单级目录和两级目录区别。
不同类型的
设备驱动程序
不同的
注:驱动程序一般会以一个独立进程的方式存在。
中断处理程序
当
首先中断处理程序会从
当中断处理程序将这次输入的数据放入内存后,接下来会交由设备驱动程序对这些数据进行进一步的处理。等设备驱动程序处理完了,会再交由上一层的设备独立性软件进行再进一步的处理。最后返回给用户。
可见除了设备驱动程序,中断处理程序也会和硬件直接打交道。
理解并记住
其中输入输出应用程序接口分为:字符设备接口、块设备接口、网络设备接口。
比如像块设备就是有寻址的功能,一个磁盘可以指定读取哪个地址。由于块设备有寻址和支持多个字节读取,因此当写程序时要用系统调用方式从块设备中读入数据,那么read系统调用就可以写成read(指明操作块设备,读入字节数,指明从哪开始读)
。这种读操作系统调用接口显然不适用于字符设备。因为字符设备是不可寻址的,且每次只读
所以设备独立性软件这一层需要对上层的应用程序提供若干种类型的程序接口。上层应用程序访问字符设备、块设备、网络设备所调用的系统接口是各不相同的。
字符型设备接口
用户从如果要读写,可以采用get
块设备接口
块设备有地址概念。可以使用read
可以看出给上层应用程序提供的不同接口其实是依据这种类型设备的特性来定义的。字符型设备没有地址概念,所以get
网络设备接口(网络套接字(socket)接口)
如:网络控制器(网卡)。用于收发网络数据包。会向上层的应用程序提供socket
系统调用,这个系统调用作用是可以创建一个网络套接字,同时需要指明连接的网络协议是TCP还是UDP。有了套接字之后可以使用bind
系统调用将套接字绑定到本地的某个端口。接着还提供了connect
系统调用,这个系统调用可以把套接字连接到远程的某个主机上。当网络套接字初始化后就可以使用read/write
对网络套接字进行数据的读写。
下面介绍网络通信原理:
上图有两个主机,之间有网络进行连接。每台主机上可能会运行多个进程(主机
首先主机2的P3进程要进行网络通信。可以先使用socket
系统调用来创建一个网络套接字(即申请一片内核空间,这片空间用于接收或发送数据)socket:6666
。socket
系统调用会给用户进程返回一个描述符,即指向套接字的指针。有了这个套接字对象后还要将这个套接字通过bind
绑定到某个端口,如绑定到上面的6666
端口。此时主机
主机1也做相同操作P1进程通过socket
系统调用来申请创建一个套接字对象,同时需要bind
系统调用指明要绑定的端口,即socket:211
。
此时两个主机都有了自己的套接字。同时也绑定了各自数据收发的端口号。接着使用connect
系统调用把本机的套接字连接到另一台机器的套接字上。如可以让主机1的P1进程使用connect
系统调用指明要把fd
所指向的套接字连接到主机2的IP地址的6666
端口号。即connect(fd,168.98...,6666)
。这个系统调用就可以使主机1和主机2建立起一个连接。此时两个主机就可以通过套接字发送数据。
假如P1要给P3发送一个数据包,P1首先在用户区准备好数据,然后使用write
系统调用指明要往fd
指向的套接字中写入数据。在设备独立性软件接收到write
系统调用后,就会把用户进程准备好的数据给复制到内核区,即套接字所对应的缓冲区中。接着数据独立性软件会调用网络控制器的驱动程序来处理这片数据。这个驱动程序会负责把准备好的数据输出到网络设备上,接着网络控制器就可以将这些数据包发送到网络上。
当数据到主机2的网络控制器时,主机2的网络控制器会向主机发送一个中断信号,主机2的中断处理程序会调用网络控制器的驱动程序,让驱动程序来把网络控制器里收到的数据搬到内核缓冲区里面。之后数据会被复制到6666
端口所对应的缓冲区中。接着P3进程要接受这个数据包,就可以用read
系统调用指明要从fd
所指向的套接字对象中读出一个数据包,这个系统调用结果就是设备独立性软件会从缓冲区中把这个数据给复制到用户进程的用户区中。这样进程P3就可以使用收到的这块数据了。
这就是设备独立性软件向应用程序提供的网络设备接口。
阻塞与非阻塞
阻塞get
调用,只要这个键盘输入的动作没有完成,这个进程就需要一直等下去。
非阻塞write
调用,这个系统调用可以很快处理完,然后迅速返回到用户进程继续往下执行。因为进程准备数据是在用户区,而操作系统内核又有内核区,当发出write
调用,即使磁盘正在忙碌,设备独立性软件那一层也会迅速响应请求。先把数据复制到内核,内核会将数据依次写入磁盘。
设备独立性软件需要根据实际操作设备的不同区调用不一样的设备驱动程序。
如上图所示若各公司开发的设备驱动程序接口不统一,则操作系统很难调用设备驱动程序。
因此操作系统规定好设备驱动程序的接口标准,各厂商必须按要求开发设备驱动程序。
同时不同的操作系统,对设备驱动程序接口的标准各不相同。所以设备厂商必须根据操作系统的接口要求,开发相应的设备驱动程序,设备才能被使用。
备独立性软件、设备驱动程序、中断处理程序属于操作系统的内核部分。即"
因此
考研中,我们需要重点理解和掌握的功能是:
用户层软件
实现假脱机技术(SPOOLing技术)。一般来说假脱机技术需要使用到磁盘这种设备的设备独立性软件这层的服务。所以假脱机技术是在用户层软件实现的。
设备独立性软件
这里先介绍两种很熟的功能实现:
和处理机调度很类似。用某种算法确定一个好的顺序来处理各个
同理,打印机等设备也可以用先来先服务算法、优先级算法、短作业优先等算法来确定
设备保护
操作系统需要实现文件保护功能,不同的用户对各个文件有不同的访问权限(如:只读、读和写等)。
在UNIX系统中,设备被看做是一种特殊的文件,每个设备也会有对应的FCB。当用户请求访问某个设备时,系统根据FCB中记录的信息来判断该用户是否有相应的访问权限,以此实现"设备保护"的功能。(参考文件保护小节)
之前计算机发展史介绍过,批处理阶段引入了脱机输入
程序员可以先用纸带机,将程序数据输入到磁带中,而磁带速度要比纸带机快很多。而这个输入过程是由一台专门的外围控制机实现的。在外围控制机的控制下,慢速输入设备的数据先被输入到更快速的磁带上。之后主机可以从快速的磁带上读入数据,从而缓解了速度矛盾。输出过程一样。
脱机,即脱离主机的控制进行的输入
基于这种脱机技术思想又发明了假脱机技术(SPOOLing技术)。
"假脱机技术",又称"SPOOLing技术"是用软件的方式模拟脱机技术。"SPOOLing"系统的组成如下:
系统会在磁盘上开辟两个存储区,一个输入井一个输出井。"输入井"模拟脱机输入时的磁带,用于收容
除了磁带外,外围控制机也是实现脱机技术当中一个很重要的部件。外围机就是一个输入进程和一个输出进程来实现的。其中输入进程模拟脱机输入时的外围控制机,输出进程模拟脱机输出时的外围控制机。
显然这个输入进程和输出进程需要和用户进程并发进行,才可以完成这种模拟脱机输入和模拟脱机输出的过程。因此要实现SPOOLing技术,必须要有多道程序技术的支持。也就是说当有数据从设备输入到计算机时候,输入进程是由软件方式模拟外围控制机,输入进程会把要输入的数据放到磁盘的输入井中,而输出进程在模拟脱机输出时候原理也类似。
同时也要注意,内存中会开辟两个缓冲区,输入缓冲区和输出缓冲区。输入缓冲区作用是在输入进程的控制下,输入缓冲区用于暂存从输入设备输入的数据,之后再转存到输入井中。输出缓冲区作用是在输出进程的控制下,输出缓冲区用于暂存从输出井送来的数据,之后再传送到输出设备上。
共享打印机原理分析:
在之前的介绍中提到过两个概念:
所以打印机是种"独占式设备",但是可以用SPOOLing技术改造成"共享设备"。
例子:若进程
当多个用户进程提出输出打印的请求时,系统会答应它们的请求,但是并不是真正把打印机分配给他们,而是由假脱机管理进程为每个进程做两件事:
在磁盘输出井中为进程申请一个空闲缓冲区(也就是说,这个缓冲区是在磁盘上的),并将要打印的数据送入其中。
为用户进程申请一张空白的打印请求表(打印任务说明书),并将用户的打印请求填入表中(其实就是用来说明用户的打印数据存放位置等信息的),再将该表挂到假脱机文件队列上。
这里的假脱机文件队列是打印的任务队列。
当打印机空闲时,输出进程会从文件队列的队头取出一张打印请求表,并根据表中的要求将要打印的数据从输出井传送到输出缓冲区,再输出到打印机进行打印。用这种方式可依次处理完全部的打印任务。
所以在采用采用SPOOLing技术后虽然系统中只有一台打印机,但每个进程提出打印请求时,系统都会为在输出井中为其分配一个存储区(相当于分配了一个逻辑设备),使每个用户进程都觉得自己在独占一台打印机,从而实现对打印机的共享。
故SPOOLing技术可以把一台物理设备虚拟成逻辑上的多台设备,可将独占式设备改造成共享设备。
加脱机技术总结:
设备独立性软件需要实现设备的分配与回收。
设备的分配时应该考虑的因素:设备的固有属性、设备分配算法、设备分配中的安全性。
其中设备的固有属性可以分为三种:独占设备、共享设备、虚拟设备。
独占设备
一个时段只能分配给一个进程(如打印机)
共享设备
可同时分配给多个进程使用( 如磁盘),各进程往往是宏观上同时共享使用设备,而微观上交替使用。
虚拟设备
采用SPOOLing技术将独占设备改造成虚拟的共享设备,可同时分配给多个进程使用(如采用SPOOLing技术实现的共享打印机)。
而设备分配算法有:先来先服务、优先级高者优先、短任务优先。
从进程运行的安全性上考虑,设备分配有两种方式:
安全分配方式
为进程分配一个设备后就将进程阻塞,本次
当一个进程请求打印机打印,并且把打印机分配给进程后,这个进程就必须先阻塞等待。一直到打印结束后,进程才能被唤醒。所以一个时段内每个进程只能使用一个设备。
优点:破坏了"请求和保持"条件,不会死锁。
缺点:对于一个进程来说,CPU和
不安全分配方式
进程发出
一个进程请求打印机服务之后,系统会把打印机资源分配给这个进程。此时进程可以继续执行下去,不需要阻塞,甚至在接下来的执行中还可以申请使用别的外部设备。所以一个进程可以同时使用多个设备。
优点:进程的计算任务和
缺点:有可能发生死锁(可用于死锁避免、死锁的检测和解除)。
静态分配与动态分配:
静态分配:进程运行前为其分配全部所需资源,运行结束后归还资源。如果运行前所需要的全部资源暂时得不到完全满足,进程就暂时不能投入运行。这种方式破坏了"请求和保持"条件,不会发生死锁。
动态分配:进程运行过程中动态申请设备资源。
先复习以下设备、控制器、通道之间的关系:
一个通道可控制多个设备控制器,每个设备控制器可控制多个设备。所以数据结构需要表示出这种关系。
设备控制表(DCT):系统为每个设备配置一张DCT,用于记录设备情况。
设备类型如:打印机
设备标识符:即物理设备名, 系统中的每个设备的物理设备名唯一。
设备状态:表示设备是忙碌
指向控制器表的指针:每个设备由一个控制器控制,该指针可找到相应控制器的信息。
重复执行次数或时间:当重复执行多次
设备队列的队首指针:指向正在等待该设备的进程队列(由进程PCB组成队列)。
注:"进程管理"章节中曾经提到过"系统会根据阻塞原因不同,将进程PCB挂到不同的阻塞队列中"。假如此时进程正在等待某一个
控制器控制表(COCT):每个设备控制器都会对应一张COCT。 操作系统根据COCT的信息对控制器进行操作和管理。
控制器标识符:各个控制器的唯一ID。
控制器状态:忙碌
指向通道表的指针:每个控制器由一个通道控制,该指针可找到相应通道的信息。
控制器队列的队首指针与控制器队列的队尾指针:指向正在等待该控制器的进程队列(由进程PCB组成队列)
通道控制表(CHCT):每个通道都会对应一张CHCT。操作系统根据CHCT的信息对通道进行操作和管理。
通道标识符:记录各个通道的唯一ID
通道状态:忙碌
与通道连接的控制器表首址:操作系统可通过该指针找到该通道管理的所有控制器相关信息(COCT)。也就是可以快速找到一个控制表的集合。
通道队列的队首指针与通道队列的队尾指针:指向正在等待该通道的进程队列(由进程PCB组成队列)。如果一个通道暂时不能为一个进程服务,那么这个进程需要等待这个通道的服务。
系统设备表(SDT):记录了系统中全部设备的情况,每个设备对应一个表目。
每个表目中又会记录这个表目所对应的设备的设备类型,如:打印机
首先操作系统根据进程请求的物理设备名查找SDT(注:物理设备名是进程请求分配设备时提供的参数)。
于是接下来根据SDT找到DCT, 若设备忙碌则将进程PCB挂到设备等待队列中,不忙碌则将设备分配给进程。
之后根据DCT找到COCT,若控制器忙碌则将进程PCB挂到控制器等待队列中,不忙碌则将控制器分配给进程。
接着根据COCT找到CHCT,若通道忙碌则将进程PCB挂到通道等待队列中,不忙碌则将通道分配给进程。
注:只有设备、控制器、通道三者都分配成功时,这次设备分配才算成功,之后便可启动
采用以上设备分配步骤缺点:
用户编程时必须使用"物理设备名",底层细节对用户不透明,不方便编程
假如一个物理设备
若换了一个物理设备,则程序无法运行
若进程请求的物理设备正在忙碌,则即使系统中还有同类型的设备,进程也必须阻塞等待
如,此时系统中又三台打印机,进程
改进方法:建立逻辑设备名与物理设备名的映射机制,用户编程时只需提供逻辑设备名。
引入上面改进方法后:
首先进程在请求外部设备时,只需要提供逻辑设备名,根据进程请求的逻辑设备名查找SDT。用户编程时提供的逻辑设备名其实就是"设备类型"。
之后查找SDT,,找到用户进程指定类型的、并且空闲的设备,将其分配给该进程。将设备分配给进程后操作系统在逻辑设备表(LUT)中新增一个表项。
这个逻辑设备表LUT就是记录逻辑设备名到物理设备名的映射关系表。同时还要记录设备驱动程序的入口地址。
之后的操作就和之前一样。根据DCT找到COCT,若控制器忙碌则将进程PCB挂到控制器等待队列中,不忙碌则将控制器分配给进程。最后根据COCT找到CHCT,若通道忙碌则将进程PCB挂到通道等待队列中,不忙碌则将通道分配给进程。
只有进程第一次通过逻辑设备名申请一个设备时,操作系统才回来查询系统控制表。如果之后用户进程再次通过相同的逻辑设备名请求使用设备,则操作系统通过LUT表即可知道用户进程实际要使用的是哪个物理设备了,并且也能知道该设备的驱动程序入口地址。
逻辑设备表的设置问题:
设备回收就是将上面的设备分配使用到的数据结构复原。
设备的分配与回收总结:
设备独立性软件需要实现缓冲区管理。
缓冲区是一个存储区域,可以由专门的硬件寄存器组成,也可利用内存作为缓冲区。
如果使用硬件作为缓冲区的成本较高,容量也较小,一般仅用在对速度要求非常高的场合(如存储器管理中所用的联想寄存器(快表),由于对页表的访问频率极高,因此使用速度很快的联想寄存器来存放页表项的副本)。
所以一般情况下,更多的是利用内存作为缓冲区,"设备独立性软件"的缓冲区管理就是要组织管理好这些缓冲区。这也是这一节重点介绍的内容。
缓冲区作用:
缓和CPU与
CPU可以把要输出的数据快速地放入缓冲区,之后就可以做别的事。之后慢速的
减少对CPU的中断频率,放宽对CPU中断相应时间的限制
如果是字符型设备,则每输出完一个字符就要向CPU发送一次中断信号。对于中断的处理是需要付出一定的时间代价的,因此CPU频繁处理这些中断,显然会降低系统的性能。而如果采用缓冲区策略的话,只有当缓冲区中的数据全部被取走,或者输入的数据充满缓冲区,CPU才需要介入来处理中断。
解决数据粒度不匹配的问题
如:输出进程每次可以生成一块数据,但
所以如果此时在CPU上运行的输出进程,每次可以生成一整块的数据,但是
提高CPU与
介绍几种需要掌握的缓冲区策略:
假设某用户进程请求某种块设备读入若干块的数据。若采用单缓冲的策略,操作系统会在主存中为其分配一个缓冲区(若题目中没有特别说明,一个缓冲区的大小就是一个块)。
注意缓冲区特性:当缓冲区数据非空时,不能往缓冲区冲入数据,只能从缓冲区把数据传出。当缓冲区为空时,可以往缓冲区冲入数据,但必须把缓冲区充满以后,才能从缓冲区把数据传出。
对于数据块的处理需要经过的步骤:
首先系统在主存中为用户进程分配一块大小的缓冲区。这个块设备会产生一块大小的数据,输入到缓冲区中。这个过程假设所需要的时间为
之后这块数据需要传送到用户进程的工作区中才可以被用户进程处理。具体是用户进程的内存空间中,会分出一片工作区来接受输入
当处理完成后,这个用户处理的工作区就可以腾空。
常考题型:计算每处理一块数据平均需要多久?
技巧:假定一个初始状态,分析下次到达相同状态需要多少时间,这就是处理一块数据平均所需时间。在"单缓冲"题型中,可以假设初始状态为工作区满,缓冲区空。所以要分析下一次到达工作区满,缓冲区空这种状态需要花多长时间。这个时间长度就是处理一个数据块平均所需要消耗的时间。
分为几种情况:
假设输入时间
根据这个条件,刚开始工作区是满的,缓冲区是空的。所以刚开始CPU就可以处理工作区中的数据。这个处理过程需要花
另外由于刚开始缓冲区就是空的,而缓冲区空的时候就可以直接冲入数据。所以刚开始块设备就会往缓冲区中冲入一块数据。这个时间总共耗费
由于输入时间
显然在到达
可以看到再经历了
因此,通过刚刚分析可以知道平均每处理一块数据需要花的时间是
假设输入一块数据时间
由于刚开始缓冲区为空,所以在
同时由于刚开始工作区是满的,所以CPU可以在
只有到
到这步又回到刚开始假设的这种初始状态,工作区是满的,缓冲区是空的。所以这个过程耗费了
因此,通过刚刚分析可以知道平均每处理一块数据需要花的时间是
结论:采用单缓冲策略,处理一块数据平均耗时
假设某用户进程请求某种块设备读入若干块的数据。若采用双缓冲的策略,操作系统会在主存中为其分配两个缓冲区(若题目中没有特别说明,一个缓冲区的大小就是一个块)
类似的双缓冲也会经常考察,每处理一块数据平均需要耗时多就这样一个问题。所以双缓冲题目中,假设初始状态为:工作区空,其中一个缓冲区满,另一个缓冲区空。分析下一次到达这种情况耗时即可。
分为以下几种情况:
假设输入时间
根据假设的初始状态,缓冲区1中有数据,缓冲区2中没有数据。工作区中也没有数据。
所以在
接着CPU就可以处理工作区中的数据,耗时
另一方面在刚开始缓冲区
由于
因此平均用时是
假设输入时间
刚开始缓冲区
另一方面,由于刚开始缓冲区
处理完工作区中的数据后,此时缓冲区
再来看数据输入过程,在
假设在第二个
可以看到很难找到和刚开始初始状态一模一样的情况。可以看到
总之,
结论:采用双缓冲策略,处理一个数据块的平均耗时为
这两种策略不仅仅是在主机和设备之间数据传送当中可以使用,其实在两台主机通信时,也可以采用这种缓冲的策略,用于数据的发送和接受。
如果为两台机器配置单缓冲区:
A机要发送的数据要先放入A机缓冲区中,等缓冲区满时将数据发出。B机的缓冲区用于接收数据。
之后可以处理这些数据,等这些数据处理完毕,这个缓冲区才会变空。只有等这些数据全部被取走,全部处理完后B主机的缓冲区才能被腾空。而只有主机B缓冲区空时,A主机才能往B主机发送数据。
显然,若两个相互通信的机器只设置单缓冲区,在任一时刻只能实现数据的单向传输。这种原因是缓冲区特点造成的。
为了实现同一时刻双向传输,可以给两台机器配置双缓冲区。其中一个缓冲区用来暂存即将发送的数据,另一个缓冲区用于接收输入的数据。
所以采用双缓冲的结构,这两台主机就可以同时往自己的发送缓冲区中冲入自己想要发送的数据。接着可以同时往对方的接受缓冲区中冲入数据。
这就实现同一时刻可以实现双向的数据传输。
很多时候只有两个缓冲区依然不能满足进程的需要。
所以操作系统可以给进曾分配多个大小相等的缓冲区,让这些缓冲区形成一个循环队列。
注:上图中,橙色表示已充满数据的缓冲区,绿色表示空缓冲区。系统用两个指针实现对缓冲区的管理。
in
指针,指向下一个可以冲入数据的空缓冲区out
指针,指向下一个可以取出数据的满缓冲区这里只需要了解大致原理即可。
缓冲池由系统中共用的缓冲区组成。这些缓冲区按使用状况可以分为:空缓冲队列、装满输入数据的缓冲队列(输入队列)、装满输出数据的缓冲队列(输出队列)。即当使用一个或者归还一个缓冲区时,就可以对缓冲池当中的各种缓冲区进行操作即可。
另外,根据一个缓冲区在实际运算中扮演的功能不同,又设置了四种工作缓冲区:用于收容输入数据的工作缓冲区(hin) 、用于提取输入数据的工作缓冲区(sin) 、用于收容输出数据的工作缓冲区(hout) 、用于提取输出数据的工作缓冲区(sout)。
执行原理:
如果一个输入进程要请求输入一块数据
首先系统会从空缓冲队列的队头当中,取下这一块空的缓冲区把它作为用于收容输入数据的缓冲区。
当这块缓冲区被冲满之后,就会被挂到输入队列的队尾。
如果计算进程想要取得一块输入数据
操作系统会从输入队列的队头取下一个缓冲区,把它作为提前输入的工作缓冲区。
接下来这块缓冲区中的数据会被传送到计算进程的工作区当中。所以这块缓冲区内容就会被取空。
当取空之后,这个缓冲区会被挂回到空缓冲队列的队尾。
如果此时计算进程想要将准备好的数据冲入缓冲区
首先系统会从空缓冲队列的队头当中,取下这一块空的缓冲区把它作为用于收容输出数据的缓冲区。
之后缓冲区冲满后会被挂到输出队列的队尾。
如果某个输出进程请求输出数据
系统会从输出队列的队头取下一块缓冲区,作为提取输入输出数据的工作缓冲区。
接着这块缓冲区中的数据会被取走,缓冲区取空之后就可以把缓冲区挂到空缓冲区队尾。
缓冲区管理总结:
磁盘的表面由一些磁性物质组成,可以用这些磁性物质来记录而进制数据。
如果要读取磁盘中的二进制数据,需要由磁头臂来带动磁头移动,然后把磁头移动到相应位置来读取这个位置存储的数据。
磁盘上的盘片会被分为一圈一圈的磁道。
个磁道又被划分成一个个扇区,每个扇区就是一个"磁盘块"。各个扇区存放的数据量相同(如
由于每个扇区中存放的数据量相同。所以最内侧的扇区面积最小,但是存储的数据量和其他扇区相同,因此内侧扇区数据密度最大。
磁盘读写数据过程如下:
首先需要把"磁头"移动到想要读
此时磁头会由磁头臂带动。磁头臂的活动范围是一个弧。
磁盘会转起来,让目标扇区从磁头下面划过,才能完成对扇区的读
实际上,磁盘中会有多个盘面累起来的。相应的每个盘面上都有一个磁头,这些磁头都会由磁头臂移动方向。同时可以给盘面进行编号。
所有的磁头都是连在同一个磁臂上的,因此所有磁头只能"共进退"。另外所有盘面中相对位置相同的磁道组成柱面。
如上图,所有黄色部分相对位置相同,所以这些磁道组成了一个柱面。
所以磁盘物理地址可以用三元组(柱面号,盘面号,扇区号)来定位任意一个"磁盘块"。在"文件的物理结构"小节中,我们经常提到文件数据存放在外存中的几号块,这个块号就可以转换成(柱面号,盘面号,扇区号)的地址形式。
根据该地址读取一个"块"的过程:
磁盘分类:
按照磁头可不可以移动分为:
活动头磁盘
磁头可以移动的称为活动头磁盘。磁臂可以来回伸缩来带动磁头定位磁道。
固定头磁盘
磁头不可移动的称为固定头磁盘。这种磁盘中每个磁道都会有一个磁头。
如果按照盘片是否可以更换分类:盘片可以更换的称为可换盘磁盘。盘片不可更换的称为固定盘磁盘。
磁盘结构总结:
一次磁盘读
寻找时间(寻道时间)
延迟时间
假设上图磁头要读取橙色部分,则旋转磁盘,当磁头到橙色部分起始位置就是所需要的时间就是延迟时间。
传输时间
可以看到延迟时间和传输时间都与磁盘转速相关,且为线性相关。而转速是硬件的固有属性,因此操作系统也无法优化延迟时间和传输时间。所以操作系统唯一能影响时间是寻道时间。根据不同的调度算法寻道时间各不相同。磁盘调度算法有四种:先来先服务(FCFS)、最短寻找时间优先(SSTF)、扫描算法(SCAN)、循环扫描算法(C-SCAN)。
根据进程请求访问磁盘的先后顺序进行调度。
假设磁头的初始位置是
按照FCFS的规则,按照请求到达的顺序,磁头需要依次移动到
所以磁头在
接下来移动到
之后依次移动,直到移动到
整个过程分析下来,发现磁头总共移动了
算法优点:公平,如果请求访问的磁道比较集中的话,算法性能还算过的去。
缺点:如果有大量进程竞争使用磁盘,请求访问的磁道很分散,则FCFS在性能 上很差,寻道时间长。
SSTF算法会优先处理的磁道是与当前磁头最近的磁道(且还没有处理)。可以保证每次的寻道时间最短,但是并不能保证总的寻道时间最短。(其实就是贪心算法的思想,只是选择眼前最优,但是总体未必最优)
假设磁头的初始位置是
首先磁头初始位置是
此时离磁头最近且还没有处理的磁道是
磁头总共移动了
算法优点是:性能较好,平均寻道时间短。缺点:可能产生"饥饿"现象。
如:本例中,如果在处理
产生饥饿的原因在于:磁头在一个小区域内来回来去地循环移动。为了解决这个问题提出了扫描算法。
由于SSTF算法会产生饥饿的原因在于:磁头有可能在一个小区域内来回来去地移动。为了防止这个问题,可以规定,只有磁头移动到最外侧磁道的时候才能往内移动,移动到最内侧磁道的时候才能往外移动。这就是扫描算法(SCAN) 的思想。由于磁头移动的方式很像电梯,因此也叫电梯算法。
假设某磁盘的磁道为
磁头会先访问
之后会依次往内移动到
移动之后由于SCAM算法会要求只有磁头移动到最外侧磁道的时候才能往内移动,所以即使
只有到了最边上的磁道才能改变磁头移动方向,所以接下来就会访问最近没有访问的
磁头总共移动了
算法优点:性能较好,平均寻道时间较短,不会产生饥饿现象。
缺点:
扫描算法(SCAN) 中,只有到达最边上的磁道时才能改变磁头移动方向,事实上,处理了
假设某磁盘的磁道为
磁头会先访问
之后会依次往内移动到
由于LOOK算法规定如果在磁头移动方向上已经没有别的请求,就可以立即改变磁头移动方向。所以接下来会处理
之后依次往后访问直到访问到
所以磁头总共移动了
优点:比起SCAN算法来,不需要每次都移动到最外侧或最内侧才改变磁头方向,使寻道时间进一步缩短。
SCAN算法对于各个位置磁道的响应频率不平均,而C-SCAN算法就是为了解决这个问题。规定只有磁头朝某个特定方向移动时才处理磁道访问请求,而返回时直接快速移动至起始端而不处理任何请求。
假设某磁盘的磁道为
磁头会先访问
之后会依次往内移动到
算法规定只有到了最边上的磁道才能改变磁头移动方向。。所以会到
之后磁头会从
此时磁头会开始下一轮扫描,同样磁头向右移动的时候才可以处理请求。所以接下来会访问
此时访问完所有要求磁道。磁头总共移动了
优点:比起SCAN来,对于各个位置磁道的响应频率很平均。
缺点:只有到达最边上的磁道时才能改变磁头移动方向,事实上,处理了
C-SCAN算法的主要缺点是只有到达最边上的磁道时才能改变磁头移动方向,并且磁头返回时不一定需要返回到最边缘的磁道上。C-LOOK 算法就是为了解决这个问题。如果磁头移动的方向上已经没有磁道访问请求了,就可以立即让磁头返回,并且磁头只需要返回到有磁道访问请求的位置即可。
磁头会先访问
之后会依次往内移动到
如果在磁头移动方向上已经没有别的请求,就可以立即让磁头返回。并且磁头只需要移动到最靠左的需要被访问的磁道,即
接下来磁头就可以向右移动,依次处理完所有请求,即到
磁头总共移动了
优点:比起C-SCAN算法来,不需要每次都移动到最外侧或最内侧才改变磁头方向,使寻道时间进一步缩短。
若题目中无特别说明,则SCAN就是LOOK,C-SCAN就是C-LOOK。
磁盘调度算法总结:
之前学习过,一次磁盘读
假设要连续读取橙色区域的
首先磁头会读取
所以如果向读取
结论:磁头读入一个扇区数据后需要一小段时间处理,如果逻辑上相邻的扇区在物理上也相邻,则读入几个连续的逻辑扇区,可能需要很长的"延迟时间"。
解决这个问题可以采用以下方法:
若采用交替编号的策略,即让逻辑上相邻的扇区在物理上有一定的间隔,可以使读取连续的逻辑扇区所需要的延迟时间更小。
即让逻辑上相邻的扇区,在物理上有一定的间隔,如上图,
可以看到采用这种方法,当访问两个逻辑上相连的地址时,可以有效地减少延迟时间。
为什么磁盘的物理地址是(柱面号,盘面号,扇区号)而不是( 盘面号,柱面号,扇区号)。
假设某磁盘有
若物理地址结构是(盘面号,柱面号,扇区号),且需要连续读取物理地址
若物理地址结构是(柱面号,盘面号,扇区号),且需要连续读取物理地址
所以柱面号在盘面号之前是因为读取地址连续的磁盘块时,采用(柱面号,盘面号,扇区号)的地址结构可以减少磁头移动消耗的时间。
假设两个盘面如下:
其中,
方案一:若相邻的盘面相对位置相同处扇区编号相同。
读取完磁盘块
为了解决这个问题方案二:采用错位命名法, 即将盘面如相同所示堆叠:
由于采用错位命名法,因此读取完磁盘块
减少延迟时间的方法:
磁盘初始化过程:
引导块:
计算机开机时需要进行系列初始化的工作,这些初始化工作是通过执行初始化程序(自举程序)完成的。
初始化程序可以放在ROM(只读存储器)中。ROM中的数据在出厂时就写入了,并且以后不能再修改。
注意:ROM一般是出厂时就集成在主板上的。
自举程序放在ROM会产生一个问题:万一需要更新自举程序,将会很不方便,因为ROM中的数据无法更改。如何解决呢?
实际上ROM中只存放很小的"自举装入程序"。而完整的自举程序放在磁盘的启动块(即引导块
所以在开始时计算机先运行"自举装入程序",通过执行该程序就可找到引导块,并将完整的"自举程序"读入内存,完成初始化。一般来说拥有启动分区的磁盘称为启动磁盘或系统磁盘(如:C盘)。
坏块的管理:
即坏了、无法正常使用的扇区就是"坏块"。这属于硬件故障,操作系统是无法修复的。应该将坏块标记出来,以免错误地使用到它。
对于简单的磁盘,可以在逻辑格式化时(建立文件系统时)对整个磁盘进行坏块检查,标明哪些扇区是坏扇区,比如:在FAT表上标明。( 在这种方式中,坏块对操作系统不透明)。
而对于复杂的磁盘,磁盘控制器(磁盘设备内部的一个硬件部件)会维护一个坏块链表。在磁盘出厂前进行低级格式化(物理格式化)时就将坏块链进行初始化。
磁盘的管理总结:
固态硬盘存储技术是基于闪存技术(Flash Memory),属于电可擦除ROM,即EEPROM。
SSD组成有两部分:
闪存翻译层:负责翻译逻辑块号,找到对应页(Page)。
存储介质层:有多个闪存芯片,每个芯片包含多个块,每个块包含多个页。
固态硬盘的读写是以页位单位的,相当于磁盘的一个扇区。固态硬盘的一个块,相当于磁盘的磁道。即系统通过
SSD读写性能:是以页为单位进行读写的。以块为单位进行擦除,擦干净的块,其中的每页都能写一次,被读无限次。且支持随机访问,系统给定一个逻辑地址,闪存翻译层可以通过电路迅速定位到对应的物理地址。
如果要写的页有数据,则不能写入,需要将有数据的块内其他页全部复制到一个新的(擦除过的)块中,再在新的块中写入想要写入的数据,最后将原来的块内所有页擦除。此时地址会放生改变,但闪存翻译层会修正写入块的地址。这就导致了SSD读的速度比写的速度快很多。
固态硬盘相较于机械硬盘特点:
由于上面的SSD一个块擦除次数过多可能会坏掉,所以现代SSD都会使用磨损均衡技术。
磨损均衡技术思想:将"擦除"平均分布在各个块上,以提升使用寿命。原理还是闪存翻译层有逻辑映射的功能,所以其真实物理地址会变。这种磨损技术分为两种:
SSD总结:
例题:某固态硬盘采用磨损均衡技术,大小为
SSD采用磨损均衡技术,最理想情况下,SSD中每个块被擦除的次数都是完全均衡的。