计算机操作系统(四)

文章目录

  • 第四章 多线程
    • 1.线程与进程
      • 1.1 线程的概念
      • 1.2 线程与进程
    • 2.用户级线程的创建和切换
      • 2.1 用户级线程间的切换
      • 2.2 用户级线程的创建
    • 3.内核级线程的切换与创建
      • 3.1 内核级线程
        • 3.1.1 多线程模型
      • 3.2 内核级线程间的切换
      • 3.3 内核级线程的创建
    • 4.0号进程和1号进程
    • 5.CPU调度
      • 5.1 调度的层次
      • 5.2 进程调度方式
      • 5.3 典型调度算法
        • 5.3.1 非交互式调度
          • (1)先来先服务调度
          • (2)最短作业优先调度
        • 5.3.2 交互式调度
          • (1)轮转调度
        • 5.3.3 综合调度
          • (1)多级队列调度
          • (2)多级反馈队列调度
        • 5.3.4 补充调度算法
          • (1)最早截止日期调度
          • (2)彩票调度算法

第四章 多线程

本章首先引出线程这个概念,并以线程为单位对CPU切换进行详细论述,这是因为线程切换是进程切换的核心内容

进程切换由资源切换和指令流切换两部分构成,其中资源切换是将分配给进程的非CPU以外的资源进行切换,如对当前地址空间的切换(资源切换将在之后的内存管理、文件系统章节详细论述);而指令流切换就是CPU切换,也就是线程切换(这是本章的重点);

1.线程与进程

1.1 线程的概念

并发是CPU高效工作的基础,并发的基本含义就是多段程序交替执行,我们思考,是否可以将这种交替执行的思想应用于一个进程(同一个可执行文件)的不同函数之间呢?这样的交替执行就产生了线程的概念;

我们给出一个浏览器使用线程和不使用线程的例子:

  • 使用四个线程实现一个浏览器,分别是获取数据的线程(GetData)、显示文本的线程(ShowText)、解压图片的线程(ProcessImage)以及渲染图片的线程(ShowImage)
    • 首先获取数据的线程(GetData)被调度开始工作,将网页的总体布局以及页面中的文本信息下载下来;
    • 然后切换到显示文本的线程(ShowText)将网页的基本结构和其中的文本信息显示在浏览器中;
    • 接下来再切回GetData工作,通过网页布局信息下载其中的图形对象标签;下载一些图片对象以后再切换到解压图片的线程(ProcesImage)执行,待解码完成以后切换到渲染图片的线程(Showlmage)执行,此时图片就会被逐个显示出来;
  • 假如只使用一个进程来实现,效果将会变成执行的代码必然是首先将页面布局、文本信息、图片对象等内容全部下载下来,然后再逐个解码渲染,最后再将所有的信息全部整理好输出到屏幕上;

肯定又有人问了,那我使用四个进程呗?和使用四个线程有什么区别?进程和线程的区别在于是否共享资源(线程之间不会共享栈!!!这在之后我们会介绍),此处因为这四个函数是可以不使用地址隔离策略将内存缓存区分离的(不存在安全性问题),况且使用四个进程会造成内存的浪费以及代码执行效率的降低,所以我们选择共享共同地址空间等进程资源的四个并发指令执行序列作为四个线程;

1.2 线程与进程

下面的表格给出了线程和进程的区别和联系

计算机操作系统(四)_第1张图片

线程相较于进程的优点:

1.从资源上来讲:线程是一种非常“节俭”的多任务操作方式。而进程的创建需要更多的资源。
2.从切换效率上来讲:运行于一个进程中的多个线程,它们之间使用相同的地址空间,而且线程间彼此切换所需时间也远远小于进程间切换所需要的时间。
据统计,一个进程的开销大约是一个线程开销的30倍左右。
3.从通信机制上来讲:对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过进程间通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其他线程所用,这不仅快捷,而且方便。

线程和进程的关系:

  • 进程是处理机提供的并发抽象;
  • 线程是进程层面提供的并发抽象;
  • 流水线是指令层面提供的并发抽象;

2.用户级线程的创建和切换

在一个地址空间下启动并交替执行的线程既可以由操作系统管理,也可以由用户程序管理:

  • 由用户程序管理的线程对操作系统透明,操作系统完全不知道这些线程的存在,称为用户级线程;
  • 由操作系统管理的线程是内核级线程;

2.1 用户级线程间的切换

想要实现多个用户级线程之间的切换,只需要在切换位置(调度点)调用一个普通的用户态切换函数(由用户自己编写)即可;

在设计用户态切换函数的时候需要注意:如果每个线程用自己的栈,那么在线程中执行函数调用和返回时就不会莫名其妙地跳转到其他线程中;

单个栈执行的两个用户级线程的切换实例

切换函数Yield是完成线程切换的核心,这里我们举例说明Yiled的工作过程:两个用户级线程,线程1执行A函数并在其中调用B函数;线程2执行C函数并在C函数中调用D函数;

计算机操作系统(四)_第2张图片

首先线程1正常执行函数A,当需要调用函数B时需要通过一些栈来保存信息以便在函数B执行完毕后可以跳回A继续执行(这是函数调用的基本常识),因此我们将需要返回的地址104压栈;同理B中调用Yield的时候也会将204压栈,接着执行Yield函数(找到下一个线程以及下一个线程切换过去的时候的执行位置)

Yiled(){jmp 300};

切换到线程2执行,同理依次压入304和404,此时再次执行Yield函数跳转回线程1,因此此时的Yield函数应该为

Yiled(){jmp 204};//跳转回204的地址,因为刚才线程1是从204之前切换出去的

当我们从204继续执行的时候,遇到B的“}”会发生弹栈进行函数返回,我们期望的情况是能够弹回104即A函数中,但是很明显我们的栈会弹出线程2的404地址,因此我们需要做出改进

独立栈执行的用户级线程的切换实例

计算机操作系统(四)_第3张图片

因为现在每个线程都拥有自己的栈,所以在Yiled函数切换的时候不仅要修改PC的值,还需要完成栈的切换,总结如下:

  • 用户级线程的切换就是在切换位置上调用Yield函数;

  • 切换函数Yield完成的基本工作就是找到下一个线程的TCB,根据当前线程中的TCB和下一个线程的TCB完成用户栈的切换,具体来说就是将寄存器ESP中的值esp1保存在当前线程的TCB中,然后从下一个线程的TCB中取出保存的esp2值赋给ESP寄存器;

  • 新栈中的Yield函数中的“}”会将PC指针切换到下一个线程要执行的指令的位置;


Q:TCB和PCB有什么区别?答案参考自(28条消息) 进程PCB与线程线程TCB_shintyan的博客-CSDN博客_pcb和tcb

A:TCB是进程控制块,PCB是线程控制块(也称为任务控制块):

  • PCB作为独立运行基本单位的标志,PCB也是进程存在于系统中的唯一标志,PCB提供了进程管理所需要的信息、提供了进程调度所需要的信息并实现与其他进程的同步与通信,PCB中主要有如下信息:

    • 进程标识符:用于唯一地标识一个进程,一个进程通常有外部标识符(方便用户对进程的访问)和内部标识符(方便系统对进程的使用)两种;
    • 处理机状态:由处理机的各种寄存器(通用寄存器、指令计数器、程序状态字、用户栈指针)中的内容组成;
    • 进程调度信息:OS进行调度的时候必须了解进程的状态及相关信息,包含进程状态、进程优先级、进程调度所需的其他信息、事件;
    • 进程控制信息:用于进程控制所必须的信息,包含程序和数据的地址、进程同步和通信机制、资源清单、链接指针;
  • 每个进程都有一个PCB,每个线程也都存在一个TCB,将所有控制和管理线程的信息记录在TCB中,TCB中主要包含如下信息:

    • 线程标识符:每个线程都存在一个唯一的线程标识符
    • 一组寄存器(PC、状态寄存器、通用寄存器)
    • 线程运行状态
    • 线程优先级
    • 线程专有存储区域:用于线程切换时存放线程保护信息以及与该线程相关的统计信息
    • 信号屏蔽:对某些信号加以屏蔽
    • 堆栈指针:在TCB中需要设置两个指向堆栈的指针,即指向用户自己堆栈的指针(当线程运行在用户态时,使用用户栈保存局部变量和返回地址)和指向核心栈的指针(当线程运行在核心态时使用系统的核心栈)

    与TCB相比,PCB额外存放地址空间、进程包含的线程Tid

2.2 用户级线程的创建

用户级线程由线程库创建和管理,其实现都在用户态,内核无法感知;(老师发的PDF里面还介绍了一种通过内核线程创建用户线程的方式,关于这点我在网上没有查阅到相关资料)

一旦明白切换的具体实现,线程的创建就非常容易理解 —— 线程的创建就是将线程做成第一次切换的样子(此处描述可能比较抽象,下面会详细描述);

一个进程通常包含多个线程,多个线程中必须包含一个主线程,进程启动时会从主线程(程序运行时即使没有创建线程后台也会存在如主线程、gc线程等多个线程)开始执行,接着主线程调用其他子线程或其他子线程之间相互调用,主线程结束意味着整个进程都会结束,那么其他所有子线程也会结束(强制结束);

我们假设线程1是主线程,线程1在执行过程中调用Yield()转换函数让出CPU切换到线程2中执行,如果线程2能够顺利执行则证明线程2创建成功(当然这里讨论的都是用户态线程的创建)

计算机操作系统(四)_第4张图片

创建用户级线程的函数thread_create()大致如下

//用户级线程创建代码
thread_create(void*func)
{
    long*stack = malloc(SIZE_OF_USERSTACK)+ SOME SIZE;
    TCB*p = malloc(SIZE OF_TCB);
	*stack = func;
	*(stack--)=eax;//初始化执行现场,可以全是0
	......
	p->esp = stack;
}

3.内核级线程的切换与创建

3.1 内核级线程

前面介绍了用户级线程,下面我们通过用户级线程引出内核级线程,主要参照文章用户级线程和内核级线程,你分得清吗? - 知乎 (zhihu.com)(这篇文章争议比较多,但是对于初学者理解用户级线程和内核级线程完全够用)

用户级线程这个概念刚提出的时候,操作系统的厂商为了避免直接将未验证过的东西加入内核于是编写了一个关于用户级线程的函数库,However,这个函数库位于用户空间,也就是说操作系统内核对这个函数库一无所知(也就意味着操作系统的眼中还是只有进程而没有线程这个概念,所以借助线程库写的多线程进程实质上还是只能在一个CPU核心上运行)

结论1:对操作系统来说,用户级线程具有不可见性(透明性)

结论2:用户级线程只能使用一个处理器,只能做到并发而无法做到并行加速

因为用户级线程的透明性,导致操作系统无法主动切换线程,也就意味着A、B两个进程同时存在时,A在运行的时候线程B想要运行只能等待A主动放弃CPU;

那么用户级线程就一无是处了?作为程序员,我们可以为自己编写的应用程序定制调度算法(自己决定什么时候退出线程),有关用户级线程的其他好处我们在之后会总结;

因为用户级线程在操作系统中是看不见的,那么当其中某一个线程阻塞(比如上面的A线程阻塞,那么B也会一直等待下去),在操作系统眼中是整个进程都阻塞了,对于这种情况也出现了相应的解决方法(如jacket),但如果我们使用内核级线程就不会存在这样的问题(线程A被阻塞,但与它同属一个进程的线程B不会被阻塞);

为了实现内核级线程,内核中需要有一个能够记录系统中所有线程的线程表,每当需要新建一个内核级线程的时候都需要进行一个系统调用进行线程表的更新,得益于线程表,操作系统可以看见内核级线程,因此操作系统可以将这些多线程放在多个CPU核心上实现真正的并行,然而内核级线程并不是完全优秀,因为内核级的线程调度需要操作系统来实现,这意味着每次切换内核级线程都需要陷入内核态,而操作系统从用户态到内核态是有开销的,同时线程表存放在堆栈空间其数量受到限制,拓展性比不上用户级线程;


总结一下用户级线程和内核级线程(原文链接(28条消息) 用户级线程和内核级线程_TABE_的博客-CSDN博客_用户线程和内核线程):

  • 用户级线程:用户级线程仅存在于用户空间中,对于操作系统是不可见的。此类线程的创建、撤销、线程之间的同步与通信功能,都无须利用系统调用来实现。每个线程并不具有自身的线程上下文(线程上下文保存的是线程用到的寄存器、内存中的数据等);
  • 内核级线程:内核级线程的管理(建立和销毁等)是由操作系统通过系统调用完成的。内核为进程及其内部的每个线程维护上下文信息,调度也是在内核基于线程架构的基础上完成。内核线程驻留在内核空间,它们是内核对象;
  • 有了内核线程的概念,每个用户线程被映射或绑定到一个内核线程,用户线程在其生命期内都会绑定到该内核线程,一旦用户线程终止,两个线程都将离开系统,这被称作”一对一”线程映射;除此之外,还有多对一,多对多的线程映射(关于线程映射之后还会细说);
  • 用户级线程和内核级线程的切换(下一节介绍)同样存在区别:
    • 用户级线程切换的核心是根据存放在用户程序中的TCB找到用户栈,通过用户栈切换完成用户级线程的切换,整个切换过程通过调用Yiled()函数引发;

    • 内核级线程切换的核心是首先进入操作系统内核并在内核中找到线程TCB,进而根据TCB找到线程的内核栈(进入内核之后需要在内核中的某个地方完成PC指针切换,于是仿照用户级线程,将这个PC指针放在栈中,利用内核栈的切换引发PC指针的切换)、通过内核栈切换完成内核级线程切换,整个切换过程由中断引发;

用户级线程的优点 用户级线程的缺点
线程切换代价的代价比内核线程少,因为保存线程状态的过程和调用程序都只是本地过程,没有上下文的切换 线程发生I/O或页面故障引起的阻塞时,如果调用阻塞系统调用则内核由于不知道有多线程的存在,而会阻塞整个进程
允许每个进程定制自己的调度算法,线程管理(创建、销毁等)比较灵活 由于每个线程并不具有自身的线程上下文。因此就线程的同时执行而言,任意给定时刻每个进程只能够有一个线程在运行,而且只有一个处理器内核会被分配给该进程。
内核级线程的优点 内核级线程的缺点
多处理器系统中,内核能够并行执行同一进程内的多个线程 即使CPU在同一个进程的多个线程之间切换,也需要陷入内核,因此其速度和效率不如用户级线程
如果进程中的一个线程被阻塞,能够切换同一进程内的其他线程继续执行

3.1.1 多线程模型

某些操作系统同时支持用户线程和内核线程,实现了用户级线程和内核级线程的连接方式:

1)多对一模型。将多个用户级线程映射到一个内核级线程,线程管理在用户空间完成。此模式中,用户级线程对操作系统不可见(即透明)。
优点:线程管理是在用户空间进行的,因而效率比较高。
缺点:一个线程在使用内核服务时被阻塞,整个进程都会被阻塞;多个线程不能并行地运行在多处理机上。
2)一对一模型。将每个用户级线程映射到一个内核级线程。
优点:当一个线程被阻塞后,允许另一个线程继续执行,所以并发能力较强。
缺点:每创建一个用户级线程都需要创建一个内核级线程与其对应,这样创建线程的开销比较大,会影响到应用程序的性能。
3)多对多模型。将n个用户级线程映射到m个内核级线程上,要求m≤n。
特点:多对多模型是多对一模型和一对一模型的折中,既克服了多对一模型并发度不高的缺点,又克服了一对一模型的一个用户进程占用太多内核级线程而开销太大的缺点。此外,还拥有多对一模型和一对一模型各自的优点,可谓集两者之所长。


Q:为什么要把用户线程和内核线程结合在一起啊?有什么意义呢?

A:用户线程就是用户自己创建的线程调度程序,但是这样的用户线程实际上不能单独运行,用户线程运行的唯一方法就是告诉内核线程,使其帮忙执行用户线程中包含的代码;

简单来说用户线程根本就不是线程,仅仅算得上是用户程序中的一堆数据,内核线程才是真正的线程,所以要使得用户线程能够运行只能是将其与内核线程映射关联;


3.2 内核级线程间的切换

回顾用户级线程的切换主要分为三个步骤:

  • TCB切换;

  • 根据TCB中存储的栈指针完成用户栈切换;

  • 根据用户栈中压入函数返回地址完成PC指针切换;

补充知识点:

内核栈(栈是用来在函数跳转时保存返回地址等重要信息以备将来返回的)中记录了当前用户栈的位置和当前用户程序执行的位置,在内核级线程切换的时候利用这两个信息完成PC指针的切换以及用户栈的切换;

内核级线程的TCB存储在操作系统的内核中,因此完成TCB切换的程序应该执行在操作系统的内核中,因此内核级线程的切换应该从进入内核开始,那么我们就必须先介绍中断,因为中断会导致用户态到内核态的切换,那么首先我们就先来弄明白发生中断过后会有什么情况(几乎所有的外部中断都会引起下面的动作):

中断指令执行时,会找到当前进程的内核栈,然后将用户态执行的一些重要信息压到内核栈中;


简单来说内核级线程切换仍然完成三个工作:切换TCB、切换栈和切换PC指针,但是这些切换动作要分散在中断入口、中断处理、线程调度、上下文切换以及中断返回等多个地方。不像用户级线程切换那样所有切换动作都在一个Yeild()函数中。因此内核级的切换过程就复杂得多,为清晰起见,将内核级线程的切换过程归纳整理为下图所示的五个阶段:


Q:很多人可能会疑惑不是说内核级线程之间的切换吗?怎么看图好像是用户线程1->内核线程1->内核线程2->用户线程2这样的一个切换顺序

A:这里因为书上有些概念没讲明白,所以我们额外讲一下

  • 用户线程和内核线程(上面已经介绍过):
    • 内核线程只工作在内核态中;
    • 用户线程既可以工作在内核态,也可以运行在用户态;
  • 用户态和内核态:操作系统的两种运行级别,线程处于用户态则其访问资源受限,处于内核态则可访问计算机任何资源;
  • 内核栈和用户栈:每个进程都有两个栈,用户栈与内核栈(不同进程共享内核空间):
    • 进程处于用户空间(用户态)的时候,CPU堆栈指针寄存器的内容是用户栈地址;
    • 进程中断或系统调用陷入内核态时,CPU堆栈指针寄存器的内容是内核空间(内核栈)地址;
    • 更多详细关于内核栈和用户栈之间的相互转换参考(28条消息) 用户线程/内核线程、用户态/内核态、用户栈/内核栈的理解_jinyidong的博客-CSDN博客

好的,现在我们来讲为什么会出现用户栈的概念(参考原文(28条消息) 8.内核级线程(核心级线程)_PacosonSWJTU的博客-CSDN博客_内核级线程):

  • 进程必须在内核中,切换进程实际上就是切换内核级线程;
  • 切换用户级线程是切换内核级线程的一部分;
  • 每个内核级线程需要的是一套栈(用户栈和内核栈)而不仅仅只是一个栈,不要觉得说内核线程就只能有内核栈,因此内核线程的切换是需要TCB切换一套栈的:
    • 进入内核态前,把线程的用户栈信息(元数据)压入到内核栈,即把同一个线程的用户栈与内核栈关联起来

计算机操作系统(四)_第5张图片

  1. 第一阶段是中断进入,是中断处理的入口,核心工作是记录当前程序在用户态执行的信息(当前使用的用户栈、当前程序执行的位置、当前执行的现场信息等);
  2. 第二阶段是调用schedule引起TCB的切换(当发现线程应当让出CPU时,系统内核会调用schedule完成TCB的切换):
    • schedule函数首先从就绪队列中选出下一个要执行线程的TCB;
    • 找到下一个TCB后使用next指针指向这个TCB;
    • 利用current(内核全局变量current指向当前线程的TCB)和next指针指向的信息就可以开始第三阶段;
  3. 第三阶段是内核栈的切换:将当前的ESP寄存器(指向当前线程的内核栈)存放在current指向的TCB中,再从next指向的TCB中取出esp字段(线程的内核栈地址)赋值给ESP寄存器;
  4. 第四阶段是中断返回:在这一阶段中,要将存放在下一个线程的内核栈(因为内核栈已经切换完成)中的用户态程序执行现场恢复出来,这个现场是这个线程在切换出去时由中断入口程序保存的;
  5. 第五阶段是用户栈切换:实际上就是切换用户态程序PC指针以及相应的用户栈,即需要将CS:EIP寄存器设置为当前用户程序执行地址,将SS:ESP寄存器设置为当前用户栈地址即可;

3.3 内核级线程的创建

内核级线程由内核直接创建并管理

前面已经介绍过内核级线程的切换主要由四个具体的切换构成:切换TCB,切换内核栈,切换用户栈,用户程序PC指针切换;

相应地创建内核级线程的关键在于初始化TCB、内核栈以及用户栈:

  • 第一,创建一个TCB,主要存放内核栈的esp指针;

  • 第二,分配一个内核栈,其中主要存放用户态程序的PC指针、用户栈地址以及执行现场;

  • 第三,分配用户栈,主要存放进入用户态函数时用到的参数等内容;

4.0号进程和1号进程

前面已经介绍过,多进程视图是操作系统的核心视图,多进程视图的核心就是创建进程系统调用fork;

fork的核心是通过复制父进程来创建子进程,操作系统中最基本的两个父进程是0号和1号进程(在操作系统初始化时由系统建立),系统中所有的进程都是从0号进程和1号进程继承而来;

fork是一个只能工作在用户态应用程序中的系统调用,创建0号进程不能使用fork,需要手动设置进程信息(PCB、内核栈、用户栈以及用户程序等)

5.CPU调度

调度的基本概念:当有一堆任务需要处理,但是由于资源有限,这些任务不能同时处理(此处是真正意义上的同时),于是就需要确定某种规则来决定处理这些任务的顺序,这就是调度研究的问题;

线程切换中我们并没有解决这样一个问题 —— 如何在一系列可供选择的就绪线程中选择下一个线程(其目的是将CPU分配给这个线程)是良好的,这就引出本节的主题,CPU调度;

CPU调度简单来说就是在就绪线程/进程队列中选择一个合适的线程/进程,再通过切换机制将CPU资源分配给选择的线程/进程;(说一下,这里本质上应该是进程调度,只是哈工大教材根本没有提及三级调度的概念)

因为根据操作系统是否支持线程,CPU调度的基本单位分为线程和进程,所以这里我们将线程和进程统称为任务;

这里我们以PC机的通用操作系统作为基本对象分析(注意不同的对象其调度策略的目的和设计、实现原则等可能不同),需要考虑如下准则:

  • 任务的周转时间(completion time):任务从新建进入操作系统到任务完成离开操作系统所经历的全部时间;
  • 任务的响应时间:用户向程序发起一个交互操作(单击菜单)到该任务响应该操作(菜单弹出)经历的时间;
  • 系统吞吐量:一段时间区域内计算机系统能够完成的任务总数;

我们举个例子说明CPU调度的重要性,PC机上交互任务和非交互任务同时存在:

  • 交互任务不关心周转时间,强调响应时间;
  • 非交互任务关心周转时间,执行过程中无需交互;

这两个目标之间存在矛盾,不可能同时优化,因此能够有效的折中任务调度策略成为CPU调度分析的核心问题;

5.1 调度的层次

一个作业从提交开始到完成,一般需要经历如下三级调度:

  • 作业调度。又称高级调度,其主要任务是按一定的原则从外存上处于后备状态的作业中挑选一个(或多个)作业,给它(们)分配内存、输入/输出设备等必要的资源,并建立相应的进程,以使它(们)获得竞争处理机的权利。简言之,作业调度就是内存与辅存之间的调度。对于每个作业只调入一次、调出一次。多道批处理系统中大多配有作业调度,而其他系统中通常不需要配置作业调度。作业调度的执行频率较低,通常为几分钟一次;

  • 中级调度。又称内存调度,其作用是提高内存利用率和系统吞吐量。为此,应将那些暂时不能运行的进程调至外存等待,把此时的进程状态称为挂起态。当它们已具备运行条件且内存又稍有空闲时,由中级调度来决定把外存上的那些已具备运行条件的就绪进程,再重新调入内存,并修改其状态为就绪态,挂在就绪队列上等待;

  • 进程调度。又称低级调度,其主要任务是按照某种方法和策略从就绪队列中选取一个进程,将处理机分配给它。进程调度是操作系统中最基本的一种调度,在一般的操作系统中都必须配置进程调度。进程调度的频率很高,一般几十毫秒一次(咱们下面讲的非交互式、交互式实际上都只是讲的进程调度)

作业调度从外存的后备队列中选择一批作业进入内存,为它们建立进程,这些进程被送入就绪队列,进程调度从就绪队列中选出一个进程,并把其状态改为运行态,把CPU分配给它。中级调度是为了提高内存的利用率,系统将那些暂时不能运行的进程挂起来。当内存空间宽松时,通过中级调度选择具备运行条件的进程,将其唤醒;

这里我们再辨析两个概念:进程调度和进程切换

  • 进程调度就是我们上面所说的从就绪队列中选中一个要运行的进程(这个进程可能是刚刚被暂停执行的进程(原因可能是因为资源不足等因素),也可能是另一个进程);

  • 进程切换就是指一个进程让出处理机,由另一个进程占用处理机,进程调度中的第一种情况不需要进程切换(因为本来就是同一个进程),第二种情况(即不同进程的调度)才需要进程切换;

调度了新的就绪进程之后才会进行进程间的切换,理论上这两个事件顺序不能颠倒(事实上也确实不会颠倒),进程切换(也就是我们上面刚开始所说的切换机制)主要完成:

  1. 对原来运行进程各种数据的保存
  2. 对新的进程各种数据的恢复

(有没有发现很类似之前介绍过的线程切换?因为线程切换是进程切换的核心,但是对于进程切换,王道和哈工大似乎都没怎么细讲)

5.2 进程调度方式

进程调度方式简单来说就是当有优先级更高的进程进入就绪队列的时候应该如何分配处理机,通常有两种进程调度方式:

  • 非抢占方式:这种方式下一旦把CPU分给一个进程,该进程就会保持CPU直到终止或转换到等待状态,这种方式不能用于分时系统和多数实时系统(无法处理紧急任务)
  • 抢占方式:若有某个更加重要的进程需要使用处理机的时候,立即暂停正在执行的进程,将处理机分配给这个更加重要的进程,这种方式可以处理更高优先级的进程,也可以实现时间片的轮流处理

5.3 典型调度算法

操作系统中有多种调度算法,有些调度算法适用于作业调度,有的适用于进程调度,有的两者都适用,下面我们介绍的都是能用于进程调度的一些调度算法;

5.3.1 非交互式调度

(1)先来先服务调度

FCFS调度算法就是选择就绪队列头部的的任务调度执行,特性是公平,缺点是很可能导致任务的平均周转时间较长,我们用下面这个例子举例

计算机操作系统(四)_第6张图片

根据FCFS的基本思想我们可以得到其算法实例如下

计算机操作系统(四)_第7张图片

则其平均周转时间(average completion time)为(10+39+42+49+61)/5=40.2,那么假如我们让T2和T3交换次序,则平均周转时间为(10+13+42+49+61)/5=35,其背后的思想是:让任务执行时间短的任务提前执行可以使平均周转时间变小,也就是下面我们会介绍的最短作业优先调度算法;

(2)最短作业优先调度

SJF算法的思想就是按照任务的执行时间从小到大排序,任务按照这个顺序依次调度执行,我们假设表4.2中的五个任务同时出现在0时刻

计算机操作系统(四)_第8张图片

基本思想:如果在调度序列中存在Ti排在Tj前面,但是其执行时间大于Tj,那么就交换Ti和Tj在调度序列中的位置


Q:不难发现这里假设的是5个任务同时到达,然后按照执行时间大小顺序排列,假如不是同时到达呢?

关于上面那个问题,其实也就引出了最短剩余时间优先调度SRTF:每当有新任务到达时,选择当前剩余执行时间最短的任务进行调度执行;

SRTF一定不是直接选择执行时间最短的!这点很容易忽略,SRTF是一种可抢占式调度,这就意味着不是由任务自身主动让出CPU才引起的调度,而是只要有新任务到达就可能导致有任务抢占当前任务的CPU(因为新出现的任务具有更短的执行时间,所以具有更高的优先级);

计算机操作系统(四)_第9张图片

5.3.2 交互式调度

(1)轮转调度

SRTF在非交互任务中完成的很好,但是在交互任务中可能会不尽人意,最简单的,假设图4.15的T2是一个交互任务(用户点击鼠标),在时刻1我们点击了鼠标,在时刻32该用户操作才被响应,这在交互式体验中是非常差的!假如我们在一段时间内让所有的任务都有机会向前推进而不是呆呆的让抢占到CPU的任务执行完毕才让出CPU,是否可以优化响应时间呢?这就引出时间片轮转RR调度:将一段时间等分(执行时间片)的分割给每个任务,当前任务的时间片用完后就会切换到下一个任务;

假设一共有N个任务,时间片长度固定为u,则对于任何一个任务,最多等待N*u的时间,这个任务一定会得到执行的机会;

因此,通过设计合理的N和u可以保证用户响应时间的上界;

5.3.3 综合调度

(1)多级队列调度

操作系统中既有交互任务,也有非交互任务,如何组合RR和SRTF来处理两种任务都存在的情况呢?

最简单的思想就是引入两个队列:

  • 交互任务队列:也称为前台任务队列,采用RR调度;
  • 非交互任务队列:也称为后台任务队列,采用SRTF调度;

通常让前台队列具有更高的优先级,即假如前台队列中存在就绪任务则采用RR调度处理这个队列中的任务(此时就只能让后台队列中的队列慢慢等待);

(2)多级反馈队列调度

多级队列调度存在一个明显的问题:假如采用非抢占式调用,则一旦被后台任务调度得到CPU,则只能等待它执行完毕之后才会主动释放CPU,这段时间可能导致前台任务的响应时间变长;但是如果采用抢占式调用(这里的抢占就是指只要有前台任务就执行前台任务),后果就是后台任务需要等待前台队列中没有任务才能调度;

解决上述问题的方法是后台任务也需要分配时间片,这样就算前台队列中存在任务,后台任务也不至于一直无法执行;

第二个问题是操作系统如何区分前台任务和后台任务 —— 多级队列中的任务类型并不是在任务创建时确定,应该根据任务在执行过程的具体表现来动态调整(编译过程看起来是后台任务,但是Ctrl+C中断编译术语用户交互),而这个动态调整实际上就是“反馈”的含义;

因此动态调整就成为了多级反馈队列调度的核心:

  • I/O动态调整:I/O操作是与用户进行交互的一种方式,因此可以根据I/O操作的多少来区分前后台任务(当然不能说某段时间内没有I/O操作就一定不是交互任务);
  • 执行时长动态调整:对于执行时间较长的任务,降低其优先级并延长其时间片

多级反馈队列调度算法实现思想如下:

  • 设置多个就绪队列,为各个队列赋予不同的优先级;
  • 赋予各个队列中进程执行时间片的大小各不相同,在优先级越高的队列中每个进程的运行时间片越小;
  • 当一个新进程进入内存后,首先将它放在第一级队列的末尾,按照FCFS先来先服务原则排队等待调度,当该进程执行时:
    • 如果能在该时间片内完成则可以准备撤离系统;
    • 如果在一个时间片结束尚未完成,则该进程转入第二级队列的末尾,按照FCFS原则等待…
  • 仅当第一级队列为空时调度程序才会调度第二级队列中的进程运行,同理推导低优先级的队列的进程执行顺序;
    • 若处理机在处理i级的进程时有更高优先级(1~i-1级)队列的进程插入,则新进程直接抢占正在运行进程的处理机

5.3.4 补充调度算法

除了上面介绍的调度算法以外,我们这里额外补充一些有特点的调度算法;

(1)最早截止日期调度

该算法是根据任务的开始截止时间来确定任务的优先级。截止时间愈早,其优先级愈高,越先被处理机执行;

该算法要求在系统中保持一个实时任务就绪队列,该队列按各任务截止时间的早晚排序,具有最早截止时间的任务排在队列的最前面。调度程序在选择任务时,总是选择就绪队列中的第一个任务,为之分配处理机,使之投入运行;

最早截止时间优先算法既可用于抢占式调度,也可用于非抢占式调度方式中;

下面我们直接给出一个例子理解

计算机操作系统(四)_第10张图片

(2)彩票调度算法

参考文章:

  • 彩票调度算法,让进程们拼手气?——分享一个有趣的进程调度算法 - 百度文库 (baidu.com)
  • https://www.cnblogs.com/shuo-ouyang/p/12747980.html

彩票调度的基本思想是:一开始的时候给每个进程发彩票(优先级越高,发的彩票越多),然后每隔一段时间(一个时间片),举行一次彩票抽奖,抽出来的号是谁的,谁就能运行;

假如有两个进程A和B,调度器想让A占用80%的 CPU 时间,B占用20%的CPU时间,调度器就给A发80张彩票,给B发20张彩票;这样,每次抽奖的时候,A就有80%的概率占用CPU,从数学期望上讲,1秒钟之内,A能运行800ms;

计算机操作系统(四)_第11张图片

实际上彩票调度并没有在CPU调度程序里广泛使用,一个原因是不能很好的适合I/O,另一个原因是票数分配问题没有确定的解决方式,比如新打开了一个浏览器进程,那该给它分配多少票?票数少了,响应跟不上,票数多了,又会浪费 CPU时间;

你可能感兴趣的:(操作系统,linux,服务器)