2.1 进程与线程
2.1.1 进程的概念和特征
1、进程的概念
为了使参与并发执行的每个程序(含数据)都能独立地运行,必须为之配置一个专门的数据结构,称为进程控制块 (Process Control Block, PCB)。系统利用 PCB 来描述进程的基本情况和运行状态,进而控制和管理进程。相应地,由程序段、相关数据段和 PCB 三部分构成了进程实体(又称进程映像)。所谓创建进程,实质上是创建进程实体中的 PCB;而撤销进程,实质上是撤销进程的 PCB。值得注意的是,进程映像是静态的,进程则是动态的。PCB 是进程存在的唯一标志,当进程被创建时,操作系统为其创建 PCB,当进程结束时,会回收其 PCB。PCB 是给操作系统用的。程序段、数据段是给进程自己用的。
从不同的角度,进程可以有不同的定义,比较典型的定义有:
(1)进程是程序的一次执行过程。
(2)进程是一个程序及其数据在处理机上顺序执行时所发生的活动。
(3)进程是具有独立功能的程序在一个数据集合上运行的过程,它是系统进行资源分配和调度的一个独立单位。
引入进程实体的概念后,我们可以把传统操作系统中的进程定义为:“进程是进程实体的运行过程,是系统进行资源分配和调度的一个独立单位。” 一个进程被 “调度”,就是指操作系统决定让这个进程上 CPU 运行。
程序:是静态的,就是个存放在磁盘里的可执行文件,如:QQ.exe。
进程:是动态的,是程序的一次执行过程,如:可同时启动多次 QQ 程序。
同一个程序多次执行会对应多个进程。
同时挂三个 QQ 号,会对应三个 QQ 进程,它们的 PCB、数据段各不相同,但程序段的内容都是相同的(都是运行着相同的 QQ 程序)。
程序封闭性是指进程执行的结果只取决于进程本身,不受外界影响。也就是说,进程在执行过程中不管是不停顿地执行,还是走走停停,进程的执行速度都不会改变它的执行结果。失去封闭性后,不同速度下的执行结果不同。
进程创建需要占用系统内存来存放 PCB 的数据结构,所以一个系统能够创建的进程数量总是有限的,进程的最大数目取决于系统内存的大小。
2、进程的特征
进程是由多道程序的并发执行而引出的,它和程序是两个截然不同的概念。进程的基本特征是对比单个程序的顺序执行提出的,也是对进程管理提出的基本要求。
- 动态性。进程是程序的一次执行,它有着创建、活动、暂停、终止等过程,具有一定的生命周期,是动态地产生、变化和消亡的。动态性是进程最基本的特征。
- 并发性。指多个进程实体同存于内存中,能在一段时间内同时运行。引入进程的目的就是使进程能和其他进程并发执行。并发性是进程的重要特征,也是操作系统的重要特征。
- 独立性。指进程实体是一个能独立运行、独立获得资源和独立接受调度的基本单位。凡未建立 PCB 的程序,都不能作为一个独立的单位参与运行。
- 异步性。由于进程的相互制约,使得进程按各自独立的、不可预知的速度向前推进。异步性会导致执行结果的不可再现性,为此在操作系统中必须配置相应的进程同步机制。
2.1.2 进程的状态与转换
进程在其生命周期内,由于系统中各进程之间的相互制约及系统的运行环境的变化,使得进程的状态也在不断地发生变化。通常进程有以下 5 种状态,前 3 种是进程的基本状态。
- 运行态。进程正在处理机上运行。在单处理机中,每个时刻只有一个进程处于运行态。
- 就绪态。进程获得了除处理机外的一切所需资源,一旦得到处理机,便可立即运行。系统中处于就绪状态的进程可能有多个,通常将它们排成一个队列,称为就绪队列。
- 阻塞态,又称等待态。进程正在等待某一事件而暂停运行,如等待某资源为可用(不包括处理机)或等待输入/输出完成。即使处理机空闲,该进程也不能运行。系统通常将处于阻塞态的进程也排成一个队列,甚至根据阻塞原因的不同,设置多个阻塞队列。
- 创建态。进程正在被创建,尚未转到就绪态。创建进程需要多个步骤:首先申请一个空白 PCB,并向 PCB 中填写用于控制和管理进程的信息;然后为该进程分配运行时所必须的资源;最后把该进程转入就绪态并插入就绪队列。但是,如果进程所需的资源尚不能得到满足,如内存不足,则创建工作尚未完成,进程此时所处的状态称为创建态。
- 结束态。进程正从系统中消失,可能是进程正常结束或其他原因退出运行。进程需要结束运行时,系统首先将该进程置为结束态,然后进一步处理资源释放和回收等工作。
注意区别就绪态和等待态:就绪态是指进程仅缺少处理器,只要获得处理机资源就立即运行;而等待态是指进程需要其他资源(除了处理机)或等待某一事件。之所以把处理机和其他资源划分开,是因为在分时系统的时间片轮转机制中,每个进程分到的时间片是若干毫秒。也就是说,进程得到处理机的时间很短且非常频繁,进程在运行过程中实际上是频繁地转换到就绪态的;而其他资源(如外设)的使用和分配或某一事件的发生(如 I/O 操作的完成)对应的时间相对来说很长,进程转换到等待态的次数也相对较少。这样来看,就绪态和等待态是进程生命周期中两个完全不同的状态,显然需要加以区分。
![]()
就绪态
运行态:处于就绪态的进程被调度后,获得处理机资源(分派处理机时间片),于是进程由就绪态转换为运行态。
运行态
就绪态:处于运行态的进程在时间片用完后,不得不让出处理机,从而进程由运行态转换为就绪态。此外,在可剥夺的操作系统中,当有更高优先级的进程就绪时,调度程序将正在执行的进程转换为就绪态,让更高优先级的进程执行。
运行态
阻塞态:进程请求某一资源(如外设)的使用和分配或等待某一事件的发生(如 I/O 操作的完成)时,它就从运行态转换为阻塞态。进程以系统调用的形式请求操作系统提供服务,这是一种特殊的、由运行用户态程序调用操作系统内核过程的形式。
阻塞态
就绪态:进程等待的事件到来时,如 I/O 操作结束或中断结束时,中断处理程序必须把相应进程的状态由阻塞态转换为就绪态。
一个进程从运行态变成阻塞态是主动的行为,而从阻塞态变成就绪态是被动的行为,需要其他相关进程的协助。
系统发生死锁时有可能进程全部处于阻塞态,或无进程任务,CPU 空闲。
单 CPU 情况下,同一时刻只会有一个进程处于运行态;多核 CPU 情况下,可能有多个进程处于运行态。进程 PCB 中,会有一个变量 state 来表示进程的当前状态。
2.1.3 进程的组织
进程是一个独立的运行单位,也是操作系统进行资源分配和调度的基本单位。它由以下三部分组成,其中最核心的是进程控制块 (PCB)。
1、进程控制块
进程创建时,操作系统为它新建一个 PCB,该结构之后常驻内存,任意时刻都可以存取,并在进程结束时删除。PCB 是进程实体的一部分,是进程存在的唯一标志。
进程执行时,系统通过其 PCB 了解进程的现行状态信息,以便操作系统对其进行控制和管理;进程结束时,系统收回其 PCB,该进程随之消亡。
当操作系统欲调度某进程运行时,要从该进程的 PCB 中查出其现行状态及优先级;在调度到某进程后,要根据其 PCB 中所保存的处理机状态信息,设置该进程恢复运行的现场,并根据其 PCB 中的程序和数据的内存始址,找到其程序和数据;进程在运行过程中,当需要和与之合作的进程实现同步、通信或访问文件时,也需要访问 PCB;当进程由于某种原因而暂停运行时,又需将其断点的处理机环境保存在 PCB 中。可见,在进程的整个生命期中,系统总是通过 PCB 对进程进行控制的,亦即系统唯有通过进程的 PCB 才能感知到该进程的存在。
PCB 主要包括进程描述信息、进程控制和管理信息、资源分配清单和处理机相关信息等。各部分的主要说明如下:
- 进程描述信息。进程标识符:标志各个进程,每个进程都有一个唯一的标识号。用户标识符:进程归属的用户,用户标识符主要为共享和保护服务。
- 进程控制和管理信息。进程当前状态:描述进程的状态信息,作为处理机分配调度的依据。进程优先级:描述进程抢占处理机的优先级,优先级高的进程可优先获得处理机。
- 资源分配清单,用于说明有关内存地址空间或虚拟地址空间的状况,所打开文件的列表和所使用的输入/输出设备信息。
- 处理机相关信息,也称处理机的上下文,主要指处理机中各寄存器的值。当进程处于执行态时,处理机的许多信息都在寄存器中。当进程被切换时,处理机状态信息都必须保存在相应的 PCB 中,以便在该进程重新执行时,能从断点继续执行。
在一个系统中,通常存在着许多进程的 PCB,有的处于就绪态,有的处于阻塞态,而且阻塞的原因各不相同。为了方便进程的调度和管理,需要将各进程的 PCB 用适当的方法组织起来。目前,常用的组织方式有链接方式和索引方式两种。链接方式将同一状态的 PCB 链接成一个队列,不同状态对应不同的队列,也可把处于阻塞态的进程的 PCB,根据其阻塞原因的不同,排成多个阻塞队列。索引方式将同一状态的进程组织在一个索引表中,索引表的表项指向相应的 PCB,不同状态对应不同的索引表,如就绪索引表和阻塞索引表等。
2、程序段
程序段就是能被进程调度程序调度到 CPU 执行的程序代码段。注意,程序可被多个进程共享,即多个进程可以运行同一个程序。
3、数据段
一个进程的数据段,可以是进程对应的程序加工处理的原始数据,也可以是程序执行时产生的中间或最终结果。
2.1.4 进程控制
进程控制的主要功能是对系统中的所有进程实施有效的管理,它具有创建新进程、撤销已有进程、实现进程状态转换等功能。在操作系统中,一般把进程控制用的程序段称为原语,原语的特点是执行期间不允许中断,它是一个不可分割的基本单位,原语用关/开中断来实现。
1、进程的创建
允许一个进程创建另一个进程,此时创建者称为父进程,被创建的进程称为子进程。子进程可以继承父进程所拥有的资源。当子进程被撤销时,应将其从父进程那里获得的资源归还给父进程。此外,在撤销父进程时,通常也会同时撤销其所有的子进程。
在操作系统中,终端用户登录系统、作业调度、系统提供服务、用户程序的应用请求等都会引起进程的创建。操作系统创建一个新进程的过程如下(创建原语):
- 为新进程分配一个唯一的进程标识号,并申请一个空白 PCB (PCB 是有限的)。若 PCB 申请失败,则创建失败。
- 为进程分配其运行所需的资源,如内存、文件、I/O 设备和 CPU 时间等(在 PCB 中体现)。这些资源或从操作系统获得,或仅从其父进程获得。如果资源不足(如内存),则并不是创建失败,而是处于创建态,等待内存资源。
- 初始化 PCB,主要包括初始化标志信息、初始化处理机状态信息和初始化处理机控制信息,以及设置进程的优先级等。
- 若进程就绪队列能够接纳新进程,则将新进程插入就绪队列,等待被调度运行。
2、进程的终止
引起进程终止的事件主要有:① 正常结束,表示进程的任务己完成并准备退出运行。② 异常结束,表示进程在运行时,发生了某种异常事件,使程序无法继续运行,如存储区越界、保护错、非法指令、特权指令错、运行超时、算术运算错、I/O 故障等。③ 外界干预,指进程应外界的请求而终止运行,如操作员或操作系统干预、父进程请求和父进程终止。
操作系统终止进程的过程如下(终止原语):
- 根据被终止进程的标识符,检索出该进程的 PCB,从中读出该进程的状态。
- 若被终止进程处于执行状态,立即终止该进程的执行,将处理机资源分配给其他进程。
- 若该进程还有子孙进程,则应将其所有子孙进程终止。
- 将该进程所拥有的全部资源,或归还给其父进程,或归还给操作系统。
- 将该 PCB 从所在队列(链表)中删除。
3、进程的阻塞和唤醒
正在执行的进程,由于期待的某些事件末发生,如请求系统资源失败、等待某种操作的完成、新数据尚末到达或无新任务可做等,进程便通过调用阻塞原语 (Block),使自己由运行态变为阻塞态。可见,阻塞是进程自身的一种主动行为,也因此只有处于运行态的进程(获得 CPU ),才可能将其转为阻塞态。阻塞原语的执行过程如下:
- 找到将要被阻塞进程的标识号对应的 PCB。
- 若该进程为运行态,则保护其现场,将其状态转为阻塞态,停止运行。
- 把该 PCB 插入相应事件的等待队列,将处理机资源调度给其他就绪进程。
当被阻塞进程所期待的事件出现时,如它所启动的 I/O 操作已完成或其所期待的数据已到达,由有关进程(比如,释放该 I/O 设备的进程,或提供数据的进程)调用唤醒原语 (Wakeup),将等待该事件的进程唤醒。唤醒原语的执行过程如下:
- 在该事件的等待队列中找到相应进程的 PCB。
- 将其从等待队列中移出,并置其状态为就绪态。
- 把该 PCB 插入就绪队列,等待调度程序调度。
应当注意,Block 原语和 Wakeup 原语是一对作用刚好相反的原语,必须成对使用。如果在某进程中调用了 Block 原语,则必须在与之合作的或其他相关的进程中安排一条相应的 Wakeup原语,以便唤醒阻塞进程;否则,阻塞进程将会因不能被唤醒而永久地处于阻塞状态。
4、进程的切换
切换原语(运行态 —> 就绪态,就绪态 —> 运行态):
- 将运行环境信息存入 PCB
- PCB 移入相应队列
- 选择另一个进程执行,并更新其 PCB
- 根据 PCB 恢复新进程所需的运行环境
引起进程切换的事件:
- 当前进程时间片到
- 有更高优先级进程到达
- 当前进程主动阻塞
- 当前进程终止
2.1.5 进程的通信
进程是分配系统资源的单位(包括内存地址空间),因此各进程拥有的内存地址空间相互独立。为了保证安全,一个进程不能直接访问另一个进程的地址空间。 进程通信 (IPC) 是指进程之间的信息交换。I/O 操作是低级通信方式,高级通信方式是指以较高的效率传输大量数据的通信方式。高级通信方法主要有以下三类,数据库不能用于进程间通信。
1、共享存储
在通信的进程之间存在一块可直接访问的共享空间,通过对这片共享空间进行写/读操作实现进程之间的信息交换,如图所示。
在对共享空间进行写/读操作时,需要使用同步互斥工具(如 P 操作、V 操作),对共享空间的写/读进行控制。共享存储又分为两种:低级方式的共享是基于数据结构的共享,比如共享空间里只能放一个长度为 10 的数组,这种共享方式速度慢、限制多;高级方式的共享则是基于存储区的共享,操作系统在内存中划出一块共享存储区,数据的形式、存放位置都由通信进程控制,而不是操作系统,这种共享方式速度很快。操作系统只负责为通信进程提供可共享使用的存储空间和同步互斥工具,而数据交换则由用户自己安排读/写指令完成。
注意,进程空间一般都是独立的,进程运行期间一般不能访问其他进程的空间,想让两个进程共享空间,必须通过特殊的系统调用实现,而进程内的线程是自然共享进程空间的。
2、消息传递
在消息传递系统中,进程间的数据交换以格式化的消息 (Message) 为单位。若通信的进程之间不存在可直接访问的共享空间,则必须利用操作系统提供的消息传递方法实现进程通信。进程通过系统提供的发送消息和接收消息两个原语进行数据交换。这种方式隐藏了通信实现细节,使通信过程对用户透明,简化了通信程序的设计,是当前应用最广泛的进程间通信机制。
在微内核操作系统中,微内核与服务器之间的通信就采用了消息传递机制。由于该机制能很好地支持多处理机系统、分布式系统和计算机网络,因此也成为这些领域最主要的通信工具。
(1)直接通信方式。发送进程直接把消息发送给接收进程,并将它挂在接收进程的消息缓冲队列上,接收进程从消息缓冲队列中取得消息,如图所示。
(2)间接通信方式。发送进程把消息发送到某个中间实体,接收进程从中间实体取得消息。这种中间实体一般称为信箱。该通信方式广泛应用于计算机网络中。
3、管道通信
管道通信是消息传递的一种特殊方式。所谓 “管道”,是指用于连接一个读进程和一个写进程以实现它们之间的通信的一个共享文件,又名 pipe 文件。向管道(共享文件)提供输入的发送进程(即写进程),以字符流形式将大量的数据送入(写)管道;而接收管道输出的接收进程(即读进程)则从管道中接收(读)数据。为了协调双方的通信,管道机制必须提供以下三方面的协调能力:互斥、同步和确定对方的存在。
在 Linux 中,管道是一种使用非常频繁的通信机制。从本质上说,管道也是一种文件,但它又和一般的文件有所不同,管道可以克服使用文件进行通信的两个问题,具体表现如下:
(1)限制管道的大小。实际上,管道是一个固定大小的缓冲区。在 Linux 中,该缓冲区的大小为 4KB,这使得它的大小不像文件那样不加检验地增长。使用单个固定缓冲区也会带来问题,比如在写管道时可能变满,这种情况发生时,随后对管道的 write() 调用将默认地被阻塞,等待某些数据被读取,以便腾出足够的空间供 write() 调用写。
(2)读进程也可能工作得比写进程快。当所有当前进程数据己被读取时,管道变空。当这种情况发生时,一个随后的 read() 调用将默认地被阻塞,等待某些数据被写入,这解决了 read()调用返回文件结束的问题。
注意:从管道读数据是一次性操作,数据一旦被读取,就释放空间以便写更多数据。管道只能采用半双工通信,即某一时刻只能单向传输。要实现父子进程互动通信,需定义两个管道。
① 管道只能采用半双工通信,某一时间段内只能实现单向的传输。如果要实现双向同时通信,则需要设置两个管道。
② 各进程要互斥地访问管道(由操作系统实现)。
③ 当管道写满时,写进程将阻塞,直到读进程将管道中的数据取走,即可唤醒写进程。
④ 当管道读空时,读进程将阻塞,直到写进程往管道中写入数据,即可唤醒读进程。
⑤ 管道中的数据一旦被读出,就彻底消失。因此,当多个进程读同一个管道时,可能会错乱。对此,通常有两种解决方案:一种是一个管道允许多个写进程,不读进程(2014 年 408真题高教社官方答案);另一种是允许有多个写进程,多个读进程,但系统会让各个读进程轮流从管道中读数据(Linux 的方案)。
管道可以理解为共享存储的优化和发展,因为在共享存储中,若某进程要访问共享存储空间,则必须没有其他进程在该共享存储空间中进行写操作,否则访问行为就会被阻塞。而管道通信中,存储空间进化成了缓冲区,缓冲区只允许一边写入、另一边读出,因此只要缓冲区中有数据,进程就能从缓冲区中读出;写进程往管道写数据,即便管道没被写满,只要管道没空,读进程就可以从管道读数据;读进程从管道读数据,即便管道没被读空,只要管道没满,写进程就可以往管道写数据 。当然,这也决定了管道通信必然是半双工通信。
1、进程空间一般都是独立的,进程运行期间一般不能访问其他进程的空间,想让两个进程共享空间,必须通过特殊的系统调用实现,而进程内的线程是自然共享进程空间的。
2、每个进程包含独立的地址空间,进程各自的地址空间是私有的,只能执行自己地址空间中的程序,且只能访问自己地址空间中的数据,相互访问会导致指针的越界错误。因此,进程之间不能直接交换数据,但利用操作系统提供的共享文件、消息传递、共享存储区等进行通信。
2.1.6 线程和多线程模型
1、线程的基本概念
引入进程的目的是更好地使多道程序并发执行,提高资源利用率和系统吞吐量;而引入线程的目的则是减小程序在并发执行时所付出的时空开销,提高操作系统的并发性能。
1、有的进程可能需要 “同时” 做很多事,而传统的进程只能串行地执行一系列程序。为此,引入了 “线程”,来增加并发度。
2、引入线程后,线程成为了程序执行的最小单位。
3、线程是一个基本的 CPU 执行单元,也是程序执行流的最小单位。引入线程之后,不仅是进程之间可以并发,进程内的各线程之间也可以并发,从而进一步提升了系统的并发度,使得一个进程内也可以并发处理各种任务(如在 QQ 中视频、文字聊天、传文件等)。
线程最直接的理解就是 “轻量级进程”,它是一个基本的 CPU 执行单元,也是程序执行流的最小单元,由线程 ID、程序计数器、寄存器集合和堆栈组成。线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属—个进程的其他线程共享进程所拥有的全部资源。一个线程可以创建和撒销另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。线程也有就绪、阻塞和运行三种基本状态。
引入线程后,进程的内涵发生了改变,进程只作为除 CPU 外的系统资源的分配单元,而线程则作为处理机的分配单元。由于一个进程内部有多个线程,若线程的切换发生在同一个进程内部,则只需要很少的时空开销。下面从几个方面对线程和进程进行比较。
2、线程与进程的比较
(1)调度。在传统的操作系统中,拥有资源和独立调度的基本单位都是进程,每次调度都要进行上下文切换,开销较大。在引入线程的操作系统中,线程是独立调度的基本单位,而线程切换的代价远低于进程。在同一进程中,线程的切换不会引起进程切换。但从一个进程中的线程切换到另一个进程中的线程时,会引起进程切换。
(2)并发性。在引入线程的操作系统中,不仅进程之间可以并发执行,而且一个进程中的多个线程之间亦可并发执行,甚至不同进程中的线程也能并发执行,从而使操作系统具有更好的并发性,提高了系统资源的利用率和系统的吞吐量。
(3)拥有资源。进程是系统中拥有资源的基本单位,而线程不拥有系统资源(仅有一点必不可少、能保证独立运行的资源),但线程可以访问其隶属进程的系统资源,这主要表现在属于同一进程的所有线程都具有相同的地址空间。要知道,若线程也是拥有资源的单位,则切换线程就需要较大的时空开销,线程这个概念的提出就没有意义。
(4)独立性。每个进程都拥有独立的地址空间和资源,除了共享全局变量,不允许其他进程访问。某进程中的线程对其他进程不可见。同一进程中的不同线程是为了提高并发性及进行相互之间的合作而创建的,它们共享进程的地址空间和资源。
(5)系统开销。在创建或撤销进程时,系统都要为之分配或回收进程控制块 PCB 及其他资源,如内存空间、I/O 设备等。操作系统为此所付出的开销,明显大于创建或撤销线程时的开销。类似地,在进程切换时涉及进程上下文的切换,而线程切换时只需保存和设置少量寄存器内容,开销很小。此外,由于同一进程内的多个线程共享进程的地址空间,因此这些线程之间的同步与通信非常容易实现,甚至无须操作系统的干预。
(6)支持多处理机系统。对于传统单线程进程,不管有多少处理机,进程只能运行在一个处理机上。对于多线程进程,可以将进程中的多个线程分配到多个处理机上执行。
3、线程的属性
多线程操作系统中的进程己不再是一个基本的执行实体,但它仍具有与执行相关的状态。所谓进程处于 “执行” 状态,实际上是指该进程中的某线程正在执行。线程的主要属性如下:
(1)线程是一个轻型实体,它不拥有系统资源,但每个线程都应有一个唯一的标识符和一个线程控制块,线程控制块记录了线程执行的寄存器和栈等现场状态。
(2)不同的线程可以执行相同的程序,即同一个服务程序被不同的用户调用时,操作系统把它们创建成不同的线程。
(3)同一进程中的各个线程共享该进程所拥有的资源。
(4)线程是处理机的独立调度单位,多个线程是可以并发执行的。在单 CPU 的计算机系统中,各线程可交替地占用 CPU;在多 CPU 的计算机系统中,各线程可同时占用不同的 CPU,若各个 CPU 同时为一个进程内的各线程服务,则可缩短进程的处理时间。
(5)一个线程被创建后,便开始了它的生命周期,直至终止。线程在生命周期内会经历阻塞态、就绪态和运行态等各种状态变化。
为什么线程的提出有利于提高系统并发性?可以这样来理解:由于有了线程,线程切换时,有可能会发生进程切换,也有可能不发生进程切换,平均而言每次切换所需的开销就变小了,因此能够让更多的线程参与并发,而不会影响到响应时间等问题。
4、线程的状态与转换
与进程一样,各线程之间也存在共享资源和相互合作的制约关系,致使线程在运行时也具有间断性。相应地,线程在运行时也具有下面三种基本状态。
执行状态:线程己获得处理机而正在运行。
就绪状态:线程已具备各种执行条件,只需再获得 CPU 便可立即执行。
阻塞状态:线程在执行中因某事件受阻而处于暂停状态。
线程这三种基本状态之间的转换和进程基本状态之间的转换是一样的。
5、线程的组织与控制
(1)线程控制块
与进程类似,系统也为每个线程配置一个线程控制块 TCB,用于记录控制和管理线程的信息。线程控制块通常包括:① 线程标识符;② 一组寄存器,包括程序计数器、状态寄存器和通用寄存器;③ 线程运行状态,用于描达线程正处于何种状态;④ 优先级;⑤ 线程专有存储区,线程切换时用于保存现场等;⑥ 堆栈指针,用于过程调用时保存局部变量及返回地址等。
同一进程中的所有线程都完全共享进程的地址空间和全局变量。各个线程都可以访问进程地址空间的每个单元,所以一个线程可以读、写或甚至清除另一个线程的堆栈。
(2)线程的创建
线程也是具有生命期的,它由创建而产生,由调度而执行,由终止而消亡。相应地,在操作系统中就有用于创建线程和终止线程的两数(或系统调用)。
用户程序启动时,通常仅有一个称为 “初始化线程〞的线程正在执行,其主要功能是用于创建新线程。在创建新线程时,需要利用一个线程创建函数,并提供相应的参数,如指向线程主程序的入口指针、堆栈的大小、线程优先级等。线程创建两数执行完后,将返回一个线程标识符。
(3)线程的终止
当一个线程完成自己的任务后,或线程在运行中出现异常而要被强制终止时,由终止线程调用相应的函数执行终止操作。但是有些线程(主要是系统线程)一旦被建立,便一直运行而不会被终止。通常,线程被终止后并不立即释放它所占有的资源,只有当进程中的其他线程执行了分离函数后,被终止线程才与资源分离,此时的资源才能被其他线程利用。
被终止但尚末释放资源的线程仍可被其他线程调用,以使被终止线程重新恢复运行。
6、线程的实现方式
线程的实现可以分为两类:用户级线程 (User-Level Thread,ULT) 和内核级线程 (Kernel-Level Thread,KLT)。内核级线程又称内核支持的线程。
(1)用户级线程 (ULT)
在用户级线程中,有关线程管理(创建、撤销和切换等)的所有工作都由应用程序在用户空间中完成,内核意识不到线程的存在。应用程序可以通过使用线程库设计成多线程程序。通常,应用程序从单线程开始,在该线程中开始运行,在其运行的任何时刻,可以通过调用线程库中的派生例程创建一个在相同进程中运行的新线程。图 2.5(a) 说明了用户级线程的实现方式。
对于设置了用户级线程的系统,其调度仍是以进程为单位进行的,各个进程轮流执行一个时间片。假设进程 A 包含 1 个用户级线程,进程 B 包含 100 个用户级线程,这样,进程 A 中线程的运行时间将是进程 B 中各线程运行时间的 100 倍,因此对线程来说实质上是不公平的。
这种实现方式的优点如下:① 线程切换不需要转换到内核空间,节省了模式切换的开销。② 调度算法可以是进程专用的,不同的进程可根据自身的需要,对自己的线程选择不同的调度算法。③ 用户级线程的实现与操作系统平台无关,对线程管理的代码是属于用户程序的一部分。
这种实现方式的缺点如下:① 系统调用的阻塞问题,当线程执行一个系统调用时,不仅该线程被阻塞,而且进程内的所有线程都被阻塞。② 不能发挥多处理机的优势,内核每次分配给一个进程的仅有一个 CPU,因此进程中仅有一个线程能执行。
(2)内核级线程 (KLT)
在操作系统中,无论是系统进程还是用户进程,都是在操作系统内核的支持下运行的,与内核紧密相关。内核级线程同样也是在内核的支持下运行的,线程管理的所有工作也是在内核空间内实现的。内核空间也为每个内核级线程设置一个线程控制块,内核根据该控制块感知某线程的存在,并对其加以控制。图 2.5(b) 说明了内核级线程的实现方式。
这种实现方式的优点如下:① 能发挥多处理机的优势,内核能同时调度同一进程中的多个线程并行执行。② 如果进程中的一个线程被阻塞,内核可以调度该进程中的其他线程占用处理机,也可运行其他进程中的线程。③ 内核支持线程具有很小的数据结构和堆栈,线程切换比较快、开销小。④ 内核本身也可采用多线程技术,可以提高系统的执行速度和效率。
这种实现方式的缺点如下:同一进程中的线程切换,需要从用户态转到核心态进行,系统开销较大。这是因为用户进程的线程在用户态运行,而线程调度和管理是在内核实现的。
(3)组合方式
有些系统使用组合方式的多线程实现。在组合实现方式中,内核支持多个内核级线程的建立、调度和管理,同时允许用户程序建立、调度和管理用户级线程。一些内核级线程对应多个用户级线程,这是用户级线程通过时分多路复用内核级线程实现的。同一进程中的多个线程可以同时在多处理机上并行执行,且在阻塞一个线程时不需要将整个进程阻塞,所以组合方式能结合 KLT 和 ULT 的优点,并且克服各自的不足。图 2.5(c) 展示了这种组合实现方式。
在线程实现方式的介绍中,提到了通过线程库来创建和管理线程。线程库 (thread Hibrary)是为程序员提供创建和管理线程的 API。实现线程库主要的方法有如下两种:
① 在用户空间中提供一个没有内核支持的库。这种库的所有代码和数据结构都位于用户空间中。这意味着,调用库内的一个函数只导致用户空间中的一个本地函数的调用。
② 实现由操作系统直接支持的内核级的一个库。对于这种情况,库内的代码和数据结构位于内核空间。调用库中的一个 API 函数通常会导致对内核的系统调用。
目前使用的三种主要线程库是:POSIX Pthreads、Windows API、Java。Pthreads 作为 POSIX 标准的扩展,可以提供用户级或内核级的库。Windows 线程库是用于 Windows 操作系统的内核级线程库。Java 线程 API 允许线程在 Java 程序中直接创建和管理。然而,由于 JVM 实例通常运行在宿主操作系统之上,Java 线程 API 通常采用宿主系统的线程库来实现,因此在Windows 系统中 Java 线程通常采用 Windows API 来实现,在类 UNIX 系统中采用 Pthreads 来实现。
7、多线程模型
有些系统同时支特用户线程和内核线程,由于用户级线程和内核级线程连接方式的不同,从而形成了下面三种不同的多线程模型。
(1)多对一模型。将多个用户级线程映射到一个内核级线程,如图 2.6(a) 所示。这些用户线程一般属于一个进程,线程的调度和管理在用户空间完成。仅当用户线程需要访问内核时,才将其映射到一个内核级线程上,但是每次只允许一个线程进行映射。
优点:线程管理是在用户空间进行的,用户级线程的切换在用户空间即可完成,不需要切换到核心态,线程管理的系统开销小,因而效率比较高。
缺点:如果一个线程在访问内核时发生阻塞,则整个进程都会被阻塞;在任何时刻,只有一个线程能够访问内核,多个线程不能同时在多个处理机上运行。
(2)一对一模型。将每个用户级线程映射到一个内核级线程,如图 2.6(b) 所示。
优点:当一个线程被阻塞后,允许调度另一个线程运行,所以并发能力较强。
缺点:每创建一个用户线程,相应地就需要创建一个内核线程,开销较大。
(3)多对多模型。将 n 个用户线程映射到 m 个内核级线程上,要求 ,如图 2.6(c) 所示。内核级线程才是处理机分配的单位。
特点:既克服了多对一模型并发度不高的缺点,又克服了一对一模型的一个用户进程占用太多内核级线程而开销太大的缺点。此外,还拥有上述两种模型各自的优点。
2.2 处理机调度
2.2.1 调度的概念
1、调度的基本概念
处理机的调度是对处理机进行分配,即从就绪队列中按照一定的算法(公平、高效的原则)选择一个进程并将处理机分配给它运行,以实现进程并发地执行。处理机调度是多道程序操作系统的基础,是操作系统设计的核心问题。
2、调度的层次
一个作业提交开始直到完成,往往要经历以下三级调度:
(1)高级调度(作业调度)
按照一定的原则从外存上处于后备队列的作业中挑选一个(或多个),给它(们)分配内存、输入/输出设备等必要的资源,并建立相应的进程,以使它(们)获得竞争处理机的权利。简言之,作业调度就是内存与辅存之间的调度。对于每个作业只调入一次、调出一次。
多道批处理系统中大多配有作业调度,而其他系统中通常不需要配置作业调度。
(2)中级调度(内存调度)
引入中极调度的目的是提高内存利用率和系统吞吐量。为此,将那些暂时不能运行的进程调至外存等待,此时进程的状态称为挂起态。当它们已具备运行条件且内存又稍有空闲时,由中级调度来决定把外存上的那些已具备运行条件的就绪进程再重新调入内存,并修改其状态为就绪态,挂在就绪队列上等待。中级调度实际上是存储器管理中的对换功能。一个进程可能会被多次调出、调入内存,因此中级调度发生的频率要比高级调度更高。
(3)低级调度(进程调度)
按照某种算法从就绪队列中选取一个进程,将处理机分配给它。进程调度是最基本的一种调度,在各种操作系统中都必须配置这级调度。进程调度的频率很高,一般几十亳秒一次。
3、三级调度的联系
作业调度从外存的后备队列中选择一批作业进入内存,为它们建立进程,这些进程被送入就绪队列,进程调度从就绪队列中选出一个进程,并把其状态改为运行态,把 CPU 分配给它。中级调度是为了提高内存的利用率,系统将那些暂时不能运行的进程挂起来。
- 作业调度为进程活动做准备,进程调度使进程正常活动起来。
- 中级调度将暂时不能运行的进程挂起,中级调度处于作业调度和进程调度之间。
- 作业调度次数少,中级调度次数略多,进程调度频率最高。
- 进程调度是最基本的,不可或缺。
2.2.2 调度的目标
1、CPU 利用率
CPU 是计算机系统中最重要和昂贵的资源之一,所以应尽可能使 CPU 保持 “忙” 状态,使这一资源利用率最高。CPU 利用率的计算方法如下:
2、系统吞吐量
表示单位时间内 CPU 完成作业的数量。长作业需要消耗较长的处理机时间,因此会降低系统的吞吐量。而对于短作业,需要消耗的处理机时间较短,因此能提高系统的吞吐量。调度算法和方式的不同,也会对系统的吞吐量产生较大的影响。
3、周转时间
指从作业提交到作业完成所经历的时间,是作业等待、在就绪队列中排队、在处理机上运行及输入/输出操作所花费时间的总和。对于用户来说,更关心自己的单个作业的周转时间。周转时间的计算方式如下:
周转时间 = 作业完成时间 - 作业提交时间
平均周转时间是指多个作业周转时间的平均值,对于操作系统来说,更关心系统的整体表现,因此更关心所有作业周转时间的平均值:
平均周转时间 = (作业 1 的周转时间 +……+ 作业 n 的周转时间) / n
带权周转时间是指作业周转时间与作业实际运行时间的比值,带权周转时间必然 1,带权周转时间与周转时间都是越小越好:
4、等待时间
指进程处于等处理机的时间之和,等待时间越长,用户满意度越低。处理机调度算法实际上并不影响作业执行或输入/输出操作的时间,只影响作业在就绪队列中等待所花的时间。因此,衡量一个调度算法的优劣,常常只需简单地考察等待时间。对于作业来说,不仅要考虑建立进程后的等待时间,还要加上作业在外存后备队列中等待的时间。
5、响应时间
指从用户提交请求到系统首次产生响应所用的时间。在交互式系统中,周转时间不是最好的评价准则,一般采用响应时间作为衡量调度算法的重要准则之一。从用户角度来看,调度策略应尽量降低响应时间,使响应时间处在用户能接受的范围之内。
要想得到一个满足所有用户和系统要求的算法几乎是不可能的。设计调度程序,一方面要满足特定系统用户的要求(如某些实时和交互进程的快速响应要求),另一方面要考虑系统整体效率(如减少整个系统的进程平均周转时间),同时还要考虑调度算法的开销。
2.2.3 调度的实现
1、调度程序(调度器)
在操作系统中,用于调度和分派 CPU 的组件称为调度程序,它通常由排队器、分派器、上下文切换器组成。
(1)排队器
将系统中的所有就绪进程按照一定的策略排成一个或多个队列,以便于调度程序选择。每当有一个进程转变为就绪态时,排队器便将它插入到相应的就绪队列中。
(2)分派器
依据调度程序所选的进程,将其从就绪队列中取出,将 CPU 分配给新进程。
(3)上下文切换器
在对处理机进行切换时,会发生两对上下文的切换操作:第一对,将当前进程的上下文保存到其 PCB 中,再装入分派程序的上下文,以便分派程序运行;第二对,移出分派程序的上下文,将新选进程的 CPU 现场信息装入处理机的各个相应寄存器。
在上下文切换时,需要执行大量 load 和 store 指令,以保存寄存器的内容,因此会花费较多时间。现在已有硬件实现的方法来减少上下文切换时间。通常采用两组寄存器,其中一组供内核使用,一组供用户使用。这样,上下文切换时,只需改变指针,让其指向当前寄存器组即可。
2、调度的时机、切换与过程
调度程序是操作系统内核程序。请求调度的事件发生后,才可能运行调度程序,调度了新的就绪进程后,才会进行进程切换。理论上这三件事情应该顺序执行,但在实际的操作系统内核程序运行中,若某时刻发生了引起进程调度的因素,则不一定能马上进行调度与切换。
现代操作系统中,不能进行进程的调度与切换的情况有以下几种:
- 在处理中断的过程中。中断处理过程复杂,在实现上很难做到进程切换,而且中断处理是系统工作的一部分,逻辑上不属于某一进程,不应被剥夺处理机资源。
- 进程在操作系统内核临界区中。进入临界区后,需要独占式地访问,理论上必须加锁,以防止其他并行进程进入,在解锁前不应切换到其他进程,以加快临界区的释放。
- 其他需要完全屏蔽中断的原子操作过程中。如加锁、解锁、中断现场保护、恢复等原子操作。在原子过程中,连中断都要屏蔽,更不应该进行进程调度与切换。
- 若在上述过程中发生了引起调度的条件,则不能马上进行调度和切换,应置系统的请求调度标志,直到上述过程结束后才进行相应的调度与切换。
应该进行进程调度与切换的情况如下:
- 发生引起调度条件且当前进程无法继续运行下去时,可以马上进行调度与切换。若操作系统只在这种情况下进行进程调度,则是非剥夺调度。
- 中断处理结束或自陷处理结束后,返回被中断进程的用户态程序执行现场前,若置上请求调度标志,即可马上进行进程调度与切换。若操作系统支持这种情况下的运行调度程序,则实现了剥夺方式的调度。
进程切换往往在调度完成后立刻发生,它要求保存原进程当前断点的现场信息,恢复被调度进程的现场信息。现场切换时,操作系统内核将原进程的现场信息推入当前进程的内核堆栈来保存它们,并更新堆栈指针。内核完成从新进程的内核栈中装入新进程的现场信息、更新当前运行进程空间指针、重设 PC 寄存器等相关工作之后,开始运行新的进程。
临界资源:一个时间段内只允许一个进程使用的资源。各进程需要互斥地访问临界资源。
临界区:访问临界资源的那段代码。
内核程序临界区一般是用来访问某种内核数据结构的,比如进程的就绪队列(由各就绪进程的 PCB 组成)。
内核程序临界区访问的临界资源如果不尽快释放的话,极有可能影响到操作系统内核的其他管理工作。因此在访问内核程序临界区期间不能进行调度与切换。
普通临界区访问的临界资源不会直接影响操作系统内核的管理工作。因此在访问普通临界区时可以进行调度与切换。
3、进程调度方式
所谓进程调度方式,是指当某个进程正在处理机上执行时,若有某个更为重要或紧迫的进程需要处理,即有优先权更高的进程进入就绪队列,此时分配处理机通常有以下两种进程调度方式:
(1)非抢占调度方式,又称非剥夺方式。是指当一个进程正在处理机上执行时,即使有某个更为重要或紧迫的进程进入就绪队列,仍然让正在执行的进程继续执行,直到该进程运行完成或发生某种事件而进入阻塞态时,才把处理机分配给其他进程。
非抢占调度方式的优点是实现简单、系统开销小,适用于大多数的批处理系统,但它不能用于分时系统和大多数的实时系统。
(2)抢占调度方式,又称剥夺方式。是指当一个进程正在处理机上执行时,若有某个更为重要或紧迫的进程需要
0使用处理机,则允许调度程序根据某种原则去暂停正在执行的进程,将处理机分配给这个更为重要或紧迫的进程。
抢占调度方式对提高系统吞吐率和响应效率都有明显的好处。但 “抢占” 不是一种任意性行为,必须遵循一定的原则,主要有优先权、短进程优先和时间片原则等。
4、闲逛进程
在进程切换时,如果系统中没有就绪进程,就会调度闲逛进程 (idle) 运行,如果没有其他进程就绪,该进程就一直运行,并在执行过程中测试中断。闲逛进程的优先级最低,没有就绪进程时才会运行闲逛进程,只要有进程就绪,就会立即让出处理机。
闲逛进程不需要 CPU 之外的资源,它不会被阻塞。
5、两种线程的调度
(1)用户级线程调度。由于内核并不知道线程的存在,所以内核还是和以前一样,选择一个进程,并给予时间控制。由进程中的调度程序决定哪个线程运行。
(2)内核级线程调度。内核选择一个特定线程运行,通常不用考虑该线程属于哪个进程。对被选择的线程赋予一个时间片,如果超过了时间片,就会强制挂起该线程。
用户级线程的线程切换在同一进程中进行,仅需少量的机器指令;内核级线程的线程切换需要完整的上下文切换、修改内存映像、使高速缓存失效,这就导致了若干数量级的延迟。
2.2.4 典型的调度算法
1、先来先服务 (FCFS) 调度算法
FCFS 调度算法是一种最简单的调度算法,它既可用于作业调度,又可用于进程调度。在作业调度中,算法每次从后备作业队列中选择最先进入该队列的一个或几个作业,将它们调入内存,分配必要的资源,创建进程并放入就绪队列。
在进程调度中,FCFS 调度算法每次从就绪队列中选择最先进入该队列的进程,将处理机分配给它,使之投入运行,直到运行完成或因某种原因而阻塞时才释放处理机。
FCFS 调度算法属于不可剥夺算法。从表面上看,它对所有作业都是公平的,但若一个长作业先到达系统,就会使后面的许多短作业等待很长时间,因此它不能作为分时系统和实时系统的主要调度策略。但它常被结合在其他调度策略中使用。例如,在使用优先级作为调度策略的系统中,往往对多个具有相同优先级的进程按 FCFS 原则处理。
FCFS 调度算法的特点是算法简单,但效率低;对长作业比较有利,但对短作业不利(相对SJF 和高响应比);有利于 CPU 繁忙型作业,而不利于 I/O 繁忙型作业,不会导致饥饿。
2、短作业优先 (SJF) 调度算法
短作业(进程)优先调度算法是指对短作业(进程)优先调度的算法。短作业优先 (SJF) 调度算法从后备队列中选择一个或若干估计运行时间最短的作业,将它们调入内存运行;短进程优先 (SPF) 调度算法从就绪队列中选择一个估计运行时间最短的进程,将处理机分配给它,使之立即执行,直到完成或发生某事件而阻塞时,才释放处理机。SJF 和 SPF 是非抢占式的算法,但是也有抢占式的版本——最短剩余时间优先算法 (SRTN)。如果题目中未特别说明,所提到的 “短作业/进程优先算法” 默认是非抢占式的。
SJF 调度算法也存在不容忽视的缺点:
(1)该算法对长作业不利,由表 2.2 和表 2.3 可知,SJP 调度算法中长作业的周转时间会增加。更严重的是,若有一长作业进入系统的后备队列,由于调度程序总是优先调度那些(即使是后进来的)短作业,将导致长作业长期不被调度(“饥饿”现象,注意区分 “死锁”,后者是系统环形等待,前者是调度策略问题)。
(2)该算法完全末考虑作业的紧迫程度,因而不能保证紧迫性作业会被及时处理。
(3)由于作业的长短是根据用户所提供的估计执行时间而定的,而用户又可能会有意或无意地缩短其作业的估计运行时间,致使该算法不一定能真正做到短作业优先调度。
注意,在所有进程都几乎同时到达时,SJP 调度算法的平均等待时间、平均周转时间最少。抢占式的短作业/进程优先调度算法(最短剩余时间优先,SRNT 算法)的平均等待时间、平均周转时间最少。
3、优先级调度算法
优先级调度算法既可用于作业调度,又可用于进程调度。该算法中的优先级用于描述作业的紧迫程度。在作业调度中,优先级调度算法每次从后备作业队列中选择优先级最高的一个或几个作业,将它们调入内存,分配必要的资源,创建进程并放入就绪队列。在进程调度中,优先级调度算法每次从就绪队列中选择优先级最高的进程,将处理机分配给它,使之投入运行。
根据新的更高优先级进程能否抢占正在执行的进程,可将该调度算法分为如下两种:
(1)非抢占式优先级调度算法。当一个进程正在处理机上运行时,即使有某个优先级更高的进程进入就绪队列,仍让正在运行的进程继续运行,直到由于其自身的原因而让出处理机时(任务完成或等待事件),才把处理机分配给就绪队列中优先级最高的进程。
(2)抢占式优先级调度算法。当一个进程正在处理机上运行时,若有某个优先级更高的进程进入就绪队列,则立即暂停正在运行的进程,将处理机分配给优先级更高的进程。
而根据进程创建后其优先级是否可以改变,可以将进程优先级分为以下两种:
(1)静态优先级。优先级是在创建进程时确定的,且在进程的整个运行期间保持不变。确定静态优先级的主要依据有进程类型、进程对资源的要求、用户要求。
(2)动态优先级。在进程运行过程中,根据进程情况的变化动态调整优先级。动态调整优先级的主要依据有进程占有 CPU 时间的长短、就绪进程等待 CPU 时间的长短。
一般来说,进程优先级的设置可以参照以下原则:
(1)系统进程 > 用户进程。系统进程作为系统的管理者,理应拥有更高的优先级。
(2)交互型进程 > 非交互型进程(或前台进程 > 后台进程)。大家平时在使用手机时,在前台运行的正在和你交互的进程应该更快速地响应你,因此自然需要被优先处理。
(3)I/O 型进程 > 计算型进程。所谓 I/O 型进程,是指那些会频繁使用 I/O 设备的进程,而计算型进程是那些频繁使用 CPU 的进程(很少使用 I/O 设备)。我们知道,I/O 设备(如打印机)的处理速度要比 CPU 慢得多,因此若将 I/O 型进程的优先级设置得更高,就更有可能让 I/O 设备尽早开始工作,进而提升系统的整体效率。
优点:用优先级区分紧急程度、重要程度,适用于实时操作系统。可灵活地调整对各种作业/进程的偏好程度。缺点:若源源不断地有高优先级进程到来,则可能导致饥饿
4、高响应比优先 (HRRN) 调度算法
高响应比优先调度算法主要用于作业调度,是对 FCFS 调度算法和 SJF 调度算法的一种综合平衡,同时考虑了每个作业的等待时间和估计的运行时间。在每次进行作业调度时,先计算后备作业队列中每个作业的响应比,从中选出响应比最高的作业投入运行,是非抢占式算法,只有当前运行的作业/进程主动放弃处理机时,才需要调度,才需要计算响应比。
响应比的变化规律可描述为:
根据公式可知:
① 作业的等待时间相同时,要求服务时间越短,响应比越高,有利于短作业,因而类似于SJF。
② 要求服务时间相同时,作业的响应比由其等待时间决定,等待时间越长,其响应比越高,因而类似于 FCFS。
③ 对于长作业,作业的响应比可以随等待时间的增加而提高,当其等待时间足够长时,也可获得处理机,克服了 “饥饿” 现象。
FCFS、SJF/SPF、HRRN 这几种算法主要关心对用户的公平性、平均周转时间、平均等待时间等评价系统整体性能的指标,但是不关心 “响应时间”,也并不区分任务的紧急程度,因此对于用户来说,交互性很糟糕。因此这三种算法一般适合用于早期的批处理系统,当然,FCFS 算法也常结合其他的算法使用,在现在也扮演着很重要的角色。
5、时间片轮转调度算法 (RR)
时间片轮转调度算法主要适用于分时系统。在这种算法中,系统将所有就绪进程按 FCFS 策略排成一个就绪队列,调度程序总是选择就绪队列中的第一个进程执行,但仅能运行一个时间片,如 50ms。在使用完一个时间片后,即使进程并末运行完成,它也必须释放出(被剥夺)处理机给下一个就绪进程,而被剥夺的进程返回到就绪队列的末尾重新排队,等候再次运行。用于进程调度(只有作业放入内存建立了相应的进程后,才能被分配处理机时间片)。
在时间片轮转调度算法中,时间片的大小对系统性能的影响很大。若时间片足够大,以至于所有进程都能在一个时间片内执行完毕,则时间片轮转调度算法就退化为先来先服务调度算法。若时间片很小,则处理机将在进程间过于频繁地切换,使处理机的开销增大,而真正用于运行用户进程的时间将减少,不区分任务的紧急程度。因此,时间片的大小应选择适当,时间片的长短通常由以下因素确定:系统的响应时间、就绪队列中的进程数目和系统的处理能力。
若进程未能在时间片内运行完,将被强行剥夺处理机使用权,因此时间片轮转调度算法属于抢占式的算法。由时钟装置发出时钟中断来通知 CPU 时间片已到。
6、多级队列调度算法
前述的各种调度算法,由于系统中仅设置一个进程的就绪队列,即调度算法是固定且单一的,无法满足系统中不同用户对进程调度策略的不同要求。在多处理机系统中,这种单一调度策略实现机制的缺点更为突出,多级队列调度算法能在一定程度上弥补这一缺点。
该算法在系统中设置多个就绪队列,将不同类型或性质的进程固定分配到不同的就绪队列。每个队列可实施不同的调度算法,因此,系统针对不同用户进程的需求,很容易提供多种调度策略。同一队列中的进程可以设置不同的优先级,不同的队列本身也可以设置不同的优先级。在多处理机系统中,可以很方便为每个处理机设置一个单独的就绪队列,每个处理机可实施各自不同的调度策略,这样就能根据用户需求将多个线程分配到一个或多个处理机上运行。
7、多级反馈队列调度算法(融合了前几种算法的优点)
多级反馈队列调度算法是时间片轮转调度算法和优先级调度算法的综合与发展,如图所示。通过动态调整进程优先级和时间片大小,多级反馈队列调度算法可以兼顾多方面的系统目标。例如,为提高系统吞吐量和缩短平均周转时间而照顾短进程;为获得较好的 I/O 设备利用率和缩短响应时间而照顾 I/O 型进程;同时,也不必事先估计进程的执行时间。
多级反馈队列调度算法的实现思想如下:
(1)设置多个就绪队列,并为每个队列赋子不同的优先级。第 1 级队列的优先级最高,第 2级队列的优先级次之,其余队列的优先级逐个降低。
(2)赋予各个队列的进程运行时间片的大小各不相同。在优先级越高的队列中,每个进程的时间片就越小。例如,第 i+1 级队列的时间片要比第 i 级队列的时间片长 1 倍。
(3)每个队列都采用 FCFS 算法。当新进程进入内存后,首先将它放入第 1 级队列的末尾,按 FCFS 原则等待调度。当轮到该进程执行时,如它能在该时间片内完成,便可撤离系统。若它在一个时间片结束时尚未完成,调度程序将其转入第 2 级队列的末尾等待调度;若它在第 2级队列中运行一个时间片后仍末完成,再将它放入第 3 级队列,依此类推。当进程最后被降到第n 级队列后,在第 n 级队列中便采用时间片轮转方式运行。
(4)按队列优先级调度。仅当第 1 级队列为空时,才调度第 2 级队列中的进程运行;仅当第 1~i-1 级队列均为空时,才会调度第 i 级队列中的进程运行。若处理机正在执行第 i 级队列中的某进程时,又有新进程进入任一优先级较高的队列,此时须立即把正在运行的进程放回到第 i 级队列的末尾,而把处理机分配给新到的高优先级进程。
多级反馈队列的优势有以下几点:
- 终端型作业用户:短作业优先。
- 短批处理作业用户:周转时间较短。
- 长批处理作业用户:经过前面几个队列得到部分执行,不会长期得不到处理。
比起早期的批处理操作系统来说,由于计算机造价大幅降低,因此之后出现的交互式操作系统(包括分时操作系统、实时操作系统等)更注重系统的响应时间、公平性、平衡性等指标。而这几种算法恰好也能较好地满足交互式系统的需求。因此这三种算法适合用于交互式系统。(比如 UNIX 使用的就是多级反馈队列调度算法)。
2.2.5 进程切换
对于通常的进程而言,其创建、撤销及要求由系统设备完成的 I/O 操作,都是利用系统调用而进入内核,再由内核中的相应处理程序予以完成的。进程切换同样是在内核的支持下实现的,因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的。
(1)上下文切换
切换 CPU 到另一个进程需要保存当前进程状态并恢复另一个进程的状态,这个任务称为上下文切换。上下文是指某一时刻 CPU 寄存器和程序计数器的内容。进行上下文切换时,内核会将旧进程状态保存在其 PCB 中,然后加载经调度而要执行的新进程的上下文。
上下文切换实质上是指处理机从一个进程的运行转到另一个进程上运行,在这个过程中,进程的运行环境产生了实质性的变化。上下文切换的流程如下:
- 挂起一个进程,保存 CPU 上下文,包括程序计数器和其他寄存器。
- 更新 PCB 信息。
- 把进程的 PCB 移入相应的队列,如就绪、在某事件阻塞等队列。
- 选择另一个进程执行,并更新其 PCB。
- 跳转到新进程 PCB 中的程序计数器所指向的位置执行。
- 恢复处理机上下文。
(2)上下文切换的消耗
上下文切换通常是计算密集型的,即它需要相当可观的 CPU 时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间,所以上下文切换对系统来说意味着消耗大量的 CPU 时间。有些处理器提供多个寄存器组,这样,上下文切换就只需要简单改变当前寄存器组的指针。
(3)上下文切换与模式切换
模式切换与上下文切换是不同的,模式切换时,CPU 逻辑上可能还在执行同一进程。用户进程最开始都运行在用户态,若进程因中断或异常进入核心态运行,执行完后又回到用户态刚被中断的进程运行。用户态和内核态之间的切换称为模式切换,而不是上下文切换,因为没有改变当前的进程。上下文切换只能发生在内核态,它是多任务操作系统中的一个必需的特性。
注意:调度和切换的区别。调度是指决定资源分配给哪个进程的行为,是一种决策行为;切换是指实际分配的行为,是执行行为。一般来说,先有资源的调度,然后才有进程的切换。
“狭义的进程调度” 与 “进程切换” 的区别:
狭义的进程调度指的是从就绪队列中选中一个要运行的进程。(这个进程可以是刚刚被暂停执行的进程,也可能是另一个进程,后一种情况就需要进程切换)。
进程切换是指一个进程让出处理机,由另一个进程占用处理机的过程。
广义的进程调度包含了选择一个进程和进程切换两个步骤。
进程切换的过程主要完成了:
1、对原来运行进程各种数据的保存。
2.、对新的进程各种数据的恢复(如:程序计数器、程序状态字、各种数据寄存器等处理机现场信息,这些信息一般保存在进程控制块)。注意:进程切换是有代价的,因此如果过于频繁的进行进程调度、切换,必然会使整个系统的效率降低,使系统大部分时间都花在了进程切换上,而真正用于执行进程的时间减少。
2.3 同步与互斥
2.3.1 同步与互斥的基本概念
在多道程序环境下,进程是并发执行的,不同进程之间存在着不同的相互制约关系。为了协调进程之间的相互制约关系,引入了进程同步的概念。下面举一个简单的例子来帮大家理解这个概念。例如,让系统计算 1+2×3,假设系统产生两个进程:一个是加法进程,一个是乘法进程。要让计算结果是正确的,一定要让加法进程发生在乘法进程之后,但实际上操作系统具有异步性,若不加以制约,加法进程发生在乘法进程之前是绝对有可能的,因此要制定一定的机制去约束加法进程,让它在乘法进程完成之后才发生,而这种机制就是本节要讨论的内容。
1、临界资源
虽然多个进程可以共享系统中的各种资源,但其中许多资源一次只能为一个进程所用,我们将一次仅允许一个进程使用的资源称为临界资源。许多物理设备都属于临界资源,如打印机等。此外,还有许多变量、数据等都可以被若干进程共享,也属于临界资源。
对临界资源的访问,必须互斥地进行,在每个进程中,访问临界资源的那段代码称为临界区。为了保证临界资源的正确使用,可把临界资源的访问过程分成 4 个部分:
- 进入区。为了进入临界区使用临界资源,在进入区要检查可否进入临界区,若能进入临界区,则应设置正在访问临界区的标志,以阻止其他进程同时进入临界区。
- 临界区。进程中访问临界资源的那段代码,又称临界段。
- 退出区。将正在访问临界区的标志清除。
- 剩余区。代码中的其余部分。
注意:
临界区是进程中访问临界资源的代码段。
进入区和退出区是负责实现互斥的代码段。
临界区也可称为 “临界段”。
do{
entry section; // 进入区
critical section; // 临界区
exit section; // 退出区
remainder section; // 剩余区
} while(true)
2、同步
同步亦称直接制约关系,是指为完成某种任务而建立的两个或多个进程,这些进程因为需要在某些位置上协调它们的工作次序而等待、传递信息所产生的制约关系。进程间的直接制约关系源于它们之间的相互合作。
例如,输入进程 A 通过单缓冲向进程 B 提供数据。当该缓冲区空时,进程 B 不能获得所需数据而阻塞,一旦进程 A 将数据送入缓冲区,进程 B 就被唤醒。反之,当缓冲区满时,进程 A 被阻塞,仅当进程 B 取走缓冲数据时,才唤醒进程A。
3、互斥
互斥也称间接制约关系。当一个进程进入临界区使用临界资源时,另一个进程必须等待,当占用临界资源的进程退出临界区后,另一进程才允许去访问此临界资源。
例如,在仅有一台打印机的系统中,有两个进程 A 和进程 B,若进程 A 需要打印时,系统己将打印机分配给进程 B,则进程 A 必须阻塞。一旦进程 B 将打印机释放,系统便将进程 A 唤醒,并将其由阻塞态变为就绪态。
为禁止两个进程同时进入临界区,同步机制应遵循以下准则:
- 空闲让进。临界区空闲时,可以允许一个请求进入临界区的进程立即进入临界区。
- 忙则等待。当己有进程进入临界区时,其他试图进入临界区的进程必须等待。
- 有限等待。对请求访问的进程,应保证能在有限时间内进入临界区。
- 让权等待。当进程不能进入临界区时,应立即释放处理器,防止进程忙等待。
2.3.2 实现临界区互斥的基本方法
1、软件实现方法
在进入区设置并检查一些标志来标明是否有进程在临界区中,若已有进程在临界区,则在进入区通过循环检查进行等待,进程离开临界区后则在退出区修改标志。
(1)算法一:单标志法。该算法设置一个公用整型变量 turn,用于指示被允许进入临界区的进程编号,即若 turn=0,则允许 进程进入临界区。该算法可确保每次只允许一个进程进入临界区。但两个进程必须交替进入临界区,若某个进程不再进入临界区,则另一个进程也将无法进入临界区(违背 “空闲让进”)。这样很容易造成资源利用不充分。若
顺利进入临界区并从临界区离开,则此时临界区是空闲的,但
并没有进入临界区的打算,turn = 1 一直成立,
就无法再次进入临界区(一直被 while 死循环困住)。
(2)算法二:双标志法先检查。该算法的基本思想是在每个进程访问临界区资源之前,先查看临界资源是否正被访问,若正被访问,该进程需等待;否则,进程才进入自己的临界区。为此,设置一个数据 flag[i],如第 i 个元素值为 FALSE,表示 进程未进入临界区,值为 TRUE,表示
进程进入临界区。
若按照 ①②③④....的顺序执行, 和
将会同时访问临界区。因此,双标志先检查法的主要问题是:违反 “忙则等待” 原则。原因在于,进入区的 “检查” 和 “上锁” 两个处理不是一气呵成的。“检查” 后,“上锁” 前可能发生进程切换。
(3)算法三:双标志法后检查。算法二先检测对方的进程状态标志,再置自己的标志,由于在检测和放置中可插入另一个进程到达时的检测操作,会造成两个进程在分别检测后同时进入临界区。为此,算法三先将自己的标志设置为 TRUE,再检测对方的状态标志,若对方标志为 TRUE,则进程等待;否则进入临界区。
双标志后检查法虽然解决了 “忙则等待” 的问题,但是又违背了 “空闲让进” 和 “有限等待” 原则,两个进程几乎同时都想进入临界区时,它们分别将自己的标志值 flag 设置为 TRUE,并且同时检测对方的状态(执行 while 语句),发现对方也要进入临界区时,双方互相谦让,结果谁也进不了临界区,从而导致 “饥饿” 现象。
(4)算法四:Peterson's Algorithm。为了防止两个进程为进入临界区而无限期等待,又设置了变量 turn,每个进程在先设置自己的标志后再设置 turn 标志。这时,再同时检测另一个进程状态标志和允许进入标志,以便保证两个进程同时要求进入临界区时,只允许一个进程进入临界区。
具体如下:考虑进程 ,一旦设置 flag[i]=true,就表示它想要进入临界区,同时 turn=j,此时若进程
已在临界区中,符合进程
中的 while 循环条件,则
不能进入临界区。若
不想要进入临界区,即 flag[j]=false,循环条件不符合,则
可以顺利进入,反之亦然。本算法的基本思想是算法一和算法三的结合。利用 flag 解决临界资源的互斥访问,而利用 turn 解决 “饥饿” 现象。Peterson 算法用软件方法解决了进程互斥问题,遵循了空闲让进、忙则等待、有限等待三个原则,但是依然未遵循让权等待的原则。
2、硬件实现方法
计算机提供了特殊的硬件指令,允许对一个字中的内容进行检测和修正,或对两个字的内容进行交换等。通过硬件支持实现临界段问题的方法称为低级方法,或称元方法。
(1)中断屏蔽方法
当一个进程正在执行它的临界区代码时,防止其他进程进入其临界区的最简方法是关中断。因为 CPU 只在发生中断时引起进程切换,因此屏蔽中断能够保证当前运行的进程让临界区代码顺利地执行完,进而保证互斥的正确实现,然后执行开中断。其典型模式为:
这种方法限制了处理机交替执行程序的能力,因此执行的效率会明显降低。对内核来说,在它执行更新变量或列表的几条指令期间,关中断是很方便的,但将关中断的权力交给用户则很不明智,若一个进程关中断后不再开中断,则系统可能会因此终止。优点:简单、高效;缺点:不适用于多处理机,只适用于操作系统内核进程,不适用于用户进程(因为开/关中断指令只能运行在内核态,这组指令如果能让用户随意使用会很危险)。
(2)硬件指令方法
TestAndset (TS/TSL) 指令:这条指令是原子操作,即执行该代码时不允许被中断。其功能是读出指定标志后把该标志设置为真。指令的功能描述如下:
可以为每个临界资源设置一个共享布尔变量 lock,表示资源的两种状态:true 表示正被占用,初值为 false。进程在进入临界区之前,利用 TestAndSet 检查标志 lock,若无进程在临界区,则其值为 false,可以进入,关闭临界资源,把 lock 置为 true,使任何进程都不能进入临界区;若有进程在临界区,则循环检查,直到进程退出。利用该指令实现互斥的过程描述如下:
优点:实现简单,无需像软件实现方法那样严格检查是否会有逻辑漏洞,适用于多处理机环境;缺点:不满足 “让权等待” 原则,暂时无法进入临界区的进程会占用 CPU 并循环执行 TSL 指令,从而导致 “忙等”。
Swap (XCHG) 指令:该指令的功能是交换两个字(字节)的内容。其功能描述如下:
注意:以上对 TestAndSet 和 Swap 指令的描述仅是功能实现,而并非软件实现的定义。事实上,它们是由硬件逻辑直接实现的,不会被中断。
用 Swap 指令可以简单有效地实现互斥,为每个临界资源设置一个共享布尔变量 lock,初值为 false;在每个进程中再设置一个局部布尔变量 key,用于与 lock 交换信息。在进入临界区 前,先利用 Swap 指令交换 lock 与 key 的内容,然后检查 key 的状态;有进程在临界区时,重复交换和检查过程,直到进程退出。其处理过程描述如下:
硬件方法的优点:适用于任意数目的进程,而不管是单处理机还是多处理机;简单、容易验证其正确性。可以支持进程内有多个临界区,只需为每个临界区设立一个布尔变量。
硬件方法的缺点:进程等待进入临界区时要耗费处理机时间,不能实现让权等待。从等待进程中随机选择一个进入临界区,有的进程可能一直选不上,不满足 “让权等待” 原则,从而导致 “饥饿” 现象。
2.3.3 互斥锁
解决临界区最简单的工具就是互斥锁(mutex lock)。一个进程在进入临界区时应获得锁;在退出临界区时释放锁。函数 acquire() 获得锁,而函数 release() 释放锁。
每个互斥锁有一个布尔变量 available,表示锁是否可用。如果锁是可用的,调用 acqiure()会成功,且锁不再可用。当一个进程试图获取不可用的锁时,会被阻塞,直到锁被释放。
acquire() 或 relcase() 的执行必须是原子操作,因此互斥锁通常采用硬件机制来实现。互斥锁的主要缺点是忙等待,当有一个进程在临界区中,任何其他进程在进入临界区时必须连续循环调用 acquire()。当多个进程共享同一 CPU 时,就浪费了 CPU 周期。因此,互斥锁通常用于多处理器系统,一个线程可以在一个处理器上等待,不影响其他线程的执行。需要连续循环忙等的互斥锁,都可称为自旋锁 (spin lock),如 TSL 指令、swap 指令、单标志法。
2.3.4 信号量
信号量机制是一种功能较强的机制,可用来解决互斥与同步问题,它只能被两个标准的原语wait(S) 和 signal(S) 访问,也可记为 “P 操作” 和 “V 操作”。P 操作和 V 操作是不可中断的程序段,称为原语。P、V 原语中 P 是荷兰语的 Proberen(测试),V 是荷兰语的 Verhogen(增加)。
原语是指完成某种功能且不被分割、不被中断执行的操作序列,通常可由硬件来实现。例如,前述的 Test-and-Set 和 Swap 指令就是由硬件实现的原子操作。原语功能的不被中断执行特性在单处理机上可由软件通过屏蔽中断方法实现。原语之所以不能被中断执行,是因为原语对变量的操作过程若被打断,可能会去运行另一个对同一变量的操作过程,从而出现临界段问题。
若考试中出现 P(S)、V(S) 的操作,除非特别说明,否则默认 S 为记录型信号量。
1、整型信号量
整型信号量被定义为一个用于表示资源数目的整型量 S,wait 和 signal 操作可描述为
在整型信号量机制中的 wait 操作,只要信号量 S≤0,就会不断地测试。因此,该机制并未遵循 “让权等待” 的准则,而是使进程处于 “忙等” 的状态。
2、记录型信号量
记录型信号量机制是一种不存在 “忙等” 现象的进程同步机制。除了需要一个用于代表资源数目的整型变量 value 外,再增加一个进程链表 L,用于链接所有等待该资源的进程。记录型信号量得名于采用了记录型的数据结构。记录型信号量可描述为
相应的 wait(S) 和 signal(S) 的操作如下:
wait 操作,S.value-- 表示进程请求一个该类资源,当 S.value<0 时,表示该类资源已分配完毕,因此进程应调用 block 原语,进行自我阻塞,放弃处理机,并插入该类资源的等待队列S.L,可见该机制遵循了 “让权等待” 的准则。
signal 操作,表示进程释放一个资源,使系统中可供分配的该类资源数增 1,因此有 S.value ++。若加 1 后仍是 S.value≤0,则表示在 S.L 中仍有等待该资源的进程被阻塞,因此还应调 wake up 原语,将 S.L 中的第一个等待进程唤醒。
3、利用信号量实现同步
信号量机制能用于解决进程问的各种同步问题。设 S 为实现进程 ,
同步的公共信号量,初值为 0。进程
中的语句 y 要使用进程
中语句 x 的运行结果,所以只有当语句 x 执行完成之后语句 y 才可以执行。其实现进程同步的算法如下:
若 先执行到 P(S) 时,S 为0,执行 P 操作会把进程
阻塞,并放入阻塞队列;当进程
中的 x 执行完后,执行 V 操作,把口从阻塞队列中放回就绪队列,当
得到处理机时,就得以继续执行。
4、利用信号量实现进程互斥
信号量机制也能很方便地解决进程互斥问题。设 S 为实现进程 ,
互斥的信号量,由于每次只允许一个进程进入临界区,所以 S 的初值应为 1(即可用资源数为 1)。只需把临界区置于 P(S) 和 V(S)之间,即可实现两个进程对临界资源的互斥访问。其算法如下:
当没有进程在临界区时,任意一个进程要进入临界区,就要执行 P 操作,把 S 的值减为 0,然后进入临界区;当有进程存在于临界区时,S 的值为 0,再有进程要进入临界区,执行 P 操作时将会被阻塞,直至在临界区中的进程退出,这样便实现了临界区的互斥。
互斥是不同进程对同一信号量进行 P,V 操作实现的,一个进程成功对信号量执行了 P 操作后进入临界区,并在退出临界区后,由该进程本身对该信号量执行 V 操作,表示当前没有进程进入临界区,可以让其他进程进入。
在同步问题中,若某个行为要用到某种资源,则在这个行为前面 P 这种资源一下;若某个行为会提供某种资源,则在这个行为后面 V 这种资源一下。在互斥问题中,P,V 操作要紧夹使用互斥资源的那个行为,中间不能有其他冗余代码。
5、利用信号量实现前驱关系
信号量也可用来描达程序之间或语句之间的前驱关系。图给出了一个前驱图,其中 是最简单的程序段(只有一条语句)。为使各程序段能正确执行,应设置若干初始值为 “0” 的信号量。例如,为保证
的前驱关系,应分别设置信号量a1,a2。同样,为保证
,应设置信号量 b1,b2,c,d,e。
实现算法如下:
semaphore a1=a2=b1=b2=c=d=e=0; // 初始化信号量
S1(){
...;
V(a1);V(a2); // S1 已经运行完成
}
S2(){
P(a1); // 检查 S! 是否运行完成
...;
V(b1);V(b2); // S2 已经运行完成
}
S3(){
P(a2); // 检查 S1 是否已经运行完成
...;
V(c); // S3 已经运行完成
}
S4(){
P(b1); // 检查 S2 是否已经运行完成
...;
V(d); // S4 已经运行完成
}
S5(){
P(b2); // 检查 S2 是否已经运行完成
...;
V(e); // S5 已经运行完成
}
S6(){
P(c); // 检查 S3 是否已经运行完成
P(d); // 检查 S4 是否已经运行完成
P(e); // 检查 S5 是否已经运行完成
...;
}
6、分析进程同步和互斥问題的方法步骤
(1)关系分析。找出问题中的进程数,并分析它们之间的同步和互斥关系。同步、互斥、前驱关系直接按照上面例子中的经典范式改写。
(2)整理思路。找出解决问题的关键点,并根据做过的题目找出求解的思路。根据进程的操作流程确定 P 操作、V 操作的大致顺序。
(3)设置信号量。根据上面的两步,设置需要的信号量,确定初值,完善整理。
这是一个比较直观的同步问题,以 为例,它是
的后继,所以要用到
的资源,在前面的简单总结中我们说过,在同步问题中,要用到某种资源,就要在行为(题中统一抽象成 L)前面 P 这种资源一下。
是
的前驱,给
提供资源,所以要在 L 行为后面 V 由
和
代表的资源一下。
2.3.5 管程
在信号量机制中,每个要访问临界资源的进程都必须自备同步的 PV 操作,大量分散的同步操作给系统管理带来了麻烦,且容易因同步操作不当而导致系统死锁。于是,便产生了一种新的进程同步工具——管程。管程的特性保证了进程互斥,无须程序员自己实现互斥,从而降低了死锁发生的可能性。同时管程提供了条件变量,可以让程序员灵活地实现进程同步。
1、管程的定义
系统中的各种硬件资源和软件资源,均可用数据结构抽象地描述其资源特性,即用少量信息和对资源所执行的操作来表征该资源,而忽略它们的内部结构和实现细节。
利用共享数据结构抽象地表示系统中的共享资源,而把对该数据结构实施的操作定义为一组过程。进程对共享资源的申请、释放等操作,都通过这组过程来实现,这组过程还可以根据资源情况,或接受或阻塞进程的访问,确保每次仅有一个进程使用共享资源,这样就可以统一管理对共享资源的所有访问,实现进程互斥。这个代表共享资源的数据结构,以及由对该共享数据结构实施操作的一组过程所组成的资源管理程序,称为管程 (monitor)。管程定义了一个数据结构和能为并发进程所执行(在该数据结构上)的一组操作,这组操作能同步进程和改变管程中的数据。
由上述定义可知,管程由 4 部分组成:
① 管程的名称;
② 局部于管程内部的共享数据结构说明;
③ 对该数据结构进行操作的一组过程(或函数);
④ 对局部于管程内部的共享数据设置初始值的语句。
管程的定义描述举例如下:
熟悉面向对象程序设计的读者看到管程的组成后,会立即联想到管程很像一个类 (class)。
(1)管程把对共享资源的操作封装起来,管程内的共享数据结构只能被管程内的过程所访问。一个进程只有通过调用管程内的过程才能进入管程访问共享资源。对于上例,外部进程只能通过调用 take_away() 过程来申请一个资源;归还资源也一样。
(2)每次仅允许一个进程进入管程,从而实现进程互斥。若多个进程同时调用 take_away() ,give_back(),则只有某个进程运行完它调用的过程后,下个进程才能开始运行它调用的过程。也就是说,各个进程只能串行执行管程内的过程,这一特性保证了进程 “互斥” 访问共享数据结构 S。
2、条件变量
当一个进程进入管程后被阻塞,直到阻塞的原因解除时,在此期间,如果该进程不释放管程,那么其他进程无法进入管程。为此,将阻塞原因定义为条件变量 condition。通常,一个进程被阻塞的原因可以有多个,因此在管程中设置了多个条件变量。每个条件变量保存了一个等待队列,用于记录因该条件变量而阻塞的所有进程,对条件变量只能进行两种操作,即 wait 和 signal。
x.wait:当 × 对应的条件不满足时,正在调用管程的进程调用 x. wait 将自己插入 × 条件的等待队列,并释放管程。此时其他进程可以使用该管程。
x.signal:x 对应的条件发生了变化,则调用 x.signal,唤醒一个因 × 条件而阻塞的进程。
下面给出条件变量的定义和使用:
条件变量和信号量的比较:
相似点:条件变量的 wait/signal 操作类似于信号量的 P/V 操作,可以实现进程的阻塞/唤醒。
不同点:条件变量是 “没有值” 的,仅实现了 “排队等待” 功能;而信号量是 “有值” 的,信号量的值反映了剩余资源数,而在管程中,剩余资源数用共享数据结构记录。
Java 中,如果用关键字 synchronized 来描述一个函数,那么这个函数同一时间段内只能被一个线程调用。
2.3.6 经典同步问题
1、生产者—消费者问题
2、多生产者—多消费者问题
3、吸烟者问题
4、读者—写者问题
5、哲学家进餐问题
2.4 死锁
2.4.1 死锁的概念
1、死锁的定义
在多道程序系统中,由于多个进程的并发执行,改善了系统资源的利用率并提高了系统的处理能力。然而,多个进程的并发执行也带来了新的问题——死锁。所谓死锁,是指多个进程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。
2、死镇产生的原因
(1)系统资源的竞争
通常系统中拥有的不可剥夺资源,其数量不足以满足多个进程运行的需要,使得进程在运行过程中,会因争夺资源而陷入僵局,如磁带机、打印机等。只有对不可剥夺资源的竞争才可能产生死锁,对可剥夺资源的竞争是不会引起死锁的。
(2)进程推进顺序非法
进程在运行过程中,请求和释放资源的顺序不当,也同样会导致死锁。例如,并发进程 分别保持了资源
,而进程
申请资源
、进程
申请资源
时,两者都会因为所需资源被占用而阻塞,于是导致死锁。
信号量使用不当也会造成死锁。进程间彼此相互等待对方发来的消息,也会使得这些进程间无法继续向前推进。例如,进程 A 等待进程 B 发的消息,进程 B 又在等待进程 A 发的消息,可以看出进程 A 和 B 不是因为竞争同一资源,而是在等待对方的资源导致死锁。
(3)死锁产生的必要条件
产生死锁必须同时满足以下 4 个条件,只要其中任意一个条件不成立,死锁就不会发生。
① 互斥条件:进程要求对所分配的资源(如打印机)进行排他性使用,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
② 不剥夺条件:进程所获得的资源在末使用完之前,不能被其他进程强行夺走,即只能由荻得该资源的进程自己来释放(只能是主动释放)。
③ 请求并保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
④ 循环等待条件:存在一种进程资源的循环等待链,链中每个进程已获得的资源同时被链中下一个进程所请求。即存在一个处于等待态的进程集合 {},其中
等待的资源被
(i=0,1,…,n-1)占有,
等待的资源被
占有,如图所示。发生死锁时一定有循环等待,但是发生循环等待时未必死锁(循环等待是死锁的必要不充分条件),如果同类资源数大于1,则即使有循环等待,也未必发生死锁。但如果系统中每类资源都只有一个,那循环等待就是死锁的充分必要条件了。
直观上看,循环等待条件似乎和死锁的定义一样,其实不然。按死锁定义构成等待环所要求的条件更严,它要求 等待的资源必须由
来满足,而循环等待条件则无此限制。例如,系统中有两台输出设备,
占有一台,
占有另一台,且 K 不属于集合 {
} 占有,
等待一台输出设备,它可从
获得,也可能从
获得。因此,虽然
和其他一些进程形成了循环等待圈,但
不在圈内,若
释放了输出设备,则可打破循环等待,如图所示。因此循环等待只是死锁的必要条件。
3、死锁的处理策咯
为使系统不发生死锁,必须设法破坏产生死锁的 4 个必要条件之一,或允许死锁产生,但当死锁发生时能检测出死锁,并有能力实现恢复。
(1)死锁预防。设置某些限制条件,破坏产生死锁的 4 个必要条件中的一个或几个。
(2)避免死锁。在资源的动态分配过程中,用某种方法防止系统进入不安全状态。
(3)死锁的检测及解除。无须采取任何限制性措施,允许进程在运行过程中发生死锁。通过系统的检测机构及时地检测出死锁的发生,然后采取某种措施解除死锁。
预防死锁和避免死锁都属于事先预防策略,预防死锁的限制条件比较严格,实现起来较为简单,但往往导致系统的效率低,资源利用率低;避免死锁的限制条件相对宽松,资源分配后需要通过算法来判断是否进入不安全状态,实现起来较为复杂。
死锁的几种处理策略的比较见表:
2.4.2 死锁预防
防止死锁的发生只需破坏死锁产生的 4 个必要条件之一即可。
1、破坏互斥条件
若允许系统资源都能共享使用,则系统不会进入死锁状态。如打印机等临界资源只能互斥使用,比如:SPOOLing 技术。操作系统可以采用 SPOOLing 技术把独占设备在逻辑上改造成共享设备。比如,用 SPOOLing 技术将打印机改造为共享设备但有些资源根本不能同时访问。所以,破坏互斥条件而预防死锁的方法不太可行,而且在有的场合应该保护这种互斥性。
2、破坏不剥夺条件
当一个已保持了某些不可剥夺资源的进程请求新的资源而得不到满足时,它必须释放已经保持的所有资源,待以后需要时再重新申请。这意味着,一个进程已占有的资源会被暂时释放,或者说是被剥夺,或从而破坏了不剥夺条件。当某个进程需要的资源被其他进程所占有的时候,可以由操作系统协助,将想要的资源强行剥夺。这种方式一般需要考虑各进程的优先级(比如:剥夺调度方式,就是将处理机资源强行剥夺给优先级更高的进程使用)。
该策略实现起来比较复杂,释放已获得的资源可能造成前一阶段工作的失效,反复地申请和释放资源会增加系统开销,降低系统吞吐量。这种方法常用于状态易于保存和恢复的资源,如 CPU 的寄存器及内存资源,一般不能用于打印机之类的资源。
3、破坏请求并保持条件
采用预先静态分配方法,即进程在运行前一次申请完它所需要的全部资源,在它的资源末满足前,不把它投入运行。一旦投入运行,这些资源就一直归它所有,不再提出其他资源请求,这样就可以保证系统不会发生死锁。
这种方式实现简单,但缺点也显而易见,系统资源被严重浪费,其中有些资源可能仅在运行初期或运行快结束时才使用,甚至根本不使用。而且还会导致 “饥饿” 现象,由于个别资源长期被其他进程占用时,将致使等待该资源的进程迟迟不能开始运行。
4、破坏循环等待条件
为了破坏循环等待条件,可采用顺序资源分配法。首先给系统中的资源编号,规定每个进程必须按编号递增的顺序请求资源,同类资源一次申请完。也就是说,只要进程提出申请分配资源 ,则该进程在以后的资源申请中就只能申请编号大于
的资源。
这种方法存在的问题是,编号必须相对稳定,这就限制了新类型设备的增加;尽管在为资源编号时已考虑到大多数作业实际使用这些资源的顺序,但也经常会发生作业使用资源的顺序与系统规定顺序不同的情况,造成资源的浪费;此外,这种按规定次序申请资源的方法,也必然会给用户的编程带来麻烦。
2.4.3 死锁避免
所谓安全序列,就是指如果系统按照这种序列分配资源,则每个进程都能顺利完成。只要能找出一个安全序列,系统就是安全状态。当然,安全序列可能有多个。如果分配了资源之后,系统中找不出任何一个安全序列,系统就进入了不安全状态。这就意味着之后可能所有进程都无法顺利的执行下去。当然,如果有进程提前归还了一些资源,那系统也有可能重新回到安全状态,不过我们在分配资源之前总是要考虑到最坏的情况。
如果系统处于安全状态,就一定不会发生死锁。如果系统进入不安全状态,就可能发生死锁 (处于不安全状态未必就是发生了死锁,但发生死锁时一定是在不安全状态)。因此可以在资源分配之前预先判断这次分配是否会导致系统进入不安全状态,以此决定是否答应资源分配请求。这也是 “银行家算法” 的核心思想。
核心思想:在进程提出资源申请时,先预判此次分配是否会导致系统进入不安全状态。如果会进入不安全状态,就暂时不答应这次请求,让该进程先阻塞等待。
2.4.4 死锁检测和解除
如果系统中既不采取预防死锁的措施,也不采取避免死锁的措施,系统就很可能发生死锁。在这种情况下,系统应当提供两个算法:
① 死锁检测算法:用于检测系统状态,以确定系统中是否发生了死锁。
② 死锁解除算法:当认定系统中已经发生了死锁,利用该算法可将系统从死锁状态中解脱出来。