𝓝𝓮𝓶𝓸&博客

【操作系统】常用总结

基础知识

  • \(1 Byte = 8 bit\)
  • \(1 KB = 2^{10} B ≈ 10^3B\)
  • \(1 MB = 2^{20} B ≈ 10^6B\)
  • \(1 GB = 2^{30} B ≈ 10^9B\)

操作系统

操作系统(Operation System, OS)是管理和控制计算机硬件与软件资源的计算机程序,是直接运行在“裸机”上的最基本的系统软件,任何其他软件都必须在操作系统的支持下才能运行。

  • 操作系统是计算机系统资源的管理者,分为:

    • 处理机管理(CPU)(控制器、运算器)
    • 存储器管理(内存)(存储器)
    • 文件管理(外存)(存储器)
    • 设备管理(I/O)(输入输出设备)
  • 操作系统是用户计算机硬件系统之间的接口,同时也是计算机硬件其他软件的接口,分为:

    • 命令接口
    • 程序接口

操作系统控制硬件的功能,都是通过一些小的函数集合体的形式来提供的。这些函数以及调用函数的行为称为系统调用,也就是通过应用进而调用操作系统的意思。ru在程序中用到了 time() 以及 printf() 函数,这些函数内部也封装了系统调用。

C 语言等高级编程语言并不依存于特定的操作系统,这是因为人们希望不管是Windows 操作系统还是 Linux 操作系统都能够使用相同的源代码。因此,高级编程语言的机制就是,使用独自的函数名,然后在编译的时候将其转换为系统调用的方式(也有可能是多个系统调用的组合)。
也就是说,高级语言编写的应用在编译后,就转换成了利用系统调用的本地代码(操作系统的底层命令,如C语言),再通过操作系统的底层命令去操作底层硬件。

理解:我们常说的解释器和编译器其实就是横架在操作系统和软件之间的沟通桥梁,软件代码通过解释器和编译器翻译为操作系统可以理解的底层代码语言,再进行运行。
所以我们的python解释器就需要下载不同的操作系统版本才能进行使用。并且正因为Python是开源的,所以很多操作系统都会自行去适配python解释器。


其实语言也只是一种规范而已,就拿图形接口举例:
常见的图形接口包括OpenGL、DirectX、Vulkan,其他的还有苹果的Metal、AMD的Mantle、英特尔的One等。图形接口,举个例子,市面上有大量不同型号的显卡,每个显得操作指令有可能都不一样,这个时候,如果有一款游戏需要创建一个2D或者3D的图形,如何适配不同的显卡将成为一个大问题。因此游戏和显卡之间的交互需要一个规范,而OpenGL、DirectX、Vulkan就是一种规范,有了这个规范,游戏开发者只需要调用这些规范库里定义好的接口就行,不需要关注底层是哪种显卡。

我们说OpenGL、DirectX、Vulkan是一种规范,意味着它们并没有提供真正的实现,真正功能的实现,则是在显卡厂商提供给你的驱动程序中。用程序员的话来比喻则是:OpenGL、DirectX、Vulkan相当于一个定义了一些接口,而显卡厂商提供驱动程序是对这些接口的实现,因此不同的显卡,才需要不同的驱动程序。


  • 功能:

    • 管理计算机系统的硬件、软件及数据资源;
    • 控制程序运行;
    • 改善人机界面;
    • 为其他应用软件提供支持,让计算机系统所有资源最大限度地发挥作用;
    • 提供各种形式的用户界面,使用户有一个好的工作环境;
    • 为其他软件的开发提供必要的服务和相应的接口等。
  • 特征:

    • 并发:两个或者多个事件在同一时间间隔内发生;
    • 共享:系统中的资源可供内存中多个并发执行的进程共同使用;
    • 虚拟:把一个物理上的实体变为若干个逻辑上的对应物;
    • 异步:在多道程序环境下,允许多个程序并发执行,但因资源有限,进程的执行不是一贯到底,而是走走停停,以不可预知的速度向前推送,这就是进程的异步性。

基本概念

  • 互斥:进程之间访问临界资源时相互排斥的现象;

    临界资源:一次仅允许一个进程使用的资源,如 打印机。
    临界区:每个进程中访问临界资源的那段代码。

  • 并发:同一时间段有几个程序都处于已经启动到运行完毕之间,并且这几个程序都在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。并发的两种关系是同步和互斥;
  • 并行:单处理器中进程被交替执行,表现出一种并发的外部特征;在多处理器中,进程可以交替执行,还能重叠执行,实现并行处理,并行就是同时发生的多个并发事件,具有并发的含义,但并发不一定是并行,也就是说事件之间不一定要同一时刻发生;
  • 同步:进程之间存在依赖关系,一个进程结束的输出作为另一个进程的输入。具有同步关系的一组并发进程之间发送的信息称为消息或者事件;
  • 异步:和同步相对,同步是顺序执行,而异步是彼此独立,在等待某个事件的过程中继续做自己的事,不要等待这一事件完成后再工作

    线程是实现异步的一个方式,异步是让调用方法的主线程不需要同步等待另一个线程的完成,从而让主线程干其他事情。

  • 多线程:多线程是进程中并发运行的一段代码,能够实现线程之间的切换执行;

    异步和多线程:不是同等关系,异步是目的,多线程只是实现异步的一个手段,实现异步可以采用多线程技术或者交给其他进程来处理。

发展历程

  • 手工阶段
  • 单道批处理系统
  • 多道批处理系统
  • 分时操作系统
  • 实时操作系统
  • 网络操作系统和分布式操作系统

    网络操作系统和分布式操作系统的不同之处在于:
    在分布式操作系统中,若干台计算机相互协同完成同一任务;
    而在网络操作系统中,每台计算机都是相互独立的,它们并不能相互协同完成同一任务。

CPU的工作状态

大多数计算机系统将CPU执行状态分为目态管态
管态就是 supervisor(管理者) mode 翻译来的。
那么目态呢,其实是 object(目标) mode 翻译来的。

  • 管态:****supervisor(管理者) mode 又叫特权态、系统态或者核心态。CPU在管态下可以执行指令系统的全集。

    如果程序处于管态,则该程序就可以访问计算机的任何资源,即 它的资源访问权限不受限制。

    通常,操作系统在管态下运行。

  • 目态:****object(目标) mode又叫常态或用户态。机器处于目态时,程序只能执行非特权指令,不能直接使用系统资源,也不能改变CPU的工作状态,并且只能访问这个用户程序自己的存储空间。

    科普:为什么叫 object mode 呢?
    通常CPU执行两种不同性质的程序:一种是操作系统内核程序;另一种是用户自编程序或系统外层的应用程序
    对操作系统而言,这两种程序的作用不同,前者是后者的管理者,因此“管理程序”要执行一些特权指令,而“被管理程序”出于安全考虑不能执行这些指令。
    因为管理者需要管理它,它就是管理者的管理目标。所以就叫 object mode

  • 目态(用户态)→管态(核心态)

    • 系统调用:这是用户态进程主动要求切换到核心态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作。
      系统调用机制的核心是使用了操作系统为用户开放的一个中断来实现。
    • 异常:当 CPU 在执行用户态程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了核心态,如 缺页异常。
    • I/O设备的中断:当 I/O 设备完成用户请求操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令,转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到核心态的切换。

      例如,硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中,执行后续的操作。

其中,系统调用可以认为是用户进程主动发起的,异常外部设备中断则是被动的。


内核空间和用户空间

我们电脑上跑着的应用程序,其实是需要经过操作系统,才能做一些特殊操作,如磁盘文件读写、内存的读写等等。因为这些都是比较危险的操作,不可以由应用程序乱来,只能交给底层操作系统来。

因此,操作系统为每个进程都分配了内存空间,一部分是用户空间,一部分是内核空间。内核空间是操作系统内核访问的区域,是受保护的内存空间,而用户空间是用户应用程序访问的内存区域。 以32位操作系统为例,它会为每一个进程都分配了4G(2的32次方)的内存空间。

  • 内核空间:主要提供进程调度、内存分配、连接硬件资源等功能
  • 用户空间:提供给各个程序进程的空间,它不具有访问内核空间资源的权限,如果应用程序需要使用到内核空间的资源,则需要通过系统调用来完成。进程从用户空间切换到内核空间,完成相关操作后,再从内核空间切换回用户空间。

什么是用户态、内核态

CPU的上下文执行状态:

  • 如果进程运行于内核空间,被称为进程的内核态
  • 如果进程运行于用户空间,被称为进程的用户态

思考:为什么需要内核态?
用户需要从用户态经过内核态才能对系统进行操作,这本质上其实是一种操作系统的权限控制
内核态会对你当前用户的权限进行检验,对一些危险操作进行禁止。

什么是上下文切换

什么是上下文?
CPU 寄存器,是CPU内置的容量小、但速度极快的内存。而程序计数器,则是用来存储 CPU 正在执行的指令位置、或者即将执行的下一条指令位置。它们都是 CPU 在运行任何任务前,必须的依赖环境,因此叫做CPU上下文。


什么是CPU上下文切换?
它是指,先把前一个任务的CPU上下文(也就是CPU寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。


一般我们说的上下文切换,就是指内核(操作系统的核心)在CPU上对进程或者线程进行切换。进程从用户态到内核态的转变,需要通过系统调用来完成。系统调用的过程,会发生CPU上下文的切换。

CPU 寄存器里原来用户态的指令位置,需要先保存起来。接着,为了执行内核态代码,CPU 寄存器需要更新为内核态指令的新位置。最后才是跳转到内核态运行内核任务。

image

理解:其实就是程序员使用编程语言,编程语言(用户态)使用操作系统内核(内核态),操作系统内核来操作计算机底层硬件。

虚拟内存

现代操作系统使用虚拟内存,即虚拟地址取代物理地址,使用虚拟内存可以有2个好处:

  • 虚拟内存空间可以远远大于物理内存空间
  • 多个虚拟内存可以指向同一个物理地址

正是多个虚拟内存可以指向同一个物理地址,可以把内核空间和用户空间的虚拟地址映射到同一个物理地址,这样的话,就可以减少IO的数据拷贝次数啦,示意图如下
image

DMA技术

DMA,英文全称是Direct Memory Access,即直接内存访问。DMA本质上是一块主板上独立的芯片,允许外设设备和内存存储器之间直接进行IO数据传输,其过程不需要CPU的参与
我们一起来看下IO流程,DMA帮忙做了什么事情。
image

  • 用户应用进程调用read函数,向操作系统发起IO调用,进入阻塞状态,等待数据返回。
  • CPU收到指令后,对DMA控制器发起指令调度。
  • DMA收到IO请求后,将请求发送给磁盘;
  • 磁盘将数据放入磁盘控制缓冲区,并通知DMA
  • DMA将数据从磁盘控制器缓冲区拷贝到内核缓冲区。
  • DMA向CPU发出数据读完的信号,把工作交换给CPU,由CPU负责将数据从内核缓冲区拷贝到用户缓冲区。
  • 用户应用进程由内核态切换回用户态,解除阻塞状态

可以发现,DMA做的事情很清晰啦,它主要就是帮忙CPU转发一下IO请求,以及拷贝数据。为什么需要它的?

主要就是效率,它帮忙CPU做事情,这时候,CPU就可以闲下来去做别的事情,提高了CPU的利用效率。大白话解释就是,CPU老哥太忙太累啦,所以他找了个小弟(名叫DMA) ,替他完成一部分的拷贝工作,这样CPU老哥就能着手去做其他事情。

传统 IO 的执行流程

做服务端开发的小伙伴,文件下载功能应该实现过不少了吧。如果你实现的是一个web程序,前端请求过来,服务端的任务就是:将服务端主机磁盘中的文件从已连接的socket发出去。关键实现代码如下:

while((n = read(diskfd, buf, BUF_SIZE)) > 0)
    write(sockfd, buf , n);

传统的IO流程,包括read和write的过程。

  • read:把数据从磁盘读取到内核缓冲区,再拷贝到用户缓冲区
  • write:先把数据写入到socket缓冲区,最后写入网卡设备。

流程图如下:
image

  • 用户应用进程调用read函数,向操作系统发起IO调用,上下文从用户态转为内核态(切换1)
  • DMA控制器把数据从磁盘中,读取到内核缓冲区。
  • CPU把内核缓冲区数据,拷贝到用户应用缓冲区,上下文从内核态转为用户态(切换2),read函数返回
  • 用户应用进程通过write函数,发起IO调用,上下文从用户态转为内核态(切换3)
  • CPU将应用缓冲区中的数据,拷贝到socket缓冲区
  • DMA控制器把数据从socket缓冲区,拷贝到网卡设备,上下文从内核态切换回用户态(切换4),write函数返回

从流程图可以看出,传统IO的读写流程,包括了4次上下文切换(4次用户态和内核态的切换),4次数据拷贝(两次CPU拷贝以及两次的DMA拷贝)

管理功能

  • 处理机管理:
    • 进程控制:在传统多道程序环境中,要是作业运行,必须先为它创建一个或多个进程,并为之分配必要的资源。当进程运行结束后,立即撤销该进程,以便能及时回收该进程所占用的各类资源。
    • 进程同步:为多个进程(含线程)的运行进行协调。
      • 进程互斥方式:进程(线程)在对临界资源进行访问时,应采用互斥方式。
      • 进程同步方式:在相互合作去完成共同任务的诸进程(线程)间,由同步机构对它们的执行次序加以协调。
    • 进程通信:在多道程序环境下,为了加速应用进程的运行,应在系统中建立多个进程,并且再为一个进程建立若干个线程,由这些进程(线程)相互合作去完成一个共同的任务,而在这些进程(线程)之间又往往需要交换信息。
    • 调度:在后备队列上等待的每个作业或者进程,通常都需要调度才能执行,调度的任务,即 将处理机分配给它。
  • 存储器管理:
    • 内存分配:采用静态和动态两种方式实现内存分配数据结构,以记录内存使用情况,按照一定算法分配,对不再需要的内存进行回收。
    • 内存保护:确保每道用户程序都只在自己的内存空间运行,彼此互不干扰。
    • 地址映射:编译后的程序的地址分为逻辑地址和物理地址,多道程序环境中,每道程序不可能都从 “0” 地址开始,要保证程序运行,则须将逻辑地址转换成内存空间中的物理地址。

      动态重定位:在程序执行过程中,每当访问指令或数据时,将要访问的程序或数据的逻辑地址转换成物理地址

      实现方法:在系统中增加一个重定位寄存器用来装入程序在内存中的起始地址。程序执行时,真正访问的内存地址是相对地址与重定向寄存器中的地址相加之和,从而实现动态重定位。

    • 内存扩充:从逻辑上去扩充内存容量,使用户所感受到的内存容量比实际容量大得多,或者让更多的程序能并发执行。
  • 设备管理:
    • 缓冲管理:缓冲区机制能够有效缓解 CPU 运行的高速性和 I/O 低速性的矛盾。
    • 设备分配:设置设备控制表、控制器控制表等数据结构,能够了解指定设备当前是否可用,是否忙碌,以及该设备被分配出去,系统是否还安全。
    • 设备处理程序:实现 CPU 和设备管理器之间的通信,由 CPU 向设备控制器发出 I/O 命令,要求它完成指定的 I/O 操作,反之由 CPU 接收从控制器发来的中断请求,并给予迅速的响应和相应的处理。
  • 文件管理:
    • 文件存储空间的管理:由文件系统对诸多文件及文件的存储空间实施统一的管理,对每个文件分配必要的外存空间,提高外存的利用率和文件系统的执行速度。
    • 目录管理:相当于文件的索引,建立目录项(文件名、文件属性、文件在磁盘中的物理位置等),方便用户查询检索。
    • 文件的读/写管理和保护:防止未经批准的用户存取文件、防止冒名顶替存取文件、防止以不正确的方式使用文件。

处理机管理(CPU)

进程与线程

对于操作系统来说,一个任务就是一个进程(Process),比如打开一个浏览器就是启动一个浏览器进程,打开一个记事本就启动了一个记事本进程,打开两个记事本就启动了两个记事本进程,打开一个 Word 就启动了一个 Word 进程。
有些进程还不止同时干一件事,比如Word,它可以同时进行打字、拼写检查、打印等事情。在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程(Thread)

类比:
进程 = 工厂
线程 = 工厂里各个流水线

注意:一个进程可能对应多个端口号,一个端口对应一个进程。

进程

进程可以认为是程序执行时的一个实例。进程是系统进行资源分配的独立实体,且每个进程拥有独立的地址空间。(即 资源的分配和调度的一个独立单元
进程控制块(Process Control Block, PCB):保存运行期间进程的数据,PCB进程存在的唯一标志

  • 进程 = 程序 + 数据 + PCB
  • 一个进程无法直接访问另一个进程的变量和数据结构,如果希望让一个进程访问另一个进程的资源,需要使用进程间通信,比如:管道、文件、套接字等。

进程的五种基本状态及其转换:

  • 创建状态:进程正在被创建,尚未转到就绪状态,创建进程需要申请一个空白的 PCB,并向 PCB 写一些控制和管理进程的信息,然后由系统分配资源,将进程转入就绪状态。
  • 就绪状态:进程已处于准备执行的状态,获得了除处理机以外的一切所需资源。
  • 执行状态:进程在处理机上运行。在单处理机环境下,每一时刻最多只有一个进程运行。
  • 阻塞状态:进程正在等待某一事件(服务请求)而暂停运行,如 等待某资源变为可用(不包括处理机)或等待输入输出 I/O 完成,即使处理机空闲,该进程也不能运行。
  • 结束状态:进程正从系统中消失,这可能是进程正常结束或其他原因中断退出运行,当进程需要结束运行时,系统首先必须置该进程为结束状态,然后再进一步处理资源释放和回收。

注意:****后备队列外存中,而就绪队列内存中。

进程同步与互斥

PV 操作是一种实现进程互斥与同步的有效方法。PV操作与信号量的处理相关,P 表示通过(荷兰语 passeren)的意思,V 表示释放(荷兰语 vrijgeven)的意思。

具体来源可以查看PV操作的来源

互斥与同步:

  • 互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性排它性。但互斥无法限制访问者对资源的访问顺序,即 访问是无序的
  • 同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。

在操作系统中,信号量S是一整数。
S≥0 时,S 表示可供并发进程使用的资源实体数
S<0 时,S 表示正在等待使用资源实体的进程数
建立一个信号量必须说明此信号量所代表的意义并且赋初值。
除赋初值外,信号量仅能通过PV操作来访问。

  • 信号量 S(semaphore) 代表“资源数”

  • P 操作的主要动作是:通过(荷兰语 passeren)(即 让进程使用资源

    • S 减 1;

      类比:“占用了一个资源”

    • 若相减结果仍大于或等于 0,则进程继续执行;

      类比:“若占用一个资源后,还有多余的资源或者刚好用完资源,那么就代表该进程有资源可以利用,进程也就可以继续执行

    • 若相减结果小于 0,则该进程被阻塞(挂起),之后放入等待该信号量的等待队列中,然后转入进程调度。

      类比:“若占用一个资源后,还欠别人资源,那么就代表该进程根本就没有资源可以用了,所以先欠着,挂个号,等待

  • V 操作的主要动作是:释放(荷兰语 vrijgeven)(即 让进程释放资源

    • S 加 1;

      类比:“资源占用完了,物归原主,释放资源”

    • 若相加结果大于 0,则进程继续执行;

      类比:“若释放资源后,资源数大于 0,就代表库存里的资源充裕,奉还资源后,还有资源可以给你利用,那就继续占用资源,继续执行

    • 若相加结果小于或等于 0,则从该信号的等待队列中释放一个等待进程(唤醒等待→就绪),然后再返回原进程继续执行 或转入进程调度。

      类比:“若一个进程结束,释放资源后,资源数还是欠别人的或者为 0,就代表库存里的资源很紧张资源刚一释放就被其他进程一抢而空,所以自己就不能用了,得先来后到把资源给下一个进程用,让下一个进程就绪
      如果执行不需要此资源,那么等自己执行完后(有的执行并不一定需要此资源)把处理机让给下一个进程用;
      如果执行需要此资源,那么转入进程调度,重新排队,等等再执行,把处理机让给下一个进程用,让下一个进程执行。”

注意:PV 操作对于每一个进程来说,都只能进行一次,而且必须成对使用

代码化如下:
P 操作:

P(S) {
	S--;
	if(S < 0) {
		保留调用进程CPU现场;
		将该进程的PCB插入S的等待队列;
		置该进程为“等待”状态;
		转入进程调度;
	}
}

V 操作:

V(S) {
	S++;
	if(S <= 0) {
		移出S等待队列首元素;
		将该进程的PCB插入就绪队列;
		置该进程为“就绪”状态;
	}
}

进程通信

根据交换信息量的多少和效率的高低,进程通信分为如下低级通信和高级通信。

  • 低级通信:只能传递状态和整数值(控制信息)。(如 同步互斥工具:PV 操作)

    由于进程的互斥和同步,需要在进程间交换一定的信息,故不少学者将它们也归为进程通信。

    • 特点:传送信息量小,效率低,每次通信传递的信息量固定,若传递较多信息则需要进行多次通信。
    • 编程复杂:用户直接实现通信的细节,容易出错。
  • 高级通信:提高信号通信的效率,传递大量数据,减轻程序编制的复杂度。
    提供三种方式:
    • 消息传递模式
    • 共享内存模式:内存
    • 共享文件模式:外存
消息传递模式

在消息传递模式中,进程间的数据交换是以格式化的消息(Message)为单位的。
进程通过系统提供的发送消息接收消息两个原语进行数据交换。

若通信进程之间不存在可直接访问的共享空间,则必须利用操作系统提供的信息传递方法实现进程通信。

可分为直接和间接两种通信方式:

  • 直接:将消息发送给接收进程,并将它挂在接收进程的信息缓冲队列中,接收进程从消息缓冲队列中取得消息。
  • 间接:将消息发送给某个中间实体(信箱),接受进程从中间实体中取得消息,又称为信箱通信方式。

    类比:
    甲给乙写信
    直接:甲直接把信交给乙
    间接:甲通过邮差把信交给乙

共享内存模式(内存)

全双工

在通信进程之间存在一块可直接访问的共享内存空间,通过对这片共享空间进行写/读操作,实现进程之间的信息交换。

在对共享空间进行写/读操作时,需要同步互斥工具(如 P操作、V操作),对共享空间的写/读进行控制。

类比:
进程 = 物品
共享空间 = 钱
用钱进行交换,而不用物物交换

共享文件模式(外存)

半双工

共享文件:用于连接一个发送进程和一个接收进程,以实现它们之间通信的文件,就是共享文件,又名 pipe(管道)文件
向管道提供输入的发送进程,以字节流形式将大量的数据送入管道;
而接收管道输出的接收进程,则从管道中接收数据。

为了协调双方的通信,管道机制必须提供互斥、同步和确定对方存在三方面的协调能力。

共享内存模式与共享文件模式的区别

个人理解:一个共享的是内存,一个共享的是外存磁盘上的文件,这就是差别,共享内存会更快。

共享内存和消息队列,FIFO,管道传递消息的区别:

消息队列,FIFO,管道的消息传递方式一般为:

  1. 服务器得到输入
  2. 通过管道,消息队列写入数据,通常需要从进程拷贝到内核。
  3. 客户从内核拷贝到进程
  4. 然后再从进程中拷贝到输出文件

上述过程通常要经过4次拷贝,才能完成文件的传递。

共享内存只需要:

  1. 从输入文件到共享内存区
  2. 从共享内存区输出到文件

上述过程不涉及到内核的拷贝,所以花的时间较少,共享内存最快。

参考:https://blog.csdn.net/m0_37806112/article/details/81671429

线程

对线程最基本的理解就是“轻量级进程”,它是一个基本的 CPU 执行单元,也是程序执行流的最小单元,由线程 ID、程序计数器、寄存器集合和堆栈组成。(即 CPU 调度的基本单元
线程控制块(Thread Control Block, TCB):保存运行期间线程的数据,TCB线程存在的唯一标志

  • 线程属于进程,是进程的一个实体,是被系统独立和分配的基本单位。
  • 线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可以与同属一个进程的其他线程共享进程所拥有的全部资源。
  • 一个进程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行。

区别

  • 进程是资源分配和调度的一个独立单元;
    线程是 CPU 调度的基本单元。
  • 同一个进程中可以包括多个线程,并且线程共享整个进程的资源(寄存器、堆栈、上下文),一个进程至少包括一个线程。
  • 进程的创建调用 fork 或者 vfork,而线程的创建调用 pthread_create;
    进程结束后它拥有的所有线程都将销毁,而线程的结束不会影响同个进程中的其他线程的结束。
  • 线程是轻量级的进程,它的创建和销毁所需要的时间比进程小很多,所有操作系统中的执行功能都是创建线程去完成的。
  • 线程中执行时一半都要进行同步和互斥,因为它们共享同一进程的所有资源。
  • 线程有自己的私有属性 TCB、线程 id、寄存器、硬件上下文;
    进程也有自己的私有属性进程控制块 PCB,
    这些私有属性是不被共享的,用来表示一个进程或一个线程的标志。

死锁

死锁是指多个进程因竞争临界资源而造成的一种僵局(互相等待),若无外力作用,这些进程都无法向前推进。

  • 产生死锁的根本原因:系统能够提供的资源个数比要求该资源的进程数要少。

  • 产生死锁的基本原因

    • 资源竞争

      例子:
      A 有纸,B 有笔
      A:你不给我笔,我就不给你纸,我就写不了作业
      B:你不给我纸,我就不给你笔,我就写不了作业
      彼此僵持不下。。

    • 进程推进顺序非法

      例子:
      A 要前进 2 步,到桌子前拿东西,再后退 2 步;
      结果顺序非法:
      A 先后退,就永远到不了桌子前,触发不了,死锁。

  • 产生死锁的必要条件:(互斥、不剥夺、占有、环路)

    • 互斥条件:涉及的资源是非共享的,即 一次只有一个进程使用。如果有另一个进程申请该资源,那么申请进程必须等待,直到该资源被释放。

    互斥:你有我没有,我有你没有

    • 占有并等待(部分分配):进程每次申请它所需要的一部分资源。在等待一新资源的同时,进程继续占用已分配到的资源。

    占有:这个资源我马上要用的,拿着等一会

    • 不剥夺条件(非抢占):进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即 只能由获得该资源的进程自己来释放。

    不剥夺:你不能强行抢我的

    • 环路条件(循环等待):存在一种进程循环链,链中每一个进程已获得的资源同时被链中下一个资源所请求。

    环路:你不给我,我不给你

    注意:这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立;
    反之,只要上述条件之一不满足,就不会发生死锁。

处理策略

预防死锁

预防死锁:通过设置一些限制条件,破坏死锁的一些必要条件,让死锁无法发生。

下面我们来举个例子吧!
大家都知道,在并发情况下对两个账户进行转账操作可能会产生死锁,可能出现死锁的原因是,并发情况下对两个账户的操作无法保证其执行顺序。

1.并发问题描述
假如现在执行下面的操作:

线程一执行的是:【账户A】给【账户B】转账

线程二执行的是:【账户B】给【账户A】转账

如果两个转账动作同时执行,则会出现线程一会请求对【账户B】进行加锁,线程二会请求对【账户A】进行加锁

由于此时的【账户A】已由线程一进行锁定,【账户B】已由线程二进行锁定 此时就会产生死锁问题。接下来分析一下产生死锁的原因,以及如何避免死锁。


2.如何避免死锁
有个叫 Coffman 的牛人总结过一条经验,只有当以下四个条件同时发生,才会出现死锁,所以只要打破其中一个条件,就可以避免死锁:

互斥,共享资源 X 和 Y 只能被一个线程占用
占有且等待,线程 A 获取到资源 X,在等待资源 Y 的时候,不释放资源 X
不可抢占,其他线程不能强行抢占线程 A 占有的资源
循环等待,线程 A 等待线程 B 占有的资源,线程 B 等待线程 A 的资源
首先,互斥这个条件是没法破坏的,因为锁存在的目的就是互斥,对于剩下的三个条件都可破坏。

破坏占有且等待

你想要画画,笔和纸不分批次给你,而是同时给你笔和纸。

对于占有且等待,可以同时获取要使用的多个资源锁X和Y,这样就不会存在取得了X还要等待Y。这种方式只在需要获取的资源锁较少的情况下使用,如果要获取的资源锁很多(例如10个),就不太可行。代码实现时,我们通过增加一个 Allocator 账号管理员对象,并且将其设置为单例,每次进行转账的时候,我们都先通过 Allocator 分配账号,如果分配账号成功,则进行转账,如果失败则重新获取,可以设置一个失败次数或是超时时间,达到失败次数或超时时间则转账失败。如下是代码实现,

package com.zhouj.endless.diy;
 
import java.util.ArrayList;
import java.util.List;
 
/**
 * @author Nemo
 * @date 2020-02-19 16:31
 * @description 账户分配
 */
public class Allocator {
 
    private static class InstanceHolder {
        static Allocator instance = new Allocator();
    }
 
    public static Allocator getInstance() {
        return InstanceHolder.instance;
    }
 
    /**
     * 上锁的账户列表
     */
    private List<Account> lockAccountList = new ArrayList<>();
 
    /**
     * 申请分配账户
     *
     * @param from 从这个账户转钱
     * @param to   转钱到这个账户
     */
    public synchronized void apply(Account from, Account to) {
        while (lockAccountList.contains(from) || lockAccountList.contains(to)) {
            // 如果两个账户中,只要有一个账户上锁了,则申请失败,进入循环等待
            try {
                // 阻塞当前线程,等待通知
                this.wait();
            } catch (InterruptedException e) {
            }
        }
        lockAccountList.add(from);
        lockAccountList.add(to);
    }
 
    /**
     * 释放账户锁
     *
     * @param from 从这个账户转钱
     * @param to   转钱到这个账户
     */
    public synchronized void free(Account from, Account to) {
        lockAccountList.remove(from);
        lockAccountList.remove(to);
        // 通知所有线程,让其再次获取锁
        this.notifyAll();
    }
 
    private Allocator() {
    }
}
破坏不可抢占条件

笔和纸分批次给你,你拿着其中一个东西,等待了一段时间获取不到,那就握不住的沙不如扬了它,释放掉,给其他人用。

对于不可抢占,可以获取了部分资源,再进一步获取其他资源时如果获取不到时,把已经获取的资源一起释放掉。破坏不可抢占条件,需要线程在获取不到锁的情况下主动释放它拥有的资源。当我们使用synchronized的时候,线程是没有办法主动释放它占有的资源的。因为,synchronized在申请不到资源时,会使线程直接进入阻塞状态,而线程进入了阻塞状态就不能主动释放占有的资源。java.util.concurrent中的Lock接口,提供了如下三种设计思想都可以解决死锁的不可抢占条件:

  1. 能够响应中断:线程处于阻塞状态时可以接收中断信号。我们便可以给阻塞的线程发送中断信号,唤醒线程,线程便有机会释放它曾经拥有的锁。这样便可破坏不可抢占条件。
  2. 支持超时:如果线程在一段时间之内没有获取到锁,不是进入阻塞状态,而是返回一个错误,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。
  3. 非阻塞地获取锁:如果尝试获取锁失败,并不进入阻塞状态,而是直接返回,那这个线程也有机会释放曾经持有的锁。这样也可以破坏不可抢占条件。

对应方法

// 支持中断的 API
void lockInterruptibly() throws InterruptedException;
 
// 支持超时的 API
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
 
// 支持非阻塞获取锁的 API
boolean tryLock();

使用非阻塞的获取锁实现:

package com.zhouj.endless.diy;
 
/**
 * @author Nemo
 * @date 2020-02-19 17:07
 * @description 可抢占
 */
public class Preemptible {
    public boolean transferMoney(Account fromAcct, Account toAcct) {
        while (true) {
            // 使用tryLock()获取锁
            if (fromAcct.lock.tryLock()) {
                try {
                    // 使用tryLock()获取锁
                    if (toAcct.lock.tryLock()) {
                        return true;
                    }
                } finally {
                    // 释放前面获取到的锁
                    fromAcct.lock.unlock();
                }
            }
        }
    }
}
破坏循环等待条件

按主键id进行排序,从小到大获取锁。

对于循环等待,可以将需要获取的锁资源排序,按照顺序获取,这样就不会多个线程交叉获取相同的资源导致死锁,而是在获取相同的资源时就等待,直到它释放。比如根据账号的主键 id 进行排序,从小到大的获取锁,这样就可以避免循环等待。

package com.zhouj.endless.diy;
 
/**
 * @author Nemo
 * @date 2020-02-19 16:31
 * @description 账户排序
 */
public class Account {
 
    private Integer id;
 
    private int balance;
 
    /**
     * 转账
     *
     * @param target 目标
     * @param amt    金额
     */
    void transfer(Account target, int amt) {
        Account left = this;
        Account right = target;
        if (this.id > target.id) {
            left = target;
            right = this;
        }
        // 锁定序号小的账户
        synchronized (left) {
            // 锁定序号大的账户
            synchronized (right) {
                if (this.balance > amt) {
                    this.balance -= amt;
                    target.balance += amt;
                }
            }
        }
    }
}

避免死锁

避免死锁:在动态分配资源的过程中,用一些算法(如 银行家算法)防止系统进入不安全状态,避免死锁的发生。

银行家算法

Dijkstra E W 于 1968 年提出银行家算法。之所以称为银行家算法,是因为该算法可用于银行系统。

新进程进入系统时,它必须说明各类资源类型的实例的最大需求量,这一数量不能超过系统各类资源的总数。
当进程申请一组资源时,该算法需要检查申请者对各类资源的最大需求量:

  • 如果系统现存的各类资源的数量可以满足当前它对各类资源的最大需求量时,就满足当前的申请;
  • 否则,进程必须等待,直到其他进程释放足够的资源为止。

换言之,仅当申请者可以在一定时间内无条件地归还它所申请的全部资源时,才能把资源分配给它。

死锁的检测及解除

死锁的检测及解除:在死锁发生前不做任何操作,只是检测当前是否发生死锁,若发生死锁,则采取一些措施(如 资源剥夺法、撤销进程法、进程回退法)来解除死锁。

处理机调度算法

处理机调度

  • 高级调度:(作业调度外存 → 内存)根据某种算法,把外存上处于后备队列中那些作业调入内存
  • 中级调度:(内存调度内存 → 外存)将那些暂时不能运行的进程调至外存等待,把进程状态改为就绪驻外存状态挂机状态
  • 低级调度:(进程调度就绪 → 执行)按照某种算法从就绪队列(内存)中选取一个进程,将处理机分配给它。

调度算法是根据系统的资源分配策略所规定的资源分配算法。
有的调度算法适用于作业调度,有的适用于进程调度,有的两种都适用。

  • 周转时间=等待时间+执行时间

先来先服务调度(First Come First Service, FCFS)

先来先服务调度(First Come First Service, FCFS):按照作业/进程进入系统的先后次序进行调度,先进入系统者先调度,即 启动等待时间最长的作业/进程。

  • 适用性:作业调度、进程调度。
  • 优点:
    • 算法简单
    • 作业/进程有利(短的要等好久)
    • 有利于CPU繁忙型作业/进程

      科普:CPU 繁忙意味着是长作业,不需要频繁的输入输出

  • 缺点:
    • 效率低
    • 对短作业/进程不利

      因为短作业执行时间很短,若令它等待较长时间,则带权周转时间会很高。

    • 不利于 I/O 繁忙型作业进程

      I/O 繁忙意味着不停地中断完成,是短作业

短作业优先调度(Shortest Job First, SJF)

短作业优先调度(Shortest Job First, SJF):该算法每次从后备队列/就绪队列中选择一个估计时间最短的作业/进程,将资源分配给它。

  • 适用性:作业调度、进程调度。
  • 优点:
    • 平均等待时间和平均周转时间最少
  • 缺点:
    • 对长作业/进程不利(可能导致长作业/进程长期不被调度,发生“饥饿”现象)
    • 不能保证紧迫性作业/进程会被及时处理
    • 由于作业/进程的长短只是根据客户说提供的估计执行时间而定的,而用户有可能会有意或无意地缩短气作业的估计运行时间,致使该算法不一定能真正做到短作业优先调度。

高响应比优先调度(Highest Response Ratio Next, HRRN)

高响应比优先调度(Highest Response Ratio Next, HRRN):该算法是对先来先服务调度算法短作业优先调度算法的一种综合平衡,同时考虑每个作业的等待时间和估计的运行时间。

在每次进行作业调度时,先计算后备作业队列中每个作业的响应比,从中选出响应比最高的作业投入运行。
$$响应比=作业周转时间/作业执行时间=(等待时间+要求服务时间)/要求服务时间$$

  • 适用性:主要用于作业调度
  • 优点:
    • 等待时间相同的作业,要求服务的时间越短,其优先权越高,此时对短作业有利
    • 等待时间相同的作业,等待时间越长,其优先权越高,此时等同于先来先服务调度算法;
    • 对于长作业,优先权随等待时间的增加而提高,其等待时间足够长时,其优先权便可提升到很高,从而也可获得处理机,此时对长作业有利,克服了饥饿状态。
  • 缺点:
    • 要进行响应比计算,增加了系统开销。

优先级调度

优先级调度:该算法每次从后备队列/就绪队列中选择优先级最高的一个作业/进程,将资源分配给它。

  • 适用性:作业调度、进程调度。

根据新的更高优先级进程能否抢占正在执行的进程,可将该调度分为:

  • 非抢占式优先级调度算法:
    当有进程正在处理机上运行时,即使有更高优先级的进程进入就绪队列,也需等待当前进程运行完成,等待主动让出处理机后,才把处理机分配给高优先级的进程。
  • 抢占式优先权调度算法:
    当有进程正在处理机上运行时,只要又出现了另一个其优先权更高的进程,进程调度程序就立即停止当前进程(原优先权最高的进程)的执行,重新将处理机分配给新到的优先权最高的进程。

时间片轮转调度

该算法将所有就绪进程按到达的先后次序排成一个队列,每次调度时,把处理机分配给队首进程,并令其执行一个时间片
当执行的时间片用完时,由一个计时器发出时钟中断请求,调度程序便停止该进程的执行,并将其放到就绪队列尾;
然后,再把处理机分配给就绪队列中新的队首。

  • 适用性:主要用于分时系统中进程调度

多级反馈队列调度

该算法是时间片轮转调度算法优先级调度算法的综合和发展,通过动态调整进程优先级和时间片大小,可以兼顾多方面的系统目标。
总结:按优先级顺序进行调度,并且随着优先级调度以及时间片轮转,慢慢增加时间片长度。
理解:每一级的用户都能在一定时间内得到反馈,不会说一直等着,所以才叫“多级反馈队列”。

设置多个就绪队列,并赋予不同优先级,优先级越高,时间片越小,进程在进入待调度的队列等待时,首先进入优先级最高的 Q1 等待,一个时间片结束后,若进程没有运行完,则转入低一级的就绪队列队尾,仅当高优先级队列中无就绪进程才开始调度低一级的就绪队列中的进程(若此刻有进程进入了高优先级队列中,那么要先转去调用高优先级队列)。

  • 例子:
    假设系统中有 3 个反馈队列 Q1,Q2,Q3,时间片分别为 2,4,8。
    设有3个作业J1,J2,J3分别在时间 0,1,3 时刻到达。而它们所需要的 CPU 时间分别是 3,2,1 个时间片。
    1. 时刻 0:J1 到达。于是进入到队列 1,运行 1 个时间片,时间片还未到,此时 J2 到达。
    2. 时刻 1:J2 到达。由于同一队列采用先来先服务,于是 J2 等待。J1 在又运行了 1 个时间片后,已经完成了在 Q1 中的 2 个时间片的限制,于是 J1 置于 Q2 等待被调度。当前处理机分配给 J2。
    3. 时刻 2:J1 进入 Q2 等待调度,J2 获得 CPU 开始运行。
    4. 时刻 3:J3 到达,由于同一队列采用先来先服务,故 J3 在 Q1 等待调度,J1 也在 Q2 等待调度。
    5. 时刻 4:J2 处理完成,由于 J3,J1 都在等待调度,但是 J3 所在的队列比 J1 所在的队列的优先级要高,于是 J3 被调度,J1 继续在 Q2 等待。
    6. 时刻 5:J3 经过 1 个时间片,完成。
    7. 时刻 6:由于 Q1 已经空闲,于是开始调度 Q2 中的作业,则 J1 得到处理器开始运行。J1 再经过一个时间片,完成了任务。于是整个调度过程结束。

从上面的例子看,在多级反馈队列中,后进的作业不一定慢完成。

问题:为什么说多级反馈队列调度算法能较好的满足各方面用户的需要?
(1)终端型作业用户提交的作业大多属于较小的交互型作业,系统只要使这些作业在第一队列规定的时间片内完成,终端作业用户就会感到满足。
(2)短批处理作业用户,开始时像终端型作业一样,如果在第一队列中执行一个时间片段即可完成,便可获得与终端作业一样的响应时间。对于稍长作业,通常只需在第二和第三队列各执行一时间片即可完成,其周转时间仍然较短。
(3)长批处理作业,它将依次在第1,2,…,n个队列中运行,然后再按轮转方式运行,用户不必担心其作业长期得不到处理。所以,多级反馈队列调度算法能满足多用户需求。

这样看来,每一级的用户都能在一定时间内得到反馈,不会说一直等着,所以才叫“多级反馈队列”。

记忆总结

  1. 先来先服务调度
  2. 短作业优先调度
  3. 高响应比优先调度:短作业优先调度 + 先来先服务调度,考虑了运行时间(短作业)等待时间(先来先服务)
  4. 优先级调度
  5. 时间片轮转调度
  6. 多级反馈队列调度:时间片轮转调度 + 优先级调度

主存管理(内存)

主存和内存是主存储器的两种不同叫法,其实就是一个东西。计算机中,主存储器又称内存储器,其作用是存放指令和数据,并能由中央处理器(CPU)直接随机存取。

主存管理的功能:

  • 虚拟存储
  • 地址映射
  • 主存分配
  • 存储保护

地址映射:为了保证CPU执行指令时可正确访问存储单元,需将用户程序中的逻辑地址转换为运行时由机器直接寻址的物理地址,这一过程称为地址映射。

地址映射分类:
地址映射也可以称为地址重定位或地址变换,可以分为以下三类:

  1. 固定地址映射:编程或编译时确定地址映射关系,以代码的形式硬编码(代码里已经把它写固定了,即移植的时候已经设计好了要把哪个物理地址映射哪个虚拟地址)
  2. 静态地址映射:当用户程序被装入内存时,一次性实现逻辑地址到物理地址的转换,以后不再转换(一般在装入内存时由软件完成)。
  3. 动态映射映射:在程序运行过程中要访问数据时再进行地址变换(即在逐条指令执行时完成地址映射。一般为了提高效率,此工作由硬件地址映射机制来完成。由硬件支持,软件硬件结合完成。硬件上一般需要一对寄存器的支持)。

实存管理:

  • 分区式管理:最简单直观的方式,在内存中连续分配一个区,将整个进程放入这个区。(数组

缺点是会产生外碎片,即 时间长了会在分区之间产生难以被利用的小空间。
- 固定分区:把主存中可分配的用户区域预先划分成若干个连续的分区,每个连续区的大小可以相同,也可以不同。但是,一旦划分好分区之后,主存中分区的个数就固定了,且每个分区的大小也固定不变。这是一种静态分区法。
在固定分区方式管理下,每个分区用来装入一个作业。由于主存中有多个分区,就可同时在每个分区中装入一个作业。所以,这种存储管理方式适用于多道程序系统。
- 可变分区:内存管理的可变分区模式,又称变长分区模式、动态分区分配模式。
这种分配方式不会预先划分内存分区,而是在进程装入内存时,根据进程的大小动态地建立分区,并使分区的大小正好适合进程的需要。因此系统分区的大小和数目是可变的。
与固定分区的区别就是:动态的划分分区。克服固定分区管理的“内碎片”问题。

  • 分页式管理:将内存分成固定大小的页,离散分配若干页将整个进程载入。(链表

页面可以不连续是其重要优点,不会产生外碎片,更有效地利用了内存,不过会产生一些内碎片,即 分配给进程的最后一个页往往不能正好用完,不过在页面大小不是很大的时候可以接受。

  • 分段式管理:将程序分为若干个段,如数据段和代码段,加以不同的保护。(链表节点大小不一

施加保护是分段式的优点,但其仍是像分区式管理一样的连续分配

  • 段页式管理:同样将程序分段,加以不同的保护,但是各段不再连续分配,而采用分页式离散分配

注意:以上四种全是实存管理,即 进程要么全部载入内存中,要么就不能载入。

虚存管理:

  • 请求式分页:将进程放入虚拟内存中,由于一个进程的页面不会同时全部被用到,只将需要用到的页面调入物理内存。即进程并没有整个在物理内存中。
    几个请求式分页的概念:
    • 固定分配:物理内存中分配给进程的内存块数一定。
    • 可变分配:物理内存先分配给进程一些内存块,如不够,可适当增加。
    • 局部置换:发生在分配的内存块已用完,又发生了缺页时,只能置换本来就是自己的内存块。
    • 全局置换:发生在分配的内存块已用完,又发生了缺页时,可以置换到操作系统保留的空闲页。这其实相当于增加了进程占有的内存块数。

    三种分配方式:固定分配局部置换、可变分配全局置换、可变分配局部置换。固定分配、全局置换不能组合。

功能

虚拟存储

1、虚存的定义

计算机系统在处理应用程序时,只装入部分程序代码和数据就启动起运行,由操作系统和硬件相配合完成主存和辅存之间的信息的动态调幅,这样的计算机系统好像为用户提供可以一个其存储容量比实际主存大得多的存储器,这个存储器称为虚拟存储器。

2、提供虚拟存储器的必要性

现代操作系统为支持多用户、多任务的同时执行,需要大量的主存空间。特别是现在需要计算机解决的问题越来越多,越来越复杂,有些科学计算或需要大量数据数据处理的问题需要相当大的主存容量,使系统中主存容量先得更为紧张,因此系统提供虚拟存储器来方便用户的使用和有效地支持多用户对主存的共享。

地址映射

1、地址映射定义

将程序地址空间中的逻辑地址变成主存中的物理地址的过程。

2、地址映射方式

地址映射也可以称为地址重定位或地址变换,可以分为以下三类:

  1. 固定地址映射:编程或编译时确定地址映射关系,以代码的形式硬编码(代码里已经把它写固定了,即移植的时候已经设计好了要把哪个物理地址映射哪个虚拟地址)
  2. 静态地址映射:当用户程序被装入内存时,一次性实现逻辑地址到物理地址的转换,以后不再转换(一般在装入内存时由软件完成)。
  3. 动态映射映射:在程序运行过程中要访问数据时再进行地址变换(即在逐条指令执行时完成地址映射。一般为了提高效率,此工作由硬件地址映射机制来完成。由硬件支持,软件硬件结合完成。硬件上一般需要一对寄存器的支持)。

主存分配

主存管理存储器的策略只要有3种:

  1. 放置策略:决定主存中放置信息的区域;
  2. 调入策略:决定信息装入主存的时机;
  3. 淘汰策略:对一个应用程序而言,它在储存中已没有可用的空闲区时,决定哪些信息可以从主存中移走。

存储保护

现代操作系统中存储器由多个程序共享。为了保证多个程序之间互不影响,必须由硬件(软件配合)保证每个程序只能在给定的存储区域内活动,即存储保护。

内存碎片

内存碎片分为:内部碎片和外部碎片

  • 内部碎片:已经被分配出去(能明确指出属于哪个进程)却不能被利用的内存空间;

    内部碎片是处于(操作系统分配的用于装载某一进程的内存)区域内部或页面内部的存储块。
    占有这些区域或页面的进程并不使用这个存储块
    而在进程占有这块存储块时,系统无法利用它
    直到进程释放它,或进程结束时,系统才有可能利用这个存储块。

  • 外部碎片:还没有被分配出去(不属于任何进程),但由于太小了无法分配给申请内存空间的新进程的内存空闲区域。

    外部碎片是处于任何两个已分配区域或页面之间的空闲存储块。
    这些存储块的总和可以满足当前申请的长度要求,但是由于它们的地址不连续或其他原因,使得系统无法满足当前申请。

分区存储管理(数组,连续分配空间)

分区存储管理中,程序的地址空间是一维线性的,因为是连续分配的,指令或操作数地址只要给出一个信息量即可决定。(连续分配

分区式管理:最简单直观的方式,在内存中连续分配一个区,将整个进程放入这个区。(数组

缺点是会产生外碎片,即 时间长了会在分区之间产生难以被利用的小空间。

  • 固定分区:把主存中可分配的用户区域预先划分成若干个连续的分区,每个连续区的大小可以相同,也可以不同。但是,一旦划分好分区之后,主存中分区的个数就固定了,且每个分区的大小也固定不变。这是一种静态分区法。
    在固定分区方式管理下,每个分区用来装入一个作业。由于主存中有多个分区,就可同时在每个分区中装入一个作业。所以,这种存储管理方式适用于多道程序系统。
  • 可变分区:内存管理的可变分区模式,又称变长分区模式、动态分区分配模式。
    这种分配方式不会预先划分内存分区,而是在进程装入内存时,根据进程的大小动态地建立分区,并使分区的大小正好适合进程的需要。因此系统分区的大小和数目是可变的。

与固定分区的区别就是:动态的划分分区。克服固定分区管理的“内碎片”问题。

分配策略

分区存储管理中常用的分配策略有:首次适应算法循环首次适应算法最佳适应算法最坏适应算法

首次适应算法

首次适应算法:地址从小到大排序,分配第一个符合条件的分区,即 第一个足够装入它的可利用的空闲区。

  • 优点:
    • 保留高地址部分的大空闲区,有利于后来的大型作业分配。
  • 缺点:
    • 低地址部分被不断划分,留下许多难以利用的小空闲区;
    • 每次分配时都要从低地址部分开始查找,增加查找时的系统开销。

循环首次适应算法

循环首次适应算法:在首次适应算法的基础上,从上次查找结束的位置开始查找,分配第一个足够装入它的可利用的空闲区。

  • 优点:

    • 使内存中的空闲分区分布更均匀,减少了查找时的系统开销。
  • 缺点:

    • 缺乏大的空闲区,可能导致不能装入大型作业。

最佳适应算法

最佳适应算法:按空间从小到大排序,分配第一个符合条件的分区,即 与它所需大小最接近的空闲区。

  • 优点:
    • 每次分配的空闲区都是最合适的
  • 缺点:
    • 在内存中留下许多难以利用的小空闲区

最坏适应算法

最坏适应算法:是按空间从大到小排序,分配第一个符合条件的分区,即 最不适合它的空闲区,即 最大的空闲区

  • 优点:
    • 产生碎片的几率最小,对中小型作业有利。
  • 缺点:
    • 缺乏大的空闲区,对大型作业不利。

页式存储管理(链表,离散分配空间,空间不连续,链表节点大小相同)

页式存储管理中,程序的地址空间是一维线性的,因为指令或操作数地址只要给出一个信息量即可决定。(离散分配

理解:页式存储管理只用给出一个逻辑地址就行,因为页的大小是固定的,不需要刻意划分,当然不是逻辑地址÷页的大小=页号...余 偏移量,因为页式存储是离散分配的。
根据下文的地址变换过程可知,我们只需要提供逻辑地址这一个地址,就可以找到这个页式存储物理地址,所以是地址空间一维的,只用一个逻辑地址。

  • 不存在外碎片,存在内碎片

页之间一页紧跟着一页,没有外碎片;但是页内有可能分配不满,有内碎片。

分页式管理:将内存分成固定大小的页,离散分配若干页将整个进程载入。于是一个连续的程序空间在主存中可能是不连续的。为了保证程序能正确地执行,必须在执行每条指令时将程序中的逻辑地址变换为实际的物理地址,即 进行动态重定位。
在页式系统中,实现这种地址变换的机构称为页面映射表,简称页表

页面可以不连续是其重要优点,不会产生外碎片,更有效地利用了内存,不过会产生一些内碎片,即 分配给进程的最后一个页往往不能正好用完,不过在页面大小不是很大的时候可以接受。

其实如何利用页表来进行地址变换,这与计算机所采用的地址结构有关,而地址结构又与选择的页面尺寸有关。

  • 虚地址结构:(因为是逻辑上的虚拟地址,所以由人为规定结构,比较随意)

    页号 页内位移
    P W

    就如上表所示,虚地址是由页号页内位移组合而成。

  • 地址变换过程:(逻辑地址→物理地址
    在收到逻辑地址(Logic Address, LA)后,进行处理,得到物理地址(Physical Address, PA):

    1. 逻辑地址 LA 划分为页号 P页内位移 W
    2. 根据页号 P 查页表,得到块号 B
    3. \(物理地址 PA = 块号 B \* 页的大小 + 页内位移 W\)

相关概念

页面

相对物理块来说,页是逻辑地址空间(虚拟内存空间)的划分,是逻辑地址空间顺序等分而成的一段逻辑空间,并依次连续编号。页的大小一般为 512B~8KB。

例如:一个 32 位的操作系统,页的大小设为\(2^{12}=4KB\),那么就有页号从 0 编到 \(2^{20}\) 的那么多页逻辑空间。

物理块

物理块则是相对于虚拟内存对物理内存按顺序等大小的划分。物理块的大小需要与页的大小一致。

例如:\(2^{31} = 2GB\) 的物理内存,按照 4KB/页的大小划分,可以划分成物理块号从 0 到 \(2^{19}\) 的那么多块的物理内存空间。

逻辑地址结构

页式存储管理中,逻辑地址可以解读成页号+地址偏移量(页内地址)。如下图所示:
image

这是一个 32 位的逻辑地址,页大小设为 \(2^{12} = 4KB\)。高 20 位则是页号,低 12 位则是页内地址。逻辑地址为 A,页面大小为 L,则页号 P 和页内地址 d 计算公式如下:
image

页表

页表是记录逻辑空间(虚拟内存)中每一页在内存中对应的物理块号。但并非每一页逻辑空间都会实际对应着一个物理块,只有实际驻留在物理内存空间中的页才会对应着物理块。
页表是需要一直驻留在物理内存中的(多级页表除外),另外页表的起址和长度存放在 PCB(Process Control Block)进程控制结构体中。
image

逻辑地址到物理地址的变换过程
image

  1. 进程访问某个逻辑地址时,分页地址机构自动将逻辑地址分为页号和页内地址
  2. 页号大于页表长度,越界错误
  3. \(页表项的地址 p = 页表起始地址 F + 页号 P * 表项大小 S\),从而得到对应的物理块号 B
  4. 页和物理块的大小是一致的,所以 \(页内地址=块内地址\)
  5. 然后 \(物理地址 = 物理块号 B * 页大小 L + 页内地址\)
  6. 根据物理地址读取数据

image

两级页表(Two-Level Page Table)

一级页表的缺陷:
由于页表必须连续存放,并且需要常驻物理内存,当逻辑地址空间很大时,导致页表占用内存空间很大。
例如,地址长度 32 位,可以得到最大空间为 4GB,页大小 4KB,最多包含 4G/4K=1M 个页。若每个页表项占用 4 个字节,则页表最大长度为 4MB,即要求内存划分出连续 4MB 的空间来装载页表;若地址程度为 64 位时,就需要恐怖的 \(4 \* 2^{52} Byte\) 空间来存储页表了。而且页表采用的是连续分配,不是分页分配。
采用离散分配方式的管理页表,将当前需要的部分页表项调入内存,其余的页表项仍驻留在磁盘上,需要时再调入。


现代的大多数计算机系统,都支持非常大的逻辑地址空间(\(2^{32}~2^{64}\))。在这样的环境下,页表就变得非常大,要占用相当大的内存空间。例如,对于一个具有32位逻辑地址空间的分页系统,规定页面大小为4 KB,则在每个进程页表中的页表项可达1兆个之多。又因为每个页表项占用四个字节,故每个进程仅仅其页表就要占用4 MB的内存空间,而且还要求是连续的。显然这是不现实的,我们可以采用下述两个方法来解决这一问题:

  • 采用离散分配方式来解决难以找到一块连续的大内存空间的问题;
  • 只将当前需要的部分页表项调入内存,其余的页表项仍驻留在磁盘上,需要时再调入。

页表,页表项,页面,页面大小,物理块
这里是真的有点混乱的说法,在操作系统中,我们知道一级页表不会有这么多称呼,但是多级页表就有了。我们知道一级页表中,就只有页号位移量了(也就是所说的物理块块号),这里不会出现页表项,但是二级页表乃至多级页表时就会出现页表项了,页表项我们知道是有一个“项”的字的尾缀,那么很明显,这里是类似于一个整体的意思,也就是二级页表中外层页表每一项表示第二层的一页,也就是页表项了,直截了当的说就是页表项就是一个二级页(次级页)

通俗的说页表项就是页表中的每一项,外层页表每一项指向的是次级页表的表头地址(代指次级页表一张),次级页表依次这样,如果是二级的话,次级页表项指的就是(页号对应一个页面,一个页面对应一个物理块)物理块了。


对于要求连续的内存空间来存放页表的问题,可利用将页表进行分页,并离散地将各个页面分别存放在不同的物理块中的办法来加以解决,同样也要为离散分配的页表再建立一张页表,称为外层页表(Outer Page Table),在每个页表项中记录了页表页面的物理块号。下面我们仍以前面的32位逻辑地址空间为例来说明。当页面大小为 4 KB时(12位),若采用一级页表结构,应具有20位的页号,即页表项应有1兆个;在采用两级页表结构时,再对页表进行分页,使每页中包含\(2^{10}\) (即 1024)个页表项,最多允许有\(2^{10}\)个页表分页;或者说,外层页表中的外层页内地址P2为10位,外层页号P1也为10位。此时的逻辑地址结构可描述如下:

页目录号 页号 页内偏移量

由下图可以看出,在页表的每个表项中存放的是进程的某页在内存中的物理块号,如第0#页存放在1#物理块中;1#页存放在4#物理块中。而在外层页表的每个页表项中,所存放的是某页表分页的首址,如第0#页表是存放在第1011#物理块中。我们可以利用外层页表和页表这两级页表,来实现从进程的逻辑地址到内存中物理地址间的变换。
image

为了地址变换实现上的方便起见,在地址变换机构中同样需要增设一个外层页表寄存器,用于存放外层页表的始址,并利用逻辑地址中的外层页号,作为外层页表的索引,从中找到指定页表分页的始址,再利用P2作为指定页表分页的索引,找到指定的页表项,其中即含有该页在内存的物理块号,用该块号和页内地址d即可构成访问的内存物理地址。右图示出了两级页表时的地址变换机构。
image


1.某计算机采用二级页表的分页存储管理方式,按字节编址,页大小为 \(2^{10} B\),页表项大小为2B,逻辑地址结构为:

页目录号 页号 页内偏移量

逻辑地址空间大小为 \(2^{16}\) 页,则表示整个逻辑地址空间的页目录表中包含表项的个数至少是()。
A,64                B,128
C,256               D,512

答案:

  • 页大小为 \(2^{10} B\),页表项大小为2B,故一页可以存放 \(2^{9}\) 个页表项,
  • 逻辑地址空间大小为 \(2^{16}\) 页,即 总共有 \(2^{16}\) 个页,二级页表共需 \(2^{16}\) 个页表项去表达整个地址空间的页目录表,
  • 每个二级页表一页可以存放 \(2^{9}\) 个页表项,则需 \(2^{16} / 2^{9} = 2^{7} = 128\) 个二级页表去保存页表项
  • 所以一级页表需要 128 个页表项来表示128个二级页表,即 页目录表中包含表项的个数至少是128。

一级页表需要使用128个页表项来表达128个二级页表的页面,每个二级页表的页面保存着 \(2^{9}\) 个页表项

  • 一级页表:页目录表。一级页表的每一个页表项对应着一个二级页表
  • 二级页表:二级页表。二级页表的每一个页表项对应着一个具体的页

也就是说,我们最终需要求解的是一级页表(页目录)下,应该有多少个二级页表


答案二:
逻辑地址 = 页目录号 + 页号 + 页内偏移量

  • 页大小为 \(2^{10} B\)(说明页内偏移量为10位,10 bit)
  • 页大小为 \(2^{10} B\),页表项大小为2B,故一页可以存放 \(2^{9}\) 个页表项(说明页号为9位,9 bit)
  • 逻辑地址空间大小为 \(2^{16}\) 页(说明虚地址总位数为16位,16 bit,所以我们可以求出页目录号为16-9=7位,所以有\(2^7 = 128\)个页表项
页目录号 页号 页内偏移量
7 9 10

多级页表

对于32位的机器,采用两级页表结构是合适的;但对于64位的机器,采用两级页表是否仍可适用的问题,须做以下简单分析。如果页面大小仍采用4 KB,即 \(2^{12} B\),那么还剩下52位,假定仍按物理块的大小(\(2^{12} 位\))来划分页表,则将余下的42位用于外层页号。此时在外层页表中可能有4096 G个页表项,要占用16 384 GB的连续内存空间。这样的结果显然是不能令人接受的,因此必须采用多级页表,将外层页表再进行分页,也就是将各分页离散地装入到不相邻接的物理块中,再利用第2级的外层页表来映射它们之间的关系。

对于64位的计算机,如果要求它能支持 \(2^{64} B(= 1 844 744 TB)\)规模的物理存储空间,则即使是采用三级页表结构也是难以办到的;而在当前的实际应用中也无此必要。故在近两年推出的64位OS中,把可直接寻址的存储器空间减少为45位长度(即 \(2^{45}\))左右,这样便可利用三级页表结构来实现分页存储管理。

多级页表和二级页表是为了节省物理内存空间。使得页表可以在内存中离散存储。(单级页表为了随机访问必须连续存储,如果虚拟内存空间很大,就需要很多页表项,就需要很大的连续内存空间,但是多级页表不需要。)

页面置换算法

在地址映射过程中,若在页面中发现所要访问的页面不在内存中,则产生缺页中断
当发生缺页中断时,如果操作系统内存中没有空闲页面,则操作系统必须在内存选择一个页面将其移出内存,以便为即将调入的页面让出空间。而用来选择淘汰哪一页的规则叫做页面置换算法

抖动(颠簸):是指在页面置换过程中,刚刚调出的页面马上又要调入内存,刚刚调入的页面马上又要调出,发生频繁的页面调度行为。

  • \[缺页中断率=成功访问次数/总访问次数 \]

  • 最佳置换算法(OPTimal replacement, OPT):将以后永不使用的或者在最长时间内不会被访问的页面调出,但由于人们无法预知进程在内存下的若干页面中哪个是未来最长时间内不再被访问的,因而该算法无法实现。
  • 先进先出置换算法(First In First Out, FIFO):将最早进入内存的页面调出。

    该算法会产生Belady异常,即 发生缺页时,如果对一个进程未分配它所要求的全部页面,有时分配页数↑,缺页率反而↑的异常现象。(先进先出,结果进来了一个需要的页,也出去了一个需要的页)

  • 最近最少使用置换算法(Least Recently Used, LRU):是将最近最长时间未访问的页面调出。该算法为每个页面设置一个访问字段,记录页面上次被访问以来所经历的时间,调出页面时选择时间最长的页面
  • 最不经常使用置换算法(Least Frequently Used LFU):将最近应用次数最少的页淘汰。
  • 时钟置换算法(CLOCK):也称为最近未用算法(NRU),该算法是为每个页面设置一个使用位,需要替换页面时,循环检查各个页面,将使用位为 1 的页面重置为 0(使用时再置为1),直到遇到第一个使用位为 0 的页面,将其调出。
    如果在每个页面再增加一个修改位,则得到改进型的 CLOCK 置换算法,类似的,需要替换页面时,将使用位和修改位都为 0 的页面调出。

最佳置换算法(OPTimal replacement, OPT)

最佳置换算法(OPTimal replacement, OPT):将以后永不使用的或者在最长时间内不会被访问的页面调出,但由于人们无法预知进程在内存下的若干页面中哪个是未来最长时间内不再被访问的,因而该算法无法实现。

先进先出置换算法(First In First Out, FIFO)

先进先出置换算法(First In First Out, FIFO):将最早进入内存的页面调出。

该算法会产生Belady异常,即 发生缺页时,如果对一个进程未分配它所要求的全部页面,有时分配页数↑,缺页率反而↑的异常现象。(先进先出,结果进来了一个需要的页,也出去了一个需要的页)

最近最少使用置换算法(Least Recently Used, LRU)(最长时间)

LRU是淘汰最长时间没有被使用的页面。

最近最久未使用置换算法(Least Recently Used, LRU):是将最近最长时间未访问的页面调出。该算法为每个页面设置一个访问字段,记录页面上次被访问以来所经历的时间,调出页面时选择时间最长的页面。

LRU 缓存机制可以通过哈希表辅以双向链表实现,我们用一个哈希表和一个双向链表维护所有在缓存中的键值对。

  • 双向链表:按照被使用的顺序存储了这些键值对,靠近尾部的键值对是最近使用的,而靠近头部的键值对是最久未使用的。
  • 哈希表:即为普通的哈希映射(HashMap),通过缓存数据的键映射到其在双向链表中的位置。
    \(hashMap = <键, 链表指针>\)

LRU含义

我们前面也说了,LRU的意思是最长不经常使用,也可以理解成最久没有使用。在这种策略下我们用最近一次使用的时间来衡量一块内存的价值,越久之前使用的价值也就越低,最近刚刚使用过的,后面接着会用到的概率也就越大,那么自然也就价值越高。

当然只有这个限制是不够的,我们前面也说了,由于内存是非常金贵的,导致我们可以存储在缓存当中的数据是有限的。比如说我们固定只能存储1w条,当内存满了之后,缓存每插入一条新数据,都要抛弃一条最长没有使用的旧数据。这样我们就保证了缓存当中的数据的价值都比较高,并且内存不会超过限制。

我们把上面的内容整理一下,可以得到几点要求:

  1. 保证缓存的读写效率,比如读写的复杂度都是\(O(1)\)
  2. 当一条缓存当中的数据被读取,将它最近使用的时间更新
  3. 当插入一条新数据的时候,弹出更新时间最远的数据

LRU原理

我们仔细想一下这个问题会发现好像没有那么简单,显然我们不能通过数组来实现这个缓存。因为数组的查询速度是很慢的,不可能做到\(O(1)\)。其次我们用HashMap好像也不行,因为虽然查询的速度可以做到\(O(1)\),但是我们没办法做到更新最近使用的时间,并且快速找出最远更新的数据。

如果是在面试当中被问到想到这里的时候,可能很多人都已经束手无策了。但是先别着急,我们冷静下来想想会发现问题其实并没有那么模糊。

首先HashMap是一定要用的,因为只有HashMap才可以做到\(O(1)\)时间内的读写,其他的数据结构几乎都不可行。但是只有HashMap解决不了更新以及淘汰的问题,必须要配合其他数据结构进行。这个数据结构需要能够做到快速地插入和删除,其实我这么一说已经很明显了,只有一个数据结构可以做到,就是链表

链表有一个问题是我们想要查询链表当中的某一个节点需要\(O(1)\)的时间,这也是我们无法接受的。但这个问题并非无法解决,实际上解决也很简单,我们只需要把链表当中的节点作为HashMap中的value进行储存即可,最后得到的系统架构如下:
image

对于缓存来说其实只有两种功能,第一种功能就是查找,第二种是更新

查找

查找会分为两种情况,第一种是没查到,这种没什么好说的,直接返回空即可。如果能查到节点,在我们返回结果的同时,我们需要将查到的节点在链表当中移动位置。我们假设越靠近链表头部表示数据越旧,越靠近链表尾部数据越新,那么当我们查找结束之后,我们需要把节点移动到链表的尾部。

移动可以通过两个步骤来完成,第一个步骤是在链表上删除该节点,第二个步骤是插入到尾部:
image

更新

更新也同样分为两种情况,第一种情况就是更新的key已经在HashMap当中了,那么我们只需要更新它对应的Value,并且将它移动到链表尾部。对应的操作和上面的查找是一样的,只不过多了一个更新HashMap的步骤,这没什么好说的,大家应该都能想明白。

第二种情况就是要更新的值在链表当中不存在,这也会有两种情况,第一个情况是缓存当中的数量还没有达到限制,那么我们直接添加在链表结尾即可。第二种情况是链表现在已经满了,我们需要移除掉一个元素才可以放入新的数据。这个时候我们需要删除链表的第一个元素,不仅仅是链表当中删除就可以了,还需要在HashMap当中也删除对应的值,否则还是会占据一份内存。

因为我们要进行链表到HashMap的反向删除操作,所以链表当中的节点上也需要记录下Key值,否则的话删除就没办法了。删除之后再加入新的节点,这个都很简单:
image

参考资料:https://zhuanlan.zhihu.com/p/265597517

具体实现方法:https://leetcode-cn.com/problems/lru-cache-lcci/solution/lruhuan-cun-by-leetcode-solution/

最不经常使用置换算法(Least Frequently Used, LFU)(应用最少次)

LFU是淘汰一段时间内,使用次数最少的页面。

最不经常使用置换算法(Least Frequently Used LFU):将最近应用次数最少的页淘汰。

实现方法一般为双哈希表+双向链表

  • 双向链表:这个链表里存放所有使用频率为 freq 的缓存,缓存里存放三个信息,分别为键 key,值 value,以及使用频率 freq。
  • 第一个 freq_table 以频率 freq 为索引,每个索引存放一个双向链表,这个链表里存放所有使用频率为 freq 的缓存,缓存里存放三个信息,分别为键 key,值 value,以及使用频率 freq。

\(freq\_table = <频率, 双向链表>\)

  • 第二个 key_table 以键值 key 为索引,每个索引存放对应缓存在 freq_table 中链表里的内存地址,这样我们就能利用两个哈希表来使得两个操作的时间复杂度均为 \(O(1)\)。同时需要记录一个当前缓存最少使用的频率 minFreq,这是为了删除操作服务的。

\(key\_table = <键, 链表指针(即 内存地址)>\)

LFU原理

我们定义两个哈希表,

  • 第一个 freq_table 以频率 freq 为索引,每个索引存放一个双向链表,这个链表里存放所有使用频率为 freq 的缓存,缓存里存放三个信息,分别为键 key,值 value,以及使用频率 freq。

\(freq_table = <频率, 双向链表>\)

  • 第二个 key_table 以键值 key 为索引,每个索引存放对应缓存在 freq_table 中链表里的内存地址,这样我们就能利用两个哈希表来使得两个操作的时间复杂度均为 \(O(1)\)。同时需要记录一个当前缓存最少使用的频率 minFreq,这是为了删除操作服务的。

\(key_table = <键, 链表指针(即 内存地址)>\)

image

具体实现方法:https://leetcode-cn.com/problems/lfu-cache/solution/lfuhuan-cun-by-leetcode-solution/

最近未用算法(Not Recently Used, NRU)

时钟置换算法(CLOCK):也称为最近未用算法(Not Recently Used, NRU),该算法是为每个页面设置一个使用位,需要替换页面时,循环检查各个页面,将使用位为 1 的页面重置为 0(使用时再置为1),直到遇到第一个使用位为 0 的页面,将其调出。
如果在每个页面再增加一个修改位,则得到改进型的 CLOCK 置换算法,类似的,需要替换页面时,将使用位和修改位都为 0 的页面调出。

段式存储管理(链表节点大小不一)

段式存储管理的用户地址是二维的、按段划分的。(离散分配

理解:段式存储管理必须给出(段号,偏移量),因为段的大小不固定,必须给出这个地址结构(逻辑地址),由此找到段表中程序员设置基址,由地址结构(逻辑地址)和基址这两个地址我们可以得到段式存储物理地址,所以是二维的,需要用到两个地址。

  • 存在外碎片,不存在内碎片

段内紧密连接为一个整体,没有内碎片;段之间不一定紧密连接,存在外碎片。

分段式管理:将程序分为若干个段,如数据段和代码段,加以不同的保护。

施加保护是分段式的优点,虽然不同段可以离散分配,但其段内仍是连续分配。

在这样的系统中作业的地址空间由若干逻辑分段组成,每个分段有自己的名字,对于一个分段而言,它是一个连续的地址区。

  • 由于标识某一程序地址时,要同时给出段名和段内地址,因此地址空间是二维的(实际上,为了实现方便,在第一次访问某段时,操作系统就用唯一的段号来代替该段的段名)。

  • 程序地址的一般形式由(s,w)组成,这里 s 是段号,w 是段内位移

    段号 段内位移
    s w

    注意:这里虽然和分页一样,但是进入段表后,要找到段表中程序员设置的基址,所以是二维的地址结构。

分页和分段的异同

相同点:

  • 分页和分段都采用离散分配(也就是不连续的分配地址,类似于链表)的方式
  • 都要通过地址映射机构来实现地址变换

不同点:

  • 从功能上看:
    是信息的物理单位,分页是为实现离散分配方式,以消除内存的外零头,提高内存利用率,是为了满足系统管理的需要;
    是信息的逻辑单位,它含有一组意义相对完整的信息,是为了满足用户的需要。
  • 的大小是由系统确定且固定不变的;
    长度不固定,取决于用户编写的程序。
  • 分页的作业地址空间是一维的;
    分段的作业地址空间是二维的。

理解:
分页存储管理里面的地址结构是:页号+位移量,这个地址结构就是一个地址;
根据页号在页表项中找到对应页项,这个页项代表了一个块号,但是这个块号并不是一个地址,因为所有块是事先已经划分好且长度相等的,这些块是操作系统自己知道的,所以操作系统就可以仅根据地址结构直接访问,即分页存储管理的地址空间是一维的。
分段存储管理里面的地址结构是:段号+段内地址(位移量),这个地址结构就是一个地址;
根据段号在段表项中找到对应段项,这个段项和页项的组成成分不一样,其中还包含了一个基址,就是段在内存中的起始地址,这个地址不是操作系统划分,而是程序员事先设置的,操作系统并不知道,所以操作系统要访问内存就必须要结合地址结构段表中的基址,所以分段的地址空间是二维的。

总结:
分页地址结构+块号,地址结构是地址,块号只是一个数字而已,所以是一维;(因为长度固定,所以只需要块号数字就行)
分段地址结构+基址,两者都是地址,所以是二维的。(因为长度不固定,所以需要基址才能找到)

段页式存储管理

段页式存储管理的用户地址也是二维的、按段划分的。只是在段中再划分成若干大小相等的页

理解:段页式存储管理也必须给出(段号,偏移量),虽然段的大小不固定,但是段内页号和页内偏移可以根据偏移量算出来,所以还是二维的。

  • 段页式管理:同样将程序分段,加以不同的保护,但是各段不再连续分配,而采用分页式离散分配

  • 地址结构就由段号、段内页号、页内位移三部分组成。

    注意:不要看三部分就是三维的了,其实用户使用的部分也是二维的。

段号 段内页号 页内位移
  • 用户使用的仍是段号和段内相对地址
    由地址变换机构自动将段内相对地址的高几位解释为段内页号,将剩余的低位解释为页内位移

  • 用户地址空间的最小单位不是段而是,而主存按页的大小划分,按页装入。

    这样,一个段可以装入到若干个不连续的主存块内,段的大小不再受主存可用区的限制了。

虚拟存储器

虚拟存储器:是指具有请求调入功能和置换功能,并能从逻辑上对内存容量加以扩充的一种存储器系统。
它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。

  • 局部性原理:是指CPU访问存储器时,无论是存取指令还是存取数据,所访问的存储单元都趋于聚集在一个较小的连续区域中。
    • 时间局部性(Temporal Locality):如果一个信息项正在被访问,那么在近期它很可能还会被再次访问。

      程序循环、堆栈等是产生时间局部性的原因。

    • 空间局部性(Spatial Locality):在最近的将来将用到的信息很可能与正在使用的信息在空间地址上是临近的。
    • 顺序局部性(Order Locality):在典型程序中,除转移类指令外,大部分指令是顺序进行的。顺序执行和非顺序执行的比例大致是 5:1。此外,对大型数组访问也是顺序的。

      指令的顺序执行、数组的连续存放等是产生顺序局部性的原因。

虚拟存储器基于局部性原理,在程序装入时,可以只将程序的一部分装入内存,就可以启动程序执行。
在执行过程中,当所访问的信息不在内存中时,由操作系统将所需内容调入内存,使程序继续执行;

解释:****因为局部性原理,所以下一步需要访问的信息很有可能就在附近。

同时,操作系统将内存中暂时不用的内容调出到外存。

这样,系统就好像为用户提供了一个比实际内存大得多的存储器,称为虚拟存储器

  • 特征:
    • 离散性:是指内存分配时采用离散分配的方式。若采用连续分配方式,需要将作业装入到连续的内存区域,这样需要连续地一次性申请一部分内存空间,无法实现虚拟存储功能,只有采用离散分配方式,才能为它申请内存空间,以避免浪费内存空间。
    • 多次性:作业无需在运行时一次性全部装入内存,而是可以分成多次调入内存。
    • 对换性:作业运行时无需常驻内存,可以进行调入调出。
    • 虚拟性:从逻辑上扩充了内存的容量,使用户所感觉到的内存容量远大于实际容量。

文件系统(外存)

外存,指的是除了cpu缓存和内存以外的存储器,硬盘、光盘、U盘都可以被称为外存。所有的数据,也都存在这里面,故他的分配方式变得极其重要,这直接影响到了计算机的运行速度。

外存分配方式主要有这几种:连续分配,链式分配,索引分配。

文件分配

文件分配对应于文件的物理结构,是指如何为文件分配磁盘块(外存)。

常用的磁盘空间分配方法有三种:

  • 连续分配:要求每个文件在磁盘上占有一组连续的块。
  • 链接分配:采取离散分配的方式,分为隐式链接分配和显式链接分配。

类比:链表那样一个一个地顺序查找
- 隐式链接分配:每个文件对应一个磁盘块的链表,磁盘块任意分布,除最后一个盘块外,每一个盘块都有指向下一个盘块的指针(类似于数据结构中的链表)。
- 显式链接分配:把用于链接文件各物理块的指针提取出来,显式地存放在内存里的一张链接表中,该表整个磁盘仅设置一张,由于分配给文件的所有盘块号都放在该表中,故称该表为文件分配表(FAT)。
这本质上仍然是链接分配,即 进程给出文件物理块起始地址等信息,然后根据内存FAT中地址的链接情况进行查找,得到所需物理块。在查找过程中仍然是一个一个地顺序查找

  • 索引分配:把每个文件的所有盘块号集中在一起构成索引块(表)。

索引分配与显式链接分配不同,索引分配是随机查找,不需要借助前一个物理块来找到后一个,直接就可以查找到,直达

一. 连续分配

原理:创建文件时,分配一组连续的块;FAT(文档分配表)中每个文件只要一项,说明起始块和文件长度。对于顺序文件有利。

优点:

  • 简便。适用于一次性写入操作。
  • 支持顺序存取和随机存取,顺序存取速度快。
  • 所需的磁盘寻道次数和寻道时间最少。(因为空间的连续性,当访问下一个磁盘块时,一般无需移动磁头,当需要移动磁头时,只需要移动一个磁道。)

缺点:

  • 文件不能动态增长。(可能文件末尾处的空块已经分配给了别的文件。)
  • 不利于文件的插入和删除。
  • 外部碎片问题。(反复增删文件后,很难找到空间大小足够的连续块,需要进行紧缩。)
  • 在创建文件时需生命文件大小。

如图:
image

二. 链式分配

原理:一个文件的信息存放在若干个不连续的物理块中,各块之间通过指针连接,前一个物理块指向下一个物理块。fat中每个文件同样只需要一项,包括文件名、起始块号和最后块号。任何一个物理块都可以加入到链中。

优点:

  • 提高磁盘的空间利用率,不存在外部碎片问题
  • 有利于文件的插入和删除。
  • 有利于文件的动态扩充。

缺点:

  • 存取速度慢,一般只适用于信息的顺序存取,不适于随机存取。
  • 查找某一块必须从头到尾沿着指针进行。
  • 可靠性问题,如指针出错。
  • 更多的寻道次数和寻道时间。
  • 链接指针占一定的空间,将多个块组成簇,按簇进行分配而不是按块进行分配。(增加了磁盘碎片)

如图:
image

image

使用FAT文件分配表法,链接分配的变种,如MS-DOS 和 OS/2.

三. 索引分配

原理:每个文件在FAT中有一个一级索引,索引包含分配给文件的每个分区的入口。文件的索引保存在单独的一个块中,FAT中该文件的入口指向这一块。

优点:

  • 保持了链接结构的优点,又解决了其缺点:按块分配可以消除外部碎片。按大小可改变的分区分配可以提高局部性。索引分配支持顺序访问文件和直接访问文件,是普遍采用的一种方式。
  • 满足了文件动态增长,插入删除的要求。(只要有空闲块)
  • 能充分利用外存空间。

缺点:

  • 较多的寻道次数和寻道空间。
  • 索引表本身带来了系统开销,如:内外存空间、存取时间。

如图:
image

磁盘调度算法

调度算法 算法思想 优点 缺点
先来先服务算法(FCFS) 按照进程请求访问磁盘的先后顺序进行调度。 简单,公平。 未对寻道进行优化,平均寻道时间较长,仅适用于磁盘请求较少的场合。
最短寻道时间优先算法(SSTF) 选择与当前磁头所在磁道距离最近的请求作为下一次服务的对象。 较 FCFS 有较好的寻道性能以及较少的寻道时间。 会导致饥饿现象
扫描(电梯调度)算法(SCAN) 在磁头当前移动方向上选择与当前磁头所在磁道距离最近的请求最为下一次服务的对象。 具有较好的寻道性能,而且防止了饥饿现象。 存在一个请求刚好被错过而需要等待很久的情形。
循环扫描算法(CSCAN) 规定磁头单向移动,如自里向外移动,当磁头移动到最外的磁道时立即返回到最里磁道,如此循环进行扫描。 兼顾较好的寻道性能,防止饥饿现象,同时解决了一个请求等待时间过长的问题。 可能出现磁臂长时间停留在某处不懂的情况(磁臂黏着)。
N-Step-SCAN 算法
(对 SCAN算法的优化)
将磁盘请求队列分成若干个长度为 N 的子队列,磁盘调度将按照 FCFS 依次处理这些子队列,而每处理一个队列时又是按照 SCAN 算法,对一个队列处理后再处理其他队列,将新请求队列放入新队列。 无磁臂黏着。
FSCAN 算法
(对 SCAN 算法的优化)
将请求队列分成两个子队列,将新出现请求磁盘 IO 的进程放入另一个子队列。 无磁臂黏着。
  • 先来先服务算法(First Come First Service, FCFS):根据进程请求访问磁盘的先后顺序进行调度。
  • 最短寻找时间优先算法(Shortest Seek Time First, SSTF):选择处理的磁道是与当前磁头所在磁道距离最近的磁道,使每次的寻找时间最短。

    该算法会产生“饥饿”现象。

  • 扫描算法(SCAN):也叫电梯算法,在磁头当前移动方向上选择与当前磁头所在距离最近的请求作为下一次服务的对象。

    实际上是在 SSTF 算法的基础上规定了磁头运动的方向。

  • 循环扫描算法(Cyclic SCAN, C-SCAN):在SCAN算法的基础上规定磁头单向移动来提供服务,到达磁盘端点返回时,直接快速返回起始端。

    若磁头移动到最远端的请求后,即刻返回,而不是到达端点再返回,则将改进后的SCAN算法和C-SCAN算法称为LOOK算法和C-LOOK算法。

先来先服务算法(First Come First Service, FCFS)

先来先服务算法(First Come First Service, FCFS):根据进程请求访问磁盘的先后顺序进行调度。

最短寻找时间优先算法(Shortest Seek Time First, SSTF)

最短寻找时间优先算法(Shortest Seek Time First, SSTF):选择处理的磁道是与当前磁头所在磁道距离最近的磁道,使每次的寻找时间最短。

该算法会产生“饥饿”现象。

扫描算法(SCAN)

扫描算法(SCAN):也叫电梯算法,在磁头当前移动方向上选择与当前磁头所在距离最近的请求作为下一次服务的对象。

实际上是在 SSTF 算法的基础上规定了磁头运动的方向。

循环扫描算法(Cyclic SCAN, C-SCAN)

循环扫描算法(Cyclic SCAN, C-SCAN):在SCAN算法的基础上规定磁头单向移动来提供服务,到达磁盘端点返回时,直接快速返回起始端。

若磁头移动到最远端的请求后,即刻返回,而不是到达端点再返回,则将改进后的SCAN算法和C-SCAN算法称为LOOK算法和C-LOOK算法。

设备管理(I/O 管理)

I/O 控制方式

  • 程序 I/O 方式:计算机从外部设备读取数据到存储器,每次读取一个字的数据。对读入的每个字,CPU 需要对外设状态进行循环检查,直到确定该字已经在 I/O 控制器的数据寄存器中。

    该方式适用于早期的无中断计算机系统中。
    CPU 和 I/O 设备只能串行工作,导致CPU的利用率相当低。

  • 中断驱动 I/O 控制方式:允许 I/O 设备主动打断 CPU 的运行并请求服务,从而使 CPU 在对 I/O 控制器发送命令后可以做其他工作。

    该方法普遍用于现代的计算机系统中。
    由于数据中每个字在存储器与 I/O 控制器之间的传输都必须经过 CPU,仍然会消耗 CPU 较多的时间。

  • DMA I/O 控制方式:(CPU 与 I/O 并行)
    I/O 设备和内存之间开辟直接的数据交换通路,数据的基本单位是数据块,所传送的数据是从设备直接送入内存;
    或者相反,仅在传送数据块的开始和结束时需要 CPU 干预,数据传送是在 DMA 控制器的控制下完成的

    该方法适用于 I/O 设备为块设备时和主机进行数据交换。

    块设备是 I/O 设备中的一类,是将信息存储在固定大小的块中,每个块都有自己的地址,还可以在设备的任意位置读取一定长度的数据,如 硬盘、U 盘、SD 卡等。

    直接内存存取(Direct Memory Access, DMA)是所有现代电脑的重要特色,它允许不同速度的硬件装置来沟通,而不需要依赖于 CPU 的大量中断负载。
    否则,CPU 需要从来源把每一片段的资料复制到暂存器,然后把它们再次写回到新的地方。在这个时间中,CPU 对于其他的工作来说就无法使用。

  • I/O 通道控制方式:是 DMA 方式的发展,只在一组数据的传输开始和结束时需要 CPU 干预,可以实现 CPU、通道和 I/O 设备三者的并行操作。

    该方式适用于设备与主机进行数据交换是一组数据块的情况,使用该方法要求系统必须配置相应的通道及通道控制器。

缓冲区

引入缓冲区的目的是什么?

  • 缓和 CPU 与 I/O 设备间速度不匹配的矛盾;
  • 减少对 CPU 的中断频率,放宽对 CPU 中断响应时间的限制;
  • 解决基本数据单元大小(即 数据粒度)不匹配的问题;
  • 提高 CPU 和 I/O 设备之间的并行性。
posted @ 2020-02-07 21:15  Nemo&  阅读(3112)  评论(0编辑  收藏  举报