操作系统笔记——进程管理

操作系统笔记——进程管理

2. 进程管理

2.1 进程与线程

2.1.1 进程的引入

在计算机操作系统中,进程是资源分配的基本单位,也是独立运行的基本单位

前趋图

在这里插入图片描述

前趋图是一个有向无循环图,每个结点可以表示一条语句、一个程序段或一个进程,结点间的有向边表示两个结点之间存在的偏序或前趋关系

→={(Pi,Pj)|Pi必须在Pj开始执行之前完成}

在这里插入图片描述

程序的顺序执行

一个程序通常由若干程序段组成,它们必须按照某种先后次序执行,仅当前一个操作执行完后才能执行后续操作,这类计算过程就是程序的顺序执行过程。例如,在处理一个作业时,总是先输入用户的程序和数据,然后再进行计算,最后将所得结果打印出来。

  • 顺序性。处理器的操作严格按照程序所规定的顺序执行,即每一个操作必须在下一个操作开始之前结束。
  • 封闭性。程序一旦开始运行,其执行结果不受外界因素的影响。因为程序在运行时独占系统的各种资源,所以这些资源的状态(除初始状态外)只有本程序才能改变。
  • 可再现性。只要程序执行时的初始条件和执行环境相同,当程序重复执行时,都将获得相同的结果(即程序的执行结果与时间无关)。

程序的并发执行

程序的并发执行是指若干个程序(或序同时在系统中运行,这些(序段)的执行在时间上是重叠的,即一个程序(或程序段)的执行尚未结束,另一个程序(或程序段)行已开始。

程序的并发执行虽然提高了系统的处理能力和资源利用率,但也带来了一些新问题,产生了一些与顺序执行时不同的特征:

  • 间断性。程序在并发执行时,由于它们共享资源或为完成同一项任务而相互合作,致使并发程序之间形成了相互制约的关系。在图2-1 中, 若C1未完成,则不能进行 P1,致使作业1的打印操作暂停运行,这是由相互合作完成同一项任务而产生的直接制约关系:若I1未完成,则不能进行 I2,致使作业 2的输入操作停运,这是共享资源而产的间接制约关系。这种相互制约关系将导致并发程序具有“执行一暂停执行一执行”这种间断性的活动规律。

  • 失去封闭性。程序在并发执行时,多个程序共享系统中的各种资源,因而这些资源的状态将由多个程序来改变,致使程序的运行失去封闭性。这样一个程序在执行时,必然会受到其他程序影响,例如,当处理器被某程序占用时,其他程序必须等待。

  • 不可再现性。程序并发执行时,由于失去了封闭性,也将导致失去其运行结果的可再现性,例如,有两个循环程序A和 B,它们共享一个变量 N。程序A 每执行一次,都要执行 N=N+1 的操作;程序 B 每执行一次,都要执行print(N)操作,然后执行 N=0。由于程序A和程序B的执行都以各自独立的速度向前推进,因此程序A的N=N+1操作可以发在 B 的print(N)作和N=0 操作之前,也可以发生在其后或中间。假设某时刻N的值为n,对于N=N+1出现在B的两个操作之前和之后两种情况(见图 2-2),执行完一个循环后打印出来的N值分别为 n+1和n。

在这里插入图片描述

  • 程序并发执行的条件

    程序并发执行时具有结果不可再现的特征,这并不是使用者希望看到的结果。为此,要求程序在并发执行时必须保持封闭性和可再现性。由于并发执行失去封闭性是共享资源的影响,因此现在要做的工作是消除这种影响。

在这里插入图片描述

若两个程序p1和 p2能满足下述3 个条件,它们便可以并发执行且其结果具有可再现性。

在这里插入图片描述

2.1.2 进程的定义及描述

在多道程序环境下,程序的并发执行破坏了程序的封闭性和可再现性,使得程序和计算不再一一对应,程序活动不再处于一个封闭系统中,程序的运行出现了许多新的特征。在这种情况下,程序这种静态概念已经不能如实地反映程序活动的这些特征,为此引入了一个新的概念——进程。

进程的定义

  • 进程是程序在处理器上的一次执行过程。
  • 进程是可以和别的进程并行执行的计算。
  • 进程是程序在一个数据集合上的运行过程,是系统进行资源分配和调度的一个独立单位。
  • 进程可定义为一个数据结构及能在其上进行操作的一个程序。
  • 进程是一个程序关于某个数据集合在处理器上顺序执行所发生的活动。

进程的特征

  • 动态性。进程是程序在处理器上的一次执行过程,因而是动态的。动态特性还表现在它因创建而产生,由调度而执行,因得不到资源而暂停,最后因撤销而消亡。
  • 并发性。并发性是指多个进程同时存在于内存中,能在一段时间内同时运行。引入进程的目的是使程序能与其他程序并发执行,以提高资源的利用率。
  • 独立性。进程是一个能独立运行的基本单位,也是系统进行资源分配和调度的独立单位。
  • 异步性。异步性是指进程以各自独立的、不可预知的速度向前推进。
  • 结构特征。为了描述和记录进程的运动变化过程,并使之能正确运行,应为每个进程配置一个进程控制块(Process ControlBlock,PCB)。这从结构上看,每个进程都由程序段、数据段和一个进程控制块组成

进程和程序的关系

  • 进程是动态的,程序是静止的。进程是程序的执行,每个进程包含了程序段和数据段以及进程控制块(PCB),而程序是有序代码的集合,无执行含义。
  • 进程是暂时的,程序是永久的。进程是一个状态变化的过程,程序可以长久保存。
  • 进程与程序的组成不同。进程的组成包括程序段、数据段和进程控制块。
  • 通过多次执行,一个程序可以产生多个不同的进程,通过调用关系,一个进程可执行多个程序。进程可创建其他进程,而程序不能形成新的程序
  • 进程具有并行特性(独立性、异步性),程序则没有。

由程序段、相关数据段和 PCB 三部分构成了进程映像,也叫进程实体。进程映像是静态的,进程是动态的,进程是进程实体的运行过程。

进程与作业的区别

作业是用户需要计算机完成某项任务而要求计算机所做工作的集合。一个作业的完成要经过作业提交、作业收容、作业执行和作业完成4个阶段。而程是已提交完的作业的执行过程,是资源分配的基本单位。

  • 作业是用户向计算机提交任务的任务实体。在用户向计算机提交作业之后,系统将它放入外存中的作业等待队列中等待执行:而进程则是完成用户任务的执行实体,是向系统申请分配资源的基本单位。任一进程,只要它被创建,总有相应的部分存在于内存中。
  • 一个作业可由多个进程组成,且必须至少由一个进程组成,但一个进程不能构成多个作业
  • 作业的概念主要用在批处理系统中。像 UNIX 这样的分时系统则没有作业的概念;而进程的概念则用在几乎所有的多道程序系统中。

进程的组成

  • 进程控制块(PCB)。每个进程均有一个 PCB,它是一个既能标识进程的存在、又能刻画执行瞬间特征的数据机构。当进程被创建时,系统为它申请和构造一个相应的 PCB。
  • 程序段。程序段是进程中能被进程度程序调度到 CPU 上执行的程序代码段,能实现相应的特定功能。
  • 数据段。一个进程的数据段可以是进程对应的程序加工处理的原始数据,也可以是程序执行时产生的中间或结果数据

PCB是进程存在的唯一标志

PCB包括的内容:

  • 进程标识符(PID)。每个进程都有唯一的进程标识符,以区别于系统内部的其他进程。在创建进程时,由系统为进程分配唯一的进程标识号。
  • 进程当前状态。说明进程的当前状态,以作为进程调度程序分配处理器的依据。
  • 进程队列指针。用于记录PCB 队列中下一个PCB 的地址。系统中的PCB可能组织成多个队列,如就绪队列、阻塞队列等。
  • 程序和数据地址。指出进程的程序和数据所在的地址。
  • 进程优先级。反映进程要求 CPU 的紧迫程度。优先级高的进程可以优先获得处理器。
  • CPU现场保护区。当进程因某种原因释放处理器时,CPU 现场信息(如指计数器状态寄存器、通用寄存器等)被保存在 PCB 的该区域中,以便该进程在重新获得处理器后能继续执行。
  • 通信信息。记录进程在执行过程中与别的进程所发生的信息交换情况。
  • 家族联系。有的系统允许进程创建子进程,从而形成一个进程家族树。在PCB中,本进程与家族的关系是必须指明的,如它的子进程与父进程的标识。
  • 占有资源清单。进程所需资源及当前已分配资源清单。

在一个系统中,通常存在着很多进程,有的处于就绪状态,有的处于阻塞状态,且阻塞的原因各不相同。为了方便进程的调度和管理,需要将各进程的PCB 用适当的方法组织起来。目前常用的组织方式有链接方式和索引方式

PCB 的作用是为了保证程序的并发执行。创建进程,实质上是创建进程的PCB;而撤销进程,实质上是撤销进程的PCB。

为什么PCB是进程存在的唯一标志?

在进程的整个生命周期中,系统总是通过PCB 对进程进行控制,亦即系统根据进程的 PCB 感知该进程的存在所以,PCB 是进程存在的唯一标志。

2.1.3 进程的状态与转换

进程的5种基本状态

  • 就绪状态。进程已获得了除处理器以外的所有资源,一旦获得处理器,就可以立即执行,此时进程所处的状态为就绪状态。
  • 执行状态(运行状态)。当一个进程获得必要的资源并正在 CPU 上执行时,该进程所处的状态为执行状态。
  • 阻塞状态(等待状态)。正在执行的进程,由于发生某事件而暂时无法执行下去(如等待 I/O 完成),此时进程所处的状态为阻塞状态。当进程处于阻塞状态时,即使把处理器分配给该进程,它也无法运行。

在做题时,要特别注意区分就绪状态与阻塞状态,区分两者的关键在于当分配给该进程处理器时,是否能立即执行,若能立即执行,则处于就绪状态,反之,则为阻塞状态。

  • 创建状态。进程正在被创建,尚未转到就绪状态。申请空白的 PCB,并向 PCB 中填写一些控制和管理进程的信息;然后由系统为该进程分配运行时所需的资源;最后把该进程转入就绪状态。
  • 结束状态。进程正在从系统中消失,可能是正常结束或其他原因中断退出运行。

进程的状态的相互转换

在这里插入图片描述

  • 就绪状态一执行状态。一个进程被进程调度程序选中。
  • 执行状态一阻塞状态。请求并等待某个事件发生。
  • 执行状态一就绪状态。时间片用完或在抢占式调度中有更高优先级的进程变为就绪状态。
  • 阻塞状态一就绪状态。进程因为等待的某个条件发生而被唤醒。

由上可得出结论

  • 进程状态的转换并非都是可逆的,进程既不能从阻塞状态变为执行状态,也不能从就绪状态变为阻塞状态
  • 进程之间的状态转换并非都是主动的,在很多情况下都是被动的,只有从执行状态到阻塞状态是程序的自我行为(因事件而主动调用阻塞原语),其他都是被动的。
  • 进程状态的唯一性。一个具体的进程在任何一个指定的时刻必须且只能处于一种状态。

2.1.4 进程的控制

进程控制的职责是对系统中的所有进程实施有效的管理,其功能包括进程的创建、进程的撤销、进程的阻塞与唤醒等。这些功能一般是由操作系统的内核来实现的。

进程的创建

  • 进程前趋图

在这里插入图片描述

  • 创建原语

    • 用户登录。在分时系统中,用户在终端输入登录信息,系统检测并通过之后就会为该终端用户建立新进程并插入到就绪队列。
    • 作业调度。在批处理系统中,当作业调度程序按一定的算法调度到某个作业时,便将该作业装入内存,为其分配资源并创建进程,并插入到就绪队列。
    • 请求服务。基于进程的需要,由其自身创建一个新进程并完成特定任务。

    进程的创建原语实现,其主要操作过程如下:

    • 先向系统申请一个空闲 PCB,并指定唯一的进程标识符(PID)。
    • 为新进程分配必要的资源。
    • 将新进程的 PCB初始化。为新进程的 PCB 填入进程名、家族信息、程序数据地址、优先级等信息。
    • 将新进程的PCB 插入到就绪队列。

进程对撤销

一个进程在完成其任务后应予以撤销,以便及时释放它所占用的各类资源。撤销原语可采用两种策略:一种是只撤销一个具有指定标识符的进程,另一种是撤销指定进程及其所有子孙进程。导致进程撤销的事件有进程正常结束、进程异常结束及外界干预等。

撤销原语的功能是撤销一个进程,其主要操作过程如下:

  • 先PCB集合中找到撤销进程的 PCB。
  • 若被撤销进程正处于执行状态,则应立即停止该进程的执行,设置重新调度标志,以便进程撤销后将处理器分配给其他进程。
  • 对后一种撤销策略,若被撤销进程有子孙进程,还应将该进程的子孙进程予以撤销。
  • 回收被撤销进程所占有的资源,或者归还给父进程,或者归还给系统。最后,回收它的PCB

进程的阻塞与唤醒

阻塞原语(P 原语)的功能是将进程由执行状态转为阻塞状态,而唤醒原语 (V 原语)的功能则是将进程由阻塞状态变为就绪状态。

阻塞原语的主要操作过程如下:

  • 首先停止当前进程的运行。因该进程正处于执行状态,故应中断处理器。
  • 保存该进程的 CPU现场,以便之后可以重新调用该进程并从中断点开始执行。
  • 停止运行该进程,将进程状态由执行状态改为阻塞状态,然后将该进程插入到相应事件的等待队列中。
  • 转到进程调度程序,从就绪队列中选择一个新的进程投入运行。

唤醒原语的主要操作过程如下:

  • 将被唤醒进程从相应的等待队列中移出。
  • 将状态改为就绪并插入相应的就绪队列。

进程切换

进程切换是指处理器从一个进程的运行转到另一个进程的运行,在这个过程中,进程的运行环境产生了实质性的变化

进程切换的过程如下:

  • 保存处理及上下文,包括程序计数器和其他寄存器。
  • 更新 PCB 信息。
  • 把进程的 PCB移入相应队列,如就绪、某事件的阻塞队列等
  • 选择另一个进程执行,更新其 PCB。
  • 更新内存管理的数据结构。
  • 恢复处理器上下文。

进程切换一定会产生中断,进行处理器模式切换,即从用户态进入内核态,之后又回到用户态;但处理器模式切换不一定产生进程切换,如系统调用同样会从用户态进入内核态,之后回到用户态,但在逻辑上,仍然是同一进程占用处理器执行。

2.1.5 进程通信

进程通信是指进程之间的信息交换进程的互斥与同步就是一种进程间的通信方式。由于进程互斥与同步交换的信息量较少且效率较低,因此称这两种进程通信方式为低级进程通信方式

相应地,也可以将P、V原语称为两条低级进程通信原语。

高级进程通信方式可以分为3 大类:共享存储器系统、消息传递系统和管道通信系统

共享存储器系统

为了传输大量数据,在存储器中划出一块共享存储区,多个进程可以通过对共享存储区进行读写来实现通信。在通信前,进程向系统申请建立一个共享存储区,并指定该共享存储区的关键字。若该共享存储区已经建立,则将该共享存储区的描述符返回给申请者。然后,申请者把获得的共享存储区附接到进程上。这样,进程便可以像读写普通存储器一样读写共享存储区了。

消息传递系统

在消息传递系统中,进程间以消息为单位交换数据,用户直接利用系统提供的一组通信命令(原语)来实现通信。操作系统隐藏了通信的实现细节,简化了通信程序,得到了广泛应用。

根据实现方式的不同,消息传递系统可以分为以下两类:

  • 直接通信方式。发送进程直接把消息发送给接收进程,并将它挂在接收进程的消息缓冲队列上,接收进程从消息缓冲队列中取得消息。
  • 间接通信方式。发送进程把消息发送到某个中间实体(通常称为信箱)中,接收进程从中取得消息。这种通信方式又称为信箱通信方式。该通信方式广泛应用于计算机网络中,与之相应的通信系统称为电子邮件系统。

管道通信系统

管道是用于连接读进程和写进程以实现它们之间通信的共享文件。向管道提供输入的发送进程(即写进程)以字符流形式将大量的数据送入管道,而接收管道输出的进程(即读进程)可以从管道中接收数据。

2.1.6 线程

线程是近年来操作系统领域出现的一种非常重要的技术,其重要程度丝毫不亚于进程。线程的引入提高了程序并发执行的程度,从而进一步提高了系统吞吐量。

线程的概念

  • 线程的引入:如果说在操作系统中引入进程的目的是使多个程序并发执行,以改善资源利用率及提高系统吞吐量,那么,在操作系统中再引入线程,则是为了减少程序并发执行时所付出的时空开销,使操作系统具有更好的并发性

  • 线程的定义

    • 线程是进程内的一个执行单元,比进程更小。
    • 线程是进程内的一个可调度实体。
    • 线程是程序(或进程)中相对独立的一个控制流序列。
    • 线程本身不能单独运行,只能包含在进程中,只能在进程中执行。

    综上所述,不妨将线程定义为:线程是进程内一个相对独立的、可调度的执行单元。线程自己基本上不拥有资源,只拥有一点在运行时必不可少的资源(如程序计数器、一组寄存器和栈),但它可以与同属一个进程的其他线程共享进程拥有的全部资源。

  • 线程的实现

    在操作系统中有多种方式可实现对线程的支持。最自然的方法是由操作系统内核提供线程的控制机制。在只有进程概念的操作系统中,可由用户程序利用函数库提供线程的控制机制。还有一种做法是同时在操作系统内核和用户程序两个层次上提供线程的控制机制。

    • 内核级线程。指依赖于内核,由操作系统内核完成创建和撤销工作的线程。在支持内核级线程的操作系统中,内核维护进程和线程的上下文信息并完成线程的切换工作。一个内核级线程由于 I/O操作而阻塞时,不会影响其他线程的运行。这时,处理器时间片分配的对象是线程,所以有多个线程的进程将获得更多处理器时间。
    • 用户级线程是指不依赖于操作系统核心,由应用进程利用线程库提供创建、同步、调度和管理线程的函数来控制的线程。由于用户级线程的维护由应用进程完成,不需要操作系统内核了解用户级线程的存在,因此可用于不支持内核级线程的多进程操作系统,甚至是单用户操作系统。用户级线程切换不需要内核特权,用户级线程调度算法可针对应用优化。许多应用软件都有自己的用户级线程。由于用户级线程的调度在应用进程内部进行,通常采用非抢占式和更简单的规则,也无须用户态/核心态切换,因此速度特别快。当然,由于操作系统内核不了解用户线程的存在,当一个线程阻塞时,整个进程都必须等待。这时处理器时间片是分配给进程的,当进程内有多个线程时,每个线程的执行时间都相对减少。
  • 线程锁

    线程锁有:互斥锁、条件锁、自旋锁、读写锁。一般而言,锁的功能越强大,性能就越低。

    • 互斥锁。互斥锁是用于控制多个线程对它们之间共享资源互斥访问的一个信号量。
    • 条件锁。条件锁是一种条件变量。某一个线程因为某个条件满足时可以使用条件锁使该线程处于阻塞状态。一旦条件满足,则以“信号量”的方式唤醒一个因为该条件而被阻塞的线程。
    • 自旋锁。与互斥锁类似,但有所不同。某线程在申请互斥锁而不得时,会转而执行其他任务,在申请自旋锁而不得时,会不停地循环检测
    • 读写锁。已经实现的读者-写者操作模型的锁。

线程的状态与转换

在这里插入图片描述

  • 线程的六种状态
    • 初始(NEW):新创建了一个线程对象,但还没有调用 start()方法。
    • 就绪状态(READY):线程对象创建后,调用该程的 start()方法,该进程就进入了“可运行线程池”中,变得可运行,只需等待获取 CPU 的使用权即可运行。即在就绪状态的线程除 CPU之外,其他运行所需的资源都已全部获得。由于刚被创建的线程在进入就绪状态之前肯定没有处于运行状态,所以它不能自己调用 start()方法而是由其他处于运行状态的线程来调用。
    • 运行状态(RUNNING):就绪状态的线程获得CPU后,开始执行该线程的代码。
    • 阻塞(BLOCKED):线程由于某种原因放弃CPU的使用权,暂时停止运行。
    • 等待(WAITING):当线程用 wait()方法后则进入等待状态(进入等待队列),进入到这个状态会释放所占有的资源,与阻塞不同,这个状态是不能被自动唤醒的,必须依赖其他线程调用notify()方法才能被唤醒
    • 超时等待(TIMED_WAITING):该状态不同于但类似于等待状态,其中的区别只在于是否有时间的限制,即该状态下的线程在等待一定时间后会被唤醒,当然也可以在没到这个时间之前被notify()方法唤醒。
    • 终止(TERMINATED):表示该线程已经执行完毕。

线程与进程的比较

  • 调度。在传统的操作系统中,拥有资源和独立调度的基本单位都是进程。而在引入线程的操作系统中,线程是独立调度的基本单位,进程是拥有资源的基本单位在同一个进程中,线程的切换不会引起进程切换在不同进程中进行线程切换,如从一个进程内的线程
    切换到另一个进程的线程中,将会引起进程切换
  • 拥有资源。不论是传统操作系统还是设有线程的操作系统,进程都是拥有资源的基本单位,而线程不拥有系统资源(也有一点必不可少的资源,并非什么资源都没有),但线程可以访问其隶属进程的系统资源。
  • 并发性。在引入线程的操作系统中,不仅进程之间可以并发执行,而且同一进程内的多个线程之间也可以并发执行。这使得操作系统具有更好的并发性,大大提高了系统的吞吐量。
  • 系统开销。由于创建进程或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/0 设备等,操作系统所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及整个当前进程 CPU 环境的保存及新调度到进程的 CPU 环境的设置:而线程切换时,只需保存和设置少量寄存器内容,因此开销很小。另外,由于同一进程内的多个线程共享进程的地址空间,因此,多线程之间的同步与通信非常容易实现,甚至无须操作系统的干预。

多线程模型

有些系统同时支持用户级线程和内核级线程,因此根据用户级线程和内核级线程连接方式的不同产生了3种不同的多线程模型。

  • 多对一模型。多对一模型将多个用户级线程映射到一个内核级线程上。在采用该模型的系统中,线程在用户空间进行管理,效率相对较高。但是,由于多个用户级线程映射到一个内核级线程,只要一个用户级线程阻塞,就会导致整个进程阻塞。而且由于系统只能识别一个线程(内核级线程),因此即使有多处理器,该进程的若干个用户级线程也只能同时运行一个,不能并行执行
  • 一对一模型。一对一模型将内核级线程与用户级线程一一对应。这样做的好处是当一个线程阻塞时,不影响其他线程的运行,因此一对一模型的并发性比多对一模型要好。而且这样做之后,在多处理器上可以实现多线程并行。这种模型的缺点是创建一个用户级线程时需要创建一个相应的内核级线程。
  • 多对多模型。多对多模型将多个用户级线程映射到多个内核级线程(内核级线程数不多于用户级线程数,内核级线程数根据具体情况确定)。采用这样的模型可以打破前两种模型对用户级线程的限制,不仅可以使多个用户级线程在真正意义上并行执行,而且不会限制用户级线程的数量。用户可以自由创建所需的用户级线程,多个内核级线程根据需要调用用户级线程,当一个用户级线程阻塞时,可以调度执行其他线程。

2.2 处理器调度

2.2.1 处理器的三级调度

调度是操作系统的一个基本功能,几乎所有的资源在使用前都需要调度。由于 CPU 是计算机的首要资源,因此调度设计均围绕如何能够高效利用 CPU 展开。

在多道程序环境下,一个作业从提交到执行,通常都要经历多级调度,如高级调度、中级调度和低级调度。而系统的运行性能在很大程度上都取决于调度,因此调度便成为多道程序的关键。

高级调度(作业调度)

高级调度又称为宏观调度、作业调度或者长程调度,其主要任务是按照一定的原则从外存上处于后备状态的作业中选择一个或者多个,给它们分配内存输入/输出设备等必要资源,并建立相应的进程,以使该作业具有获得竞争处理器的权利(作业是用户在一次运算过程或一次事务处理中要求计算机所做工作的总和)。作业调度的运行频率较低,通常为几分钟一次。

调度程序必须决定操作系统可以接纳多少个作业?

作业调度每次要接纳多少个作业进入内存取决于多道程序的并发程度,即允许有多少个作业同时在内存中运行。当内存中可以同时运行的作业太多时,可能会影响到系统的服务质量,如导致周转时间太长。而当内存中同时运行的作业太少时,又会导致系统资源利用率和吞吐量下降。因此,多道程序的并发程度应根据系统的规模和运行速度来确定。

调度程序必须决定接纳哪些作业?

应将哪些作业从外存调入内存取决于所采取的调度算法。最简单的调度算法是先来先服务调度算法,它将最早进入外存的作业最先调入内存:较常用的一种调度算法是短作业优先调度算法,它将外存上执行时间最短的作业最先调入内存,此外还有其他调度算法。

中级调度

中级调度又称为中程调度或者交换调度,引入中级调度是为了提高内存利用率和系统吞吐量,其主要任务是按照给定的原则和策略,将处于外存对换区中的具备运行条件的进程调入内存,并将其状态修改为就绪状态,挂在就绪队列上等待,或者将处于内存中的暂时不能运行的进程交换到外存对换区,此时的进程状态称为挂起状态。中级调度主要涉及内存管理与扩充(其实中级调度可以理解为在换页时将页面在外与内之间调度)。

低级调度(进程调度)

低级调度又称为微观调度、进程调度或者短程调度,其主要任务是按照某种策略和方法从就绪队列中选取一个进程,将处理器分配给它。进程调度的运行频率很高,一般隔几十毫秒要运行一次

高级调度与低级调度的区别:

  • 作业调度为进程被调用做准备,进程调度使进程被调用。换言之,作业调度的结果是为作业创建进程,而进程调度的结果是进程被执行
  • 作业调度次数少,进程调度频率高。
  • 有的系统可以不设置作业调度,但进程调度必须有。

2.2.2 调度的基本原则

不同调度算法有不同的调度策略,这也决定了调度算法对不同类型的作业影响不同。在选择调度算法时,我们必须考虑不同算法的特性。为了衡量调度算法的性能,人们提出了一些评价标准。

CPU利用率

CPU 是系统最重要、也是最昂贵的资源,其利用率是评价调度算法的重要指标。在批处理以及实时系统中,一般要求 CPU 的利用率要达到比较高的水平,不过对于 PC 和某些不强调利用率的系统来说,CPU 利用率并不是最主要的。

系统吞吐量

系统吞吐量表示单位时间内 CPU 完成作业的数量。对长作业来说,由于它要占用较长的CPU处理时间,因此会导致系统吞吐量下降;而对短作业来说,则相反。

响应时间

相对于系统吞吐量和 CPU 利用率来说,响应时间主要是面向用户的。在交互系统中,尤其在多用户系统中,多个用户同时对系统进行操作,都要求在一定时间内得到响应,不能使某些用户的进程长期得不到调用。因此,从用户角度看,调度策略要保证尽量短的响应时间,使响应时间在用户的接受范围内。

周转时间

从每个作业的角度来看,完成该作业的时间是至关重要的,通常用周转时间或者带权周转时间来衡量。

  • 周转时间:指作业从提交至完成的时间间隔,包括等待时间和执行时间。周转时间Ti;用公式表示为作业i的周转时间Ti=作业i的完成时间-作业i的提交时间

  • 平均周转时间:平均周转时间是指多个作业(如n个作业)周转时间的平均值。平均周转时间T用公式表示为T=(T1+T22+…+Tn) /n

  • 带权周转时间:带权周转时间是指作业周转时间与运行时间的比。作业i的带权周转时间Wi用公式表示Wi=作业i的周转时间/作业i的运行时间

  • 平均带权周转时间:与平均周转时间类似,平均带权周转时间是多个作业的带权周转时间的平均值。

2.2.3 进程调度

在多道程序系统中,用户进程数往往多于处理器数,这将导致用户进程争夺处理器。此外,系统进程同样需要使用处理器。因此,系统需要按照一定的策略动态地把处理器分配给就绪队列中的某个进程,以便使之执行。处理器分配的任务由进程调度程序完成。

进程调度的功能

  • 记录系统中所有进程的有关情况以及状态特征。
  • 选择获得处理器的进程。
  • 处理器的分配。

引起进程调度的原因

  • 当前运行进程运行结束。因任务完成而正常结束,或者因出现错误而异常结束。
  • 当前运行进程因某种原因,如 I/ 请求、P 操作、阻塞原语等,从运行状态进入阻塞状态。
  • 执行完系统调用等系统程序后返回用户进程,这时可以看作系统进程执行完毕,从而可以调度一个新的用户进程。
  • 在采用抢占调度方式的系统中,若有一个具有更高优先级的进程要求使用处理器则使当前运行进程进入就绪队列(这与调度方式有关)。
  • 在分时系统中,分配给该进程的时间片已用完(这与系统类型有关)。

不能进行进程调度的情况

  • 处理中断的过程中。中断处理过程复杂,在实现上很难做到进程切换,且中断处理是系统工作的一部分,逻辑上不属于某一进程,不应被剥夺处理器资源。
  • 在操作系统内核程序临界区中。进程进入临界区后,需要独占式地访问共享数据理论上必须加锁,以防止其他并行程序进入,在解锁前不应切换到其他进程运行,以加快该共享数据的释放。
  • 其他需要完全屏蔽中断的原子操作过程中。如加锁、解锁、中断现场保护、恢复等原子操作。原子操作不可再分,必须一次完成,不能进行进程切换。

进程调度的方式

进程调度方式是指当某一个进程正在处理器上执行时,若有某个更为重要或紧迫的进程需要进行处理(即有优先级更高的进程进入就绪队列),此时应如何分配处理器

  • 抢占式:又称为可剥夺方式。这种调度方式是指当一个进程正在处理器上执行时,若有某个优先级更高的进程进入就绪队列,则立即暂停正在执行的进程,将处理器分配给新进程。
  • 非抢占方式。又称为不可剥夺方式。这种方式是指当某一个进程正在处理器上执行时,即使有某个优先级更高的进程进入就绪队列,仍然让正在执行的进程继续执行,直到该进程完成或因发生某种事件而进入完成或阻塞状态时,才把处理器分配给新进程。

2.2.4 常见的调度算法

先来先服务调度算法(作业调度、进程调度)

先来先服务调度算法(FCFS)是一种最简单的调度算法,可以用于作业调度与进程调度。其==基本思想是按照进程进入就绪队列的先后次序来分配处理器==。先来先服务调度算法采用非抢占的调度方式,即一旦一个进程(或作业)占有处理器,它就一直运行下去,直到该进程(或作业)完成其工作或因等待某一事件而不能继续执行时才释放处理器。

从表面上看,先来先服务调度算法对于所有进程(或作业)是公平的,即按照它们到来的先后次序进行服务。但假设有等数量的长进程(10t) 和短进程(t),因为数量相等,所以谁先到的概率也相等。当长进程先来时,短进程的等待时间为 10t,而当短进程先来时,长进程的等待时间仅为t。所以先来先服务调度算法有利于长进程(作业),不利于短进程(作业)

现在,先来先服务调度算法已经很少作为主要的调度策略,尤其是不能作为分时系统和实时系统的主要调度策略,但它常被结合在其他调度策略中使用。例如,在使用优先级作为调度策略的系统中,往往对多个具有相同优先级的进程或作业按照先来先服务原则进行处理

短作业优先调度算法(作业调度、进程调度)

短作业优先(SJF)调度算法用于进程调度时被称为短进程优先调度算法,该算法既可以用于作业调度,也可以用于进程调度

短作业(或进程)优先调度算法的基本思想就是把处理器分配给最快完成的作业(或进程)。在作业调度中,短作业优先调度算法每次从后备作业队列中选择估计运行时间最短的一个或几个作业调入内存,分配资源,创建进程并放入就绪队列。在进程调度中,短进程优先调度算法每次从就绪队列中选择估计运行时间最短的进程将处理器分配给它,使该进程运行并直到完成或因某种原因阻塞才释放处理器。

在所有作业同时到达时,SJF 调度算法是最佳算法,平均周转时间最短(如果短进程先执行,长进程等待时间较长进程先执行的情况要短很多,因此平均等待时间最短,而进程运行时间是确定不变的)。但该算法很显然对长作业不利,当有很多短作业不断进入就绪队列时,长作业会因长期得不到调度而产生“饥饿”现象(“饥饿”现象是指在一段时间内进程得不到调度执行或得不到所需资源)。

优先调度算法(作业调度、进程调度)

优先级调度算法是一种常用的进程调度算法,既可用于作业调度,也可用于进程调度。其基本思想是把处理器分配给优先级最高的进程。该算法的核心问题是如何确定进程的优先级。进程的优先级用于表示进程的重要性,即运行的优先性。

进程的优先级通常分为两种静态优先级和动态优先级。

  • 静态优先级是在创建进程时确定的,确定之后在整个进程运行期间不再改变。确定静态优先级的依据有以下几种:
    • 按进程类确定。系统进程优先级 > 用户进程优先级
    • 按作业的资源要求确定。申请资源多的进程 > 申请资源少的进程
    • 按用户类型和要求确定。用户的收费标准越高,则该用户作业对应进程的优先级也越高。
  • 动态优先级是指在创建进程时,根据进程的特点及相关情况确定一个优先级,在进程运行过程中再根据情况的变化调整优先级。确定动态优先级的依据有以下几种:
    • 根据进程占有 CPU 时间的长短来决定。一个进程占用 CPU 的时间越长,则优先级越低,再次获得调度的可能性就越小;反之,一个进程占用 CPU 的时间越短,则优先级越高
      再次获得调度的可能性就越大。
    • 据就进程等待CPU间的来定。一个就进程在就队列中等待的时间越长,则优先级越高,获得调度的可能性就越大;反之,一个就绪进程在就绪队列中等待的时间越短,则优先级越低,获得调度的可能性就越小。

基于优先级的调度算法还可以按调度方式的不同分为非抢占优先级调度算法和抢占优先级调度算法。

  • 非抢占优级调度算法的实现思想是系统一旦将处理器分配给就绪队列中优先级最高的进程,该进程便会一直运行下去,直到由于其自身原因(任务完成或申设备等)主动让出处理器时,才将处理器分配给另一个当前优先级最高的进程。
  • 抢占优先级调度算法的实现思想是将处理分配给优先级最高的进程,并使之运行。在进程运行过程中,一旦出现了另一个优先级更高的进程(如一个更高优先级进程因等待的事件发生而变为就绪状态),进程调度程序就停止当前的进程,而将处理器分配给新出现的优先级更高的进程。

时间片轮转调度算法(进程调度)

进程调度通常采用时间片轮转调度算法。在时间片轮转调度算法中,系统将所有的就绪进程按到达时间的先后次序排成一个队列,进程调度程序总是选择队列中的第一个进程执行,并规定执行一定时间,称为时间片(如100ms)。当该进程用完这一时间片时(即使进程并未执行结束),系统将它送至就绪队列队尾,再把处理器分配给下一个就绪进程。这样,处于就绪队列中的进程就可以依次轮流获得一个时间片的处理时间,然后重新回到队列尾部排队等待执行,如此不断循环,直至完成

如果时间片设置得太大,所有进程都能在一个时间片内执行完毕,那么时间片轮转调度算法就退化为先来先服务调度算法;如果时间片设置得太小,那么处理器将在进程之间频繁切换,处理器真正用于运行用户进程的时间将减少。因此,时间片的大小应设置适当。

时间片的大小通畅由以下因素确定:

  • 系统的响应时间。分时系统必须满足系统对响应时间的要求,系统响应时间与时间片的关系可以表示为

    T=Nxq
    其中,T 为系统的响应时间, 为时间片的大小,N 为就绪队列中的进程数。根据这个关系可以得知,若系统中的进程数一定,时间片的大小与系统响应时间成正比

  • 就绪队列中的进程数目。在响应时间固定的情况下,就绪队列中的进程数与时间片的大小成反比。

  • 系统的处理能力。通常要求用户键入的常用命令能够在一个时间片内处理完毕。因此,计算机的速度越快,单位时间内可处理的命令就越多,时间片就可以越小。

高响应比优先调度算法(作业调度)

高响应比优先调度算法综合了先来先服务与短作业优先两种调度算法的特点,即考虑了作业的等待时间和作业的运行时间两个因素,弥补了之前两种调度算法只考虑其中一个因素的不足。

高响应比优先调度算法主要用于作业调度。其基本思想是每次进行作业调度时,先计算就绪队列中的每个作业的响应比,挑选响应比最高的作业投入运行。

响应比=作业响应时间/估计运行时间

响应比=(作业等待时间+估计运行时间)/估计运行时间

该算法有利于短作业(作业等待时间相同时,估计运行时间越短,响应比越高),同时考虑长作业(只要作业等待时间足够长,响应比就会变为最高。该算法对于短作业和长作业都有考虑,但由于要计算每个后备作业的响应比,因此增加了系统开销

多级队列调度算法(进程调度)

多级队列调度算法的基本思想是根据进程的性质或类型,将就绪队列划分为若干个独立的队列,每个进程固定地分属于一个队列。每个队列采用一种调度算法,不同的队列可以采用不同的调度算法。例如,为交互型任务设置一个就绪队列,该队列采用时间片轮转调度算法:再如,为批处理任务另外设置一个就绪队列,该队列采用先来先服务调度算法。

多级反馈队列调度算法(进程调度)

多级反馈队列调度算法是时间片轮转调度算法和优先级调度算法的综合与发展。通过动态调整进程优先级和时间片的大小,多级反馈队列调度算法可兼顾多方面的系统目标。

在这里插入图片描述

首先,应设置多个就绪队列,并为每个队列赋予不同的优先级。第一个队列的优先级最高,第二个队列的优先级次之,其余队列的优先级逐次降低

其次,每个队列中的进程执行时间片的大小也各不相同,进程所在队列的优先级越高其相应的时间片就越短。通常,第 i+1 队列的时间片是第i队列时间片的两倍

当一个新进程进入系统时,应先将其放入第一个队列末尾,按先来先服务的原则排队等待调度。当轮到该进程执行时,如能在此时间片内完成,便可准备撤离系统:如果该进程在一个时间片结束时尚未完成,调度程序便将该进程转入第二个队列的末尾,再同样按照先来先服务的原则等待调度执行:如果该进程在第二个队列中运行一个时间片后仍未完成再以同样方法转入第三个队列。如此下去,最后一个队列中使用时间片轮转调度算法。

最后,仅当第一个队列空闲时,调度程序才调度第二个队列中的进程运行;仅当第一个至第-1个队列均为空时,才会调度第个队列中的进程运行。当处理器正在为第i个队列中的某进程服务时,若又有新进程进入优先级较高的队列中,则此时新进程将抢占正在运行进程的处理器,即由调度程序把正在执行的进程放回第 个队列末尾,并重新将处理器分配给新进程。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

2.3 同步与互斥

2.3.1 进程同步的基本概念

两种形式的制约关系

  • 间接相互制约关系(互斥):若某一进程要求使用某种资源,而该资源正被另一进程使用,并且该资源不允许两个进程同时使用,那么该进程只好等待已占用资源的进程释放资源后再使用。这种制约关系的基本形式是“进程一资源一进程”

    这种制约关系源于多个同种进程需要互斥地共享某种系统资源(如打印机),互斥是设置在同种进程之间以达到互斥地访问资源的目的(如在生产者-消费者问题中,生产者与生产者之间需要互斥地访问缓冲池)

  • 直接相制约关系(同步)

    某一进程若收不到另一进程给它提供的必要信息就不能继续运行下去,这种情况表明了两个进程之间在某些点上要交换信息,相互交流运行情况。这种制约关系的基本形式是“进程-进程”

    这种制约主要源于进程间的合作,同步设置在不同进程之间以达到多种进程间的同步(如在生产者-消费者问题中,生产者可以生产产品并放入缓冲池,消费者从缓冲池取走产品进行消费,若生产者没有生产产品,则消费者无法进行消费)

只要是同类进程即为互斥关系,不同类进程即为同步关系,例如,消费者与消费者就是互斥关系,消费者和生产者就是同步关系。

临界资源与临界区

进程在运行过程中,一般会与其他进程共享资源,而有些资源的使用具有排他性。把同时仅允许一个进程使用的资源称为临界资源。许多物理设备都属于临界资源,如打印机、绘图机等。

临界资源的访问可以分为4部分:

在这里插入图片描述

  • 进入区。为了进入临界区使用临界资源,在进入区要检查是否可以进入临界区;如果可以进入临界区,通常设置相应的“正在访问临界区”标志,以止其他进程同时进入临界区
  • 临界区。进程中用于访问临界资源的代码,又称临界段
  • 退出区。临界区后用于将“正在访问临界区”志清除的部分
  • 剩余区。进程中除上述 3 部分以外的其他部分。

简单来说,临界资源是一种系统资源,需要不同进程互斥访问,而临界区则是每个进程中访问临界资源的一段代码,是属于对应进程的,临界区的前后需要设置进入区和退出区以进行检查和恢复。临界区和临界资源是不同的,临界资源是必须互斥访问的资源,这种资源同时只能被一个进程所使用,但需要这种资源的进程不止一个,因此需要对使用临界资源的进程进行管理,这也就产生了临界区的概念。

每个进程的临界区代码可以不相同。进程对临界资源进怎样的操作,这和临界资源及互斥同步管理是无关的。

互斥的概念与要求

根据互斥的定义,当一个进程进入临界区使用临界资源时,另一个进程必须等待,直到占用该临界资源的进程退出临界区后,才允许新的进程访问该临界资源。

为了禁止两个进程同时进入临界区,软件算法或同步机构都应遵循以下准则:

  • 空闲让进。当没有进程处于临界区时,可以允许一个请求进入临界区的进程立即进入自己的临界区。
  • 忙则等待。当已有进程进入其临界区时,其他试图进入临界区的进程必须等待。
  • 有限等待。对要求访问临界资源的进程,应保证能在有限的时间内进入自己的临界区。
  • 让权等待。当一个进程因为某些原因不能进入自己的临界区时,应释放处理器给其他进程。

同步的概念与实现机制

一般来说,一个进程相对另一个进程的运行速度是不确定的。也就是说,进程之间是在异步环境下运行的。但是相互合作的进程需要在某些关键点上协调它们的工作。所谓进程同步,是指多个相互合作的进程在一些关键点上可能需要互相等待或互相交换信息,这种相互制约关系称为进程同步可以用信号量实现同步

2.3.2 互斥实现方法

互斥既可以用软件方法来实现,也可以用硬件方法来实现。

软件方法

算法1:设置一个公用整型变量 turn,用来表示允许进入临界区的进程标识。若 turn 为0则允许进程P0进入临界区;否则循环检查该变量,直到turn 变为本进程标识;在退出区,修改允许进入进程的标识 turn 为1。进程P1的算法与此类似。两个进程的程序结构如下:

在这里插入图片描述

此方法可以保证互斥访问临界资源,但存在的问题是强制两个进程以交替次序进入临界区,很容易造成资源利用不充分

例如,当进程P0退出临界区后将 turn 置为1,以便允许进程P1进入临界区,但如果进程P1 暂时并未要求访问该临界资源,而P0又想再次访问临界资源,则它将无法进入临界区。可见,此算法不能保证实现“空闲让进”准则

算法2:设置标志数组flag表示进程是否在临界区中执行,初值均为假。在每个进程访问该临界资源之前,先检查另一个进程是否在临界区中,若不在,则修改本进程的临界区标志为真并进入临界区,在退出区修改本进程临界区标志为假。两进程的程序结构如下:

在这里插入图片描述

此算法解决了“空闲让进”的问题,但又出现了新问题,即当两个进程都未进入临界区时,它们各自的访问标志都为 false,若此时刚好两个进程同时都想进入临界区,并且都发现对方的标志值为 false(当两进程交替执行了检查语句后,都满足 flag[]=false 的条件),于是两个进程同时进入了各自的临界区,这就违背了临界区的访问规则“忙则等待”

算法3:本算法仍然设置标志数组 flag,但标志用来表示进程是否希望进入临界区,每个进程在访问临界资源之前,先将自己的标志设置为真,表示希望进入临界区,然后检查另一个进程的标志。若另一个进程的标志为真,则进程等待;反之,则进入临界区。两进程的程序结构如下:

在这里插入图片描述

此算法可以有效防止两进程同时进入临界区,但存在两个进程都进不了临界区的问题,即当两个进程同时想进入临界区时,它们分别将自己的标志位设置为 true,并且同时去检查对方的状态,发现对方也要进入临界区,于是都阻塞自己,结果导致两者都无法进入临界区,造成“死等”现象,这就违背了“有限等待”准则

算法 4:本算法的思想是算法 3 和算法1的结合。标志数组flag[]表示进程是否希望进入临界区或是否在临界区中执行。此外,还设置了一个 turn 变量,用于表示允许进入临界区的进程标识。两进程的程序结构如下:

在这里插入图片描述

在这里插入图片描述

至此,算法4可以完全正常工作,利用 flag[]解决临界资源的互斥访问,而利用 tumn 解决“饥饿”现象

硬件方法

完全利用软件方法实现进程互斥有很大的局限性,现在已经很少单独采用软件方法。硬件方法的主要思想是用一条指令完成标志的检查和修改这两个操作,因而保证了检查操作与修改操作不被打断;或通过中断屏蔽的方式来保证检查和修改作为一个整体执行

硬件方法主要有两种:一种是中断屏蔽;另一种是硬件指令

硬件方法的优点

  • 适用范围广。硬件方法适用于任何数目的进程,在单处理器和多处理器环境中完全相同。
  • 简单。硬件方法的标志设置简单,含义明确,容易验证其正确性。
  • 支持多个临界区。当一个进程内有多个临界区时,只需为每个临界区设立一个布尔变量。

硬件方法有诸多优点,但也有一些自身无法克服的缺点。这些缺点主要包括进程在等待进入临界区时要耗费处理器时间,不能实现“让权等待”(需要软件配合进行判断);进入临界区的进程的选择算法用硬件实现有一些缺陷,可能会使一些进程一直选不上,从而导致“饥饿”现象

2.3.3 信号量

虽然前面讲解的软件及硬件方法都可以解决互斥问题,但它们都存在缺点。软件方法的算法太复杂,效率不高、不直观,而且存在“忙等”现象(在进入区时会持续检测标志变量)。就硬件方法而言,对于用户进程,中断屏蔽方法不是一种合适的互斥机制;硬件指令方法有不能实现“让权等待”等缺点。

信号量及同步原语

信号量是一个确定的二元组 (s,q),其中 s 是一个具有非负初值的整型变量,q 是一个初始状态为空的队列。整型变量 s 表示系统中某类资源的数目,当其值大于0时,表示系统中当前可用资源的数目:当其值小于0 时,其绝对值表示系统中因请求该类资源而被阻塞的进程数目。除信号量的初值外,信号量的值仅能由P操作(又称为 wait 操作)和V操作(又称为signal操作)改变。操作系统利用它的状态对进程和资源进行管理。

一个信号量的建立必须经过说明,即应该准确说明 s 的意义和初值(注:这个初值不是一个负值)。每个信号量都有相应的一个队列,在建立信号量时队列为空

设s 为一个信号量,

P(s)执行时主要完成以下动作:先执行 s=s-1;若s>=0,则该进程继续运行;若 s<0,则阻塞该进程,并将它插入该信号量的等待队列中

V(s)执行时主要完成下述动作:先执行 s-s+1;若 s > 0,则该进程继续执行:若s<=0,则从该信号量等待队列中移出第一个进程,使其变为就绪状态并插入就绪队列,然后再返回原进程继续执行

在这里插入图片描述

在这里插入图片描述

P、V 操作均为不可分的原子操作,这保证了对信号量进行操作的过程不会被打断或阻塞P 操作相当于申请资源,V 操作相当于释放资源P 操作和V 操作在系统中一定是成对出现的,但未必在一个进程中,可以分布在不同进程中

信号量的分类

  • 整型信号量:整型信号量是一个整型量 s,除初始化外,仅能通过标准的原子操作 P 和V 来访问。整型信号量引入了 P、V 操作,但是在进行 P 操作时,若无可用资源,则进程持续对该信号量进行测试,存在“忙等”现象,未遵循“让权等待”准则

  • 记录型信号量(资源信号量):为了解决整型信号量存在的“忙等”问题,添加了链表结构,用于链接所有等待该资源的进程,记录型信号量正是因采用了记录型的数据结构而得名。

    当进程对信号量进行P操作时,若此时无剩余资源可用,则进程自我阻塞,放弃处理器并插入到等待链表中。可见,该机制遵循“让权等待”准则。当进程对信号量进行V操作时若链表中仍有等待该资源的进程,则唤醒链表中的第一个等待进程。

    如果信号量初值为1,表示该资源为同时只允许一个进程访问的临界资源

信号量的应用

  • 实现进程的同步

    假设存在并发进程 P1和P2。P1中有一条语 S1,P2中有一条语 S2,要S1必须S2之前执行。这种同步问题使用信号量就能很好解决。

在这里插入图片描述

在这里插入图片描述

  • 实现进程互斥

    假设有进程 P1和 P2,两者有各自的临界区,但系统要求同时只能有一个进程进入自己的临界区。这里使用信号量可以很方便地解决临界区的互斥进入。设置信号量N,初值为1(即可用资源数为1),只需要将临界区放在 P(N)和 V(N)之间即可实现两进程的斥进入。

在这里插入图片描述

若有两个或者多个进程需要互斥访问某资源,可以设置一个初值为 1的信号量,在这些进程的访问资源的代码前后分别对该信号量进行 P 操作和 V 操作,即可保证进程对该资源的互斥访问

2.3.4 经典同步问题

生产者-消费者问题

生产者-消费者问题是著名的进程同步问题。它描述的是一组生产者向一组消费者提供产品,他们共享一个有界缓冲区,生产者向其中投入产品,消费者从中取走产品。这个问题是许多相互合作进程的一种抽象。例如,在输入时,输入进程是生产者,计算进程是消费者:在输出时,计算进程是生产者,打印进程是消费者。

为解决这一问题,应当设置两个同步信号量:一个说明空缓冲区数目,用empty 表示,初值为有界缓冲区大小n;另一个说明满缓冲区数目(即产品数目),用full表示,初值为0。此外,还需要设置一个互斥信号量 mutex,初值为1,以保证多个生产者或者多个消费者互斥地访问缓冲池

在这里插入图片描述

P(full)/P(empty)与P(mutex)的顺序不可颠倒,必须先对资源信号量进行P操作再对互斥信号量进行P操作,否则会导致死锁。

在有多个信号量同时存在的情况下,P操作往往是不能颠倒顺序的,必须先对资源信号量进行 P操作,再对互斥信号量进行 P 操作,这样可以在占有信号量访问权时保证有资源可以使用,否则会产生占用使用权而无资源可用的“死等”现象

只要有多个同类进程,就一定需要互斥信号量。

读者-写者问题

在读者-写者问题中,有一个许多进程共享的数据区,这个数据区可以是一文件或者主存的一块空间,有一些只读取这个数据区的进程(读者)和一些只往数据区写数据的进程(写者)。此外还需要满足以下条件:

  • 任意多个读者可以同时读这个文件。
  • 一次只能有一个写者可以往文件中写(写者必须互斥)。
  • 如果一个写者正在进行操作,禁止任何读进程读文件和其他任何写进程写文件。
  • 读者优先算法

    一个读者试图进行读操作时,如果这时正有其他读者在进行读操作,他可以直接开始读操作,而不需要等待。由于只要有读者在进行读操作,写者就不能够写,但后续读者可以直接进行读操作,因此只要读者陆续到来,读者一到就能够开始读操作,而写者进程只能等待所有读者都退出才能够进行写操作,这就是读者优先。

    要解决此问题,需要设置如下几个信号量:设置记录读者数量的整型变量 readcount,初值为0,当其值大于0时,表明有读者存在,写者不能进行写操作:设置互斥信号量rmutex初值为1,用于保证多个读者进程对于 readcount 的互斥访问:设置互斥信号量 mutex,初值为1,用于控制写者进程对于数据区的互斥访问。算法如下:

在这里插入图片描述

在这里插入图片描述

  • 公平情况算法

    进程的执行顺序完全按照到达顺序,即一个读者试图进行读操作时,如果有写者正等待进行写操作或正在进行写操作,后续读者要等待先到达的写者完成写操作后才能开始读操作。

    要解决此问题,与读者优先算法相比,需要增设一个信号量 wmutex,其初值为1,用于表示是否存在正在写或者等待的写者,若存在,则禁止新读者进入。算法如下:

在这里插入图片描述

在这里插入图片描述

  • 写者优先算法

    有的书把公平情况算法也叫作写者优先,但并不是真正意义上的写者优先,只是按照到达顺序进行读写操作而已。若要实现真正的写者优先(即当写者和读者同时等待时,后续写者到达时可以插队到等待的读者之前,只要等待队列中有写者,不管何时到达,都优先于读者被唤醒),则需要增设额外的信号量进行控制。

    为了达到这一目的,需要增设额外的一个信号量 readable,用于控制写者到达时可以优先于读者进入临界区,当有写者到达时,只需要等待前面的写者写完就可以直接进入临界区,而不论读者是在该写者之前还是之后到达。另外,需要增设一个整数 writeount 用于统计写者的数量。与之前的算法相比,wmutex 的作用有所变化,现在是用于控制写者互斥访问writecount。算法如下:

在这里插入图片描述
在这里插入图片描述

本方法增设了 readable 信号量,用于实现写者插队的目的。当第一个写者到达时,申请占用readable信号量,占用成功之后就一直占用,后续到达的读者进程会因申请不到readable信号量而阻塞,而后续写者到达时,由于不需要申请 readable 信号量,因此排在了这个写者的后面,从而达到插队的目的。直到所有写者都已经写完,最后一个写者释放了 readable 信号量之后,读者才能够继续执行读操作。当有新的写者到达时,继续占用readable 信号量,阻止后续的读者进行读操作,重复进行此过程此算法真正实现了写者优先,新写者也可以优先于先到的等待读者占用数据区进行操作

哲学家进餐问题

5个哲学家围绕一张圆桌而坐,桌子上放着 5 根子,每两个哲学家之间放一根:哲学家的动作包括思考和进餐,进餐时需要同时拿起他左边和右边的两根筷子,思考时则同时将两根筷子放回原处。哲学家进餐问题可以看作并发进程执行时处理临界资源的一个典型问题。

筷子是临界资源,不能同时被两个哲学家一起用,因此使用一个信号量数组来表示筷子。

在这里插入图片描述

这种解法存在问题,会导致死锁(假如 5 个哲学家同时饥饿而各自拿左边的筷子时,会导致 5根筷子均被占用,当他们试图拿右边的筷子时,都将因没有筷子而“无限等待”)

对于这种死锁问题,可以采用如下几种解决方法:

  • 最多只允许 4 个哲学家同时进餐。
  • 仅当一个哲学家左右两边的筷子同时可用时,他才可以拿起筷子。
  • 将哲学家编号,要求奇数号的哲学家先拿左边筷子,偶数号的哲学家先拿右边筷子。

现给出最后一种方法的解法:规定奇数号的哲学家先拿左边筷子,然后拿右边筷子:偶数号的哲学家则相反。算法如下:

在这里插入图片描述

在这里插入图片描述

理发师问题

理发店有一位理发师、一把理发椅和若干供顾客等候用的凳子(这里假设有 n个凳子)。若没有顾客,则理发师在理发椅上睡觉,当一个顾客到来时,他必须先叫醒理发师。若理发师正在给顾客理发,如果有空凳子,该顾客等待;如果没有空凳子,顾客就离开。要为理发师和顾客各设计一段程序来描述其活动。

对本题有两种思路:一种是将理发椅与等待用的凳子分别看作两种不同的资源;另一种是将理发椅和凳子看成统一的一种椅子资源。第一种思路所用代码写起来有些复杂,但是容易想到;第二种代码量较少,但不容易想明白。

在这里插入图片描述

  • 在这里插入图片描述

    在这里插入图片描述

  • 在这里插入图片描述

信号量机制问题的解题步骤分析

  • 关系分析:首先应该确定问题中存在哪些同步关系。只要存在一对同步关系往往就需要一种资源信号量,资源信号量的初值应设置为题目中的对应资源数量。题目中的每一句话一般都暗示一种同步关系或暗示一类资源,同步关系不仅可能存在于两种角色之间(如生产者和消费者),也可能存在于同一角色之间(如生产者与生产者,这里仅做可能性举例,实际上生产者之间无同步关系)。

  • 确定临界资源:访问临界资源的代码称为临界区,由于临界资源每次只允许一个进程访问,因此访问临界资源时需要用到互斥信号量,互斥信号量的初值为 1,一般使用mutex作为互斥信号量的名称。临界区的通用写法为:

    在这里插入图片描述

    需要注意的是,如果访问临界资源时,还有其他同步关系或互斥关系的限制(加锁),与此限制有关的资源信号量的 P、V 操作一般写在上述通用临界区代码之外。假设其资源信号量为 N,则临界区的变形写法为:

    在这里插入图片描述

  • 整理思路:确定问题中不同角色进程的具体代码及其所用到的信号量,完成信号量机制问题的解答。在进行作答时,可以将 P 操作(wait 操作)看作是将此类资源数量减1,将V操作(signal操作)看作是将此类资源数量加1

解决同步互斥问题时,到底应不应该添加循环语句来表达并发?

是否要添加循环语句要根据实际进程类型来判断。例如,在生产者-消费者问题中,由于生产者和消费者是在不断生产和消费的,因此生产和消费的代码就要循环执行,这时就要在这种代码中加入循环语句(通常用 while 语)来保证代码的不断执行。如果题目要求在某些条件下停止执行,只要在循环内的合适位置加入 break 语句就可以了。有些题目的进程是不需要循环执行的,如理发师问题中的顾客进程,一个顾客通常是理发结束之后就离开,即顾客进程只要执行完一次之后就结束,类似这种进程的代码就只需执行一次,因此不需要添加循环语句(顾客不可能一直要理发,这不符合常理)。

2.3.5 管程

用信号量机制可以实现进程间的同步和互斥,但由于信号量的控制分布在整个程序中,其正确性分析很困难,使用不当还可能导致进程死锁。针对信号量机制中存在的这些问题,Diikstra 于1971 年提出为每个共享资源设立一个“秘书”来管理对它的访问一切来访者都要通过“秘书”,而“秘书”每次仅允许一个来访者(进程)访问共享资源。这样既便于系统管理共享资源,又能保证互斥访问和进程间的同步。1973 年,Hanson 和 Hoare 又把“秘书概念发展为管程概念。

管程定义了一个数据结构和能为并发进程所执行的一组操作,这组操作能同步进程和改变管程中的数据。由管程的定义可知,管程由局部于管程的共享数据结构说明、操作这些数据结构的一组过程以及对局部于管程的数据结构设置初值的语句组成。管程把分散在各个进程中互斥访问公共变量的临界区集中起来,提供对它们的保护

管程的基本特征:

  • 局部于管程的数据只能被局部于管程内的过程所访问。
  • 一个进程只有通过调用管程内的过程才能进入管程访问共享数据。
  • 每次仅允许一个进程在管程内执行某个内部过程,即进程互斥地通过调用内部过程进入管程。其他想进入管程的过程必须等待,并阻塞在等待队列。

进程定义中包含以下支持同步的设施:

  • 局限于管程并仅能从管程内进行访问的若干条件变量,用于区别各种不同的等待原因。
  • 在条件变量上进行操作的两个函数过程 wait 和signal。wait 将调用此函数的进程阻塞在与该条件变量相关的队列中,并使管程可用,即允许其他进程进入管程。signal 唤醒在该条件变量上阻塞的进程,若有多个这样的进程,则选择其中的一个进程唤醒;若该条件变量上没有阻塞进程,则什么也不做。管程的 signal过程必须在 wait 过程调用之后调用

2.4 死锁

2.4.1 死锁的概念

在多道程序系统中,虽然多个进程的并发执行改善了系统资源的利用率,并提高了系统的处理能力,然而,多个进程的并发执行也带来了新的问题一一死锁。

当多个进程因竞争系统资源或相互通信而处于永久阻塞状态时,若无外力作用,这些进程都将无法向前推进。这些进程中的每一个进程,均无限期地等待此组进程中某个其他进程占有的、自己永远无法得到的资源,这种现象称为死锁

  • 参与死锁的进程至少有两个。
  • 每个参与死锁的进程均等待资源。
  • 参与死锁的进程中至少有两个进程占有资源。
  • 死锁进程是系统中当前进程集合的一个子集。

2.4.2 死锁产生的原因和必要条件

资源分类

操作系统是一个资源管理程序,它负责分配不同类型的资源给进程使用。现代操作系统所管理的资源类型十分丰富,并且可以从不同的角度出发对其进行分类,例如,可以把资源分为可剥夺资源和不可剥夺资源。

  • 可剥夺资源是指虽然资源占有者进程需要使用该资源,但另一个进程可以强行把该资源从占有者进程处剥夺来归自己使用
  • 不可剥夺资源是指除占有者进程不再需要使用该资源而主动释放资源,其他进程不得在占有者进程使用资源过程中强行剥夺

一个资源是否属于可剥夺资源,完全取决于资源本身的性质

死锁产生的原因

死锁产生的原因是资源竞争。若系统中只有一个进程在运行,所有资源为这个进程独享,则不会出现死锁现象。当系统中有多个进程并发执行时,若系统中的资源不足以同时满足所有进程的需要,则会引起进程对资源的竞争,从而可能导致死锁的产生。

虽然资源竞争可能导致死锁,但是资源竞争并不等于死锁,只有在进程运行过程中请求和释放资源的顺序不当时(即进程的推进顺序不当时),才会导致死锁

死锁产生的原因是系统资源不足和进程推进顺序不当

系统资源不足是产生死锁的根本原因,设计操作系统的目的就是使并发进程共享系统资源。而进程推进顺序不当是产生死锁的重要原因,当系统资源刚好够进程使用时,进程的推进顺序不当就很容易导致进程彼此占有对方需要的资源,从而导致死锁。

死锁产生的必要条件

  • 互斥条件。进程要求对所分配的资源进行排他性控制,即在一段时间内某种资源仅为一个进程所占有。
  • 不剥夺条件。进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放。
  • 请求与保持条件。进程每次申请它所的一部分资源,在等待分配新资源的同时进程继续占有已经分配到的资源。请求与保持条件也称为部分分配条件。
  • 环路等待条件。存在一种进程资源的循环等待链,而链中的每一个进程已经获得的资源同时被链中的下一个进程所请求。

要产生死锁,这 4 个条件缺一不可,因此可以通过破坏其中的一个或几个条件来避免死锁的产生

2.4.3 处理死锁的基本方法

  • 鸵鸟算法:像鸵鸟一样对死锁视而不见,即不理睬死锁。
  • 预防死锁。通过设置某些限制条件,从而破坏产生死锁的 4 个必要条件中的一个或几个来预防死锁的产生。
  • 避免死锁。在资源的动态分配过程中,用某种方法防止系统进入不安全状态,从而避免死锁的产生。
  • 检测及解除死锁。通过系统的检测机构及时检测出死锁的发生,然后采取某种措施解除死锁。

预防死锁是在调度方式上破坏死锁产生的必要条件,使系统无法产生死锁,如采用可剥夺式的进程调度方法,优先级高的进程总能得到资源并完成运行,因此系统不会产生死锁。

避免死锁是在动态分配过程中,预知系统是否会进入不安全状态,若该资源分配有可能产生死锁,则不进行这种分配,后面要讲到的银行家算法就是一种避免死锁的方法。

检测及解除死锁是一种比较被动的方法,是在检测到死锁已经发生之后进行处理,如采用剥夺死锁进程的资源等方法强制进程释放资源或结束死锁进程来解除死锁状态。

2.4.4 死锁的预防

要想防止死锁的发生,只需破坏死锁产生的 4 个必要条件之一即可。

互斥条件

为了破坏互斥条件,就要允许多个进程同时访问资源。但是这会受到资源本身固有特性的限制,有些资源根本不能同时访问,只能互斥访问,如打印机就不允许多个进程在其运行期间交替打印数据,只能互斥使用。由此看来,通过破坏互斥条件来防止死锁的发生是不大可能的

不剥夺条件

为了破坏不剥夺条件,可以制定这样的策略:对于一个已经获得了某些资源的进程,若新的资源请求不能立即得到满足,则它必须释放所有已经获得的资源,以后需要资源时再重新申请。这就意味着一个进程已获得的资源在运行过程中可以被剥夺,从而破坏了不剥夺条件。该策略实现起来比较复杂,释放已获得资源可能造成前一段工作的失效,重复申请和释放资源会增加系统的开销,降低系统吞吐量。这种方法通常不会用于剥夺资源之后代价较大的场合,如不会用于对打印机的分配,在一个进程正在打印时,不会采用剥夺的方法来解除死锁

请求与保持条件

为了破坏请求与保持条件,可以采用预先静态分配方法预先静态分配法要求进程在其运行之前一次性申请所需要的全部资源,在它的资源未满足前,不投入运行。一旦投入运行后,这些资源就一直归它所有,也不再提出其他资源请求,这样就可以保证系统不会发生死锁。这种方法既简单又安全,但降低了资源利用率,因为采用这种方法必须事先知道该作业(或进程)所需要的全部资源,即使有的资源只能在运行后期使用,甚至有的资源在正常运行中根本不用,也不得不预先统一申请,结果导致系统资源不能被充分利用。

以打印机为例,一个作业可能只在最后完成时才需要打印计算结果,但在作业运行前就需要把打印机分配给它,那么在该作业的整个执行过程中,打印机基本处于闲置状态,而其他等待打印机的进程迟迟不能开始运行,进而导致其他进程产生“饥饿”现象。

环路等待条件

为了破坏环路等待条件,可以采用有序资源分配法有序资源分配法是将系统中的所有资源都按类型赋予一个编号(如打印机为 1,磁带机为 2),要求每一个进程均严格按照编号递增的次序请求资源,同类资源一次申请完。也就是说,只要进程提出请求资源 Ri,则在以后的请求中只能请求排在 Ri后面的资源(i 为资源编号),不能再请求编号排在 Ri前面的资源对资源请求做了这种限制后,系统中不会再出现几个进程对资源的请求形成环路的情况。

这种方法由于对各种资源编号后不宜修改,从而限制了新设备的增加;不同作业对资源使用的顺序也不会完全相同,即便系统对资源编号考虑到多数情况,但总会有与系统编号不符的作业,从而造成资源浪费;对资源按序使用也会增加程序编写的复杂性

2.4.5 死锁的避免

预防死锁的方法中所采用的几种策略,总的来说都施加了较强的限制条件,虽然实现起来较为简单,却严重损害了系统性能。在避免死锁的办法中,所施加的限制条件较弱,有可能获得较好的系统性能。在该方法中把系统的状态分为安全状态和不安全状态,只要能使系统始终处于安全状态,便可以避免死锁的发生

安全状态与不安全状态

在避免死锁的方法中,允许进程动态地申请资源,系统在进行资源分配之前,先计算资源分配的安全性。若此次分配不会导致系统进入不安全状态,便将资源分配给进程,否则进程必须等待

若在某一时刻,系统能按某种顺序为每个进程分配其所需的资源,直至最大需求,使每个进程都可顺利完成,则称此时的系统状态为安全状态,称该序列为安全序列。若某一时刻系统中不存在这样的一个安全序列,则称此时的系统状态为不安全状态。需要注意的是,安全序列在某一时刻可能并不唯一,即可以同时存在多种安全序列

虽然并非所有不安全状态都是死锁状态,但当系统进入不安全状态后,便可能进入死锁状态;反之,只要系统处于安全状态,便可避免进入死锁状态

需要注意的两点:

  • 不安全状态不是指系统中已经产生死锁。不安全状态是指系统可能发生死锁的状态并不意味着系统已经发生死锁。
  • 处于不安全状态的系统不会必然导致死锁。死锁是不安全状态的真子集。

银行家算法

具有代表性的避免死锁的算法是 Dijkstra 给出的银行家算法。为实现银行家算法,系统中必须设置若干数据结构。

假定系统中有n个进程(P1,P2,…Pn)、m类资源(R1,R2,…Rm),银行家算法中使用的数据结构如下:

  • 可利用资源向量 Available。这是一个含有 m个元素的数组,其中 Available[i]的值表示第 i 类资源的现有空闲数量,其初始值为系统中所配置的该类资源的数目,其数值随着该类资源的分配和回收而动态改变
  • 最大需求矩阵 Max。这是一个 n×m 的矩阵,它定义了系统中每一个进程对 m 类资源的最大需求数。Max[i] [j]的值表示第 i 个进程对第 j 类资源的大需求数
  • 分配矩阵 Allocation。这也是一个 n×m 的矩阵,它定义了系统中每一类资源当前已经分配给每一个进程的资源数目。Allocation[i] [j]的值表示第 i 个进程当前拥有的第 j 类资源的数量
  • 需求矩阵 Need。这同样是一个 n×m 的矩阵,它定义了系统中每个进程还需要的各类资源数目(注意:是“还需要”,不是“总需要”,这表示此矩阵也是变化的)。Need[i] [j]的值表示第 i 个进程还需要的第 j 类资源的数。向量Needi;是矩阵 Need 的第 i 行,是进程 i 的需求资源向量。

Need[i] [j]=Max[i] [j]-Allocation[i] [j]


银行家算法的描述

在这里插入图片描述

在这里插入图片描述

安全性算法描述如下

在这里插入图片描述

在这里插入图片描述

2.4.6 死锁的检测和解除

死锁的检测

  • 资源分配图

    一个系统资源分配图(System Resource Allocation Graph)可定义为一个二元组,即SRAG=(V,E),其中V是顶点集合,而E 是有向边集合。顶点集合可分为两部分:P=(P1,P2··,Pn)是由系统内的所有进程组成的集合,每一个P代表一个进程。R=(r1,r2,··,rm)
    是系统内所有资源组成的集合,每一个r代表一类资源。

    有向边集合 E中的每一条边是一个有序对表示请求资源或者分配资源,<Pi,ri>是请求资源,<ri,Pi>是分配资源。

    在SRAG中,用圆圈代表进程,用方框表示每类资源。每一类资源n可能有多个,可用方框中的圆圈表示各个资源。申请边是从进程到资源的有向边,表示进程申请一个资源,但当前该进程尚未得到该资源。分配边是从资源到进程的有向边,表示有一个资源分配给进程。一条申请边仅指向代表资源类r的方框,表示申请时不指定哪一个具体资源。当进程 Pi申请资源类ri的一个资源时,将一条申请边加入资源分配图,若这个申请是可以满足的,则该申请边立即转换成分配边;当进程随后释放了某个资源时,则删除分配边。

    在这里插入图片描述

    在这里插入图片描述

  • 死锁定理

    可以用简化资源分配图的方法来检测系统状态S是否是死锁状态。

    • 在资源分配图中,找出一个既不阻塞又非孤立的进程结点Pi(即从进程集合中找到一个存在连接的边,且资源申请数量小于系统中已有空闲资源数量的进程)。因进程 Pi 获得了所需要的全部资源,它能继续运行直到完成,然后释放其占有的所有资源(这相当于消去Pi;的所有申请边与分配边,使之成为孤立的结点)。
    • 进程Pi释放资源后,可以唤醒因等待这些资源而阻塞的进程,原来阻塞的进程可能变为非阻塞进程,根据第一步中的简化方法消去分配边与申请边。
    • 重复前两步的简化过程后,若能消去图中所有边,使所有进程成为孤立结点,则称该图是可完全简化的;若通过任何过程均不能使该图完全简化,则称该图是不可完全简化的。

    可以证明的是,不同的简化顺序将得到相同的不可简化图。系统状态 S 为死锁状态的条件是:当且仅当S 状态的资源分配图是不可完全简化的,该定理称为死锁定理

死锁与检测算法

检测死锁算法的基本思想是:获得某时刻t系统中各类可利用资源的数目向量 available(t),对于系统中的一组进程{P1,P2,··Pn},找出那些对各类资源请求数目均小于系统现有的各类可利用资源数目的进程。这样的进程可以获得它们所需要的全部资源并运行结束,当运行结束后,它们会释放所占有的全部资源,从而使可用资源数目增加,将这样的进程加入到可运行结束的进程序列中,然后对剩下的进程再进行上述考查。如果一组进程中有几个不属于该序列,那么它们会发生死锁。

在这里插入图片描述

在这里插入图片描述

死锁解除

旦检测出系统中出现了死锁,就应使陷入死锁的进程从死锁状态中解脱出来,即死锁解除。

常用的解除死锁的方法有以下 3 种:

  • 剥夺资源。从其他进程中抢占足够的资源给死锁的进程以解除其死锁状态。
  • 撤销进程。撤销一些进程,直到有足够的资源分配给其他进程,解除死锁状态。
  • 进程回退。让一个或多个进程回退到足以回避死锁的地步,进程回退时自愿释放资源而不是被剥夺。要求系统保持进程的历史信息,设置还原点。

2.4.7 死锁与饿死

即使系统没有发生死锁,某些进程也可能会长时间等待。当等待时间给进程推进和响应带来明显影响时,称此时发生了进程饥饿,当饥饿到一定程度,进程所赋予的任务即使完成也不再具有实际意义时,则称该进程被饿死

死锁与饿死的区别:

  • 从进程状态考虑,死锁进程都处于等待状态;忙时等待(处于运行或就绪状态)的进程并非处于等待状态,但却可能被饿死。
  • 死锁进程等待的是永远不会被释放的资源:而饿死进程等待的是会被释放但却不会分配给自己的资源,表现为等待时间没有上界(排队等待或忙时等待)。
  • 死锁一定发生了循环等待,而饿死则不然。这也表明通过资源分配图可以检测死锁存在与否,但却不能检测是否有进程饿死。
  • 死锁一定涉及多个进程,而饥饿或被饿死的进程可能只有一个。

饥饿和饿死与资源分配策略有关,因而可从公平性方面考虑防止饥饿与饿死,以确保所有进程不被忽视,如多级反馈队列调度算法。

猜你喜欢

转载自blog.csdn.net/pipihan21/article/details/129808475