一、进程的概念、组成、特征
1.1进程的概念
进程来源于程序,当我们在电脑中打开一个程序的时候,这个操作系统就会创建一个该程序的进程。我们可以打开多个来源于同一个程序的进程,例如我们可以登录多个QQ号。这就是进程与程序的区别。
我们说过,同一个程序可以开启多个进程,那我们如何去区分这些进程呢?这就要提起进程控制块 (PCB) 的概念了,PCB是一种数据结构,它是一个表,表中就存放了各个进程的PID。而PID就像我们的身份证号一样,每个进程对应一个单独的PID。而PCB中当然不可能仅仅存放PID,还存放进程的CPU占用率等其他数据。
由于每个进程对应一个单独的PCB,故PCB是进程存在的唯一标志,如果进程运行结束,那么该进程所对应的PCB,包括该进程子进程的PCB会一起被操作系统删除,例如我们用QQ来视频通话,如果关掉QQ,那么视频也会被一起关闭。
1.2进程的组成
进程的组成包括PCB、程序段、数据段。PCB是进程存在的唯一标志所以包括PCB理所应当;而程序段是该程序对应的代码,没有程序段也就没有程序,更没有进程;而数据段就是我们用户的东西了,比如不同的QQ号对应不同的账号密码,这些不同QQ号对应的进程的数据自然也不相同。PCB是操作系统需要用的,而程序段和数据段是给进程自己用的。故进程要有PCB、程序段、数据段,缺一不可。
进程的运行大抵分为:PCB的创建,程序段的程序运行,数据段的参数改变。所以进程是动态的,因为进程在运行过程中PCB的数据在改变,程序一直在运行,数据段参数也一直在改变。但是进程映像是静态的,进程映像(进程实体)是进程运行中的某个时刻的进程的状态,也可以理解成进程是进程实体的运行过程。这里要区分好进程与进程映像(进程实体)的区别。
1.3进程的特征
动态性、并发性、独立性、异步性、结构性。动态性是进程的最基本特征,而进程的这5个特征如果掌握了进程的组成与概念都不难理解,唯一需要说明的是异步性。
进程(这里先不提线程)是系统调用的基本单位,而系统调用又分很多种,比如先来先服务算法,短作业优先调度算法……这样一来,系统在调用进程的时候(这里先不提作业调度),哪怕进程A和进程B是同时从外存进入内存的,但是可能进程A的优先级高于进程B等原因导致两个进程不是同步运行的,而是分别以两个不可预知的速度向前推进。
二、进程的状态与转换
既然要将进程的状态与转换就要先知道进程有哪些状态:1.创建态 2.就绪态 3.运行态 4.阻塞态 5.终止态。而运行态、就绪态、阻塞态是进程的基本状态,所以接下来我们也要重点讲述这三种状态的相互转换。
2.1创建态与终止态
由于这两种状态比较简单,故放在一起说了。创建态:当我们运行一个程序的时候,操作系统会给该程序创建一个进程,包括初始化PCB、分配资源等,一个进程被创建的过程就是创建态。终止态:当一个进程运行完毕后,操作系统会回收该进程的PCB、系统资源,该进程被彻底删除,而这个删除进程的过程就是终止态。
2.2就绪态->运行态
就绪态就是系统创建完毕就会进入就绪态,而由于CPU在同一时刻只能处理有限的进程,故当内存中有多个进程处于就绪态,那么CPU当然不会将所有进程同时运行而是由操作系统来选出哪个程序应该运行,而这个操作系统选中的程序就将会从就绪态转化为运行态。
2.3运行态->阻塞态、阻塞态->就绪态
当一个进程在运行的过程中需要使用互斥共享资源的时候,且如果该资源(也称I/O资源,I/O设备)正在被其他进程使用,那么该进程就会自发的向CPU发送一个陷入指令,使CPU停止继续运行该进程,而该进程就会进入一种等待I/O设备空闲的状态,我们称这种状态为阻塞态。直到我们的I/O设备已经空闲了,那么我们的程序等待完毕就会再次进入就绪态,等待操作系统的调用。
2.4运行态->就绪态
我们前面学过了时钟管理,所以这里我们就拿时钟管理举例。我们都知道,操作系统拥有并发性,我们是通过时钟管理来向CPU发送一条外部中断指令来强制中断该进程的运行。那么此时程序不再运行了,也就不再是运行态,所以此时程序会回到就绪态,等待操作系统的再次调用,这种运行态->就绪态的过程也保证了操作系统的并发性。
根据上面的学习,我们也知道了,进程可以从就绪态到运行态、可以从运行态到就绪态、可以从运行态到阻塞态也可以从阻塞态到就绪态。但是我们要捋清楚,进程绝不可能从阻塞态直接到运行态,也不可能从就绪态直接到达阻塞态。
2.5进程的组织
前面我们说过,进程有多种状态,那么我们如何区分此时进程的状态呢?我们需要设置不同的链表来区分进程的多种状态,比如1表示运行态,2表示就绪态等。那么是不是可以说在不同的状态下也有多个程序呢。我们可以将链表1设置为运行态,而单核CPU在同一时刻只能运行一个进程,故链表1只有一个进程结点。
我们这里主要要说明的是阻塞态,虽然阻塞态只是一种状态,但是阻塞态需要多个链表来完成对阻塞态更细致的区分。比如阻塞态1是来源于打印设备的阻塞,阻塞态2是来源于摄像头设备的阻塞……这里细致区分后才能方便我们后续系统调度算法的学习。
三、进程控制
3.1原语
进程控制是通过原语来实现的,那么原语是什么呢?还记得我们之前讲的操作系统的组成吗,分为大内核和微内核,而原语就是存放在操作系统内核中的一种特殊的程序,原语具有原子性,在执行过程中不受中断影响一气呵成。且原语的等级极高,只有操作系统才可使用。
那么原语为什么需要有这样的特点呢?其实原语一气呵成的特点与操作系统对进程的5种状态的定义方式有关。我们在看Linux操作系统的源码的时候发现,定义一个进程的状态是通过一个state变量来实现的,比如就绪态用0来表示,运行态用1来表示,而每种状态的进程又被组织成了一个链表,运行态和就绪态在不同的链表中,如果原语在对进程的状态进行切换的过程中不能够一气呵成,将一个进程从就绪态的链表拿出放到运行态的链表中,而对state变量进行修改之前被中断,那么在运行态的链表中就会出现一个state=0的结点,这会导致操作系统的崩溃。
原语是通过两个指令来完成一气呵成这个动作的,一个是关中断指令,另一个是开中断指令。正如他们的名字一样,关中断指令是一种关闭程序的大门不让中断影响程序的进程的指令,而开中断指令则是打开程序的大门,此时该程序就可以被中断。如果在原语运行开始的前面写上关中断指令,在原语结尾的后面写上开中断指令,那么原语便可以一气呵成的运行。等运行完毕后再回去检查前面被 “屏蔽” 的中断指令。
3.2原语的类型
1.创建原语
当用户或操作系统要开启某个进程的时候用到的原语,创建原语就是一个进程从创建态到就绪态所要进行的步骤:申请空白PCB,分配系统资源,初始化PCB,将PCB插入就绪队列。
2.撤消原语
一个进程被关闭的过程就要用到撤销原语,无论是进程运行结束自行关闭,还是用户干预关闭。撤消原语第一步就是终止其PCB,然后将其占用的CPU给其他进程用,然后关闭其所有子进程,然后将其占用资源归还给操作系统或其父进程,最后删除其PCB。
3.阻塞原语和唤醒原语
阻塞原语也是对PCB进行一系列的操作,我就不赘述,主要要说的就是当一个程序被阻塞的时候,需要对其原本在CPU寄存器中的重要数据进行保护,否则会因其他进程的运行而覆盖其原本寄存器中的数据,导致进程崩溃。唤醒原语也是没什么好说的,需要注意的只有一个进程因为什么原因被阻塞就需要因为什么原因被唤醒。
切换原语与阻塞原语一样,唯一需要注意的就是需要对CPU中寄存器数据的保护。
总之这些进程的不同原语总结下来无非做的只有三件事:1.更新PCB中的信息 2.将PCB插入合适的队列 3.分配/回收资源。
四、进程通信
进程通信就是一个进程要向其他进程发送一些消息,比如今日头条的新闻可以转发到微信中。那么在我们在实现这种通信的时候就会涉及到读写信息对方进程的过程,但问题是,如果让一个进程随便读写另一个进程的数据,这种操作是很危险的,因为可能会窃取隐私,所以设计了三种方法来实现进程之间的通信
4.1共享存储区
共享存储区实现进程之间的通信时通过增加一个页表段(我也不清楚是什么,操作系统就是这样总是有知识的交叉,很痛苦,后面学了再说吧)来映射内存中某个空间,然后进程可以在这个空间中读写数据,这样通过公共的方式就可以完成进程间的通信还不会随意读取其他进程的信息。那么这种方式存在一个问题:如果两个进程同时向共享存储区中的某个位置写数据,那么就会发生数据覆盖的问题。所以我们在实现这种方式的时候要保证共享存储区的共享方式是互斥的。而互斥可以通过PV操作来进行(又涉及到我的知识盲区了,真可恶)。
这种互斥的共享存储的方式可以使一个进程在这个区域中随便写数据,没有限制。而写完数据后,其他进程可以在这片区域的任意位置随便读数据,也没有限制,显然这种方式实现的进程通信是十分高效的。
基于存储区的互斥共享来实现进程间通信十分高效,但是还有一种方式就是将存储区换成一种数据结构,比如数组之类的数据结构。这种方式由于空间十分有限,所以读写数据十分受限,故效率并不是很高。我也不知道为什么要有这种方式,可能是因为这种方式比较稳定,不容易出错吧,毕竟存在即合理,hh。
4.2消息传递
消息传递就是由操作系统来作为传话人来完成进程之间的通信,消息传递分为直接消息传递和间接消息传递。
直接消息传递:操作系统会向进程提供发送原语以及接收原语,我们这里定义发送为send,接收为receive。如果进程A想给进程B发送消息,那么就要用到发送原语send(B,massage)。(知识回顾:这里用到的函数调用其实就是进行一个系统调用,因为库函数就是建立在程序员与系统调用之间的一个中介嘛,使CPU转变为核心态,这样才能使用CPU的内核程序,也就是使用我们的发送原语)。然后操作系统将这个信息写到进程B的PCB消息队列中,完成传话工作,当进程B需要读取消息的时候也是从消息队列中读取消息,但是由于此刻PCB消息队列中可能有很多进程发送来的消息所以如果想要读取进程A的消息就需要再次调用函数receive(A,&massage)。(这里函数调用毕竟是要传参,但是函数中接收的参数都是形式参数,所以要传实参的地址才能真正接收到这个消息也就是实参)。这种点名道姓的消息传递方式就是直接消息传递。
间接消息传递:间接消息传递是通过某个操作系统的地址空间来作为媒介进行消息传递的,一般来说我们称这个媒介为邮箱。比如进程A要给进程B传递消息,那么进程A就可以向邮箱P中发送一个消息send(A,mes),然后进程B接收receive(A,mes)。我们发现,发送和接收都没有特定的说明要发送给谁,接收谁的信息。但是我目前学到的知识所理解来看,这种传递方式有一定的局限,如果进程B只想接收A的消息,但是此刻邮箱中有多个其他进程的信息,那么B就要接收所有的信息,可能会造成CPU资源浪费。这种通过邮箱来间接传递消息的方式就是间接消息传递。
4.3管道通信
管道通信就是在一个队列中,队列的一边由多个程序写数据,而另一边只允许一个进程读数据,且写数据的时候只能将数据写在队列的队尾,而读数据只能在队列的队头(这是高教社的官方说法,Linux系统中是允许多个程序读数据的)。其实管道就是一个pipe文件,而这个文件所能存储的数据是有限的,当多个进程同时向文件中输入数据后,文件被写满的话,那么写数据这个操作就会被阻塞,反之,当文件为空,读数据的操作也会被阻塞。
管道通信所使用的管道类似于队列,但是这个队列空间是互斥的,也就是说,进程A在写进程的时候,是不允许其他进程在这个队列中读数据的。但是这种管道文件的空间又是有限的,那么如何最大化的利用这有限的空间呢?
我们可以将这个队列设置成循环队列,当文件写满后,读数据的过程就相当于释放了队列的空间,而读出的数据也将彻底在队列中彻底消失,而队头指针和队尾指针就可以重新在整个文件空间中重新规划队头和队尾,然后再由操作系统决定是读数据还是写数据。
管道通信中只要管道没满就可以写数据,只要管道没空就可以读数据。
五、线程的实现方式
线程就是进程的分支,在引入线程之前,进程是处理器调度的基本单位,同时也是系统资源分配的基本单位。但是在引入线程后,线程就是处理器调度的基本单位,不过进程仍然是系统资源分配的基本单位。
用QQ举例,QQ中由不同的功能,包括视频聊天,QQ空间,文件发送这些功能。而这些功能就是QQ这个进程的分支,也就是线程。如果我们想同时使用QQ的这些功能,就要并发的执行这些功能,所以线程才是处理器调度的基本单位。不过于与进程不同,进程的调度切换需要更换系统环境,不过线程的切换由于是在进程中完成的,故不需要系统环境的切换。而由于进程在创建的时候就已经包括了PCB的创建和系统资源的分配,故进程才是系统资源分配的基本单位。
5.1用户级线程
早期的CPU是不支持线程的运行的,只支持进程的运行。那如何在一个进程中同时完成他们的子进程呢?还是拿QQ这个进程举例,我们想要同时完成QQ视频聊天、QQ空间的发布、文件传输这三个“线程”,我们可以在编写QQ这个程序的时候写入一个while循环,当满足对应条件时,才会执行对应的子进程。由于while循环的速度是非常快的,所以会分别给三个“线程”分配碎片化的时间来使他们同时运行,这样就达到了线程看似并行的效果,但实际上还是在单核CPU上运行。而这种在程序中嵌入“线程”的操作称为建立线程库,而这些线程就存放于线程库中。
这种实现方式由于是在系统中写一个while循环来实现的,每个线程的切换是满足一定条件即可切换,所以并不涉及到系统调用,所以也不会使CPU发生状态的改变,CPU一直处于用户态。所以线程的切换也并不会发生系统调度,故用户级线程不是系统调度的基本单位。
这种线程由于是程序员自己编写的线程库来完成,CPU只管运行这个进程即可,CPU并不会注意到这个线程的存在。
这种实现方式优点很明显,由于每个线程的切换并不会使CPU变态,故减少了CPU状态改变的开销,提升了效率;但是缺点远远大于优点,由于这些线程在同一个while循环下循环运行,故如果某一时刻QQ视频聊天的I/O设备被更紧急的任务占用导致其阻塞,那么QQ聊天的这条代码就无法执行,那么在同一个while循环中的QQ空间以及文件发送也会停止运转。
用户级线程是不涉及到线程的转换,切换线程也不需要CPU变态,所以开销小。
5.2内核级线程
内核级线程才是CPU真正看得见的线程,线程可以在多核CPU的处理下并发运行,故并发性很高。实现方式为每个用户级线程对应一个内核级线程,显然当某个内核级线程阻塞的时候,由于每个线程之间没什么联系,故不会影响其他线程。但是这种每个用户级线程对应一个内核级线程的实现方式线程的转换就需要CPU从用户态转化为核心态,导致开销大。
那么有没有一种折中的办法,既能克服线程库的阻塞问题,又能客服内核级线程1对1开销大的问题呢?那么我们只要采用n个用户级线程对应m个内核级线程即可(n>m),这样一来,我们可以将CPU开销大的用户级线程单独对应一个内核级线程,解决CPU开销大的问题;并且,由于有多个内核级线程,故当某个用户级线程对应的内核级线程阻塞了,还可以有其他的内核级线程来接应用户级线程,解决了一个阻塞,全家阻塞的问题。
内核级线程涉及到线程从用户态转换为核心态,且涉及到线程与线程之间的切换,故开销大
5.3线程的组织与控制
线程的组织与控制与进程的组织与控制类似。一个进程的创建首先就要申请空白PCB,线程也是如此,不过线程改了个名字,叫做TCB,而线程对应的ID叫做TID。那么线程的创建就是围绕TCB,首先申请空白TCB,然后初始化TCB即可。TCB中保存了线程的各种数据,线程的TID、线程的程序计数器PC、寄存器的值、数据的指针、运行状况等。存放寄存器的值就相当于保护线程运行现场,而数据的指针是用来保存数据,由于数据所占空间庞大,不是一个TCB就可以存的下的,故存放其指针就可以找到对应的数据。
注:这里从用户级线程转换到核心态线程是线程的转换,而线程的切换是从线程1切换到线程2,这两个切换的含义不同,前者是CPU从用户态到核心态,接收到的是来自进程的外部中断指令,需要CPU来帮忙解决该线程的问题;后者是CPU由于接收到了时钟管理的外部中断指令,从用户态转化为核心态,由于线程1运行时间片用完,故需要CPU需要不再运行线程1而切换到线程2来运行。
讲调度之前,我希望你能回忆一下系统调用和进程调度的区别,这俩很容易混淆,系统调用是当一个进程需要使用CPU的核心功能而向CPU发送的一个来自进程的外部中断指令来实现的,会使CPU从用户态转化为核心态,这个过程并不会发生进程的切换;而进程调度是指操作系统将处于就绪态的进程上CPU运行,或切换进程上CPU运行,是通过时钟管理等这种外部中断指令来实现的,也会令CPU从用户态转化为核心态。其实这就是我们前面红字部分说的线程的两个切换的不同含义。
六、调度的层次
6.1高级调度(作业调度)
当一个进程还在外存中时,我们通常将该进程称之为作业,此时操作系统将此作业从外存调度到内存中变成了进程,从无到创建态再到运行态,相应的创建与其对应的PCB以及为其分配系统资源,这个调度就称之为高级调度。是从外存到内存的过程。
6.2中级调度(内存调度)
众所周知我们电脑的内存空间是远远小于外存空间的,所以当内存中的进程特别多的时候就会使电脑内存被装满导致电脑很卡,那么为了避免这种情况,就绪态和阻塞态下的进程都没有运行我们就可以将一些这些没有运行的进程调回外存,并将其封装成相应的队列,我们将这些队列称之为挂起队列,处于挂起队列的进程映像也会被调出外存。
当我们的操作系统意识到某个处于挂起队列的进程需要运行,那么就会将其从外存的挂起队列调度回内存,比如处于就绪挂起队列的进程可以回到就绪态等状态的切换,这种调度称之为中级调度,也是从外存到内存的过程。
这里要注意,虽然高级调度和中级调度都是从外存到内存的过程,但是高级调度由于作业从来没有进内存,故也没有其对应的PCB,所以作业只要在外存中就永远是作业。但是中级调度虽然将进程从内存调到外存,但是由于进程已经在内存中呆过,有了其对应的PCB,故哪怕进程出了内存进了外存,进程也是进程,永远不会变成作业。
6.3低级调度(进程调度)
当一个进程处于就绪态时,操作系统将该进程调度成了运行态,也就是进程上CPU运行,这种调度就称之为低级调度。低级调度与并发有着联系,因为低级调度的频率极高,故各个进程轮流上CPU运行的速度很快,就可以宏观的实现进程的并发。是从内存到CPU的过程。
七、进程调度
进程调度分为狭义进程调度和广义进程调度,狭义进程调度是指操作系统将处于就绪队列中的进程赋予CPU的使用权;而广义的进程调度是指操作系统剥夺一个进程的CPU使用权,而让另一个进程上CPU运行。
7.1非抢占式调度
这种调度方式是,当一个进程主动下CPU时才会发生,比如程序主动请求阻塞等,而外部信号并不能使该进程下CPU。这种调度方式显然并发性极差,适用于早期的操作系统。
7.2抢占式调度
这种调度就可以通过时钟管理等外部中断手段,来对一个进程进行中断操作,然后再调度其他处于就绪态的进程。这种抢占式的调度可以通过外部中断来使一个优先级较低的进程先下CPU,然后使优先级更高的进程上CPU运行。显然这种调度方式下的进程并发度高,且存在于用户的交互性,适用于更高级的分时操作系统和实时操作系统。
以上两种调度都属于进程的调度,不过在某些时刻,进程调度是会被阻止的,比如:1.在处理中断的过程中,这个过程非常复杂,不可以中间停止。2.当进程正在访问操作系统内核程序临界区时,程序会给这个内核临界区上一个锁,而进程的调度又需要访问这个内核临界区,故在该进程访问结束之前,该进程不会被中断。3.当操作系统处理原语的过程。
这三种情况主要要讲的是第二种,我们要区分好内核临界区和临界区的区别,内核临界区是指操作系统管理的PCB等核心数据,当这些数据被进程访问而上锁的时候,操作系统是无法进行进程调度的,不过这种内核临界区的访问速度是很快的,宏观上不会影响系统调用。而临界区不同,临界区是当一个进程访问某个I/O设备时所访问的区域,当一个进程访问临界区时,访问I/O设备大多都是一个慢速的过程,虽然进程访问I/O设备时也会给I/O设备上锁以免其他进程使用I/O设备,但是这个慢速的过程如果不允许进程调用,那么在宏观上就会出现很大的问题。
八、调度算法
8.1评价调度算法的指标
1.CPU利用率:CPU的实际运行时间 / 总时间 2.系统吞吐量:总共完成了多少作业 / 总时间共花了多少时间 3.周转时间:作业完成时间 - 作业提交时间 4.平均周转时间:总周转时间 / 作业数 5.带权周转时间:作业周转时间 / 作业运行时间 6.平均带权周转时间:总带权周转时间 / 作业数 7.等待时间:进程就绪态到运行态的时间或者作业从创建态到运行态的时间。 8.响应时间:用户做出一个操作,操作系统响应该操作的时间
8.2先来先服务调度算法(FCFS)(进程 作业)
算法思想:这种算法就是哪个进程 / 作业优先到来,哪个进程 / 作业就优先服务的算法。适用于进程以及作业,各个进程以一种先到先得的公平的方式来切换。
算法实现:
/ | 进程A | 进程B | 进程C | 进程D |
到达时间 | 0 | 2 | 4 | 5 |
运行时间 | 7 | 4 | 1 | 4 |
进程A最先到达,故其最先上CPU进行处理,而处理到时刻2的时候,进程B就需要等待进程A完成后B才能上CPU运行,而进程C在4时刻就到达了,那么就需要等待A于B运行完毕后,C才能上CPU运行,进程D同理。 在分析的过程中我们发现,进程C的运行时间很短,但等待时间却很长;而进程A的运行时间很长,而等待时间却很短。我们来分别算一下他们的算法指标。 周转时间:PA=7-0=7;PB=7+4-2=9;PC=7+4+1-4=8;PD=7+4+1+4-5=11 带权周转时间:PA=7/7=1;PB=9/4=2.25;PC=8/1=8;PD=11/4=2.75 等待时间:PA=0-0=0;PB=7-2=5;PC=7+4-4=7;PD=7+4+1-5=7 我们通过上面的计算可以发现,进程A的运行时间最长,但其周转时间却最短,原因就是其等待时间最短,但是进程C的运行时间最短,但其周转时间却比A还长,原因就是进程C等待时间最长。而通过带权周转时间的公式我们可以看出带权周转时间可以反应一个进程使用CPU的幸福程度,进程C只需要1个单位的运行时间,但是却要等待7个单位的时间。显然这对于C来讲是十分不利的。
优缺点:优点为算法实现起来简单,先来后到的原则十分公平;缺点是对于长作业用户来说体检较好,但是对于短作业用户来说却十分不利。算法的效率也很低。
这种算法由于秉持着先来后到的原则,故这种算法是非抢占式算法。且由于进程的这种先来后到的原则,故某个作业 / 进程准备好后,排在其前面的进程 / 作业必然是可以在有限的时间内完成并处理该进程,故不会导致饥饿。
8.3短作业优先调度算法(SJF)(进程 作业)
算法思想:这种算法的第一步就是如果此时CPU没有处理其他进程 / 作业,那么进来一个进程 / 作业就直接让其上CPU运行,不需要管其运行时间的长短。但是在第一个进程运行到一半还剩下7个单位的时间才能运行完毕的时候进来一个运行时间仅为1的进程,那么这种算法由于是短作业优先,故操作系统会剥夺先来进程的CPU使用权,转而让这个运行时间较短的进程上CPU运行。但是这种算法争议比较大,默认为非抢占式的,不过也有抢占式的。而且如果有无限个运行时间极短的进程,那么就会导致那些长进程的饥饿,甚至是饿死。
注意:这种抢占式短作业优先算法可以使平均等待时间和平均周转时间最少,但是不同的教材有不同的说法,非抢占式的短作业优先需要各个进程几乎同时到达才行。
抢占式算法实现:
/ | 进程A | 进程B | 进程C | 进程D |
到达时间 | 0 | 2 | 4 | 5 |
运行时间 | 7 | 4 | 1 | 4 |
进程A最先到达,但是在运行到时刻2的时候进程B进来,此时进程A还剩下5个单位的运行时间,但进程B只需要4个单位的运行时间,故操作系统会剥夺进程A的CPU使用权,转而让进程B上CPU运行,进程B运行2个单位的时间后还剩下2个单位的运行时间,但此时进程C进入就绪态,此时进程C只需要1个单位的运行时间,故操作系统会剥夺进程B的CPU使用权,转而让进程C上CPU运行,依次类推。我们发现这种短作业优先似乎提高了整体的算法效率,我们来计算一下。 周转时间:PA=7+4+1+4-0=16;PB=2+2+1+2-2=5;PC=5-4=1;PD=11-5=6 带权周转时间:PA=16/7=2.28;PB=5/4=1.25;PC=1/1=1;PD=6/4=1.5 等待时间:PA=16-7=9;PB=5-4=1;PC=1-1=0;PD=6-4=2
优缺点:优点就是通过上面的计算可以看出抢占式短作业优先可以使平均周转时间于等待时间达到很短,提升算法效率。缺点为对长作业不利,而且容易引起饥饿现象。
8.4高响应比优先调度算法(进程 作业)
算法思想:该算法融合了先来先服务算法以及短作业优先算法,它利用一种叫做响应比的变量来克服长进程可能会引发饥饿的缺点,又克服了先来先服务对短作业不利的缺点。响应比=(等待时间+运行时间) / 运行时间,而CPU会优先处理响应比高的进程。 算法实现:通过响应比公式我们不难想象,当一些进程想要被CPU调度的时候,等待时间越长,运行时间越短的进程会被优先执行。也就是如果长作业进入时间较晚,而根据运行时间越短越会被优先执行的特点,长作业会被暂时搁置,而长作业等待时间足够长的时候,长作业又会被运行,这样就即保证了短作业先处理,又保证了别让长作业饿死的特点。不过这种算法使非抢占式的
优缺点:这种算法没什么缺点,优点就是综合了FCFS算法以及SJF算法的优点。
以上三种算法单拿出来都是早期操作系统的算法,由于早期的操作系统并不会为用户提供交互的功能,所以这三种算法都默认为非抢占式,不过SJF也有抢占式。
下面几种算法是现代操作系统的算法,具有交互性,故下面的进程调度算法都为抢占式的。
8.5时间片轮转调度算法(RR)(进程)
算法思想:这种算法是伴随着分时操作系统的诞生而诞生的,通过时钟管理来向CPU发送时钟中断的方式来为每个进程分配时间片供其上CPU运行,而被剥夺了CPU使用权的进程以及刚刚到达的进程将被放到就绪队列中,操作系统通过就绪队列来判断下一个该让哪个进程进入CPU运行。这种算法更注重时间片的轮转,故不考虑其周转时间。由于这种算法是基于为各个进程分配时间片的方式进行调度,那么也就意味着,该算法只针对进程,因为只有进程才有PCB,才能被分配时间片。由于操作系统会剥夺某个程序的CPU使用权,故这个算法为抢占式。
算法实现:
/ | 进程A | 进程B | 进程C | 进程D |
到达时间 | 0 | 2 | 4 | 5 |
运行时间 | 5 | 4 | 1 | 6 |
我们假设时间片为2个单位时间,首先进程A最先到达,进程A上CPU运行了两个单位时间结束的同时进程B也随之到达,问题来了,A此时时间片用完需要被放到就绪队列中,但是B刚刚到达也需要被放到就绪队列中,那么A和B谁先进入就绪队列呢?那么我们说:默认情况下,刚刚到达的进程会被优先的放入就绪队列。故此时进程B进入CPU运行,而B运行了2个时间片后,进程C随之到达,那么还是让C先进入就绪队列,进程B后进入。此时的队列顺序为A=>C=>B,那么进程A上CPU运行了1个单位时间此时进程D到达,由于A的时间片还未用完故需要让D进入队尾等待。此时的就绪队列为C=>B=>D,这样依次类推。
优缺点:该算法对每个进程都很公平,时间片都是一样的,而且对进程的响应很快。不过相应的,由于需要快速响应进程,就需要高频率的进程切换,所以会消耗一定的CPU资源,适用于分时操作系统,不能辨别并及时处理紧急任务。
这里要注意,该算法的时间片不能过大也不能过小,时间片过大会导致该算法退化为FCFS算法,而时间片过小的话会导致进程调度次数过多而使CPU资源大多被浪费在了进程调度上而非进程运行上,一般我们认为进程调度占用资源不大于总资源的1%即可。
8.6优先级调度算法(作业 进程 I/O调度)
算法思想:这种算法是根据一个叫做优先数的变量来决定哪个进程先使用CPU。优先数越大代表该进程优先级越高,该进程也将越优先被执行。这种算法由于没有像时间片那种明确的中断机制,故该算法即有抢占式的,也有非抢占式的。
算法实现:
/ | 进程A | 进程B | 进程C | 进程D |
到达时间 | 0 | 2 | 4 | 5 |
运行时间 | 7 | 4 | 1 | 4 |
优先数 | 1 | 2 | 3 | 2 |
非抢占式:首先是进程A到达,此时CPU空闲,故虽然进程A的优先数很低,不过进程A也可以先上CPU运行,当进程A运行结束后此时就绪队列中进程C的优先数最大,故令进程C上CPU运行,当进程C运行结束后出现了问题,此时的时间远远大于5,也就是说此时就绪队列中有两个优先数相同的进程,那么我们遵守先来先服务原则,令进程B先上CPU运行,最后是进程D。
抢占式:首先是进程A到达,CPU空闲进程A直接运行,当A运行了2个单位时间后,进程B到达此时进程B的优先级高于进程A,故进程A将被剥夺CPU使用权返回就绪队列中,当进程B运行2个时间后,进程C到达,进程C的优先数高于进程B,故进程B也将返回就绪队列,进程C上CPU运行,运行1个单位时间后进程D到达,但是此时就绪队列中进程B虽然优先数与进程D相同,但是由于进程B是先来的,秉持着先来先服务原则,故进程B会先上CPU运行,运行完毕后正好进程D上CPU运行完毕后,此时没有其他优先数高于进程A的进程,故让进程A上CPU运行。
优先数的设定:优先级分为静态优先数和动态优先数,静态优先数是在进程建立好之后按照其重要程度分配到,一旦分配好就不会改变;而动态优先数比较人性化,当一个进程占用了CPU的时间过长的时候可以适当的降低其优先数,而一个进程很久没有上CPU运行的时候可以适当提升其优先数。 优先数的设定一般秉持着三种原则:1.系统进程优先级高于用户进程。原因是系统进程都是核心进程,核心进程需要先完成才能去做用户进程,例如电脑不开机怎么玩游戏呢?2.前台进程优先级高于后台进程。原因是前台进程一般来说都是直接会被用户看到的进程,所以需要保持其流畅度,才能给用户带来更好的体验。3.I/O型进程优先级很高。原因是,I/O设备大多是慢速设备,如果适当提升其优先级可以使I/O设备更早的投入工作,这样可以提升I/O设备的利用率。
优缺点:这种算法可以优先运行更重要的进程,适用于实时操作系统,不过如果有源源不断的高优先级的进程到达,那么会导致低优先级的进程饥饿甚至是饿死。
8.7多级反馈队列调度算法(进程)
算法思想:多级反馈队列综合了以上所有的调度算法,可谓是全能王。这种算法是设置多个优先级从高到低的就绪队列,进程处于优先级越高的就绪队列被分配的时间片越少,进程进入CPU是按照先来先服务的原则,而优先级高的队列进程会抢占低级队列进程。这样一套操作下来,即使用到了FCFS算法和抢占式的SJF算法,又使用了RR算法的时钟管理,又使用了优先级调度算法,而短作业又会根据就绪队列的时间片大小会被优先完成。所以这种算法集中了前几种算法的优点。由于该算法涉及到了时间管理,以及优先级高的队列进程抢占优先级低的队列进程,故进程在运行的时候可能会被强行剥夺CPU的使用权,故这种算法为抢占式算法,且仅适用于进程。不知道你们发没发现,一旦涉及到了时间管理,那这种算法就只能适用于进程,且一定是抢占式,故可以理解记忆,不要死记硬背。
算法实现:
/ | 进程A | 进程B | 进程C |
到达时间 | 0 | 1 | 5 |
运行时间 | 8 | 4 | 1 |
我们共规划3个队列,一级队列优先级最高,时间片为1,二级队列时间片为2,三级队列时间片为4。进程A首先到达,上一级队列运行,运行1个时间片后,进程B到达,此时进程A的时间片用完,故会进入二级队列。进程B在一级队列运行1个时间片后,进程B进入二级队列。此时无新进程到达,故令二级队列的队头进程上CPU运行,进程A运行2个单位时间后,时间片用完进程A下CPU进入三级队列,此时还是无新进程到达,故让进程B上CPU运行,运行1个单位时间后进程C到达,不过此时CPU正在被进程B使用,由于这种算法为抢占式的,所以进程C会抢占进程B的CPU使用权,这种由于被抢占而下CPU的进程并不会掉到下一级队列,而是返回他原本所在的队列。当进程C运行1个单位时间后运行完毕,由于二级队列中还有进程B,故先让进程B上CPU运行,运行2个单位时间后运行完毕,进程A上CPU运行,由于此时已经无其他进程,故让进程A直接运行完毕即可。
优缺点:该算法综合了上面所有算法的优点,只有一个缺点就是会导致饥饿。
8.8多级队列调度算法
这种调度算法类似于优先级调度算法,只不过这种算法给各种优先级设置了一个队列,优先级调度算法中,系统进程优先级最高,交互式进程优先级其次,批处理进程优先级最低。系统进程由于重要程度很高,故优先级高。
而交互式进程就类似于我们平时的用户使用的进程,比如我们点击鼠标,电脑会迅速作出反应,而我们使用APP的时候点击什么功能,APP也会迅速作出反应,由于需要快速作出反应,故这种进程一般采用时间片轮转调度算法来频繁的切换进程,那么这种进程由于是面向用户的进程,需要保证他们的流畅度,故优先级也很高,仅次于系统进程。
优先级最差的就是批处理进程了,我们所谓的批处理进程可以理解成后台进程,是我们用户看不见的进程,比如我们在看视频的时候,可能会给视频上一层特效渲染,这种后台进程优先级差一点对我们用户来说其实没什么影响。这种进程一般采用FCFS调度算法。
九、进程互斥
这里先简要提一下什么是进程同步,进程同步是用来解决进程异步性的问题的,操作系统会是这些进程以一种我们所期望的运行速度来运行。比如我们之前讲到的管道通信,我们直到管道通信不能同时写信息和读信息,所以我们需要使他们以一种我们期望的方式来读写信息,比如写三条信息,读三条信息,这就是进程同步。
进程互斥概念:进程互斥就是当多个进程要使用有限的系统资源时,需要这些进程按顺序使用,说白了就是我们操作系统中的互斥共享方式,而这种有限的系统资源就叫做临界资源,而访问临界资源所执行的代码就叫做临界区。
那么是如何完成这种互斥的效果呢?我们规定,当一个进程访问临界资源的时候,该进程会给临界资源上一个 “锁” ,用来告诉其他进程:此临界资源正在被占用,你们需要等待。
知识连锁:这里要注意,系统的内核临界区和这种临界资源的临界区,虽然都是临界区,且进程访问临界区的时候都会给临界区上锁。但是由于内核临界区是进程调度所必须要访问的,故内核临界区上锁的时候,CPU只能服务给临界区上锁的进程,这个过程十分迅速宏观上不会影响操作系统的进程调度,而临界资源的临界区被上锁却没有这种影响。
为了保证互斥访问的高效性我们规定了进程互斥访问的四个准则: 1.空闲让进:当临界资源空闲的时候,任何进程都可以直接访问该临界资源。 2.忙则等待:当临界资源正在被其他进程占用的时候,其他进程就需要等待临界资源忙完后访问。 3.有限等待:对于想要访问临界区的进程,需要在规定时间内使其使用临界资源以免饥饿。 4.让权等待:当一个进程无法进入临界区的时候,需要释放处理机,防止进程盲等待。