操作系统:考点总结

填空题、判断题、简答题(10)、设计题、编程题(IPC)


引论

操作系统所处的位置

多数计算机由两种运行模式:内核态(管态)和用户态(目态)。软件中最基础的部分是操作系统,它运行在内核态,在这个模式中,操作系统具有对所有硬件的完全访问权,可以执行机器能够运行的任何指令。软件的其余部分运行在用户态下,只使用机器指令中的一个子集。用户接口程序(shell 或 GUI)处于用户态程序中的最低层次,允许用户运行其他程序。操作系统运行在裸机之上,为所有其他软件提供基础的运行环境。操作系统中的程序由硬件进行保护,防止用户试图对其进行修改。

什么是操作系统

操作系统主要有两个方面的重要作用:管理系统中的各种资源,并为用户提供良好的界面。

操作系统有两个基本上独立的任务:为应用程序提供一个资源集的清晰抽象,并管理这些硬件资源。

操作系统是硬件的扩展板,是资源的管理器。

  • 作为扩展机器:例如,硬盘驱动。
  • 作为资源管理者:资源管理包括用两种不同的方式实现多路复用(共享)资源:时间复用和空间复用。

操作系统的历史

  1. 真空管和穿孔卡片
  2. 晶体管和批处理系统
  3. 集成电路和多道程序设计(SPOOLing)
  4. 个人计算机

SPOOLing 假脱机

同时的外部设备联机操作(Simultaneous Peripheral Operation On-Line)

假脱机是多道程序设计系统中处理独占 I/O 设备的一种方法。

SPOOLing 系统的三大组成部分:

  1. 输入井和输出井
  2. 输入缓冲和输出缓冲
  3. 输入进程和输出进程

若有进程要求对它打印输出时,SPOOLing系统并不是将这台打印机直接分配给进程,而是在共享设备(磁盘或磁鼓)上的输出SPOOLing存储区中为其分配一块存储空间,进程的输出数据以文件形式存放于此。各进程的数据输出文件形成了一个输出队列,由输出SPOOLing系统控制这台打印机进程,依次将队列中的输出文件实际打印输出。在SPOOLing 系统中,实际上并没有为任何进程分配,而只是在输入井和输出井中,为进程分配一存储区和建立一张I/O请求表。这样,便把独占设备改造为共享设备。

技术特点

  1. 提高了I/O速度。从对低速I/O设备进行的I/O操作变为对输入井或输出井的操作,如同脱机操作一样,提高了I/O速度,缓和了CPU与低速I/O设备速度不匹配的矛盾。
  2. 设备并没有分配给任何进程。在输入井或输出井中,分配给进程的是一存储区和建立一张I/O请求表。
  3. 实现了虚拟设备功能。多个进程同时使用一独享设备,而对每一进程而言,都认为自己独占这一设备,不过,该设备是逻辑上的设备。

工作原理

1、SPOOLing的含义是什么?试述SPOOLing系统的特点、功能以及控制过程。

答:SPOOLing是Simultaneous Peripheral Operation On-Line(即外部设备联机并行操作)的缩写,它是关于慢速字符设备如何与计算机主机交换信息的一种技术,通常称为"假脱机技术"。SPOOLing技术是在通道技术和多道程序设计基础上产生的,它由主机和相应的通道共同承担作业的输入输出工作,利用磁盘作为后援存储器,实现外围设备同时联机操作。SPOOLing系统由专门负责I/O的常驻内存的进程以及输入井、输出井组成;它将独占设备改造为共享设备,实现了虚拟设备功能。

2、SPOOLing技术如何使一台打印机虚拟成多台打印机?

答:将一台独享打印机改造为可供多个用户共享的打印机,是应用SPOOLing技术的典型实例。具体做法是:系统对于用户的打印输出,但并不真正把打印机分配给该用户进程,而是先在输出井中申请一个空闲盘块区,并将要打印的数据送入其中;然后为用户申请并填写请求打印表,将该表挂到请求打印队列上。若打印机空闲,输出程序从请求打印队首取表,将要打印的数据从输出井传送到内存缓冲区,再进行打印,直到打印队列为空。

计算机硬件简介

  • 处理器:从内存中取出指令并执行之。
  • 存储器:寄存器、高速缓存、主存、磁盘。
  • 磁盘
  • I/O设备:CPU 和存储器不是操作系统唯一需要管理的资源。
  • 总线
  • BIOS(基本输入输出系统,Basic Input Output Stream)

操作系统概念

进程

进程本质上是正在执行的一个程序。与每个进程相关的是地址空间,这是从某个最小值的存储位置(通常是零)到某个最大值的存储位置的列表。

与一个进程有关的所有信息,除了该进程自身地址空间的内容以外,均存放在操作系统的一张表中,称为进程表,进程表是数组或链表结构,当前存在的每个进程都要占用其中的一项。

一个挂起的进程包括:进程的地址空间(磁芯映像,纪念过去使用的磁芯存储器,即主存),以及对应的进程表项(其中包括寄存器以及稍后重启该进程所需要的许多其他信息)。

与进程管理有关的最关键的系统调用是那些进行进程创建和进程终止的系统调用。

一个进程能够创建一个或多个子进程,而这些子进程又可以创建子进程,得到进程树。合作完成某些作业的相关进程经常需要彼此通信以便同步它们的行为,这种通信称为进程间通信

由于定时器到期,或各种由硬件检测出来的陷阱,如非法指令或使用了无效地址,操作系统会向该进程发送一个警告信号,此信号引起该进程暂时挂起,系统将其寄存器的值保存到堆栈,并开始运行一个特别的信号处理过程,如重新发送可能丢失的信息。

系统管理器授权每个进程使用一个给定的UID。每个被启动的进程都有一个启动该进程的用户 UID。子进程拥有和父进程一样的 UID。用户可以是某个组的成员,每个组也有一个 GID。在 UNIX 中,有一个 UID 称为超级用户,在 Windows 中为管理员,它具有特殊的权力,可以违背一些保护规则。

地址空间

较复杂的操作系统允许在内存中同时运行多道程序。为了避免它们互相干扰,需要有某种保护机制。虽然这种机制必然是硬件形式的,但是由操作系统掌控。

管理进程的地址空间同样重要,在本质上,操作系统创建了一个地址空间的抽象,作为进程可以引用地址的集合。该地址空间与机器的物理内存解耦,可能大于也可能小于该地址空间,操作系统可以把部分地址空间装入主存,部分留在磁盘上,并且在需要时来回交换它们。对地址空间和物理空间的管理组成了操作系统功能的一个重要部分。

文件

目录层结构中的每一个文件都可以通过从目录的顶部即根目录开始的路径名来确定。

在实例中,每个进程有一个工作目录,对于没有斜线开头给出绝对地址的路径,将在这个工作目录下寻找。进程可以通过使用系统调用指定新的工作目录,从而变更其工作目录。

提供特殊文件是为了使 I/O 设备看起来像文件一般,这样,I/O 设备可以像使用系统调用读写文件一样进行读写。

  • 块特殊文件:指那些由可随机存取的块组成的设备,如磁盘等。
  • 字符特殊文件:用于打印机、调制解调器和其他接收或输出字符流的设备。

按照惯例,特殊文件保存在/dev目录中。

管道是一种虚文件,它可连接两个进程。这样在 UNIX 中两个进程之间的通信就非常类似于普通文件的读写了。而且若进程想发现它所写入的输出文件不是真正的文件而是管道,则需要特殊的系统调用。

输入/输出

某些 I/O 软件是设备独立的,即这些 I/O软件部分可以同样应用于许多或者全部的 I/O 设备上。

I/O 软件的其他部分,如设备驱动程序,是专门为特定的 I/O 设备设计的。

保护

rwx 位:所有者、其他组成员、其他人。

shell

操作系统是进行系统调用的代码。编辑器、编译器、汇编程序、链接程序、效用程序以及命令解释器等都不是操作系统的组成部分。

shell 本身不是操作系统的一部分,但它体现了许多操作系统的特性,并很好地说明了系统调用的具体用法。shell 同时也是终端用户与操作系统之间的接口。现在,很多个人计算机使用 GUI,GUI 与 shell 类似,GUI 只是一个运行在操作系统顶部的程序。

系统调用

任何单 CPU 计算机一次只能执行一条指令。如果一个进程正在用户态运行一个用户程序,并且需要一个系统服务,比如从一个文件读取数据,那么它就必须执行一个陷阱(trap)或系统调用(system call)指令,将控制转移到操作系统。操作系统接着通过参数检查找出所需要的调用进程。然后,它执行系统调用,并把控制返回给在系统调用后面跟随着的指令。在某种意义上,进行系统调用就像进行一个特殊的过程调用,但是只有系统调用可以进入内核,而过程调用则不能。

TRAP 指令实际上与过程调用指令非常类似,它们后面都跟随一个来自远处位置的指令,以及供以后使用的一个保存在栈中的返回地址。然而,TRAP 指令与过程指令存在两个方面的差别

  • TRAP 指令的副作用是切换到内核态,而过程调用指令并不改变模式。
  • 不像给定过程所在的相对或绝对地址那样,TRAP 指令不能跳转到任意地址上。根据机器的体系结构,或者跳转到一个单固定地址上,或者指令中有一8位长的字段,它给定了内存中一张表格的索引,这张表格中含有跳转地址。

跟随在 TRAP 指令后的内核代码开始检查系统调用编号,然后分派给正确的系统调用处理器。

此时,系统调用处理器运行。

一旦系统调用处理器完成其工作,控制可能会在跟随 TRAP 指令后面的指令中返回给用户空间库过程。

这个过程接着以通常的过程调用返回的方式,返回到用户程序。

什么是陷阱指令(TRAP)?在操作系统中解释它的用途?

答:TRAP 是由于系统调用引起处理机中断的指令。在系统调用中,TRAP 负责由用户模式切换为内核模式,并将返回地址保存至堆栈中以备后用。

读取日期-时钟指令可以在非内核态使用
读取用户地址空间可以在非内核态使用

陷阱指令和中断的区别?

答:陷阱指令可以使执行流程从用户态陷入内核;而中断是由外部事件导致并且它发生的时间是不可预测的。外部事件主要是指时钟中断,硬件中断等。所以说中断的主要作用是完成进程间切换,从而支持CPU和设备之间的并行。

系统调用的目的是?

答:操作系统编制了许多不同功能的子程序,供用户程序执行中调用。这些由操作系统提供的子程序称为系统功能调用,简称系统调用。

操作系统特性

  • 并发
  • 共享
  • 虚拟化
  • 异步

中断、异常和陷入

  • 中断:是为了设备与CPU之间的通信。
  • 异常:异常是由当前正在执行的进程产生。

中断的两种方式:外部和陷入:

  • interrupt 即外中断,指来自处理机和内存外部的中断,包括 I/O 设备发出的 I/O中断、外部信号中断、各种定时器引起的时钟中断以及调试程序中设置的断点等引起的调试中断等。
  • trap 即内中断,主要指在处理机和内存内部产生的中断。它包括程序运算引起的各种错误。软中断是通信进程之间用来模拟硬中断的一种信号通信方式。

中断和陷阱的主要区别:

  1. 陷阱通常由处理机正在执行的现行指令引起,而中断则是由与现行指令无关的中断源引起的。
  2. 陷阱处理程序提供的服务为当前进程所用,而中断处理程序提供的服务则不是为了当前进程的。
  3. CPU 在执行完一条指令之后,下一条指令开始之前响应中断,而在一条指令执行中也可以响应陷阱。
  4. 在有的系统中,陷入处理程序被规定在各自的进程上下文中执行,而中断处理程序则在系统上下文中执行。

进程与线程

进程模型

一个进程就是一个正在执行程序的实例,包括程序计数器、寄存器和变量的当前值。

进程和程序的区别:一个进程是某种类型的一个活动,它有程序、输入、输出以及状态。单个处理器可以被若干进程共享,它使用某种调度算法决定何时停止一个进程的工作,并转而为另一个进程提供服务。如果一个程序运行了两遍,则算作两个进程。

进程的创建

4种主要事件会导致进程的创建:

  1. 系统初始化:前台进程,后台进程(守护进程,daemon)。
  2. 正在运行的程序执行了创建进程的系统调用。
  3. 用户请求创建一个新的进程。
  4. 一个批处理作业的初始化。

UNIX:只有一个系统调用可以用来创建新进程:fork。这个系统调用会创建一个与调用进程相同的副本。通常,子进程接着执行 execve 或一个类似的系统调用,以修改其内存映像并运行一个新的程序。(之所以要安排两步建立进程,是为了在 fork 之后但在 execve 之前允许孩子进程处理其文件描述符,这样可以完成对标准输入文件、标准输出文件和标准错误文件的重定向。)

文件描述符:UNIX 内核(kernel)利用文件描述符(file descriptor)来访问文件。文件描述符是非负整数。打开现存文件或新建文件时,内核会返回一个文件描述符。读写文件也需要使用文件描述符来指定待读写的文件。

Windows:一个 Win32 函数调用 CreateProcess 既处理进程的创建,也负责把正确的程序装入新的进程。

在 UNIX 和 Windows 中,进程创建之后,父进程和子进程由各自不同的地址空间。在 UNIX 中,子进程的初始地址空间是父进程的一个副本,但是这里涉及两个不同的地址空间,不可写的内存区是共享的。或者,子进程共享父进程的所有内存,但这种情况下内存通过写时复制(copy-on-write)共享,这意味着一单两者之一想要修改部分内存,则这块内存首先被明确地复制,以确保修改发生在私有内存区域。在 Windows 中,从一开始父进程的地址空间和子进程的地址空间就是不同的。

进程的终止

进程的终止通常由下列条件引起:

  1. 正常退出(自愿)
  2. 出错退出(自愿,进程发现了严重错误)
  3. 严重错误(非自愿,由进程引起的错误)
  4. 被其他进程杀死(非自愿)

UNIX:kill
Windows:TerminateProcess

进程的层次结构

  • UNIX

在 UNIX 中,进程和它的所有子进程以及后裔共同组成一个进程组。当用户从键盘发出一个信号时,该信号被送给当前与键盘相关的进程组中的所有成员(它们通常是在当前窗口创建的所有活动进程)。每个进程可以分别捕获该信号、忽略该信号或采取默认的动作,即被该信号杀死。在 UNIX 的整个系统中,所有的进程都属于以 init 为根的一棵树。

  • Windows

在 Windows 中没有进程层次的概念,所有的进程都是地位相同的。唯一类似于进程层次的暗示是在创建进程的时候,父进程得到一个特别的令牌(句柄),该句柄可以用来控制子进程。但是,它有权把这个令牌传送给某个其他进程,这样就不存在进程层次了。在 UNIX 中,进程不能剥夺其子进程的“继承权 ”。

进程的状态

在这里插入图片描述

  1. 进程因为等待输入而被阻塞
  2. 调度程序选择另一个进程
  3. 调度程序选择这个进程
  4. 出现有效输入

在这里插入图片描述

基于进程的操作系统中最底层的是中断和调度处理,在该层之上的是顺序进程。

操作系统的最底层是调度程序,在它上面有许多进程。所以有关于中断处理、启动进程和停止进程的具体细节都隐藏在调度程序中。实际上,调度系统是一段非常短小的程序,操作系统的其他部分被简单地组织成进程的形式。

进程的实现

为实现进程模型,操作系统维护着一张表格(进程表)。每个进程占用一个进程表项(进程控制块),该表项包含了进程状态的重要信息,字段分三类:进程管理、存储管理和文件管理。其中进程的 PID 代表进程的唯一存在。

多道程序设计模型

在这里插入图片描述

CPU 利用率 = 1-p^n

p:一个进程等待 I/O 操作的时间与其停留在内存中时间的比。
n:进程的数目。

e.g. 假设一个内存 8GB 的计算机,操作系统占用 2GB,每个进程占用 2GB,且进程有 80% 的时间用于 I/O 等待,则内存空间中最多允许同时运行 3 个进程,CPU 利用率为 1-0.8^3 ≈ 49%。若增加 8GB 的内存,CPU 利用率可增加到 79%,再增加 8GB 内存可使 CPU 利用率增加到 91%。所以增加一次内存就够了。

线程的使用

需要多线程的主要原因:在许多应用中同时发生着多种活动,其中某些活动随着时间的推移会被阻塞,通过将这些应用程序分解成可以准并行运行的多个顺序线程,程序设计模型会变得更简单。

多线程的优点

  1. 某些应用需要并行实体拥有共享同一个地址空间和所有可用数据的能力,而这正是多进程模型(它们具有不同的地址空间)所无法表达的。
  2. 由于线程比进程更轻量级,所以它们比进程更容易(即更快)创建,也更容易撤销。在有大量线程需要动态和快速修改时,具有这一特性是很有用的。
  3. 若多个线程都是 CPU 密集型的,那么并不能获得性能上的增强,但是如果存在着大量的计算的大量的 I/O 处理,拥有多个线程允许这些活动彼此重叠进行,从而会加快应用程序执行的速度。
  4. 在多 CPU 系统中,多线程是有益的,在这样的系统中,真正的并行有了实现的可能。

进程和线程的区别

  • 地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
  • 资源拥有:同一进程内的线程共享本进程的资源如内存、I/O、cpu等,但是进程之间的资源是独立的。
    • 一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
    • 进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程。
  • 执行过程:每个独立的进程程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
  • 进程是处理器调度的基本单位,但是线程不是。
  • 两者均可并发执行。

经典的线程模型

  • 一个进程中的所有线程共享的内容

    • 地址空间
    • 全局变量
    • 打开文件
    • 子进程
    • 即将发生的定时器
    • 信号与信号处理程序
    • 账户信息
  • 每个线程中的内容

    • 程序计数器
    • 寄存器
    • 堆栈
    • 状态

线程的实现

用户空间

把整个线程包放在用户空间中,内核对线程包一无所知。从内核角度考虑,就是按正常的单线程进程的方式管理。

在用户空间管理线程时,每个进程需要有其专用的线程表,用来跟踪该进程中的线程。该线程表由运行时系统管理,当一个线程转换到就绪状态或阻塞状态时,在该线程表中存放重新启动该线程所需的信息,与内核在进程表中存放进程的信息完全一样。

优点:
1. 用户级线程包可以在不支持线程的操作系统上实现。
2. 保存该线程状态的过程和调度程序都只是本地过程,所以启动它们比进行内核调用效率更高。而且不需要陷入内核,不需要上下文切换,也不需要对内存高速缓存进行刷新,这就使得线程调度非常快捷。
3. 允许每个进程有自己定制的调度算法。
缺点:
1. 很难实现阻塞系统调用,因为一个线程使用阻塞调用的时候要避免被阻塞的线程影响其他的线程。
2. 缺页中断问题:如果有一个线程引起页面故障,内核由于不知道有线程的存在,通常会把整个进程阻塞直到磁盘 I/O 完成为止,尽管其他线程是可以运行的。
3. 如果一个线程开始运行,那么在该进程中的其他线程就不能运行,除非第一个线程自动放弃 CPU。因为在一个单独的进程内部,没有时钟中断,所以不可能使用轮转调度的方式调度线程。

内核

考虑内核支持和管理线程的情形,此时不再需要运行时系统了。每个进程中也没有线程表,但是在内核中有用来记录系统中所有线程的线程表。当某个线程希望创建一个新线程或撤销一个已有线程时,它进行一个系统调用,这个系统调用通过对线程表的更新完成线程创建或撤销工作。

  • 所有能够阻塞线程的调用都以系统调用的形式实现,这与运行时系统相比,代价非常大。内核线程不需要新的、非阻塞的系统调用。
  • 由于在内核中创建或撤销线程的代价比较大,可以在线程被撤销时标志其为不可用,但是其内核数据结构没有受到影响,稍后再创建一个新线程的时候,就重新启动这个旧线程。而用户级线程管理的代价很小,没有必要进行这项工作。
  • 信号是发送给进程而不是线程的,线程可以注册它们“感兴趣”的信号,但是处理起来还有很多问题。

混合实现

使用内核级线程,然后将用户级线程与某些或者全部内核线程多路复用起来。在这个模型中,每个内核级线程有一个可以轮流使用的用户级线程集合。

进程间通信的一些基本概念

  • 竞争条件:两个或多个进程读写某些共享数据,而最后的结果取决于进程运行的精确时序,称为竞争条件。

  • 临界区:我们把对共享内存进行访问的程序片段称作临界区域(critical region)或临界区。即临界区是一段代码。如果我们能够适当地安排,使得两个进程不可能同时处于临界区中,就能够避免竞争条件。

  • 互斥:以某种手段确保当一个进程在使用一个共享变量或文件时,其他进程不能做同样的操作。

  • 信号量(Semaphore):使用一个整型变量来累计唤醒次数,取值可以为0或者正值。信号量的操作均作为一个单一的、不可分割的原子操作完成。保证一旦一个信号量操作开始,则在该操作完成或阻塞之前,其他进程均不允许访问该信号量。

  • 原语:由若干条指令组成的,用于完成一定功能的一个过程。具有不可分割性,即原语的执行必须是连续的,在执行过程中不允许被中断。

  • 忙等待:连续测试一个变量直到某个值出现为止。用于忙等待的锁,称为自旋锁

  • 互斥量(Mutex):如果不需要信号量的计数能力,可以使用信号量的一个简化版本。

  • 条件变量:允许线程由于一些未达到的条件而阻塞。绝大部分情况下与互斥量一起使用。

  • 优先级倒置(priority inversion):在某一时刻,Low 处于临界区中,此时 High 变到就绪态。现在 High 开始忙等待,但是由于 High 优先级高,所以 Low 不会被调度也就无法离开临界区,所以 High 将永远忙等待下去。

临界区互斥访问解决方案的4个条件

  1. 任何两个进程不能同时处于临界区。
  2. 不应对 CPU 的速度和数量做任何假设。
  3. 临界区外运行的进程不得阻塞其他进程。
  4. 不得使进程无限期等待进入临界区。

忙等待的互斥访问

  1. 屏蔽中断

在单处理器系统中,最简单的方法是使每个进程在刚刚进入临界区后立即屏蔽所有中断,并在就要离开之前再打开所有中断。屏蔽中断后,时钟中断也被屏蔽。而 CPU 只有发生时钟中断或其他中断时才会进行进程切换,这样,在屏蔽中断之后 CPU 将不会被切换到其他进程。于是,一旦某个进程屏蔽中断之后,它就可以检查和修改共享内存,而不必担心其他进程介入。

但是把屏蔽中断的权力交给用户进程是不明智的。但是对内核来说,当它在更新变量或列表的几条指令期间将中断屏蔽是很方便的。所以结论是:屏蔽中断对于操作系统本身而言是一项很有用的技术,但对于用户进程则不是一种合适的通用互斥机制。

  1. 锁变量

设想有一个共享(锁)变量,其初始值为0。当一个进程想进入其临界区时,它首先测试这把锁。如果该锁的值为0,则该进程将其设置为1并进入临界区。若这把锁的值已经为,则该进程将等待直到其值变为0。

但是,这样还是会造成竞争条件。

  1. 严格轮换法

整型变量 turn,初始值为0,用于记录轮到哪个进程进入临界区,并检查或更新共享内存。开始时,进程0检查 turn,发现其值为0,于是进入临界区。进程1也发现其值为0,所以在一个等待循环中不断测试 turn,看其值何时变为1。

存在问题:忙等待(用于忙等待的锁,称为自旋锁,spin lock),两个进程速度不匹配,进程0在进程1在非临界区时被阻塞(违反了条件3)。

  1. Peterson 解法
#define FALSE 0
#define TRUE 1
#define N 2                 /*number of processes*/ 
shared int turn;            /*whose turn is it?*/ 
shared int interested[N];   /*all values initially 0*/ 
void enter_region(int process)
{
    int other; 
    other=1-process; 
    interested[process]=TRUE;
    turn=process;
    while(turn == process && interested[other] == TRUE);
}
void leave_region(int process) {
    interested[process]=FALSE; 
}
  1. TSL 指令,测试并加锁(test and set lock)

需要硬件支持的一种方案:TSL RX, LOCK。它将一个内存字 lock 读到寄存器 RX 中,然后在该内存地址上存一个非零值。

enter_region:
TSL REGISTER,LOCK /复制锁到寄存器并将锁设为1/
CMP REGISTER,#0 /若锁不是0则循环/
JNE enter_region
RET
leave_region:
MOVE LOCK,#0
RET

一个可替代 TSL 的指令是 XCHG,它原子性地交换了两个位置的内容。

enter_region:
MOVE REGISTER,#1
XCHG REGISTER,LOCK /交换寄存器与锁变量的内容/
CMP REGISTER,#0 /若锁不是0则循环/
JNE enter_region
RET
leave_region:
MOVE LOCK,#0
RET

信号量(semaphore)

  • Down,P 操作:若其值大于0,则将其减1并继续;若该值为0,则进程将休眠,而且测试 down 操作并未结束。
  • Up,V 操作:对信号量的值增1。如果一个或多个进程在该信号量上睡眠,无法完成先前的 down 操作,则由系统选择其中的一个并允许该进程完成它的 down 操作。于是 up 操作之后,该信号量的值仍旧是0,但在其上睡眠的进程却少了一个。

信号量解决生产者-消费者问题:

#define N 100 /*number of slots in the buffer*/
typedef int semaphore; 
semaphore mutex=1; 
semaphore empty=N; 
semaphore full=0;

void producer(void){ 
    int item;
    while(TRUE){ 
        produce_item(&item); 
        down(&empty); 
        down(&mutex); 
        enter_item(item); 
        up(&mutex); 
        up(&full);
    } 
}

void consumer(void){ 
    int item;
    while(TRUE){ 
        down(&full);
        down(&mutex); 
        remove_item(&item);
        up(&mutex); 
        up(&empty); 
        consume_item(item)
    }
}

互斥量(mutex)

如果不需要信号量的计数能力,可以使用信号量的一个简化版本,称为互斥量(mutex)。由于互斥量在实现时既容易又有效,所以互斥量在实现用户空间线程包时非常有用。

互斥量是一个可以处于两态之一的变量:解锁和加锁。

用户级线程包的 mutex_lock 和 mutex_unlock 的代码:

mutex_lock:
TSL REGISTER,MUTEX
CMP REGISTER,#0
JZE ok
CALL thread_yield ;如果互斥信号不为0,则调度另一个线程,稍后再尝试
JMP mutex_lock
ok: RET

mutex_unlock:
MOVE MUTEX,#0
RET

mutex_lock 的代码与上面 TSL 中 enter_region 的代码类似。但是当后者在进入临界区失败时,会始终重复测试锁(忙等待),而由于时钟超时的作用,会调度其他进程运行。但在用户线程中,没有时钟会停止运行时间过长的线程,所以前者需要主动放弃 CPU 给另一个线程,这样就没有忙等待。

条件变量(condition variable)

互斥量可以允许或阻塞对临界区的访问,而条件变量则允许线程由于一些未达到的条件而阻塞。

条件变量与互斥量经常一起使用。这种模式用于让一个线程锁住一个互斥量,然后当它不能获得它期待的结果时等待一个条件变量。最后另一个线程会向它发信号,使它可以继续执行。wait(condition,mutex) 原子性地调用并解锁它持有的互斥量,所以互斥量也是 wait(condition,mutex) 的参数之一。

但是条件变量不会像信号量那样存在内存中,如果将一个信号传递给一个没有线程在等待的条件变量,那么这个信号就会丢失。所以必须小心使用以防丢失信号。

管程(monitor)

管程是一种高级同步原语。是一个由过程、变量及数据结构等组成的一个集合,它们组成一个特殊的模块或软件包。进程可在任何需要的时候调用管程中的过程,但它们不能再管程之外声明的过程中直接访问管程内的数据结构。

管程有一个很重要的特性,即任一时刻管程中只能有一个活跃进程,这一特性使管程能有效地完成互斥。而进入管程时的互斥由编译器负责,一般的解决方法是引入条件变量,wait 和 signal 操作

Java 中可以使用 synchronized ,wait 和 notify 来实现管程,没有内嵌的条件变量,而且方法 wait 会被中断,需要显式表示异常处理。

消息传递(message passing)

消息传递这种进程间通信的方法使用两条原语 send(des,&msg) 和 receive(src,&msg),它们像信号量而不像管程,是系统调用而不是语言成分。send 向一个给定的目标发送一条消息,receive 从一个给定的源接收一条消息,如果没有消息可用,则接收者可能阻塞,直到一条消息到达,或者带着一个错误码立即返回。

屏障(barrier)

用于进程组,实现除非所有进程都准备就绪进入下一个阶段,否则任何进程和都不能进入。可以通过在每个阶段的结尾安置屏障(barrier)来实现。当一个进程到达屏障时,它就被屏障阻拦,直到所有进程都到达该屏障为止。屏障可以用于一组进程同步

经典的 IPC 问题

哲学家就餐问题

哲学家就餐问题对于互斥访问有限资源的竞争问题(如I/O设备)一类的建模过程十分有用。

在这里插入图片描述

#define N 5
#define LEFT (i+N-1)%N 
#define RIGHT (i+1)%N 
#define THINKING 0 
#define HUNGRY 1 
#define EATING 2 
typedef int semaphore; 
int state[N];
semaphore mutex=1; 
semaphore s[N];             // 每个哲学家一个信号量

void philosopher(int){
    while(TRUE){ 
        think();
        take_forks(i); 
        eat();
        put_forks(i); 
    }
}

void take_forks(int i){ 
    down(&mutex); 
    state[i]=HUNGRY; 
    test(i);                // 尝试获取两把叉子
    up(&mutex); 
    down(&s[i]);            // 如果得不到所需要的叉子则阻塞
}

void put_forks(int i){
    down(&mutex); 
    state[i]=THINKING;
    test(LEFT);             // 检查左边的邻居现在可以吃吗
    test(RIGHT);
    up(&mutex); 
}

void test(int i){
    if(state[i]==HUNGRY && state[LEFT]!=EATING && state[RIGHT]!=EATING){ 
        state[i]=EATING;
        up(&s[i]);
    }
}

读者-写者问题(可能考)

读者-写者问题为数据库访问建立了一个模型。多个进程同时读数据库是可以接受的,但是如果一个进程正在更新(写)数据库,则所有的其他进程都不能访问该数据库,即使读操作也不可以。

typedef int semaphore; 
semaphore mutex=1;              // 控制对 rc 的访问
semaphore db=1;                 // 控制对数据库的访问
int rc=0;                       // 正在读或者即将读的进程数目

void reader(void){ 
    while(TRUE){
        down(&mutex); 
        rc=rc+1;                // 现在多了一个读者
        if(rc==1) down(&db);    // 如果这是第一个读者
        up(&mutex); 
        read_data_base();
        
        down(&mutex); 
        rc=rc-1;                // 现在少了一个读者
        if (rc==0) up(&db);     // 如果这是最后一个读者
        up(&mutex); 
        use_data_read();
    }
}


void writer(void){ 
    while(TRUE){
        think_up_data(); 
        down(&db); 
        write_data_base(); 
        up(&db);
    }
}

睡眠的理发师问题(可能考)

有一个理发师,有一个理发椅,5个等候椅,如果没有顾客,则理发师睡觉,如果有顾客,则叫醒理发师;理发师理发时,如果有顾客过来,且有等候椅,则坐下来等候;如果没有等候椅,则离开。

#define CHAIRS 5 
typedef int semaphore; 
semaphore customers=0; 
semaphore barbers=0; 
semaphore mutex=1;
int waiting =0;

void barber(void){
    while(TRUE){ 
        down(&customers);
        down(&mutex); 
        waiting=waiting-1; 
        up(&barbers);           // 如果有顾客,理发师醒来
        up(&mutex); 
        cut_hair();
    }
}

void customer(void){ 
    down(&mutex); 
    if(waiting<CHAIRS){
        waiting=waiting+1; 
        up(&customers); 
        up(&mutex); 
        down(&barbers); 
        get_haircut();
    }else{ 
        up(&mutex);
    }
}

调度程序

当计算机系统是多道程序设计系统时,通常就会有多个进程或线程同时竞争 CPU。只要有两个或更多的进程处于就绪状态,这种情形就会发生。如果只有一个 CPU 可用,那么就必须选择下一个要运行的进程。在操作系统中,完成选择工作的这一部分称为调度程序,该程序使用的算法称为调度算法

  • 调度的三个层次

    • 作业调度:从外存后备队列中选择作业进入内存就绪队列。
    • 存储调度:在内存和外存对换区之间按照给定的策略选择进程对换。
    • 进程和线程调度:从就绪队列中选择一个进程来执行并由分派程序分配处理机。
  • 根据如何处理时钟中断,可以把调度算法分为两类:

    • 抢占式
    • 非抢占式

何时调度

  1. 在创建一个新进程之后,需要决定是运行父进程还是运行子进程。(任意决定,调度程序可以选择)
  2. 在一个进程退出时必须所处调度决策。
  3. 当一个进程阻塞在 I/O 和信号量上或由于其他原因阻塞时,必须选择另一个进程运行。
  4. 在一个 I/O 中断发生时,必须做出调度决策。

调度算法

批处理系统中的调度

  1. 先来先服务
  2. 最短作业优先
  3. 最短剩余时间优先

交互式系统中的调度

  1. 轮转调度
  2. 优先级调度:在各个优先级类中使用轮转调度
  3. 多级队列:属于最高优先级类的进程运行一个时间片,次高运行2个,再次运行4个。。。
  4. 最短进程优先:根据进程过去的行为进程预测,并执行估计运行时间最短的那个,可以和之前的运行时间做加权和来预测
  5. 保证调度:确保 n 个进程中每个进程占用 CPU 的时间约为 1/n
  6. 彩票调度:反应迅速,所有的进程都是平等的,但是可以给更重要的进程额外的彩票
  7. 公平分享调度:以进程的所有者均分 CPU 时间而不论进程数目

实时系统中的调度

  • 分类1:
    • 硬实时:必须满足绝对的截止时间
    • 软实时:虽然不希望偶尔错失截止时间,但是可以容忍
  • 分类2:
    • 周期性:事件以规则的时间间隔发生
    • 非周期性:事件发生的时间不可预知
  • 分类3:
    • 静态调度:在系统开始运行前作出调度决策
    • 动态调度:在运行过程中进行调度决策
  1. 最小延误调度
  2. 优先级调度
  3. 速率单调调度
  4. 最早截止优先调度
  5. 成比例分享调度
  6. POSIX 实时调度

资源

资源:软件、硬件、信息,我们把这类需要排他性使用的对象称为资源。当某个资源有多个实例时,其中任何一个都可以用来满足对资源的请求。简单来说,资源就是随着时间的推移,必须能获得、使用以及释放的任何东西。

资源分为两类:可抢占的和不可抢占的。

可抢占资源可以从拥有它的进程中抢占而不会产生任何副作用,存储器就是一类可抢占的资源。

不可抢占资源是指在不引起相关的计算失败的情况下,无法把它从占有它的进程处抢占过来。

使用一个资源所需要的时间顺序可以用抽象的形式表示如下:

  1. 请求资源
  2. 使用资源
  3. 释放资源

死锁定义

死锁的规范定义如下:如果一个进程集合中的每个进程都在等待只能由该进程集合中的其他进程才能引发的事件,那么,该进程集合就是死锁的。

资源死锁:竞争资源的进程集合按一定顺序运行形成死锁。

资源死锁的四个必要条件

  1. 互斥条件。每个资源要么已经分配给了一个进程,要么就是可用的。
  2. 占有和等待条件。已经的到了某个资源的进程可以再请求新的资源。
  3. 不可抢占条件。已经分配给一个进程的资源不能强制性地被抢占,它只能被占有它的进程显式地释放。
  4. 环路等待条件。死锁发生时,系统中一定有由两个或两个以上的进程组成一条环路,该环路中的每个进程都在等待着下一个进程所占有的资源。

四种死锁处理策略

  1. 忽略该问题。
  2. 检测死锁并恢复。让死锁发生,检测它们是否发生,一旦发生死锁,采取行动解决问题。
  3. 仔细对资源进行分配,动态地避免死锁。
  4. 通过破坏死锁的四个必要条件之一,防止死锁的产生。

死锁检测和恢复

死锁检测

每种类型一个资源的死锁检测

对于每种资源类型只有一个资源,可以对这样的系统构造一张资源分配图,如果这张图包含了一个或一个以上的环,那么死锁就存在,在此环中的任何一个进程都是死锁进程。

  • 如下的资源分配图:○表示进程,□表示资源,□→○表示进程占用资源,○→□表示进程请求资源

在这里插入图片描述

死锁检测算法是依次将每个节点作为一棵树的根节点,并进行深度优先搜索,如果碰到了已经遇到过的节点,那么就算找到了一个环。

每种类型多个资源的死锁检测

  • 如下的资源矩阵:E 是现有资源向量,A 是可用资源向量,C 代表当前分配矩阵(第 n 行是进程 n 当前已分配到的资源),R 代表请求矩阵(第 n 行是进程 n 需要的资源)

在这里插入图片描述

死锁检测算法如下:

  1. 寻找一个没有标记的进程 Pi,对它而言 R 矩阵的第 i 行向量小于等于 A。
  2. 如果找到了这样一个进程,那么将 C 矩阵的第 i 向量加到 A 中,标记该进程,并转到第1步,表示该进程可以得到所需资源并运行完成。
  3. 如果没有这样的进程,那么算法终止。
  4. 算法结束时,所有没有标记过的进程都是死锁进程。

死锁恢复

  • 利用抢占恢复
  • 利用回滚恢复
  • 通过杀死进程恢复

死锁避免

资源轨迹图

  • 资源轨迹图:进程 A 在 I1 到 I3 使用打印机,I2 到 I4 使用绘图仪;进程 B 在 I6 到 I8 使用打印机,I5 到 I7 使用绘图仪

在这里插入图片描述

图中的阴影部分表示两个进程同时使用打印机或绘图仪,而这是不可能实现的,所以不可能进入该区域。

如果系统一旦进入由 I1、I2、I5 和 I6 组成的矩形区域,那么最后一定会到达 I2 和 I6的交叉点,此时产生死锁。

安全状态和不安全状态

  • 安全状态:状态 a 为安全状态

在这里插入图片描述

  • 不安全状态:状态 b 为不安全状态

在这里插入图片描述

  • 安全状态,不安全状态和死锁

在这里插入图片描述

不安全状态并不是死锁,在不安全状态系统还能运行一段时间,甚至还有一些进程可以运行完成。

安全状态和不安全状态的区别是:从安全状态出发,系统能够保证所有进程都能完成;而从不安全状态出发,就没有这样的保证。

单个资源的银行家算法

Dijkstra 提出了一种能够避免死锁的调度算法,称为银行家算法,这是上面通过检测死锁算法的扩展。

  • 单个资源的银行家算法:Has 表示已有数量,Max 表示最大需求,下图中 a,b安全,c 不安全

在这里插入图片描述

银行家算法就是对每一个请求进行检查,检查如果满足这一请求是否会达到安全状态。若是,那么就满足该需求;否则,就推迟对这一请求的满足。

为了检查状态是否安全,银行家需要考虑他是否有足够的资源满足某一客户。如果可以,那么这笔贷款是能够收回的,并且接着检查最接近最大限额的一个客户,以此类推。如果所有投资最终都能被收回,那么该状态是安全的,最初的请求可以批准。

多个资源的银行家算法

  • 多个资源的银行家算法:左矩阵表示已分配资源,右矩阵表示仍然需要的资源,E 表示现有资源,P 表示已分配资源,A 表示可用资源

在这里插入图片描述

同上面资源矩阵的检测算法。

死锁预防

破坏互斥条件

如果一个资源不被一个进程独占,那么死锁肯定不会产生。

通过采用假脱机(打印机)技术可以允许若干个进程同时产生输出。该模型中唯一真正请求物理打印机的进程是打印机守护进程,由于守护进程不会请求别的资源,所以不会因打印机而产生死锁。

可以考虑为所有资源建立一个资源池

破坏占有并等待条件

禁止已持有资源的进程再等待其他资源。

  • 实现方法一:规定所有进程在开始执行前请求所需的全部资源。
  • 实现方法二:当一个进程请求资源时,先暂时释放其当前占用的所有资源,然后再尝试一次获得所需的全部资源。

破坏不可抢占条件

抢占正在打印的打印机可能会造成混乱,但是可以对这类资源可以采用虚拟化的方式来避免发生这类情况,假脱机打印机向磁盘输出。

破坏环路等待条件

  • 实现方法一:保证每个进程在任何时刻只能占用一个资源,如果要请求另外一个资源,它必须先释放第一个资源。
  • 实现方法二:将所有资源统一编号,进程可以在任何时刻提出资源请求,但是所有请求必须按照资源编号顺序(升序)提出。

有关死锁的其他问题

两阶段加锁

在第一阶段,进程试图对所有所需的记录进行加锁,一次锁一个记录。如果第一阶段加锁成功,就开始第二阶段,完成更新然后释放锁。在第一阶段并没有做实际的工作。如果在第一阶段某个进程需要的记录已经被加锁,那么该进程释放它所有加锁的记录,然后重新开始第一阶段。

通信死锁

资源死锁是最普遍的一种类型,但不是唯一的一种。

通信死锁发生在通信系统(e.g. 网络)中,一个普遍的情形是:进程 A 向进程 B 发送请求信息,然后阻塞直至 B 回复,但是请求信息在网络中丢失,A 将阻塞以等待回复,而 B 也会阻塞等待一个向其发送命令的请求,因此发送死锁。

可以设置适当的超时机制来解决通信死锁。

活锁

在某些情况下,当进程意识到它不能获取所需要的下一个锁时,就会释放已经得到的锁,等待1ms,然后再尝试一次,但是,如果另一个进程在相同的时刻做了相同的操作,那么两个进程就像两个人在一条路相遇并同时给对方让路一样,相同的步调将导致双方都无法前进。

保持饥饿:SJF

一些管理资源的策略可能使一些进程永远得不到服务,比如最小作业优先策略,可以使用先进先出的分配策略来避免饥饿。


内存管理

存储管理

存储器管理的对象是主存,也称内存。它的主要功能包括分配和回收主存空间、提高主存利用率、扩充主存、对主存信息实现有效保护。

存储管理方案的主要目的是解决多个用户使用主存的问题,其存储管理方案主要包括:

  • 分区存储管理
    • 静态分区
    • 可变分区
    • 可重定位分区
  • 分页存储管理
  • 分段存储管理
  • 段页式存储管理
  • 虚拟存储管理。

无存储器抽象

在没有存储器抽象的系统中实现并行的一种方法是使用多线程来编程。由于在引入线程时就假设一个进程中的所有线程对同一内存映像都可见,那么实现并行也就不是问题,

在不使用存储器抽象的情况下运行多个程序

操作系统只需要把当前内存中所有内容保存到磁盘文件中,然后把下一个程序读入到内存中再运行即可。只要在某一个时间内存中只有一个程序,那么就不会发生冲突。

在特殊硬件的帮助下(防止用户进程之间相互干扰),即使没有交换功能,并发地运行多个程序也是可能的。但这样会有重定位问题

  • 保护:(IBM 360)给内存块标记上一个保护键,并且比较执行进程的键和其访问的每个内存字的保护键。
  • 静态重定位:当一个程序被装载到地址16384时,常数16384被加到每一个程序地址上。装载器还需要一定的方法来辨别地址和常数。

地址空间

把物理地址暴露给进程会带来两个严重问题:

  1. 如果用户程序可以寻址内存的每个字节,那么它们就可以很容易地破坏操作系统。即使在只有一个用户进程运行的情况下,这个问题也是存在的。
  2. 在系统中没有对物理内存的抽象的情况下,很难实现同时运行多个程序。

要使多个应用程序同时处于内存中并且不相互影响,需要解决两个问题:保护的和重定位

比无存储器抽象时的保护和静态重定位更好的办法是创造一个新的存储器抽象:地址空间

就像进程的概念创造了一类抽象的 CPU 以运行程序一样,地址空间为程序创造了一种抽象的内存,是一个进程可用于寻址内存的一套地址集合。每个进程都有一个自己的地址空间,并且这个地址空间独立于其他进程的地址空间。

  • 动态重定位
    • 基址寄存器
    • 界限寄存器
    • 当使用基址寄存器和界限寄存器时,程序装载到内存中连续的空闲位置且装载期间无须重定位,当一个程序运行时,程序的起始物理地址装载到基址寄存器中,程序的长度装载到界限寄存器中。
    • 使用基址寄存器和界限寄存器重定位的缺点是:每次访问内存都需要进行加法和比较运算。

交换技术

有两种处理内存超载的通用方法。

  • 交换技术:即把一个进程完整调入内存,使该进程运行一段时间,然后把它存回磁盘。
  • 虚拟内存:能使程序在只有一部分被调入内存的情况下运行。

交换在内存中产生了多个空闲区(hole,也称为空洞),通过把所有的进程尽可能向下移动,有可能将这些小的空闲区合成一大块。该技术称为内存紧缩(memory compaction)。

如果进程在运行时内存需要增长,为了减少因内存不够而引起的进程交换和移动所产生的开销,当换入或移动进程时可以为它分配一些额外的内存。

  • 为可能增长的数据段预留空间;
  • 为可能增长的数据段和堆栈段预留空间。

空闲内存管理

  • 使用位图的存储管理
    • 分配单元的大小是一个重要的设计因素。内存的大小和分配单元的大小决定了位图的大小。
    • 主要问题:查找位图中指定长度的连续0串是耗时的操作。(为了把一个占 k 个分配单元的进程调入内存)
  • 使用链表的存储管理
    • 维护一个记录已分配内存段和空闲内存段的链表。其中链表的一个结点或者包含一个进程,或者是两个进程间的一块空闲区。
    • 链表中的每一个结点都包含以下域:空闲区(H)或进程(P)的指示标识、起始地址、长度和指向下一结点的指针。
    • 当按照地址顺序在链表中存放进程和空闲区时,有以下几种算法可以用来为创建的进程,或从磁盘换入的已存在的进程分配内存(假设存储管理器知道要为进程分配多少内存):
      • 首次适配
      • 下次适配:每次从上次结束的地方开始搜索。
      • 最佳适配:比首次适配算法慢,而且比首次适配和下次适配算法浪费更多的内存,因为它会产生大量无用的小空闲区。
      • 最差适配
      • 快速适配:为那些常用大小的空闲区维护单独的链表。
        • 优点:寻找一个指定大小的空闲区是十分快速的。
        • 缺点:在一个进程终止或被换出时,寻找它的相邻块并查看是否可以合并的过程非常费时。
  • 伙伴式的内存管理
    • 拆分和合并涉及到较多的链表和位图操作。
    • Buddy算法的分配原理:
      • 假如系统需要4(2*2)个页面大小的内存块,该算法就到free_area[2]中查找,如果链表中有空闲块,就直接从中摘下并分配出去。如果没有,算法将顺着数组向上查找free_area[3],如果free_area[3]中有空闲块,则将其从链表中摘下,分成等大小的两部分,前四个页面作为一个块插入free_area[2],后4个页面分配出去,free_area[3]中也没有,就再向上查找,如果free_area[4]中有,就将这16(2*2*2*2)个页面等分成两份,前一半挂如free_area[3]的链表头部,后一半的8个页等分成两等分,前一半挂free_area[2]的链表中,后一半分配出去。假如free_area[4]也没有,则重复上面的过程,直到到达free_area数组的最后,如果还没有则放弃分配。
    • Buddy算法的释放原理:
      • 内存的释放是分配的逆过程,也可以看作是伙伴的合并过程。当释放一个块时,先在其对应的链表中考查是否有伙伴存在,如果没有伙伴块,就直接把要释放的块挂入链表头;如果有,则从链表中摘下伙伴,合并成一个大块,然后继续考察合并后的块在更大一级链表中是否有伙伴存在,直到不能合并或者已经合并到了最大的块。

基础内存管理:离散分配

  • 分段
  • 分页
    • 多级页表
    • 倒排页表

为什么引入虚拟内存

需要运行的程序往往大到内存无法容纳,而且必然需要系统能够支持多个程序同时运行,即使内存可以满足其中单独一个程序的需要,总体来看它们仍然超出了内存大小。而交换技术并不一个具有吸引力的解决方案,因此引入了虚拟内存。

虚拟内存的基本思想是:每个程序拥有自己的地址空间,这个空间被分割成多个块,每个块称作一页或页面。每一页有连续的地址范围。这些页被映射到物理内存,但并不是所有的页都必须在内存中才能运行程序。当程序引用到一部分在物理内存中的地址空间时,由硬件立刻执行必要的映射。当程序引用到一部分不在物理内存中的地址空间时,由操作系统负责将缺失的部分装入物理内存并重新执行失败的指令。

虚拟内存的特点

  1. 虚拟扩充 即不是物理上而是逻辑上扩充了内存容量。

  2. 部分装入 即每个作业不是全部一次性地装入内存,而是只装入一部分。

  3. 离散分配 即不必占用连续的内存空间,而是“见缝插针”。

  4. 多次对换 即所需的全部程序和数据要分成多次调入内存。

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

  6. 对换性(交换性):是指无需在作业运行时一直常驻内存,而是允许在作业的运行过程中,进行换进和换出。

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

局部性原理

局部性原理: CPU访问存储器时,无论是存取指令还是存取数据,所访问的存储单元都趋于聚集在一个较小的连续区域中。

  • 时间局部性:如果一个信息项正在被访问,那么在近期它很可能还会被再次访问。
  • 空间局部性:在最近的将来将用到的信息很可能与现在正在使用的信息在空间地址上是临近的。

页面置换算法

  • 最优页面置换算法
    • 替换最长时间不会被访问的页
  • 先进先出页面置换算法
  • 第二次机会页面置换算法
    • 访问位(R)位0则替换,否则减1
  • 时钟页面置换算法
    • 将第二次机会页面置换算法中的链表改为环形链表
  • 最近最少使用页面置换算法(LRU,Least Recently Used)
    • 需要在内存中维护一个所有页面的链表,最近最多使用的页面在表头,最近最少使用的页面在表尾,每次访问内存时都必须更新整个链表。(找到一个页面,删除它,然后把它移动到表头)
  • 最近未使用页面置换算法(NRU,Not Recently Used)
    • 检查所有页面的 R 和 M 位,分为4类:(每次时钟中断清除 R 位)
      • 0:没有被访问,没有被修改。
      • 1:没有被访问,被修改。
      • 2:被访问,没有被修改。
      • 3:被访问,被修改。
    • 随机选择编号最小的非空类中的一个页面淘汰。
    • 主要优点:易于理解和能够有效地被实现。
  • 老化算法(修改后的 NRU,可以模拟 LRU)
    • 与 LRU 的区别:
      • 在一个时钟滴答内无法区分先后。
      • 计数器只有有限位数。
  • 工作集页面置换算法(解决抖动
    • 请求调页:页面是在需要时调入的,而不是预先装入的。
    • 工作集:一个进程当前正在使用的页面的集合。
    • 颠簸:每执行几条指令程序就发生一次缺页中断。
    • 工作集模型:不少分页系统都会设法跟踪进程的工作集,以确保在让进程运行以前,它的工作集就已在内存中了。
    • 预先调页:在进程运行前就预先装入其工作集页面。
    • 工作集是随着时间变化的。
  • 工作集时钟页面置换算法(WSClock)
    • 工作集算法有合理的性能,但它的实现开销较大。工作集时钟算法是它的一种变体,不仅具有良好的性能,并且还能高效地实现。

Belady’s Anomaly: 所谓Belady现象是指:采用FIFO算法时,如果对—个进程未分配它所要求的全部页面,有时就会出现分配的页面数增多但缺页率反而提高的现象。(FIFO 不是栈式算法)


文件系统

文件类型

文件至少包括两部分:文件名字和文件内容。

  • 字符特殊文件(UNIX):和输入/输出有关,用户串行 I/O 类设备,如终端、打印机、网络等。
  • 块特殊文件(UNIX):用于磁盘类设备。
  • 目录:管理文件系统结构的系统文件。是多级树的结构。
  • 普通文件:包含用户信息的文件。
    • ASCII 文件
      • 由多行正文组成,以回车符、换行符或回车换行表示一行的结束。
      • 最大的优势是可以显示和打印,还可以用任何文本编辑器进行编辑。
    • 二进制文件
      • 通常,二进制文件有一定的内部结构,使用该文件的程序才了解这种结构。
      • 早期版本的 UNIX 的一个简单的可执行二进制文件以魔数开始,表明该文件是一个可执行的文件。

文件的实现

  1. 连续分配:把每个文件作为一串连续数据块存储在磁盘上。

    • 优势
      1. 实现简单:记录每个文件只需记住第一块的磁盘地址和文件的块数。
      2. 读写性能好:单个操作就可以从磁盘上读出整个文件。只需要一次需要(第一个块),之后不再需要寻道和旋转延迟。
    • 不足:随着时间的推移,磁盘会变得零碎。
    • 适用于CD-ROM 这样的文件系统,所有的文件大小都事先知道,而且在后续使用中不会再改变。
  2. 链表分配:为每个文件构造磁盘块链表。每个块的第一个字作为指向下一块的指针,块的其他部分存放数据。

    • 可以充分利用每个磁盘块。
    • 尽管顺序读文件非常方便,但是随机访问却相当缓慢。
    • 由于指针占用了一些字节,每个磁盘块中存储数据的字节数不再是2的整数次幂,降低了系统的运行效率。因为许多程序以长度为2的整数次幂来读写磁盘块,所以要读取一个完整的块就需要从磁盘块中获得和拼接信息,这就因复制引发了额外的开销。
  3. 采用内存中的表进行链表分配:取出每个磁盘块的指针字,把它们放在内存的一个表中,就可以解决上述链表分配的两个不足。

    • 内存中这样一个表格称为文件分配表(File Allocation Table,FAT)
    • 这种方法的主要缺点是必须把整个表都存放在内存中。
  4. i 节点:给每个文件赋予一个 i 节点(index-node)的数据结构,其中列出了文件属性和文件块的磁盘地址。

    • 相对于在内存中采用表的方式而言,这种机制具有很大的优势,即只有在对应的文件打开时,其 i 节点才在内存中。如果每个 i 节点占有 n 个字节,最多允许 k 个文件同时打开,那么只需在内存中提前保留 nk 个字节。
    • 一个问题是,当一个文件中所含的磁盘块数目超出了 i 节点所能容纳的数目时,可以用两个或更多包含额外磁盘块地址的块(一级间接块),或指向其他存放地址的磁盘块的磁盘块(二级间接块)。
    • UNIX Inode:

    在这里插入图片描述

目录的实现

目录中提供了查找文件磁盘块所需要的信息:

  • 整个文件的磁盘地址(连续分配方案)
  • 第一个块的编号(两种链表分配方案)
  • i 节点号

存放文件属性:

  • 把文件属性直接存放在目录项中。
  • 文件属性存放在 i 节点中而不是目录项中。目录项只有文件名和 i 节点号。

在目录中处理长文件名的两种方法:a)在行中;b)在堆中

在这里插入图片描述

目录项的实现:

  • 线性表
  • 散列表:查找迅速,但是需要复杂的管理。

共享文件

  • 硬链接:使用两个文件名指向同一个内部数据结构来代表一个文件。
  • 符号链接:是一类特殊的文件,这个文件包含了另一个文件的路径名(绝对路径或者相对路径)。
    • 在对符号文件进行读或写操作的时候,系统会自动把该操作转换为对源文件的操作,但删除链接文件时,系统仅仅删除链接文件,而不删除源文件本身。符号链接的操作是透明的:对符号链接文件进行读写的程序会表现得直接对目标文件进行操作。某些需要特别处理符号链接的程序(如备份程序)可能会识别并直接对其进行操作。一个符号链接文件仅包含有一个文本字符串,其被操作系统解释为一条指向另一个文件或者目录的路径。它是一个独立文件,其存在并不依赖于目标文件。如果删除一个符号链接,它指向的目标文件不受影响。如果目标文件被移动、重命名或者删除,任何指向它的符号链接仍然存在,但是它们将会指向一个不复存在的文件。这种情况被有时被称为被遗弃。
    • 符号链接的优点在于它能够跨越磁盘的界限,甚至可以命名在远程计算机上的文件,不过符号链接的实现不如硬链接那样有效率。

空闲块管理

  • 位图
  • 链表:每个空闲块包含下一个空闲块的指针
  • 成组链表:每个空闲块包含一些空闲块号和一个指向下一个块的指针

输入/输出

设备管理的功能

  1. 缓冲管理:为达到缓解CPU和I/O设备速度不匹配的矛盾,达到提高CPU和I/O设备利用率,提高系统吞吐量的目的,许多操作系统通过设置缓冲区的办法来实现。
  2. 设备分配:设备分配的基本任务是根据用户的I/O请求,为他们分配所需的设备。如果在I/O设备和CPU之间还存在设备控制器和通道,则还需为分配出去的设备分配相应的控制器和通道。
  3. 设备处理:设备处理程序又称设备驱动程序。其基本任务是实现CPU和设备控制器之间的通信。
  4. 设备独立性和虚拟设备:用户向系统申请和使用的设备与实际操作的设备无关

I/O 设备

I/O 设备大致可以分为两类:块设备字符设备

块设备把信息存储在固定大小的块中,每个块有自己的地址。通常块的大小在521字节至65536字节之间。所有传输以一个或多个完整的连续的块为单位。块设备的基本特征是每个块都能独立于其他块而读写。硬盘、蓝光光盘和 USB 盘是最常见的块设备。

字符设备以字符为单位发送或接收一个字符流,而不考虑任何块结构。字符设备是不可寻址的,也没有任何寻道操作。打印机、网络接口、鼠标可看做字符设备。

设备控制器

I/O 设备一般由机械部件和电子部件两部分组成。电子部件称作设备控制器适配器

控制器的任务是把串行的位流转换为字节块,并进行必要的错误校正工作。

内存映射 I/O

设备控制器中有控制寄存器数据缓冲区用来与 CPU 进行通信。

在这里插入图片描述

  • 单独的 I/O 和内存空间
    • 每个控制寄存器被分配一个 I/O 端口号,所有的 I/O 端口形成 I/O 端口空间,并且受到保护使得普通用户程序不能对其进行访问,只有操作系统可以访问。
  • 内存映射 I/O
    • 将所有控制寄存器映射到内存空间中,每个控制寄存器被分配唯一的一个内存地址,并且不会有内存被分配这一地址。
    • 在大多数系统中,分配给控制寄存器的地址位于或者靠近地址空间的顶端。
  • 混合方案

内存映射 I/O 的优点:

  1. 对于内存映射 I/O,设备控制寄存器只是内存中的变量,在 C 语言中可以和任何其他变量一样寻址,因此 I/O 设备驱动程序可以完全用 C 语言编写。否则,就要用到某些汇编代码。
  2. 对于内存映射 I/O,不需要特殊的保护机制来阻止用户进程执行 I/O 操作,操作系统必须要做的全部事情只是避免把包含控制寄存器的那部分地址空间放入任何用户的虚拟地址空间之中。
  3. 对于内存映射 I/O,可以引用内存的每一条指令也可以引用控制寄存器。

中断

当一个 I/O 设备完成交给它的工作时,它就产生一个中断。

中断信号导致 CPU 停止当前正在做的工作并且开始做其他的事情。地址线上的数字被用做指向一个称为中断向量的表格的索引,以便读取一个新的程序计数器。这一程序计数器指向相应的中断服务过程的开始。一般情况下,陷阱和中断从这一点上看使用相同的机制,并且常常共享相同的中断向量。中断向量的位置可以硬布线到机器中,也可以在内存中的任何地方通过一个 CPU 寄存器(由操作系统装载)指向其起点。

四种 I/O 访问机制

  • 程序控制 I/O
  • 中断驱动 I/O
  • 使用 DMA 的 I/O
  • 使用通道的 I/O

程序控制 I/O

I/O 可以采用三种根本上不同的方式来实现:程序控制 I/O、中断驱动 I/O 和使用 DMA 的 I/O。

以打印一个字符串为例:首先,数据被复制到内核空间。然后,操作系统进入一个密闭的循环,一次输出一个字符。在输出一个字符之后,CPU 要不断地查询设备以了解它是否就绪准备接收另一个字符。这一行为通常称为轮询忙等待

程序控制 I/O 十分简单但是有缺点:直到全部 I/O 完成之前要占用 CPU 的全部时间。

中断驱动 I/O

这种允许 CPU 在等待打印机变为就绪的同时做某些其他事情的方式就是使用中断。

当打印字符串的系统调用发出时,字符串缓冲区被复制到内核空间,并且一旦打印机准备好接收一个字符时就将第一个字符复制到打印机中。这时,CPU 要调用调度程序,并且某个其他进程将运行,请求打印字符串的进程将被阻塞,直到整个字符串打印完。

当打印机将字符打印完并且准备好接收下一个字符时,它将产生一个中断。这一中断将停止当前进程并且保存其状态。然后,打印机中断服务过程将运行。如果没有更多的字符要打印,中断处理程序就采取某个操作将用户进程解除阻塞。否则,它将输出下一个字符,应答中断,并且返回到中断之前正在运行的进程,该进程将从其停止的地方继续运行。

使用 DMA 的 I/O

中断驱动 I/O 的一个明显缺点是中断发生在每个字符上。中断要花费时间,所以这一方法将浪费一定数量的 CPU 时间。这一问题的一种解决方法是使用直接存储器 DMA

DMA 控制器能够独立于 CPU 而访问系统总线。

让 DMA 控制器一次给打印机提供一个字符,而不必打扰 CPU。本质上,DMA 是程序控制 I/O,只是由 DMA 控制器而不是主 CPU 做全部工作。这一策略需要特殊的硬件(DMA 控制器),但是使 CPU 获得自由从而可以在 I/O 期间做其他工作。

DMA 的重大成功是将中断的次数从打印每个字符一次减少到打印每个缓冲区一次。

I/O 系统的层次以及每一层的主要功能

I/O 请求 层次 I/O 应答 I/O 功能
用户进程 产生 I/O 请求;对 I/O 进行格式化;假脱机
与设备无关的软件 命名、保护、分块、缓冲、分配
设备驱动程序 设置设备寄存器;检查状态
中断处理程序 当 I/O 完成时唤醒驱动程序
硬件 执行 I/O 操作

中断处理程序

当中断发生时,中断处理程序将做它必须要做的全部工作以便对中断进行处理。然后,它可以将启动中断的驱动程序接触阻塞。

设备驱动程序

每个连接到计算机上的 I/O 设备都需要某些设备特定的代码来对其进行控制。这样的代码称为设备驱动程序,它一般由设备的制造商编写并随同设备一起交付。因为每一个操作系统都需要自己的设备驱动程序,所以设备制造商通常要为若干流行的操作系统提供驱动程序。

为了访问设备的硬件,意味着访问设备控制器的寄存器,设备驱动程序通常必须是操作系统内核的一部分,至少对目前的体系结构是如此。

与设备无关的 I/O 软件

虽然 I/O 软件中有一些是设备特定的,但是其他部分 I/O 软件都是与设备无关的。

与设备无关的软件的基本功能是执行对所有设备公共的 I/O 功能,并且向用户层软件提供一个统一的接口。

与设备无关的 I/O 软件的功能:

  1. 设备驱动程序的统一接口
    • 设备驱动程序与操作系统其余部分之间的接口。
    • 如何给 I/O 设备命名,把符号化的设备名映射到适当的驱动程序上。
  2. 缓冲
    • 缓冲可以提高应用的性能。
  3. 错误报告
    • 错误在 I/O 上下文中比在其他上下文中要常见得多。当错误发生时,操作系统必须尽最大努力对它们进行处理。许多错误是设备特定的并且必须由适当的驱动程序来处理,但是错误处理的框架是设备无关的。
    • 错误的类型:
      • 编程错误
      • 实际的 I/O 错误
  4. 分配与释放专用设备
  5. 提供与设备无关的块大小
    • 不同的磁盘可能具有不用的扇区大小。应该由与设备无关的软件来隐藏这一事实并且向高层提供一个统一的块大小。

用户空间的 I/O 软件

尽管大部分 I/O 软件都在操作系统内部,但是仍然有一小部分在用户空间,包括与用户程序连接在一起的库,甚至完全运行于内核之外的程序。有一个重要的类别是假脱机系统,假脱机是多道程序设计系统中处理独占 I/O 设备的一种方法。

缓冲管理

  • 无缓冲
  • 用户空间中的缓冲
  • 内核空间中的缓冲接着复制到用户空间
  • 内核空间中的双缓冲

磁盘臂调度算法

读写磁盘的时间由以下三个因素决定:

  1. 寻道时间:将磁盘臂移动到适当的柱面上所需的时间。
  2. 旋转延迟:等待适当扇区旋转到磁头下所需的时间。
  3. 实际数据传输时间。

对大多数磁盘而言,寻道时间与另外两个时间相比占主导地位,所以减少平均寻道时间可以充分改善系统性能。

许多磁盘驱动程序都维护着一张表,该表按柱面号索引。每一柱面的未完成的请求组成一个链表,链表头存放在表的相应表目中。

  • 先来先服务,FCFS
  • 最短寻道优先,SSF
  • 电梯算法

RAID

  • Redundant Array of Inexpensive Disk(廉价磁盘冗余阵列)
  • Redundant Array of Independent Disk(独立磁盘冗余阵列)

改进磁盘的性能和可靠性

将数据分布在多个驱动器上称为划分条带

  • 0级 RAID:将连续的条带以轮转方式写到全部驱动器上。
  • 1级 RAID:复制了所有的磁盘。

在这里插入图片描述


虚拟化和云

虚拟化的必要条件

虚拟机管理程序需要在以下三个维度上有良好的表现:

  1. 安全性:虚拟机管理程序应完全掌控虚拟资源。
  2. 保真性:程序在虚拟机上执行的行为应与在裸机上相同。
  3. 高效性:虚拟机中运行的大部分代码应不受虚拟机管理程序的干涉。

敏感指令:在内核态和用户态执行的行为不同。(进行 I/O 操作或修改 MMU 设置)
特权指令:在用户态执行会导致陷入。
机器可虚拟化的一个必要条件是:敏感指令为特权指令的子集。简单来说,如果用户态想要做不应该在用户态做的事情,硬件必须陷入。

VT 技术的基本思想是创建可以运行虚拟机的容器

第一类和第二类虚拟机管理程序

第一类虚拟机管理程序运行在裸机上;第二类虚拟机管理程序依赖于宿主操作系统的系统服务。

第一类虚拟机管理程序就像一个操作系统,因为它是唯一一个运行在最高特权级的程序。它的工作是支持真实硬件的多个虚拟机拷贝,类似于不同操作系统支持的进程。

第二类虚拟机是一个依赖于 Windows、Linux 等操作系统分配和调度资源的程序,很像一个普通的进程。当然,第二类虚拟机管理程序仍伪装成具有 CPU 和各种设备的完整计算机。

两类虚拟机管理程序都必须以一种安全的方式执行机器指令。

运行在两类虚拟机管理程序上的操作系统都称作客户操作系统。对于第二类虚拟机管理程序,运行在底层硬件上的操作系统称作宿主操作系统

  • 第一类虚拟机管理程序:Xen、Hyper-V、vSphere
  • 第二类虚拟机管理程序:VMare、Wine、Parallels

在不支持虚拟化的平台上实现虚拟化

客户机内核的敏感指令被替换为对模拟这些指令的例程的调用。真实硬件不会直接执行客户操作系统中的敏感指令。这些敏感指令被转为对虚拟机管理程序的调用,虚拟机管理程序模拟了这些指令的功能。

二进制翻译:虚拟机管理程序在运行中改写了部分代码,将有问题的指令替换成了安全的指令序列,模拟原指令的功能。由于进行了改写操作,因此可以替换掉不属于特权指令的敏感指令。其他的指令可以直接执行。(例如,一条不安全的 I/O 指令会被替换成一个陷入操作,经过安全性检查之后,执行等价的指令并返回结果)

云的五条必要特征:

  1. 按需自助服务:无需人为操作就能自动为用户提供资源。
  2. 普适的网络访问:所有资源都可以通过网络用标准化的机制访问,以支持各种异构设备。
  3. 资源池:云提供商拥有的资源可以服务多个用户并动态再分配,用户通常不知道他们使用的资源的具体位置。
  4. 快速可伸缩:能根据用户需求弹性甚至是自动地获取和释放资源。
  5. 服务可计量:云提供商按服务类型计量用户使用的资源。

云即服务:云的功能是提供一个用户可以直接访问并任意使用的虚拟机。因而,同一个云中可能运行着不同的操作系统。这种云称作基础设施即服务

  • 基础设置即服务(IAAS)
  • 平台即服务(PAAS)
  • 软件即服务(SAAS)
  • 网络即服务(NAAS)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章