剖析虚幻渲染体系(18)- 操作系统(进程)

18.4 进程

本章将阐述各种操作系统下的进程的概念、特点和技术内幕。

18.4.1 进程模型

在这种模型中,计算机上所有可运行的软件,有时包括操作系统,都被组织成若干顺序进程,或者简称为进程。进程只是执行程序的一个实例,包括程序计数器、寄存器和变量的当前值。从概念上讲,每个进程都有自己的虚拟CPU。当然,在现实中,真正的CPU在进程之间来回切换,但为了理解系统,考虑以(伪)并行方式运行的进程集合要比跟踪CPU如何在程序之间切换容易得多。这种快速的来回切换称为多道程序设计(multiprogramming)

在下图(a)中,我们看到一台计算机在内存中多道编程四个程序。在下图(b)中,我们看到四个进程,每个进程都有自己的控制流(即自己的逻辑程序计数器),每个进程独立于其他进程运行。当然,只有一个物理程序计数器,因此当每个进程运行时,其逻辑程序计数器被加载到实际程序计数器中。当它完成时(暂时),物理程序计数器保存在内存中进程的存储逻辑程序计数器中。在下图(c)中,我们可以看到,从足够长的时间间隔来看,所有进程都取得了进展,但在任何给定的时刻,只有一个进程实际在运行。

(a) 多道程序设计四个程序。(b) 四个独立、连续进程的概念模型。(c) 一次只有一个程序处于活动状态。

进程或任务是正在执行的程序的实例,进程的执行必须按编程顺序。任何时候最多执行一条指令,包括由程序计数器的值和处理器寄存器的内容表示的当前活动,还包括包含临时数据(如方法参数返回地址和局部变量)的进程堆栈和包含全局变量的数据段。进程还可能包括堆,堆是在进程运行时动态分配的内存。

常规的进程实现和内存结构。

进程和程序之间的差异:程序本身不是一个进程,正在执行的程序称为进程。程序是一个被动实体,例如存储在磁盘上的文件的内容,而进程是一个活动实体,有一个程序计数器指定要执行的下一条指令,一组相关的资源可以在多个进程之间共享,使用一些调度算法来确定何时停止一个进程并为另一个进程服务。

当进程执行时,它会改变状态,其状态由该进程的正确活动定义。每个进程可能处于以下状态之一:

  • 新建(New):正在创建进程。
  • 就绪(Ready):进程正在等待分配给处理器。
  • 正在运行(Running Man):正在执行指令。
  • 等待(Waiting):进程正在等待某些事件发生。
  • 已终止(Terminated:):进程已完成执行。

许多进程可能同时处于就绪和等待状态,但在任何一个处理器上,任何时候都只能运行一个进程。下图是进程不同状态之间的切换:

RTOS+的进程状态切换如下:

Linux进程状态切换如下:

两状态的进程模型:

五状态的进程模型:

进程队列模型样例图:

带一个或两个暂停状态的进程状态转换图:

Unix进程状态转换表:

18.4.2 进程控制块

每个进程在操作系统中由进程控制块(Process Control Block,PCB)表示,也由进程控制块控制。进程控制块也称为任务控制块,包含与特定过程相关联的许多信息,包括以下信息:

  • 进程状态:状态可以是新状态、就绪状态、运行状态、等待状态或终止状态。
  • 程序计数器:它指示为此目的执行的下一条指令的地址。
  • CPU寄存器:寄存器的数量和类型因计算机架构而异。它包括累加器、索引寄存器、堆栈指针和通用寄存器,加上在发生中断时必须保存的任何条件代码信息,以便在之后正确地继续处理。
  • CPU调度信息:此信息包括调度队列的进程优先级指针和任何其他调度参数。
  • 内存管理信息:根据操作系统使用的内存系统,该信息可能包括诸如条形和限制寄存器、页面表或段表的值等信息。
  • 账号信息:此信息包括CPU数量和实时使用时间、时间限制、帐号、作业或进程编号等。
  • I/O状态信息:此信息包括分配给此进程的I/O设备列表、打开的文件列表等,PCB只是用作存储可能因进程而异的任何信息的存储库。

CPU利用PCB来切换进程的执行。

18.4.3 上下文切换

当CPU切换到另一个进程时,系统必须保存旧进程的状态,并加载新进程的保存状态,这个行为称为上下文切换。上下文切换时间开销大,系统在切换时没有做任何有用的工作。切换速度因机器而异,具体取决于内存速度、必须复制的寄存器数量以及特殊指令的存在,典型的速度是几毫秒。上下文切换时间高度依赖于硬件支持。

18.4.4 进程组成

进程是一个包含和管理对象,表示程序的运行实例。以往经常使用的“进程运行”是不准确的,进程实际上不运行——而是进程管理。线程才是执行代码并在技术上运行的载体。从高层次的角度来看,一个进程具有以下特点:

  • 一个可执行程序,其中包含用于在进程中执行代码的初始代码和数据。
  • 一个私有虚拟地址空间,用于为进程内的代码需要的任何目的分配内存。
  • 访问令牌(有时称为主令牌),是存储进程默认安全上下文的对象,由进程内执行代码的线程使用(除非线程通过模拟使用不同的令牌)。
  • 执行(内核)对象的私有句柄表,如事件、信号量和文件。
  • 一个或多个执行线程。使用一个线程(执行进程的主入口点)创建普通用户模式进程,没有线程的用户模式进程通常是无用的,通常情况下会被内核销毁。

一个进程的重要组成部分。

进程的寻址需求。

操作系统控制表的常规结构。

Windows进程和它的资源构成。

进程由其进程ID唯一标识,只要内核进程对象存在,进程ID就保持唯一。一旦它被销毁,相同的ID就可以重新用于新的进程。可执行文件本身不是进程的唯一标识符,例如,Windows的记事本(notepad.exe)可能有5个实例的exe同时运行,每个进程都有自己的地址空间、线程、句柄表、进程ID等。这5个进程都使用相同的镜像文件(notepad.exe)作为其初始代码和数据,但每个实例都有自己的属性。

动态链接库(DLL)是可执行文件,可以包含代码、数据和资源(至少其中之一)。DLL在进程初始化时(称为静态链接)或在显式请求时(动态链接)动态加载到进程中。DLL由于不包含可执行文件等标准主函数,因此无法直接运行。DLL允许在使用同一DLL的多个进程之间共享物理内存中的代码,下图显示使用映射到相同物理(和虚拟)地址的共享DLL的两个进程。

尽管自Windows NT第一次发布以来,进程的基本结构和属性没有改变,但新的进程类型已经引入到具有特殊行为或结构的系统中。以下是当前支持的所有进程类型的快速概述:

  • 受保护进程。是在Windows Vista中引入的。创建它们是为了通过防止对呈现受数字版权管理(DRM)保护内容的进程的侵入性访问来支持数字版权管理。例如,没有其他进程(即使以管理员权限运行)可以读取具有受保护进程地址空间的内存,因此DRM保护的数据不能直接被窃取。
  • UWP进程。从Windows 8开始可用,承载Windows运行时,通常发布到Microsoft应用商店。UWP进程在AppContainer中执行——是一个沙盒,限制了该进程可以执行的操作。
  • 受保护的轻量进程(PPL)。扩展了Vista的保护机制,增加了多个级别的保护,甚至允许第三方服务作为PPL运行,保护它们免受入侵访问和终止,即使是管理级进程。
  • 最小化进程(Minimal Process)。是一种真正新的进程形式,其地址空间不包含正常进程所包含的常用图像和数据结构。例如,没有映射到进程地址空间的可执行文件,也没有DLL,进程地址空间实际上是空的。
  • 微进程(Pico Process)。这些进程是最小的进程,只有一个附加:微提供程序,它是一个内核驱动程序,可以拦截Linux系统调用并将其转换为等效的Windows系统调用。这些进程用于Windows Subsystem for Linux(WSL),可从Windows 10版本1607获得。

18.4.5 进程调度

几乎所有进程都会交替使用(磁盘或网络)I/O请求进行计算,如下图所示。通常,CPU会运行一段时间而不停止,然后进行系统调用以读取文件或写入文件。当系统调用完成时,CPU会再次计算,直到需要更多数据或必须写入更多数据,依此类推。请注意,一些I/O活动算作计算。例如,当CPU将位复制到视频RAM以更新屏幕时,它是在计算,而不是执行I/O,因为CPU正在使用中。在这种意义上,I/O是指进程进入阻塞状态,等待外部设备完成其工作。

CPU使用的突发与等待I/O的时间交替发生。(a) CPU受限的进程。(b) I/O受限的进程。

关于上图,需要注意的重要一点是,一些进程,如(a)中的进程,花费了大部分时间进行计算,而其他进程,如(b)所示,花费了大量时间等待I/O。

前者称为计算受限或CPU受限;后者称为I/O受限。计算受限的进程通常有较长的CPU突发,因此很少有I/O等待,而I/O受限进程有较短的CPU突发时间,因此频繁的I/O等待。注意,关键因素是CPU突发的长度,而不是I/O突发的长度。I/O受限进程是I/O受限的,因为它们不会在I/O请求之间进行大量计算,而不是因为它们有特别长的I/O请求。发出读取磁盘块的硬件请求需要同样的时间,无论数据到达后处理数据需要多少时间。

值得注意的是,随着CPU的速度越来越快,进程往往会获得更多的I/O受限。出现这种效果是因为CPU的改进速度比磁盘快得多。因此,I/O受限进程的调度在未来可能会成为一个更重要的主题。这里的基本思想是,如果一个I/O受限的进程想要运行,它应该能够很快获得机会,以便发出磁盘请求并保持磁盘繁忙。当进程受到I/O限制时,需要相当多的进程来保持CPU的完全占用。

与进程调度相关的一个关键问题是何时做出进程调度策略。事实证明,在各种情况下都需要调度。首先,创建新进程时,需要决定是运行父进程还是子进程。由于这两个进程都处于就绪状态,这是一个正常的调度决策,可以选择任何一种方式,也就是说,调度程序可以合法地选择下一个运行父进程或子进程。

其次,当进程退出时,必须做出调度决策。该进程无法再运行(因为它不再存在),因此必须从就绪进程集中选择其他进程。如果没有进程就绪,则系统提供的空闲进程通常会运行。

第三,当一个进程在I/O、信号量或其他原因上阻塞时,必须选择另一个进程来运行。有时,阻塞的原因可能会影响选择。例如,如果A是一个重要的进程,它正在等待B退出其关键区域,那么让B接下来运行将允许它退出其关键区,从而让A继续。然而,问题是调度程序通常没有必要的信息来考虑这种依赖关系。

第四,当发生I/O中断时,可以做出调度决策。如果中断来自现已完成其工作的I/O设备,则等待I/O的某些进程可能已准备好运行。由调度程序决定是运行新准备好的进程、中断时正在运行的进程还是第三个进程。

如果硬件时钟以50或60 Hz或其他频率提供周期性中断,则可以在每个时钟中断或每个第k个时钟中断时做出调度决策。调度算法可以根据如何处理时钟中断分为两类。非临时调度算法选择要运行的进程,然后让它运行,直到它阻塞(在I/O上或等待另一个进程)或自动释放CPU。即使它运行了许多小时,也不会被强制暂停。实际上,在时钟中断期间不会做出调度决策。时钟中断处理完成后,中断前运行的进程将恢复,除非高优先级进程正在等待现已满足的超时。

相比之下,抢占式调度算法选择一个进程,并让它最多运行一段固定时间。如果它在时间间隔结束时仍在运行,那么它将被挂起,并且调度程序会选择另一个要运行的进程(如果有的话)。执行抢占式调度需要在时间间隔结束时发生时钟中断,以便将CPU控制权交还给调度程序。如果没有可用的时钟,则非临时调度是唯一的选择。

在不同的环境中,需要不同的调度算法。调度程序应该优化的内容在所有系统中并不相同,值得区别的三种环境是:批次、互动、实时。

为了设计调度算法,有必要了解好的算法应该做什么。有些目标取决于环境(批处理、交互式或实时),但有些目标在所有情况下都是可取的。下面列出了一些目标:

  • 所有系统:
    • 公平性:给每个进程公平的CPU份额。
    • 策略执行:确保所述策略得到执行。
    • 平衡:使系统的所有部分保持忙碌。
  • 批处理系统:
    • 吞吐量:最大化每小时作业数。
    • 周转时间:最小化提交和终止之间的时间。
    • CPU利用率:使CPU始终处于繁忙状态。
  • 交互式系统:
    • 响应时间:快速响应请求。
    • 比例:满足用户的期望。
  • 实时系统:
    • 满足最后期限:避免丢失数据。
    • 可预测性:避免多媒体系统的质量下降。

在任何情况下,公平都很重要。可比进程应获得可比服务,给一个进程比同等进程多很多CPU时间是不公平的。当然,不同类别的进程可能会有不同的处理方式。与公平相关的是执行系统的策略,如果本地策略是安全控制进程可以在任何时候运行,即使意味着工资单延迟30秒,调度程序也必须确保执行此策略。

另一个总体目标是尽可能使系统的所有部分保持繁忙。如果CPU和所有I/O设备都可以一直运行,那么与某些组件处于空闲状态相比,每秒完成的工作量会更多。例如,在批处理系统中,调度程序可以控制哪些作业进入内存以运行。

在内存中同时使用一些CPU受限进程和一些I/O受限进程比首先加载和运行所有CPU受限作业,然后在它们完成时加载和运行全部I/O受限作业要好。如果使用后一种策略,当CPU受限的进程正在运行时,它们将争夺CPU,磁盘将处于空闲状态。稍后,当I/O受限作业进入时,它们将争夺磁盘,CPU将处于空闲状态。最好通过仔细混合进程来保持整个系统同时运行。

系统在不同调度级别下的状态转换图如下:

调度队列图如下:

18.4.5.1 调度基础

进程调度是操作系统的基本功能。当一台计算机进行多程序设计时,它有多个进程同时竞争CPU。如果只有一个CPU可用,那么必须选择下一个执行哪个进程。这一决策过程称为调度(scheduling),做出此选择的操作系统部分称为调度程序(scheduler),用于进行此选择的算法称为调度算法(scheduling algorithm)

调度队列(Scheduling queue)是进程进入系统时被放入的作业队列,此队列由系统中的所有进程组成,驻留在主存中并已准备好等待执行或保存在名为就绪队列的列表中的进程。

此队列通常存储为链接列表,就绪队列标题包含指向列表中第一个和最后一个PCB的指针,PCB包含一个指向就绪队列中下一个PCB的指针字段。等待特定I/O设备的进程列表保存在名为设备队列的列表中,每个设备都有自己的设备队列。新进程最初被放入就绪队列。它在就绪队列中等待,直到它被选中执行并被赋予CPU。

0.png)

18.4.5.2 调度程序

调度程序的描述如下:

  • 进程在其生命周期内在各种调度队列之间迁移。操作系统必须以某种方式从这些队列中选择调度进程。此选择过程由适当的调度程序执行。
  • 在批处理系统中,会提交更多进程,然后立即执行。因此,这些进程被假脱机到一个大容量存储设备(如磁盘)中,并保存在那里供以后执行。

调度程序的类型有以下几种:

长期调度程序(Long term scheduler)

长期调度程序从磁盘中选择进程并将其加载到内存中以便执行。它控制多重编程的程度,即内存中进程的数量,执行频率低于其他调度程序。如果多道程序设计的程度是稳定的,那么进程创建的平均速度等于进程离开系统的平均离开速度。因此,仅当进程离开系统时才需要调用长期调度程序。由于执行之间的间隔较长,它可以花费更多的时间来决定应该选择哪个进程来执行。

CPU中的大多数进程要么是I/O密集的,要么是CPU密集的。I/O密集的进程(交互式“C”程序)是一个将大部分时间花在I/O操作上的进程,而不是花在执行I/O操作上,CPU密集的进程在计算上花费的时间比I/O操作(复杂的排序程序)要多。长期调度程序应选择I/O绑定和CPU绑定进程的良好组合,这一点很重要。

短期调度程序(Short term scheduler)

短期调度程序在准备执行的进程中进行选择,并将CPU分配给其中一个进程,这两个调度程序之间的主要区别是它们的执行频率。短期调度程序必须经常为CPU选择新进程,它必须在100毫秒内至少执行一次。由于两次执行之间的时间间隔很短,因此必须非常快。

中期调度程序(Medium term scheduler)

一些操作系统引入了一种称为中期调度程序的额外中间级别的调度,这个调度器背后的主要思想是,有时从内存中删除进程是有利的,从而降低多道程序的程度。然后,该进程可以重新引入内存,并且可以从中断的地方继续执行,称为交换。进程稍后由中期调度程序调出和调入。交换对于改善进程未命中是必要的,或者由于内存需求的某些变化,超出了可用内存限制,这需要释放一些内存。

18.4.5.3 调度目标

调度的目标有:

  • CPU利用率:使CPU尽可能繁忙。
  • 吞吐量:每个时间单位完成其执行的进程数。
  • 周转时间:执行特定流程的时间量。
  • 等待时间:进程在就绪队列中等待的时间。
  • 响应时间:从提交请求到生成第一个响应(而不是输出)所用的时间(对于分时环境)。

调度的总体目标:

  • 公平。在任何情况下,公平都很重要。调度程序确保每个进程都能获得其CPU的公平份额,并且没有进程会无限期延迟。
    请注意,给予同等或同等的时间是不公平的。想想核电站的安全控制和工资单。
  • 政策执行。调度程序必须确保系统的策略得到执行。例如,如果局部策略是安全的,那么安全控制处理必须能够在任何时候运行,即使意味着工资单处理延迟。
  • 效率。如果可能的话,调度程序应该使系统(特别是CPU)在百分之几的时间内保持繁忙。如果CPU和所有输入/输出设备可以一直运行,那么与某些组件处于空闲状态时相比,每秒完成的工作更多。
  • 响应时间。调度程序应尽量减少交互式用户的响应时间。
  • 轮回。调度程序应最小化批处理用户必须等待输出的时间。
  • 吞吐量。调度程序应最大化单位时间内处理的作业数。

稍加思考就会发现其中一些目标是相互矛盾的。可以看出,任何支持某类作业的调度算法都会损害另一类作业。毕竟,可用的CPU时间是有限的。

就如何处理时钟中断而言,调度算法可以分为两类:

  • 非抢占式调度。如果一个进程一旦被赋予CPU,CPU就不能从该进程中取出,那么调度程序是非抢占性的。以下是非抢占式调度的一些特征:

    • 在非抢占式系统中,短作业由长作业等待,但所有进程的总体处理是公平的。
    • 在非抢占式系统中,响应时间更容易预测,因为传入的高优先级作业不能取代等待的作业。
    • 在非抢占式调度中,调度程序在两种情况下执行作业:1、当进程从运行状态切换到等待状态时;2、当进程终止时。
  • 抢占式调度。如果一个进程一旦被给予,CPU就可以被拿走,那么调度规程是优先的。允许逻辑上可运行的进程暂时挂起的策略称为抢占式调度,它与“运行到完成”方法相反。

18.4.5.4 CPU调度算法

CPU调度处理决定就绪队列中的哪些进程将分配给CPU的问题。下面是我们将要研究的一些调度算法。

先到先服务(FCFS)

FCFS是最简单的CPU调度算法。首先请求CPU的进程,即首先分配给CPU的进程。可以通过FIFO队列轻松管理,当进程进入就绪队列时,其PCB链接到队列的后部。但是,FCFS的平均等待时间很长。考虑以下情况:

进程CPU时间
P13
P24
P32
P44

如果顺序为P1、P2、P3、P4,则使用FCFS算法计算平均等待时间和平均周转时间。解决方案:如果进程以P1、P2、P3、P4的顺序到达,则根据FCFS,甘特图将为:

不同的时间描述如下:

  • 进程等待时间:P1 = 0,P2 = 3,P3 = 8,P4 = 10。

  • 进程周转(Turnaround)时间:P1 = 0 + 3 = 3,P2 = 3 + 5 = 8,P3 = 8 + 2 = 10,P4 = 10 + 4 =14。

  • 平均等待时间:(0 + 3 + 8 + 10) / 4 = 21 / 4 = 5.25。

  • 平均周转时间:(3 + 8 + 10 + 14)/4 = 35 / 4 = 8.75。

FCFS算法是非抢占式的,即一旦CPU分配给进程,该进程就会通过终止或请求I/O来保持CPU,直到释放CPU为止。

最短作业优先(SJF)

此算法的另一个名称是下一个最短进程(SPN),如果CPU可用,此算法将与每个进程关联。这种调度也称为最短的下一次CPU迸发(burst),因为调度是通过检查进程的下一个CPU迸发的长度而不是其总长度来完成的。考虑以下情况:

进程CPU时间
P13
P25
P32
P44

解决方案:根据SJF,甘特图将是:

不同的时间描述如下:

  • 进程等待时间:P1 = 0,P2 = 2,P3 = 5,P4 = 9。
  • 进程周转时间:P3 = 0 + 2 = 2,P1 = 2 + 3 = 5,P4 = 5 + 4 = 9,P2 = 9 + 5 =14。
  • 平均等待时间:(0 + 2 + 5 + 9) / 4 = 16 / 4 = 4。
  • 平均周转时间:(2 + 5 + 9 + 14)/4 = 30 / 4 = 7.5。

SJF算法可以是抢占式或非抢占式算法,抢占式的SJF也称为最短剩余时间优先。考虑以下示例:

进程到达时间CPU时间
P108
P214
P329
P435

此情况的甘特图如下:

进程等待时间:P1 = 10 - 1 = 9,P2 = 1 – 1 = 0,P3 = 17 – 2 = 15,P4 = 5 – 3 = 2。

平均等待时间:(9 + 0 + 15 + 2) / 4 = 26 / 4 = 6.5。

优先级调度

在这个调度中,每个进程都有一个优先级编号(整数),CPU被分配给优先级最高的进程(最小的整数,最高的优先级),可分为抢占式和非抢占式。同等优先级的流程以FCFS方式安排。SJF是一种优先级调度,其中优先级是预测的下一个CPU突发时间。

存在饥饿问题——低优先级进程可能永远不会执行,解决方案是老化——随着时间的推移,进程的优先级增加。

优先级可以在内部或外部定义。内部优先事项的例子有:时间限制、内存要求、文件要求(如打开文件的数量)、CPU与I/O要求。外部定义的优先级由操作系统外部的标准设置,例如进程的重要性、为使用计算机而支付的资金类型或金额、赞助工作的部门、政策。

优先级队列。

考虑以下示例:

进程到达时间CPU时间
P1103
P211
P323
P414
P552

根据优先级调度,甘特图将为:

进程等待时间:P1 = 6,P2 = 0,P3 = 16,P4 = 18,P5 = 1。

平均等待时间:(0 + 1 + 6 + 16 + 18) / 5 = 41 / 5 = 8.2。

轮询(Round Robin)

这种算法仅用于分时系统设计,类似于具有抢占条件的FCFS调度,可以在进程之间切换。一个称为量程时间或时间片的小时间单位用于在进程之间切换,轮询制下的平均等待时间很长。考虑以下示例:

进程CPU时间
P13
P25
P32
P44

时间片 = 1ms,则甘特图为:

进程等待时间:

  • P1 = 0 + (4 – 1) + (8 – 5) = 0 + 3 + 3 = 6
  • P2 = 1 + (5 – 2) + (9 – 6) + (11 – 10) + (13 – 12) = 1 + 3 + 3 + 1 + 1 = 9
  • P3 = 2 + (6 – 3) = 2 + 3 = 5
  • P4 = 3 + (7 – 4) + (10 – 8) + (12 – 11) = 3 + 3 + 2 + 1 = 9

平均等待时间:(6 + 9 + 5 + 9) / 4 = 7.2

最短剩余时间(SRT)

SRT是SJF的抢占式对等物,在分时环境中很有用。在SRT调度中,下一步运行估计运行时间最短的进程,包括新到达的进程。在SJF方案中,一旦作业开始执行,它就会一直运行到完成,一个正在运行的进程可以被一个估计运行时间最短的新到达进程抢占。SRT的开销高于对应的SJF,必须跟踪运行进程的运行时间,并且必须处理偶尔的抢占。在这个方案中,小进程的到达几乎会立即运行,然而,更长的工作意味着更长的等待时间。

最短进程优先(Shortest Process Next)

因为最短的作业首先总是为批处理系统产生最小的平均响应时间,所以如果它也可以用于交互式流程,那就更好了。在一定程度上是可以的。交互进程通常遵循等待命令、执行命令、等待命令、运行命令等模式。如果我们将每个命令的执行视为单独的“作业”,那么我们可以通过先运行最短的一个来最小化总体响应时间,前提是找出当前可运行的进程中最短的进程。

保证调度(Guaranteed Scheduling)

调度的一种完全不同的方法是向用户作出关于性能的真正承诺,然后兑现这些承诺。一个切实可行且易于实现的承诺是:如果在你工作时有n个用户登录,你将获得大约1/n的CPU电量。类似地,在一个运行n个进程的单用户系统上,在所有条件都相同的情况下,每个进程应该获得1/n的CPU周期,这似乎很公平。

为了兑现这一承诺,系统必须跟踪每个进程自创建以来有多少CPU。然后,它计算每个进程有权使用的CPU数量,即自创建以来的时间除以n。由于每个进程实际拥有的CPU时间量也是已知的,因此计算实际消耗的CPU时间与有权使用CPU时间的比率非常简单。比率为0.5意味着一个进程只拥有它应该拥有的一半,比率为2.0意味着进程拥有的是它应有的两倍。然后,算法以最低比率运行该进程,直到其比率超过其最接近的竞争对手的比率。然后选择下一个运行。

彩票调度

虽然向用户作出承诺,然后兑现承诺是一个好主意,但很难实现。然而,可以使用另一种算法以更简单的实现给出类似的可预测结果。这被称为彩票调度(Lottery Scheduling)

基本思想是为各种系统资源(如CPU时间)提供进程彩票。每当必须做出调度决策时,都会随机选择彩票,持有该彩票的进程将获得资源。当应用于CPU调度时,系统可能每秒举行50次抽奖,每个优胜者都会获得20毫秒的CPU时间作为奖品。

公平调度(Fair-Share Scheduling)

到目前为止,我们假设每个进程都是自己调度的,而不管其所有者是谁。因此,如果用户1启动九个进程,而用户2启动一个进程,并且具有循环或同等优先级,则用户1将获得90%的CPU,用户2仅获得10%的CPU。

为了防止这种情况,一些系统在调度进程之前会考虑哪个用户拥有进程。在这个模型中,每个用户都被分配了CPU的一部分,调度程序以强制执行的方式选择进程。因此,如果向两个用户承诺每人50%的CPU,那么无论他们有多少进程,他们都会得到50%的CPU。

举个例子,考虑一个有两个用户的系统,每个用户承诺占用50%的CPU。用户1有四个进程(A、B、C和D),用户2只有一个进程(E)。如果使用循环调度,则满足所有约束的可能调度序列如下:

A E B E C E D E A E B E C E E D E ...

另一方面,如果用户1有权获得用户2两倍的CPU时间,我们可能会得到:

A B E C D E A B E C D E ...

当然,还有许多其他的可能性,可以利用,取决于公平的概念是什么。

多队列

多级队列调度算法将就绪队列划分为几个单独的队列,例如,在多级队列调度中,进程被永久分配给一个队列。根据进程的某些属性,如内存大小、进程优先级、进程类型,这些进程被永久分配给另一个进程。算法从具有最高优先级的已占用队列中选择进程,然后运行该进程。

多级反馈队列

多级反馈队列调度算法允许进程在队列之间移动,使用许多就绪队列,并将不同的优先级与每个队列相关联。算法从占用的队列中选择优先级最高的进程,并以抢占或非抢占方式运行该进程,如果进程使用了太多CPU时间,它将移动到低优先级队列。类似地,在较低优先级队列中等待时间过长的进程可能会被移动到较高优先级队列,也可能被移动到最高优先级队列。请注意,这种形式的老化可以防止饥饿。例子:

  • 进入就绪队列的进程被放置在队列0中。
  • 如果未在8毫秒内完成,则会将其移动到队列1的尾部。
  • 如果它没有完成,它将被抢占并放入队列2中。
  • 队列2中的进程仅在队列0和队列1为空时,在FCFS基础上运行,只有当队列2在FCFS基本队列上运行。

三个队列:Q0–RR,时间量为8毫秒;Q1–RR时间量16毫秒;Q2–FCFS。

通常,多级反馈队列调度程序定义的参数有:队列数,每个队列的调度算法,用于确定何时将进程升级到更高优先级队列的方法,用于确定何时将进程降级到较低优先级队列的方法,用于确定进程需要服务时将进入哪个队列的方法。

18.4.5.5 调度总结

进程调度在实际运行环境中,需要考量CPU、核心数量、IO等因素的影响,然后通过不同的调度算法来统计其数据,从而得出相对客观且有参考价值的调度数据。评估示意图如下:

各种调度策略的特点:

调度策略的时序对比图:

调度策略的综合对比:

除了上述出现的调度算法,实际上还有很多其它调度策略,如适用于实时操作系统的时限调度(Deadline Scheduling)比率单调调度(Rate Monotonic Scheduling)等。

另外,还存在优先级反转(Priority Inversion),它是一种可能发生在任何基于优先级的抢占式调度方案中的现象,但在实时调度环境中尤其相关。优先级反转的最著名例子涉及火星探路者(Pathfinder)任务,这个漫游机器人于1997年7月4日登陆火星,开始收集大量数据并将其传送回地球。但在任务开始几天之后,着陆器软件开始经历整个系统重置,每次都会导致数据丢失。在建造“探路者”号的喷气推进实验室团队进行了大量努力之后,问题被追溯到优先级反转。

在任何优先级调度方案中,系统应始终以最高优先级执行任务。当系统内的情况迫使较高优先级的任务等待较低优先级的任务时,会发生优先级反转。如果较低优先级的任务锁定了资源(如设备或二进制信号量),而较高优先级的任务试图锁定同一资源,则会发生优先级反转的简单示例。在资源可用之前,优先级较高的任务将处于阻塞状态。如果较低优先级的任务很快完成并释放资源,则较高优先级的任务可能会很快恢复,并且可能不会违反实时约束。

一种更严重的情况称为无限优先级反转(Unbound Priority Inversion),其中优先级反转的持续时间不仅取决于处理共享资源所需的时间,还取决于其他不相关任务的不可预测的操作。Pathfinder软件中经历的优先级反转是无限的,是个很好的例子。

18.4.6 进程属性

Windows进程的常见属性在任务管理器中可以查看,它们的详情如下所述:

  • 名字。通常是进程所基于的可执行文件名,但不是进程的唯一标识符。有些进程似乎根本没有可执行名称,例如包括系统、安全系统、注册表、内存压缩、系统空闲进程和系统中断。

    • 系统中断(System Interrupt)实际上不是一个进程,只是用来衡量内核服务硬件中断和延迟过程调用所花费的时间。
    • 系统空闲进程也不是真正的进程,它的进程ID(PID)始终为零,只是描述了Windows空闲时间——CPU无事可做时的占比。
    • 系统进程是一个真正的过程,在技术上也是一个最小化进程,总是有一个4的PID。它代表内核空间中发生的一切——内核和内核驱动程序使用的内存、开放句柄、线程等。
    • 安全系统进程仅在Windows 10和Server 2016(及更高版本)系统上可用,这些系统启动时启用了基于虚拟化的安全性。它代表了安全内核中发生的一切。
    • 注册表进程是Windows 10版本1803(RS4)中可用的最小化进程,用作管理注册表的“工作区”,而不是像以前版本那样使用分页池。
    • 内存压缩进程是Windows 10版本1607上可用的最小化进程,并在其地址空间中保存压缩内存。内存压缩是Windows 10中添加的一项功能,用于保存物理内存(RAM),特别适用于资源有限的设备,如手机和物联网设备。令人困惑的是,任务管理器没有显示此进程,但可被Process Explorer正确显示。
  • PID。进程的唯一ID,是4的倍数,其中最低有效PID值为4(属于系统进程)。一旦进程终止,进程ID将被重用,因此可以看到一个新进程。如果进程需要唯一标识符,则PID和流程启动时间的组合在特定系统上确实是唯一的。

  • 状态(Status)。状态可以有三个值之一:运行(Running)、挂起(Suspended)和不响应(Not Responding),根据进程类型总结了它们的含义。

    进程类型运行时的情况挂起的情况不响应的情况
    GUI进程(非UWP)GUI线程可响应时进程中的所有线程都挂起GUI线程至少5秒未检查消息队列
    CUI进程(非UWP)至少有一个线程未挂起进程中的所有线程都挂起用不
    UWP进程在后台在后台GUI线程至少5秒未检查消息队列

    常见的状态转换如下图所示:

  • 用户名。用户名指示进程正在哪个用户下运行。令牌对象附加到进程(称为主令牌),该进程基于用户保存进程的安全上下文。该安全上下文包含用户所属的组、权限等信息。进程可以在特定的内置用户下运行,例如本地系统(在任务管理器中显示为系统)、网络服务和本地服务。这些用户帐户通常用于运行服务。

  • 会话ID。进程在其下执行会话的会话号,会话0用于系统进程和服务,会话1及以上用于交互式登录。

  • CPU。显示该进程的CPU消耗百分比,注意它仅显示整数。要获得更好的精度,请使用Process Explorer。

  • 内存。与内存相关的列有些棘手,任务管理器显示的默认列是内存(活动专用工作集)或内存(专用工作集,早期版本)。术语工作集是指RAM(物理存储器),私有工作集是进程使用的RAM,不与其他进程共享。共享内存最常见的例子是DLL代码。活动专用工作集与专用工作集相同,但对于当前挂起的UWP进程设置为零。以上两个计数器是否能很好地指示进程使用的内存量?不幸的是,不是。这些指示使用的是专用RAM,但是当前被调出的内存呢?还有另一列——提交大小(Commit Size),用于了解进程内存使用情况的最佳列。任务管理器默认情况下不显示此列。

  • 基本优先级。基本优先级列(正式称为优先级类)显示了六个值中的一个,为该进程中执行的线程提供基本调度优先级。与优先级相关联的可能值如下:

    • 低(Idle)= 4
    • 低于正常 = 6
    • 正常(Normal) = 8。最常见(默认)的优先级类别是正常(8)。
    • 高于正常 = 10
    • 高(High) = 13
    • 实时(Real-Time) = 24
  • 句柄。显示在特定进程中打开的内核对象的句柄数量。

  • 线程。“线程”列显示每个进程中的线程数量。通常至少应该是一个,因为没有线程的进程是无用的。但是,一些进程显示为没有线程。具体来说,安全系统显示为没有线程,因为安全内核实际上使用普通内核进行调度。系统中断伪进程根本不是进程,因此不能有任何线程。最后,系统空闲进程也不拥有线程。此进程显示的线程数是系统上的逻辑处理器数。

进程更详细的属性表如下:

在虚拟内存的用户进程:

18.4.7 进程操作

进程创建中涉及的主要部分如下图所示。

1、Open Image File

内核打开镜像(可执行文件)文件,并验证其是否为可移植可执行文件(PE)的正确格式。文件扩展名并不重要,实际内容才重要。假设各种头文件有效,内核将创建一个新的进程内核对象和一个线程内核对象,因为一个正常进程是由一个线程创建的,最终应该执行主入口点。

2、Create & Initialize Kernel Process Object

此时,内核将映像映射到新进程的地址空间以及NtDll.Dll。NtDll映射到每个进程(最小和微进程除外),因为它在进程创建的最后阶段具有非常重要的职责,并且是调用系统调用的最终阶段。创建进程仍在执行的最后一个主要步骤是通知Windows子系统进程(Csrss.exe)已创建新进程和线程。(Csrss可以被认为是内核管理Windows子系统进程某些方面的助手)。

3、Create & Initialize Kernel Thread Object

此时,从内核的角度来看,进程已经成功创建,因此调用方调用的进程创建函数(通常是CreateProcess)返回成功。然而,新进程尚未准备好执行其初始代码。进程初始化的第二部分必须在新进程的上下文中由新创建的线程执行。

一些开发人员认为,在新流程中运行的第一件事是可执行文件的主要功能。然而,在实际的主函数开始运行之前,还有很多事情要做,最明显的是NtDll,因为目前进程中没有其他操作系统级代码。此时,NtDll有几个职责:

  • 首先,它为进程创建用户模式管理对象,称为进程环境块(PEB),并为第一个线程创建用户模式控制对象,称之为线程环境块(TEB)。这些结构部分记录(在<winternl.h>中),正式不应由开发人员直接使用。也就是说,在某些情况下,此结构是有用的,特别是在试图实现难以实现的事情时。
  • 然后执行其他一些初始化,包括创建默认进程堆、创建和初始化默认进程线程池等。
  • 入口点开始执行之前的最后一个主要部分是加载所需的DLL,通常称为加载器。加载程序查看可执行文件的导入部分,其中包括可执行文件所依赖的所有库。这些通常包括Windows子系统dll,如kernel32.dll、user32.dll,gdi32.dll和advapi32.dll。

实际上,开发人员可以编写四个主要函数,每个函数都有相应的C/C++运行时函数。下表总结了这些名称及其使用时间。

开发人员的mainC/C++运行时起点场景
mainmainCRTStartup使用ASCII字符的控制台应用程序
wmainwmainCRTStartup使用Unicode字符的控制台应用程序
WinMainWinMainCRTStartup使用ASCII字符的GUI应用程序
wWinMainwWinMainCRTStartup使用Unicode字符的GUI应用程序

大多数进程将在系统关闭之前的某个时间点终止,有几种方法可以退出或终止进程。需要记住的一点是,无论进程如何终止,内核都会确保进程没有私有的内容:释放所有私有(非共享)内存,并关闭进程句柄表中的所有句柄。如果满足以下任一条件,则过程终止:

1、进程中的所有线程退出或终止。

2、进程中的任何线程调用了ExitProcess。

3、使用TerminateProcess终止进程(通常在外部,但可能是由于未处理的异常)。

编写Windows应用程序的开发者通常会在某个时刻发现执行主函数的线程是“特殊的”,通常称为主线程。可以观察到,无论何时主函数返回,进程都会退出——似乎是上述流程退出原因中未列出的场景。然而,它确实如此,实际是上述的情形2。C/C++运行时库调用main/WinMain,然后执行所需的清理,如调用全局C++析构函数、C运行时清理等,最后调用ExitProcess,导致进程退出。

从内核的角度来看,进程中的所有线程都是相等的,并且没有主线程。当内核中的所有线程退出/终止时,内核会销毁进程,因为没有线程的进程几乎是无用的。实际上,这种情况只能在原生进程(仅依赖于NtDll.dll且没有C/C++运行时的可执行文件)中实现。换句话说,在正常的Windows编程中不太可能发生。

18.4.8 进程补述

18.4.8.1 多程序设计建模

当使用多道程序设计时,CPU利用率可以提高。粗略地说,如果平均进程只计算了它在内存中的20%的时间,那么当五个进程同时在内存中时,CPU应该一直处于繁忙状态。然而,这个模型是不切实际的乐观,因为它默认所有五个进程永远不会同时等待I/O。

一个更好的模型是从概率的角度来看CPU的使用情况。假设一个进程花了一小部分时间等待I/O完成,当内存中同时有nn个进程时,所有nn个进程等待I/O的概率为pnpn(在这种情况下,CPU将处于空闲状态)。CPU利用率由以下公式给出:

CPU利用率=1−pnCPU利用率=1−pn

下图显示了CPU利用率作为nn的函数——称为多道程序设计的程度。

CPU利用率是内存中进程数的函数。

从图中可以明显看出,如果进程花费80%的时间等待I/O,那么必须同时在内存中至少有10个进程才能使CPU浪费低于10%。当你意识到等待用户在终端键入内容(或单击图标)的交互式进程处于I/O等待状态时,应该很清楚,80%以上的I/O等待时间并不罕见。但即使在服务器上,执行大量磁盘I/O的进程通常也会有这个百分比或更多。

为了准确起见,应该指出,刚才描述的概率模型只是一个近似值。它隐式假设所有n个进程都是独立的,意味着内存中有5个进程的系统可以有三个运行,两个等待。但是,对于单个CPU,我们不能同时运行三个进程,因此在CPU繁忙时准备就绪的进程将不得不等待,因此,这些进程不是独立的。使用排队论可以构建更精确的模型,但我们所做的多道程序设计让进程在CPU空闲时使用它,当然,即使上图中的真实曲线与图中所示的曲线略有不同,它仍然有效。

尽管上图中的模型思想简单,但它仍然可以用于对CPU性能进行具体的、尽管是近似的预测。例如,假设一台计算机有8GB内存,操作系统及其表占2GB,每个用户程序也占2GB。这些大小允许三个用户程序同时在内存中,平均I/O等待时间为80%时,CPU利用率(忽略操作系统开销)为1−0.831−0.83或约49%。再增加8GB内存,系统就可以从三路多道程序设计过渡到七路多道编程,从而将CPU利用率提高到79%。换句话说,额外的8GB将提高30%的吞吐量。

再增加8GB只会将CPU利用率从79%提高到91%,因此吞吐量只会再提高12%。使用这个模型,计算机的所有者可能会认为第一次增加内存是一项不错的投资,但第二次不是。

18.4.8.2 加载和链接

创建活动进程的第一步是将程序加载到主内存中并创建进程映像(下图),下下图描述了大多数系统的典型场景。应用程序由多个编译或组装的目标代码形式的模块组成,这些模块被链接以解析模块之间的任何引用,同时,解析对库例程的引用。库例程本身可以合并到程序中或作为共享代码引用,这些代码必须在运行时由操作系统提供。

加载函数。

一个加载和链接的场景。

  • 加载

在上图中,加载器从位置开始将加载模块放置在主存储器中。在加载程序时,必须满足寻址要求。一般来说,可以采取三种方法:

1、绝对加载

绝对加载绝对加载程序要求将给定的加载模块始终加载到主内存中的同一位置。因此,在呈现给加载器的加载模块中,所有地址引用都必须指向特定的或绝对的主内存地址。例如,如果上图中的x是位置1024,那么加载模块中指定给该内存区域的第一个字具有地址1024。

将特定地址值分配给程序内的内存引用可以由程序员完成,也可以在编译或汇编时完成。前一种方法有几个缺点,首先,每个程序员都必须知道将模块放入主存的预期分配策略,其次,如果对程序进行了任何修改,涉及到模块主体中的插入或删除,那么所有地址都必须更改。因此,最好允许程序中的内存引用以符号方式表示,然后在编译或汇编时解析这些符号引用,如下图所示,对指令或数据项的每个引用最初都用符号表示,在准备模块输入到绝对加载器时,汇编程序或编译器将把所有这些引用转换为特定地址(在本例中,对于从1024位置开始加载的模块),如下下图b所示。

2、可重定位加载

可重定位加载在加载之前将内存引用绑定到特定地址的缺点是,生成的加载模块只能放在主内存的一个区域中。然而,当许多程序共享主内存时,可能不希望提前决定将特定模块加载到内存的哪个区域。最好在加载时做出该决定,因此,我们需要一个可以位于主内存中任何位置的加载模块。

为了满足这个新的要求,汇编程序或编译器生成的不是实际的主存储器地址(绝对地址),而是与某个已知点(例如程序的开始)相关的地址。这种技术如下图c所示。加载模块的开头被分配了相对地址0,并且模块内的所有其他内存引用都是相对于模块的开始来表示的。

由于所有内存引用都以相对格式表示,所以加载器将模块放置在所需的位置就成为一项简单的任务。如果要从位置开始加载模块,则加载程序必须在将模块加载到内存中时简单地添加到每个内存引用中。为了协助该任务,加载模块必须包含告诉加载器地址引用在哪里以及如何解释地址引用的信息(通常相对于程序源,但也可能相对于程序中的其他点,例如当前位置)。这组信息由编译器或汇编程序准备,通常称为重定位字典(relocation dictionary)

3、动态运行时加载

重定位加载比较常见,相对于绝对加载,它具有明显的优势。然而,在多道程序设计环境中,即使是不依赖虚拟内存的环境,可重定位的加载方案依然不够。需要在主内存中交换进程映像,以最大限度地提高处理器的利用率。为了最大限度地提高主内存利用率,我们希望能够在不同的时间将进程映像交换回不同的位置。因此,一个程序一旦加载,就可以交换到磁盘上,然后在不同的位置重新交换。如果在初始加载时将内存引用绑定到绝对地址,是不可能的。

另一种方法是推迟绝对地址的计算,直到在运行时实际需要它。为此,加载模块被加载到主内存中,所有内存引用都以相对形式(上图c)。直到实际执行了一条指令,才计算出绝对地址。为了确保此功能不会降低性能,必须通过特殊的程序或硬件而不是软件来完成。

动态地址计算提供了完全的灵活性。程序可以加载到主存储器的任何区域,随后,程序的执行可以被中断,程序可以从主存储器中调出,稍后再调回另一个位置。

  • 链接

链接器的功能是将一组目标模块作为输入,并生成一个加载模块,该加载模块由一组集成的程序和数据模块组成,并传递给加载器。在每个对象模块中,可能存在对其他模块中位置的地址引用。每个这样的引用只能在未链接的对象模块中用符号表示。链接器创建一个加载模块,它是所有对象模块的连续连接。每个模块内引用必须从符号地址更改为对整个加载模块内某个位置的引用。例如,图7中的模块A包含模块B的过程调用。当这些模块在加载模块中组合时,对模块B的符号引用将更改为对加载模块中B入口点位置的特定引用。

链接编辑器此地址链接的性质将取决于要创建的加载模块的类型以及链接发生的时间(上表b)。通常情况下,如果需要可重新定位的负载模块,则通常按以下方式进行连接。每个编译或组装的对象模块都是用相对于对象模块开头的引用创建的,所有这些模块都被放在一个单独的可重定位加载模块中,其中包含相对于加载模块原点的所有引用,该模块可以用作可重定位加载或动态运行时加载的输入。

产生可重定位加载模块的链接器通常被称为链接编辑器。下图显示了链接编辑器功能。

与加载一样,可以延迟某些链接功能。术语动态链接用于指将某些外部模块的链接延迟到加载模块创建之后的做法,因此加载模块包含对其他程序的未解析引用,这些引用可以在加载时或运行时解析。

对于加载时动态链接,需要执行以下步骤。将要加载的加载模块(应用程序模块)读入存储器,对外部模块(目标模块)的任何引用都会导致加载器找到目标模块,加载它,并从应用程序模块的开头将引用更改为内存中的相对地址。与所谓的静态链接相比,动态链接有几个优点:

1、合并目标模块的更改或升级版本变得更容易,目标模块可以是操作系统实用程序或其他通用例程。对于静态链接,对这种支持模块的更改将需要重新链接整个应用程序模块,这不仅效率低下,而且在某些情况下可能是不可能的。例如,在个人计算机领域,大多数商业软件都是以加载模块的形式发布的:源码和对象版本不发布。

2、在动态链接文件中包含目标代码为自动代码共享铺平了道路。操作系统可以识别多个应用程序正在使用相同的目标代码,因为它加载并链接了该代码。它可以使用该信息加载目标代码的单个副本并将其链接到两个应用程序,而不必为每个应用程序加载一个副本。

3、独立软件开发人员更容易扩展广泛使用的操作系统(如Linux)的功能。开发人员可以提出一个对各种应用程序有用的新函数,并将其打包为动态链接模块。

对于运行时动态链接,一些链接被推迟到执行时。目标模块的外部引用保留在加载的程序中。当调用不存在的模块时,操作系统定位该模块,加载该模块,并将其链接到调用模块。这些模块通常是可共享的,在Windows环境中,这些称为动态链接库(DLL)。因此,如果一个进程已经在使用动态链接的共享模块,则该模块位于主内存中,新进程可以简单地链接到已加载的模块。

如果两个或多个进程共享一个DLL模块,但期望该模块的不同版本,则使用DLL可能会导致通常称为DLL地狱(DLL hell)的问题。例如,可能会重新安装应用程序或系统功能,并将较旧版本的DLL文件带入其中。

我们已经看到,动态加载允许整个加载模块到处移动,但是,模块的结构是静态的,在整个进程的执行过程中以及从一个执行到下一个执行都保持不变。但是,在某些情况下,无法在执行之前确定需要哪些对象模块,例如事务处理应用程序(如航空公司预订系统或银行应用程序),事务的性质决定了需要哪些程序模块,它们被适当地加载并与主程序链接。使用这种动态链接器的优点是,除非引用了程序单元,否则不必为程序单元分配内存。此功能用于支持分段系统。

一个额外的细化是可行的:应用程序不需要知道可能被调用的所有模块或入口点的名称。例如,可以编写绘图程序以与各种绘图仪配合使用,每个绘图仪都由不同的驱动程序包驱动。应用程序可以从另一个进程或在配置文件中查找当前安装在系统上的绘图仪的名称,这允许应用程序的用户安装在编写应用程序时不存在的新绘图仪。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值