中断
一开始的计算机只是简单的串行执行程序。
现在的操作系统不仅可以并发执行程序,而且收到中断指令时,CPU 会切换到内核模式,中断当前程序的执行,按中断指令调整程序执行顺序,然后恢复到用户态继续执行。
中断分内中断、外中断。区别在于中断指令来自于 CPU 内部还是外部。
系统调用
我们知道计算机硬件为了供用户使用,向上层提供了一些接口。用户直接使用的接口叫命令接口;用户通过应用程序间接使用的接口叫程序接口。系统调用是操作系统提供给应用程序的接口。
系统调用可以增加安全性,不让用户可以直接随意访问所有功能。如两个人去打印店用打印机,第一个人打到一半第二个人发送了他的打印任务请求,可打印机最终还是有序地把两个人的任务分别打印好了。如果用户能直接让打印机打印自己的任务,不加协调,无法实现这样的结果。
哪些操作要通过系统调用的方式进行?凡是和资源相关的。这样可以保证系统的安全性和稳定性。
编程语言提供的一些库函数也是从下往上提供的一些封装的功能。但库函数不一定是系统调用。如求绝对值的库函数,这个库函数就不是系统调用,用户直接就能访问。
现在大多数系统调用都是高级语言中封装的部分库函数。
陷入指令核心态不能执行,可以理解为:核心态只能执行系统调用,不能发起系统调用给自己。
进程
进程(process)是操作系统中最基本的抽象。
进程就是运行中的程序,程序本身只是存放在磁盘上的一些静态指令,是操作系统让其运行起来。
现在我们的计算机可以同时运行上百个进程,是通过虚拟化 CPU 而实现。每个进程只运行一个时间片段,然后跳转到其他进程,造成多个进程同时运行的假象。
内存中存放每个进程的程序段和数据段,但内存怎么知道哪个是哪个进程的?通过一种数据结构叫进程控制块(PCB)找到对应进程的额程序段和数据段。程序段、数据段、PCB 组成了进程实体。
操作系统需要一些低级机制(mechanism)切换程序运行,如上下文切换(context switch,停止当前程序,并运行另一个程序);还需要一些智能决定要切换到哪个程序,如策略(poliicy,根据一些算法判断要运行哪个程序,如“哪个程序在上一分钟运行的时间更长?”)
进程的几种状态
就绪态就是除了处理机其他资源都准备就绪了。阻塞态还需要准备资源才能进入就绪态。
进程控制
就是这几种进程状态之间的切换。
通过两个指针实现:就绪队列指针和阻塞队列指针,用于存放就绪和阻塞的进程(阻塞队列可能还有好几个,按阻塞原因分组)。
状态切换使用原语,因为原语执行过程中不会受到中断的干扰。
原语做的操作无非是:1. 修改 PCB;2. 把 PCB 放到对应队列中;3. 分配/释放资源。
进程通信
进程之间互相通信,安全起见不能直接进行。
- 共享存储,共用一块存储空间。有基于数据结构的分享(给定数据结构存储方式)和基于存储区的分享(内存中划定一块存储区,进程自己决定怎么存储)。
共享的缓冲区叫做管道,如果只采用一个管道只能使用半双工型,全双工型需要两根管道。两个进程访问该管道要互斥的访问。
管道写满才能读,读空了才能写。
读出来的数据就直接被丢弃了。所以安全起见只能有一个读的进程。
- 消息传递。类似计网的数据报,消息封装好之后发给另一个进程的消息队列。
线程
让一个进程可以并发执行多个任务。比如 qq 聊天的同时可以发文件收消息发消息。
一个进程包含多个线程。线程是调度, CPU 的程序执行单元,进程是资源分配的单元,比如把显示器资源分配给 QQ。
线程有两种实现方式:
- 用户级线程 (ULT),进程的切换由应用程序实现,而不需要操作系统管理,因此用户态下就能实现线程的切换,且线程的存在对用户透明,对操作系统不透明。
- 内核级线程(KLT),线程管理靠操作系统在核心态下实现、
- 两者组合的形式,n 个用户级线程映射到 m 个内核级线程上。
操作系统分配 CPU 处理机只能分配给内核级线程(因为用户级线程对操作系统来说不透明)。所以如果有三个用户级线程,但这个应用程序只有两个内核级线程,最多也只能被分配到两个处理机,最多也只能有两个用户线程并发执行(哪怕这个操作系统是三核的,四核的,有很多核空闲出来)。
几个用户级进程映射到几个内核级进程上?这就是多线程模型问题。
- 多个用户级进程映射到一个内核级进程上。
线程切换不用在核心态下进行,切换效率高,但是并发度不高。
- 一对一。
优缺点正好和1相反。
- n 对 m,用户级线程多于内核级线程,较为折中。
进程调度和切换
线程数往往多于处理机数,因此要考虑按怎样的算法分配处理机。
进程调度和切换的区别是什么?
进程调度先选再切换。
进程切换包括:
- 保存原来运行的数据
- 恢复新进程的数据。
切换会影响效率。
调度层次1:高级调度(→就绪态)
首先先不说处理机够不够处理内存中的线程,有时候线程多到内存中放不下。高级调度需要按一定的原则从外存中挑选一些作业放到内存中并建立进程(PCB),让他们有进一步竞争处理机的机会。主要解决的是调入问题。
调度层次2:中级调度(挂起态→就绪态)
引入虚拟存储技术之后,暂时不能运行的进程可以先调至外存等待(挂起)。等可以运行再拿回来,这样能提高内存利用率和系统吞吐量。
其对应的 PCB 并不会一起移出内存,而是存储了被挂起的进程的信息,被放到内存中的挂起队列里。
中级调度就是挑选挂起的进程调入内存。
引入挂起的进程实际上可以说是有七种状态。不能运行的进程都会先放到就绪挂起态或阻塞挂起态,能运行再回到内存(有的操作系统阻塞挂起态直接回到就绪挂起态)。注意挂起和阻塞的区别!
调度层次3:初级调度(就绪态→运行态)
就是从就绪队列中按一定算法挑一个进程来执行。
调度时机
当前运行的进程主动(运行完了,或者异常终止)或被动放弃处理机,就会发生调度。
有以下几种情况不能调度:
- 进程在处理中断时。
- 进程在操作系统内核程序临界区中。(临界资源及临界区(内核/普通)以及三种进程不能切换的情况_Unstoppable~~~的博客-CSDN博客_内核临界区 在此感谢这位博主!调度区和调度资源是两回事,调度区又分普通调度区和内核调度区。)
- 原语执行时.
调度方式
非剥夺/非抢占调度方式:只允许进程主动放弃处理机。哪怕有更紧急的进程到达,也不会把当前正在使用处理机的进程挤开。开销小,但没法处理紧急情况。
剥夺/抢占调度方式:允许进程主动或被动放弃处理机。
调度算法评价指标
CPU 利用率:CPU 有活干的时间/总时间。
系统吞吐量:完成了的作业道数/总用时。
周转时间:提交作业到作业完成用时。包括在外存等待高级调度→在内存就绪队列等待低级调度→在 CPU 上执行→等待 I/O 操作完成的时间。
注意概念问题,进程是运行中的程序,所以作业在外存的时候不可以被称为进程。进入内存才创建了进程。
带权周转时间:周转时间/实际运行时间。越小越好(排队用时少吧)。
等待时间:作业等待处理机状态的时间之和。(不包括 IO)
响应时间:用户提交请求到首次产生响应的时间。
调度算法
早期批处理调度算法:只根据等待时间和预估处理时间调度,不考虑是否紧急。
算法名 | 思想 | 规则 | 用于何种调度 | 是否可抢占 | 优缺点 | 是否会导致饥饿(某个作业长期得不到服务) |
---|---|---|---|---|---|---|
先来先服务 FCFS | 公平 | 先来后到 | 都用 | 非抢占式 | 公平;但排在后面的短作业体验差 | 不会 |
短作业优先 SJF | 让平均等待、周转、带权周转时间最短 | 最短的作业优先服务 | 都用 | 非抢占式(最短剩余优先算法是抢占式的,当有进程入队的时候立刻调度。平均时间抢占式的更少) | 平均时间少,但对长作业不公平,可能“饥饿 | 会 |
高响应比优先 HRRN | 综合考虑等待时间和处理时间 | $\frac{等待时间+服务时间}{服务时间}$,优先执行响应比大的 | 都用 | 非抢占式(当前进程主动结束时才进行调度) | 综合考虑了等待时间和服务时间,长作业等久了也会执行 | 不会 |
后期交互式系统算法:
算法名 | 思想 | 规则 | 用于何种调度 | 是否可抢占 | 优缺点 | 是否会导致饥饿 |
---|---|---|---|---|---|---|
时间片轮转 RR | 公平轮流地为所有进程服务 | 按先来后到,轮流给各个进程一个时间片执行。如果没执行完就交给下一个进程,然后重新到队尾排队(时间片大小要合适。太大就是 FCFS 算法了,太小效率低) | 进程调度 | 是 | 公平;响应快;但是频繁切换效率低,不区分紧急程度 | 不会 |
优先级调度 | 按紧急程度处理 | 优先度高的先执行 | 都用 | 是 | 可以优先处理紧急任务;但总是高优先级任务到来可能饥饿 | 会 |
多级反馈队列调度算法 | 根据时间片计算优先级 | 进程刚到达放入1队列,一个时间片内没完成放入2队列,还没完成一直往后放,如果已经在最后一个队列了就重新放到该队列结尾。1队列优先级最高 | 进程调度 | 是 | 综合了各个调度算法优点 | 会 |
进程同步和互斥
虽然之前提过进程是异步的,各个进程相互独立,但是有的工作是有顺序的,比如先读入再写。
同步,即相互制约,指部分工作的次序需要协调。
互斥:一些共享资源不允许多个进程同时访问。比如一次只允许一个进程访问的资源叫临界资源。
进程互斥遵循以下原则:
- 空闲让进
- 忙则等待
- 有限等待,防止饥饿,不让进程等太久
- 让权等待,如果该进程进不去,那就不给他了,赶紧给别人。
进程互斥软件实现方法
- 单标志法
一开始只允许 P1 访问。一直到P1把 turn 变为0,然后切时间片的时候才能交给 P0. 两者交替访问。
但是违背了空闲让进。如果只有 P0 想访问临界区,就一直进不去。
- 双标志先检查法
判断对方有没有在访问。
但是如果按照①⑤③②⑥⑦的顺序,两者会同时访问临界资源,违反了忙则等待。
- 双标志后检查法
先上锁再检查。但是两个进程要是都先锁住了,就都执行不了了,可能出现死锁问题。
- Peterson 算法
可惜没有遵守让权等待。
进程互斥硬件实现方法
- 中断屏蔽方法
访问临界资源的时候把中断关上;访问完了打开,再允许调度。但是用户态不适用(用户态不应该搞中断的问题),而且对多处理机不适用。
- TestAndSet 指令
是硬件实现方法,下图为软件实现方式。
如果 lock 原来是 true,那么其他进程访问的时候一直卡在 while 处。知道访问临界区的进程退出并解锁,其他进程访问的时候 lock 才是 false,才能跳出循环。
但是无法解决让权等待问题,如果有一个进程把 lock 变成 true 之后又进不去临界区,其他进程就永远无法访问,一直忙等。
- Swap 指令
逻辑上等同于 TS,也无法解决让权等待问题。
- 信号量
信号量用于表示系统中某种资源的数量。我们用一对原语(等待 P,信号 V)对信号量做操作.信号量涉及到的三个操作就是初始化、P、V。
然而这样的信号量也没能解决让权等待问题。不过一个记录型信号量可以解决。
释放完资源,S.value≤0 说明还有进程在阻塞队列中,直接把当前刚释放的处理机给阻塞队列的进程执行。
这样如果该进程进不去自己的处理机,就会把自己调整到堵塞态,把处理机让出来给别人,解决了让权等待问题。
信号量实现进程同步和互斥
互斥:
同步:如怎样保证2一定在4之前执行
进程种类
生产者和消费者
生产者把产品放入未满的缓冲区,消费者从未空的缓冲区取出。两者要互斥地对缓冲区访问;同时如果生产者要放入满缓冲区要先等消费者取出,消费者要从空缓冲区取要先等生产者放,这两件事是同步关系。
注意 决定互斥的 mutex 顺序不能和决定同步的 empty full 颠倒!!!
如上图,如果先进行①②,则互斥锁打开,生产者因为仓库已满无法放入,阻塞。消费者又因为互斥锁打开无法取东西,阻塞,就死锁了。
多生产者 多消费者
缓冲区大小(盘子)如果为1,那么不用互斥变量也能解决。
因为苹果、橘子、盘子变量同时只能有一个=1,每次最多也只能有一个变量访问。如果缓冲区变为2就需要 mutex 了,父母同时放入水果可能覆盖对方的值。
吸烟者问题
因为只有一个缓冲区,所以不需要 mutex 就能实现互斥。
吸烟者问题其实是实现了一个生产者生产多种产品的问题。此例中生产者生产的产品顺序固定,还可以修改生产顺序逻辑。
读者-写者问题
允许多人同时读,但是有人在写的时候其他人不能读写。
读进程有限的解决办法:mutex 用于所有读进程互斥访问 count 变量,当 count 变量=0时才可写入。但是写进程有饿死的风险。
如果再加一个写的互斥信号量,就能让写操作优先于读操作了。
哲学家进餐问题
一个进程同时持有多个临界资源的情况。
如果只是简单地用两个信号量判断左右两根筷子是否空闲,可能所有进程并发拿起了左手的筷子,并发地卡住了右手的筷子,造成死锁。
有几种解决方案:①拿两只筷子的行为添加一个互斥信号量。这样一个哲学家拿不起来被阻塞的时候,其他哲学家也不会尝试拿。
②奇数先拿左手,偶数先拿右手,这样相邻的人拿筷子就会互斥。