操作系统复习

主要是对操作系统的一些回顾复习! 参考

概述

操作系统基本的特征

1、并发
并发:在一段时间内能够同时运行多个程序
并行:同一时刻能运行多个指令

2、共享
系统中的资源可以被多个并发进程共同使用

3、虚拟

4、异步
进程不是一次性执行完毕的,而是断断续续

系统调用

如果一个进程在用户态需要使用内核态的功能,就会系统调用,陷入内核,由操作系统处理

Linux系统的系统调用主要有:

中断

CPU对系统发生的某个事件做出的一种反应,CPU暂停正在执行的程序,保留现场后自动地转去执行相应的处理程序,处理完该事件后再返回断点继续执行被“打断”的程序

中断分为三类:
1、外中断

由CPU外部引起的,称作中断,如I/O中断、表示设备输入/输出处理已经完成,处理器能够发送下一个输入/输出请求。时钟中断、控制台中断等。

2、异常

来自CPU的内部事件或程序执行中的事件引起的过程,称作异常,如由于CPU本身故障(电源电压低于105V或频率在47~63Hz之外)、程序故障(非法操作码、地址越界、浮点溢出等)等引起的过程。

3、陷入

由于在程序中使用了请求系统服务的系统调用而引发的过程,称作“陷入”(trap,或者陷阱)。

前两类通常都称作中断,它们的产生往往是无意、被动的,而陷入是有意和主动的

进程

概念

1、进程

一个进程就是一个正在执行程序的实例;进程是某种类型的活动,它有程序、输入、输出以及状态。

2、进程控制块 (Process Control Block, PCB)

描述进程的基本信息和运行状态,所谓的创建进程和撤销进程,都是指对 PCB 的操作。

3、线程

线程是独立调度的基本单位。

一个进程中可以有多个线程,它们共享进程资源。

  • 区别

拥有资源:进程是资源分配的基本单位,但是线程不拥有资源,线程可以访问隶属进程的资源。

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

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

通信方面:进程间通信 (IPC) 需要进程同步和互斥手段的辅助,以保证数据的一致性。而线程间可以通过直接读/写同一进程中的数据段(如全局变量)来进行通信。

进程状态的切换

就绪态:等待被调度执行

运行态:获得CPU 执行

阻塞态:等待资源

调度算法

批处理系统

1、先来先服务 FCFS
按照请求的顺序进行调度。先请求的,先执行。

2、短作业优先 SJF
时间最短的先调度,可能出现饿死的情况,长作业一直得不到调度

3、最短剩余时间优先 SRTN
按估计剩余时间最短的顺序进行调度。

交互式系统

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

时间片轮转算法的效率和时间片的大小有很大关系。因为进程切换都要保存进程的信息并且载入新进程的信息,如果时间片太小,会导致进程切换得太频繁,在进程切换上就会花过多时间。

2、优先级调度

为每个进程分配一个优先级,按优先级进行调度。

为了防止低优先级的进程永远等不到调度,可以随着时间的推移增加等待进程的优先级。

3、多级反馈队列

如果一个进程需要执行 100 个时间片,如果采用时间片轮转调度算法,那么需要交换 100 次。

多级队列是为这种需要连续执行多个时间片的进程考虑,它设置了多个队列,每个队列时间片大小都不同,例如 1,2,4,8,..。进程在第一个队列没执行完,就会被移到下一个队列。这种方式下,之前的进程只需要交换 7 次。

每个队列优先权也不同,最上面的优先权最高。因此只有上一个队列没有进程在排队,才能调度当前队列上的进程。

可以将这种调度算法看成是时间片轮转调度算法和优先级调度算法的结合。

进程同步

1、临界区

对临界资源进行访问的那段代码称为临界区

2、同步和互斥

同步:多个进程按一定顺序执行;

互斥:多个进程在同一时刻只有一个进程能进入临界区。

3、信号量

信号量(Semaphore)是一个整型变量

如果信号量的取值只能为0或者1,那么就成为了互斥量(Mutex) ,0 表示临界区已经加锁,1

经典同步问题

  • 生产者-消费者问题

问题描述:使用一个缓冲区来保存物品,只有缓冲区没有满,生产者才可以放入物品;只有缓冲区不为空,消费者才可以拿走物品。

因为缓冲区属于临界资源,因此需要使用一个互斥量 mutex 来控制对缓冲区的互斥访问。

为了同步生产者和消费者的行为,需要记录缓冲区中物品的数量。数量可以使用信号量来进行统计,这里需要使用两个信号量:empty 记录空缓冲区的数量,full 记录满缓冲区的数量。其中,empty 信号量是在生产者进程中使用,当 empty 不为 0 时,生产者才可以放入物品;full 信号量是在消费者进程中使用,当 full 信号量不为 0 时,消费者才可以取走物品。

注意,不能先对缓冲区进行加锁,再测试信号量。也就是说,不能先执行 wait(mutex) 再执行 wait(empty)。如果这么做了,那么可能会出现这种情况:生产者对缓冲区加锁后,执行 wait(empty) 操作,发现 empty = 0,此时生产者睡眠。消费者不能进入临界区,因为生产者对缓冲区加锁了,也就无法执行 signal(empty) 操作,empty 永远都为 0,那么生产者和消费者就会一直等待下去,造成死锁。

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
27
#define N 100
typedef int semaphore;
semaphore mutex = 1;
semaphore empty = N;
semaphore full = 0;

void producer() {
while(TRUE){
int item = produce_item();
wait(&empty);//当 empty 不为0时,生产者才可以放入物品
wait(&mutex);
insert_item(item);
signal(&mutex);
signal(&full);
}
}

void consumer() {
while(TRUE){
wait(&full);
wait(&mutex);
int item = remove_item();
signal(&mutex);
signal(&empty);
consume_item(item);
}
}
  • 读者-写者问题

允许多个进程同时对数据进行读操作,但是不允许读和写以及写和写操作同时发生。

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) {
wait(&count_mutex);
count++;//记录读者数量
if(count == 1) wait(&data_mutex);//第一个读者需要对数据进行加锁,防止写进程访问
signal(&count_mutex);
read();
wait(&count_mutex);//对count加锁
count--;
if(count == 0) signal(&data_mutex);//没有读者读了,对data解锁
signal(&count_mutex);
}
}

void writer() {
while(TRUE) {
wait(&data_mutex);
write();
signal(&data_mutex);
}
}
  • 哲学家就餐问题

五个哲学家围着一张圆桌,每个哲学家面前放着食物。哲学家的生活有两种交替活动:吃饭以及思考。当一个哲学家吃饭时,需要先拿起自己左右两边的两根筷子,并且一次只能拿起一根筷子。

解决方法:

1、至多只允许四位哲学家同时去拿左筷子,最终能保证至少有一位哲学家能进餐,并在用完后释放两只筷子供他人使用。

2、仅当哲学家的左右手筷子都拿起时才允许进餐。

3、规定奇数号哲学家先拿左筷子再拿右筷子,而偶数号哲学家相反。

死锁

必要条件:

互斥:一个资源每次只能被一个进程使用。

占有和等待:已经得一个进程因请求资源而阻塞时,对已获得的资源保持不放。

不可抢占:已经分配给一个进程的资源不能强制性地被抢占,它只能被占有它的进程显式地释放。

循环等待:若干进程之间形成一种头尾相接的循环等待资源关系。

解决死锁的方法

  • 死锁预防:
    通过设置某些限制条件,去破坏死锁的四个条件中的一个或几个条件,来预防发生死锁。但由于所施加的限制条件往往太严格,因而导致系统资源利用率和系统吞吐量降低。

  • 死锁避免:
    允许前三个必要条件,但通过明智的选择,确保永远不会到达死锁点,因此死锁避免比死锁预防允许更多的并发。

  • 死锁检测:
    不须实现采取任何限制性措施,而是允许系统在运行过程发生死锁,但可通过系统设置的检测机构及时检测出死锁的发生,并精确地确定于死锁相关的进程和资源,然后采取适当的措施,从系统中将已发生的死锁清除掉。

  • 死锁解除:
    与死锁检测相配套的一种措施。当检测到系统中已发生死锁,需将进程从死锁状态中解脱出来。常用方法:撤销或挂起一些进程,以便回收一些资源,再将这些资源分配给已处于阻塞状态的进程。死锁检测盒解除有可能使系统获得较好的资源利用率和吞吐量,但在实现上难度也最大。

银行家算法:

内存管理

页面置换算法

在程序运行过程中,如果要访问的页面不在内存中,就发生缺页中断从而将该页调入内存中。此时如果内存已无空闲空间,系统必须从内存中调出一个页面到磁盘对换区中来腾出空间。

在缓存系统中,缓存的大小有限,当有新的缓存到达时,需要淘汰一部分已经存在的缓存,这样才有空间存放新的缓存数据。

页面置换算法的主要目标是使页面置换频率最低(也可以说缺页率最低)。

1、最佳置换算法(OPT)(理想置换算法)

这是一种理想情况下的页面置换算法,但实际上是不可能实现的。该算法的基本思想是:发生缺页时,有些页面在内存中,其中有一页将很快被访问(也包含紧接着的下一条指令的那页),而其他页面则可能要到10、100或者1000条指令后才会被访问,每个页面都可以用在该页面首次被访问前所要执行的指令数进行标记。最佳页面置换算法只是简单地规定:标记最大的页应该被置换。这个算法唯一的一个问题就是它无法实现。当缺页发生时,操作系统无法知道各个页面下一次是在什么时候被访问。虽然这个算法不可能实现,但是最佳页面置换算法可以用于对可实现算法的性能进行衡量比较。

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

最简单的页面置换算法是先入先出(FIFO)法。这种算法的实质是,总是选择在主存中停留时间最长(即最老)的一页置换,即先进入内存的页,先退出内存。理由是:最早调入内存的页,其不再被使用的可能性比刚调入内存的可能性大。建立一个FIFO队列,收容所有在内存中的页。被置换页面总是在队列头上进行。当一个页面被放入内存时,就把它插在队尾上。

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

FIFO算法和OPT算法之间的主要差别是,FIFO算法利用页面进入内存后的时间长短作为置换依据,而OPT算法的依据是将来使用页面的时间。如果以最近的过去作为不久将来的近似,那么就可以把过去最长一段时间里不曾被使用的页面置换掉。它的实质是,当需要置换一页时,选择在最近一段时间里最久没有使用过的页面予以置换。这种算法就称为最久未使用算法(Least Recently Used,LRU)。
LRU算法是与每个页面最后使用的时间有关的。当必须置换一个页面时,LRU算法选择过去一段时间里最久未被使用的页面。

4、Clock置换算法

给每个页帧关联一个使用位。当该页第一次装入内存或者被重新访问到时,将使用位置为1。每次需要替换时,查找使用位被置为0的第一个帧进行替换。在扫描过程中,如果碰到使用位为1的帧,将使用位置为0,在继续扫描。如果所谓帧的使用位都为0,则替换第一个帧。

分页与分段的比较

1、对程序员的透明性:分页透明,但是分段需要程序员显示划分每个段。

2、地址空间的维度:分页是一维地址空间,分段是二维的。

3、大小是否可以改变:页的大小不可变,段的大小可以动态改变。

出现的原因:分页主要用于实现虚拟内存,从而获得更大的地址空间;分段主要是为了使程序和数据可以被划分为逻辑上独立的地址空间并且有助于共享和保护。

设备管理

磁盘调度算法:

1、先来先服务

按照磁盘请求的顺序进行调度。

优点是公平和简单。缺点也很明显,因为未对寻道做任何优化,使平均寻道时间可能较长。

2、最短寻道时间优先

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

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

3、电梯算法

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

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

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