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.4.1 宏内核
宏内核是将操作系统功能作为一个紧密结合的整体放到内核。 由于各模块共享信息,因此有很高的性能。
1.4.2 微内核
由于操作系统不断复杂,因此将一部分操作系统移出内核,从而降低内核的复杂性。移出的部分根据分层的原则划分成若干服务,相互独立。
在微内核结构下,操作系统被划分成小的、定义良好的模块,只有微内核这一个模块运行在内核态,其余模块运行在用户态。
因为需要频繁地在用户态和核心态之间进行切换,所以会有一定的性能损失。
1.5 中断分类
1.5.1 外中断
由CPU执行指令以外的时间引起,如I/O完成中断,表示设备输入/输出处理已经完成,处理器能够发送下一个输入/输出请求。此外还有时钟中断、控制台中断等。
1.5.2 异常
由CPU执行指令的内部时间引起,如非法操作码、地址越界、算术溢出等。
1.5.3 陷入
在用户程序中使用系统调用。
2 进程管理
2.1 进程与线程
2.1.1 进程
进程是资源分配的基本单位。
进程控制块(Process Control Block,PCB)描述进程的基本信息和运行状态,所谓的创建进程和撤销进程,都是指对PCB的操作。
2.1.2 线程
线程是独立调度的基本单位。
一个进程中可以有多个线程,它们共享进程资源。
eg:QQ 和浏览器是两个进程,浏览器进程里面有很多线程,例如 HTTP 请求线程、事件响应线程、渲染线程等等,线程的并发执行使得在浏览器中点击一个新链接从而发起 HTTP请求时,浏览器还可以响应用户的其它事件。
2.1.3 区别
- 拥有资源
进程是资源分配的基本单位,但是线程不拥有资源,线程可以访问隶属进程的资源。
- 调度
线程是独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换,从一个进程中的线程切换到另一个进程中的线程时,会引起进程切换。
- 系统开销
由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O设备等,所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程CPU环境的保存及新调度进程CPU环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销很小。
- 通信方面
线程间可以通过直接读写同一进程中的数据进行通信,但是进程通信需要借助IPC。
2.2 进程状态的切换
- 就绪状态(ready):等待被调度
- 运行状态(running)
- 阻塞状态(waiting):等待资源
应该注意以下内容:
- 只有就绪态和运行态可以相互转换,其它都是单向转换。就绪状态的进程通过调度算法从而获得CPU时间,转为运行状态;而运行状态的进程,在分配给它的CPU时间片用完之后就会转为就绪状态,等待下一次调度。
- 阻塞状态是缺少需要的资源从而由运行状态转换而来,但是该资源不包括CPU时间,缺少CPU时间会从运行态转换为就绪态。
2.3 进程调度算法
不同环境的调度算法目标不同,因此需要针对不同环境来讨论调度算法。
2.3.1 批处理系统
批处理系统没有太多的用户操作,在该系统中,调度算法目标是保证吞吐量和周转时间(从提交到终止的时间)。
2.3.1.1 先来先服务 first-come first-serverd (FCFS)
非抢占式的调度算法,按照请求的顺序进行调度。
有利于长作业,但不利于短作业,因为短作业必须一直等待前面的长作业执行完毕才能执行,而长作业又需要执行很长时间,造成了短作业等待时间过长。
2.3.1.2 短作业优先 shortest job first (SJF)
非抢占式的调度算法,按估计运行时间最短的顺序进行调度。
长作业有可能会饿死,处于一直等待短作业执行完毕的状态。因为如果一直有短作业到来,那么长作业永远得不到调度。
2.3.1.3 最短剩余时间优先 shortest remaining time next (SRTN)
最短作业优先的抢占式版本,按剩余运行时间的顺序进行调度。当一个新的作业到达时,其整个运行时间与当前进程的剩余时间作比较。如果新的进程需要的时间更少,则挂起当前进程,运行新的进程,否则新的进程等待。
2.3.2 交互式系统
交互式系统有大量的用户交互操作,在该系统中调度算法的目标是快速地进行响应。
2.3.2.1 时间片轮转
将所有就绪进程按FCFS的原则排成一个队列,每次调度时,把CPU时间分配给队首进程,该进程可以执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序便停止该进程的执行,并将它送往就绪队列的末尾,同时继续把CPU时间分配给队首的进程。
时间片轮转算法的效率和时间片的大小有很大关系:
- 因为进程切换都要保存进程的信息并且载入新进程的信息,如果时间片太小,会导致进程切换得太频繁,在进程切换上就会花过多时间。
- 而如果时间片过长,那么实时性就不能得到保证。
2.3.2.2 优先级调度
为每个进程分配一个优先级,按优先级进行调度。
为了防止低优先级的进程永远等不到调度,可以随着时间的推移增加等待进程的优先级。
2.3.2.3 多级反馈队列
一个进程需要执行100个时间片,如果采用时间片轮转调度算法,那么需要交换100次。
多级队列是为这种需要连续执行多个时间片的进程考虑,它设置了多个队列,每个队列时间片大小都不同,例如1,2,4,8…。进程在第一个队列没执行完,就会被移到下一个队列。这种方式下,之前的进程只需要交换7次。
每个队列优先权也不同,最上面的优先权最高。因此只有上一个队列没有进程在排队,才能调度当前队列上的进程。
可以将这种调度算法看成是时间片轮转调度算法和优先级调度算法的结合。
2.3.3 实时系统
实时系统要求一个请求在一个确定时间内得到响应。
分为硬实时和软实时,前者必须满足绝对的截止时间,后者可以容忍一定的超时。
2.4 进程同步
2.4.1 临界区
对临界资源进行访问的那段代码称为临界区。
为了互斥访问临界资源,每个进程在进入临界区之前,需要先进行检查。
1 | // entry 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表示临界区解锁。
1 | typedef int semaphore; |
2.4.4 管程
使用信号量机制实现的生产者消费者问题需要客户端代码做很多控制,而管程把控制的代码独立出来,不仅不容易出错,也使得客户端代码调用更容易。
略。
2.5 经典同步问题
2.5.1 使用信号量实现生产者-消费者问题
问题描述:
1 | 使用一个缓冲区来保存物品,只有缓冲区没有满,生产者才可以放入物品;只有缓冲区不为空,消费者才可以拿走物品。 |
因为缓冲区属于临界资源,因此需要使用一个互斥量mutex
来控制缓冲区的互斥访问。
为了同步生产者和消费者的行为,需要记录缓冲区中物品的数量。数量可以使用信号量来进行统计。
这里需要使用两个信号量:empty
记录空缓冲区的数量,full
记录满缓冲区的数量。
其中,empty
信号量是在生产者进程中使用,当empty
不为0时,生产者才可以放入物品;full
信号量时在消费者进程中使用,当full
信号量不为0时,消费者才可以取走物品。
注意:不能先对缓冲区进行加锁,再测试信号量。也就是说,不能先执行down(mutex)再执行down(empty)。如果这么做了,那么可能会出现这种情况:生产者对缓冲区加锁后,执行down(empty)操作,发现empty=0,此时生产者睡眠,消费者不能进入临界区,因为生产者对缓冲区加锁了,消费者就无法执行up(empty)操作,empty永远都为0,导致生产者永远等待下去,不会释放锁,消费者因此也会永远等待下去。
1 | # |
2.5.2 哲学家进餐问题
问题描述:
1 | 五个哲学家围着一张圆桌,每个哲学家面前放着食物。哲学家的生活有两种交替活动:吃饭以及思考。当一个哲学家吃饭时,需要先拿起自己左右两边的两根筷子,并且一次只能拿起一根筷子。 |
下面是一种错误的解法,如果所有哲学家同时拿起左手边的筷子,那么所有哲学家都在等待其他哲学家吃完并释放自己手中的筷子,导致死锁。
1 | # |
为了防止死锁的发生,可以设置两个条件:
- 必须同时拿起左右两根筷子;
- 只有再两个邻居都没有进餐的情况下才允许进餐。
1 | # |
2.5.3 读者-写者问题
问题描述:
1 | 允许多个进程同时对数据进行读操作,但是不允许读和写以及写和写的操作同时发生。 |
一个整型变量count
记录在对数据进行读操作的进程数量,一个互斥量count_mutex
用于对count
加锁,一个互斥量data_mutex
用于对读写的数据加锁。
1 | typedef int semaphore; |
2.5.4 理发师问题
问题描述:
1 | 假设有一个理发店只有一个理发师,一张理发时坐的椅子,若干张普通椅子顾客供等候时坐。没有顾客时,理发师就坐在理发的椅子上睡觉。顾客一到,他不是叫醒理发师,就是离开。如果理发师没有睡觉,而在为别人理发,他就会坐下来等候。如果所有的椅子都坐满了人,最后来的顾客就会离开。 |
使用三个信号量和一个控制变量:
- 控制变量
waiting
用来记录等候理发的顾客数,init = 0; - 信号量
customers
用来记录等候理发的顾客数,并用作阻塞理发师进程,init = 0; - 信号量
barbers
用来记录正在等候顾客的理发师数,并用作阻塞进程,init = 0(刚开始时理发师在睡觉,所以理发师这个资源数目为0); - 信号量
mutex
用作互斥,init = 1;
1 | # |
理发师问题和生产者消费者问题在于
- 当等候理发的凳子被坐满时候,顾客离开,即资源不满足的情况下,进程结束;当缓冲区满的时候,生产者出现等待事件,等到资源满足时往下执行,即资源不满足的情况下,进程等待。
- 当资源用信号量表示,资源不满足的情况下进程等待,所以理发师问题中,椅子不因该用信号量表示而因该用变量表示,再加上一个互斥信号量。
- 生产者消费者可以更改公共资源如缓冲区empty当前值,无论资源是否满足;而理发师问题中对于公共的资源椅子,资源条件不满足时,顾客是不能更改waiting的值。
2.6 进程通信
进程同步与进程通信很容易混淆,它们的区别在于:
- 进程同步:控制多个进程按一定顺序执行;
- 进程通信:进程间传输信息。
进程通信是一种手段,而进程同步是一种目的。也可以说,为了能够达到进程同步的目的,需要让进程进行通信,传输一些进程同步所需要的信息。
2.6.1 管道
管道是通过调用pipe
函数创建的,fd[0]用于读,fd[1]用于写。
1 | # |
它具有以下限制:
- 它支支持半双工通信(单向交替传输);
- 只能在父子进程或者兄弟进程中使用。
2.6.2 FIFO
也称为命名管道,去除了管道只能在父子进程中使用的限制。
1 | # |
FIFO常用于客户-服务器应用程序中,FIFO用作汇聚点,在客户进程和服务器进程之间传递数据。
2.6.3 消息队列
相比于FIFO,消息队列具有以下优点:
- 消息队列可以独立于读写进程存在,从而避免了FIFO中同步管道的打开和关闭时可能产生的困难;
- 避免了FIFO的同步阻塞问题,不需要进程自己提供同步方法;
- 读进程可以根据消息类型有选择地接收消息,而不像FIFO那样只能默认地接收。
2.6.4 信号量
它时一个计数器,用于为多个进程提供对共享数据对象的访问。
2.6.5 共享存储
允许多个进程共享一个给定的存储区。因为数据不需要在进程之间复制,所以这是最快的一种IPC。 需要使用信号量用来同步对共享存储的访问。 多个进程可以将同一个文件映射到它们的地址空间从而实现共享内存。另外XSI共享内存不是使用文件,而是使用内存的匿名段。
2.6.6 套结字
与其它通信机制不同的是,它可用于不同机器间的进程通信。
3 死锁
3.1 必要条件
- 互斥:每个资源要么已经分配给了一个进程,要么就是可用的。
- 占有和等待:已经得到了某个资源的进程可以再请求新的资源。
- 不可抢占:已经分配给一个进程的资源不能强制性地被抢占,它只能被占有它的进程显式地释放。
- 环路等待:有两个或者两个以上的进程组成的一条环路,该环路中的每个进程都在等待下一个进程所占有的资源。
3.2 处理方法
主要有以下四种方法:
- 鸵鸟策略
- 死锁检测与死锁恢复
- 死锁预防
- 死锁避免
3.3 鸵鸟策略
把头埋在沙子里,假装根本没发生问题。
因为解决死锁问题的代价很高,因此鸵鸟策略这种不采取任务措施的方案会获得更高的性能。
当发生死锁时不会对用户造成多大影响,或发生死锁的概率很低,可以采用鸵鸟策略。
大多数操作系统,包括 Unix,Linux和Windows,处理死锁问题的办法仅仅是忽略它。
3.4 死锁检测与死锁恢复
不试图阻止死锁,而是当检测到思索发生时,采取措施进行恢复。
3.4.1 每种类型一个资源的死锁检测
上图为资源分配图,其中方框表示资源,圆圈表示进程。资源指向进程表示该资源已经分配给该进程,进程指向资源表示进程请求获取该资源。
图 a 可以抽取出环,如图 b,它满足了环路等待条件,因此会发生死锁。
每种类型一个资源的死锁检测算法是通过检测有向图是否存在环来实现,从一个节点出发进行深度优先搜索,对访问过的节点进行标记,如果访问了已经标记的节点,就表示有向图存在环,也就是检测到死锁的发生。
3.4.2 每种类型多个资源的死锁检测
上图中,有三个进程四个资源,每个数据代表的含义如下:
- E 向量:资源总量
- A 向量:资源剩余量
- C 矩阵:每个进程所拥有的资源数量,每一行都代表一个进程拥有资源的数量
- R 矩阵:每个进程请求的资源数量
进程 P1 和 P2 所请求的资源都得不到满足,只有进程 P3 可以,让 P3执行,之后释放 P3 拥有的资源,此时 A = (2 2 2 0)。P2 可以执行,执行后释放 P2 拥有的资源,A = (4 2 2 1) 。P1也可以执行。所有进程都可以顺利执行,没有死锁。
算法总结如下:
每个进程最开始时都不被标记,执行过程有可能被标记。当算法结束时,任何没有被标记的进程都是死锁进程。
- 寻找一个没有标记的进程Pi,它所请求的资源小于等于A。
- 如果找到了这样一个进程,那么将C矩阵的第i行向量加到A中,标记该进程,并转回1。
- 如果没有这样一个进程,算法终止。
3.4.3 死锁恢复
- 利用抢占恢复
- 利用回滚恢复
- 通过杀死进程恢复
3.5 死锁预防
再程序运行之前预防发生死锁。
3.5.1 破坏互斥条件
例如假脱机打印机技术允许若干个进程同时输出,唯一真正请求物理打印机的进程是打印机守护进程。
3.5.2 破坏占有和等待条件
一般实现方式是规定所有进程在开始执行前请求所需要的全部资源。
3.5.3 破坏不可抢占条件
3.5.4 破坏环路等待条件
给资源统一编号,进程只能按编号顺序来请求资源。
3.6 死锁避免
在程序运行时避免发生死锁。
3.6.1 银行家算法
3.6.1.1 数据结构
Available
:可利用资源向量,代表系统中所配置的该类全部可用资源的数量Max
: 最大需求矩阵,代表进程对资源的最大需求Allocation
:定义系统中每一类资源已分配给每一进程的资源数Need
:表示进程还需要的各类资源数,Need[i, j] = Max[i, j] - Allocation[i, j]
3.6.1.2 算法
设Request
是进程给Pi的请求向量,若Request[j] = k
表示进程Pi需要k个j资源,当发出请求后,会进行以下检查:
Request[j] <= Need[j]
,请求合理,转向2,否则认为请求不合理Request[j] <= Available[j]
,请求合理,转向3,否则表示没有足够资源,进程需等待- 尝试将资源分配给进程Pi:
Available[j] = Available[j] - Request[j]
Allocation[j] = Allocation[j] + Request[j]
Need[j] = Need[j] - Request[j]
- 运行安全性算法检查,检查资源分配后系统是否处于安全状态,安全,正式分配,否则恢复资源分配,进程继续等待
3.6.2 安全性算法
设置工作向量Work表示系统可提供给进程继续运行所需的给雷资源数目(Work = Availalbe
),设置Finish
,表示系统是否有足够的资源分配给进程 从进程集合中找到一个满足下列条件的进程:
Finish[i] = False
Need[i, j] <= Work[j]
找到则分配资源: Work[j] = Work[j] + Allocation[i, j]
Finish[i] = True
重复寻找进程的步骤
如果所有进程Finish[i] = True
,则系统处于安全状态,否则系统处于不安全状态
3.6.3 什么是安全状态与不安全状态?安全状态和不安全状态和死锁之间的关系?
- 系统能按某种进程顺序,若{p1, p2, …, pn}为每个进程分配所需资源,直到最大需求,是每个进程都可顺利完成,这个进程序列即为安全序列,不存在安全序列,则称系统处于不安全状态
- 并非所有不安全状态都将导致死锁状态,但当系统进入不安全状态后,有可能进入死锁状态,只要系统处于安全状态,则可避免进入死锁状态
- (安全性检查中使用的MAX,是进程执行前提供的,实际执行过程中,进程需要的最大资源量可能小于Max,例如:进程需要错误处理的代码需要n个资源,实际运行过程中没触发错误,就不会请求资源)
4 内存管理
4.1 虚拟内存
虚拟内存的目的是为了让物理内存扩充成更大的逻辑内存,从而让程序获得更多的可用内存。
为了更好的管理内存,操作系统将内存抽象成地址空间。每个程序拥有自己的地址空间,这个地址空间被分割成多个块,每一块称为一页。这些页被映射物理内存,但不需要映射到连续的物理内存,也不需要所有页都必须在物理内存中。当程序引用到不在物理内存中的页时,由硬件执行必要的映射,将缺失的部分装入物理内存并重新执行失败的指令。
从上面的描述中可以看出,虚拟内存允许程序不用将地址空间中的每一页都映射到物理内存,也就是说一个程序不需要全部调入内存就可以运行,这使得有限的内存运行大程序成为可能。例如有一台计算机可以产生16位地址,那么一个程序的地址空间范围是0\~64K。该计算机只有32KB的物理内存,虚拟内存技术允许该计算机运行一个64K大小的程序。
4.2 分页系统地址映射
本文作者:jujimeizuo
本文地址: https://blog.jujimeizuo.cn/2022/05/11/os/
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0 协议。转载请注明出处!