进程的概念、组成、特征
进程的概念
程序:是静态的,就是一个存放在磁盘里的可执行文件,就是一系列的指令集合
进程:是动态的,是程序的一次执行过程
同一程序多次执行会产生多个不同的进程
为了区分不同的进程,当进程被创建时,操作系统会为该进程分配一个唯一的、不重复的编号 —— PID(Process ID,进程ID)
进程的组成
PCB
操作系统要记录PID、进程所属用户ID(UID)
- 基本的进程描述信息,可以让操作系统区分各个进程
还要记录给进程分配了哪些资源(如:分配了多少内存、正在使用哪些I/O设备、正在使用哪些文件)
- 可用于实现操作系统对资源的管理
还要记录进程的运行情况(如:CPU使用时间、磁盘使用情况、网络流量使用情况)等
- 可用于实现操作系统对进程的控制、调度
这些信息都被保存在一个数据结构PCB(Process Control Block)中,即进程控制块
PCB是进程存在的唯一标志,当进程被创建时,操作系统为其创建PCB,当进程结束时,会回收其PCB
操作系统需要对各个并发运行的进程进行管理,但凡是在管理进程时所需要的信息,都会被放在PCB中
PCB中保存的信息大致可分为:
程序段、数据段
程序段:用于保存程序的代码(指令序列)
数据段:用于保存程序运行过程中产生的各种数据(如程序中定义的变量)
PCB是给操作系统用的
程序段、数据段是给进程自己用的,与进程自身运行逻辑有关
程序是如何运行的?
一个程序,经过编译和链接后,会形成一个可执行文件,这个可执行文件会被存放在磁盘中
可执行文件中的内容其实就是程序对应的二进制机器指令序列,程序运行前,需要把程序从磁盘读入内存并创建一个与之对应的进程
操作系统会创建相应的PCB,然后操作系统会把程序的机器指令序列读入内存(即程序段),程序执行的过程就是CPU从程序段中一条一条取指令并执行的过程,在执行指令的过程中,可能会使用到一些中间的数据,因此操作系统还会开辟一块空间(即数据段)用于保存程序中产生的数据
一个进程实体(进程映象)由PCB、程序段和数据段组成
所说的进程由哪些部分组成,其实严格来讲是进程实体由哪些部分组成
进程是动态的,进程实体是静态的,进程实体反映了进程在某一时刻的状态
进程是进程实体的运行过程,是系统进行资源分配和调度的一个独立单位
一个进程的“调度”就是操作系统决定让这个进程上CPU运行
在没有引入线程的情况下,进程是系统进行调度的一个独立单位是正确的
同一程序多次运行会产生多个进程,这些进程的PCB和数据段各不相同,但程序段的内容都会相同的
进程的特征
程序是静态的,进程是动态的,相比于程序,进程具有以下特征:
-
动态性
进程是程序的一次执行过程,是动态地产生、变化和消亡的。动态性是进程的最基本特征
-
并发性
内存中有多个进程实体,各进程可以并发执行
-
独立性
进程是能独立运行、独立获得资源、独立接受调度的基本单位(引入线程后,线程就是独立接受调度的基本单位)
-
异步性
各进程按各自独立的、不可预知的速度向前推进,异步性会导致并发程序执行结果的不确定性,操作系统要提供“进程同步机制”来解决这些问题
-
结构性
每个进程都会配置一个PCB,结构上看,进程由程序段、数据段和PCB组成
总结
进程的状态与转换、进程的组织
进程的状态
创建态
进程正在被创建时(即PCB、数据段和程序段正在被创建时),它的状态为“创建态”,在这个阶段操作系统会为进程分配资源,初始化PCB
就绪态
当进程创建完成后或原本处于阻塞态的进程它所等待的事件发生了,便会进入“就绪态”,处于就绪态的进程已经具备运行条件,若此时CPU不空闲,则暂时不能运行处于该就绪态的进程
处于就绪态的进程,具备了运行的条件,但是CPU还没有执行它,所以就绪态进程具备除CPU外的所有运行条件
运行态
同一时刻系统中可能会有多个处于就绪态的进程,当CPU空闲时,操作系统就会选择一个就绪进程让它上CPU运行,一个进程此时正在CPU上运行,那么该进程将就处于“运行态”
处于运行态的进程具备所有运行条件,包括CPU
阻塞态
在进程运行的过程中,可能会请求等待某个事件的发生(如等待某种除处理机外的其它系统资源分配给自己或等待其他进程的响应自己等),而在这个事件发生之前,进程无法继续往下执行,此时操作系统会让这个进程下CPU,并让它进入“阻塞态”,此时CPU又处于空闲状态,于是又选择另一个“就绪态”的进程上CPU运行
如此时CPU正在执行进程1中的指令,当执行到某条打印指令时,发现打印机此时正在为其它进程服务,于是操作系统就会让进程1下CPU并更改进程的状态为“阻塞态”
当打印机服务完其它进程后,就可以为进程1服务,此时操作系统可以分配打印机资源给进程1,并让进程1从“阻塞态”变换为“就绪态”,于是进程1就再次具备上CPU运行所需的条件
处于阻塞态的进程不具备CPU资源以及完整的其它运行条件(因为正在等待某个事件的发生,事件发生前进程无法进行运行)
终止态
一个进程可以执行exit系统调用,请求操作系统终止该进程,此时该进程会进入“终止态”,操作系统会让该进程下CPU,并回收内存空间等资源,最后还要回收该进程的PCB,当终止进程的工作结束之后,这个进程就彻底消失了
使用exit系统调用请求终止进程属于主动终止结束,除了主动终止结束自身外,当程序的运行过程中出现了错误,如除0等,也会导致程序运行结束,也需要操作系统对进程进行善后工作(即回收内存空间等)
进程的五个状态及其转换
进程从运行态到阻塞态的过程是一种进程自身做出的主动行为
进程从阻塞态到就绪态则不是进程自身所能控制的,是一种被动行为
如果操作系统给进程分配的时间片到时时,进程就会从运行态转换为就绪态
注意:进程不能由阻塞态直接转换为运行态,也不能由就绪态直接转换为阻塞态,因为进入阻塞态是进程主动请求的,必然需要进程在运行时才能发出这种请求
小结
运行态、就绪态和阻塞态是进程的三个基本状态,因为进程的整个生命周期大部分时间都处于这三种状态中
注意:在单个CPU情况下,同一时刻只会有一个进程处于运行态,多核CPU情况下,可能有多个进程处于运行态
在进程的PCB中,会有一个变量state来表示进程的当前状态
为了方便对同种状态下的各个进程进行统一的管理,操作系统会将各个进程的PCB组织起来
进程的组织
链接方式
操作系统会管理一系列的队列,每个队列都会保存着相应状态的PCB
索引方式
操作系统会给各种状态的进程分别建立索引表,每个索引表的表项会指向某个PCB
小结
总结
进程控制
进程控制的基本概念
进程控制的主要功能是对系统中的所有进程实施有效的管理,它具有创建新进程、撤销已有进程、实现进程状态转换等功能
简单点说,进程控制就是实现对进程状态的转换
实现进程控制
进程控制需要使用“原语”来实现,原语是一种特殊的程序,它的执行具有原子性,也就是说,原语程序的运行必须是一气呵成的,不可中断
如果进程控制的过程不能“一气呵成”,就有可能导致操作系统中的某些关键数据结构信息不统一的情况,这会影响操作系统进行别的管理工作
假设PCB中的变量state表示进程当前所处的状态,1表示就绪态,2表示阻塞态...
此时进程2所等待的事件发生,则操作系统中,负责进程控制的内核程序至少需要做这样两件事:
- 将PCB2的state设置为1
- 将PCB2从阻塞队列中放到就绪队列中
假设CPU在完成动作1时收到中断信号,那么PCB2的state此时就为1,但是它却被放在阻塞队列里
如何实现原语的“原子性”
可以用“关中断指令”和“开中断指令”这两个特权指令实现原子性
正常情况下,CPU每执行完一条指令都会例行检查是否有中断信号需要处理,如果有,则暂停运行当前这段程序,转而执行相应的中断处理程序
当CPU执行了关中断指令后,就不再例行检查中断信号,直到执行开中断指令之后才会恢复检查
这样,关中断、开中断之间的这些指令序列就是不可被中断的,这就实现了“原子性”
进程控制相关的原语
创建原语
场景:操作系统在创建一个进程时使用的原语
动作:操作系统首先会申请一个空白的PCB,并为新进程分配所需资源,然后初始化PCB,最后将PCB插入到就绪队列中
作用:创建原语会让一个进程从创建态转换为就绪态
会引起进程创建的事件有:
-
用户登录
分时系统中,用户登录成功,系统会为其建立一个新的进程
-
作业调度
多道批处理系统中,有新的作业从磁盘放入到内存中并开始运行时,系统会为其建立一个新的进程
-
提供服务
用户向操作系统提出某些请求时,会新建一个进程处理该请求
-
应用请求
由用户进程主动请求创建一个子进程
撤销原语(终止原语)
操作系统要杀死一个进程时使用的原语
操作系统会从PCB集合中找到要终止的进程的PCB,若进程正在运行,则立即剥夺进程的CPU使用权,让CPU分配给其他进程,同时还要终止其所有的子进程,之后将该进程的所有资源归还给其父进程或操作系统,最终删除其PCB
撤销原语会让一个进程从就绪态/阻塞态/运行态转换为终止态,最终进程消失
会引起进程终止的事件有:
-
正常结束
进程自己请求终止(exit系统调用)
-
异常结束
整数除0、非法使用特权指令,然后被操作系统强行杀掉
-
外界干预
用户选择结束进程(如在Windows系统的任务管理器中选择一个进程后点击“结束进程”按钮)
阻塞原语
操作系统找到要阻塞的进程对应的PCB,然后保护进程运行现场,并将PCB状态信息设置为“阻塞态”,暂时停止进程运行,最后将PCB插入到相应事件的等待队列中
阻塞原语会让进程从运行态转换为阻塞态
会引起进程阻塞的事件有:
- 需要等待系统分配某种资源
- 需要等待相互合作的其他进程完成工作
唤醒原语
操作系统从事件等待队列中找到PCB,将该PCB从等待队列中移除,并设置进程状态为就绪态,之后将PCB插入到就绪队列中,等待被调度
唤醒原语会让进程从阻塞态转换为就绪态
会引起进程唤醒的事件有:
-
等待的事件发生
进程因何种事件阻塞,就应该被何种事情唤醒
只要使用阻塞原语,那么必然会使用到唤醒原语,即阻塞和唤醒要成对出现
切换原语
操作系统将当前进程的运行环境(CPU中各种寄存器的内容)信息存入对应的PCB中,并让PCB移出相应的队列,之后操作系统选择另一个进程上CPU执行,并更新该新进程的PCB,并根据其PCB恢复新进程所需的运行环境
切换原语会让一个进程从运行态转换为就绪态,让另一个进程从就绪态转换为运行态
会引起进程切换的事件有:
- 当前进程时间片到
- 有更高优先级的进程到达
- 当前进程主动阻塞
- 当前进程终止
总结
无论哪个进程控制原语,要做的无非就是三类事情:
- 更新PCB中的信息(如修改进程状态state,保存/恢复运行环境等)
- 将PCB插入到相应的队列
- 分配或回收相关资源
进程通信(IPC)
进程间通信(Inter-Process Communication,IPC)是指两个进程之间产生数据交互
进程间通信需要操作系统的支持
进程是系统分配资源的最基本单位(包括内存地址空间),因此各进程拥有的内存地址空间是相互独立的,不同的进程只能访问属于自己的内存空间,而不能访问其他进程的内存空间
因为进程之间不能直接进行内存空间的访问,因此当两个需要数据交互时,必须操作系统作为中介
共享存储
各个进程除了拥有独属于自己的内存空间外,还可以申请一块共享的内存空间,即共享存储区,共享存储区可以被其它进程所访问
进程通过“增加页表项/段表项”即可将同一片共享内存区映射到自身进程的地址空间中
一个进程如果要给另一个进程传送数据,则只需要将数据写入到共享存储区中,之后让另一个进程从共享存储区中访问即可
为了避免出错(如两个进程同时对共享存储区中的某一块区域进行写操作),各个进程对共享空间的访问应该是互斥的,各个进程可以使用操作系统内核提供的同步互斥工具(如P、V操作)达到互斥访问共享存储区的目的
互斥操作需要由通信进程自己负责实现的
共享存储又可进一步划分为以下两种:
-
基于存储区的共享
操作系统在内存中划出一块共享存储区,数据的形式、存放位置都由通信进程控制,而不是操作系统,这种共享方式速度很快,是一种高级通信方式
-
基于数据结构的共享
比如共享空间里只能存放一个长度为10的int型数组,这种共享方式速度慢、限制多,是一种低级通信方式
消息传递
进程间的数据交换以格式化的消息(Message)为单位。进程通过操作系统提供的“发送消息/接收消息”两个原语进行数据交换
格式化的消息由两个部分组成:消息头、消息体
消息头:发送进程ID、接收进程ID、消息长度等信息
消息体:具体要传送的数据
消息传递可进一步划分为两种:
-
直接通信方式
消息发送进程要指明接收进程的ID
-
间接通信方式
通过“信箱”间接地通信,因此又称“信箱通信方式”
直接通信方式
在操作系统的内核区域管理着各个进程的PCB,在各个进程的PCB中包含了消息队列,队列中保存着其它进程发送给自己的消息
假设进程P要给进程Q发送一个消息,首先进程P会在自己的内存空间中完善消息的内容(包括消息头和消息体),最终形成消息msg
接下来进程P会使用发送原语send(Q, msg)
send(Q, msg)指明了接收消息msg的进程为Q
发送原语的执行会导致操作系统内核接收到该消息,内核接收到消息后,根据原语指明的接收进程ID(进程Q的ID),把消息挂到进程Q的消息队列中
若进程Q想要接收进程P发来的消息,可以使用接收原语receive(P, &msg)
receive(P, &msg)指明了要接收进程P发来的消息
操作系统内核在接收原语的执行作用下,从进程Q的PCB中的消息队列里取出由进程P发来的消息,并传送给进程Q
间接通信方式
每一个进程,都可以通过系统调用申请一个或多个“信箱”(信箱本质上还是内存中的一块区域)
假设进程P要给进程Q发送一个消息,首先进程P会在自己的内存空间中完善消息的内容(包括消息头和消息体),最终形成消息msg
接下来进程P会使用发送原语send(A, msg)
send(A, msg)指明了接收消息msg的信箱为A
发送原语的执行会导致信箱A接收到该消息
当进程Q想要接收进程P发来的消息,可以使用接收原语receive(A, &msg)
receive(A, &msg)指明了要从信箱A中读取进程P发来的消息
接收原语的执行会导致操作系统内核从信箱A中读取P发送给Q的消息,并将它传送给进程Q
一般来说,操作系统允许多个进程往同一个信箱send消息,也可以多个进程从同一个信箱中receive消息
管道通信
“管道”是一个特殊的共享文件,又名pipe文件,其实就是在内存中开辟一个大小固定的内存缓冲区
“管道”的申请需要使用到系统调用,因此管道通信也需要操作系统的支持
“管道”可以理解为一个循环队列,数据的流向是单向的(写进程只能从一边写数据,读进程只能从另一边读数据)且满足先进先出的特点
共享存储方式中的共享区域,写进程可以随机往该区域中的某一部分进行写入、读进程可以往该区域中的任意一部分进行读取,而管道通信中的共享区域,写进程只能固定在“队头”处写入数据,读进程只能固定在“队尾”处读取数据
由于管道通信只允许在管道的一边写入数据,另一边读出数据,因此管道通信允许读写进程同时访问同一个管道,而共享存储方式则是读写进程必须互斥访问共享存储区
管道通信的特点:
- 一个管道只能采用半双工通信,某一时间段只能实现单向的传输,如果要实现双向同时通信,则需要设置正反向的两个管道
- 各进程对管道的访问应该是互斥的(互斥操作由操作系统实现)
- 当管道写满时,写进程(的写动作)将阻塞,直到读进程将管道中的数据取走(即管道有空位后),即可唤醒写进程
- 当管道读空时,读进程(的读动作)将阻塞,直到写进程往管道中写入数据(即管道有数据后),即可唤醒读进程
- 管道中的数据一旦被读出,就彻底消失,因此,当多个进程读同一个管道时,可能会错乱,对此,通常有两种解决方案:① 一个管道允许多个写进程写入,一个读进程读取;② 一个管道允许多个写进程写入,多个读进程读取,但各个读进程只能轮流从管道中读数据
注:
- 写进程往管道写数据,只要管道没空,读进程就可以从管道中读数据
- 读进程从管道读数据,只要管道没满,写进程就可以往管道中写数据
总结
线程的概念
对于单处理机系统,在没有引入进程机制之前,系统中的各应用程序只能串行执行,如打开音乐软件听音乐时就不能使用微信进行聊天,引入进程机制之后,由于进程之间可以并发执行,就能做到各应用程序之间在宏观上并行执行
但一个应用程序,它可能包含很多功能(可以理解为每一个功能就是一个子程序),如微信可以传送文件、文字聊天、视频通话等,在没有引入线程机制前,运行一个应用程序,只能让其的某一个功能得到使用(即让其某一个子程序得到运行),如使用微信传送文件时就不能进行微信视频通话,在引入线程机制后,对线程进行并发执行(即并发执行这些子程序),就可以让一个应用程序的不同功能在宏观上并行使用
有的进程可能需要“同时”做很多事,而传统的进程只能串行地执行一系列程序,为此,引入线程来增加系统的并发度
传统的进程是程序执行流的最小单位,引入线程后,线程成为了程序执行流的最小单位
传统的进程是CPU并发执行的最小单位,引入线程后,线程成为了CPU并发执行的最小单位
线程可以理解为“轻量级进程”,线程是一个基本的CPU执行单元,也是程序执行流的最小单位
引入线程之后,不仅是进程之间可以并发,进程内的各线程之间也可以并发,从而进一步提升了系统的并发度,使得一个进程内也可以并发处理各种任务(如QQ视频、文字聊天、传送文件等)
引入线程后,进程只作为除CPU外的系统资源的分配单元(如打印机、内存地址空间等都是分配给进程的)
线程机制相比与进程机制的变化:
-
资源分配、调度
传统进程机制中,进程是资源分配以及CPU调度的基本单位
引入线程后,进程仍是资源分配的基本单位,但线程成为CPU调度的基本单位
-
并发性
传统进程机制中,只能在进程间并发
引入线程后,各线程间也能并发,提升了系统并发度
-
系统开销
传统的进程间并发,进程切换需要切换进程的运行环境,系统开销很大
线程间并发,如果是同一进程内的线程切换,则不需要切换进程环境,系统开销小
线程的属性
- (引入线程后)线程是处理机调度的基本单位
- 在多处理机系统中,各个线程可占用不同的CPU
- 每个线程都有一个线程ID、线程控制块TCB
- 线程也有就绪、阻塞、运行三种基本状态
- 系统资源在进程手上,线程不拥有系统资源,但同一进程中的不同线程间可以共享该进程所拥有的系统资源(以及进程的整个地址空间)
- 同一进程中的不同线程可以使用同一块内存地址空间,由于共享内存地址空间,同一进程中的线程间通信甚至无需系统干预
- 同一进程中的线程切换不会引起进程切换,不同进程间的线程切换会引起进程切换,就需要切换进程环境
- 切换同进程内的线程,系统开销很小;切换进程,由于需要切换进程环境,所以系统开销较大
线程的实现方式和多线程模型
线程的实现方式
用户级线程(User-Level Thread,ULT)
历史背景:早期的操作系统(如早期Unix)只支持进程,不支持(真正的)线程,当时的“线程”是由线程库实现的
int main(){
int i = 0;
while(true){
if(i == 0){
处理视频聊天的代码;
}
if(i == 1){
处理文字聊天的代码;
}
if(i == 2){
处理文件传输的代码;
}
i = (i + 1) % 3; // i的值为0,1,2,0,1,2,0,1,2...
}
}
从代码的角度看,“线程”其实就是一段代码逻辑,上述三段代码逻辑上可以看作三个“线程”,while循环就是一个最简陋的“线程库”,“线程库”完成了对线程的管理工作(如线程的切换,即线程调度)
很多编程语言提供了强大的线程库,可以实现线程的创建、销毁、调度等功能
在用户级线程下,操作系统看到的依然是进程,进程仍是CPU调度的最小单位,线程只不过是用一段代码逻辑模拟实现的,是逻辑上的“线程”
用户级线程由应用程序通过线程库实现,所有的线程管理工作都由应用程序负责(包括线程切换)
用户级线程中,线程切换在用户态下即可完成,无需操作系统干预,不需要切换状态为内核态
在用户看来,确实是有多个线程,但在操作系统内核看来,它并意识不到线程的存在,“用户级线程”就是“从用户视角看能看到的线程”
用户级线程的优缺点:
- 优点:用户级线程的切换在用户态即可完成,不需要切换到核心态,线程管理的系统开销小,效率高
- 缺点:当一个用户级线程被阻塞后,整个进程都会被阻塞(当程序运行被阻塞后,程序中的其他代码就不能运行),导致系统并发度不高;线程只是逻辑上的“线程”,进程还是CPU调度的最小单位,一个CPU一次只能执行一个进程,所以一个进程中的多个线程就不能在多个CPU上并行执行,即多个线程不可在多核处理机上并行运行
内核级线程(Kernel-Level Thread,KLT,又称“内核支持的线程”)
在内核级线程中,操作系统看到的就是真正的线程,而不是逻辑上的线程
内核级线程的管理工作由操作系统内核完成,内核级线程是操作系统能够看到的真正的线程
线程调度、切换等工作都由内核负责,因此内核级线程的切换必然需要在核心态下才能完成,这就需要系统进行状态的转换
操作系统会为每个内核级线程建立相应的TCB(Thread Control Block,线程控制块),通过TCB实现对线程进行管理。“内核级线程”就是“从操作系统内核视角看能看到的线程”
内核级线程的优缺点:
- 优点:当一个线程被阻塞后,别的线程还可以继续执行,并发能力强;在此系统中,线程成为CPU调度的基本单位,多个线程可在多核处理机的不同核上执行,从而使得多个线程并行执行
- 缺点:一个用户进程可能会拥有多个内核级线程,线程切换需要由操作系统内核完成,因此需要切换到核心态,线程切换完成后还需要转换回用户态,因此线程管理的成本高,开销大
多线程模型
在支持内核级线程的系统中,再引入线程库,就可以实现将若干个用户级线程映射为一个内核级线程
根据用户级线程和内核级线程的映射关系,可以划分为三种多线程模型
一对一模型
一个用户级线程映射到一个内核级线程,每个用户进程拥有与用户级线程同数量的内核级线程
内核级线程是处理机分配的最小单位
优点:当一个线程被阻塞后,别的线程还可以继续执行,并发能力强;在此系统中,线程成为CPU调度的基本单位,多个线程可在多核处理机的不同核上执行,从而使得多个线程并行执行
缺点:一个用户进程可能会拥有多个内核级线程,线程切换需要由操作系统内核完成,因此需要切换到核心态,从而导致线程管理的成本高,开销大
多对一模型
多个用户级线程映射到一个内核级线程,且一个进程只被分配一个内核级线程
多对一模型的方式类似于用户级线程方式,但多对一模型中,内核级线程才是处理机分配的基本单位,只不过在此方式下,一个进程只有一个内核级线程
优点:用户级线程的切换在用户态即可完成,不需要切换到核心态,线程管理的系统开销小,效率高
缺点:当一个用户级线程被阻塞后,整个进程都会被阻塞,导致系统并发度不高;一个进程只对应一个内核级线程,而一个CPU一次只能执行一个进程中的一个内核级线程(这里可以将内核级线程可以理解为用户级线程结构中的进程。在执行内核级线程的过程中,一次只会执行其中的某一个用户级线程),这就导致多个用户级线程不可在多核处理机上并行执行
多对多模型
n个用户级线程映射到m个内核级线程(n ≥ m),每个进程对应m个内核级线程
在多对多模型下,内核级线程是处理机分配的最小单位
多对多模型克服了多对一模型并发度不高的缺点(即一个阻塞全体阻塞的缺点),又克服了一对一模型中一个用户进程对应太多内核级线程,导致开销太大的缺点
内核级线程中可以运行任意一个有映射关系的用户级线程代码,只有一个进程中的所有内核级线程中正在运行的代码逻辑都阻塞时,这个进程才会阻塞
总结
线程的状态与转换
线程的组织与控制
线程的控制其实就是实现线程的状态转换
线程的组织其实就是组织线程的TCB
线程的TCB中包含了以下内容:
-
线程标识符
即TID,作用与PID类似
-
程序计数器PC
线程目前执行到的位置
-
其他寄存器
线程运行的中间结果
-
堆栈指针
堆栈中保存函数调用信息,局部变量等
-
线程运行状态
运行/就绪/阻塞
-
优先级
线程调度、资源分配的参考
线程切换时需要保存和恢复程序计数器PC、其他寄存器、堆栈指针的内容
将多个线程的TCB组织起来就可以形成线程表(Thread table)
TCB的组织的方式可以:
- 每个进程中的各线程的TCB独立成为一个线程表
- 系统中所有线程的TCB成为一个线程表
- 相同状态的线程的TCB成为一个线程表
调度的概念、层次
调度的基本概念
当有一堆任务要处理,但由于资源有限,这些事情无法同时处理,就需要确定某种规则来决定处理这些任务的顺序,这就是“调度”要研究的问题
调度的三个层次
高级调度(作业调度)
按一定的原则从外存的作业后备队列中挑选一个(或多个)作业调入内存,并为其创建进程,每个作业只调入一次、调出一次(任务完成后调出),作业调入时会创建PCB,调出时会撤销PCB
同时有好几个程序需要启动,使用高级调度就可以选择先启动哪一个
作业:一个具体的任务
用户向系统提交一个作业 ≈ 用户让操作系统启动一个程序来处理一个具体的任务
要启动一个程序,就需要把程序相关的数据从外存放入内存中,但内存空间是有限的,有时无法将用户提交的所有作业全部放入内存,这时操作系统就需要进行高级调度来解决这一问题
低级调度(进程调度/处理机调度)
按照某种策略从进程就绪队列中选取一个进程,将处理机分配给它
进程调度是操作系统中最基本的一种调度,在一般的操作系统中都必须配置进程调度,进程调度的频率很高(但也不能过高),一般几十毫秒一次(这样才能让多个进程在宏观上看是同时执行的)
中级调度(内存调度)
当内存不够时,可将某些进程的数据从内存调出外存,等内存空间足够或者进程需要运行时再重新调入内存
暂时调到外存等待的进程状态为挂起状态,被挂起的进程的PCB会被组织成挂起队列
而中级调度要做的事情就是按照某种策略决定将哪个处于挂起状态的进程重新调入内存
同一个进程可能会被多次调出、调入内存,因此中级调度发生的频率要比高级调度更高
进程的挂起状态与七状态模型
暂时调到外存等待的进程状态为挂起状态(挂起态,suspend)
挂起态又可以进一步细分为就绪挂起、阻塞挂起两种状态
- 一个系统,如果此时系统的负载比较高,就有可能把一个处于就绪态的进程把它调到外存中,此时这个进程就处于就绪挂起的状态,当内存有空闲或进程需要执行时,处于就绪挂起状态的进程就会被激活,即调回内存并修改为就绪态
- 一个处于阻塞状态的进程也有可能被挂起,即阻塞挂起,处于阻塞挂起状态的进程也可以被激活,激活后即为阻塞态
- 一个处于阻塞挂起状态的进程,当原本阻塞进程所等待的事件发生时,这个进程就可以直接进入就绪挂起态
- 一个处于运行态的进程需要暂时下处理机时,也可以直接进入就绪挂起态
- 一个进程在创建完成后,如果此时内存不够,不足以支持进程进入就绪态,就会将进程直接进入就绪挂起态
注意:
- “挂起”和“阻塞”的区别,两种状态都是暂时不能获得CPU的服务,但挂起态是将进程映像调到外存,而阻塞态下进程映像还在内存中
- 有的操作系统会把就绪挂起、阻塞挂起分为两个挂起队列,甚至会根据阻塞原因不同把阻塞挂起进程进一步细分为多个队列
三种调度的联系、对比
| 做什么 | 什么时候做 | 发生频率 | 对进程状态的影响 | |
|---|---|---|---|---|
| 高级调度 | 按照某种规则,从后备队列中选择合适的作业将其调入内存,并为其创建进程 | 外存 → 内存 (面向作业) | 最低 | 无 → 创建态 → 就绪态 |
| 中级调度 | 按照某种规则,从挂起队列中选择合适的进程将其数据调回内存 | 外存 → 内存 (面向进程) | 中等 | 挂起态 → 就绪态 (阻塞挂起 → 阻塞态) |
| 低级调度 | 按照某种规则,从就绪队列中选择一个进程为其分配处理机 | 内存 → CPU | 最高 | 就绪态 → 运行态 |
总结
进程调度的时机、切换与过程、方式
进程调度的时机
进程调度(低级调度),就会按照某种算法从就绪队列中选择一个进程为其分配处理机
需要进行进程调度与切换的情况:
-
当前运行的进程主动放弃处理机:
进程正常结束
进程运行过程中发生异常而终止
进程主动请求阻塞(如等待I/O)
-
当前运行的进程被动放弃处理机:
分给进程的时间片用完
有更紧急的事情需要处理(如I/O中断)
有更高优先级的进程进入就绪队列
不能进行进程调度与切换的情况:
- 在处理中断的过程中,中断处理过程复杂,与硬件密切相关,很难做到在中断处理过程中进行进程切换
- 进程在操作系统内核程序临界区中(但是进程在普通临界区中是可以进行调度、切换的)
临界资源:一个时间段内只允许一个进程使用的资源,各进程只能互斥地访问临界资源
临界区:访问临界资源的那段代码
而操作系统内核程序临界区一般是用来访问某种内核数据结构的,比如进程的就绪队列
- 在原子操作上的过程中(如执行原语),原子操作不可中断,要一气呵成(如修改PCB中进程状态标志,并把PCB放到相应队列中,才能算是完成了对进程状态的转换,而不能在进行到一半时就把进程切换走,这会导致错乱)
进程处于内核程序临界区时:
当一个进程此时处于内核程序临界区时,并且这个内核程序临界区是用来访问就绪队列的,那么进程在访问该内核临界资源时,会先将资源上锁,直到进程对资源访问结束后才将资源解锁,在资源上锁的期间,其它进程不能对资源进行访问。如果此时系统要发生进程调度时,进程调度相关的程序就需要访问就绪队列(从就绪队列中挑选一个PCB为它分配处理机),但由于就绪队列处于上锁的状态,因此进程调度就无法顺利进行
也就是说,内核程序临界区访问的临界资源如果不能尽快解锁的话,就极有可能影响到操作系统内核的其他管理工作,因此在进程在访问内核程序临界区期间不能进行进程调度与切换
进程访问普通临界区时:
当一个进程此时处于普通程序临界区来访问如打印机这样的临界资源时,在打印机完成打印之前,进程一直处于临界区内,临界资源不会解锁,但打印机又是慢速设备,此时如果一直不允许进程调度的话,就会导致进程一直霸占CPU,但打印的过程其实是不需要使用到CPU的,这就导致CPU一直处于空闲的状态(而不能为那些需要运行的进程服务)
普通临界区访问的临界资源不会直接影响操作系统内核的管理工作,因此在访问普通临界区时可以进行进程调度与切换
进程调度的方式
有的系统中,只允许进程主动放弃处理机
有的系统中,进程可以主动放弃处理机,但当有更紧急的任务需要处理时,也会强行剥夺进程持有的处理机(被动放弃)
非剥夺调度方式
又称非抢占方式
只允许进程主动放弃处理机,在进程运行过程中即便有更紧迫的任务到达,当前进程依然会继续使用处理机,直到该进程终止或主动要求进入阻塞态,才会发生进程调度
优点:实现简单,系统开销小但是无法及时处理紧急任务,适合于早期的批处理系统
剥夺调度方式
又称抢占方式
当一个进程正在处理机上执行时,如果有一个更重要或更急迫的进程需要使用处理机,那么会立即暂停正在执行的进程,并将处理机分配给更重要急迫的进程
优点:可以优先处理更紧急的进程,也可实现让各进程按时间片轮流执行的功能(通过时钟中断),适合于分时操作系统、实时操作系统
进程的切换与过程
“狭义的进程调度”与“进程切换”的区别:
狭义的进程调度指的是从就绪队列中选中一个要运行的进程(这个进程可以是刚刚暂停执行的进程,也可以是另一个进程,前者不需要进程切换,后一者就需要进程切换)
进程切换是指一个进程让出处理机,由另一个进程占用处理机的过程
广义的进程调度包含了选择一个进程和进程切换两个过程
进程切换的过程主要完成了:
- 保存原来运行进程的各种数据
- 恢复新的进程的各种数据
保存、恢复的数据包括程序计数器、程序状态字、各种数据寄存器等处理机现场信息,这些信息一般保存在进程控制块中
进程切换是有代价的,因此如果过于频繁地进程调度、切换,必然会使整个系统的效率降低,使系统大部分时间都花在进程切换上,而真正用于执行进程的时间减少
总结
调度器和闲逛进程
调度器,即调度程序(scheduler),它负责让进程从就绪态到运行态或从运行态到就绪态
调度程序要决定“让谁运行”、“运行多长时间”
- “让谁运行”涉及调度程序采用的调度算法是什么
- “运行多长时间”涉及调度程序给待运行的进程分配多长的时间片大小
会触发“调度程序”的事件:
- 创建新进程
- 进程退出
- 运行进程发生阻塞
- I/O中断发生(代表着I/O操作已经完成,这会使得某些处于阻塞态的进程进入就绪态)
进程运行结束后,需要”调度程序“从就绪队列中选择一个新进程给它分配处理机
当就绪队列发生改变时,“调度程序”都需要对进入就绪队列中的这个新进程进行检查,检查是否可以为它分配处理机等,所以凡是导致就绪队列改变的事件,都会触发“调度程序”的执行
如果系统采用非抢占式调度策略,那么只有运行进程阻塞或主动退出时才会触发调度程序工作
如果系统采用抢占式调度策略,那么每个时钟中断或每k个时钟中断会触发调度程序工作
不支持内核级线程的操作系统,调度程序的处理对象是进程
支持内核级线程的操作系统,调度程序的处理对象是内核级线程
闲逛进程
当就绪队列在没有就绪的进程时,调度程序就会给闲逛进程(idle)分配处理机
在实际的存在闲逛进程的系统中,CPU永远不可能空闲,即使没有需要运行的进程,系统也会选择闲逛进程来运行
闲逛进程的特性:
- 优先级最低(但凡有一个就绪队列中有一个就绪进程,调度程序也不会选择让闲逛进程来运行)
- 闲逛进程中的指令一般都是0地址指令(指令执行过程中不需要访存,也不需要访问寄存器)
- 执行闲逛进程的能耗低
调度算法的评价指标
评价调度算法好坏的指标包括:
- CPU利用率
- 系统吞吐率
- 周转时间
- 等待时间
- 响应时间
CPU利用率
CPU利用率即CPU处于“忙碌”的时间占总时间的比例
CPU利用率 = CPU忙碌的时间 / 总时间
除了CPU利用率外,还包括各种设备的利用率,计算方法同样是
设备忙碌的时间 / 总时间
某计算机只支持单道程序,某个作业开始需要在CPU上运行5秒,再用打印机打印输出5秒,之后再执行5秒,才能结束,在此过程中,CPU利用率、打印机利用率分别是多少?
CPU利用率 = (5 + 5) / (5 + 5 + 5) = 66.66%
打印机利用率 = 5 / 15 = 33.33%
系统吞吐量
系统吞吐量指单位时间内完成作业的数量
系统吞吐量 = 总共完成了多少道作业 / 总共花了多少时间
某计算机系统处理完10道作业共花费100秒是,则系统吞吐量为?
10 / 100 = 0.1道/秒
周转时间
周转时间是指从作业被提交给系统开始,到作业完成为止的这段时间间隔
作业周转时间 = 作业完成时间 - 作业提交时间
对于用户来说,用户更关心自己的单个作业的周转时间
平均周转时间 = 各作业周转时间之和 / 作业数
对于操作系统来说,更关心系统的整体表现,因此更关心所有作业周转时间的平均值
周转时间包括四个部分:
- 作业在外存后备队列上等待作业调度(高级调度)的时间
- 进程在就绪队列上等待进程调度(低级调度)的时间(就绪的时间)
- 进程在CPU上执行的时间(运行的时间)
- 进程等待I/O操作完成的时间(阻塞的时间)
后三项在一个作业的整个处理过程中可能发生多次
有的作业运行时间短,有的作业运行时间长,因此在周转时间固定的情况下,运行时间长的作业给用户带来的体验是更好的(因为运行时间长的作业其运行时间占整个周转时间的比例就更大),为此,人们又提出了带权周转时间这个指标
带权周转时间 = 作业周转时间 / 作业实际运行的时间 = (作业完成时间 - 作业提交时间) / 作业实际运行的时间
带权周转时间必然 ≥ 1,带权周转时间与周转时间都是越小越好
对于周转时间相同的两个作业,实际运行时间长的作业在相同时间内提供服务的时间更多,带权周转时间更小,用户满意度更高
对于实际运行时间相同的两个作业,周转时间短的带权周转时间更小,用户满意度更高
平均带权周转时间 = 各作业带权周转时间之和 / 作业数
等待时间
等待时间指进程/作业处于等待处理机状态时间之和,等待时间越长,用户满意度越低
当一个作业刚被提交时,作业会被保存在外存里的作业后备队列中,作业会在后备队列等待被服务(调度),当作业被调度后,作业就会转移至内存中并建立起对应的进程,进程建立后,就可以被CPU服务、被I/O设备服务,当然,进程也有等待被服务的时间
对于进程来说,等待时间就是指进程建立后等待被服务的时间之和,而在等待I/O完成的期间其实进程是在被I/O设备服务的,所以这段时间不计入等待时间
对于作业来说,不仅要考虑建立进程后的等待服务时间,还要加上作业在外存后备队列中等待的时间
一个作业总共需要被CPU服务多久,被I/O设备服务多久一般是确定不变的,因此调度算法其实只会影响作业/进程的等待时间
与前面指标类似,也有“平均等待时间”来评价整体性能
响应时间
响应时间指从用户提交请求(如输入命令并按下回车)到首次产生响应所用的时间
总结
调度算法(1)
饥饿:指某进程/作业长期得不到服务
先来先服务算法(FCFS,First Come First Serve)
规则:按照作业/进程到达的先后顺序进行服务
按照到达的先后顺序调度,事实上就是等待时间越久的越优先得到服务
先来先服务算法适用于作业调度和进程调度
用于作业调度时,考虑的是哪个作业先到达后备队列;用于进程调度时,考虑的是哪个进程先到达就绪队列
属于非抢占式算法(只有当前运行的进程主动放弃处理机时,才会使用到先来先服务算法进行调度)
各进程到达就绪队列的时间、需要的运行时间如下表所示
使用先来先服务调度算法,计算各进程的等待时间、平均等待时间、周转时间、平均周转时间、带权周转时间、平均带权周转时间
进程 到达时间 运行时间 P1 0 7 P2 2 4 P3 4 1 P4 5 4 上面四个进程的调度顺序为:P1 → P2 → P3 → P4
各进程的开始执行时间与执行结束时间轴如下:
周转时间 = 完成时间 - 到达时间:
- P1 = 7 - 0 = 7
- P2 = 11 - 2 = 9
- P3 = 12 - 4 = 8
- P4 = 16 - 5 = 11
带权周转时间 = 周转时间 / 运行时间:
- P1 = 7 / 7 = 1
- P2 = 9 / 4 = 2.25
- P3 = 8 / 1 = 8
- P4 = 11 / 4 = 2.75
等待时间 = 周转时间 - 运行时间:
- P1 = 7 - 7 = 0
- P2 = 9 - 4 = 5
- P3 = 8 - 1 = 7
- P4 = 11 - 4 = 7
注意:本例中的进程都是纯计算型的进程(只需要CPU来服务的进程),一个进程到达后要么在等待,要么在运行,如果是又有计算、又有I/O操作的进程,其等待时间就是 周转时间 - 运行时间 - I/O操作时间
平均周转时间 = (7 + 9 + 8 + 11) / 4 = 8.75
平均带权周转时间 = (1 + 2.25 + 8 + 2.75) / 4 = 3.5
平均等待时间 = (0 + 5 + 7 + 7) / 4 = 4.75
对于P3进程,它的运行时间很短,但需要等待P1和P2这两个长进程都运行完后才能运行,导致P3的带权周转时间很大
优点:公平、算法实现简单
缺点:排在长作业(进程)后面的短作业需要等待很长时间,它的带权周转时间会很大,对短作业来说用户体验不好,即FCFS算法对长作业有利,对短作业不利
先来先服务算法不会导致饥饿现象出现
短作业优先(SJF,Shortest Job First)
算法思想:追求最少的平均等待时间,最少的平均周转时间、最少的平均带权周转时间
算法规则:最短的作业/进程优先得到服务(所谓“最短”,是指要求服务的时间最短)
每次调度时选择当前已到达且所需运行时间最短的作业/进程
短作业优先算法即可用于作业调度,也可用于进程调度,用于进程调度时称为“短进程优先(SPF,Shortest Process First)算法”
SJF和SPF是非抢占式算法,但是也有抢占式的版本 —— 最短剩余时间优先算法(SRTN,Shortest Remaining Time Next)
最短剩余时间优先算法:当就绪队列改变时(即有进程加入就绪队列时)就可能需要调度,如果新到达的进程剩余时间比当前的进程剩余时间更短,则由新进程抢占处理机,当前运行的进程重新回到就绪队列。另外,当一个进程完成时也需要调度
如果题目中未特别说明,短作业优先默认是非抢占式的
各进程到达就绪队列的时间、需要的运行时间如下表所示
使用非抢占式的短作业优先调度算法,计算各进程的等待时间、平均等待时间、周转时间、平均周转时间、带权周转时间、平均带权周转时间
进程 到达时间 运行时间 P1 0 7 P2 2 4 P3 4 1 P4 5 4 上面四个进程的调度顺序为:P1 → P3 → P2 → P4
解析:在0时刻,只有P1进程到达就绪队列,因此最先被调度的进程为P1,当P1运行结束后,即时刻7时,其它所有进程都已经到达就绪队列,根据短作业优先的算法规则,调度运行时间最短的P3进程;当P3运行结束后,就绪队列中就只有运行时间相同的P2和P4,此时就不能根据它们的运行时间决定谁先运行,需要根据这两个进程到达就绪队列的先后顺序选择谁先运行,这里选择的就是P2进程,最后是P4
各进程的开始执行时间与执行结束时间轴如下:
周转时间 = 完成时间 - 到达时间:
- P1 = 7 - 0 = 7
- P3 = 8 - 4 = 4
- P2 = 12 - 2 = 10
- P4 = 16 - 5 = 11
带权周转时间 = 周转时间 / 运行时间:
- P1 = 7 / 7 = 1
- P3 = 4 / 1 = 4
- P2 = 10 / 4 = 2.5
- P4 = 11 / 4 = 2.75
等待时间 = 周转时间 - 运行时间:
- P1 = 7 - 7 = 0
- P3 = 4 - 1 = 3
- P2 = 10 - 4 = 6
- P4 = 11 - 4 = 7
平均周转时间 = (7 + 4 + 10 + 11) / 4 = 8
平均带权周转时间 = (1 + 4 + 2.5 + 2.75) / 4 = 2.56
平均等待时间 = (0 + 3 + 6 + 7) / 4 = 4
对比FCFS算法的结果,显然SPF算法的平均等待/周转/带权周转时间都要更低
使用抢占式的短作业优先调度算法:
分析:
0时刻(P1到达):P1(7)
2时刻(P2到达):P1(5)、P2(4)
4时刻(P3到达):P1(5)、P2(2)、P3(1)
5时刻(P3完成且P4到达):P1(5)、P2(2)、P4(4)
7时刻(P2完成):P1(5)、P4(4)
11时刻(P4完成):P1(5)
各进程的开始执行时间与执行结束时间轴如下:
周转时间 = 完成时间 - 到达时间:
- P1 = 16 - 0 = 16
- P2 = 7 - 2 = 5
- P3 = 5 - 4 = 1
- P4 = 11 - 5 = 6
带权周转时间 = 周转时间 / 运行时间:
- P1 = 16 / 7 = 2.28
- P2 = 5 / 4 = 1.25
- P3 = 1 / 1 = 1
- P4 = 6 / 4 = 1.5
等待时间 = 周转时间 - 运行时间:
- P1 = 16 - 7 = 9
- P2 = 5 - 4 = 1
- P3 = 1 - 1 = 0
- P4 = 6 - 4 = 2
平均周转时间 = (16 + 5 + 1 + 6) / 4 = 7
平均带权周转时间 = (2.28 + 1.25 + 1 + 1.5) / 4 = 1.5
平均等待时间 = (9 + 1 + 0 + 2) / 4 = 3
对于非抢占式的短作业优先算法,显然抢占式的这几个指标又要更低
优点:“最短的”平均等待时间、平均周转时间
严谨地说,应该是抢占式的短作业优先具有最短的平均等待时间和平均周转时间
缺点:不公平,对短作业有利,对长作业不利,可能产生饥饿现象。另外,作业/进程的运行时间是由用户提供的,并不一定真实,不一定能够做到真正的短作业优先
如果队列中源源不断地有更短的作业/进程到来,可能使长作业/进程长时间得不到服务,产生“饥饿”现象,如果一直得不到服务,则称为“饿死”
FCFS算法是在每次调度时选择一个等待时间最长的作业(进程)为其服务,但是没有考虑到作业的运行时间,因此导致了对短作业不友好的问题
SJF算法是选择一个执行时间最短的作业为其服务,但是又完全不考虑各个作业的等待时间,因此导致了对长作业不友好的问题,甚至还会造成饥饿问题
高响应比优先(HRRN,Highest Response Ratio Next)
思想:要综合考虑作业/进程的等待时间和要求服务的时间(即运行时间)
规则:在每次调度时先计算各个作业/进程的响应比,选择响应比最高的作业/进程为其服务
当当前运行的进程主动放弃CPU时(正常/异常退出或主动阻塞),才需要进行调度,调度时计算所有就绪进程的响应比,选择响应比最高的进程上处理机
响应比 = (等待时间 + 要求服务时间) / 要求服务时间
高响应比优先算法即可用于作业调度,也可用于进程调度
HRRN属于非抢占式算法,因此只有当前运行的作业/进程主动放弃处理机时,才需要调度,才需要计算响应比
各进程到达就绪队列的时间、需要的运行时间如下表所示
使用高响应比优先调度算法,计算各进程的等待时间、平均等待时间、周转时间、平均周转时间、带权周转时间、平均带权周转时间
进程 到达时间 运行时间 P1 0 7 P2 2 4 P3 4 1 P4 5 4 上面四个进程的调度顺序为:P1 → P3 → P2 → P4
分析(不考虑异常终止和主动阻塞的情况,只考虑进程运行完成后自动退出处理机):
0时刻:只有P1到达就绪队列,P1上处理机
7时刻:P1运行完成,主动放弃处理机,就绪队列中有P2、P3、P4,P2的响应比为(5 + 4) / 4 = 2.25,P3的响应比为(3 + 1) / 1 = 4,P4的响应比为(2 + 4) / 4 = 1.5
8时刻:P3运行完成,主动放弃处理机,就绪队列中有P2(2.5),P4(1.75)
12时刻:P2运行完成,主动放弃处理机,就绪队列中只剩下P4
各进程的开始执行时间与执行结束时间轴如下:
...
特点:该算法综合考虑了等待时间和运行时间(即要求服务时间)。当等待时间相同时,要求服务时间短的优先(SJF的优点);当要求服务时间相同时,等待时间长的优先(FCFS的优点)。对于长作业来说,随着等待时间越来越久,其响应比也会越来越大,从而避免了长作业饥饿的问题
总结
| 算法 | 思想&规则 | 可抢占 | 优点 | 缺点 | 考虑到等待时间&运行时间 | 会导致饥饿 |
|---|---|---|---|---|---|---|
| FCFS | ... | 非抢占式 | 公平;实现简单 | 对短作业不利 | 等待时间✔️ 运行时间❌ | 不会 |
| SJF/SPF | ... | 默认为非抢占式,也有抢占式版本 | “最短的”平均等待/周转时间 | 对长作业不利,可能导致饥饿;难以做到真正的短作业优先 | 等待时间❌ 运行时间✔️ | 会 |
| HRRN | ... | 非抢占式 | 上述两种算法的权衡折中,综合考虑了等待时间和运行时间 | 等待时间✔️ 运行时间✔️ | 不会 |
这几种算法主要关心对作业/进程的公平性、平均周转时间、平均等待时间等评价系统整体性能的指标,但是不关心“响应时间”,也并不区分任务的紧急程度,因此对于用户来说,交互性很差。因此这三种算法一般适合于早期的批处理系统,当然,FCFS算法也常结合其他的算法使用,在现在也扮演着很重要的角色,而适合用于交互式系统的调度算法在下一小节介绍
调度算法(2)
时间片轮转(RR,Round-Robin)
思想:公平地、轮流地为各个进程服务,让每个进程在一定时间间隔内都可以得到响应
规则:按照各进程到达就绪队列的顺序,轮流让各个进程执行一个时间片(如10ms),若进程未在一个时间片内执行完,则剥夺处理机,将进程重新放到就绪队列队尾重新排队
轮流让就绪队列中的进程依次执行一个时间片,每次选择的都是排在就绪队列队头的进程
时间片轮转只适用于进程调度(只有作业放入内存并建立完相应的进程后,才能被分配处理机时间片)
时间片是处理机进行处理的时间片,因此不适用于作业调度
时间片轮转调度算法常用于分时操作系统,算法更注重“响应时间”,而不太关心周转时间
若进程未能在时间片内运行完,将被强行剥夺处理机使用权,因此时间片轮转调度算法属于抢占式的算法,由时钟装置发出时钟中断来通知CPU时间片已到
各进程到达就绪队列的时间、需要的运行时间如下表所示
使用时间片轮转调度算法,分析时间片大小分别为2、5时的进程运行情况
进程 到达时间 运行时间 P1 0 5 P2 2 4 P3 4 1 P4 5 6 注:就绪队列:[队头 → ... → 队尾]
时间片为2的情况:
分析:
0时刻[P1(5)]:P1到达就绪队列,让P1上处理机运行一个时间片
2时刻[P2(4) → P1(3)]:P2到达就绪队列,P1运行完一个时间片,被剥夺处理机,重新放到队尾,此时P2排在队头,因此让P2上处理机(同一时刻如果出现当前进程时间片用完且有新进程到达的情况,默认新进程先进入就绪队列,剥夺处理机的进程进入队尾)
4时刻[P1(3) → P3(1) → P2(2)]:P3到达,先插到就绪队尾,紧接着,P2下处理机再插到队尾
5时刻[P3(1) → P2(2) → P4(6)]:P4到达并插入到就绪队尾(此时P1的时间片还没用完,因此此时不进行调度,另外,此时P1处于运行态,并不在就绪队列中)
6时刻[P3(1) → P2(2) → P4(6) → P1(1)]:P1时间片用完,下处理机,重新回到就绪队列队尾,P3发生调度
7时刻[P2(2) → P4(6) → P1(1)]:虽然P3的时间片没用完,但是由于P3只需运行1个单位的时间,运行完后会主动放弃处理机,因此也会发生调度,队头进程P2上处理机
9时刻[P4(6) → P1(1)]:进程P2时间片用完,并刚好运行完成,发生调度,P4上处理机
11时刻[P1(1) → P4(4)]:P4时间片用完,重新回到就绪队列,P1上处理机
12时刻[P4(4)]:P1运行完,主动放弃处理机,此时就绪队列中只剩P4,P4上处理机
14时刻[]:就绪队列为空,因此让P4接着运行一个时间片
16时刻[]:P4运行完成,所有进程运行结束
各进程的开始执行时间与执行结束时间轴如下:
时间片为5的情况:
分析:
0时刻[P1(5)]:只有P1到达,P1上处理机
2时刻[P2(4)]:P2到达,但P1时间片尚未结束,暂不调度
4时刻[P2(4) → P3(1)]:P3到达,但P1时间片尚未结束,因此暂不调度
5时刻[P2(4) → P3(1) → P4(6)]:P4到达,同时P1运行结束,发生调度,P2上处理机
9时刻[P3(1) → P4(6)]:P2运行结束,虽然时间片没用完,但是会主动放弃处理机,发生调度
10时刻[P4(6)]:P3运行结束,虽然时间片没用完,但是会主动放弃处理机,发生调度
15时刻[]:P4时间片用完,但就绪队列为空,因此会让P4继续执行一个时间片
16时刻[]:P4运行完,主动放弃处理机,所有进程运行完
各进程的开始执行时间与执行结束时间轴如下:
注意:如果将这几个进程按照先来先服务算法进行调度,那么各进程的执行时间轴会和上面时间片为5的时间片轮转时间轴一样
因此如果时间片设置得太大,使得每个进程都可以在一个时间片内就完成,则时间片轮转调度算法就退化为先来先服务调度算法,并且会增加进程的响应时间,因此时间片不能设置得太大
比如系统中有10个进程在并发执行,如果时间片为1秒,则一个进程被响应可能需要等9秒,比如用户在自己进程的时间片外通过键盘发出调试命令(之后相应进程进入队尾),就需要等待9秒才能被系统响应
另一方面,进程调度、切换是有一定时间代价的(需要保存、恢复运行环境),因此如果时间片太小,会导致进程切换过于频繁,系统会花大量的时间来处理进程切换,从而导致实际用于进程执行的时间比例减少,所以时间片也不能设置得太小
一般来说,设计时间片要让切换进程的开销时间占比不超过1%
优点:公平;响应快,适用于分时操作系统
缺点:由于高频率的进程切换,因此有一定的开销;不区分任务的紧急程度
时间片轮转算法不会导致饥饿现象发生
优先级调度算法
背景:随着计算机的发展,特别是实时操作系统的出现,越来越多的应用场景需要根据任务的紧急程度决定处理任务的顺序
规则:每个作业/进程有各自的优先级,调度时选择优先级最高的作业/进程
优先级调度算法即可用于作业调度,又可用于进程调度,甚至还可以用于I/O调度
作业调度时选择后备队列中优先级最高的作业让它从外存放入内存
进程调度时选择就绪队列中优先级最高的进程让它上处理机
优先级调度算法既有抢占式也有非抢占式版本,非抢占式只需在进程主动放弃处理机时进行调度即可,而抢占式还需在就绪队列变化时检查是否会发生抢占
非抢占式的优先级调度算法:每次调度时选择当前已经到达且优先级最高的进程,当前进程主动放弃处理机时才发生调度
抢占式的优先级调度算法:每次调度时选择当前已经到达且优先级最高的进程,当前进程主动放弃处理机时1发生调度;另外,当就绪队列发生改变时也需要检查是否会发生抢占
各进程到达就绪队列的时间、需要运行的时间、进程优先数如下表所示
使用非抢占式的优先级调度算法,分析进程运行情况(优先数越大,优先级越高):
进程 到达时间 运行时间 优先数 P1 0 7 1 P2 2 4 2 P3 4 1 3 P4 5 4 2 分析:
0时刻(P1):只有P1到达,P1上处理机
7时刻(P2、P3、P4):P1运行完成主动放弃处理机,其余进程都已到达,P3优先级最高,P3上处理机
8时刻(P2、P4):P3完成,P2、P4优先级相同是,但由于P2先到达就绪队列,因此P2优先上处理机
12时刻(P4):P2完成,就绪队列只剩下P4,P4上处理机
16时刻():P4完成,所有进程都结束
各进程的开始执行时间与执行结束时间轴如下:
使用抢占式的优先级调度算法,分析进程运行情况:
分析:
0时刻(P1):只有P1到达,P1上处理机
2时刻(P2):P2到达就绪队列,优先级比P1更高,发生抢占,P1回到就绪队列,P2上处理机
4时刻(P1,P3):P3到达,优先级比P2更高,P2回到就绪队列,P3抢占处理机
5时刻(P1,P2,P4):P3完成,主动释放处理机,同时P4也到达,由于P2比P4更先进入就绪队列,因此选择P2上处理机
7时刻(P1,P4):P2完成,主动放弃处理机,就绪队列只剩下P1和P4,P4上处理机
11时刻(P1):P4完成,P1上处理机
16时刻():P1完成,所有进程结束
各进程的开始执行时间与执行结束时间轴如下:
补充:
-
有的系统中就绪队列不止一个,它可以按照进程的优先级组织多个能保存相同优先级进程的就绪队列(每一个队列中保存相同优先级的进程,不同队列中进程的优先级不同),每次先从更高优先级的队列中寻找是否有进程;另外,也可以把更高优先级的进程插入到队列的队头位置,每次调度队头位置的进程
-
根据优先级是否可以改变,又可将优先级分为静态优先级和动态优先级两种
静态优先级:创建进程时确定,之后一直不变
动态优先级:创建进程时有一个初始值,之后会根据情况动态地调整进程的优先级
如何合理地设置各类进程的优先级(或优先级初始值)?
- 系统进程高于用户进程
- 前台进程高于后台进程
- 操作系统更偏向I/O型进程(或称I/O繁忙型进程)
与I/O型进程对应的是计算型进程(或称CPU繁忙型进程)
I/O设备和CPU是可以并行工作的,如果优先让I/O进程先运行,则越有可能让I/O设备尽早地投入工作,尽早地和CPU并行工作,这会使得资源利用率、系统吞吐量得到提升
如果采用动态优先级时,什么时候需要调整进程优先级?
可以从追求公平、提升资源利用率等角度考虑
- 如果某个进程从就绪队列中等待了很长时间,则可以适当提升其优先级
- 如果某个进程占用处理机运行了很长时间,则可以适当降低其优先级
- 如果发现一个进程频繁地进行I/O操作,则可以适当提升其优先级
优点:用优先级区分任务的紧急程度、重要程度,适用于实时操作系统,可灵活地调整各种作业/进程的偏好程度
缺点:若源源不断地有高优先级进程到来,则可能导致饥饿
优先级调度算法可能会出现“饥饿”现象
多级反馈队列调度算法
FCFS算法的优点是公平;SJF算法的有点是能尽快处理完短作业,平均等待/周转时间等参数很优秀;时间片轮转可以让各个进程得到及时的响应;优先级调度算法可以灵活地调整各种进程被服务的机会
思想:就是对上述算法特点的综合折中
规则:
- 设置多级就绪队列,各级队列优先级从高到低,时间片从小到大
- 新进程到达时先进入第1级队列,按FCFS原则排队等待被分配时间片,若用完时间片进程还未结束,则进程进入下一级队列队尾;如果此时已经是在最低级的队列,则直接放回到队列队尾
- 只有第k级队列为空时,才会为第k+1级队头的进程分配时间片
- 被抢占处理机的进程直接放回原队列队尾(不用降低1级)
多级反馈队列算法适用于进程调度(涉及时间片的调度都只能是进程调度)
多级反馈队列算法属于抢占式的算法。在第k级队列的进程运行过程中,若更上级的队列(1 ~ k-1级)中进入了一个新进程,则由于新进程处于优先级更高的队列中,因此新进程会抢占处理机,原来运行的进程放回到其原所属队列的队尾
现在也出现了非抢占式的多级反馈队列版本
各进程到达就绪队列的时间、需要的运行时间如下表所示
使用多级反馈队列调度算法,分析进程运行的过程:
进程 到达时间 运行时间 P1 0 8 P2 1 4 P3 5 1 步骤:
设置多级就绪队列,各级队列优先级从高到低,时间片从小到大
假设设置3个级别的就绪队列,第1级队列分配时间片大小为1,第2级队列分配时间片大小为2,第3级队列分配时间片大小为4
新进程到达时先进入第1级队列,按FCFS原则排队等待被分配时间片,若用完时间片进程还未结束,则进程进入下一级队列队尾;如果此时已经是在最低级的队列,则直接放回到队列队尾
只有第k级队列为空时,才会为第k+1级队头的进程分配时间片
被抢占处理机的进程重新放回原队列的队尾
0时刻:P1到达,进入第1级队列,此时第1级队列中只有P1一个进程,因此P1上处理机
进程剩余运行时间:P1(8)(含义:在0时刻处P1还需要运行8个时间片的时间)
1时刻:P2到达,进入第1级队列,且P1时间片到,下处理机,并进入第2级队列队尾处,此时第1级队列中有进程,因此先执行第1级队列中的进程,第1级队列中只有P2一个进程,因此P2上处理机
进程剩余运行时间:P1(7)、P2(4)
2时刻:P2时间片到,下处理机,并进入第2级队列,此时第1级队列中没有进程,因此执行第2级队列中的进程,按照FCFS的原则,先执行P1进程,因此P1上处理机
进程剩余运行时间:P1(7)、P2(3)
4时刻:P1时间片到,下处理机,并进入第3级队列,此时第1级队列中没有进程,第2级队列中有P2一个进程,因此P2上处理机
进程剩余运行时间:P1(5)、P2(3)
5时刻:P3到达,进入第1级队列,此时虽然P2只运行了一个时间片的时间,其时间片还未到,但由于更高级队列中有新进程,所以剥夺P2的处理机,并将P2放到其原队列即第2级队列的队尾,让第1级队列中的进程上处理机,第1级队列中只有P3一个进程,因此P3上处理机
进程剩余运行时间:P1(5)、P2(2)、P3(1)
6时刻:P3完成,无需进入下一级队列,下处理机,此时第1级队列为空,第2级队列中只有P2一个进程,因此P2上处理机
进程剩余运行时间:P1(5)、P2(2)
8时刻:P2完成,无需进入下一级队列,下处理机,此时第1级和第2级为空,第3级队列中只有P3一个进程,因此P3上处理机
进程剩余运行时间:P1(5)
12时刻:P1时间片到,下处理机,由于P1原来已经处于最低级的队列中,因此直接插入到原队列队尾,此时所有队列中只有P1一个进程,因此P1再次上处理机
进程剩余运行时间:P1(1)
13时刻:P1完成,无需再次进入队列,下处理机,所有进程运行结束
各进程开始执行到执行结束的过程为:
P1(1) → P2(1) → P1(2) → P2(1) → P3(1) → P2(2) → P1(4) → P1(1)注:这里的
P1(1)是指P1运行了1个单位的时间
优点:
- 对各类型进程相对公平(FCFS的优点)
- 每个新到达的进程都可以很快得到响应(RR的优点)
- 短进程只用较少的时间就可以完成(SPF的优点)
- 不必事先估计进程的运行时间(避免用户作假)
- 可以灵活地调整对各类进程的偏好程度,比如CPU密集型进程、I/O密集型进程(即可以将因I/O而阻塞的进程重新放回原队列而不是放到低一级队列,这样I/O型进程就可以保持较高优先级)
多级反馈队列算法可能会导致“饥饿”现象发生
当源源不断有新进程到达时,处于低级队列中的进程就不能被响应
总结
| 算法 | 思想&规则 | 可抢占 | 优点 | 缺点 | 会导致饥饿 |
|---|---|---|---|---|---|
| 时间片轮转 | ... | 抢占型 | 公平,适用于分时系统 | 频繁切换有开销,不区分优先级 | 不会 |
| 优先级调度 | ... | 有抢占型的,也有非抢占型的 | 区分优先级,适用于实时系统 | 可能导致饥饿 | 会 |
| 多级反馈队列 | ... | 抢占型 | 各种调度算法优点的综合 | 可能导致饥饿 | 会 |
比起早期的批处理系统来说,由于计算机造价大幅降低,因此之后出现的交互式操作系统(包括分时操作系统、实时操作系统等)更注重系统的响应时间、公平性、平衡性等指标,而这几种算法恰好也能较好地满足交互式系统的需求,因此这三种算法更适合于交互式系统(Unix系统使用的就是多级反馈队列算法)
调度算法(3)
多级队列调度算法
系统中按照进程类型设置多个队列,进程创建成功后插入到某个队列中
调度时如何选择一个队列(即调度时应该从哪个队列中选择进程):
① 先选中高优先级的队列,高优先级队列空时低优先级队列中的进程才能被调度(这对低优先级队列不公平)
② 如按100ms对时间进行划分,在这100ms中,前50ms选择系统队列、中间40ms选择交互式队列、最后10ms选择批处理队列(相对公平)
调度时如何选择队列中的某个进程(即确定了选择队列后应该如何选择其中的进程):
不同的队列可以采用不同的调度策略,如系统进程队列采用优先级调度;交互式进程队列采用RR;批处理进程队列采用FCFS