• 三. 内存管理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. 控制器2. 控制方式2.1 程序直接控制方式执行流程CPU干预的频率与其他条件分析2.2 中断驱动方式执行流程CPU干预的频率与其他条件分析2.3 DMA方式DMA执行流程CPU干预的频率与其他条件分析2.4 通道控制方式执行流程CPU干预的频率与其他条件分析3. 软件层次结构4. 输入输出应用程序接和驱动程序接口4.1 输入输出应用程序接口4.2 设备驱动程序接口5. 核心子系统5.1 假脱机技术(SPOOLing技术)5.2 设备的分配与回收设备分配考虑因素设备分配中使用到的数据结构设备分配的具体步骤5.3 缓冲区管理单缓冲双缓冲双缓冲策略通信的区别循环缓冲区缓冲池6. 磁盘6.1 磁盘调度算法先来先服务(FCFS)最短寻找时间优先(SSTF)扫描算法(SCAM)LOOK算法循环扫描算法(C-SCAN)C-LOOK算法6.2 减少磁盘延迟时间的方法交替编号法磁盘地址结构错位命名法6.3 磁盘的管理7. 固态硬盘SSD

    三. 内存管理

    内存可存放数据。程序执行前需要先放到内存中才能被CPU处理,目的是为了缓和CPU与硬盘之间的速度矛盾。

    由于在计算机组成原理知识可知指令中的地址是逻辑地址,而如何将逻辑地址转换为物理地址这里给出三种策略:绝对装入、可重定位装入(静态重定位)和动态运行时装入(动态重定位)。

    程序的运行过程如下:

    开发人员编写完代码之后,需要进行编译,由编译程序将用户源代码编译成若干个目标模块(编译就是把高级语言翻译为机器语言)。这些模块已经包含目标代码所对应的指令,指令编址都是从逻辑地址0开始,相互独立的。

    接着把目标模块都组装起来形成一个完整的装入模块。在Windows中装入模块就是exe可执行文件。此时装入模块有一个完整的链接地址。

    最后把装入模块装入内存中即可。当装入内存中就确定了进程所对应的实际物理地址是多少。

    程序的执行过程

    上面已经介绍了三种装入方式,但是链接的方式也有三种:

    内存基础知识总结:

    内存基础知识总结

    1. 内存管理的概念

    操作系统作为系统资源的管理者,当然也需要对内存进行管理。操作系统需要对内存进行以下几种管理:

    1. 操作系统负责内存空间的分配与回收。

      程序装入内存后,操作系统要怎么记录哪些内存区域已经被分配出去了,哪些区域还处于空闲状体。当某个进程运行结束之后,要将进程占用的内存空间回收。当一个新的进程需要装入内存时操作系统应该指出放到哪片区域。

    2. 操作系统需要提供某种技术从逻辑上对内存空间进行扩充

      某个游戏的大小超过60GB,按理来说这个游戏程序运行之前需要把60GB数据全部放入内存。然而,实际我的电脑内存才4GB,但操作系统使用虚拟技术(操作系统的虚拟性),就可以运行。把物理上很小得内存拓展为逻辑上很大的内存。

    3. 操作系统需要提供地址转换功能,负责程序的逻辑地址与物理地址的转换。

      为了使编程更方便,程序员写程序时应该只需要关注指令、数据的逻辑地址。而逻辑地址到物理地址的转换(这个过程称为地址重定位)应该由操作系统负责,这样就保证了程序员写程序时不需要关注物理内存的实际情况。

      装入的方式有三种:绝对装入(编译时产生绝对地址)、可重定位装入(装入时将逻辑地址转换为物理地址)、动态运行时装入(运行时将逻辑地址转换为物理地址,需设置重定位寄存器)。

    4. 操作系统需要提供内存保护功能。保证各进程在各自存储空间内运行,互不干扰

      让各个进程只能访问自己的内存空间。

      内存保护

      假设进程1的逻辑地址空间为0179,实际物理地址空间为100279。内存保护的可以采取两种方法:

      方法一:在CPU中设置一对上、下限寄存器,存放进程的上、下限地址。进程的指令要访问某个地址时,CPU检查是否越界。

      内存保护两种方式

      方法二:采用重定位寄存器(又称基址寄存器)和界地址寄存器(又称限长寄存器)进行越界检查。重定位寄存器中存放的是进程的起始物理地址。界地址寄存器中存放的是进程的最大逻辑地址。

      内存保护两种方式2

      假如进程当前要访问一个逻辑地址为80的内存单元,首先逻辑地址会和界地址寄存器值进行对比,如果没有超过界地址寄存器种保存的最大逻辑地址的话,就认为这个地址是合法的。之后会和重定位寄存器的起始物理地址进行相加,最终就可以得到实际想要访问的物理地址。

    2 内存空间的扩充

    内存空间扩充技术有三种:覆盖技术、交换技术和虚拟存储技术(之后会介绍)

    2.1 覆盖技术

    早期的计算机内存很小,比如IBM推出的第一台PC机最大只支持1MB大小的内存。因此经常会出现内存大小不够的情况。后来人们引入了覆盖技术,用来解决"程序大小超过物理内存总和"的问题。

    覆盖技术的思想:将程序分为多个段(多个模块)。常用的段常驻内存,不常用的段在需要时调入内存。

    具体的做法是内存中分为一个"固定区"和若干个"覆盖区"。需要常驻内存的段放在"固定区"中,调入后就不再调出(除非运行结束)不常用的段放在"覆盖区",需要用到时调入内存,用不到时调出内存。

    覆盖技术例子

    如上图,A函数会依次调用B和C函数(注意是依次调用而不是同时调用)。B函数又会调用D函数,而C函数会依次调用E和F函数。

    可以把A函数(包含main()函数)模块放到内存的固定去里,大小就是A模块的大小,即8k。另外由于B模块和C模块不可能同时访问,所以可以让B和C模块共享一个覆盖区即可,覆盖区大小为B和C模块中最大的,即10k。同样的D、E、F模块也会共享一个覆盖区,大小为其中最大的,即12k。

    覆盖技术例子2

    这样只用30k的大小就可以让程序顺利运行。

    这种方式也有很明显的缺点:必须由程序员声明覆盖结构,操作系统完成自动覆盖。所以对用户不透明,增加了用户编程负担。覆盖技术只用于早期的操作系统中,现在已成为历史。

    2.2 交换技术

    交换(对换)技术的设计思想:内存空间紧张时,系统将内存中某些进程暂时换出外存,把外存中某些已具备运行条件的进程换入内存(进程在内存与磁盘间动态调度)。

    之前学到的中级调度(内存调度),就是要决定将哪个处于挂起状态的进程重新调入内存。

    交换技术

    暂时换出外存等待的进程状态为挂起状态(挂起态,suspend)。挂起态又可以进一步细分为就绪挂起、阻塞挂起两种状态。在加入这两种状态后就有了七状态模型。

    七状态

    而此时会产生第一个问题,应该在外存(磁盘)的什么位置保存被换出的进程?

    具有对换功能的操作系统中,通常把磁盘空间分为文件区和对换区两部分。文件区主要用于存放文件,主要追求存储空间的利用率,因此对文件区空间的管理采用离散分配方式。对换区空间只占磁盘空间的小部分,被换出的进程数据就存放在对换区。由于对换的速度直接影响到系统的整体速度,因此对换区空间的管理主要追求换入换出速度,因此通常对换区采用连续分配方式(学过文件管理章节后即可理解)。总之,对换区的I/O速度比文件区的更快。

    对换区和文件区

    第二个问题是什么时候应该交换?

    交换通常在许多进程运行且内存吃紧时进行,而系统负荷降低就暂停。例如:在发现许多进程运行时经常发生缺页,就说明内存紧张,此时可以换出一些进程。如果缺页率明显下降,就可以暂停换出。

    第三个问题是应该换出哪些进程?

    可优先换出阻塞进程。可换出优先级低的进程。为了防止优先级低的进程在被调入内存后很快又被换出,有的系统还会考虑进程在内存的驻留时间。

    注意:PCB会常驻内存,不会被换出外存。所以操作系统换出进程并不是将所有的数据都放入外存,操作系统为了保持对换出进程的管理,进程的PCB信息还是会放在内存中进行管理。

    覆盖与交换总结:

    覆盖与交换总结

    3 连续分配方式

    内存空间的分配与回收有两种方式:连续分配方式和非连续分配方式。连续分配方式又可以进一步细分为:单一连续分配、固定分区分配、动态分区分配。

    连续分配:指为用户进程分配的必须是一个连续的内存空间。非连续分配:为用户进程分配的是一个离散的内存空间。

    3.1 分配方式

    单一连续分配

    在单一连续分配方式中,内存被分为系统区和用户区。系统区通常位于内存的低地址部分,用于存放操作系统相关数据。用户区用于存放用户进程相关数据。内存中只能有一道用户程序,用户程序独占整个用户区空间,并不支持多道程序并发运行。

    单一连续分配

    当一个用户进程所需内存空间很小,但放入内存的用户区之后,用户去中其他的空闲区间,也不会分配给别的用户程序。所以说是整个用户程序独占整个用户区的存储空间。

    单一连续分配2

    所以优点:实现简单。无外部碎片。可以采用覆盖技术扩充内存。不一定需要采取内存保护(如:早期的PC操作系统MS-DOS)

    缺点:只能用于单用户、单任务的操作系统中。有内部碎片。存储器利用率极低。

    内部碎片是指分配给某进程的内存区域中,如果有些部分没有用就是"内部碎片"。

    固定分区分配

    20世纪60年代出现了支持多道程序的系统,为了能在内存中装入多道程序,且这些程序之间又不会相互干扰,于是将整个用户空间划分为若干个固定大小的分区,在每个分区中只装入一道作业,这样就形成了最早的、最简单的一种可运行多道程序的内存管理方式。

    固定分区分配策略有两种:分区大小相等和分区大小不等。

    如果采用分区大小相当这种策略,系统会把用户区整片的内存区间分割为若干个固定大小相等的区域。

    分区大小相等

    如果采用分区大小相当这种策略,系统会把用户区整片的内存区间分割为大小固定,但各个分区大小不相等的区域。

    分区大小不相等

    分区大小相等:缺乏灵活性,但是很适合用于用一台计算机控制多个相同对象的场合(比如:钢铁厂有n个相同的炼钢炉,就可把内存分为n个大小相等的区域存放n个炼钢炉控制程序)。

    而分区大小不等:增加了灵活性,可以满足不同大小的进程需求。根据常在系统中运行的作业大小情况进行划分(比如:划分多个小分区、适量中等分区、少量大分区)。

    另外操作系统需要建立一个数据结构,即分区说明表,来实现各个分区的分配与回收。每个表项对应一个分区,通常按分区大小排列。每个表项包括对应分区的大小、起始地址、状态(是否已分配)。

    分区大小不相等

    如果某个系统分区如下图所示,则对应的分区表如下:

    分区说明表

    用数据结构的数组(或链表)即可表示这个表。当某用户程序要装入内存时,由操作系统内核程序根据用户程序大小检索该表,从中找到一个能满足大小的、未分配的分区,将之分配给该程序,然后修改状态为"已分配"。

    优点:实现简单,无外部碎片。

    缺点:当用户程序太大时,可能所有的分区都不能满足需求,此时不得不采用覆盖技术来解决,但这又会降低性能。同时会产生内部碎片,内存利用率低。

    动态分区分配

    动态分区分配又称为可变分区分配。这种分配方式不会预先划分内存分区,而是在进程装入内存时,根据进程的大小动态地建立分区,并使分区的大小正好适合进程的需要。因此系统分区的大小和数目是可变的。

    如:假设某计算机内存大小为64MB,系统区8MB,用户区共56 MB。则进程1占用20MB、进程2占14MB、进程3占用18MB。

    动态分区分配

    这样会产生三个问题:

    1. 系统要用什么样的数据结构记录内存的使用情况?

      系统中的分区的大小和数量会变的,并且有的分区已经分配,而有的没有分配,所以需要用一个数据结构表示。

    2. 当很多个空闲分区都能满足需求时,应该选择哪个分区进行分配?

      如上图,此时进程2运行结束,移除内存。如果有一个4MB的进程到达,这个新到达的进程要放到哪个内存空间是一个问题。

    3. 如何进行分区的分配与回收操作?

      假设进程3运行结束,移除内存。此时会空出18MB的内存空间,那么这18MB空闲分区处理是要和其他合并还是要进行其他操作。

    接下来会详细介绍这三个问题。

    注:新增一个表项时,各表项的顺序不一定按照上面地址递增顺序排列,具体的排列方式需要依据动态分区分配算法来确定。

    动态分区分配没有内部碎片,但是有外部碎片。

    内部碎片:分配给某进程的内存区域中,如果有些部分没有用上就是内部碎片。 外部碎片:是指内存中的某些空闲分区由于太小而难以利用。

    内部碎片

    如上图所示,假设此时有一个进程5所需要20MB内存空间,但上面每个空闲分区都不够,所以这些空闲的小分区就是外部碎片。如果内存中空闲空间的总和本来可以满足某进程的要求,但由于进程需要的是一整块连续的内存空间,因此这些"碎片"不能满足进程的需求。可以通过紧凑(拼凑,Compaction) 技术来解决外部碎片。

    如上图,虽然每个空闲区都不够进程5放入,但空闲区的总和正好是20MB。此时使用紧凑技术就是将内存中各个进程挪位,腾出一个更到的空闲区。

    内部碎片2

    前面介绍的三种装入方式当中,动态重定位方式显然最方便实现这些进程在内存中移动位置这件事。其实就是将进程起始地址放入重定位寄存器中。

    连续分配管理总结:

    连续分配管理总结

    3.2 动态分区分配算法

    用于解决在动态分区分配方式中,当很多个空闲分区都能满足需求时,应该选择哪个分区进行分配的问题。

    动态分区分配算法有四种:首次适应算法(First Fit)、最佳适应算法(Best Fit)、最坏适应算法(Worst Fit)、邻近适应算法(Next Fit)。

    首次适应算法

    算法思想:每次都从低地址开始查找,找到第一个能满足大小的空闲分区。

    实现方式:空闲分区以地址递增的次序排列。每次分配内存时顺序查找空闲分区链(或空闲分区表),找到大小能满足要求的第一个空闲分区。

    首次适应算法

    某个系统中内存分配如上图所示,采用首次适应算法对应的空闲分区表如下:

    首次适应算法分区表

    上面是按照起始地址由低到高排列。对应的空闲分区链如下:

    首次适应算法分区链

    以空闲分区链为例,当有一个进程5(15MB),其分配到内存中的情况根据首次适应算法的规则,从空闲分区链的链头依次查找找到第一个能够满足大小的分区。经过检查发现第一个20字节的空闲分区已经可以满足要求,所以将进程5分配给20MB的空闲分区中。

    首次适应算法分区链1

    此时这里剩余5MB剩余空间,修改空闲分区链

    首次适应算法分区链2

    如果再来一个进程6(8MB),使用首次适应算法,依次从空闲分区链表头部检查,直到第二个空闲分区10MB的可以满足。所以将进程6放入空闲分区,此时这个空闲分区还剩2MB。最后修改链表对应的位置空闲值即可。

    最佳适应算法

    算法思想:由于动态分区分配是一种连续分配方式,为各进程分配的空间必须是连续的一整片区域。因此为了保证当"大进程"到来时能有连续的大片空间,可以尽可能多地留下大片的空闲区,即,优先使用更小的空闲区。

    实现方式:空闲分区按容量递增次序链接。每次分配内存时顺序查找空闲分区链(或空闲分区表),找到大小能满足要求的第一个空闲分区。即找到的第一个空闲分区一定是能满足进程所需空间,并且最小的空闲分区。

    最佳适应算法

    如果系统中的内存使用情况如上图所示。对应的空闲分区链和空闲分区表如下:

    最佳适应算法分区表和分区链

    是按照空闲分区从小到大递增的顺序排列。假如当前有一个新进程6(9MB),根据最佳适应算法,从空闲分区链表表头开始遍历找到第一个能满足新进程大小的分区即可(同时也能满足是进程所需的最小空闲分区)。这里是第二个空闲分区。

    最佳适应算法分区表和分区链1

    由于最佳适应算法的空闲分区链和空闲分区表需要按照空闲大小的递增次序排列,所以这里新进程占据空闲分区大小为10MB的分区后,需要修改空闲分区链。

    最佳适应算法分区表和分区链2

    缺点:每次都选最小的分区进行分配,会留下越来越多的、很小的、难以利用的内存块。因此这种方法会产生很多的外部碎片。

    最坏适应算法

    又称最大适应算法(Largest Fit),与最佳适应算法相反,算法思想是为了解决最佳适应算法的问题,即留下太多难以利用的小碎片,可以在每次分配时优先使用最大的连续空闲区,这样分配后剩余的空闲区就不会太小,更方便使用。

    实现方式:空闲分区按容量递减次序链接。每次分配内存时顺序查找空闲分区链(或空闲分区表),找到大小能满足要求的第一个空闲分区。

    最佳适应算法

    如果系统中的内存使用情况如上图所示。对应的空闲分区链和空闲分区表如下:

    最坏适应分区表和分区链

    此时有个新进行5(3MB),按照最坏适应算法遍历,从空闲分区链表表头开始遍历找到第一个能满足新进程大小的分区即可(同时也能满足是进程所需的最大空闲分区)。这里是第一个空闲分区。

    最坏适应分区表和分区链1

    同时修改分区链和分区表:

    最坏适应分区表和分区链2

    如果有另一个新进程6(9MB),按照最坏适应算法遍历,从空闲分区链表表头开始遍历找到第一个能满足新进程大小的分区即可(同时也能满足是进程所需的最大空闲分区)。这里是第一个空闲分区。

    最坏适应分区表和分区链3

    同时修改空闲分区链,修改后第一个空闲分区变为8MB,为了维持最坏适应算法递减次序的规则,需要对空闲分区链进行修改。

    最坏适应分区表和分区链4

    缺点:每次都选最大的分区进行分配,虽然可以让分配后留下的空闲区更大,更可用,但是这种方式会导致较大的连续空闲区被迅速用完。如果之,后有"大进程"到达,就没有内存分区可用了。

    邻近适应算法

    算法思想:首次适应算法每次都从链头开始查找的。这可能会导致低地址部分出现很多小的空闲分区,而每次分配查找时,都要经过这些分区,因此也增加了查找的开销。如果每次都从上次查找结束的位置开始检索,就能解决上述问题。

    实现方式:空闲分区以地址递增的顺序排列(可排成一个循环链表)。每次分配内存时从上次查找结束的位置开始查找空闲分区链(或空闲分区表),找到大小能满足要求的第一个空闲分区。

    邻近适应算法

    如果系统中的内存使用情况如上图所示。对应的空闲分区链表如下:

    邻近适应算法分区链

    此时有一个新进程5(5MB),根据邻近适应算法,由于是第一次遍历,需要从头遍历。所以第二个6MB空闲分区能满足。

    邻近适应算法分区链1

    同时修改空闲分区链

    邻近适应算法分区链2

    可以看到采用邻近适应算法和首次适应算法,只需要按照地址递增的次序进行排列即可。所以这里发生内存空闲分区变化,也不用对整个链表进行重现排列。这也是邻近适应算法和首次适应算法比最佳适应算法、最坏适应算法更优秀的地方。

    假如又有一个进程6(5MB),按照邻近适应算法规则,只需要从上一次查找到的位置依次往后查找即可。即从第二个结点开始往后遍历,发现第三个结点能够满足。

    邻近适应算法分区链3

    修改空闲分区链

    邻近适应算法分区链4

    可以看出邻近适应算法不需要从链头位置重新遍历,所以比首次适应算法更快。但这也并不代表比首次适应算法更优秀。

    首次适应算法每次都要从头查找,每次都需要检索低地址的小分区。但是这种规则也决定了当低地址部分有更小的分区可以满足需求时,会更有可能用到低地址部分的小分区,也会更有可能把高地址部分的大分区保留下来(所以首次适应算法包含了最佳适应算法的优点)。

    邻近适应算法的规则可能会导致无论低地址、高地址部分的空闲分区都有相同的概率被使用,也就导致了与首次适应算法相比,高地址部分的大分区更可能被使用,划分为小分区,最后导致无大分区可用(邻近适应算法拥有最坏适应算法的缺点)。

    四种算法中,首次适应算法的效果最好。

    四种动态分区分配算法总结:

    四种算法对比

    4. 非连续分配的管理方式

    非连续分配:为用户进程分配的可以是一些分散的内存空间。

    如果一个系统支持分页存储的话,那么系统会把内存分为一个一个大小相等的区域。

    分页存储

    如上图所示每个区域是4KB。每个分区就是一个"页框"(页框=页帧=内存块=物理块=物理页面)。每个页框有一个编号,即"页框号"(页框号=页帧号=内存块号=物理块号=物理页号),页框号从0开始。

    为了把进程数据和指令放入页框中,操作系统会把各个进程的逻辑地址空间分为与页框大小相等的单元。

    分页存储进程

    某个进程逻辑地址空间如上图所示,可以看到该进程是一个大小为16KB的进程,所以如果某个系统内存中页框大小是4KB,则该进程分为与内存页框相等的部分,因此每个部分是4KB。

    分页存储进程2

    上面将进程的逻辑地址空间也分为与页框大小相等的一个个部分,每个部分称为一个"页"或"页面",每个页面也有一个编号,即"页号"。页号也是从0开始。如上图系统给进程的各个进行编号,这个编号就是"页号"(如进程A_0)或者叫页编号。进程的各个页会被放到内存的各个页框当中,即操作系统以页框为单位为各个进程分配内存空间,所以进程的每个页面分别放入一个页框中。也就是说,进程的页面与内存的页框有一一对应的关系。

    分页存储进程3

    同时各个页面不必连续存放,可以放到不相邻的各个页框中。

    这种一一对应的关系需要用数据结构"页表"来记录。即为了能知道进程的每个页面在内存中存放的位置,操作系统要为每个进程建立一张页表。而页表通常存在PCB(进程控制块)中。

    上面提到过,进程的逻辑地址空间会被分为一个个的页面。每一个页面就会对应页表当中的一个页表项(一行)。而每个页表项由"页号"和"块号"组成。

    页表

    所以这样一个页表就可以记录下来进程各个页对应的实际存放的内存块之间的映射关系。

    页表表示

    在计算机内部,地址是用二进制表示的,如果页面大小刚好是2的整数幂,则计算机硬件可以很快速的把逻辑地址拆分成(页号,页内偏移量)

    假设某计算机用32个二进制位表示逻辑地址,页面大小为4KB=212B=4096B

    确定逻辑地址的页号2

    12位代表页内偏移量,前20位代表第几页。

    确定逻辑地址的页号3

    结论1:如果每个页面大小为2KB,用二进制数表示逻辑地址,则末尾K位即为页内偏移量,其余部分就是页号。

    假设通过查询页表得知1号页面存放的内存块号是9(1001),则9号内存块的起始地址=94096=00000000000000001001000000000000 则逻辑地址4097对应的物理地址=页面在内存中存放的起始地址+页内偏移量=(00000000000000001001000000000001)

    结论2:如果页面大小刚好是2的整数幂,则只需把页表中记录的物理块号拼接上页内偏移量就能得到对应的物理地址。

    总结:页面大小刚好是2的整数幂有以下好处:

    1. 逻辑地址的拆分更加迅速,如果每个页面大小为2KB,用二进制数表示逻辑地址,则末尾K位即为页内偏移量,其余部分就是页号。因此,如果让每个页面的大小为2的整数幂,计算机硬件就可以很方便地得出一个逻辑地址对应的页号和页内偏移量,而无需进行除法运算,从而提升了运行速度。
    2. 物理地址的计算更加迅速,根据逻辑地址得到页号,根据页号查询页表从而找到页面存放的内存块号,将二进制表示的内存块号和页内偏移量拼接起来,就可以得到最终的物理地址。

    所以分页存储管理的逻辑地址结构如下所示:

    逻辑地址结构

    如果有K位表示"页内偏移量",则说明该系统中一个页面的大小是2K个内存单元。如果有M位表示"页号",则说明在该系统中,一个进程最多允许有2M个页面。页面大小、业内偏移量和逻辑地址结构三个关系如下:

    4.1 基本分页存储管理

    基本地址变换机构

    所谓的基本地址变换机构就是在基本分页存储管理当中,用于实现逻辑地址到物理地址转换的一组硬件机构。这里要掌握原理和流程。

    在分页存储管理当中,如果要把逻辑地址转换成物理地址,需要知道逻辑地址对应的页号、逻辑地址对应的页内偏移量、逻辑地址对应的页面在内存中存放的位置、最后根据页面在内存当中的起始位置和页内偏移量就可以得到最终的物理地址。

    为了实现这个地址转换的功能,系统中会设置一个页表寄存器,用来存放页表在内存中的起始地址F和页表长度M。进程未执行时,页表的起始地址和页表长度放在进程控制块(PEB) 中,当进程被调度时,操作系统内核会把它们放到页表寄存器中。

    设页面大小为L,逻辑地址A到物理地址E的变换过程如下:

    1. 计算页号P和页内偏移量W(如果用十进制数手算,则P=A/LW=A%L。但是在计算机实际运行时,逻辑地址结构是固定不变的,因此计算机硬件可以更快地得到二进制表示的页号、页内偏移量)。
    2. 比较页号P和页表长度M,若PM,则产生越界中断,否则继续执行。(注意:页号是从0开始的,而页表长度至少是1,因此P=M时也会越界)
    3. 页表中页号P对应的页表项地址=页表起始地址F+页号P页表项长度,取出该页表项内容b,即为内存块号。(注意区分页表项长度、 页表长度、页面大小的区别。页表长度指的是这个页表中总共有几个页表项,即总共有几个页。页表项长度指的是每个页表项占多大的存储空间。页面大小指的是一个页面占多大的存储空间)。
    4. 计算E=bL+W,用得到的物理地址E去访存。( 如果内存块号、页面偏移量是用二进制表示的,那么把二者拼接起来就是最终的物理地址了)

    逻辑地址转换为物理地址过程

    例:若页面大小L为1K字节,页号2对应的内存块号b=8,将逻辑地址A=2500转换为物理地址E

    等价描述:某系统按字节寻址,逻辑地址结构中,页内偏移量占10位(说明一个页面的大小为210B=1KB),页号2对应的内存块号b=8,将逻辑地址A=2500转换为物理地址E。计算如下:

    1. 计算页号、页内偏移量

      页号P=AL=25001024=2;页内偏移量W=A%L=2500%1024=452

    2. 根据题中条件可知,页号2没有越界,起存放的内存块号b=8

    3. 物理地址E=bL+W=81024+425=8644

    在分页存储管理(页式管理)的系统中,只要确定了每个页面的大小,逻辑地址结构就确定了。因此,页式管理中地址是一维的。即,只要给出一个逻辑地址,系统就可以自动地算出页号、页内偏移量两个部分,并不需要显式地告诉系统这个逻辑地址中,页内偏移量占多少位。

    上小节知道每个页表项的长度是相同的,页号是"隐含"的。假设某系统物理内存大小为4GB,页面大小为4KB,内存总共会被分为232/212=220个内存块,因此内存块号的范围应该是02201。因此至少要20个二进制位才能表示这么多的内存块号,所以至少要3个字节才够(每个字节8个二进制位,3个字节共24个二进制位)。

    页表项的进一步探讨

    各页表项会按顺序连续地存放在内存中。如果该页表在内存中存放的起始地址为X,则M号页对应的页表项是存放在内存地址为X+3M。一个页面为4KB,则每个页框可以存放4096/3=1365个页表项,但是这个页框会剩余4096%3=1B页内碎片。因此,1365号页表项存放的地址为X+31365+1

    页表项的进一步探讨2

    但如果每个页表项占4字节,则每个页框刚好可存放1024个页表项。1024号页表项虽然是存放在下一个页框中的,但是它的地址依然可以用X+41024得出。因为这个页框中没有任何碎片。

    页表项的进一步探讨3

    结论:理论上,页表项长度为3B即可表示内存块号的范围,但是,为了方便页表的查询,常常会让一个页表项占更多的字节,使得每个页面恰好可以装得下整数个页表项。

    上面例子也可以看出进程页表通常是装在连续的内存块中的。

    基本地址变换结构总结:

    基本地址变换结构总结

    可以看出在根据逻辑地址计算出物理地址访问内存单元的整个过程中,总共需要两次访问内存的操作。第一次访问内存是在查询页表的时候,第二次访问内存是在是在实际访问内存单元的时候进行的。

    具有快表的地址变换机构

    是基本地址变换机构的改进版本。在基本地址变换机构的基础上引入快表,就可以让地址变换的过程更快。

    快表,又称联想寄存器(TLB,translation lookaside buffer ),是一种访问速度比内存快很多的高速缓存(TLB不是内存),用来存放最近访问的页表项的副本,可以加速地址变换的速度。与此对应,内存中的页表常称为慢表。

    引入快表后,地址的变换过程:

    1. CPU给出逻辑地址,由某个硬件算得页号、页内偏移量,将页号与快表中的所有页号进行比较。
    2. 如果找到匹配的页号,说明要访问的页表项在快表中有副本,则直接从中取出该页对应的内存块号,再将内存块号与页内偏移量拼接形成物理地址,最后,访问该物理地址对应的内存单元。因此,若快表命中,则访问某个逻辑地址仅需一次访存即可。
    3. 如果没有找到匹配的页号,则需要访问内存中的页表,找到对应页表项,得到页面存放的内存块号,再将内存块号与页内偏移量拼接形成物理地址,最后,访问该物理地址对应的内存单元。因此,若快表未命中,则访问某个逻辑地址需要两次访存(注意:在找到页表项后,应同时将其存入快表,以便后面可能的再次访问。但若快表已满,则必须按照一定的算法对旧的页表项进行替换)

    基本地址快表变换机构

    由于查询快表的速度比查询页表的速度快很多,因此只要快表命中,就可以节省很多时间。因为缓存的局部性原理,一般来说快表的命中率可以达到90%以上。

    例:某系统使用基本分页存储管理,并采用了具有快表的地址变换机构。访问一次快表耗时1us,访问一次内存耗时100us。若快表的命中率为90%,那么访问一个逻辑地址的平均耗时是多少?

    访问依次命中后还需要花100us时间访存,且这样情况概率为90%。但如果没有缓存命中,则需要访问两次缓存再加上一次快表时间,这样情况概率为10%。故访问一个逻辑地址的平均耗时是(1+100)0.9+(1+100+100)0.1=111us

    而有的系统支持快表和慢表同时查找,如果是这样,平均耗时应该是(1+100)0.9+(100+100)0.1=110.9us。

    快表慢表同时查找的甘特图如下:

    快表慢表一起查找

    可以很直观看到,当快表查询同时,慢表也在查询,经过1us时间后,如果快表缓存没有命中,那么此时慢表已经查了1us。再接着查慢表即可。

    具有快表的地址变换机构与基本地址变换机构对比:

    具有快表的地址变换机构与基本地址变换机构对比

    TLB和普通Cache的区别:TLB中只有页表项的副本,而普通Cache中可能会有其他各种数据的副本。

    4.2 两级页表

    某计算机系统按字节寻址,支持32位的逻辑地址,采用分页存储管理,页面大小为4KB,页表项长度为4B。

    4KB=212B,因此页内地址要用12位表示,剩余20位表示页号。因此,该系统中用户进程最多有220页。相应的,一个进程的页表中,最多会有220=1M=1048576个页表项,所以一个页表最大需要2204B=222B,共需要222/212=210个页框存储该页表。

    根据页号查询页表的方法:K号页对应的页表项存放位置=页表始址+K4,要在所有的页表项都连续存放的基础上才能用这种方法找到页表项。所以需要专门给进程分配210=1024个连续的框来存放它的页表。

    根据局部性原理可知,很多时候,进程在一段时间内只需要访问某几个页面就可以正常运行了。因此没有必要让整个页表都常驻内存。

    所以单级页表页表有以下问题:

    1. 问题一:页表必须连续存放,因此当页表很大时,需要占用很多个连续的页框。
    2. 问题二:没有必要让整个页表常驻内存,因为进程在一段时间内可能只需要访问某几个特定的页面。

    解决单级页表第一个问题

    问题一:页表必须连续存放,因此当页表很大时,需要占用很多个连续的页框。

    解决第一个问题可以参考解决进程在内存中必须连续存储的问题,做法是将进程地址空间分页,并为其建立一张页表,记录各页面的存放位置。同样的思路也可用于解决"页表必须连续存放"的问题,把必须连续存放的页表再分页。

    具体的做法是:

    可将长长的页表进行分组,使每个内存块刚好可以放入一个分组(比如上个例子中,页面大小4KB,每个页表项4B,每个页面可存放1K个页表项,因此每1K个连续的页表项为一组, 每组刚好占一个内存块,再将各组离散地放到各个内存块中)另外,要为离散分配的页表再建立一张页表,称为页目录表,或称外层页表,或称顶层页表。

    假如:某计算机系统按字节寻址,支持32位的逻辑地址,采用分页存储管理,页面大小为4KB,页表项长度为4B。

    两级页表

    由于单级页表的长度过大,所以可以将这个页表拆分成一个个的小分组,每个小分组的大小刚好可以装入一个内存块。由于每个页面的大小是4KB,每个页表项的大小是4B,所以一个页面可以存放4K/4=210=1024个页表项。因此可以将该页表拆分成一个个的小分组,每个分组页表项有1024个。

    两级页表2

    另外可以对这些小页表进行编号(如0#号页表),进行这样的拆分后,最后就会形成1024个小页表。

    在把大页表拆分成小页表之后,由于每一个页表的大小都是4KB,因此每个小页表都可以依次放入不同的内存块中。

    两级页表3

    为了记录这些小页表之间的相对顺序,以及在内存中存放的块号位置,需要为这些小页表再建立更上一级的页表,及页目录表(或者叫顶级页表、处层页表)。相应的这层小页表可以统称为二级页表。

    页目录表

    从这个图中可以直观看出,页目录表其实是建立了,二级页表的页号,及二级页表在内存中存放的块号之间的映射关系。

    页目录表2

    所以此时如果想找0号页表可以通过页目录表就可以知道,0号页表存放在3号内存卡中,只需要在主存3号内存块中找0号页表即可。

    采用这样的两级页表结构后逻辑地址的结构也需要发生变化。可以把以前20位的页号拆分成两个部分,第一个部分是10位的二进制用来表示一级页号。第二部分也是10位二进制用来表示二级页号。10位一级页号刚好可以表示01023这样的范围。相应的二级页号用来表示二级页表的页号。

    两级页表的逻辑地址结构

    例:将逻辑地址0000000000,0000000001,1111111111转换为物理地址(用十进制表示)。

    1. 按照地址结构将逻辑地址拆分成三部分

    2. 从PCB中读出页目录表始址,再根据一级页号查页目录表,找到下一级页表在内存中的存放位置

      假设页目录表如下所示:

      页目录表的位置

      则其一级页号转化为十进制是0,所以对应内存块号是3

      页目录表的位置2

    3. 根据二级页号查表,找到最终想访问的内存块号

      可以从这个位置读出二级页表,再用二级页号进行查询。

      页目录表的位置3

      二级页号转换为十进制是1,所以对应的内存块号是4,即要访问的地址是在4号内存块中。

      页目录表的位置4

    4. 结合页内偏移量得到物理地址

      最终要访问的内存块号为4,并且每个内存块大小是1KB,所以该内存块的起始地址为44096=16384。且逻辑地址页内偏移量转换为十进制是1023,故最终的物理地址为16384+1023=17407

    解决单级页表第二个问题

    问题二:没有必要让整个页表常驻内存,因为进程在一段时间内可能只需要访问某几个特定的页面。

    可以在需要访问页面时才把页面调入内存(虚拟存储技术)。可以在页表项中增加一个标志位,用于表示该页面是否已经调入内存。

    虚拟存储

    若想访问的页面不在内存中,则产生缺页中断(内中断),然后将目标页面从外存调入内存。之后会有更详细介绍。

    需要注意的细节:

    1. 若采用多级页表机制,则各级页表的大小不能超过一个页面

      例:某系统按字节编址,采用40位逻辑地址,页面大小为4KB,页表项大小为4B,假设采用纯页式存储,则要采用()级页表,页内偏移量为多少位?

      页面大小=4KB=212B,按字节编址,因此页内偏移量为12位。所以页号=4012=28位。

      页面大小=212B,页表项大小=4B,则每个页面可存放212/4=210个页表项。因此各级页表最多包含210个页表项,需要10位二进制位才能映射到210个页表项,因此每一级的页表对应页号应为10位。总共28位的页号至少要分为三级。

      页表大小不能超过一个页面

      如果只分为两级页表,则一级页号占18位,也就是说页目录表中最多可能有218个页表项,显然,一个页面是放不下这么多页表项的。

    2. 两级页表的访存次数分析(假设没有快表机构)

      第一次访存:访问内存中的页目录表

      第二次访存:访问内存中的二级页表

      第三次访存:访问目标内存单元

      在没有快表机构的情况下,n级页表在访问逻辑地址的时候,访存次数是n+1次。

    两级页表总结:

    两级页表总结

    4.3 基本分段存储管理方式

    与"分页存储"最大的区别就是,离散分配时所分配地址空间的基本单位不同。

    分段概念

    进程的地址空间会按照程序自身的逻辑关系划分为若干个段,每个段都有一个段名(在低级语言中,程序员使用段名来编程),每段从0开始编址。

    假如一个进程是16KB,可以按照自身的逻辑关系可以分为若干个段,每个段代表一个完整的逻辑模块。

    分段

    可以看到每一个段都会有一个段名,是开发人员在编程的时候使用,另外每个段的地址都是从0开始编址的。

    分段2

    操作系统为用户进程分配内存规则:以段为单位进行分配,每个段在内存中占据连续空间,但各段之间可以不相邻。

    分段3

    由于分段存储管理当中是按逻辑功能模块划分,用户编程更方便,程序的可读性更高。

    由于各个分段按逻辑功能模块划分,并且上面的段名也是用户自己定义的,所以用户在读这段程序的时候可读性很高。

    在用户编程的时候使用段名表示,而编译程序会将段名转换为与之对应的段号。CPU根据这些段号来区分各个段。

    采用这种分段机制后逻辑地址变为:段号(段名)和段内地址(段内偏移量)所组成。

    分段逻辑地址

    注意:段号的位数决定了每个进程最多可以分几个段,段内地址位数决定了每个段的最大长度是多少

    在上图中,若系统是按字节寻址的,则段号占16位,因此在该系统中,每个进程最多有216=64K个段。段内地址占16位,因此每个段的最大长度是216=64KB。

    而上面的汇编代码,在写程序时使用的段名[D]、[X] 会被编译程序翻译成对应段号。而像<A>单元、<B>单元会被编译程序翻译成段内地址。

    分段地址

    程序分多个段,各段离散地装入内存,为了保证程序能正常运行,就必须能从物理内存中找到各个逻辑段的存放位置。为此,需为每个进程建立一张段映射表,简称"段表"。段表和之前的页表作用类似。

    段表

    之前学习的页表是建立了各个逻辑页面到实际的物理页框之间的映射关系。而段表是记录了各个逻辑段到实际的五路内存存放位置之间的映射关系。可以看到每个段表由段号、段长、基址组成。

    每个段对应一个段表项,其中记录了该段在内存中的起始位置(又称基址)和段的长度。

    需要注意的是各个段表项的长度是相同的。例如:某系统按字节寻址,采用分段存储管理,逻辑地址结构为(段号16位,段内地址16位),因此16位即可表示最大段长。如果某个内存物理大小为4GB (可用32位表示整个物理内存地址空间),因此,可以让每个段表项占16+32=48位,即6B。由于段表项长度相同,因此段号可以是隐含的,不占存储空间。若段表存放的起始地址为M,则K段对应的段表项存放的地址为M+K6

    地址表示

    有以下一段汇编代码:

    经过编译程序编译后,形成等价的机器指令:取出段号为2,段内地址为1024的内存单元中的内容,放到寄存器1中。

    CPU执行指令时需要将逻辑地址变换为物理地址。上面逻辑地址机器指令用二进制表示: 00000000000000100000000100000000

    逻辑地址转换为物理地址详细过程:

    在内存的系统区当中存放着很多用于管理系统当中软硬件的数据结构,包括进程控制模块PCB也在系统区当中。当一个进程要上处理机运行之前,进程切换相关的内核程序负责恢复进程运行环境,其中包括一个很重要的段表寄存器硬件。其值是进程的段表在进程当中的起始地址和段表长度。因此当进程还没有运行时,段表起始地址和段表长度是存放在进程的PCB当中,当进程上处理机运行时,这两个信息会放到速度很快的段表寄存器当中。

    逻辑地址转换为物理地址

    当知道段表的起始地址后,就可以知道段表存放在内存中哪个位置。接下来这个进程在运行过程中避免不了要访问一些逻辑地址,如访问逻辑地址A,系统会根据逻辑地址得到段号S和段内地址W,知道了段号之后需要和段表长度做一个对比判断段号是否越界。如果段号段表长度,就会产生越界中断。之后会由中断程序负责处理中断。这里需要特别注意段表长度至少是1,而段号从0开始。

    逻辑地址转换为物理地址2

    确定段号是合法的没有越界之后,就会根据段号S和段表起始地址F来查询段表,找到段号对应的段表项。之前提到过由于各个段表项的大小相同,所以用段表起始地址+段号段表项长度,就可以找到要找的目标段对应的段表项在内存当中的位置。接着就可以读出这个段表项的内容。

    逻辑地址转换为物理地址3

    在找到了段号对应的段表项之后,系统还会对逻辑地址当中的段内地址W进行检查,看看是否已经超过了这个段的最大段长。如果段内地址段长,则会产生越界中断,否则继续执行。这要是与页式段长区别最大的一步,因为在页式管理当中每个页面的页长是一样的,所以系统并不需要检查页内偏移量是否超过了页面长度。但是在分段存储管理方式当中,各个段的长度不一样,所以一定需要对段内地址进行越界检查。

    此时已经找到目标段的段表项,所以就知道目标段在内存中存放的位置了。最后根据这个段基址b+段内地址W,就可以得到最终的物理地址E

    逻辑地址转换为物理地址4

    假设逻辑地址A(2,1024),首先需要用段号2和段表长度M进行检查。显然此时这个进程的段表长度是3,因为有三个段表项。所以段号段表长度的。因此段号合法。继续下一步,用段号和段表起始地址查到对应的段表项,即2号段表项。接着需要对段内地址的合法性进行检查。段内地址和段长对比,2号段长是6K,而段内地址是1024=1K。显然不会产生越界中断,可以继续执行下去。接着通过段表项知道这个段在内存当中的存放起始地址是40K,所以用段的起始地址40K+段内地址1024,这样就得到最终要访问的目标内存单元。也就是内存变量<A>存放的位置。

    分段与分页管理的对比

    页是信息的物理单位。分页的主要目的是为了实现离散分配,提高内存利用率。分页仅仅是系统管理上的需要,完全是系统行为,对用户是不可见的

    段是信息的逻辑单位。分段的主要目的是更好地满足用户需求。一个段通常包含着一组属于一个逻辑模块的信息。分段对用户是可见的,用户编程时需要显式地给出段名。

    页的大小固定且由系统决定。段的长度却不固定,决定于用户编写的程序。

    从地址空间的角度来说,分页的用户进程地址空间是一维的,程序员只需给出一个记忆符即可表示一个地址。

    分段分页管理的对比

    如上图所示,用户可以用记忆符<A>来表示某个页面当中的内存单元。

    而用分段的用户进程地址空间是二维的,程序员在标识一个地址时,既要给出段名,也要给出段内地址。

    分段分页管理的对比2

    如上图所示,用户需要显示给出段名[D]和段内地址<A>。因此在分页管理当中,在用户自己看来的进程地址空间是连续的。但是在分段存储管理当中用户知道自己的进程地址空间是被分为一个个段,并且每个段会占据一连串的连续地址空间。

    分段比分页更容易实现信息的共享和保护

    假如有一个生产者进程(16KB),将其分为三段,其中的一号段该功能段用来判断缓冲区此时是否可访问。除了这个生产者进程之外,其他所有生产者、消费者进程也需要判断缓冲区此时是否需要访问,因此1号段中的代码允许所有的生产者进程和消费者进程共享访问。

    分段分页管理的对比3

    假设当前的这个生产者进程有一个段表,它的1号段,即判断缓冲区的段是是存放在内存的120K这个地址开始的内存空间当中。

    分段分页管理的对比4

    如果此时有一个消费者进程想要和生产者进程共享使用一号段,可以让消费者进程的段表项同样指向120K起始地址即可。

    分段分页管理的对比5

    所以,如果要实现共享,只需要让进程的某一个段表项指向同一个内存段即可。

    而不能被修改的代码称为纯代码或可重入代码(不属于临界资源),这样的代码是可以共享的。可修改的代码是不能共享的(比如,有一个代码段中有很多变量,各进程并发地同时访问可能造成数据不一致)。假如,有一个代码段只是简单的输出"Hello World !",那么所有进程并发访问这段代码并没有问题。

    而如果采用分页管理方式:

    分段分页管理的对比6

    如果让消费者进程的某个页表项指向这个页面,显然不合理,因为这个页面中的橙色部分是不允许共享的,只有绿色部分可以。可以看出由于分页管理方式的页面不是按逻辑模块划分的。这就很难实现共享。

    保护的原理也类似,对于分段式存储方式,可以在段表中增加一个"是否允许其他进程访问"的标记列即可。

    分段分页管理的对比7

    而对于分页式管理,一块的内存块中有的可以被访问,有的不能被访问,这要是由于各个页面不是按逻辑划分造成的。因此很难用页表实现信息保护。

    再来看看访问一个逻辑地址需要几次访存

    对于分页(单级页表):第一次访存,要查内存中的页表。第二次访存,访问目标内存单元。总共两次访存

    分段:第一次访存,查内存中的段表。第二次访存,访问目标内存单元。总共两次访存。同时与分页系统类似,分段系统中也可以引入快表机构,将近期访问过的段表项放到快表中,这样可以少一次访问,加快地址变换速度。

    基本分段式存储管理总结:

    基本分段式存储管理总结

    4.4 段页式管理方式

    段页式管理方式是分段、分页这两种管理方式的结合。

    分页、分段的优缺点分析:

    分页、分段的优缺点分析

    分段管理中产生的外部碎片同样可以参考动态分区分配解决,所以也可以用"紧凑"来解决,只是需要付出较大的时间代价

    为了解决上述缺点,可以采用段页式管理方式。

    在采用段页式管理方式的系统当中,一个进程会按照逻辑模块进行分段,再将各段分页(如每个页面4KB)。

    段页式管理

    对于内存来说,内存空间可以分为大小相同的内存块。每个内存块的大小和系统当中页面的大小是一样的,也就是4KB。最后进程的这些页面会被依次放到各个内存块当中。

    段页式管理2

    段页式系统的逻辑地址结构由段号、页号、页内地址(页内偏移量)组成。如:

    段页式系统的逻辑地址结构

    这个地方的页号和页内偏移量就是,分段管理当中的段内地址进行再拆分的一个结果。需要注意的是:段号的位数决定了每个进程最多可以分几个段;页号位数决定了每个段最大有多少页;页内偏移量决定了页面大小、内存块大小是多少。

    在上述例子中,若系统是按字节寻址的,则段号占16位,因此在该系统中,每个进程最多有216=64K个段。页号占4位,因此每个段最多有24=16页。页内偏移量占12位,因此每个页面/每个内存块大小为212=4096=4KB。

    "分段"过程对用户是可见的,程序员编程时需要显式地给出段号、段内地址。而将各段"分页"的过程对用户是不可见的。系统会根据段内地址自动划分页号和页内偏移量。因此段页式管理的地址结构是二维的。

    段表、页表

    与之前的分页式和分段式管理类似,对进程分段再分页之后需要段表和页表。

    所以进程先分段,分段后系统会为各个段建立一个段表。进程当中的各个段会对应段表中的各个段表项。而每个段表项由段号、页表长度、页表存放块号(页表起始地址)组成。每个段表项长度相等,段号是隐含的。

    段表和页表

    可以根据块号即可算出页表存放的内存地址。如0号段对应的页表存放块号式1

    段表和页表2

    于是可以从1号内存块中读出0号段对应的页表。由于0号段是7KB,而每个页面的大小是4KB,所以会被分为两个页面。

    段表和页表3

    相应的这两个页面,会依次对应页表当中的一个页表项。每个页表项记录了每个页面存放的内存块号位置。

    段表和页表4

    可以通过两个页号查询对应页表找到内存中的位置。

    段表和页表5

    所以可以看到,段式管理当中的段表记录的式段号、段长度、段的起始地址。而段页式管理当中记录的是段号、页表长度、页表存放块号,这三个信息,即后面两个不一样。而对于页表来说结构式相同的,都是记录了页号到物理块号的映射关系。

    从上面分析当中可以知道,一个进程只会对应一个段表,而一个段表会对应多个页表。

    地址转换过程

    首先需要知道的是系统中也会有一个段表寄存器这个硬件。在进程上处理机运行之前会从PCB当中读出段表起始地址和段表长度这些信息。之后放到段表寄存器当中。在进行地址转换的时候第一步需要根据逻辑地址得到段号、页号、页内偏移量。第二步需要将段号和段表长度进行一个对比,检查段号是否越界。如果段号合法,接下来可以根据段号和段表起始地址来计算出这个段号对应的段表项在内存当中的位置。这样就找到了想要找的段表项。

    段页式存储地址转换

    接下里需要注意的是由于各个段的长度是不一样的,所以各个段分页之后可以分为数量不等的页面。所以这里需要对页号合法性进行检查,看是否越界。如果页号没有超出页表长度的话就可以继续往下执行。通过这个段表项知道页表存放的位置,于是就可以读出页表。之后就可以根据逻辑地址中的页号找到来找到相应的页表项。找到页表项之后就可以知道这个页面在内存中的存放位置。最后可以根据内存块号、页内偏移量得到最终的物理地址。

    段页式存储地址转换2

    因此在段页式管理当中,进行地址转换的过程总共需要三次访存。第一次访存是访问内存当中的段表,第二次访存是访问内存当中的页表。第三次访存才是访问最终的目标内存单元。同样的也可引入快表机制,用段号和页号作为查谢快表的关键字。若快表命中则仅需一次访存。

    段式存储管理总结:

    段式存储管理总结

    5. 虚拟内存

    在传统存储管理方式(连续分配/非连续分配)的基础上引入了交换技术、覆盖技术,使得内存利用率有所提升,并且能从逻辑上扩充内存容量。而虚拟内存技术也是内存空间的扩充技术,比交换技术和覆盖技术要先进一些。

    虚拟内存是基于高速缓存技术的思想提出的内存管理方案。传统存储方式如下:

    传统存储方式

    系统使用的如果是上面的管理方案,很多暂时用不到的数据也会长期占用内存,导致内存利用率不高。所以传统的存储管理方式有两个很明显的缺点:

    这些缺点可以用虚拟存储技术解决问题。虚拟内存技术遵循局部性原理:

    时间局部性:如果执行了程序中的某条指令,那么不久后这条指令很有可能再次执行。如果某个数据被访问过,不久之后该数据很可能再次被访问。(因为程序中存在大量的循环)。

    空间局部性:一旦程序访问了某个存储单元,在不久之后,其附近的存储单元也很有可能被访问。(因为很多数据在内存中都是连续存放的,并且程序的指令也是顺序地在内存中存放的)。

    基于局部性原理,在程序装入时,可以将程序中很快会用到的部分装入内存,暂时用不到的部分留在外存,就可以让程序开始执行。在程序执行过程中,当所访问的信息不在内存时,由操作系统负责将所需信息从外存调入内存,然后继续执行程序。若内存空间不够,由操作系统负责将内存中暂时用不到的信息换出到外存。

    在操作系统的管理下,在用户看来似乎有一个比实际内存大得多的内存,这就是虚拟内存。

    虚拟内存是操作系统虚拟性的一个体现,实际的物理内存大小没有变,只是在逻辑上进行了扩充。

    计算机中存储器的层次结构

    虚拟内存有一下三个主要特征:

    多次性:无需在作业运行时-次性全部装入内存,而是允许被分成多次调入内存。

    对换性:在作业运行时无需一直常驻内存,而是允许在作业运行过程中,将作业换入、换出。

    虚拟性:从逻辑上扩充了内存的容量,使用户看到的内存容量,远大于实际的容量。

    虚拟内存技术,允许一个作业分多次调入内存。如果采用连续分配方式,会不方便实现。因此,虚拟内存的实现需要建立在离散分配的内存管理方式基础上,即传统的非连续分配存储管理技术:

    传统的非连续分配存储管理技术

    在传统的非连续分配存储的三种管理技术之上运用虚拟内存技术,就形成了与之相对应的请求分页存储管理、请求分段存储管理、请求段页式存储管理。

    虚拟内存的实现

    传统内存管理方式和虚拟内存的管理方式最主要区别是在采用虚拟内存技术,程序在执行的过程中,当所访问的信息不在内存时,由操作系统负责将所需信息从外存调入内存,然后继续执行程序。若内存空间不够,由操作系统负责将内存中暂时用不到的信息换出到外存。

    所以为了满足这两个全新的需求,操作系统需要在基本的存储管理方式的基础上再增加两个主要的功能:

    虚拟内存的基本概念总结:

    虚拟内存的基本概念总结

    5.1 请求分页管理方式

    请求分页存储管理是在基本分页存储管理方式的基础上进行拓展,从而实现的一种虚拟内存管理技术。

    程序在执行的过程中,当所访问的信息不在内存时,由操作系统负责将所需信息从外存调入内存,然后继续执行程序。若内存空间不够,由操作系统负责将内存中暂时用不到的信息换出到外存。

    所以为了满足这两个全新的需求,操作系统需要在基本的存储管理方式的基础上再增加两个主要的功能:

    针对这两个功能如何实现会介绍请求管理方式中页表机制和基本分页存储管理方式种的区别。另外为了实现请求调页功能请求分页管理系统引入了缺页中断机构

    请求分页管理方式

    页表机制对比

    与基本分页管理相比,请求分页管理中,为了实现"请求调页",操作系统需要知道每个页面是否已经调入内存。如果还没调入,那么也需要知道该页面在外存中存放的位置。所以为了知道这些信息,肯定需要把这些信息记录在页表这种数据结构中。

    另外当内存空间不够时,要实现"页面置换",操作系统需要通过某些指标来决定到底换出哪个页面。有的页面没有被修改过,就不用再浪费时间写回外存。有的页面修改过,就需要将外存中的旧数据覆盖,因此,操作系统也需要记录各个页面是否被修改的信息。

    所以相对于基本分页存储管理的页表来说,请求分页存储管理的页表增加了:

    1. 状态位

      表示这个页面是不是已经调入内存了

    2. 访问字段

      操作系统在置换页面的时候可以根据访问字段的数据来决定到底要换出哪个页面。所以可以在访问字段当中记录每个页面最近被访问过几次。可以选择把访问次数更少的换出外存。也可以记录上次访问时间,这样就可以换出很久没有访问的页面。

    3. 修改位

      用来标记这个页面在调入内存后是否被修改过。如果没有修改,那么就不需要写回内存可以节省时间。

    4. 外存地址

      各个页面在外存中存放的地址

    页表机制

    缺页中断机制

    例子:假设此时要访问逻辑地址=(页号,页内偏移量)=(0,1024)。对应的页表如下:

    缺页中断机制

    为了访问这个逻辑地址需要查询页表,缺页中断机构会根据对应的页表项来判断此时这个页面是否已经在内存中。如果没有在内存当中,即状态位为0,此时会产生一个缺页中断信号。之后操作系统的缺页中断处理程序会负责处理这个中断。由于中断处理过程需要I/O操作把页面从外存调入内存,所以在等待I/O操作完成的过程当中,之前发生缺页的进程会阻塞,放入阻塞队列中。只有调页完成后再将其唤醒,放回就绪队列。

    缺页中断机制2

    如果内存中空闲块,如上图的a号块,就可以将这个空闲块分配给此时缺页的进程。再把目标页面从外存放入内存当中。相应的也需要修改页表项当中对应的一些数据。

    缺页中断机制3

    这是第一种情况,就是有空闲内存块的情况。下面是第二种情况,即无空闲内存块的情况:

    如果内存无内存块,需要用页面置换算法,通过某种规则来选择要淘汰一个页面。如页面置换算法选中了要置换2号页面,可以看到页表中2号页的内容是被修改过的(修改为1),所以2号页内容需要从内存写回外存。这样2号页面占用的c号块就可以空出来,让0号页面使用。于是可以把0号页面从外存调入到内存的c号块中。相应的需要更改页表数据:

    缺页中断机制4

    缺页中断是因为当前执行的指令想要访问的目标页面未调入内存而产生的,因此属于内中断。内外中断分类如下:

    中断的分类

    另外需要注意的是一条指令在执行期间,可能产生多次缺页中断。(如:copy A to B,即将逻辑地址A中的数据复制到逻辑地址B,而A、B属于不同的页面,且都没有放入内存,则有可能产生两次中断)

    请求分页存储管理与基本分页存储管理的在地址变换时候需要新增哪些步骤:

    1. 请求调页( 查到页表项时进行判断)

      在查找到页面对应的页表项时候,一定需要对页面是否在内存这个状态进行判断。

    2. 页面置换(需要调入页面,但没有空闲内存块时进行)

      在地址变换过程中,如果发现此时想要访问的页面暂时没有调入到内存,但此时内存中没有空闲的内存块时,那么在这个地址变化的过程中也需要进行页面置换的工作。

    3. 要修改请求页表中新增的表项

      当页面调入、调出或者被访问时,需要对与之对应的页表项进行数据修改。

    请求分页管理方式执行过程与之前的基本分页管理类似:

    请求分页管理方式执行过程

    需要注意的是:在找到对应页表项后,若对应页面未调入内存,则产生缺页中断,之后由操作系统的缺页中断处理程序进行处理。

    另外快表中有的页面一定是在内存中的。若某个页面被换出外存,则快表中的相应表项也要删除,否则可能访问错误的页面。

    执行过程流程图如下:

    请求分页中的地址变换过程2

    补充细节:

    ①中只有"写指令"才需要修改修改位。并且,一般来说只需修改快表中的数据,只有要将快表项删除时才需要写回内存中的慢表。这样可以减少访存次数。

    ②和普通的中断处理一样,缺页中断处理依然需要保留CPU现场。

    ③需要用某种"页面置换算法"来决定一个换出页面(下节内容)

    ④换入/换出页面都需要启动慢速的I/O操作,可见,如果换入/换出太频繁,会有很大的开销。

    ⑤页面调入内存后,需要修改慢表,同时也需要将表项复制到快表中

    在具有快表机构的请求分页系统中,访问一个逻辑地址时,若发生缺页,则地址变换步骤是:

    查快表(未命中)查慢表(发现未调入内存)调页(调入的页面对应的表项会直接加入快表)查快表(命中)访问呢目标内存单元。

    请求分页管理方式总结:

    请求分页管理方式总结

    5.2 页面置换算法

    通过之前的学习可以知道,在请求分页存储管理当中如果内存空间不够的话,操作系统会负责将内存中用不到信息换出到外存。

    而页面置换算法就是由于选择到底要把哪个页面换出到外存。通过之前的学习知道页面的换入、换出需要磁盘I/O,会有较大的开销,因此好的页面置换算法应该追求更少的缺页率。

    页面置换算法有五种:最佳置换算法(OPT)、先进先出置换算法(FIFO)、最近最久未使用置换算法(LRU)、时钟置换算法(CLOCK)、改进型的时钟置换算法。

    最佳置换算法(OPT)

    最佳置换算法(OPT,Optimal):即每次选择淘汰的页面将是以后永不使用,或者在最长时间内不再被访问的页面,这样可以保证最低的缺页率。

    例:假设系统为某进程分配了三个内存块,并考虑到有一下页面号引用串(会依次访问这些页面):7,0,1,2,0,3,0,4,2,3,0,3,2,1,2,0,1,7,0,1

    最佳置换算法

    由于有三个内存块,所以前三次访问直接将不同的页面放入内存块即可。第四次访问2号页面,此时由于内存块已经满了,所以要调用页面置换算法替换掉一个内存块。可以看出后面的访问页面顺序中7号页面一直到倒数第三次才会访问,所以可以认为这个7号页面是最长时间不再被访问的页面,所以将内存块17号页面替换为当前要访问的2号页面。后面的原理类似。

    所以整个过程缺页中断发生了9次,页面置换发生了6次。即缺页率=920=45%

    注意:缺页时未必发生页面置换。若还有可用的空闲内存块,就不用进行页面置换。

    综上所述最佳置换算法可以保证最低的缺页率,但实际上,只有在进程执行的过程中才能知道接下来会访问到的是哪个页面。操作系统无法提前预判页面访问序列。因此,最佳置换算法是无法实现的。

    先进先出置换算法(FIFO)

    先进先出置换算法(FIFO):每次选择淘汰的页面是最早进入内存的页面。

    实现方法:把调入内存的页面根据调入的先后顺序排成一个队列,需要换出页面时选择队头页面即可。队列的最大长度取决于系统为进程分配了多少个内存块。

    例:假设系统为某进程分配了三个内存块,并考虑到有以下页面号引用串:3,2,1,0,3,2,4,3,2,1,0,4

    先进先出置换算法(FIFO)

    由于有三个内存块,所以前三次访问直接将不同的页面放入内存块即可,同时将放入内存块的页面放在队列的队尾。

    先进先出置换算法(FIFO)2

    第四次访问0号页面,由于内存块已经满了,所以要调用页面置换算法替换掉一个内存块。可以看到3号页面进入内存块时间最早,所以将3号页码替换为要被访问的0号页面。相应的0号页面放入队列的队尾,3号页面出队。

    先进先出置换算法(FIFO)3

    之后要访问的页面替换原理类似。可以看出分配三个内存块时,缺页次数为9次。

    如果修改一下题目,加入系统为某个进程分配的是四个内存块。则访问情况如下:

    先进先出置换算法(FIFO)4

    可以看出分配三个内存块时,缺页次数为10次。正常情况下分配的内存块越多,缺页的次数应该越少,但使用FIFO算法不减反增。这种现象称为Belady异常:当为进程分配的物理块数增大时,缺页次数不减反增的异常现象。

    只有FIFO算法会产生Belady异常。另外,FIFO算法虽然实现简单,但是该算法与进程实际运行时的规律不适应,因为先进入的页面也有可能最经常被访问。因此,算法性能差。

    最近最久未使用置换算法(LRU)

    最近最久未使用置换算法(LRU,leastrecentlyused):每次淘汰的页面是最近最久未使用的页面。

    实现方法:赋予每个页面对应的页表项中,用访问字段记录该页面自上次被访问以来所经历的时间t。当需要淘汰一个页面时,选择现有页面中t值最大的,即最近最久未使用的页面。

    最近最久未使用置换算法(LRU)

    例:假设系统为某进程分配了四个内存块,并考虑到有以下页面号引用串1,8,1,7,8,2,7,2,1,8,3,8,2,1,3,1,7,1,3,7

    最近最久未使用置换算法(LRU)2

    由于有四个内存块,所以前几次访问直接将不同的页面放入内存块即可。直到访问3号页面,此时由于内存块已经满了,所以要调用页面置换算法替换掉一个内存块。此时可以逆向往前检查在内存中出现的最后一个页数,并替换这个页。这里8号页最先出现,接着是1,2号页,最后出现的是7号页,所以7号页是最近最久未使用的页面。将被访问3号页面替换掉7号页面。

    最近最久未使用置换算法(LRU)3

    后面的7号页置换原理类似。所以在手动做题时,若需要淘汰页面,可以逆向检查此时在内存中的几个页面号。在逆向扫描过程中最后 一个出现的页号就是要淘汰的页面

    该算法的实现需要专门的硬件支持,虽然算法性能好,但是实现困难,开销大

    时钟置换算法(CLOCK)

    最佳置换算法性能最好,但无法实现。先进先出置换算法实现简单,但算法性能差,会出现Belady异常。最近最久未使用置换算法性能好,是最接近OPT算法性能的,但是实现起来需要专门的硬件支持,算法开销大。

    时钟置换算法是一种性能和开销较均衡的算法,又称CLOCK算法,或最近未用算法(NRU,Not Recently Used)。

    简单的CLOCK算法实现方法:为每个页面设置一个访问位,再将内存中的页面都通过链接指针链接成一个循环队列。当某页被访问时,其访问位置为1。当需要淘汰一个页面时,只需检查页的访问位。如果是0,就选择该页换出;如果是1,则将它置为0,暂不换出,继续检查下一个页面,若第一轮扫描中所有页面都是1,则将这些页面的访问位依次置为0后,再进行第二轮扫描(第二轮扫描中一定会有访问位为0的页面,因此简单的CLOCK算法选择一个淘汰页面最多会经过两轮扫描)。

    时钟置换算法

    例:假设系统为某进程分配了五个内存块,并考虑到有以下页面号引用串:1,3,4,2,5,6,3,4,7

    由于有五个内存块,所以前五个访问的页号1,3,4,2,5都会装入内存块中,并且装入时的访问位都会置为1。内存中五个页面会通过链接指针的方式链接成一个循环队列。

    时钟置换算法(CLOCK)

    在访问到6号页面的时候才会考虑页面置换。于是会从循环队列的队首(1号页)开始扫描,尝试找到一个访问位为0的页面,并且被扫描过的页面访问位会置为0。所以在经过第一轮扫描后所有的访问位都置为0

    时钟置换算法(CLOCK)2

    进行第二轮扫描时,1号页面的访问位为0,所以会选择淘汰1号页面。所以6号页会装入1号页以前占有的内存块当中,并且6号页的访问位会置为1,扫描的指针会指向下一个页面,即3号页面的位置。接着会访问3号页面,该页面在内存块中所以将3号页的访问位修改为1

    时钟置换算法(CLOCK)3

    之后的访问的替换原理类似。

    改进型的时钟置换算法

    简单的时钟置换算法仅考虑到一个页面最近是否被访问过。事实上,如果被淘汰的页面没有被修改过,就不需要执行I/O操作写回外存。只有被淘汰的页面被修改过时,才需要写回外存。因此,除了考虑一个页面最近有没有被访问过之外,操作系统还应考虑页面有没有被修改过。在其他条件都相同时,应优先淘汰没有修改过的页面,避免I/O操作。这就是改进型的时钟置换算法的思想。

    实现方法:增加一个修改位。修改位=0,表示页面没有被修改过;修改位=1,表示页面被修改过。其他与之前一样将所有可能被置换的页面排成一个循环队列。

    为方便讨论,用(访问位,修改位)的形式表示各页面状态。如(1,1)表示一个页面近期被访问过,且被修改过。

    规则:

    第一轮:从当前位置开始扫描到第一个(0,0)的帧用于替换。本轮扫描不修改任何标志位

    第二轮:若第一轮扫描失败,则重新扫描,查找第一个(0,1)的帧用于替换。本轮将所有扫描过的帧访问位设为0

    第三轮:若第二轮扫描失败,则重新扫描,查找第一个(0,0)的帧用于替换。本轮扫描不修改任何标志位。

    第四轮:若第三轮扫描失败,则重新扫描,查找第一个(0,1)的帧用于替换。

    由于第二轮已将所有帧的访问位设为0,因此经过第三轮、第四轮扫描一定会有一个帧被选中,因此改进型CLOCK置换算法选择一个淘汰页面最多会进行四轮扫描。

    例:假设系统为某进程分配了五个内存块。内存中五个页面会通过链接指针的方式链接成一个循环队列。

    改进型的时钟置换算法

    此时如果要淘汰一个页面,需要从队列的队头位置(0,1)开始依次的扫描。

    改进型的时钟置换算法2

    根据算法规则,第一轮扫描只需要找到访问位和修改位都为0的页面。所以往后可以找到,因此会淘汰这个页面。

    改进型的时钟置换算法3

    接着假设循环队列如下所示

    改进型的时钟置换算法4

    如果要淘汰一个页面,需要从队列的队头位置(1,1)开始依次的扫描。根据算法规则,第一轮扫描只需要找到访问位和修改位都为0的页面。第一轮查找下来并没有。

    所以进行第二轮查找。第二轮查找第一个(0,1)的帧用于替换。本轮将所有扫描过的帧访问位设为0。可以找到该页面:

    改进型的时钟置换算法5

    综上所述,替换的优先级是:

    第一优先级:最近没访问,且没修改的页面

    第二优先级:最近没访问,但修改过的页面

    第三优先级:最近访问过,但没修改的页面

    第四优先级:最近访问过,且修改过的页面

    页面置换算法总结:

    页面置换算法总结

    5.3 页面分配策略

    首先要了解驻留集:指请求分页存储管理中给进程分配的物理块的集合。

    页面分配、置换策略

    在采用了虚拟存储技术的系统中,驻留集大小一般小于进程的总大小。考虑一个极端情况,若某进程共有100个页面,则该进程的驻留集大小为100时进程可以全部放入内存,运行期间不可能再发生缺页。若驻留集大小为1,则进程运行期间必定会极频繁地缺页。

    所以若驻留集太小,会导致缺页频繁,系统要花大量的时间来处理缺页,实际用于进程推进的时间很少。而驻留集太大,权会导致多道程序并发度下降,资源利用率降低。所以应该选择一个合适的驻留集大小。

    针对驻留集大小是否可变问题,提出了两种分配策略:

    另外当页面置换的时候,页面置换的范围是什么,根据这个问题提出了置换范围策略:

    将两种分配和置换策略两两结合,可以得到:固定分配局部置换、可变分配局部置换和可变分配全局置换。

    没有固定分配全局置换的概念。因为全局置换意味着一个进程拥有的物理块数量必然会改变,因此不可能是固定分配。

    三种策略介绍:

    1. 固定分配局部置换

      系统为每个进程分配一定数量的物理块,在整个运行期间都不改变。若进程在运行中发生缺页,则只能从该进程在内存中的页面中选出一-页换出,然后再调入需要的页面。

      这种策略的缺点是:很难在刚开始就确定应为每个进程分配多少个物理块才算合理。(采用这种策略的系统可以根据进程大小、优先级、或是根据程序员给出的参数来确定为一个进程分配的内存块数)。因此灵活性差。

    2. 可变分配全局置换

      刚开始会为每个进程分配一定数量的物理块。操作系统会保持一个空闲物理块队列。当某进程发生缺页时,从空闲物理块中取出一块分配给该进程;若已无空闲物理块,则可选择一个未锁定的页面换出外存,再将该物理块分配给缺页的进程。

      可以看出采用这种策略时,只要某进程发生缺页,都将获得新的物理块,仅当空闲物理块用完时,系统才选择一个未锁定的页面调出。被选择调出的页可能是系统中任何一个进程中的页,因此这个被选中的进程拥有的物理块会减少,缺页率会增加。

      上面未锁定的页面是指系统会锁定一些页面,这些页面中的内容不能置换出外存(如:重要的内核数据可以设为"锁定")

      显然只要进程缺页就分配一个新的物理块,这种方式也不太合理。

    3. 可变分配局部置换

      刚开始会为每个进程分配一定数量的物理块。当某进程发生缺页时,只允许从该进程自己的物理块中选出一个进行换出外存。如果进程在运行中频繁地缺页,系统会为该进程多分配几个物理块,直至该进程缺页率趋势适当程度。反之,如果进程在运行中缺页率特别低,则可适当减少分配给该进程的物理块。

      这样就可以让系统的多道程序并发度,也保持在一个相对理想的位置。

    所以可变分配全局置换:只要缺页就给分配新物理块。可变分配局部置换:要根据发生缺页的频率来动态地增加或减少进程的物理块。

    调入页面时机及何处调入

    在什么要调入所需要的页面:

    1. 预调页策略

      根据局部性原理(特别是空间局部性),一次调入若干个相邻的页面可能比一次调入一个页面更高效。但如果提前调入的页面中大多数都没被访问过,则又是低效的。因此可以预测不久之后可能访问到的页面,将它们预先调入内存,但目前预测成功率只有50%左右。故这种策略主要用于进程的首次调入,由程序员指出应该先调入哪些部分。

      所以该策略是在运行前就进行调入的。

    2. 请求调页策略

      进程在运行期间发现缺页时才将所缺页面调入内存。由这种策略调入的页面一定会被访问到,但由于每次只能调入一页,而每次调页都要磁盘I/O操作,因此I/O开销较大。

      所以该策略是在运行时就进行调入的。

    从什么地方调入页面:

    之前介绍过磁盘中存储区域分为对换区和文件区两个部分。其中对换区采用连续分配的方式,读写速度更快。而文件区采用的是离散分配方式,读写速度较慢。

    从何处调入页面

    方式一:由于对换区读写速度更快,如果系统拥有足够的对换区空间:页面的调入、调出都是在内存与对换区之间进行,这样可以保证页面的调入、调出速度很快。在进程运行前,需将进程相关的数据从文件区复制到对换区。之后再调入内存中,如果内存已满,会将内存中的页面调出到对换区。

    页面调出

    方式二:如果系统中缺少足够的对换区空间,凡是不会被修改的数据都直接从文件区调入内存,由于这些页面不会被修改,因此换出时不必写回磁盘,下次需要时再从文件区调入即可。对于可能被修改的部分,换出时需写回磁盘对换区,下次需要时再从对换区调入。

    页面调出2

    方式三:UNIX方式:运行之前进程有关的数据全部放在文件区,故未使用过的页面,第一次都会从文件区调入内存。若内存不够,使用过的页面需要换出,则写回对换区,下次需要时再从对换区调入。

    页面调出3

    抖动现象与工作集

    又称颠簸现象。指刚刚换出的页面马上又要换入内存,刚刚换入的页面马上又要换出外存,这种频繁的页面调度行为。产生抖动的主要原因是进程频繁访问的页面数目高于可用的物理块数(分配给进程的物理块不够)。

    所以如果为进程分配的物理块太少,会使进程发生抖动现象。为进程分配的物理块太多,又会降低系统整体的并发度,降低某些资源的利用率。所以为进程分配多少物理块是值得思考的问题。

    为了研究为应该为每个进程分配多少个物理块,Denning 提出了进程"工作集"的概念。

    工作集:指在某段时间间隔里,进程实际访问页面的集合。

    操作系统会根据"窗口尺寸"来算出工作集。例:某进程的页面访问序列如下,窗口尺寸为4,各时刻的工作集为?

    工作集

    由于窗口尺寸的大小是4。所以当访问到23页面时,其工作集是24,15,18,23。访问到17号页面时,其工作集是18,24,17

    工作集2

    所以可以看出工作集大小可能小于窗口尺寸,实际应用中,操作系统可以统计进程的工作集大小,根据工作集大小给进程分配若干内存块。如:窗口尺寸为5, 经过一段时间的监测发现某进程的工作集最大为3,那么说明该进程有很好的局部性,可以给这个进程分配3个以上的内存块即可满足进程的运行需要。即系统可以根据工作集的大小来确定驻留级的大小。

    一般来说, 驻留集大小不能小于工作集大小,否则进程运行过程中将频繁缺页。

    拓展:基于局部性原理可知,进程在一段时间内访问的页面与不久之后会访问的页面是有相关性的。因此,可以根据进程近期访问的页面集合(工作集)来设计一种页面置换算法:如果一个进程需要置换出某个页面,可以选择一个不在工作集中的页面进行淘汰。

    页面分配策略总结:

    页面分配策略总结

    6. 内存映射文件

    内存映射文件指的是操作系统向上层程序员提供的功能(系统调用)。通过这个功能好处:

    四. 文件管理

    文件就是一组有意义的信息/数据集合。

    一个文件有以下属性:

    文件内部的数据组织方式:

    1. 无结构文件(如文本文件)

      由一些二进制或字符流组成,又称"流式文件"。

    2. 有结构文件(如数据库表):

      由一组相似的记录组成,又称"记录式文件"。

      记录是一组相干的数据项集合。

      文件组织

    文件组织结构:

    文件组织结构

    这些记录如何组织的问题,如:应该使用顺序存放、还是索引表来表示记录间的顺序。这是文件的逻辑结构重点要探讨的问题。所以文件的逻辑结构要探讨的问题其实就是文件内部这些记录,这些数据应该被怎么组织起来的问题。

    文件之间的组织方式:

    文件组织形式

    这种平时常见的文件组织形式是树状的组织形式。这里所谓的目录就是文件夹。用户可以自己创建一层层的目录,各层目录中存放相应的文件。系统中的各个文件就通过一层一层的目录合理有序的组织起来了。目录其实也是一种特殊的有结构文件(由记录组成),如何实现文件目录是之后会重点探讨的问题。

    操作系统向上提供的功能:

    操作系统向上提供的文件功能

    可用几个基本操作完成更复杂的操作,比如:"复制文件",可以先创建一个新的空文件,再把源文件读入内存,再将内存中的数据写到新文件中。

    文件如何存放在外存:

    与内存一样,外存也是由一个个存储单元组成的,每个存储单元可以存储一定量的数据(如1B)。每个存储单元对应一个物理地址。

    类似于内存分为一个个"内存块",外存会分为一个个"块/磁盘块/物理块"。每个磁盘块的大小是相等的,每块一般包含2的整数幂个地址(如本例中,一块包含210个地址,即1KB)。同样类似的是,文件的逻辑地址也可以分为(逻辑块号,块内地址),操作系统同样需要将逻辑地址转换为外存的物理地址(物理块号,块内地址)的形式。块内地址的位数取决于磁盘块的大小。

    操作系统以"块"为单位为文件分配存储空间,因此即使一个文件大小只有10B,但它依然需要占用1KB的磁盘块。外存中的数据读入内存时同样以块为单位。

    所以其实文件的物理结构探讨的是文件这些数据在物理上应该是怎么存放怎么组织的问题。而上面提到的文件的逻辑结构是文件的各个记录在逻辑上应该是什么样的组织关系问题。

    其他需要由操作系统实现的文件管理功能:

    初始文件管理总结:

    初始文件管理总结

    1. 文件的逻辑结构

    所谓的"逻辑结构"就是指在用户看来,文件内部的数据应该是如何组织起来的。而"物理结构"指的是在操作系统看来,文件的数据是如何存放在外存中的。文件逻辑结构可以分为无结构文件和有结构文件。有结构文件逻辑结构是:顺序文件、索引文件和索引顺序文件。

    文件逻辑结构

    算法的具体实现与逻辑结构、物理结构都有关(文件也一样,文件操作的具体实现与文件的逻辑结构、物理结构都有关)

    无结构文件:文件内部的数据就是一系列二进制流或字符流组成。又称"流式文件"。如:Windows操作系统中的.txt文件。

    文件内部的数据其实就是一系列字符流,没有明显的结构特性。因此也不用探讨无结构文件的"逻辑结构"问题。所以重点探讨有结构文件。

    有结构文件:由一组相似的记录组成,又称"记录式文件"。每条记录又若干个数据项组成。如:数据库表文件。一般来说,每条记录有一个数据项可作为关键字(作为识别不同记录的ID)。

    有结构文件

    上面例子中,"学号"即可作为各个记录的关键字。每个学生对应一条记录,每条记录由若干个数据项组成。而根据各条记录的长度(占用的存储空间)是否相等,又可分为定长记录和可变长记录两种。

    有结构文件定长记录和可变长记录

    如上图所示,学号、姓名和性别都是定长记录。而特长长度不确定,所以是可变长记录。

    之前例子中专业可以给60字节存储空间也不会造成多大的浪费。而这里学生的特长有的人可能很多,所需要的空间就会很大,而有的学生没有特长,显然对这个数据项的存储空间利用很不充分。这样就会导致空间利用率极低的问题。所以最好让这个特长记录是可变长的记录组成。

    接下来会讨论这些记录应该在逻辑上怎么被组织起来的问题。根据有结构文件中的各条记录在逻辑上如何组织,可以分为三类:顺序文件、索引文件和索引顺序文件。

    1.1 顺序文件

    顺序文件:文件中的记录一个接一个地顺序排列(逻辑上),记录可以是定长的或可变长的。各个记录在物理上可以顺序存储或链式存储。

    如果是顺序存储,那么逻辑上相邻的记录物理上也相邻(类似于顺序表)

    顺序存储

    而如果采用链式存储结构,逻辑上相邻的记录物理上不一定相邻(类似于链表)

    顺序文件的链式存储

    根据记录是否按照关键字顺序排列,又可以把顺序文件分为:串结构和顺序结构。

    显然记录是定长的还是可变长的,记录是否按照关键字有序排列,另外这些记录在物理上是顺序存储还是链式存储,所有的区别都能影响顺序文件能不能实现一些操作的功能。

    假设:已经知道了文件的起始地址(也就是第一个记录存放的位置)

    思考一:能否快速找到第i个记录对应的地址?(即能否实现随机存取)

    思考二:能否快速找到某个关键字对应的记录存放的位置?

    结论:定长记录的顺序文件,若物理上采用顺序存储,则可实现随机存取。若能再保证记录的顺序结构,则可实现快速检索(即根据关键字快速找到对应记录)。

    注意:一般来说,考试题目中所说的"顺序文件"指的是物理上顺序存储的顺序文件,而不是链式存储。之后的讲解中提到的顺序文件也默认如此。并且顺序文件的缺点是:增加/删除一个记录比较困难(如果是串结构则相对简单,因为不需要按关键字顺序排列)。

    在实际应用过程中,为了减少I/O次数,一般来说操作系统会管理一个日志文件,用这个日志文件来管理记录对某个文件中的记录进行修改的信息。之后每隔一段比较长的时间再把这些信息统一合并到外存中对应的文件。

    1.2 索引文件

    对于可变长记录文件,要找到第i个记录,必须先顺序第查找前i1个记录,但是很多应用场景中又必须使用可变长记录。如何解决这个问题?

    基于这种需求提出索引文件这种逻辑结构。每个文件会建立一张索引表,并且索引表的表项会对应文件的一条记录。文件的记录在物理内存中可以离散存放,但是索引表的表项在物理上需要连续存放。

    索引文件

    另外每一个索引表的表项大小都是相等的。如:索引号、长度、指针都占4个字节,那么一个索引表的表项就是12个字节的长度。

    所以索引表本身是定长记录的顺序文件。因此可以快速找到第i个记录对应的索引项。

    可将关键字作为索引号内容,若按关键字顺序排列,则还可以支持按照关键字折半查找。每当要增加/删除一个记录时,需要对索引表进行修改。由于索引文件有很快的检索速度,因此主要用于对信息处理的及时性要求比较高的场合

    另外,可以用不同的数据项建立多个索引表。如:学生信息表中,可用关键字"学号"建立一张索引表。也可用"姓名"建立一张索引表。这样就可以根据"姓名"快速地检索文件了。(如:SQL就支持根据某个数据项建立索引的功能)

    1.3 索引顺序文件

    索引文件的缺点:每个记录对应一个索引表项,因此索引表可能会很大。比如:文件的每个记录平均只占8B,而每个索引表项占32个字节,那么索引表都要比文件内容本身大4倍,这样对存储空间的利用率就太低了。

    索引顺序文件是索引文件和顺序文件思想的结合。索引顺序文件中,同样会为文件建立张索引表,但不同的是:并不是每个记录对应一个索引表项,而是一组记录对应一个索引表项。

    索引顺序文件

    在上图中,学生记录按照学生姓名的开头字母进行分组。每个分组就是一个顺序文件,且每一个分组对应一个索引顺序文件的索引项,分组内的记录不需要按关键字排序。

    索引顺序文件2

    每个索引项记录了名字和存放的位置。索引顺序文件的索引项也不需要按关键字顺序排列,这样可以极大地方便新表项的插入。

    也就是说索引顺序文件的索引表其实是一个定长记录的串结构的顺序文件。可以看到索引表的表项少了很多。

    若一个顺序文件有10000个记录,则根据关键字检索文件,只能从头开始顺序查找(这里指的并不是定长记录、顺序结构的顺序文件),平均须查找5000个记录。

    若采用索引顺序文件结构,可把10000个记录分为10000=100组,每组100个记录。则需要先顺序查找索引表找到分组(共100个分组,因此索引表长度为100,平均需要查50次),找到分组后,再在分组中顺序查找记录(每个分组100个记录,因此平均需要查50次)。可见,采用索引顺序文件结构后,平均查找次数减少为50+50=100次。

    同理,若文件共有106个记录,则可分为1000个分组,每个分组1000个记录。根据关键字检索一个记录平均需要查找500+500=1000次。这个查找次数依然很多,解决方式是可以建立多级索引:

    为了进一步提高检索效率,可以为顺序文件建立多级索引表。例如,对于一个含106个记录的文件,可先为该文件建立一张低级索引表,每100个记录为一组,故低级索引表中共有10000个表项(即10000个定长记录),再把这10000个定长记录分组,每组100个,为其建立顶级索引表,故顶级索引表中共有100个表项。

    多级索引顺序文件

    此时,检索一个记录,平均检索顶级索引表50次,低级索引表50次,最后的分组50次,所以平均需要查找50+50+50=150次。

    有结构文件总结:

    有结构文件总结

    2. 文件目录

    层级目录结构(Windows资源管理器),文件之间的组织结构清晰,易于查找。编程时也可以很方便的用文件路径找到一个文件。如:FILE *fp;fp=fopen("F:\data\myfile.dat");用户可以轻松实现"按名存取"。

    从操作系统角度来探讨这些目录结构应该如何实现。所谓的文件目录就是熟悉的Windows操作系统的文件夹。要实现文件目录的功能需要有很关键的数据结构"文件控制块"。另外随着计算机的发展,目录的结构也出现了不同的变化。对文件控制块的优化也很重要,所以还要了解索引结点知识。

    文件目录

    2.1 文件控制块

    对于某个根目录来说,对应的目录文件如下:

    文件控制块

    其实就是用上图的目录表来表示根目录下存放哪些文件。所以每一个文件夹,每一个文件都会对应一个表项。故目录本身就是一种有结构文件,由一条条记录组成。每条记录对应一个在该放在该目录下的文件。

    注意上图目录表中还保存了文件的物理位置。假如当我们双击"照片"后,操作系统会在这个目录表中找到关键字"照片"对应的目录项(也就是记录),然后从外存中将"照片"目录的信息读入内存,于是,"照片"目录中的内容就可以显示出来了。

    目录文件中的一条记录就是一个"文件控制块(FCB)"。FCB的有序集合称为"文件目录",一个FCB就是一个文件目录项。同时由上图还可以知道FCB中包含了文件的基本信息(文件名、物理地址、逻辑结构、物理结构等),存取控制信息( 是否可读/可写、禁止访问的用户名单等),使用信息(如文件的建立时间、修改时间等)。其中最重要,最基本的还是文件名、文件存放的物理地址,因为FCB实现了文件名和文件之间的映射。使用户(用户程序)可以实现"按名存取"。

    对文件控制块的操作:

    1. 搜索:当用户要使用一个文件时,系统要根据文件名搜索目录,找到该文件对应的目录项
    2. 创建文件:创建一个新文件时,需要在其所属的目录中增加一个目录项
    3. 删除文件:当删除一个文件时,需要在目录中删除相应的目录项
    4. 显示目录:用户可以请求显示目录的内容,如显示该目录中的所有文件及相应属性
    5. 修改目录:某些文件属性保存在目录中,因此这些属性变化时需要修改相应的目录项(如:文件重命名)

    2.2 文件的目录结构

    文件控制块的有序集合就组成了文件的目录。

    单级目录结构

    早期操作系统并不支持多级目录,整个系统中只建立一张目录表,每个文件占一个目录项。

    单级目录结构

    单级目录实现了"按名存取",但是不允许文件重名。

    在创建一个文件时,需要先检查目录表中有没有重名文件,确定不重名后才能允许建立文件,并将新文件对应的目录项插入目录表中,显然,单级目录结构不适用于多用户操作系统。

    两级目录结构

    为了解决单级目录结构不适用于多用户操作系统的问题,后来提出了两级目录结构。

    早期的多用户操作系统,采用两级目录结构。分为主文件目录(MFD,Master File Directory)和用户文件目录(UFD,User Flie Directory)。

    一个主文件目录记录用户名及相应用户文件目录的存放位置,而用户文件目录由该用户的文件FCB组成。

    两级目录结构

    由于不同的用户文件是存放在不同的用户文件目录下,所以允许不同用户的文件重名。文件名虽然相同,但是对应的其实是不同的文件。

    两级目录结构允许不同用户的文件重名,也可以在目录上实现实现访问限制(检查此时登录的用户名是否匹配,用户A不能访问用户B目录,反之一样)。但是两级目录结构依然缺乏灵活性,用户不能对自己的文件进行分类。

    但是这种结构缺点就是不可以把自己的文件进行分类。

    多级目录结构

    多级目录结构又称树形目录结构。这是现在常用的目录结构。

    树形目录结构

    每个目录下可以有更低一级别的目录,同时在各个目录下面还有一些文件。并且不同目录下的文件是可以重名的。

    用户(或用户进程)要访问某个文件时要用文件路径名标识文件,文件路径名是个字符串。各级目录之间用"1"隔开。从根目录出发的路径称为绝对路径。例如:自拍.jpg的绝对路径是/照片/2015-08/自拍jpg

    系统根据绝对路径一层一层地找到下一级目录。刚开始从外存读入根目录的目录表。找到"照片"目录的存放位置后,从外存读入对应的目录表。再找到"2015-08"目录的存放位置,再从外存读入对应目录表。最后才找到文件"自拍.jpg"的存放位置。整个过程需要3次读磁盘I/O操作。

    很多时候,用户会连续访问同一目录内的多个文件(比如:接连查看2015-08目录内的多个照片文件),显然,每次都从根目录开始查找,是很低效的。因此可以设置一个"当前目录"。例如:此时已经打开了照片的目录文件,也就是说,这张目录表已调入内存,那么可以把它设置为当前目录。当用户想要访问某个文件时,可以使用从当前目录出发的相对路径。

    在Linux中,"."表示当前目录,因此如果照片是当前目录,则自拍.jpg的相对路径为:./2015-08/自拍.jpg。从当前路径出发,只需要查询内存中的照片目录表,即可知道2015-08目录表的存放位置,从外存调入该目录,即可知道自拍.jpg存放的位置了。所以只用访存一次I/O即可。

    可见,引入"当前目录"和"相对路径"后,磁盘I/O的次数减少了。这就提升了访问文件的效率。所以树形目录结构可以很方便地对文件进行分类,层次结构清晰,也能够更有效地进行文件的管理和保护。但是,树形结构不便于实现文件的共享。为此,提出了"无环图目录结构"。

    无环图目录结构

    无环图目录结构在树形目录结构的基础上,增加一些指向同一节点的有向边,使整个目录成为一个有向无环图。可以更方便地实现多个用户间的文件共享。

    无环图目录结构

    可以用不同的文件名指向同一个文件,甚至可以指向同一个目录(共享同一目录下的所有内容)。在引入共享目录之后对于文件的删除就不能向之前一样简答,只要一个文件有可能是被多个用户同时使用。当一个用户提出删除文件时候还需要判断这个文件当前是否有其他用户正在访问。所以需要为每个共享结点设置一个共享计数器,用于记录此时有多少个地方在共享该结点。用户提出删除结点的请求时,只是删除该用户的FCB、并使共享计数器减1,并不会直接删除共享结点。

    如上图所示两个用户User1User2同时访问一个文件,所以当前这个文件的共享计数器=2。此时User1用户提出删除该文件,操作系统会把该文件的目录项删除,并让共享计数器1,而并不会直接删除该文件。

    无环图目录结构共享删除文件

    只有共享计数器减为0时,才会删除这个共享文件。

    注意:共享文件不同于复制文件。在共享文件中,由于各用户指向的是同一个文件,因此只要其中一个用户修改了文件数据,那么所有用户都可以看到文件数据的变化。如果是某个用户复制的共享文件,那么该用户修改副本文件,并不会影响原本的共享文件。

    索引结点

    是对FCB的改进。通过之前的学习知道,由一些列的文件控制块组成了一个一个文件目录,但是操作系统在查找各级目录的过程中只需要使用文件名这个信息就可以,其他信息就暂时不需要。只有当文件匹配的时候才会去关心文件的存放位置。所以可以对目录表进行一个简化,来提升搜索的效率。

    因为按照文件名来搜索目录的时候,并不需要关心除了文件名之外其他的信息,因此可以把其他信息放到索引结点当中。即除了文件名 之外的文件描述信息都放到索引结点中。每一个文件都会有一个唯一的索引结点。

    索引结点

    而采用了索引结点机制后目录中只包含文件名和索引结点指针,这个目录表所需要占用的空间会小很多。

    假设一个FCB是64B,磁盘块的大小为1KB,则每个盘块中只能存放16个FCB。若一个文件目录中共有640个目录项,则共需要占用640/16=40个盘块。因此按照某文件名检索该目录,平均需要查询320个目录项,平均需要启动磁盘20次(每次磁盘I/O读入一块)。

    但若使用索引结点机制,文件名占14B,索引结点指针站2B,则每个盘块可存放64个目录项,那么按文件名检索目录平均只需要读入320/64=5个磁盘块。显然,这将大大提升文件检索速度。

    当找到文件名对应的目录项时,才需要将索引结点调入内存,索引结点中记录了文件的各种信息,包括文件在外存中的存放位置,根据"存放位置"即可找到文件。而存放在外存中的索引结点称为"磁盘索引结点",当索引结点放入内存后称为"内存索引结点"。相比之下内存索引结点中需要增加一些信息,比如:文件是否被修改、此时有几个进程正在访问该文件等。

    文件目录总结:

    文件目录总结

    3. 文件的物理结构

    由之前的学习知道操作系统作为最接近硬件的软件层次,需要对硬件进行管理,包括外存(磁盘)。操作系统对磁盘的管理主要是对非空闲磁盘块进行管理(存放文件数据的磁盘块)、对空闲磁盘块的管理。对非空闲磁盘块的管理是探讨文件的物理结构/文件分配方式。对空闲磁盘块的管理是探讨文件存储空间管理的问题。

    这里重点介绍文件的物理结构(文件分配方式)。也即文件数据应该怎样存放在外存中。分为三种方式:连续分配、链接分配(进一步细分为:隐式链接、显式链接)、索引分配。

    文件分配方式

    补充:在内存管理中,进程的逻辑地址空间被分为一个一个页面。同样的,在外存管理中,为了方便对文件数据的管理,文件的逻辑地址空间也被分为了一个一个的文件"块"。于是文件的逻辑地址也可以表示为(逻辑块号,块内地址)的形式。若块的大小是1KB,则1MB大小的文件可以被分为1K个块。同样操作系统为文件分配存储空间都是以块为单位的。用户通过逻辑地址来操作自己的文件,操作系统要负责实现从逻辑地址到物理地址的映射

    3.1 连续分配方式

    文件的连续分配方式要求每个文件在磁盘上占有一组连续的块。

    连续分配方式

    如上图所示,文件aaa被分为3个相同的逻辑块。这个三个相同的逻辑块在被放到物理内存时,需要占有一组连续的块(4,5,6号块)。

    用户通过逻辑地址来操作自己的文件,操作系统负责实现从逻辑地址转换为物理地址的映射方式。为了实现这个地址映射的功能,在文件的目录表中必须记录两个文件属性:文件的起始块号和文件的长度。

    连续分配方式例子

    如文件aaa,它的起始块号是物理内存中的4号存储单元,长度是3,所以4,5,6号内存单元共同组成aaa文件。

    所以用户给出要访问的逻辑块号,操作系统找到该文件对应的目录项(FCB),之后物理块号=起始块号+逻辑块号。如用户要访问aaa文件逻辑块号为2的块,那么只要用逻辑块号2+aaa文件的起始地址4=6。所以物理块号6就是要访问的真实地址。当然,还需要检查用户提供的逻辑块号是否合法(逻辑块号长度就不合法)。

    可以直接算出逻辑块号对应的物理块号,因此连续分配支持顺序访问和直接访问(即随机访问)。

    另外磁盘读取某个磁盘块时,需要移动磁头。访问的两个磁盘块相隔越远,移动磁头所需时间就越长。而采用连续分配方式,文件的块在物理内存上是连续存放的,所以访问一个文件移动的磁头所需时间就很短。

    结论:连续分配的文件在顺序读/写时速度最快。

    连续分配方式例子2

    如上图,若此时文件A要拓展,需要再增加一个磁盘块(总共需要连续的4个磁盘块)。由于采用连续结构,因此文件A占用的磁盘块必须是 连续的,而文件A最后一个块(黄色部分)后面的块已经被占用了。所以不得不把整个文件A迁移到下面绿色区域的位置。

    连续分配方式例子3

    结论:物理上采用连续分配的文件不方便拓展。

    若橙色区域为非空闲块,绿色区域为空闲磁盘块。

    连续分配方式例子4

    若此时创建的新文件大小为3个块,那么无法为其分配足够的存储空间。

    结论:物理上采用连续分配,存储空间利用率低,会产生难以利用的磁盘碎片可以用紧凑来处理碎片,但是需要耗费很大的时间代价。

    总结:

    1. 连续分配方式要求每个文件在磁盘上占有一组连续的块。
    2. 优点:支持顺序访问和直接访问( 即随机访问)。连续分配的文件在顺序访问时速度最快。
    3. 缺点:不方便文件拓展。存储空间利用率低,会产生磁盘碎片。

    3.2 链接分配方式

    文件的链接分配采取离散分配的方式,可以为文件分配离徽的磁盘块。分为隐式链接和显式链接两种。

    隐式链接

    如果采用这种方式,在文件的目录项,也就是文件对应的FCB中需要记录这个文件的起始块号和结束块号。另外各个块之间都会一定的存储空间存储指向下一块的链接指针,当然最后一个块是没有链接指针的。

    隐式链接

    这些链接对于用户来说是透明的。采用这种方式实现文件的逻辑块号到物理块号的转变方法如下:

    用户给出要访问的逻辑块号i,操作系统找到该文件对应的目录项(FCB),找到这个文件的起始块号。于是操作系统可以先读入这个文件的起始块,而这个起始块就是逻辑上的0号块。只有将这个起始块读入内存后才能知道下一个块的位置(链接指针指向下一块)。因此,读入i号逻辑块,需要先依次读入0n1号逻辑块,才能找到i号块存放的位置,所以总共需要i+1次磁盘I/O

    结论:采用链式分配(隐式链接)方式的文件,只支持顺序访问,不支持随机访问,查找效率低。另外,指向下一个盘块的指针也需要耗费少量的存储空间。

    采用这种方式很适合拓展文件:

    隐式链接

    假设此时这个文件aaa需要拓展,也就是需要再分一个新的磁盘块,由于这些文件的块是离散分配的,因此只需要从文件空闲地方随便找一个空闲的位置,再将这个空闲块写入数据后挂到aaa文件链尾即可。假如这里挑选空闲块号是8号块,之后还需要把结束块号设置为8

    隐式链接2

    结论:采用隐式链接的链接分配方式,很方便文件拓展。另外,所有的空闲磁盘块都可以被利用,不会有碎片问题,外存利用率高。

    总结:

    1. 隐式链接,即除文件的最后一个盘块之外,每个盘块中都存有指向下一个盘块的指针。文件目录包括文件第一块的指针和最后一块的指针。
    2. 优点:很方便文件拓展,不会有碎片问题,外存利用率高。
    3. 缺点:只支持顺序访问,不支持随机访问,查找效率低,指向下一个盘块的指针也需要耗费少量的存储空间。

    显示连接

    把用于链接文件各物理块的指针显式地存放在一张表中。即文件分配表(FAT,File Allocntion Table)。

    磁盘当中的各个块的先后顺序都是统一记录在文件分配表当中的。所以一个磁盘有多少个块在文件分配表(FAT)中就相对应的有多少个表项。

    假设某个新创建的文件aaa依次存放在磁盘块2501

    aaa的FCB,也就是目录项当中,需要记录文件aaa存放的起始块号。另外在文件分配表中会显示的记录,文件aaa这几个物理块的链接关系。由于2号块是起始块,所以FAT中2号块表项的下一块会记录5

    显示链接

    接着依次将FAT对应块号的下一块设置为对应的值。当最后一个0号物理块项的下一块记录是1,而1号物理块作为最后一个块,所以1号块的下一块记录值可以设置为一个特殊的值,如:1

    显示链接2

    注意:一个磁盘仅设置一张FAT。在开机时,将FAT读入内存,并常驻内存。FAT 的各个表项在物理上连续存储,且每一个表项长度相同,因此"物理块号"字段可以是隐含的。

    实现文件逻辑块号到物理块号的转换:

    用户给出要访问的逻辑块号i,操作系统找到该文件对应的目录项(FCB)。可以从目录项中找到起始块号,若i>0, 则查询内存中的文件分配表FAT,往后找到i号逻辑块对应的物理块号。逻辑块号转换成物理块号的过程不需要读磁盘操作。

    如要找到文件aaa1号逻辑块号,需要先找到起始块号1,再根据FAT找到1号逻辑块即可。

    显示链接3

    这里文件aaa1号逻辑块号是0

    结论:采用链式分配(显式链接)方式的文件,支持顺序访问,也支持随机访问(想访问i号逻辑块时,并不需要依次访问之前的0i1(号逻辑块),由于块号转换的过程不需要访问磁盘,因此相比于隐式链接来说,访问速度快很多。

    显然,显式链接也不会产生外部碎片,也可以很方便地对文件进行拓展。

    总结:

    1. 显式链接,即把用于链接文件各物理块的指针显式地存放在一张表中,即文件分配表(FAT, FileAllocation Table)。一个磁盘只会建立一张文件分配表。开机时文件分配表放入内存,并常驻内存。
    2. 优点:很方便文件拓展,不会有碎片问题,外存利用率高,并且支持随机访问。相比于隐式链接来说,地址转换时不需要访问磁盘,因此文件的访问效率更高。
    3. 缺点:文件分配表的需要占用一定的存储空间。

    考试题目中遇到未指明隐式/显式的"链接分配",默认指的是隐式链接的链接分配。

    3.3 索引分配

    索引分配允许文件离散地分配在各个磁盘块中,系统会为每个文件建立一张索引表,索引表中记录了文件的各个逻辑块对应的物理块(索引表的功能类似于内存管理中的页表。建立逻辑页面到物理页之间的映射关系)。索引表存放的磁盘块称为索引块。文件数据存放的磁盘块称为数据块。

    假设某个新创建的文件aaa的数据依次存放在磁盘块25139。采用所以分配方式,系统会为aaa,这个文件建立一张索引表。这个索引表记录了aaa这些逻辑块和物理块之间一一对应的映射关系。

    索引分配

    如果aaa文件的索引表是存放在7号磁盘块当中的,那么7号磁盘块就是aaa的索引块。而2,5,13,9这几个磁盘块是aaa文件的数据块。采用索引分配方式的文件,需要在自己的目录项FCB中记录自己的索引块到底是哪一块。而索引块中存放的是索引表。因此系统可以根据索引块的块号找到对应的索引表,接着就可以找到各个逻辑块对应的块号了。

    索引分配方式

    如上图,aaa文件的索引表存放在7号存储单元,实际数据是依次存放在2,5,13,9这几个块中。

    注:在显式链接的链式分配方式中,文件分配表FAT是一个磁盘对应一张。而索引分配方式中,索引表是一个文件对应一张。

    可以用固定的长度表示物理块号( 如:假设磁盘总容量为1TB=240B,磁盘块大小为1KB,则共有230个磁盘块,则可用4B表示磁盘块号),因此,索引表中的"逻辑块号"可以是隐含的。

    采用索引分配方式,如何实现文件的逻辑地址到物理地址的转换:

    假如用户给出要访问的逻辑块号i,操作系统找到该文件对应的目录项(FCB),从中找到这个文件索引块的块号,再从索引块中读出这个文件索引表的内容。接着只需要通过逻辑块号来查询这个索引表即可。

    索引分配方式2

    可见,索引分配方式可以支持随机访问。文件拓展也很容易实现(只需要给文件分配一个空闲块,并增加一个索引表项即可)。但是索引表需要占用一定的存储空间。

    假设每个磁盘块1KB,一个索引表项4B,则一个磁盘块只能存放1K/4B=256个索引项。如果一个文件的大小超过了256块,那么一个磁盘块是装不下文件的整张索引表的,解决这个问题方法如下:

    总结:

    1. 索引分配允许文件离散地分配在各个磁盘块中,系统会为每个文件建立一张索引表, 索引表中记录了文件的各个逻辑块对应的物理块(索引表的功能类似于内存管理中的页表。建立逻辑页面到物理页之间的映射关系)。索引表存放的磁盘块称为索引块。文件数据存放的磁盘块称为数据块。但若文件太大,索引表项太多,可以采取以下三种方法解决:

    2. 链接方案:如果索引表太大,一个索引块装不下,那么可以将多个索引块链接起来存放。

      缺点:若文件很大,索引表很长,就需要将很多个索引块链接起来。想要找到i号索引块,必须先依次读入0i1号索引块,这就导致磁盘I/O次数过多,查找效率低下。

    3. 多层索引:建立多层索引(原理类似于多级页表)。使第一层索引块指向第二层的索引块。还可根据文件大小的要求再建立第三层、第四层索引块。采用K层索引结构,且顶级索引表未调入内存,则访问一个数据块只需要K+1次读磁盘操作。

      缺点:即使是小文件,访问一个数据块依然需要K+1次读磁盘。

    4. 混合索引:多种索引分配方式的结合。

      例如,一个文件的顶级索引表中,既包含直接地址索引(直接指向数据块),又包含一级间接索引(指向单层索引表)、还包含两级间接索引(指向两层索引表)。

      优点:对于小文件来说,访问一个数据块所需的读磁盘次数更少。

    重要考点:

    ①要会根据多层索引、混合索引的结构计算出文件的最大长度(注意:各级索引表最大不能超过一个块)。

    ②要能自己分析访问某个数据块所需要的读磁盘次数(注意:FCB中会存有指向顶级索引块的指针,因此可以根据FCB读入顶级索引块。每次读入下一级的索引块都需要一次读磁盘操作。另外,要注意题目条件,即顶级索引块是否已调入内存)。

    文件的物理结构总结:

    文件的物理结构总结

    4. 文件的逻辑结构和物理结构区别

    所谓的"逻辑结构"就是指在用户看来,文件内部的数据应该是如何组织起来的。而"物理结构"指的是在操作系统看来,文件的数据是如何存放在外存中的。

    逻辑结构和物理结构

    4.1 无结构文件

    例子:用C语言创建一个无结构文件

    执行这段代码后会创建一个文本文件(无结构文件),里面会写入一万个"Hello world!"。

    test

    每个字符1B。在用户看来,整个文件占用一片连续的逻辑地址空间。

    逻辑地址空间

    如果此时想读入第17个字符:

    运行结果是o。总之在用户看来,似乎所有的字符都是连续存放的,所以只需要提供想访问那个字符在文件中的逻辑地址(17),就可以顺序找到想要找的任何一个数据。这种从用户的视角来说,但是从操作系统视角来说,这些字符就是一堆二进制数据,每个磁盘块可存储1KB,所以这些字符会被拆分为一块一块。

    系统拆分

    接着操作系统会根据文件管理的策略来决定是用连续分配还是连接分配或者是索引分配方式来存放在磁盘中。

    采用连续分配方式

    采用连接分配方式

    采用索引分配方式

    所以从用户角度看就做了以下事情:

    1. 使用C语言库函数fseek,将文件读写指针指向位置n。即指明逻辑地址。

    2. 使用C语言库函数fgetc,从读写指针所指位置读出1B内容。

      fgetc底层使用了Read系统调用,总之操作系统总能将(逻辑块号,块内偏移量)转换为(物理块号,块内偏移量)。

    4.2 顺序文件

    用C语言创建顺序文件

    上面程序运行后,学生信息就会存放在这个文件中。

    c语言创建顺序文件

    每个学生记录占64B。如果要读出第五个学生的信息,需要用以下代码实现:

    这样就可以读出5号学生的编号。总之在用户看来每个学生的记录似乎是连续存放的。并且每个学生占的空间记录是64B,是相同的。所以当要从问件中读出某一个学生对应的记录时,就可以方便算出那条记录对应的逻辑地址。总之只需要给操作系统提供逻辑地址,接下来操作系统会完成转换的操作。

    上面是用顺序存储方式,同样用户也可以采用链式存储方式:

    链式存储

    对于用户来说

    1. 顺序存储,各条记录相邻这存放。

      支持随机访问:指可以直接确定第i条记录的逻辑地址。

    2. 链式存储,各条记录离散着存放,用指针表示先后关系。

    所以用户可以自己决定采用什么方式。但是无论如何在用户看来这些记录都是占有一整片连续空间。

    从操作系统视角来看,学生信息就是一堆二进制数据,每个磁盘块可存储1KB,学生信息会被拆分为一块一块!

    物理结构

    此时操作系统可以采用连续分配、链接分配或者索引分配方式存放这些数据。至于哪一种分配方式作为用户不需要关心。

    所以文件内部各条记录链式存储是由创建文件的用户自己设计的。而文件整体用链接分配方式是由操作系统决定。

    4.3 索引文件

    C语言实现索引结构:

    这样一个IndexTable结构体就对应了一个索引项,每个索引项记录就学生的学号和学生记录的地址。之后的空间可以定义一个Student_info存放学生信息。之后索引项会建立起学号和各个学生的映射关系。

    建立索引文件

    从用户视角来看,索引文件中的数据项依然是连续存放的。如:前1MB存放索引项,后续部分存放记录。在找某个学生时候可以先把前面1MB字节的索引数据读入内存。查询其中的索引项找到目标学生的索引项,之后在根据这个索引项的信息确定目标学生的记录存放在哪个逻辑地址当中。

    从操作系统视角来看,这些信息就是一堆二进制数据,每个磁盘块可存储1KB,所以会被拆分为一块一块!

    物理结构

    此时操作系统可以采用连续分配、链接分配或者索引分配方式存放这些数据。至于哪一种分配方式作为用户不需要关心。

    所以索引文件的索引表是用户自己建立的,映射是由关键字记录存放的逻辑地址。而索引分配的索引表是操作系统建立的,映射是从逻辑块号物理块号。

    逻辑结构和物理结构总结:

    逻辑结构和物理结构总结

    5. 文件存储空间管理

    之前的文件物理结构/文件分配方式探讨的是非空闲磁盘块的管理。而对空闲磁盘的管理是文件存储空间管理要探讨的内容。

    操作系统需要对磁盘块进行的管理

    文件的存储空间管理内容如下:

    文件存储空间管理知识总览

    5.1 存储空间的划分与初始化

    安装Windows操作系统的时候,一个必经步骤是为磁盘分区(C:盘、D: 盘、E: 盘等)。其本质是存储空间的划分,将物理磁盘划分为一个个文件卷(逻辑卷、逻辑盘)。

    在存储空间的初始化过程中,需要把文件卷进行进一步的划分。分目录区和文件区。:

    物理磁盘

    另外有的系统支持超大型文件,可支持由多个物理磁盘组成个文件卷。

    物理磁盘卷

    下面会介绍几种存储空间管理:空闲表法、空闲链表法、位示图法、成组链接法。

    空闲表法

    假设一个磁盘情况如下:

    空闲表法

    上图绿色的为空闲块,橙色是非空闲快。假设用的是空闲表法,系统对应的空闲盘快表如下:

    空闲盘块表

    表中记录了每个空闲块的起始位置(第一个空闲盘块号)和空闲区间的长度(空闲盘块数)。这种方法适用于适用于"连续分配方式"。

    采用这种方法如何分配磁盘块:与内存管理中的动态分区分配很类似,为一个文件分配连续的存储空间。同样可采用首次适应、最佳适应、最坏适应等算法来决定要为文件分配哪个区间。

    假如:新创建的文件请求3个块,采用首次适应算法。

    则系统会因此检查空闲盘快表中的表项,然后找到第一个能够满足3个块的空闲区间。前两个空闲盘块数位2,1都不满足,但第三个空闲盘块数是5,所以满足,故从这个盘块的起始位置10开始,往后分配三个块。即10,11,12这三个空闲盘块分配给新创建的文件使用。

    空闲表法2

    之后再修改空闲盘块表。

    空闲盘块表2

    采用这种方式如何回收磁盘块:与内存管理中的动态分区分配很类似,当回收某个存储区时需要有四种情况:

    1. 回收区的前后都没有相邻空闲区。

      会再空闲盘块表中新增一个表项。

    2. 回收区的前后都是空闲区。

      需要把前后空闲区和新的空闲区进行合并。发生这种情况表项的数量会减少一个。

    3. 回收区前面是空闲区。

      新的空闲区和前面的空闲区进行合并,不会导致表项的数量改变。

    4. 回收区后面是空闲区。

      新的空闲区和前面的空闲区进行合并,不会导致表项的数量改变。

    5. 总之,回收时需要注意表项的合并问题。

    空闲链表法

    空闲链表法可以进一步划分为空闲盘块链和空闲盘区链。区别在于:

    比起空闲盘块链来说,空闲盘区链效率要更高。因为空闲盘块链只能从链当中一个一个把这些磁盘块划分出来。而空闲盘区链可以一次性划分出一大片连续的空闲区间。所以在分配多个盘块时效率更高。

    位示图法

    位示图:每个二进制位对应一个盘块。 在本例中,"0"代表盘块空闲,"1"代表盘块已分配。位示图一般用连续的"字"来表示,如本例中一个字的字长是16位,字中的每一位对应一个盘块。因此可以用(字号,位号)对应一个盘块号。当然有的题目中也描述为(行号,列号)。

    位示图法

    如上图,如果绿色表示空闲盘块,橙色的表示已分配盘块。则第一行四个盘块在位示图中的表示是0,1,0,1。故上面磁盘块位示图如下:

    位示图法2

    上图一个字的字长是16位。因此可以用字号和位号这样的二元组定位到其中的一个二进制位。

    位示图法3

    要能自己推出盘块号与(字号,位号)相互转换的公式

    要注意题目条件:盘块号、字号、位号到底是从0开始还是从1开始如本例中盘块号、字号、位号从0开始,若n表示字长,

    (,)=(i,j)的二进制位对应的盘块号b=ni+j。例如(1,10)b=161+10=26

    b号盘块对应的字号i=b/n,位号j=b%n。例如b=13i=13/16=0,j=13%16=13

    如何分配:若文件需要K个块:

    1. 顺序扫描位示图,找到K个相邻或不相邻的0
    2. 根据字号、位号算出对应的盘块号,将相应盘块分配给文件。
    3. 将相应位设置为1

    如何回收:

    1. 根据回收的盘块号计算出对应的字号、位号。
    2. 将相应二进制位设为0

    采用位示图法连续分配、离散分配都适用。

    成组链接法

    空闲表法、空闲链表法不适用于大型文件系统,因为空闲表或空闲链表可能过大。UNIX系统中采用了成组链接法对磁盘空闲块进行管理。

    文件卷的目录区中专门用一个磁盘块作为"超级块",当系统启动时需要将超级块读入内存。并且要保证内存与外存中的"超级块"数据一致。

    成组链接法

    超级块作用:在超级块中记录了下一组空闲盘块数。然后还要记录对应一组空闲盘块的块号。

    超级块

    通过这些盘块号就可以依次找到下一个分组的各个盘块了。

    成组链接法盘块

    可以看到每组的盘号对应的第一个盘块会记录下一个分组的信息,即超级块信息。而注若已经没有下一组空闲快,则第一个盘块处设为某特殊值,如1

    同时还需要注一个分组中的块号不需要连续,此处只是为了让大家更方便看出各个分组的数量。并且最后一组空闲块的数量一般会少1,因为第一个空闲块内记录一个特殊值。

    成组链接法盘块2

    如何分配:

    例1:如需要1个空闲块

    1. 检查第一个分组的块数是否足够。1<100,因此是足够的。

      成组链接法盘块5

    2. 分配第一个分组中的1个空闲块,并修改相应数据。

      成组链接法盘块3

      会选择一组空闲块的最后一个空闲块,放入数据后删除该空闲块,之后修改对应的超级块信息。

    例2:如需要100个空闲块

    1. 检查第一个分组的块数是否足够。此时100=100,是足够的。

      成组链接法盘块6

    2. 分配第一个分组中的100个空闲块。但是由于300号块内存放了再下一组的信息,因此300号块的数据需要复制到超级块中,之后删除这一组所有空闲块。

      成组链接法盘块4

    如何回收: 假设每个分组最多为100个空闲块,此时第一个分组已有100个块,还要再回收一块。

    成组链接法盘块4

    需要将超级块中的数据复制到新回收的块中,并修改超级块的内容,让新回收的块成为第一个分组。

    成组链接法盘块7

    文件存储空间管理总结:

    文件存储空间管理总结

    6. 文件的基本操作

    操作系统向上提供了几个文件管理最基本的功能:创建文件(create 系统调用)、删除文件(delete系统调用)、读文件(read 系统调用)、写文件(write系统调用)、打开文件(open系统调用)、关闭文件(close 系统调用)。

    向上提供几个最基本的功能

    文件基本操作总结:

    文件基本操作总结

    7. 文件共享与保护

    操作系统为用户提供文件共享功能,可以让多个用户共享地使用同一个文件。

    文件共享方式有:基于索引结点的共享方式(硬链接)、基于符号链的共享方式(软链接)

    注意:多个用户共享同一个文件,意味着系统中只有"一份"文件数据。并且只要某个用户修改了该文件的数据,其他用户也可以看到文件数据的变化。

    如果是多个用户都"复制"了同一个文件,那么系统中会有"好几份"文件数据。其中一个用户修改了自己的那份文件数据,对其他用户的文件数据并没有影响。

    7.1 基于索引结点的共享方式(硬链接)

    知识回顾:索引结点,是一种文件目录瘦身策略。由于检索文件时只需用到文件名,因此可以将除了文件名之外的其他信息放到索引结点中。这样目录项就只需要包含文件名、索引结点指针。

    假设用户User1创建一个文件aaa,这个文件会对应一个索引结点,并且索引结点中会包含文件的物理地址及其他属性。

    硬链接

    索引结点中设置一个链接计数变量count,用于表示链接到本索引结点上的用户目录项数。

    count=2,说明此时有两个用户目录项链接到该索引结点上,或者说是有两个用户在共享此文件。

    硬链接多用户

    若某个用户决定"删除"该文件,则只是要把用户目录中与该文件对应的目录项删除,且索引结点的count值减1

    count>0, 说明还有别的用户要使用该文件,暂时不能把文件数据删除,否则会导致指针悬空。当count=0时系统负责删除文件。

    7.2 基于符号链的共享方式(软链接)

    软连接是Link类型的文件,记录了文件1的存放路径C:/User1/aaa。类似于Windows操作系统的"快捷方式"。

    软连接

    当User3访问ccc时,操作系统判断文件ccc属于Link类型文件,于是会根据其中记录的路径层层查找目录,最终找到User1的目录表中的aaa表项,于是就找到了文件1的索引结点。

    如果文件1已删除,但是文件2(软连接)依然存在,只是通过C:/User1/aaa这个路径已经找不到文件1了。

    软链接找不到文件

    由于用软链接的方式访问共享文件时要查询多级目录,会有多次磁盘I/O,因此用软链接访问比硬链接访问要慢。

    文件共享总结:

    文件共享总结

    7.3 文件保护

    操作系统需要保护文件数据的安全。保护的方式一般有三种:口令保护、加密保护、访问控制。

    注意:这里的口令是指日常生活俗称的密码,这里的密码是指直接对文件内容的加解密。

    文件保护总结:

    文件保护总结

    8. 文件系统

    文件系统层次结构如下:

    文件系统层次结构

    下面会对每一层进行分析:

    用一个例子来辅助记忆文件系统的层次结构:

    假设某用户请求删除文件D:/工作目录/学生信息.xIsx的最后100条记录。

    1. 用户需要通过操作系统提供的接口发出上述请求,即用户接口层
    2. 由于用户提供的是文件的存放路径,因此需要操作系统一层一层地查找目录,找到对应的目录项,即文件目录系统层
    3. 找到文件对应的FCB/索引结点后,不同的用户对文件有不同的操作权限,因此为了保证安全,需要检查用户是否有访问权限,即存取控制模块(存取控制验证层)
    4. 验证了用户的访问权限之后,需要把用户提供的"记录号"转变为对应的逻辑地址,即逻辑文件系统与文件信息缓冲区
    5. 知道了目标记录对应的逻辑地址后,还需要转换成实际的物理地址,即物理文件系统
    6. 如果要删除这条记录,必定要对磁盘设备发出请求,即设备管理程序模块
    7. 删除这些记录后,会有一些盘块空闲, 因此要将这些空闲盘块回收,即辅助分配模块

    8.1 文件系统的全局结构

    从一个磁盘出厂,物理格式化,逻辑格式化来一步一步认识文件系统在外存中是如何被建立的。

    磁盘

    文件系统在外存中的结构

    文件系统在内存中的结构

    内存结构如下:

    内存结构

    内核区中有个重要的目录缓存,即最近访问过的目录文件数据会被暂时缓存在内存当中。这样做可以加快目录检索的速度。

    还有两个重要的数据结构:系统打开文件表和进程打开文件表。

    系统打开文件表整个系统之有一张,而进程打开文件表每一个进程都有一张。这个进程打开文件表是包含在每个进程的PCB中的,记录了某一个进程当前打开了哪些文件。

    下面介绍open系统调用打开文件过程:

    系统调用打开文件过程

    通过open系统调用打开目录M下的A文件,即目录/M/A。假设已经找到了文件A存放的目录M,接下来会把目录M的数据读入到主存,并缓存。读入主存后就可以检查目录项,一个一个对比是否有没有要找的文件A。找到后会把文件A的FCB复制到系统打开文件表中,表示这个文件被打开,同时设置打开计数为1。另外发起open调用的进程需要在进程打开文件表中,新建一个条目记录打开文件A的方式,同时返回这个文件的文件描述符,即指向进程打开文件表的指针。

    8.2 虚拟文件系统

    普通文件系统:

    普通文件系统

    计算机会有很多外部设备。不同外部设备里面的文件系统是各不相同的,所以就导致各个外部设备的函数接口不同,如上图的open函数各个外部设备都不相同。这对上层的开发人员来说很麻烦,因此操作系统内核听该向上层用户进程提供一个统一标准的函数调用接口。所以在操作系统中就引入了虚拟文件系统(VFS)。

    虚拟文件系统

    有了虚拟文件系统之后,用户在打开一个文件进程的时候,只需要根据虚拟文件系统制定的标准来调用函数就可以。

    所以虚拟文件系统向上层用户进程提供统一标准的系统调用接口,屏蔽底层具体文件系统的实现差异

    虚拟问价系统可以处理上层用户发来的标准系统调用请求,之后虚拟文件系统会负责操作底层一个具体的文件系统(如UFS文件系统)。但是每个文件系统的函数名或者参数都不一样,所以为了解决这个问题,虚拟文件系统会要求底层的文件系统必须实现虚拟文件系统规定好的函数接口。

    所以VFS要求下层的文件系统必须实现某些规定的函数功能,如:open/read/write等。一个新的文件系统想要在某操作系统上被使用,就必须满足该操作系统VFS的要求

    另外还存在问题:不同的文件系统,表示文件数据结构各不相同。打开文件后,其在内存中的表示就不同。

    UFS和FAT文件系统结构:

    UFS和FAT文件系统结构

    所以VFS在内存中要根据不同的文件系统结构建立不同的数据结构,这显然不合理。为了解决这个问题在VFS每当打开一个文件之后,虚拟文件系统就会主存中建立一个vnode(v结点)的数据结构。

    V结点

    这个V结点中包含的就是文件的信息,所以无论打开的文件属于哪个文件系统,都会把文件相关的信息赋值到这个V结点中。这样的话VFS就可以用一个统一的数据结构V结点,来表示任何一个文件的信息,无论文件存放在哪一个文件系统中。

    每打开一个文件,VFS就在主存中新建一个vnode,用统一的数据结构表示文件,无论该文件存储在哪个文件系统

    注意:vnode只存在于主存中,而UFS文件系统目录项的结点记录信息inode既会被调入主存,也会在外存中存储。如果此时要打开的文件是在UFS文件系统中,那么在找到文件所对应的目录项之后,会把文件的inode从外存先读入主存,然后在主存中新建一个vnode,inode的各种信息会被复制到vnode中。

    可以看到vnode结点中的函数功能指针,之前说过不同的文件系统需要实现文件系统规定的函数功能。而每个文件系统背后的具体代码是各不相同的。所以vnode中的函数功能指针其实是指向对应文件系统的函数功能列表。这样的话只要open打开一个文件,之后要对文件进行的任何操作,都可以先找到vnode中的函数功能指针,根据这个指针再找到具体对应的文件系统的函数功能列表,之后再去执行具体的函数。这样就可以实现从上到下一层一层的调用。

    V结点的函数功能指针

    所以在打开文件后,创建vnode,并将文件信息复制到vnode中,vnode 的功能指针指向具体文件系统的函数功能。

    虚拟文件系统(VFS)特点

    1. 向上层用户进程提供统一标准的系统调用接口,屏蔽底层具体文件系统的实现差异。
    2. VFS要求下层的文件系统必须实现某些规定的函数功能,如:open/read/write等。一个新的文件系统想要在某操作系统上被使用,就必须满足该操作系统VFS的要求。
    3. 每打开一个文件,VFS就在主存中新建一个vnode,用统一的数据结构表示文件,无论该文件存储在哪个文件系统。

    8.3 文件系统的挂载

    文件系统挂载(mounting),即文件系统安装/装载,指如何将一个文件系统挂载到操作系统中。

    挂载功能经常用到,如U盘插入电脑后,U盘的文件系统就需要挂载到操作系统上。具体来说就是要挂载到操作系统的虚拟文件系统上。

    文件系统挂载

    文件系统挂载要做的事情:

    1. 在VFS中注册新挂载的文件系统。

      在内存中VFS会管理一个数据结构,即挂载表( mount table)。其中包含每个文件系统的相关信息,包括文件系统类型、容量大小等。

      如上图三个外部设备对应的文件系统挂载到VFS中,此时VFS的挂载表中会有三个表项,三个表项分别指向UFS、NTFS和FAT三个文件系统。当要挂载一个新的文件系统的时候,新挂载的文件系统也会给他新建一个表项,这就完成一个文件系统的注册。

    2. 新挂载的文件系统,要向VFS提供一个函数地址列表。即函数功能列表存放在哪个位置。

    3. 将新文件系统加到挂载点(mount point),也就是将新文件系统挂载在某个父目录下。如:对于Windows操作系统而言,新挂载的文件系统挂载点就是可移动磁盘(H)

    五. 设备管理

    通过之前的学习知道,操作系统作为系统资源的管理者,即需要对上层的软件进行管理,也需要对下层的硬件进行管理。这一章节会探讨操作系统对外部设备的管理。

    操作系统作为系统资源的管理者

    外部设备又可以称为I/O设备。即I/O设备就是可以将数据输入到计算机,或者可以接收计算机输出数据的外部设备,属于计算机中的 硬件部件。

    常见的外部设备

    输入过程无非就是把设备准备好的数据读入计算机当中。而输出的过程就是把计算机当中准备好的数据写出到输出设备中。因此UNIX系统将外部设备抽象为一种特殊的文件,用户可以使用与文件操作相同的方式对外部设备进行操作。

    如:

    1. Write操作:向外部设备写出数据
    2. Read操作:从外部设备读入数据

    I/O设备按使用特性分类:

    按传输速率分类:

    按信息交换的单位分类:

    I/O设备概念总结:

    IO设备概念总结

    1. I/O控制器

    I/O设备是有机械设备和电子部件组成的。电子部件即I/O控制器或者叫设备控制器。

    I/O设备的机械部件主要用来执行具体I/O操作。如我们看得见摸得着的鼠标/键盘的按钮、显示器的LED屏、移动硬盘的磁臂、磁盘盘面等。

    I/O设备的电子部件通常是一块插入主板扩充槽的印刷电路板。这里要重点掌握电子部件的知识。

    I/O设备连上电脑后,CPU没办法直接控制这些设备的机械部件。必须通过电子部件来间接控制这些部件。因此I/O设备还要有一个电子部件作为CPU和I/O设备机械部件之间的"中介",用于实现CPU对设备的控制。而这个电子部件就是I/O控制器,又称设备控制器。

    CPU可控制I/O控制器,又由I/O控制器来控制设备的机械部件。

    I/O控制器的功能有:

    1. 接受和识别CPU发出的命令

      如CPU发来的read/write命令,I/O控制器中会有相应的控制寄存器来存放命令和参数。

    2. 向CPU报告设备的状态

      I/O控制器中会有相应的状态寄存器,用于记录必设备的当前状态。如:1表示空闲,0表示忙碌。

    3. 数据交换

      I/O控制器中会设置相应的数据寄存器。如果数据要从主机输出到设备时,数据寄存器用于暂存CPU发来的数据,之后再由控制器传送设备。输入时,数据寄存器用于暂存设备发来的数据,之后CPU从数据寄存器中取走数据。

    4. 地址识别。

      类似于内存的地址,为了区分设备控制器中的各个寄存器,也需要给各个寄存器设置一个特定的"地址"。I/O控制器通过CPU提供的"地址"来判断CPU要读/写的是哪个寄存器。

    I/O控制器由三部分组成:CPU与控制器接口、I/O逻辑、控制器与设备的接口。其中数据处理主要在I/O逻辑部分完成。:

    一个I/O控制器可以控制多个外部设备接口,为了区分这些外部设备,需要给设备接口一个编号。CPU在发出I/O命令时候也需要指明要操作的是哪个设备。

    IO控制器执行流程

    CPU首先会通过控制线,向I/O控制器发出具体的I/O指令。同时CPU还会在地址线上说明要操纵的是哪一个设备。如果此时要输出一个数据,CPU通过数据总线把自己要输出的数据放到I/O控制器的数据寄存器当中。之后I/O逻辑就可以从数据寄存器当中取得CPU想要输出的数据,再通过对应的设备接口输出数据。有时候,CPU发出的I/O指令可能会有一些相应的参数,这些参数会放到控制寄存器当中I/O逻辑可以从控制寄存器中读出相应的参数。另外为了实现对各个设备管理,CPU还会从状态寄存器中读出各个设备的状态。如:忙碌/空闲/故障等。这个数据是由I/O逻辑放入状态寄存器的。数据输入原理一样,通过控制器与设备接口进入I/O逻辑,之后放入数据寄存器中。

    另外设备接口除了状态线路保存设备状态之外,还需要有一个控制线路,可以向外部设备发出控制信息。

    IO控制器执行流程完整版

    值得注意的小细节:

    1. 一个I/O控制器可能会对应多个设备接口。
    2. 数据寄存器、控制寄存器、状态寄存器可能有多个(如:每个控制/状态寄存器对应一个具体的设备),且这些寄存器都要有相应的地址,才能方便CPU操作。有的计算机会让这些寄存器占用内存地址的一部分,称为内存映像I/O。另一些计算机则采用I/O专用地址,即寄存器独立编址。

    所以对于寄存器编址方式有两种:内存映像I/O和寄存器独立编址。

    内存映射I/O。控制器中的寄存器与内存地址统一编址。这种方式的优点是简化了指令,可以采用对内存进行操作的指令来对控制器进行操作。

    内存映像IO

    寄存器独立编址:控制器中的寄存器使用单独的地址。这种方式缺点是需要设置专门的指令来实现对控制器的操作,不仅要指明寄存器的地址,还要指明控制器的编号。

    寄存器独立编址

    I/O控制器总结:

    IO控制器总结

    2. I/O控制方式

    随着计算机发展I/O控制器不断发展,相应的I/O控制器对设备的控制方式也出现变化。而I/O控制方式本质是用什么样的方式来控制I/O设备的数据读/写。

    常见的I/O控制方式有四种:程序直接控制方式、中断驱动方式、DMA方式、通道控制方式。

    2.1 程序直接控制方式

    执行流程

    完成一次读/写操作的流程(以读操作为例):

    CPU首先通过控制线向I/O控制器发出读指令,然后I/O控制器会根据CPU的要求来启动相应的设备,并且会把设备的状态设置为1(未就绪)。接着设备就会准备读入数据,但是由于设备速度要比CPU慢很多,所以在设备还没有完成I/O之前,CPU会不断轮询检查设备状态寄存器。如果状态寄存器数字一直为1就表明设备正在忙碌,还没有准备好想要读入的数据。如果设备已经准备好了数据,这个设备接口就会给I/O逻辑传送这次输入的数据,并且将自己状态改为0(已就绪)。I/O逻辑会把设备传送过来的数据放到数据寄存器当中。当CPU发现设备状态寄存器变为0,CPU就可以从数据寄存器中读取此次输入的数据。CPU会首先读入CPU自己的寄存器中,再把CPU寄存器中的内容放入内存。

    程序直接控制方式

    所以其实数据输入的过程本来应该从设备输入到内存,但是这个过程必须先经过CPU寄存器然后再由于寄存器转存当内存中。最后若还要继续读入数据,则CPU继续发出读指令。

    直接控制方式流程图:

    直接控制方式流程图

    CPU干预的频率与其他条件分析

    CPU需要不断轮询检查这个I/O操作是否完成。所以CPU干预很频繁,I/O操作开始之前、完成之后需要CPU介入,并且在等待I/O完成的过程中CPU需要不断地轮询检查。

    数据传送的单位:每次读入或者写出的数据量是一个字。

    数据流向:读操作:I/O设备CPU内存。写数据:内存CPUI/O设备。所以每个字的读/写都需要CPU的帮助。也就是说CPU需要花费大量时间来辅助完成这个I/O过程。

    优点:实现简单。在读/写指令之后,加上实现循环检查的一系列指令即可(因此才称为"程序直接控制方式")。

    缺点:CPU和I/O设备只能串行工作,CPU需要一直轮询检查,长期处于"忙等"状态,CPU利用率低。相应的当CPU在进行别的工作时,I/O设备是空闲的,所以外部设备利用率也低。

    2.2 中断驱动方式

    执行流程

    引入中断机制。由于I/O设备速度很慢,因此在CPU发出读/写命令后,可将等待I/O的进程阻塞,先切换到别的进程执行。当I/O完成后,控制器会向CPU发出一个中断信号,CPU检测到中断信号后,会保存当前进程的运行环境信息,转去执行中断处理程序处理该中断。处理中断的过程中,CPU从I/O控制器读一个字的数据传送到CPU寄存器,再写入主存。接着,CPU恢复等待I/O的进程(或其他进程)的运行环境,然后继续执行。

    中断驱动方式

    注意:

    1. CPU会在每个指令周期的末尾检查中断;
    2. 中断处理过程中需要保存、恢复进程的运行环境,这个过程是需要一定时间开销的。可见,如果中断发生的频率太高,也会降低系统性能。

    CPU干预的频率与其他条件分析

    每次I/O操作开始之前、完成之后需要CPU介入。等待I/O完成的过程中CPU可以切换到别的进程执行。

    所以引入中断之后才实现CPU和I/O设备,并行工作的特点。

    数据传送的单位:每次读/写一个字。

    数据流向:读操作:I/O设备CPU内存。写数据:内存CPUI/O设备。

    优点:与"程序直接控制方式"相比,在"中断驱动方式"中,I/O控制器会通过中断信号主动报告I/O已完成,CPU不再需要不停地轮询。CPU和I/O设备可并行工作,CPU利用率得到明显提升。

    缺点:每个字在I/O设备与内存之间的传输,都需要经过CPU。而频繁的中断处理会消耗较多的CPU时间。另外采用这种方式,在读入数据和写出数据时候都必须先经过CPU,但是前面的学习可以知道读入数据就是将I/O设备准备好的数据放入内存的过程,而写出数据就是将内存中的数据写出到I/O设备。所以CPU起到一个没必要的中转作用。

    2.3 DMA方式

    与"中断驱动方式"相比,DMA方式(Direct Memory Access,直接存储器存取。主要用于块设备的I/O控制)有这样几个改进:

    1. 数据的传送单位是"块"。不再是一个字一个字的传送。
    2. 数据的流向是从设备直接放入内存,或者从内存直接到设备。不再需要CPU作为"中转"。
    3. 仅在传送一个或多个数据块的开始和结束时,才需要CPU干预。

    DMA执行流程

    所以DMA方式流程如下:

    DMA方式执行流程

    首先CPU会给I/O模块发出读/写块命令,之后CPU就可以做其他事情,接着DMA控制器会根据CPU发出的这些命令参数,完成CPU指定一系列的读写工作。当CPU指定的这些块读写完毕后,会由DMA控制器向CPU发出一个中断信号,之后CPU会转而处理这个中断信号。

    这里的DMA控制器也是一种I/O控制器。由以下几个部分组成:

    DMA控制器

    1. DR(Data Register,数据寄存器):暂存从设备到内存,或从内存到设备的数据。
    2. MAR(Memory Address Register,内存地址寄存器):在输入时,MAR 表示数据应放到内存中的什么位置。输出时MAR表示要输出的数据放在内存中的什么位置。
    3. DC(Data Counter,数据计数器):表示剩余要读/写的字节数。
    4. CR(Command Register,命令/状态寄存器):用于存放CPU发来的I/O命令,或设备的状态信息。

    上面部件是最重要的,而在DMA控制器和块设备之间也有一个相对应的接口,通过这个接口可以实现控制器对于外部设备的通信控制。同时系统总线还会把DMA控制器和内存连接在一起。所以DMA控制器和内存之间可以直接进行数据的读写不再需要经过CPU。

    DMA控制器方式

    假如CPU在刚开始指明这次要读入的数据是存放在磁盘什么位置。这个位置信息是存放在MAR中的。并且还会说明此时要读入的数据量是多少。接着DMA控制器会根据CPU提供的一系列参数,从磁盘的相应位置读入数据,然后写到内存里。而这个过程就不需要CPU介入。只有DMA控制器完成这一系列指定的任务之后才会向CPU发出一个中断信号,让CPU进行后序处理。

    这里需要注意其实DMA控制器并不是每次直接读入一整块的数据,在读入数据的过程中也是一个字一个字读入的。每次读入的字会先存放在DR中。再从DR写入到内存中,用这种方式一个字一个字读入最终就可以完成一整块数据的读入工作。

    CPU干预的频率与其他条件分析

    仅在传送一个或多个数据块的开始和结束时,才需要CPU干预。

    数据传送的单位:每次读/写一个或多个块(注意:每次读写的只能是连续的多个块,且这些块读入内存后在内存中也必须是连续的)。

    数据的流向(不再需要经过CPU),读操作(数据输入):I/O设备内存。写操作(数据输出):内存I/O设备。

    优点:数据传输以"块"为单位,CPU介入频率进一步降低。数据的传输不再需要先经过CPU再写入内存,数据传输效率进一步增加。CPU和I/O设备的并行性得到提升。

    缺点:CPU每发出一条I/O指令,只能读/写一个或多个连续的数据块。如果要读/写多个离散存储的数据块,或者要将数据分别写到不同的内存区域时,CPU要分别发出多条I/O指令,进行多次中断处理才能完成。

    2.4 通道控制方式

    通道:是一种硬件,可以理解为是"弱鸡版的CPU"。通道可以识别并执行一系列通道指令。

    执行流程

    首先CPU向通道发出I/O指令。指明通道程序在内存中的位置,并指明要操作的是哪个I/O设备。之后CPU就切换到其他进程执行了。接着通道执行内存中的通道程序(其中指明了要读入/写出多少数据,读/写的数据应放在内存的什么位置等信息)。这里的通道程序可以理解为一种任务清单,是一系列通道指令的集合。当通道执行完一些列任务之后,会向CPU发送中断信号,之后CPU对中断进行处理。

    通道控制方式

    通道被称为弱鸡版CPU是因为与CPU相比,通道可以执行的指令很单一,并且通道程序是放在主机内存中的,也就是说通道与CPU共享内存。

    通道控制方式执行流程

    CPU干预的频率与其他条件分析

    CPU的干预频率变得极低,通道会根据CPU的指示执行相应的通道程序,只有完成一组数据块的读/写后才需要发出中断信号,请求CPU干预。

    数据传送的单位:每次读/写一组数据块。

    数据的流向(不再需要经过CPU),读操作(数据输入):I/O设备内存。写操作(数据输出):内存I/O设备。

    缺点:实现复杂,需要专门的通道硬件支持。

    优点:CPU、通道、I/O设备可并行工作,资源利用率很高。

    四种控制方式总结:

    四种控制方式总结

    3. I/O软件层次结构

    I/O软件的层次从上到下依次为:用户层软件、设备独立性软件、设备驱动程序、中断处理程序、硬件。

    IO软件的层次结构

    设备独立性软件、设备驱动程序、中断处理程序属于操作系统的内核部分。即"I/O系统",或称"I/O核心子系统"。

    I/O设备的硬件又是由机械部分和电子部分组成的,所以硬件部分的原理已经在I/O控制器这一小节讲解过。这里重点关注软件层次。

    可以看出越上面的层次越接近用户。越下面的层次越接近硬件。各个层次都会使用下面一层提供的功能,并且向上层的软件提供一些服务。像这种每一层会利用其下层提供的服务,实现某些功能,并屏蔽实现的具体细节,向高层提供服务("封装思想")。

    I/O软件层次结构总结:

    IO软件层次结构总结

    理解并记住I/O软件各个层次之间的顺序,要能够推理判断某个处理应该是在哪个层次完成的(最常考的是设备独立性软件、设备驱动程序这两层。只需理解一个特点即可:直接涉及到硬件具体细节、且与中断无关的操作肯定是在设备驱动程序层完成的。没有涉及硬件的、对各种设备都需要进行的管理工作都是在设备独立性软件层完成的)。

    4. 输入输出应用程序接和驱动程序接口

    其中输入输出应用程序接口分为:字符设备接口、块设备接口、网络设备接口。

    输入输出管理预览

    4.1 输入输出应用程序接口

    I/O软件分为多个层次,上层的用户应用程序需要通过系统调用的方式来请求使用底层的某一种I/O设备。外部设备种类特性非常多样化,所以上层的应用程序很难通过一个统一的系统调用接口来完成所有类型的I/O设备。

    输入输出应用程序接口

    比如像块设备就是有寻址的功能,一个磁盘可以指定读取哪个地址。由于块设备有寻址和支持多个字节读取,因此当写程序时要用系统调用方式从块设备中读入数据,那么read系统调用就可以写成read(指明操作块设备,读入字节数,指明从哪开始读)。这种读操作系统调用接口显然不适用于字符设备。因为字符设备是不可寻址的,且每次只读1个字符。所以,用户层的应用程序无法用一个统一的系统调用接口来完成所有类型设备的I/O

    所以设备独立性软件这一层需要对上层的应用程序提供若干种类型的程序接口。上层应用程序访问字符设备、块设备、网络设备所调用的系统接口是各不相同的。

    可以看出给上层应用程序提供的不同接口其实是依据这种类型设备的特性来定义的。字符型设备没有地址概念,所以get/put就不需要提供地址参数。而块设备有地址概念所以块设备访问就需要提供一个地址参数,指明要读或写的是哪个地址。

    阻塞与非阻塞I/O

    阻塞I/O:应用程序发出I/O系统调用,进程需转为阻塞态等待。如:字符设备接口,即从键盘读一个字符,使用get调用,只要这个键盘输入的动作没有完成,这个进程就需要一直等下去。

    非阻塞I/O:应用程序发出I/O系统调用,系统调用可迅速返回,进程无需阻塞等待。如:块设备接口,即往磁盘写数据,使用write调用,这个系统调用可以很快处理完,然后迅速返回到用户进程继续往下执行。因为进程准备数据是在用户区,而操作系统内核又有内核区,当发出write调用,即使磁盘正在忙碌,设备独立性软件那一层也会迅速响应请求。先把数据复制到内核,内核会将数据依次写入磁盘。

    4.2 设备驱动程序接口

    设备独立性软件需要根据实际操作设备的不同区调用不一样的设备驱动程序。

    设备驱动程序接口

    如上图所示若各公司开发的设备驱动程序接口不统一,则操作系统很难调用设备驱动程序。

    因此操作系统规定好设备驱动程序的接口标准,各厂商必须按要求开发设备驱动程序。

    统一的设备驱动程序接口

    同时不同的操作系统,对设备驱动程序接口的标准各不相同。所以设备厂商必须根据操作系统的接口要求,开发相应的设备驱动程序,设备才能被使用。

    5. I/O核心子系统

    备独立性软件、设备驱动程序、中断处理程序属于操作系统的内核部分。即"I/O系统",或称"I/O核心子系统"。

    IO系统层次结构

    因此I/O核心子系统要实现的功能其实就是中间三层要实现的功能。

    考研中,我们需要重点理解和掌握的功能是:I/O调度、设备保护、假脱机技术(SPOOLing技术)、设备分配与回收、缓冲区管理(即缓冲与高速缓存)。这些功能实现层次如下:

    1. 用户层软件

      实现假脱机技术(SPOOLing技术)。一般来说假脱机技术需要使用到磁盘这种设备的设备独立性软件这层的服务。所以假脱机技术是在用户层软件实现的。

    2. 设备独立性软件

      I/O调度、设备保护、设备分配与回收、缓冲区管理(即缓冲与高速缓存)

    IO系统实现功能

    这里先介绍两种很熟的功能实现:I/O调度、设备保护。

    5.1 假脱机技术(SPOOLing技术)

    之前计算机发展史介绍过,批处理阶段引入了脱机输入/输出技术(用磁带完成):

    程序员可以先用纸带机,将程序数据输入到磁带中,而磁带速度要比纸带机快很多。而这个输入过程是由一台专门的外围控制机实现的。在外围控制机的控制下,慢速输入设备的数据先被输入到更快速的磁带上。之后主机可以从快速的磁带上读入数据,从而缓解了速度矛盾。输出过程一样。

    脱机技术

    脱机,即脱离主机的控制进行的输入/输出操作。引入脱机技术后,缓解了CPU与慢速I/O设备的速度矛盾。另一方面,即使CPU在忙碌,也可以提前将数据输入到磁带。即使慢速的输出设备正在忙碌,也可以提前将数据输出到磁带。

    基于这种脱机技术思想又发明了假脱机技术(SPOOLing技术)。

    "假脱机技术",又称"SPOOLing技术"是用软件的方式模拟脱机技术。"SPOOLing"系统的组成如下:

    SPOOLing系统构成

    系统会在磁盘上开辟两个存储区,一个输入井一个输出井。"输入井"模拟脱机输入时的磁带,用于收容I/O设备输入的数据。"输出井"模拟脱机输出时的磁带,用于收容用户进程输出的数据。所以回到例子中,输入井和输出井就是模拟两个磁带的过程:

    输入井和输出井

    除了磁带外,外围控制机也是实现脱机技术当中一个很重要的部件。外围机就是一个输入进程和一个输出进程来实现的。其中输入进程模拟脱机输入时的外围控制机,输出进程模拟脱机输出时的外围控制机。

    显然这个输入进程和输出进程需要和用户进程并发进行,才可以完成这种模拟脱机输入和模拟脱机输出的过程。因此要实现SPOOLing技术,必须要有多道程序技术的支持。也就是说当有数据从设备输入到计算机时候,输入进程是由软件方式模拟外围控制机,输入进程会把要输入的数据放到磁盘的输入井中,而输出进程在模拟脱机输出时候原理也类似。

    同时也要注意,内存中会开辟两个缓冲区,输入缓冲区和输出缓冲区。输入缓冲区作用是在输入进程的控制下,输入缓冲区用于暂存从输入设备输入的数据,之后再转存到输入井中。输出缓冲区作用是在输出进程的控制下,输出缓冲区用于暂存从输出井送来的数据,之后再传送到输出设备上。

    共享打印机原理分析:

    在之前的介绍中提到过两个概念:

    1. 独占式设备,只允许各个进程串行使用的设备。一段时间内只能满足一个进程的请求。
    2. 共享设备,允许多个进程"同时"使用的设备(宏观上同时使用,微观上可能是交替使用)。可以同时满足多个进程的使用请求。

    所以打印机是种"独占式设备",但是可以用SPOOLing技术改造成"共享设备"。

    例子:若进程1正在使用打印机,则进程2请求使用打印机时必然阻塞等待。若采用SPOOLing技术:

    共享打印机原理

    当多个用户进程提出输出打印的请求时,系统会答应它们的请求,但是并不是真正把打印机分配给他们,而是由假脱机管理进程为每个进程做两件事:

    1. 在磁盘输出井中为进程申请一个空闲缓冲区(也就是说,这个缓冲区是在磁盘上的),并将要打印的数据送入其中。

    2. 为用户进程申请一张空白的打印请求表(打印任务说明书),并将用户的打印请求填入表中(其实就是用来说明用户的打印数据存放位置等信息的),再将该表挂到假脱机文件队列上。

      这里的假脱机文件队列是打印的任务队列。

      共享打印机原理2

    当打印机空闲时,输出进程会从文件队列的队头取出一张打印请求表,并根据表中的要求将要打印的数据从输出井传送到输出缓冲区,再输出到打印机进行打印。用这种方式可依次处理完全部的打印任务。

    所以在采用采用SPOOLing技术后虽然系统中只有一台打印机,但每个进程提出打印请求时,系统都会为在输出井中为其分配一个存储区(相当于分配了一个逻辑设备),使每个用户进程都觉得自己在独占一台打印机,从而实现对打印机的共享。

    故SPOOLing技术可以把一台物理设备虚拟成逻辑上的多台设备,可将独占式设备改造成共享设备。

    加脱机技术总结:

    加脱机技术总结

    5.2 设备的分配与回收

    设备独立性软件需要实现设备的分配与回收。

    设备分配考虑因素

    设备的分配时应该考虑的因素:设备的固有属性、设备分配算法、设备分配中的安全性。

    设备分配时考虑的因素

    其中设备的固有属性可以分为三种:独占设备、共享设备、虚拟设备。

    而设备分配算法有:先来先服务、优先级高者优先、短任务优先。

    从进程运行的安全性上考虑,设备分配有两种方式:

    1. 安全分配方式

      为进程分配一个设备后就将进程阻塞,本次I/O完成后才将进程唤醒。(如:考虑进程请求打印机打印输出的例子)

      当一个进程请求打印机打印,并且把打印机分配给进程后,这个进程就必须先阻塞等待。一直到打印结束后,进程才能被唤醒。所以一个时段内每个进程只能使用一个设备。

      优点:破坏了"请求和保持"条件,不会死锁。

      缺点:对于一个进程来说,CPU和I/O设备只能串行工作。CPU利用率会低。

    2. 不安全分配方式

      进程发出I/O请求后,系统为其分配I/O设备,进程可继续执行,之后还可以发出新的I/O请求。只有某个I/O请求得不到满足时才将进程阻塞。

      一个进程请求打印机服务之后,系统会把打印机资源分配给这个进程。此时进程可以继续执行下去,不需要阻塞,甚至在接下来的执行中还可以申请使用别的外部设备。所以一个进程可以同时使用多个设备。

      优点:进程的计算任务和I/O任务可以并行处理,使进程迅速推进。

      缺点:有可能发生死锁(可用于死锁避免、死锁的检测和解除)。

    静态分配与动态分配:

    静态分配:进程运行前为其分配全部所需资源,运行结束后归还资源。如果运行前所需要的全部资源暂时得不到完全满足,进程就暂时不能投入运行。这种方式破坏了"请求和保持"条件,不会发生死锁。

    动态分配:进程运行过程中动态申请设备资源。

    设备分配中使用到的数据结构

    先复习以下设备、控制器、通道之间的关系:

    设备控制器通道之间的关系

    一个通道可控制多个设备控制器,每个设备控制器可控制多个设备。所以数据结构需要表示出这种关系。

    设备分配的具体步骤

    首先操作系统根据进程请求的物理设备名查找SDT(注:物理设备名是进程请求分配设备时提供的参数)。

    设备分配步骤SDT

    于是接下来根据SDT找到DCT, 若设备忙碌则将进程PCB挂到设备等待队列中,不忙碌则将设备分配给进程。

    设备分配步骤DCT

    之后根据DCT找到COCT,若控制器忙碌则将进程PCB挂到控制器等待队列中,不忙碌则将控制器分配给进程。

    设备分配步骤COCT

    接着根据COCT找到CHCT,若通道忙碌则将进程PCB挂到通道等待队列中,不忙碌则将通道分配给进程。

    设备分配步骤CHCT

    注:只有设备、控制器、通道三者都分配成功时,这次设备分配才算成功,之后便可启动I/O设备进行数据传送。

    采用以上设备分配步骤缺点:

    1. 用户编程时必须使用"物理设备名",底层细节对用户不透明,不方便编程

      假如一个物理设备A,后来换成了物理设备B,之后也需要修改代码。

    2. 若换了一个物理设备,则程序无法运行

    3. 若进程请求的物理设备正在忙碌,则即使系统中还有同类型的设备,进程也必须阻塞等待

      如,此时系统中又三台打印机,进程A使用了一台打印机,之后的两台打印机虽然是空闲的,但只要第一台打印机没有结束,这个进程依然需要阻塞等待。但是显然进程的打印任务可以分配给剩余两个打印机。

    改进方法:建立逻辑设备名与物理设备名的映射机制,用户编程时只需提供逻辑设备名。

    引入上面改进方法后:

    首先进程在请求外部设备时,只需要提供逻辑设备名,根据进程请求的逻辑设备名查找SDT。用户编程时提供的逻辑设备名其实就是"设备类型"。

    之后查找SDT,,找到用户进程指定类型的、并且空闲的设备,将其分配给该进程。将设备分配给进程后操作系统在逻辑设备表(LUT)中新增一个表项。

    LUT

    这个逻辑设备表LUT就是记录逻辑设备名到物理设备名的映射关系表。同时还要记录设备驱动程序的入口地址。

    之后的操作就和之前一样。根据DCT找到COCT,若控制器忙碌则将进程PCB挂到控制器等待队列中,不忙碌则将控制器分配给进程。最后根据COCT找到CHCT,若通道忙碌则将进程PCB挂到通道等待队列中,不忙碌则将通道分配给进程。

    只有进程第一次通过逻辑设备名申请一个设备时,操作系统才回来查询系统控制表。如果之后用户进程再次通过相同的逻辑设备名请求使用设备,则操作系统通过LUT表即可知道用户进程实际要使用的是哪个物理设备了,并且也能知道该设备的驱动程序入口地址。

    逻辑设备表的设置问题:

    设备回收就是将上面的设备分配使用到的数据结构复原。

    设备的分配与回收总结:

    设备的分配与回收总结

    5.3 缓冲区管理

    设备独立性软件需要实现缓冲区管理。

    缓冲区是一个存储区域,可以由专门的硬件寄存器组成,也可利用内存作为缓冲区。

    如果使用硬件作为缓冲区的成本较高,容量也较小,一般仅用在对速度要求非常高的场合(如存储器管理中所用的联想寄存器(快表),由于对页表的访问频率极高,因此使用速度很快的联想寄存器来存放页表项的副本)。

    所以一般情况下,更多的是利用内存作为缓冲区,"设备独立性软件"的缓冲区管理就是要组织管理好这些缓冲区。这也是这一节重点介绍的内容。

    缓冲区作用

    缓冲区作用:

    1. 缓和CPU与I/O设备之间速度不匹配的矛盾

      CPU可以把要输出的数据快速地放入缓冲区,之后就可以做别的事。之后慢速的I/O设备可以慢慢,从缓冲区取走数据。数据输入时类似。

      缓和CPU与IO之间速度矛盾

    2. 减少对CPU的中断频率,放宽对CPU中断相应时间的限制

      如果是字符型设备,则每输出完一个字符就要向CPU发送一次中断信号。对于中断的处理是需要付出一定的时间代价的,因此CPU频繁处理这些中断,显然会降低系统的性能。而如果采用缓冲区策略的话,只有当缓冲区中的数据全部被取走,或者输入的数据充满缓冲区,CPU才需要介入来处理中断。

    3. 解决数据粒度不匹配的问题

      如:输出进程每次可以生成一块数据,但I/O设备每次只能输出一个字符。

      所以如果此时在CPU上运行的输出进程,每次可以生成一整块的数据,但是I/O设备每次只能输出一个字符,如果没有采用缓冲区策略,输出字符只能以一个字符一个字符给这个I/O设备传送数据。但如果采用缓冲区策略,输出进程可以直接把一整块的数据放到缓冲区中,让I/O设备从这个缓冲区中一个字符一个字符往外取走。

    4. 提高CPU与I/O设备之间的并行性

    介绍几种需要掌握的缓冲区策略:

    单缓冲

    假设某用户进程请求某种块设备读入若干块的数据。若采用单缓冲的策略,操作系统会在主存中为其分配一个缓冲区(若题目中没有特别说明,一个缓冲区的大小就是一个块)。

    注意缓冲区特性:当缓冲区数据非空时,不能往缓冲区冲入数据,只能从缓冲区把数据传出。当缓冲区为空时,可以往缓冲区冲入数据,但必须把缓冲区充满以后,才能从缓冲区把数据传出。

    对于数据块的处理需要经过的步骤:

    1. 首先系统在主存中为用户进程分配一块大小的缓冲区。这个块设备会产生一块大小的数据,输入到缓冲区中。这个过程假设所需要的时间为T

      单缓冲步骤

    2. 之后这块数据需要传送到用户进程的工作区中才可以被用户进程处理。具体是用户进程的内存空间中,会分出一片工作区来接受输入/输出数据(一般也默认工作区大小与缓冲区相同)。之后用户进程就可以对这一块数据进行处理。假设对一块数据进行处理所需要的时间为C

      单缓冲步骤2

    3. 当处理完成后,这个用户处理的工作区就可以腾空。

      单缓冲步骤3

    常考题型:计算每处理一块数据平均需要多久?

    技巧:假定一个初始状态,分析下次到达相同状态需要多少时间,这就是处理一块数据平均所需时间。在"单缓冲"题型中,可以假设初始状态为工作区满,缓冲区空。所以要分析下一次到达工作区满,缓冲区空这种状态需要花多长时间。这个时间长度就是处理一个数据块平均所需要消耗的时间。

    单缓冲步骤3

    分为几种情况:

    1. 假设输入时间T>处理时间C

      根据这个条件,刚开始工作区是满的,缓冲区是空的。所以刚开始CPU就可以处理工作区中的数据。这个处理过程需要花C这么长的时间。

      单缓冲分析

      另外由于刚开始缓冲区就是空的,而缓冲区空的时候就可以直接冲入数据。所以刚开始块设备就会往缓冲区中冲入一块数据。这个时间总共耗费T。即将缓冲区充满需要花费T这么长的时间。

      单缓冲分析2

      由于输入时间T>处理时间C,因此CPU在花了C这么长的时间处理完数据后,并不可以紧接着继续处理下一块的数据(因为此时缓冲区数据还没有冲满)。因此必须等到缓冲区数据冲满再送入工作区之后CPU才能进行下一块数据的处理。

      显然在到达T这个时间点时,缓冲区充满,接着可以把缓冲区数据传送到工作区中,这个过程花费M这么长的时间。

      单缓冲分析3

      可以看到再经历了T+M这么长的时候后,就再次回到了假设的初始状态(工作区满,缓冲区空)。接下来的数据处理无非也就是重复刚刚的分析的过程。

      单缓冲分析4

      因此,通过刚刚分析可以知道平均每处理一块数据需要花的时间是T+M这么长。

    2. 假设输入一块数据时间T<处理一块数据的时间

      由于刚开始缓冲区为空,所以在0时刻,块设备就向缓冲区输入一块数据。这个冲满过程需要花费T这么长的时间。

      单缓冲分析5

      同时由于刚开始工作区是满的,所以CPU可以在0时刻就开始处理这一块数据,处理完这一块的数据需要C这么长的时间。而C>T。虽然缓冲区中数据冲满,但是由于工作区数据还没有处理完,所以在T时刻,并不可以把缓冲区数据放入工作区。

      单缓冲分析6

      只有到C时刻,工作区中数据才会处理完。之后缓冲区数据会放入工作区,这个过程需要M时间。

      单缓冲分析7

      到这步又回到刚开始假设的这种初始状态,工作区是满的,缓冲区是空的。所以这个过程耗费了C+M这么长时间。经过M这么长的时间后,缓冲区中的数据全部取空。因此接下来块设备可以接着向缓冲区发送数据。

      单缓冲分析8

      因此,通过刚刚分析可以知道平均每处理一块数据需要花的时间是C+M这么长。

    结论:采用单缓冲策略,处理一块数据平均耗时Max(C,T)+M

    双缓冲

    假设某用户进程请求某种块设备读入若干块的数据。若采用双缓冲的策略,操作系统会在主存中为其分配两个缓冲区(若题目中没有特别说明,一个缓冲区的大小就是一个块)

    类似的双缓冲也会经常考察,每处理一块数据平均需要耗时多就这样一个问题。所以双缓冲题目中,假设初始状态为:工作区空,其中一个缓冲区满,另一个缓冲区空。分析下一次到达这种情况耗时即可。

    分为以下几种情况:

    1. 假设输入时间T>处理一块数据时间C+传送一块数据时间M

      根据假设的初始状态,缓冲区1中有数据,缓冲区2中没有数据。工作区中也没有数据。

      双缓冲步骤

      所以在0这个时刻可以把缓冲区1当中的数据传到工作区当中。这个过程耗时M

      双缓冲步骤2

      接着CPU就可以处理工作区中的数据,耗时C这么长时间。

      双缓冲步骤3

      另一方面在刚开始缓冲区2就是空的,所以刚开始块设备就可以往缓冲区2中输入数据,而冲满这个数据总共耗时T

      双缓冲步骤4

      由于T>C+M,所以虽然在C时间点,CPU已经把工作区中的这块数据给处理完了,工作区已经空了。但是由于缓冲区2此时这个时刻还没有冲满,所以暂时不能把缓冲区2中的数据传送到工作区当中。必须等到T这个时间点缓存2中的数据满了。所以在T这个时刻就回到了初始状态。即工作区是空的,一个缓存区空的,一个是满的。之后就是重复。

      双缓冲步骤5

      因此平均用时是T

    2. 假设输入时间T<处理一块数据时间C+传送一块数据时间M

      刚开始缓冲区2是空的,所以设备可以往缓冲区中放入数据。耗时是T时间。

      双缓冲步骤6

      另一方面,由于刚开始缓冲区1当中是满数据的,所以刚开始就可以把缓冲区数据放入工作区。耗时M。即到M时刻工作区数据可以冲满,CPU就可以处理这块数据,处理过程消耗C时间。

      双缓冲步骤7

      处理完工作区中的数据后,此时缓冲区2当中也已经冲满了下一块的数据,因此接着可以把缓冲区2当中的数据传送到工作区当中。接着继续处理工作区中的这块数据。

      双缓冲步骤8

      再来看数据输入过程,在T时刻缓冲区2已经冲满,之后设备开始空闲,并且由于缓冲区1中的数据在M(1)这个时刻已经取空。因此缓冲区2数据冲满之后,设备就可以紧接着往缓冲区1中冲入数据。这个耗时也是T

      双缓冲步骤9

      假设在第二个T时间点,缓冲区2中的数据还没有完全被取走,所以TM(2)之间有一段设备空闲时间。只又缓冲区2中的数据全部取走后,设备才能接着往缓冲2中放入数据。

      双缓冲步骤10

      可以看到很难找到和刚开始初始状态一模一样的情况。可以看到M+C时候,一个工作区是空的,而缓冲区2满的,但是缓冲区1是半满状态。所以如果T<C+M,这中方法就不太好用了。但是可以根据上面甘特图方式多往下分析几次就会发现,其实每经过M+C这么长的时间,就会又一块数据被处理完毕。

      总之,T<C+M意味着设备输入数据块的速度要比处理机处理数据块的速度更快,所以每处理一个数据块平均耗时是C+M

      结论:采用双缓冲策略,处理一个数据块的平均耗时为Max(T,C+M)

    /双缓冲策略通信的区别

    这两种策略不仅仅是在主机和设备之间数据传送当中可以使用,其实在两台主机通信时,也可以采用这种缓冲的策略,用于数据的发送和接受。

    如果为两台机器配置单缓冲区:

    单双缓冲通信时区别

    A机要发送的数据要先放入A机缓冲区中,等缓冲区满时将数据发出。B机的缓冲区用于接收数据。

    单双缓冲通信时区别2

    之后可以处理这些数据,等这些数据处理完毕,这个缓冲区才会变空。只有等这些数据全部被取走,全部处理完后B主机的缓冲区才能被腾空。而只有主机B缓冲区空时,A主机才能往B主机发送数据。

    显然,若两个相互通信的机器只设置单缓冲区,在任一时刻只能实现数据的单向传输。这种原因是缓冲区特点造成的。

    为了实现同一时刻双向传输,可以给两台机器配置双缓冲区。其中一个缓冲区用来暂存即将发送的数据,另一个缓冲区用于接收输入的数据。

    单双缓冲通信时区别3

    所以采用双缓冲的结构,这两台主机就可以同时往自己的发送缓冲区中冲入自己想要发送的数据。接着可以同时往对方的接受缓冲区中冲入数据。

    单双缓冲通信时区别4

    这就实现同一时刻可以实现双向的数据传输。

    循环缓冲区

    很多时候只有两个缓冲区依然不能满足进程的需要。

    所以操作系统可以给进曾分配多个大小相等的缓冲区,让这些缓冲区形成一个循环队列。

    循环缓冲区

    注:上图中,橙色表示已充满数据的缓冲区,绿色表示空缓冲区。系统用两个指针实现对缓冲区的管理。

    循环缓冲区2

    这里只需要了解大致原理即可。

    缓冲池

    缓冲池由系统中共用的缓冲区组成。这些缓冲区按使用状况可以分为:空缓冲队列、装满输入数据的缓冲队列(输入队列)、装满输出数据的缓冲队列(输出队列)。即当使用一个或者归还一个缓冲区时,就可以对缓冲池当中的各种缓冲区进行操作即可。

    缓冲池

    另外,根据一个缓冲区在实际运算中扮演的功能不同,又设置了四种工作缓冲区:用于收容输入数据的工作缓冲区(hin) 、用于提取输入数据的工作缓冲区(sin) 、用于收容输出数据的工作缓冲区(hout) 、用于提取输出数据的工作缓冲区(sout)。

    缓冲池结构

    执行原理:

    1. 如果一个输入进程要请求输入一块数据

      首先系统会从空缓冲队列的队头当中,取下这一块空的缓冲区把它作为用于收容输入数据的缓冲区。

      缓冲池执行原理

      当这块缓冲区被冲满之后,就会被挂到输入队列的队尾。

      缓冲池执行原理2

    2. 如果计算进程想要取得一块输入数据

      操作系统会从输入队列的队头取下一个缓冲区,把它作为提前输入的工作缓冲区。

      缓冲池执行原理3

      接下来这块缓冲区中的数据会被传送到计算进程的工作区当中。所以这块缓冲区内容就会被取空。

      缓冲池执行原理4

      当取空之后,这个缓冲区会被挂回到空缓冲队列的队尾。

      缓冲池执行原理5

    3. 如果此时计算进程想要将准备好的数据冲入缓冲区

      首先系统会从空缓冲队列的队头当中,取下这一块空的缓冲区把它作为用于收容输出数据的缓冲区。

      缓冲池执行原理6

      之后缓冲区冲满后会被挂到输出队列的队尾。

      缓冲池执行原理7

    4. 如果某个输出进程请求输出数据

      系统会从输出队列的队头取下一块缓冲区,作为提取输入输出数据的工作缓冲区。

      缓冲池执行原理8

      接着这块缓冲区中的数据会被取走,缓冲区取空之后就可以把缓冲区挂到空缓冲区队尾。

      缓冲池执行原理9

    缓冲区管理总结:

    缓冲区管理总结

    6. 磁盘

    磁盘的表面由一些磁性物质组成,可以用这些磁性物质来记录而进制数据。

    磁盘结构

    如果要读取磁盘中的二进制数据,需要由磁头臂来带动磁头移动,然后把磁头移动到相应位置来读取这个位置存储的数据。

    磁盘上的盘片会被分为一圈一圈的磁道。

    磁盘盘片

    个磁道又被划分成一个个扇区,每个扇区就是一个"磁盘块"。各个扇区存放的数据量相同(如1KB)。同时可以给每个扇区编号。

    磁盘盘片扇区

    由于每个扇区中存放的数据量相同。所以最内侧的扇区面积最小,但是存储的数据量和其他扇区相同,因此内侧扇区数据密度最大。

    磁盘读写数据过程如下:

    1. 首先需要把"磁头"移动到想要读/写的扇区所在的磁道上。

      此时磁头会由磁头臂带动。磁头臂的活动范围是一个弧。

    2. 磁盘会转起来,让目标扇区从磁头下面划过,才能完成对扇区的读/写操作。

    实际上,磁盘中会有多个盘面累起来的。相应的每个盘面上都有一个磁头,这些磁头都会由磁头臂移动方向。同时可以给盘面进行编号。

    盘面柱面

    所有的磁头都是连在同一个磁臂上的,因此所有磁头只能"共进退"。另外所有盘面中相对位置相同的磁道组成柱面。

    盘面柱面2

    如上图,所有黄色部分相对位置相同,所以这些磁道组成了一个柱面。

    所以磁盘物理地址可以用三元组(柱面号,盘面号,扇区号)来定位任意一个"磁盘块"。在"文件的物理结构"小节中,我们经常提到文件数据存放在外存中的几号块,这个块号就可以转换成(柱面号,盘面号,扇区号)的地址形式。

    根据该地址读取一个"块"的过程:

    1. 首先根据"柱面号"移动磁臂,让磁头指向指定柱面。
    2. 根据盘面号选择激活指定的盘面对应的磁头。
    3. 磁盘旋转的过程中,指定的扇区会从磁头下面划过,这样就完成了对指定扇区的读/写。

    磁盘分类:

    按照磁头可不可以移动分为:

    如果按照盘片是否可以更换分类:盘片可以更换的称为可换盘磁盘。盘片不可更换的称为固定盘磁盘。

    磁盘结构总结:

    磁盘结构总结

    6.1 磁盘调度算法

    一次磁盘读/写操作需要的时间:

    寻找时间(寻道时间)Ts:在读/写数据前,将磁头移动到指定磁道所花的时间。

    1. 启动磁头臂是需要时间的。假设耗时为s
    2. 移动磁头也是需要时间的。假设磁头匀速移动,每跨越一个磁道耗时为m,总共需要跨越n条磁道。则:寻道时间Ts=s+mn

    延迟时间Tp:通过旋转磁盘,使磁头定位到目标扇区所需要的时间。设磁盘转速为r(单位:转/秒, 或转/分),则平均所需的延迟时间TR=(1/2)(1/r)=1/2r。其中1/r就是转一圈需要的时间。找到目标扇区平均需要转半圈,因此再乘以1/2

    一次读写操作需要的时间

    假设上图磁头要读取橙色部分,则旋转磁盘,当磁头到橙色部分起始位置就是所需要的时间就是延迟时间。

    传输时间Tt:从磁盘读出或向磁盘写人数据所经历的时间,假设磁盘转速为r,此次读/写的字节数为b,每个磁道上的字节数为N。则:传输时间Tt=(1/r)(b/N)=b/(rN)。其中每个磁道要可存N字节的数据,因此b字节的数据需要b/N个磁道才能存储。而读/写一个磁道所需的时间刚好又是转一圈所需要的时间1/r

    可以看到延迟时间和传输时间都与磁盘转速相关,且为线性相关。而转速是硬件的固有属性,因此操作系统也无法优化延迟时间和传输时间。所以操作系统唯一能影响时间是寻道时间。根据不同的调度算法寻道时间各不相同。磁盘调度算法有四种:先来先服务(FCFS)、最短寻找时间优先(SSTF)、扫描算法(SCAN)、循环扫描算法(C-SCAN)。

    先来先服务(FCFS)

    根据进程请求访问磁盘的先后顺序进行调度。

    假设磁头的初始位置是100号磁道,有多个进程先后陆续地请求访问555839189016015038184号磁道。

    按照FCFS的规则,按照请求到达的顺序,磁头需要依次移动到555839189016015038184号磁道。

    所以磁头在100号磁道,首先移动到55号磁道,移动磁头数是(10055=45)

    FCFS

    接下来移动到58号磁道,磁头移动数量是5855=3

    FCFS例子

    之后依次移动,直到移动到184号磁道:

    FCFS例子2

    整个过程分析下来,发现磁头总共移动了45+3+19+21+72+70+10+112+146=498个磁道。响应一个请求平均需要移动498/9=55.3个磁道(平均寻找长度)。

    算法优点:公平,如果请求访问的磁道比较集中的话,算法性能还算过的去。

    缺点:如果有大量进程竞争使用磁盘,请求访问的磁道很分散,则FCFS在性能 上很差,寻道时间长。

    最短寻找时间优先(SSTF)

    SSTF算法会优先处理的磁道是与当前磁头最近的磁道(且还没有处理)。可以保证每次的寻道时间最短,但是并不能保证总的寻道时间最短。(其实就是贪心算法的思想,只是选择眼前最优,但是总体未必最优)

    假设磁头的初始位置是100号磁道,有多个进程先后陆续地请求访问555839189016015038184号磁道。

    首先磁头初始位置是100号磁道,此时离磁头最近且还没有处理的磁道是90,读取完90号磁道后,最近的是58,所以依次访问直到18号磁道。

    SSTF例子

    此时离磁头最近且还没有处理的磁道是150,所以会去处理150号磁道,访问完后会依次处理右边所有磁道。

    SSTF例子2

    磁头总共移动了(10018)+(18418)=248个磁道。响应一个请求平均需要移动248/9=27.5个磁道(平均寻找长度)。

    算法优点是:性能较好,平均寻道时间短。缺点:可能产生"饥饿"现象。

    如:本例中,如果在处理18号磁道的访问请求时又来了一个38号磁道的访问请求,处理38号磁道的访问请求时又来了一个18号磁道的访问请求。如果有源源不断的18号、38号磁道的访问请求到来的话,150160184号磁道的访问请求就永远得不到满足,从而产生"饥饿"现象。

    产生饥饿的原因在于:磁头在一个小区域内来回来去地循环移动。为了解决这个问题提出了扫描算法。

    扫描算法(SCAM)

    由于SSTF算法会产生饥饿的原因在于:磁头有可能在一个小区域内来回来去地移动。为了防止这个问题,可以规定,只有磁头移动到最外侧磁道的时候才能往内移动,移动到最内侧磁道的时候才能往外移动。这就是扫描算法(SCAN) 的思想。由于磁头移动的方式很像电梯,因此也叫电梯算法。

    假设某磁盘的磁道为0200号,磁头的初始位置是100号磁道,且此时磁头正在往磁道号增大的方向移动,有多个进程先后陆续地请求访问555839189016015038184号磁道。

    磁头会先访问150号磁道:

    SCAN例子

    之后会依次往内移动到184号:

    SCAN例子2

    移动之后由于SCAM算法会要求只有磁头移动到最外侧磁道的时候才能往内移动,所以即使200号磁道没有要求访问,也会移动到这里:

    SCAN例子3

    只有到了最边上的磁道才能改变磁头移动方向,所以接下来就会访问最近没有访问的90号磁道,之后依次访问直到18号磁道访问完毕:

    SCAN例子4

    磁头总共移动了(200100)+(20018)=282个磁道。响应一个请求平均需要移动282/9=31.3个磁道(平均寻找长度)。

    算法优点:性能较好,平均寻道时间较短,不会产生饥饿现象。

    缺点:

    1. 只有到达最边上的磁道时才能改变磁头移动方向,事实上,处理了184号磁道的访问请求之后就不需要再往右移动磁头了。
    2. SCAN算法对于各个位置磁道的响应频率不平均(如:假设此时磁头正在往右移动,且刚处理过90号磁道,那么下次处理90号磁道的请求就需要等磁头移动很长一段距离。而响应了184号磁道的请求之后,很快又可以再次响应184号磁道的请求了)

    LOOK算法

    扫描算法(SCAN) 中,只有到达最边上的磁道时才能改变磁头移动方向,事实上,处理了184号磁道的访问请求之后就不需要再往右移动磁头了。LOOK 调度算法就是为了解决这个问题,如果在磁头移动方向上已经没有别的请求,就可以立即改变磁头移动方向。(边移动边观察, 因此叫LOOK)

    假设某磁盘的磁道为0200号,磁头的初始位置是100号磁道,且此时磁头正在往磁道号增大的方向移动,有多个进程先后陆续地请求访问555839189016015038184号磁道。

    磁头会先访问150号磁道:

    SCAN例子

    之后会依次往内移动到184号:

    SCAN例子2

    由于LOOK算法规定如果在磁头移动方向上已经没有别的请求,就可以立即改变磁头移动方向。所以接下来会处理90号磁道。

    LOOK调度算法

    之后依次往后访问直到访问到18号磁道结束

    LOOK调度算法2

    所以磁头总共移动了(184100)+(18418)=250个磁道。响应一个请求平均需要移动250/9=27.5个磁道(平均寻找长度)。

    优点:比起SCAN算法来,不需要每次都移动到最外侧或最内侧才改变磁头方向,使寻道时间进一步缩短。

    循环扫描算法(C-SCAN)

    SCAN算法对于各个位置磁道的响应频率不平均,而C-SCAN算法就是为了解决这个问题。规定只有磁头朝某个特定方向移动时才处理磁道访问请求,而返回时直接快速移动至起始端而不处理任何请求。

    假设某磁盘的磁道为0200号,磁头的初始位置是100号磁道,且此时磁头正在往磁道号增大的方向移动,有多个进程先后陆续地请求访问555839189016015038184号磁道。

    磁头会先访问150号磁道:

    SCAN例子

    之后会依次往内移动到184号:

    SCAN例子2

    算法规定只有到了最边上的磁道才能改变磁头移动方向。。所以会到200号磁道:

    C-SCAN例子

    之后磁头会从200号磁道,直接移动到0号磁道(起始端)。且磁头返回途中不处理任何请求:

    C-SCAN例子1

    此时磁头会开始下一轮扫描,同样磁头向右移动的时候才可以处理请求。所以接下来会访问18号磁道,依次往右扫描直到90号磁道:

    C-SCAN例子2

    此时访问完所有要求磁道。磁头总共移动了(200100)+(2000)+(900)=390个磁道。响应一个请求平均需要移390/9=43.3个磁道(平均寻找长度)。

    优点:比起SCAN来,对于各个位置磁道的响应频率很平均。

    缺点:只有到达最边上的磁道时才能改变磁头移动方向,事实上,处理了184号磁道的访问请求之后就不需要再往右移动磁头了。并且,磁头返回时其实只需要返回到18号磁道即可,不需要返回到最边缘0号磁道。另外,比起SCAN算法来,平均寻道时间更长。

    C-LOOK算法

    C-SCAN算法的主要缺点是只有到达最边上的磁道时才能改变磁头移动方向,并且磁头返回时不一定需要返回到最边缘的磁道上。C-LOOK 算法就是为了解决这个问题。如果磁头移动的方向上已经没有磁道访问请求了,就可以立即让磁头返回,并且磁头只需要返回到有磁道访问请求的位置即可。

    磁头会先访问150号磁道:

    SCAN例子

    之后会依次往内移动到184号:

    SCAN例子2

    如果在磁头移动方向上已经没有别的请求,就可以立即让磁头返回。并且磁头只需要移动到最靠左的需要被访问的磁道,即18号磁道的位置。

    C-LOOK调度算法

    接下来磁头就可以向右移动,依次处理完所有请求,即到90号磁道。

    C-LOOK调度算法2

    磁头总共移动了(184100)+(18418)+(9018)=322个磁道。响应一个请求平均需要移动322/9=35.8个磁道(平均寻找长度)。

    优点:比起C-SCAN算法来,不需要每次都移动到最外侧或最内侧才改变磁头方向,使寻道时间进一步缩短。

    若题目中无特别说明,则SCAN就是LOOK,C-SCAN就是C-LOOK。

    磁盘调度算法总结:

    磁盘调度算法总结

    6.2 减少磁盘延迟时间的方法

    之前学习过,一次磁盘读/写操作需要的时间主要是:寻找时间(寻道时间)、延迟时间、传输时间。

    一次磁盘读写操作需要的时间

    假设要连续读取橙色区域的234扇区:

    一次磁盘读写操作需要的时间2

    首先磁头会读取2号扇区,但在实际过程中磁头读取一块的内容(也就是一个扇区的内容)后,需要一小段时间处理,而盘片又在不停地旋转。即磁头读取2号扇区后不能直接读取3号扇区,而是会进行处理,但是处理过程中磁盘仍然不停旋转,因此3号扇区会错过磁头读取时间。

    所以如果向读取3号扇区,磁盘仍要不停旋转,直到下一次磁头到达3号扇区,才开始读取。

    一次磁盘读写操作需要的时间3

    结论:磁头读入一个扇区数据后需要一小段时间处理,如果逻辑上相邻的扇区在物理上也相邻,则读入几个连续的逻辑扇区,可能需要很长的"延迟时间"。

    解决这个问题可以采用以下方法:

    交替编号法

    若采用交替编号的策略,即让逻辑上相邻的扇区在物理上有一定的间隔,可以使读取连续的逻辑扇区所需要的延迟时间更小。

    交替编号法

    即让逻辑上相邻的扇区,在物理上有一定的间隔,如上图,0号扇区再间隔一个扇区之后才有1号扇区。

    可以看到采用这种方法,当访问两个逻辑上相连的地址时,可以有效地减少延迟时间。

    磁盘地址结构

    为什么磁盘的物理地址是(柱面号,盘面号,扇区号)而不是( 盘面号,柱面号,扇区号)。

    假设某磁盘有8个柱面/磁道(假设最内侧柱面/磁道号为0),4个盘面,8个扇区。则可用3个二进制位表示柱面,2个二进制位表示盘面,3个二进制位表示扇区。

    磁盘地质结构的设计

    所以柱面号在盘面号之前是因为读取地址连续的磁盘块时,采用(柱面号,盘面号,扇区号)的地址结构可以减少磁头移动消耗的时间。

    错位命名法

    假设两个盘面如下:

    错位命名法

    其中,0号盘面橙色区域地址是000,00,1111号盘面橙色区域是000,01,000。此时要访问两个橙色区域内数据,注意,所有盘面都是一起连轴转的。

    方案一:若相邻的盘面相对位置相同处扇区编号相同。

    读取完磁盘块(000,00,111)之后,需要短暂的时间处理,而盘面又在不停地转动,因此当(000,01,000)第一次划过1号盘面的磁头下方时,并不能,读取数据,只能再等该扇区再次划过磁头。

    为了解决这个问题方案二:采用错位命名法, 即将盘面如相同所示堆叠:

    错位命名减少延迟

    由于采用错位命名法,因此读取完磁盘块(000,00,111)之后,还有一段时间处理,当(000,01,000)第一次划过1号盘面的磁头下方时,就可以直接读取数据。从而减少了延迟时间。

    错位命名减少延迟2

    减少延迟时间的方法:

    减少延迟时间的方法

    6.3 磁盘的管理

    磁盘初始化过程:

    1. 进行低级格式化(物理格式化),将磁盘的各个磁道划分为扇区。一个扇区通常可分为头、数据区域(如512B大小)、尾三个部分组成。管理扇区所需要的各种数据结构一般存放在头、尾两个部分,包括扇区校验码(如奇偶校验、CRC循环冗余校验码等,校验码用于校验扇区中的数据是否发生错误)。
    2. 将磁盘分区,每个分区由若干柱面组成(即分为我们熟悉的C盘、D盘、E盘)
    3. 进行逻辑格式化,创建文件系统。包括创建文件系统的根目录、初始化存储空间管理所用的数据结构(如位示图、空闲分区表)

    磁盘初始化

    引导块:

    计算机开机时需要进行系列初始化的工作,这些初始化工作是通过执行初始化程序(自举程序)完成的。

    初始化程序可以放在ROM(只读存储器)中。ROM中的数据在出厂时就写入了,并且以后不能再修改。

    注意:ROM一般是出厂时就集成在主板上的。

    自举程序放在ROM会产生一个问题:万一需要更新自举程序,将会很不方便,因为ROM中的数据无法更改。如何解决呢?

    实际上ROM中只存放很小的"自举装入程序"。而完整的自举程序放在磁盘的启动块(即引导块/启动分区)上,启动块位于磁盘的固定位置。

    所以在开始时计算机先运行"自举装入程序",通过执行该程序就可找到引导块,并将完整的"自举程序"读入内存,完成初始化。一般来说拥有启动分区的磁盘称为启动磁盘或系统磁盘(如:C盘)。

    坏块的管理:

    即坏了、无法正常使用的扇区就是"坏块"。这属于硬件故障,操作系统是无法修复的。应该将坏块标记出来,以免错误地使用到它。

    对于简单的磁盘,可以在逻辑格式化时(建立文件系统时)对整个磁盘进行坏块检查,标明哪些扇区是坏扇区,比如:在FAT表上标明。( 在这种方式中,坏块对操作系统不透明)。

    而对于复杂的磁盘,磁盘控制器(磁盘设备内部的一个硬件部件)会维护一个坏块链表。在磁盘出厂前进行低级格式化(物理格式化)时就将坏块链进行初始化。

    磁盘的管理总结:

    磁盘的管理总结

    7. 固态硬盘SSD

    固态硬盘存储技术是基于闪存技术(Flash Memory),属于电可擦除ROM,即EEPROM。

    操作系统固态硬盘SSD

    SSD组成有两部分:

    SSD读写性能:是以页为单位进行读写的。以块为单位进行擦除,擦干净的块,其中的每页都能写一次,被读无限次。且支持随机访问,系统给定一个逻辑地址,闪存翻译层可以通过电路迅速定位到对应的物理地址。

    如果要写的页有数据,则不能写入,需要将有数据的块内其他页全部复制到一个新的(擦除过的)块中,再在新的块中写入想要写入的数据,最后将原来的块内所有页擦除。此时地址会放生改变,但闪存翻译层会修正写入块的地址。这就导致了SSD读的速度比写的速度快很多。

    固态硬盘相较于机械硬盘特点:

    由于上面的SSD一个块擦除次数过多可能会坏掉,所以现代SSD都会使用磨损均衡技术。

    磨损均衡技术思想:将"擦除"平均分布在各个块上,以提升使用寿命。原理还是闪存翻译层有逻辑映射的功能,所以其真实物理地址会变。这种磨损技术分为两种:

    1. 动态磨损均衡:写入数据时,优先选择累计擦除次数少的新闪存块。
    2. 静态磨损均衡:SSD监测并自动进行数据分配、迁移,让老旧的闪存块承担以读为主的储存任务,让较新的闪存块承担更多的写任务。如:假设电脑中存储一部电影,对于电影来说读的次数更多基本上不会进行修改。所以SSD会监测到,并将电影放入擦除次数多的块中。

    SSD总结:

    SSD总结

    例题:某固态硬盘采用磨损均衡技术,大小为240B=1TB, 闪存块的擦写寿命只有210=1K次。平均每天会对该固态硬盘写237B=128GB数据。在最理想的情况下,这个固态硬盘可以用多久?

    SSD采用磨损均衡技术,最理想情况下,SSD中每个块被擦除的次数都是完全均衡的。1TB128GB=8,所以平均每8天,每个闪存块需要擦除一次。而每个闪存块可以被擦除1K次,因此,经过8K天,约23年后,该固态硬盘会坏。