秋招-操作系统概述


1. 概述

我们知道,计算机是本质是通过硬件来实现功能的,为了更方便地充分利用硬件,研发了操作系统,来更加高效地操作硬件获取服务。具体的细节这里不再过多展开,主要了解一下内容即可。

1.1 基本特征

操作系统有以下基本特征

1.1.1 并发

并发是指宏观上在一段时间内能同时运行多个程序而并行则指同一时刻能运行多个指令。并行需要硬件支持,如多流水线、多核处理器或者分布式计算系统。

操作系统通过引入进程和线程,使得程序能够并发运行

1.1.2 共享

共享是指系统中的资源可以被多个并发进程共同使用。有两种共享方式:互斥共享和同时共享。

互斥共享的资源称为临界资源,例如打印机等,在同一时刻只允许一个进程访问,需要用同步机制来实现互斥访问

1.1.3 虚拟

虚拟技术把一个物理实体转换为多个逻辑实体。主要有两种虚拟技术:时(时间)分复用技术和空(空间)分复用技术。

多个进程能在同一个处理器上并发执行就是使用了时分复用技术,让每个进程轮流占用处理器,每次只执行一小个时间片并快速切换。

虚拟内存使用了空分复用技术,它将物理内存抽象为地址空间,每个进程都有各自的地址空间。地址空间的页被映射到物理内存,地址空间的页并不需要全部在物理内存中,当使用到一个没有在物理内存的页时,执行页面置换算法,将该页置换到内存中。

1.1.4 异步

异步指进程不是一次性执行完毕,而是走走停停,以不可知的速度向前推进。

1.2 基本功能

操作系统为了完成对硬件控制,以及充分服务用户,具有以下功能。

1.2.1 进程管理

为了充分利用硬件资源和时间资源,出现了多进程和并发。因此需要对进程进行管理,主要有进程控制、进程同步、进程通信、死锁处理、处理机调度等。

1.2.2 内存管理

内存主要是在外存的基础上改进而来,为了对数据加速存取,出现了内存。因为内存是有限的,断电数据会消失。所以对内存进行管理,主要有内存分配、地址映射、内存保护与共享、虚拟内存等。

1.2.3 文件管理

外存是存放数据的,物理保存数据。对外存进行管理,也就是文件管理。主要有文件存储空间的管理、目录管理、文件读写管理和保护等。

1.2.4 设备管理

上面三类主要是程序运行所需的基本要求,除此之外,除了计算机主机,还有外设,比如键盘、鼠标、打印机、显示器等等。使得整套设备综合运行,需要进行设备管理,为了完成用户的 I/O 请求,方便用户使用各种设备,并提高设备的利用率。主要包括缓冲管理、设备分配、设备处理、虛拟设备等。

1.3 系统调用

操作系统是从里到外是逐层封装的,可从逻辑上认为,对硬件进行了逐层封装。里面涉及到的硬件部分称为内核态,外面的部分称为用户态。一般情况下,应用程序正常运行只需要在用户态运行即可,但是涉及到读取硬盘等操作时,就需要调用内核态的功能,即进行系统调用,由操作系统代为完成。

Linux 的系统调用主要有以下这些:

Task Commands
进程控制 fork(); exit(); wait();
进程通信 pipe(); shmget(); mmap();
文件操作 open(); read(); write();
设备操作 ioctl(); read(); write();
信息维护 getpid(); alarm(); sleep();
安全 chmod(); umask(); chown();

1.4 宏内核和微内核

宏内核就是将操作系统功能作为一个紧密结合的整体放到内核,由于各模块共享信息,因此有很高的性能。

而微内核则是由于操作系统不断复杂,因此将一部分操作系统功能移出内核,从而降低内核的复杂性。移出的部分根据分层的原则划分成若干服务,相互独立。

在微内核结构下,操作系统被划分成小的、定义良好的模块,只有微内核这一个模块运行在内核态,其余模块运行在用户态。因为需要频繁地在用户态和核心态之间进行切换,所以会有一定的性能损失。

1.5 中断分类

中断指的是操作系统停止运行本程序,然后去运行另一段程序来处理紧急事务,处理完之后继续运行本程序。产生中断主要分为两类:

  1. 外部中断

    由 CPU 执行指令以外的事件引起,如 I/O 完成中断,表示设备输入/输出处理已经完成,处理器能够发送下一个输入/输出请求。此外还有时钟中断、控制台中断等。

  2. 内部中断

    当指令执行时由CPU主动产生的,受到CPU控制。比如程序异常、地址越界等等。或者是用户程序主动使用系统调用产生中断。

1.6 总结

操作系统是为了更加充分利用硬件、管理硬件为用户提供服务所设计的软件。运行程序需要硬盘(文件管理)、内存(内存管理),为了充分利用资源,采用并发,时间片轮转来运行多个进程(进程管理),另外,还有键盘、显示器、打印机等硬件(设备管理)。

总体来说,操作系统就是将硬件逐层封装,内层称为内核态,外层称为用户态。

2. 进程管理

本节介绍进程管理。

2.1 进程与线程

2.1.1 进程

进程是资源分配的基本单位进程控制块(Process Control Block,PCB)用于描述进程的基本信息和运行状态,所谓的创建进程和撤销进程,都是指对PCB的操作。换句话说,进程控制块是进程的物理实体,而进程只是逻辑实体,创建进程时,会创建进程控制块,用于控制进程。下图显示了4个程序创建了4个进程,这4个进程可以并发地执行。

2.1.2 线程

线程是独立调度的基本单位一个进程中可以有多个线程,它们共享进程资源。QQ和浏览器是两个进程,浏览器进程里面有很多线程,例如HTTP请求线程、事件响应线程、渲染线程等等。线程的并发执行使得在浏览器中点击一个新链接发起HTTP请求时,浏览器还可以响应用户的其他事件。

2.1.3 区别

  1. 拥有资源

    进程是资源分配的基本单位,但是线程不拥有资源【只拥有线程运行所必须的一点点资源】,线程可以访问隶属进程的资源。

  2. 调度

    线程是独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换,从一个进程中的线程切换到另一个进程中的线程时,会引起进程切换。

  3. 系统开销

    由于进程是资源分配的基本单位,所以在创建和撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O设备等,所付出的开销远大于创建或撤销线程时的开销。类似地,在进程进程切换时,涉及当前执行进程CPU环境的保存以及新调度CPU环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销很小。

  4. 通信方面

    线程间可以通过直接读写同一进程中的数据进行通信,但是进程通信需要借助IPC(Inter-Process Communication)。

2.2 进程状态的切换

进程主要有以下5个状态:

  1. 进程创建:创建进程。
  2. 就绪状态:进程运行所需的资源已经准备好,等待调度算法分配CPU时间片。
  3. 运行状态:进程分配了CPU时间片后,处于运行状态。时间片用完之后(如果还未运行完),则回到就绪状态。
  4. 阻塞状态:进程所需的资源还未准备充足,等待分配非CPU时间片资源。一般情况下,阻塞状态是由运行状态因缺少相关资源转化而来。
  5. 进程销毁:进程执行完毕,任务完成。

2.3 进程调度算法

其实上面进程运行的最关键的资源就是CPU时间片。而面对很多进程,该如何分配CPU时间片呢?即如何调度进程?不同的环境有不同的调度算法。

2.3.1 批处理系统

批处理系统没有太多的用户操作,在该系统中,调度算法目标是保证吞吐量和周转时间(从提交到终止的时间)。有以下几种调度算法:

  1. 先来先服务(First Come First Serverd,FCFS)

    非抢占式的调度算法,按照请求的顺序进行调度。有利于长作业,但不利于短作业,因为短作业必须一直等待前面的长作业执行完毕才能执行,而长作业又需要执行很长时间,造成了短作业等待时间过长

  2. 短作业优先(Shortest Job First,SJF)

    非抢占式的调度算法,按估计运行时间最短的顺序进行调度。长作业有可能会饿死,处于一直等待短作业执行完毕的状态。因为如果一直有短作业到来,那么长作业永远得不到调度

  3. 最短剩余时间优先(Shortest Remaining Time Next,SRTN)

    最短作业优先的抢占式版本,按剩余运行时间的顺序进行调度。 当一个新的作业到达时,其整个运行时间与当前进程的剩余时间作比较。如果新的进程需要的时间更少,则挂起当前进程,运行新的进程。否则新的进程等待。

2.3.2 交互式系统

交互式系统有大量的用户交互操作,在该系统中调度算法的目标是快速地进行响应。所以,这种系统的目标不再是吞吐量,而是尽可能多地响应用户。而上面批处理系统,显然是一个作业处理完才进行下一个作业,显然交互量比较少,即实时性不能满足。

  1. 时间片轮转

    将所有就绪进程按FCFS的原则排成一个队列,每次调度时,把CPU时间片分配给队首进程,该进程可以执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序便停止该进程的执行,并将它送往就绪队列的队尾,同时把CPU时间片分配给队首的进程。

    时间片轮转算法的效率和时间片的大小有很大关系:

    • 因为进程切换要保存进程的信息以及载入新进程的信息,如果时间片太小,会导致进程切换地太频繁,在进程切换上就会花过多时间。
    • 而如果时间片太长,那么实时性就不能得到保证。

  2. 优先级调度

    时间片轮转,是平等的对待每一个进程,按照任务的创建时间来排序的。为了处理应急情况,可为每个进程分配一个优先级,按优先级进行调度。为了防止低优先级的进程永远等不到调度,可以随着时间的推移增加等待进程的优先级。

  3. 多级反馈队列

    一个进程需要执行 100 个时间片,如果采用时间片轮转调度算法,那么需要交换 100 次。其实可以改进一下,如果在当前就绪队列中没有执行完,下次可到CPU时间片更长的就绪队列中排列,即多级队列。

    多级队列是为这种需要连续执行多个时间片的进程考虑,它设置了多个队列,每个队列时间片大小都不同,例如 1,2,4,8,..。进程在第一个队列没执行完,就会被移到下一个队列。这种方式下,之前的进程只需要交换 7 次。每个队列优先权也不同,最上面的优先权最高。因此只有上一个队列没有进程在排队,才能调度当前队列上的进程。可以将这种调度算法看成是时间片轮转调度算法和优先级调度算法的结合。

2.3.3 实时系统

实时系统要求一个请求在一个确定时间内得到响应。分为硬实时和软实时,前者必须满足绝对的截止时间,后者可以容忍一定的超时。

2.4 进程同步

前面提到过,如果多进程之间,一个进程需要另一个进程执行完才能执行,这就是进程同步。除了同步,还有另一个互斥的概念,就是说,进程之间执行没有相对顺序,但是不能同时访问某个资源。由此引入了如下几个概念:

2.4.1 临界区

对临界资源进行访问的那段代码称为临界区。为了互斥访问资源,每个进程在进入临界区之前,需要先进行检查。

1
2
3
// entry section
// critical section;
// exit section

2.4.2 同步与互斥

  • 同步:多个进程之间因为合作产生的直接制约关系,使得进程有一定的先后执行关系。
  • 互斥:多个进程在同一时刻只有一个进程能进入临界区

2.4.3 信号量

信号量,可认为是一个变量,用于限制进程的执行。信号量(Semaphore)是一个整型变量,可以对其执行 down 和 up 操作,也就是常见的 P 和 V 操作。

  • down : 如果信号量大于 0 ,执行 -1 操作;如果信号量等于 0,进程睡眠,等待信号量大于 0;
  • up :对信号量执行 +1 操作,唤醒睡眠的进程让其完成 down 操作。

down 和 up 操作需要被设计成原语,不可分割,通常的做法是在执行这些操作的时候屏蔽中断。如果信号量的取值只能为 0 或者 1,那么就成为了 互斥量(Mutex) ,0 表示临界区已经加锁,1 表示临界区解锁。

2.5 经典同步问题

2.5.1 生产者消费者问题

生产者向仓库中放数据,消费者从仓库中取数据,两个进程对仓库的操作是互斥的。

2.5.2 哲学家进餐问题

5个哲学家,围成圆形桌子周围,形成5个空隙,每个空隙存放一根筷子。哲学家进餐需要用到左右两个空隙中的筷子,此时如果5个哲学家都拿一侧的筷子,显然会造成死锁。

因此可设置两个条件:

  • 必须同时拿起左右两根筷子。
  • 只有在两个邻居都没有进餐的情况下才允许进餐。

2.5.3 读者-写者问题

注意,此问题和生产者消费者是不同的,生产者和消费者显然是互斥的,即使多个生产者也是互斥的。而本问题,多个读者之间是允许同时读操作的,但是只要有写者,就不能同时进行。

因此,如果有了读进程,那么接下来允许有读进程,但是不允许有写进程。所以对于读写的数据,需要设置一个互斥量data_mutex。那么怎么实现多个读进程共享一个互斥量呢?

可对读进程数量count设置互斥量count_mutex,如果count为0,并且当前读进程拿到了数据互斥量data_mutex,那么就对数据加锁。如果count为0,并且执行完了读操作,显然就释放数据锁。至于其他情况,则无需拿数据锁,因为其他情况,则表示已经有读操作了。

至于写操作,则无需count,只需要对数据锁操作即可。

注意,读进程数量count是一个互斥的数据,显然对其自增和自减操作是互斥的。所以必须先拿到这个锁,然后再拿数据锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
typedef int semaphore;
semaphore count_mutex = 1;
semaphore data_mutex = 1;
int count = 0;

void reader() {
while(TRUE) {
down(&count_mutex);
count++;
if(count == 1) down(&data_mutex); // 第一个读者需要对数据进行加锁,防止写进程访问
up(&count_mutex);
read();
down(&count_mutex);
count--;
if(count == 0) up(&data_mutex);
up(&count_mutex);
}
}

void writer() {
while(TRUE) {
down(&data_mutex);
write();
up(&data_mutex);
}
}

2.6 进程通信

顾名思义,进程通信用于进程之间消息的传输【注意,进程可以看成是一个应用程序,进程间通信,可认为是应用程序间通信,最直观的就是Socket套接字了】。有以下几种方式:

  1. 管道

    管道是通过调用 pipe 函数创建的,fd[0] 用于读,fd[1] 用于写。它具有以下限制:

    • 只支持半双工通信(单向交替传输);
    • 只能在父子进程或者兄弟进程中使用。

  2. FIFO(命名管道)

    也称为命名管道,去除了管道只能在父子进程中使用的限制。FIFO 常用于客户-服务器应用程序中,FIFO 用作汇聚点,在客户进程和服务器进程之间传递数据。

  3. 消息队列

    相比于 FIFO,消息队列具有以下优点:

    • 消息队列可以独立于读写进程存在,从而避免了 FIFO 中同步管道的打开和关闭时可能产生的困难;
    • 避免了 FIFO 的同步阻塞问题,不需要进程自己提供同步方法;
    • 读进程可以根据消息类型有选择地接收消息,而不像 FIFO 那样只能默认地接收。
  4. 共享存储

    允许多个进程共享一个给定的存储区。因为数据不需要在进程之间复制,所以这是最快的一种 IPC。需要使用信号量用来同步对共享存储的访问。多个进程可以将同一个文件映射到它们的地址空间从而实现共享内存。另外 XSI 共享内存不是使用文件,而是使用内存的匿名段。

  5. 套接字

    可用于不同机器间的进程通信。

3. 死锁

即使在写程序时,非常小心,但是仍然可能发生死锁。本节讲解死锁发生的必要条件,以及死锁的处理方法。

3.1 死锁发生的必要条件

死锁发生有以下四个必要条件:

  • 互斥:每个资源要么已经分配给了一个进程,要么就是可用的。
  • 占有和等待:已经得到了某个资源的进程可以再请求新的资源。
  • 不可抢占:已经分配给一个进程的资源不可强制性地被抢占,它只能被占有它的进程显式地释放。
  • 环路等待:有两个或两个以上的进程组成一条环路,该环路中的每个进程都在等待下一个进程所占有的资源。

3.2 处理方法

针对死锁,可以提前预防其发生,也可提前避免其发生,或者当其发生时不做任何行为等,主要有以下四种策略。

3.2.1 鸵鸟策略

把头埋在沙子里,假装根本没发生问题。因为解决死锁问题的代价很高,因此鸵鸟策略这种不采取任务措施的方案会获得更高的性能。

对于某些程序,当发生死锁时不会对用户造成多大影响,或发生死锁的概率很低,可以采用鸵鸟策略。大多数操作系统,包括 Unix,Linux 和 Windows,处理死锁问题的办法仅仅是忽略它。

3.2.2 死锁检测与死锁恢复

对于一些死锁,可能无法忽视。因此,可主动检测,然后进行干预恢复正常。注意,不试图阻止死锁,而是当检测到死锁发生时,采取措施进行恢复。显然,如果资源和进程之间形成环路,就会发生死锁。因此可进行环路检测。

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

    上图为资源分配图,其中方框表示资源,圆圈表示进程。资源指向进程表示该资源已经分配给该进程,进程指向资源表示进程请求获取该资源。图 a 可以抽取出环,如图 b,它满足了环路等待条件,因此会发生死锁。

    每种类型一个资源的死锁检测算法是通过检测有向图是否存在环来实现,从一个节点出发进行深度优先搜索,对访问过的节点进行标记,如果访问了已经标记的节点,就表示有向图存在环,也就是检测到死锁的发生。

    注意,这是每种类型仅有一个资源,很容易判断是否有环路。

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

    上图中,有三个进程四个资源,每个数据代表的含义如下:

    • E 向量:资源总量
    • A 向量:资源剩余量
    • C 矩阵:每个进程所拥有的资源数量,每一行都代表一个进程拥有资源的数量
    • R 矩阵:每个进程请求的资源数量

    进程 P1 和 P2 所请求的资源都得不到满足,只有进程 P3 可以,让 P3 执行,之后释放 P3 拥有的资源,此时 A = (2 2 2 0)。P2 可以执行,执行后释放 P2 拥有的资源,A = (4 2 2 1) 。P1 也可以执行。所有进程都可以顺利执行,没有死锁。

    算法总结如下:

    每个进程最开始时都不被标记,执行过程有可能被标记。当算法结束时,任何没有被标记的进程都是死锁进程。

    1. 寻找一个没有标记的进程 Pi,它所请求的资源小于等于 A。
    2. 如果找到了这样一个进程,那么将 C 矩阵的第 i 行向量加到 A 中,标记该进程,并转回 1。
    3. 如果没有这样一个进程,算法终止。

    换句话说,就是根据当前状态以及当前资源情况,对后续进程资源占用情况进行模拟。如果最终进程都能执行,那么就说明不会造成死锁,否则就会发生死锁。

发生了死锁之后,即环路等待,该怎么恢复呢?显然,只要打破环路就行,使得某个进程释放占有的资源。即杀死进程。另外,还可以利用抢占恢复、回滚恢复。

3.2.3 死锁预防

死锁检测,指的是程序已经处于运行的状态。当然,也可以在运行之前就进行预防,即在程序运行之前预防发生死锁。

  1. 破坏互斥条件

    例如假脱机打印机技术允许若干个进程同时输出,唯一真正请求物理打印机的进程是打印机守护进程。

  2. 破坏占有和等待条件

    一种实现方式是规定所有进程在开始执行前请求所需要的全部资源。

  3. 破坏不可抢占条件

    即资源可以抢占。

  4. 破坏环路等待

    给资源统一编号,进程只能按编号顺序来请求资源。

3.2.4 死锁避免

在程序运行前可以破坏条件来预防死锁的发生,在程序运行过程中也可以避免死锁的发生【和死锁检测类似】。

  1. 安全状态

    图 a 的第二列 Has 表示已拥有的资源数,第三列 Max 表示总共需要的资源数,Free 表示还有可以使用的资源数。从图 a 开始出发,先让 B 拥有所需的所有资源(图 b),运行结束后释放 B,此时 Free 变为 5(图 c);接着以同样的方式运行 C 和 A,使得所有进程都能成功运行,因此可以称图 a 所示的状态时安全的。

    定义:如果没有死锁发生,并且即使所有进程突然请求对资源的最大需求,也仍然存在某种调度次序能够使得每一个进程运行完毕,则称该状态是安全的。

    安全状态的检测与死锁的检测类似,因为安全状态必须要求不能发生死锁。下面的银行家算法与死锁检测算法非常类似,可以结合着做参考对比。

  2. 单个资源的银行家算法

    一个小城镇的银行家,他向一群客户分别承诺了一定的贷款额度,算法要做的是判断对请求的满足是否会进入不安全状态,如果是,就拒绝请求;否则予以分配。

    上图 c 为不安全状态,因此算法会拒绝之前的请求,从而避免进入图 c 中的状态。

  3. 多个资源的银行家算法

    上图中有五个进程,四个资源。左边的图表示已经分配的资源,右边的图表示还需要分配的资源。最右边的 E、P 以及 A 分别表示:总资源、已分配资源以及可用资源,注意这三个为向量,而不是具体数值,例如 A=(1020),表示 4 个资源分别还剩下 1/0/2/0。

    检查一个状态是否安全的算法如下:

    • 查找右边的矩阵是否存在一行小于等于向量 A。如果不存在这样的行,那么系统将会发生死锁,状态是不安全的。
    • 假若找到这样一行,将该进程标记为终止,并将其已分配资源加到 A 中。
    • 重复以上两步,直到所有进程都标记为终止,则状态时安全的。

    如果一个状态不是安全的,需要拒绝进入这个状态。

4. 内存管理

内存是程序运行的必须资源,而内存因为硬件限制,成为计算机效率的瓶颈。因此,高效管理内存是操作系统不可缺少的任务。内存的出现,是因为硬盘的存取效率较低,因此内存作为硬盘的副本,使得CPU可以方便存取硬盘中的数据。

因为硬件关系,内存的容量比较小,而硬盘的容量则比较大。这时候,肯定无法将硬盘中的全部数据都加载到内存中。同样,在程序运行过程中,为程序分配的内存也是有限的,同样,也不可能将程序全部加载到内存中。这时候,就需要加载当前程序运行所需的数据,随着运行,肯定有一部分已经加载的数据不需要了,此时再将其他需要的数据从硬盘加载到内存,覆盖刚才不需要的数据。

上面的操作,是不需要程序来执行的,而是操作系统自己实现的内存管理。对程序来说是透明的,即从程序的角度来看,内存完全分配了程序所需的全部内存。但是从操作系统的角度来看,则是不断的覆盖刷新一小块内存。对程序来说,这就是虚拟内存技术。

4.1 虚拟内存

虚拟内存的目的是为了让物理内存扩充成更大的逻辑内存,从而让程序获得更多的可用内存。

为了更好的管理内存,操作系统将内存抽象成地址空间。每个程序拥有自己的地址空间,这个地址空间被分割成多个块,每一块称为一页。这些页被映射到物理内存,但不需要映射到连续的物理内存,也不需要所有页都必须在物理内存中。当程序引用到不在物理内存中的页时,由硬件执行必要的映射,将缺失的部分装入物理内存并重新执行失败的指令。

从上面的描述中可以看出,虚拟内存允许程序不用将地址空间中的每一页都映射到物理内存,也就是说一个程序不需要全部调入内存就可以运行,这使得有限的内存运行大程序成为可能。例如有一台计算机可以产生 16 位地址,那么一个程序的地址空间范围是 0~64K。该计算机只有 32KB 的物理内存,虚拟内存技术允许该计算机运行一个 64K 大小的程序。

那么,从硬盘加载到内存的基本单位是什么呢?重新覆盖内存中的部分内存的基本单位是什么呢?指定大小的空间,还是即时的大小呢?比较主流的有两种:分页和分段,以及段页结合。

4.2 分页置换算法

本算法将内存分成若干个页,即基本单位是页。

在程序运行过程中,如果要访问的页面不在内存中,就发生缺页中断从而将该页调入内存中。此时如果内存已无空闲空间,系统必须从内存中调出一个页面到磁盘对换区中来腾出空间。页面置换算法和缓存淘汰策略类似,可以将内存看成磁盘的缓存。在缓存系统中,缓存的大小有限,当有新的缓存到达时,需要淘汰一部分已经存在的缓存,这样才有空间存放新的缓存数据。

页面置换算法的主要目标是使页面置换频率最低(也可以说缺页率最低),这样也就是意味着硬盘IO较低,整体效率较高。主要有以下几种算法:

4.2.1 最佳置换算法(理论上最优)

当内存的页面占满之后,被换出的页面应该是将是接下来最长一段时间内不再被访问的,这就保证了最低的缺页率。这是一种理论上的算法,因为无法知道一个页面多长时间不被访问。

4.2.2 最近最久未使用(LRU,Least Recently Used)

虽然无法知道将来要使用的页面情况,但是可以知道过去使用页面的情况。LRU 将最近最久未使用的页面换出。

为了实现 LRU,需要在内存中维护一个所有页面的链表。当一个页面被访问时,将这个页面移到链表表头。这样就能保证链表表尾的页面是最近最久未访问的。因为每次访问都需要更新链表,因此这种方式实现的 LRU 代价很高。

由于程序运行的局部性:时间局部性和空间局部性,所以从已经运行的程序来推断后面即将要运行的程序,因此最近最久未使用,这种方法是可行的。

4.2.3 最近未使用(NRU,Not Recently Used)

每个页面都有两个状态位:R 与 M,当页面被访问时设置页面的 R=1,当页面被修改时设置 M=1。其中 R 位会定时被清零。可以将页面分成以下四类:

  • R=0,M=0
  • R=0,M=1
  • R=1,M=0
  • R=1,M=1

当发生缺页中断时,NRU 算法随机地从类编号最小的非空类中挑选一个页面将它换出。

NRU 优先换出已经被修改的脏页面(R=0,M=1),而不是被频繁使用的干净页面(R=1,M=0)。

4.2.4 先进先出(FIFO,First In First Out)

选择换出的页面是最先进入的页面。该算法会将那些经常被访问的页面换出,导致缺页率升高。

4.2.5 第二次机会算法

FIFO 算法可能会把经常使用的页面置换出去,为了避免这一问题,对该算法做一个简单的修改:

当页面被访问 (读或写) 时设置该页面的 R 位为 1。需要替换的时候,检查最老页面的 R 位。如果 R 位是 0,那么这个页面既老又没有被使用,可以立刻置换掉;如果是 1,就将 R 位清 0,并把该页面放到链表的尾端,修改它的装入时间使它就像刚装入的一样,然后继续从链表的头部开始搜索。

4.2.6 时钟算法

第二次机会算法需要在链表中移动页面,降低了效率。时钟算法使用环形链表将页面连接起来,再使用一个指针指向最老的页面。

4.3 分段置换算法

分页是简单生硬地将分成若干部分,没有考虑程序的内部关联,使得数据共享和保护无法实现。并且页的尺寸比较大,对于部分程序,比如只有几行代码,但是却需要空闲出整个页面来保存该代码,空间利用率较低。

为了更加灵活,出现了分段,对程序进行分段,逐段加载程序。每段的大小不同。但是这样做,因为段的大小不同,导致内存划分不规律,出现了很多内存碎片。

4.4 段页式置换算法

段页式则是整体分页,局部分段。整体分页,使得内存整体划分上比较规律,不会产生大片的内存碎片。页内分段,是内存置换的基本单位,这时候,内存碎片只出现在某页中,同时内存浪费也只出现在某页中。

即拥有分段系统的共享和保护,也拥有了分页系统的虚拟内存功能。

4.5 分页与分段的比较

  • 对程序员的透明性:分页透明,但是分段需要程序员显式划分每个段。
  • 地址空间的维度:分页是一维地址空间,分段是二维的。
  • 大小是否可以改变:页的大小不可变,段的大小可以动态改变。
  • 出现的原因:分页主要用于实现虚拟内存,从而获得更大的地址空间;分段主要是为了使程序和数据可以被划分为逻辑上独立的地址空间并且有助于共享和保护

5. 设备管理

除了CPU之外,计算机还包括很多种设备,最重要的数据设备是磁盘,负责存储数据以及和CPU数据交互。

5.1 磁盘结构

  • 盘面(Platter):一个磁盘有多个盘面;
  • 磁道(Track):盘面上的圆形带状区域,一个盘面可以有多个磁道;
  • 扇区(Track Sector):磁道上的一个弧段,一个磁道可以有多个扇区,它是最小的物理储存单位,目前主要有 512 bytes 与 4 K 两种大小;
  • 磁头(Head):与盘面非常接近,能够将盘面上的磁场转换为电信号(读),或者将电信号转换为盘面的磁场(写);
  • 制动手臂(Actuator arm):用于在磁道之间移动磁头;
  • 主轴(Spindle):使整个盘面转动。

5.2 磁盘调度算法

磁盘就是为了读写数据,而读写数据的基本单位是扇区。因此,如何在多个扇区之间高效读写,这是效率的关键。

读写一个磁盘块的时间的影响因素有:

  • 旋转时间(主轴转动盘面,使得磁头移动到适当的扇区上)
  • 寻道时间(制动手臂移动,使得磁头移动到适当的磁道上)
  • 实际的数据传输时间

其中,寻道时间最长,因此磁盘调度的主要目标是使磁盘的平均寻道时间最短。

5.2.1 先来先服务(FCFS)

按照磁盘请求的顺序进行调度。优点是公平和简单。缺点也很明显,因为未对寻道做任何优化,使平均寻道时间可能较长。

5.2.2 最短寻道时间优先(SSTF)

优先调度与当前磁头所在磁道距离最近的磁道

虽然平均寻道时间比较低,但是不够公平。如果新到达的磁道请求总是比一个在等待的磁道请求近,那么在等待的磁道请求会一直等待下去,也就是出现饥饿现象。具体来说,两端的磁道请求更容易出现饥饿现象。

5.2.3 电梯算法(SCAN)

电梯总是保持一个方向运行,直到该方向没有请求为止,然后改变运行方向。

电梯算法(扫描算法)和电梯的运行过程类似,总是按一个方向来进行磁盘调度,直到该方向上没有未完成的磁盘请求,然后改变方向。

因为考虑了移动方向,因此所有的磁盘请求都会被满足,解决了 SSTF 的饥饿问题。

6. 备注

参考CS-Notes (cyc2018.xyz)


文章作者: 浮云
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 浮云 !
  目录