计算机考研408强化之操作系统 | 2.进程管理

2 进程管理

2.1 进程与线程

进程的概念和特征

进程的概念:进程是进程实体(程序软件)的运行过程,是系统进行资源分配和调度的一个独立单位。(进程主要强调一个"动态性",也就是说,进入进程这个概念,需要运行的,需要进行对资源分配和调度的)

进程控制块(Process Control Block,PCB):是为了让参与并发执行的程序(含数据)能独立的运行,必须配置的一个专门的数据结构。系统利用PCB来描述进程的基本情况和运行状态(比如就绪态、运行态、阻塞态等信息就在PCB中),进而控制和管理进程。PCB是进程存在的唯一标志

进程实体:由程序段、相关数据段和PCB三部分构成的进程映像。而一般所谓的创建进程,实质上是创建进程映像中的PCB;而撤销进程,实质上是撤销进程的PCB。

进程实体是静态的,进程是动态的。

进程的五大基本特征(动并独异结,动冰毒一劫)

  • 动态性:进程是程序的一次执行,它有着创建、就绪、运行、阻塞、终止等过程,具有一定的生命周期,是动态地产生、变化和消亡的。动态性是进城最基本的特征。
  • 并发性:指多个进程实体同时存在在内存中,能在一段时间内同时运行
  • 独立性:进程实体是一个能独立运行、独立获得资源和独立接受调度的基本单位
  • 异步性:进程按各自独立的、不可预知的速度前进,异步性导致执行结果的不可再现
  • 结构性:每一个进程都配置一个PCB对其进行描述

进程的状态和转换

进程的五种状态:

  • 创建态:进程正在被创建,但尚未转到就绪态。创建进程的步骤有(申写配转)
    • 首先申请一个空白的PCB
    • 然后向PCB中填写一些控制和管理进程的信息(比如唯一的进程标识号)
    • 之后由系统为该进程分配运行时所必须的资源(为新进程的程序和数据及用户栈分配必要的内存空间,资源不足,进程处于创建态或阻塞态)
    • 初始化PCB,最后把该进程转入就绪态
  • 就绪态:进程除了处理机以外所有的资源都获得了
  • 运行态:进程在处理机上运行
  • 阻塞态:进程正在等待某一事件而暂停。即除了处理机以外的、进程运行必需的资源(或等待IO完成)没有获取。即使处理机空闲,该进程也不能运行
  • 结束态:进程正从系统中消失,可能是进程正常结束或其他原因中断退出运行
    • 首先必须置该进程为结束态
    • 然后再进一步处理资源释放和回收等工作

就绪态、运行态和阻塞态为三种基本状态,三者转换如下:

  • 就绪态➡️运行态:处于就绪态的进程被调度后,获得处理机资源(分派处理机时间片),于是进程由就绪态转为运行态
  • 运行态➡️就绪态:处于运行态的进程在时间片用完之后,不得不让出处理机,从而进程由运行态转为就绪态。此外,在可剥夺的操作系统中,当有更高优先级的进程就绪时,调度程序将正执行的进程转换为就绪态
  • 运行态➡️阻塞态:进程请求某一资源(如外设)的使用和分配或等待某一事件的发生(如IO操作完成)时,它就从运行态转为阻塞态。
  • 阻塞态➡️就绪态:进程等待的事件到来,如IO操作结束或者中断结束,中断处理程序必须把相应的状态由阻塞态转换为就绪态

在这里插入图片描述


进程控制

进程的创建

父子进程:允许一个进程创建另一个进程。此时创建者被称为父进程,被创建的进程称为子进程。

父子进程的特点:子进程可以继承父进程所拥有得资源。当子进程被撤销时,应将父进程那里获得资源归还给父进程。另外,当父进程被撤销的时候,必须同时撤销其所有的子进程。

进程创建的过程:(见上面五种状态中的创建态)

进程的终止

  • 正常结束:表示进程的任务已经完成并准备退出运行
  • 异常结束:表示进程在运行时,发生了某种异常,使程序无法继续运行(比如存储区越界、保护错、非法指令、特权指令错、IO故障)
  • 外界干预:指进程应外界的请求而终止运行

进程终止的过程:

  1. 根据被终止进程的标识符,检索PCB,从中读出该进程的状态
  2. 若被终止进程处于执行状态,立即终止该进程的执行,并将处理机资源分配给其他进程
  3. 若进程中还有子进程,则应将所有的子进程终止
  4. 将该进程所拥有的全部资源还给其父进程或操作系统
  5. 将该PCB从所在队列中删除

进程的阻塞和唤醒

当正在运行的程序由于期待的某些事件没有发生(就是有些资源还没有获得),比如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作可做,由(进程自己调用系统中的Block原语)系统自动执行阻塞原语(Block),使自己由运行态变为阻塞态。

进程的阻塞态是一种主动行为,只有运行态的进程才能直接转化为阻塞态。

阻塞原语的执行:

  1. 找到将要被阻塞进程的标识号对应的PCB
  2. 若该进程在运行态,保护其现场,将其状态转为阻塞态,停止运行
  3. 把该PCB事件插入等待队列

当被阻塞的进程所期待的事件出现时,调用唤醒原语(Wakeup),将等待该事件的进程唤醒。

唤醒原语的执行:

  1. 在该事件的等待队列中找到相应进程的PCB
  2. 将其从等待队列中移出,并置其状态为就绪态
  3. 把该PCB进程插入就绪队列,等待调度程序

注意Block原语和Wakeup原语必须成对使用。

不同的是,Block原语时被阻塞进程自我调用实现的;Wakeup原语则是由一个与被唤醒进程合作或被其他相关的进程调用实现的

进程切换

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

进程切换的过程如下

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

一些不同

进程切换和处理机模式切换

  • 处理机的切换描述的对象是同一个进程
  • 进程切换描述的对象不是同一个进程

调度和切换

  • 调度是指,决定资源分配给哪个进程的行为,是一种决策行为
  • 切换是指,实际的分配行为,是执行行为

一般来说,先有资源调度,然后才有进程的切换


进程的组织

进程控制块PCB

PCB主要的信息包括进程描述信息、进程控制和管理信息、资源分配信息和处理机相关信息

  • 进程描述信息。
    • 进程标识符:标识各个进程,每一个进程都有一个唯一的标识号。
    • 用户标识符:进程归属用户,用户标识符主要为共享的保护服务。
  • 进程控制和管理信息
    • 进程当前状态:描述进程的状态信息,作为处理机分配制度的依据
    • 进程优先级:描述进程抢占处理机的优先级,优先级高的进程可优先获得处理机
  • 资源分配清单
    • 用于说明有关内存地址空间的状况,所打开文件的列表和所使用的输入输出设备信息
  • 处理机相关信息
    • 主要指处理机中各存储器的值,当进程被切换时,处理机状态信息都必须保存在相应的PCB中,以以便在该进程重新执行时,能从断点继续执行

在一个系统中,有很多进程,这些进程的状态也各不一样,为了方便调度,就将各进程的PCB组织起来。目前常用的组织方式有链接方式和索引方式

  • 链接方式:将同一状态的PCB连接成一个队列,不同状态对应不同的队列。也可以把处于阻塞态的进程的PCB,根据阻塞原因的不同,排成多个阻塞队列
  • 索引方式:将同一状态的进程组织在一个索引表中,索引表的表项指向相应的PCB,不同状态对应不同的索引表

程序段

  • 程序段是指,能被进程调度的程序调度到CPU执行的程序代码段(程序是可以被多个进程共享的)

数据段

  • 一个进程的数据段,可以是对应的程序加工处理的原始数据,也可以是程序执行时产生的中间或最终结果。

进程的通信

进程通信是指进程之间的信息交换。需要进程通信的原因是,进程是分配系统资源的单位,因此各进程拥有的内存地址空间相互独立,一个进程不能直接访问另一个进程的地址空间

PV操作是一种低级的通信方式,高级通信方式是指以较高的效率传输大量数据的通信方式,主要有以下三种

  • 共享存储
  • 消息传递
  • 管道通信

共享存储

共享存储是指,在通信的进程之间存在一块可直接访问的共享空间,通过对这片空间进行读/写操作实现进程之间的信息交换。但是这个共享空间是一块临界资源,需要使用同步互斥工具(比如PV),对共享空间进行控制。

共享存储又分两种:

  • 低级方式:共享基于数据结构的共享
  • 高级方式:共享基于存储区的共享(P.S. 这里的低级和高级指的是能否以较高效率传输大量数据的通信方式)

这种方式中,操作系统只负责为通信进程提供可共享使用的存储空间和同步互斥工具,而数据交换则由用户自己安排读/写指令完成


消息传递

当通信的进程之间不存在可直接访问的共享空间,则必须利用操作系统提供的消息传递方法实现进程通信。进程之间的数据交换以格式化的信息为单位的。进程通过系统提供的发送消息和接收消息两个源于进程数据交换

  • 直接通信方式:发送进程直接把消息发送给接收进程,并将它挂在接收进程的消息缓冲队列上,接收进程从消息缓冲队列中取得消息
  • 间接通信方式:发送进程把消息发送到某个中间实体,接收进程从中间实体中获取信息

管道通信

管道通信是消息传递的特殊方式。所谓"管道",是指用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件,又名pipe文件。向管道(共享文件)提供输入的发送进程(即写进程),以字符流形式将大量的数据送入(写)管道;而接收管道输出的接收进程(即读进程)则从管道中接收(读)数据。

从管道读数据是一次性操作,数据一旦被读取,它就从管道中被抛弃,释放空间一边写更多的数据。管道只能采用半双工通信,也就是某一时刻只能单向传输。要实现父子进程双方互动通信,那就定义两个管道

管道可以理解为共享存储的优化和发展,因为在共享存储中,若某进程要访问共享存储空间,则必须没有其他进程在该共享存储空间中进行读写操作,否则访问行为就会被阻塞。而管道通信中,存储空间进化成了缓冲区,缓冲区只允许一边写入、一边读出,因此只要缓冲区中有数据,进程就能从缓冲区中读出,而不必担心会因为其他进程在其中进行写操作而遭到阻塞,因为写进程会先把缓冲区写满,当缓冲区中还有数据时,写进程不会往缓冲区写数据。


线程概念和多线程模型

线程的基本概念

线程可以直观理解为"轻量级进程",它是一个基本的CPU执行单元,也是程序执行流的最小单元,由线程ID、程序计数器、寄存器集合和堆栈组成。

引入进程的目的是为了更好的使多道程序并发执行,提高资源利用率和系统吞吐量,增加并发程度;

引入线程的目的则是为了减小程序在并发执行时所付出的时空开销,提高操作系统的并发性能

线程是进程的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其他线程共享进程的所拥有的全部资源。

线程也拥有就绪、运行、阻塞三种基本状态。

引入线程后,进程的内涵发生了改变,进程只作为除CPU外系统资源的分配单元,线程则作为处理机的分配单元。

由于一个进程内部有多个线程,若线程的切换发生在同一个进程内部,则需要很少的时空开销。

进程与线程的比较

  • 调度
    • 在传统操作系统中,拥有资源和独立调度的基本单位都是进程
    • 在引入线程之后,线程是独立调度的基本单位,进程是拥有资源的基本单位。在同一个进程内线程切换不会引起进程切换,不同进程中的线程切换,会引起进程切换
  • 拥有资源
    • 进程是拥有资源的基本单位
    • 线程不拥有系统资源,但是线程可以访问所隶属的进程的 系统资源
  • 并发性
    • 传统进程机制中,只能进程间并发
    • 引入线程之后,各线程之间也可以并发,提高了并发度
  • 系统开销
    • 传统的进程间并发,需要切换进程的运行环境,系统开销很大
    • 线程间并发,若是同一进程内线程切换,则不需要切换进程环境
    • 引入线程后,并发所带来的系统开销减小
  • 地址空间和其他资源
    • 进程的地址空间之间互相独立,同一进程的各线程间共享进程资源,某进程内的线程对于其他进程不可见
  • 通信方便
    • 进程通信需要进程同步和互斥手段的辅助,以保证数据的一致性
    • 线程间可以直接读/写进进程数据段来进行通信

线程的属性

  1. 线程是处理机调度的单位
  2. 多CPU计算机中,各个线程可占用不同的CPU
  3. 每个线程都有一个线程ID,线程控制块
  4. 线程也有就绪、阻塞、运行状态
  5. 线程机会不拥有系统资源
  6. 统一进程的不同线程共享进程的资源
  7. 由于共享内存地址空间,同一进程中线程间通信甚至无需系统干预
  8. 同一进程中的线程切换,不会引起进程切换
  9. 不同进程中的线程切换,会引起进程切换
  10. 切换同一进程中的线程,系统开销很小
  11. 不同地线程可以执行相同的程序

线程的实现方式:用户级线程和内核级线程

  • 用户级进程:有关线程管理的所有工作都由应用程序完成,内核意识不到线程的存在,也就是说,这种线程切换可以在用户态即可完成,而没必要操作系统干预。
  • 内核级线程:线程管理的所有工作由内核完成,应用程序没有进行线程管理的代码,只有一个到内核级线程的编程接口
  • 组合方式:有些系统中使用组合方式的多线程实现,线程创建完全在用户空间中完成,线程的调度和同步也在应用程序中进行。一个应用程序中的多个用户级线程被映射到一些(小于等于用户级线程的数目)内核级线程上。

多线程模型

  • 多对一模型:将多个用户级线程映射到一个内核级线程,线程管理在用户空间完成。此模型中,用户级线程对操作系统不可见(透明)
    • 优点:线程管理是在用户空间进行的,因而效率比较高
    • 缺点:一个线程在使用内核服务是被阻塞,整个进程都会被阻塞;多个线程不能并行的运行在多处理机上

在这里插入图片描述

  • 一对一模型:每个用户级线程映射到一个内核级线程
    • 优点:当一个线程被阻塞后,允许另一个线程继续执行,所以并发能力较强
    • 缺点:每创建一个用户线程都需要创建一个内核级线程与其对应,这样创建线程的开销比较大,会影响到应用程序的性能

在这里插入图片描述

  • 多对多模型:将n个用户级线程映射到m个内核级线程上,m小于等于n
    • 多对多模型是多对一模型和一对一模型的折中,既克服了多对一并发度不高的缺点,又克服了一对一模型的一个用户进程占用太多内核级线程而开销太大的缺点,还拥有两者的优点

在这里插入图片描述

2.2 处理机调度

调度概念

处理机调度是对处理机进行分配,即从就绪队列中按照一定的算法选择一个进程并将处理机分配给他运行,以实现进程并发的执行。

调度层次

  • 作业调度,又被称为高级调度,其主要任务是按照一定的原则从外存上处于后备状态的作业中挑选一个(或多个)作业,给它(们)分配内存、输入/输出设备等必要资源,并建立相应的进程,以使它(们)获得竞争处理机的权利。简而言之,作业调度就是内存与辅存之间的调度。对于每个作业只调入一次、调出一次。
  • 中级调度,又被称为内存调度,其作用是提高内存利用率和系统吞吐量。为此,应将那些暂时不能运行的进程调至外存等待,把此时的进程状态称为挂起态。当她们已具备运行条件且内存又稍有空闲时,由中级调度来决定把外存上的那些已具备运行条件的就绪进程,再重新调入内存,并修改其状态为就绪态,挂在就绪队列上等待。
  • 进程调度,又被称为低级调度,其主要任务是按照某种方法和策略从就绪队列中选取一个进程,将处理机分配给它。进程调度是操作系统中最基本的一种调度,且进程调度的频率很高,一般几十毫秒一次。

在这里插入图片描述

三级调度的联系

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


调度的时机、切换与过程

时机

  • 什么时候需要调度?

    • 进程主动放弃处理机:进程正常终止、运行过程中发生异常而终止、进程主动请求阻塞
    • 进程被动放弃:时间片用完、有更紧急的事需要处理、有更高级的进程进入就绪队列
  • 什么时候不能进行调度

    • 在处理中断过程中(在实现上很难进行进程切换)
    • 进程在操作系统内核程序临界区中(进入临界区,理论上需要加锁,之后的操作类似一个原子操作)
    • 其他需要完全屏蔽中断的原子操作过程中(连中断都要屏蔽)

切换与过程

  • 狭义的"调度"和"切换"的区别:

    • 调度:从就绪队列中选择一个要运行的进程
    • 切换:一进程让出处理机让另一进程使用

调度的方式

  • 非抢占式:只能由当前运行的进程主动放弃CPU
    • 优点:实现简单、系统开销小,适用于大多数的批处理系统
    • 缺点:不能用于分时系统过和绝大多数的实时系统
  • 抢占式:马上进行进程调度与切换
    • 优点:对提高系统吞吐率和响应效率
    • 缺点:必须遵循一定的原则,主要有优先权、短进程优先和时间片原则

调度的基本准则

CPU利用率:CPU使用时间占总时间

CPU利用率=(忙碌的时间)/(总时间)​

系统吞吐量:单位时间内CPU完成作业的数量

系统吞吐量=(总共完成了多少道作业)/(总共花了多少时间)

周转时间:从作业提交到作业完成所经历的时间,是作业等待、在就绪队列中排队、在处理机上运行及进行输入输出操作所花费时间的总和

周转时间=作业完成时间-作业提交时间

平均周转时间=(周转时间1+周转时间2+……+周转时间n)/n

平均带权周转时间=(带权周转时间1+带权周转时间2+……+带权周转时间n)/n

等待时间:进程处于等待处理机状态的时间之和,等待时间越长,用户满意度越低

响应时间:用户提交请求到系统首次产生响应所用的时间


经典调度算法

先来先服务(FCFS)调度算法

  • 算法思想:主要从"公平"的角度考虑(类似我们生活中排队买东西)
  • 算法规则:按照作业/进程到达的先后
  • 用于作业/进程调度:用于作业调度时,考虑的是哪个作业先到达后备队列;用于进程调度时,考虑哪个进程先到达就绪队列
  • 是否可抢占:非抢占式算法
  • 优点:公平、算法实现简单
  • 缺点:排在长作业(进程)后的短作业需要等待的时间很长,带权周转时间很大,对于短作业来说,用户体验十分不好。即FCFS算法对长作业有利,对短作业不利
  • 是否会导致饥饿:不会

短作业优先(SJF)算法

  • 算法思想:每次调度时选择当前已经到达且运行时间最短的作业/进程。(批处理)最追求最少的平均等待时间,最少的平均周转时间,最少的平均带权时间
  • 算法规则:最短的作业/进程优先得到服务(最短是指,要求服务的时间最短)
  • 用于作业/进程调度:即可用于作业调度,也可用于进程调度
  • 是否可抢占:SJF和SPF都是非抢占的
  • 优点:"最短"的平均等待时间、"最短"平均周转时间
  • 缺点:不公平,对短作业有利,对长作业不利;会导致饥饿现象,另外,作业/进程的运行时间是由用户提供,不一定真实
  • 是否会导致饥饿:会。若短作业源源不断到来,可能使长作业/进程长时间得不到服务

优先级调度算法(交互式-实时)

  • 算法思想:随着实时操作系统的出现,越来越多的应用场景需要根据任务的紧急程度来决定处理顺序
  • 算法规则:调度时选择优先级最高的作业/进程
  • 用于作业/进程调度:即可用于作业调度,也可以用于进程调度
  • 是否可抢占:可抢占式和非抢占式都有。非抢占式只需在进程主动放弃处理机时进行调度即可;而抢占式还需要在就绪队列变化时,检查是否发生抢占
  • 优点:用优先级区分紧急程度、重要程度,适用于实时操作系统,可灵活调整对各作业/进程的偏好程度
  • 缺点:当源源不断有高优先级进程来到,可能会导致饥饿现象
  • 是否会导致饥饿:会

高响应比优先调度算法

  • 算法思想:要综合考虑作业/进程的等待时间和要求服务的时间
  • 算法规则:在进行调度时,先计算各个作业/进程的响应比,选择响应比最高的作业/进程为其服务。响应比R_p=(等待时间+要求服务的时间)/要求服务的时间
  • 用于作业/进程调度:即可用于作业调度,也可以用于进程调度
  • 是否可抢占:非抢占式
  • 优点:综合考虑了等待时间和运行时间(要求服务时间)。对于长作业来说,等待的时间越久,其响应比也越大,从而避免了长作业饥饿的问题。等待时间相同时,要求服务时间短的优先(SJF);要求服务的时间相同时,先到的进程/作业有限(FCFS)。
  • 是否会导致饥饿:不会

时间片轮转调度算法(RR算法,交互式)

  • 算法思想:公平的、轮流的为各个进程服务,让每个进程在一定的时间间隔内都可以得到响应
  • 算法规则:按照各进程到达就绪队列的顺序,轮流让各个进程执行一个时间片。若进程未在一个时间片内执行完,则剥夺处理机,将进程重新放到就绪队列中重新排队
  • 用于作业/进程调度:只用于进程调度
  • 是否可抢占:若进程未在时间片内运行完,被强行剥夺处理机,可抢占
  • 优点:公平、响应快、适用于分时操作系统
  • 缺点:由于高频率低进程切换,因此有一定开销,不分是否紧急;时间片大小需要选好
  • 是否会导致饥饿:不会

多级反馈队列调度算法

  • 算法思想:对其他调度算法的这种权衡

  • 算法规则

    • 设置多个就绪队列,并为每个队列赋予不同优先级。第1级优先级最高,之后依次递减
    • 赋予各队列中进程执行时间片大小各不相同。在优先级越高的队列中,每个进程的运行片时间越小
    • 一个新进程进入内存后,首先将它放到第一季队列末尾,按FCFS原则排队等待。当轮到该进程执行时,若能在该时间片内完成任务,便可撤离系统;否则,调度程序会将该进程转入第2级队列末尾,同样按FCFS原则等待,依次递降
    • 只有当第1级队列为空时,调度程序才调度第2级队列中的进程运行。只有当第1~(n-1)级队列中进程运行光之后,调度程序才能调度第n级队列。
    • 若处理机正在处理第n级中的进程,此时,有新的进程进入比n级更高级的队列,则此时新进程将抢占正在运行进程的处理机,即有调度程序把正在运行的进程返回到第n级队列末尾,把处理机分配给新到的更高优先级的进程
  • 用于作业/进程调度:只用于进程调度

  • 是否可抢占:可

  • 优点

    • 对各类型进程相对公平(FCFS)
    • 每个新到达的进程都可以得到很快响应(RR)
    • 短进程只用较少的时间就可以完成(SJF)
    • 不必实现估计进程的执行时间(避免用户主观,避免了SJF和高响应比优先的缺点)
    • 可灵活的调整对各类型进程的偏好
  • 是否会导致饥饿:会,短进程或新进程源源不断进入

2.3 进程同步

进程同步的基本概念

临界资源:一次仅允许一个进程使用的资源

临界资源的访问步骤

  1. 进入区:设置正在访问临界区的标志,以阻止其他进程同时进入临界区
  2. 临界区:进程中访问临界资源的那段代码,又称临界段
  3. 退出区:将正在访问临界区的标志清除
  4. 剩余区:代码中剩余部分

同步:又被称为直接制约关系,是指为完成某种任务而建立的两个或多个进程,这些进程因为需要在某些位置上协调它们的工作次序而等待,传递信息所产生的制约关系

互斥:又被称为间接制约关系,是指当一个进程进入临界区使用临界资源时,另一个进程必须等待,当占用临界资源的进程推出临界区后,另一个进程才能访问该临界区

为了禁止两个进程同时进入临界区,同步机制应该遵循以下原则

  • 空闲让进:临界区空闲时,可以允许一个请求进入临界区的进程立即进入临界区
  • 忙则等待:当已经有进程进入临界区的时候,其他试图进入临界区的进程必须等待
  • 有限等待:对请求访问的进程,应保证能在有限时间内进入临界区
  • 让权等待:当进程不能进入临界区的时候,应立即释放处理机,防止进程忙等待
2.4 实现临界区互斥的基本方法

一、单标志法

该算法设置一个公用整型变量turn,用于指示被允许进入临界区的进程编号。

若turn=0,则允许 P 0 P_0 P0 进程进入临界区

该算法可确保每次只允许一个进程进入临界区。

//P_0进程 
while(turn!=0); 
critical section; 
turn=1;    
//P_1进程 
while(turn!=1); 
critical section;
turn=0;              

但两个进程必须交替进入临界区,若某个进程不再进入临界区,则另一个进程也无法进入临界区

违背了"空闲让进"原则,让资源利用不充分

此外,若 P 0 P_0 P0 顺利进入临界区并从临界区离开,则此时临界区是空闲的,但 P 1 P_1 P1 并没有进入临界区的打算,turn=1一直成立, P 0 P_0 P0 就无法再次进入临界区


二、双标志法先检查

该算法的基本思想是在每个进程访问临界区资源之前,先检查临界资源是否正在被访问,若正在被访问,该进程需要等待;

否则,进程才进入自己的临界区

为此,设置一个数据 flag[i],如第 i 个元素值为 FALSE,表示 P i P_i Pi 进程未进入临界区;

值为TURE,表示 P i P_i Pi 进程进入临界区

//P_i进程(写在一个代码块中,但不是一个程序,两个进程具有并发性和独立性) 
while(flag[j]); 
flag[i]=TRUE; 
critical section; 
flag[i]=FLASE; 
remainder section; 
//P_j进程 
while(flag[i]); 
flag[j]=TRUE; 
critical section; 
flag[j]=FALSE;              

该算法的优点是不需要交替进入,可连续使用。

但这种算法也是有缺点的。

正常来说,这种算法正常的顺序是:2-3-4-5-6-8-9-10-11-12

但是,当顺序变成:2-8-3-9-4-5-6-10-11-12,28是同时进行的,相当于while没拦截住

此时,两个进程都进入了临界区,违背了"忙则等待"的原则

也就是在检查对方的flag后切换自己的flag之前的一段时间,结果检查都通过了(打个比方进程i的第2行代码在0.1ms完成,独立的进程j的第8行0.15ms完成,但进程i的第3行代码在0.2ms完成,这就有时间差了,两者都进入了临界区)


三、双标志后检查法

对于双标志先检查法先检查对方的进程状态标志,再设置自己的标志,由于在检测和放置中可插入另一个进程到达时的检测操作,会造成两个进程在分别检查后同时进入临界区。

针对上述缺点,双标志后检查法就先将自己的标志设置为TRUE,然后在检测对方的状态标志。

若对方标志为TRUE,则进程等待;

否则进入临界区

//P_i进程 
flag[i]=TRUE; 
while(flag[j]); 
critical section; 
flag[i]=FLASE; 
remainder section; 
//P_j进程 
flag[j]=TURE; 
while(flag[i]); 
critical section; 
flag[j]=FLASE; 
remainder section; 

双标志后检查法固然解决了双标志先检查法的同时进入临界区、违背"忙着等待"的原则的问题。

但是双标志后检查法在某些时候会违背"有限等待"的原则

比如,双标志后检查法的正常流程是:2-3-4-5-6-8-9-10-11-12

但是在某些时候,也会有:2-8-3-9(陷入死循环)

这个现象就是在两个进程同时都想要进入临界区的时候,它们分别将自己的标志之flag设置为TRUE,且同时检测到对方的状态,当发现对方也要进入临界区的时候,双方互相谦让,导致谁也进不了临界区,产生了饥饿现象,违背"有限等待"原则


四、皮特森算法(三标志后检查法)

为了防止两个进程未进入临界区而无限等待,又设置一个变量turn,每个进程在先设置自己的标志后再设置turn标志。(三标志后检查法)这时,再同时检测另一个进程状态标志和不允许进入标志,以便保证两个进程同时要求进入临界区试,只允许一个进程进入临界区

//p_i进程 
flag[i]=TRUE; turn=j;
//表示自己可以让给j 
while(flag[j]&&turn==j); 
critical section; 
flag[i]=FALSE; 
remainder section; 
//p_j进程 
flag[j]=TRUE; turn=i; 
while(flag[i]&&turn==i); 
critical section; 
flag[j]=FALSE;              

第2行代码表示 p i p_i pi 进程相进入临界区,第3行代码表示如果 p j p_j pj 进程也想进入临界区,可以把机会让给他。由于 turn 是一个变量,所以 ij 只允许等于一个值,就解决了双标志后检查法的饥饿现象。

比如再次出现双标志后检查法出现饥饿现象的执行顺序,2-9 之后 `turn 只能为i或为j,当turn为i时,11行代码处于循环状态,不能继续向下执行,于是系统执行进程 p i p_i pi;当turn为j时,4行代码处于循环状态,不能继续向下执行,于是系统执行进程 p j p_j pj但无法实现“让权等待”原则


五、中断屏蔽方法

当一个进程正在使用处理机执行它的临界区代码时,防止其他进程进入其临界区进行访问的最简单方法是——禁止一切中断发生,或称之为屏蔽中断(指屏蔽掉中断的发生,不再提供中断服务)、关中断(指关闭中断,不再提供中断服务)

因为CPU只在发生中断的时候引起进程切换,因此屏蔽中断能够保证当前运行的进程让临界区代码顺利的执行完,进而保证互斥的正确实现,然后执行开中断

   …… 关中断;
   //关闭中断服务,不允许中断发生 临界区; 
   开中断;
   //重新打开中断服务,允许中断发生              

这种方法固然解决了进程单独进入临界区的问题,但是也限制了处理机交替之行程序的能力,因此执行的效率会明显降低。

对内核来说,在他执行更新变量或列表的几条指令期间,关中断是很方便的,但,将关中断权力交给用户则很不明智,若一个进程关中断后不再开中断,则系统可能会因此终止


六、硬件指令方法

TestAndSet指令:这条指令时原子操作,即执行该代码是不允许被中断。其功能是读出指定标志后把该标志设置为真。

 boolean TestAndSet(boolean *lock){ 
     boolean old; 	
     old=*lock;  	
     *lock=true;   	
     return old;
 }

可以为每个临界资源设置一个共享的boolean类型的变量lock,表示资源有两种状态:true表示正被占用,初始值为false。在进程访问临界资源之前,利用TestAndSet检查和修改标志lock;若有进程在临界区,则重复检查,直到进程退出。

while TestAndSet(&lock); 
critical section; 
lock=false;       

Swap指令:该指令的功能是交换两个字(字节)的内容

 Swap(boolean *a, boolean *b){ 
     boolean temp; 	
     temp = *a;  	
     *a = *b;   	
     *b = temp;
 }

以上只是对TestAndSet和Swap指令的描述仅仅是功能上的实现,而并非软件实现的定义。事实上它们是由硬件逻辑直接实现的,不会被中断。

应为每个临界资源设置一个共享的boolean类型的变量lock,初值为false;在每个进程中再设置一个局部的boolean类型的变量key,用于与lock交换信息。在进入临界区之前,先利用Swap指令交换lock和key的内容,然后检查key的状态;若有进程在临界区,则重复检查,直到进程退出。

key = true; 
while(key!=false);
Swap(&lock, &key); 
critical section; 
lock = false;         

硬件方法的优点在于:适用于任意数目的进程而不需要考虑是单处理机还是多处理机。简单容易验证其正确性。可以支持进程内有多个临界区,只需要为每个临界区设置一个boolean变量

硬件方法的缺点在于:进程等待进入临界区时,要花费处理机的时间,不能实现让权等待。从等待进程中随机选择一个进入临界区,有的进程可能一直选不上,从而会导致饥饿现象。


信号量

学习的小问题:

什么是信号量?

我们需要用信号量来干什么?

信号量事如何工作的?

信号量机制是一种功能较强的机制,用于解决互斥和同步的问题,他只能被两个标准的原语wait(S)和signal(S)访问,也可以记作"P操作"和"V操作"。因为时原语指令,所以过程中不可被中断、不可被分割

整型信号量

  1. 用一个整型变量为信号量,数值表示某种资源
  2. 整型信号量与普通信号量之间的区别是,对信号量仅为初始化,P操作,V操作三种
  3. 整型信号量存在的问题:不满足让权等待
wait(S){ 
    while(S<=0);
    S=S-1; 
} 

signal(S){ 
    S=S+1;
}   

在wait操作中,只要S<=0,就会不断的测试,因此没有遵循让权等待,而是使进程处于忙等状态

记录型信号量

记录型信号不存在"忙等"现象(为什么?)

处理饿需要一个用于代表资源数目的整型变量value之外,再增加一个进程链表L,用于链接所有等待该资源的的进程

记录型信号量得名于采用用了记录型数据结构

该数据结构为

typedef struct{ 
    int value; 	
    struct process *L;
}

相应的wait操作为

void wait(semaphore S){
//P操作 
    S.value--; 	
    if(S.value<0){
        add this process to S.L;
        block(S.L);
    }
}

wait操作,S.value–表示进程请求一个该资源类,当S.value<0时,表示该资源已经被分配完毕,因此进程应调用block语句进行自我阻塞,放弃处理机(解决了"忙等"),并插入该类资源的等待队列S.L

void signal(semaphore S){
    //V操作 
    S.value++; 	
    if(S.value<=0){ 	
        remove a process P from S.L;  		
        wakeup(P); 
    }
}

signal 操作,表示进程释放一个资源,使系统中可供分配的该类资源数目+1,固有 S.value++。若 +1 之后仍是 S.value<=0,则表示在S.L中仍有等待该资源的进程被阻塞,顾还应调用 wakeup 原语,将 S.L 中第一个等待进程唤醒(比如当进程 P i P_i Pi 完成 wait 操作之后,S.value=-1,进程 P i P_i Pi 自我阻塞,并进入 S.L ;在进程 P j P_j Pj 完成 signal 操作,经过 S.value++ 之后,S.value=0,此时应调用wakeup ,将进程 P i P_i Pi 唤醒)

  1. S.value表示某种资源数,S.L指向等待该资源的进程数
  2. P操作中,一定是先S.value–,之后才能执行block原语
  3. V操作中,一定是先S.value++,之后才能执行wakeup原语
  4. 注意⚠️:要能够知道在什么条件下执行block和wakeup
  5. 可以用记录型信号实现系统资源的"请求"和"释放"
  6. 可以用记录型信号实现同步和互斥问题

利用信号量实现进程同步

用一个例子说明

semaphore S=0; 
P1(){ 	
	x;  
    V(S);	//S++   
    ... 
} 
P2(){
    ... 	
    P(S);	//S--  	
    y;   
    ...  
}

当P2进程先执行到第9行,S初始值为0,执行完第9行的代码之后,S=-1,此时P操作就会把进程P2阻塞,并放入阻塞队列;当进程P1中第3行执行完毕,执行V操作也就是代码第4行,此时S++,执行完后S=0,唤醒进程P2进入就绪队列中。当P2得到处理机时,得以继续执行

利用信号量实现进程互斥

还是用一个例子说明

semaphore S=1//初始化信号量 
P1(){ 
    ... 
    P(S);	// 准备开始访问临界资源,加锁 	
    critical section;  	
    V(S);	
    //访问结束,解锁   	
    ... 
} 
P2(){ 
    ... 	
    P(S);	// 准备开始访问临界资源,加锁  	
    critical section;   
    V(S);	//访问结束,解锁      
}

以上代码的意思是,当没有进程在临界区时,,任意一个进程要进入临界区,就要执行P操作,把S的值减为0,然后进入临界区;当有进程存在于临界区的时候,S=0,再要进入临界区,S=-1,会被P阻塞。直到临界区中的进程退出,这样便实现了临界区的互斥

利用信号量实现前驱关系

实现如下顺序(S1-S3=a1,S1-S2=a2,S2-S4=b1,S2-S5=b2,S3-S6=c,S4-S6=d,S5-S6=e)

image-20220602134546206
semaphore a1=a2=b1=b2=c=d=e=0; 
S1(){ 
    V(a1),V(a2); 
} 
S2(){ 
    P(a2); 	
    ...  	
    V(b1),V(b2); 
} 
S3(){
    P(a1); 	
    ...  	
    V(c);
} 
S4(){ 
    P(b1); 	
    ...  	
    V(d); 
} 
S5(){    
    P(b2);    
    ...    
    V(e); 
} 
S6(){ 
    P(c); 	
    P(d);  	
    P(e); 
}

分析进程同步和互斥问题的方法步骤

  1. 分析关系,找出问题中的进程数,并分析它们之间的同步和互斥关系
  2. 整理思路

管程

什么是管程?

管程的作用是什么?

管程是由一组数据及定义在这组数据之上的、对这组数据的操作组成的软件模块,这组操作初始化并改变管程中的数据和同步进程(抽象类)

管程的组成

  • 局部于管程的共享结构数据说明(共享数据结构)
  • 对该数据结构进行操作的一组过程(一组用来访问数据结构的过程-函数)
  • 对局部于管程的共享数据设置初始值的语句(对数据结构初始化的语句)

管程的基本特性

  1. 局部于管程的数据只能被局部于管程内的过程所访问
  2. 一个进程只能通过调用管程内的过程才能进入管程访问共享数据
  3. 每次仅允许一个进程在管程内执行某个内部过程

第1、2条,可以总结为:各外部进程/线程只能通过管理提供的特定"入口"才能访问共享数据

经典同步问题

一、生产者-消费者问题

总结为生产多少,消费多少

semaphore mutex = 1;	//临界区互斥信号量 
semaphore empty = n;	//空闲缓冲区,容器 
semaphore full = 0;		//缓冲区初始化为空,盘子 
producer(){ 
    while(1){ 	
        produce an item in nextp;  		
        P(empty);	//装东西的容器空容量-1    	
        P(mutex);	//进入临界区     	
        critical section;      	
        V(mutex);	//撤出临界区       	
        V(full);	//盘子中的产品+1 
    } 
} 
consumer(){ 
    while(1){     	
        P(full);	//消耗盘子中的东西      	
        P(mutex);	//进入临界区       	
        critical setcion,remove an item from buffer;        	
        V(mutex);	//撤出临界区         	
        V(empty);	//容器空容量+1          	
        remainder section; 
    }              
}

二、读者-写者问题

有读者、写者两组并发的进程,共享一个文件,当两个或两个以上的读进程访问共享数据时,不会产生副作用,但若某一个写进程和其他进程(读进程或写进程)同时访问临界资源的时候,会导致数据不一致的错误。

读者-写者问题的要求

  1. 允许多个读者可以同时对文件执行读操作
  2. 只允许一个写者往文件中写信息
  3. 任一写者在完成写操作之前不允许其他读者或写者工作
  4. 写者使用写操作之前,应让已有的读者和写者全部退出
int count = 0;	//用以记录当前读者数量 
semaphore mutex = 1;//保证修改count变量时互斥的标记 
semaphore rw = 1;	//用于保证读者和写者互斥的访问文件 
writer(){ 
    while(1){ 	
        P(rw);	//互斥访问共享文件  		
        critical section,writing;    	
        V(rw);	//释放共享文件 
    } 
} 
reader(){ 
    while(1){ 	
        P(mutex);	//互斥访问count变量,确认有多少度进程  		
        if(count==0)	//当第一个读进程读共享文件时    		
            P(rw);	//阻止写进程      	
        count++;	//读者计数器+1       	
        V(mutex);	//退出访问互斥变量count        	
        reading;         	
        P(mutex);	 	
        count--;  		
        if(count==0)	//当最后一个读进程读完共享程序    		
            V(rw);	//允许写进程写      	
        V(mutex); 
    }           
}

上述算法一读进程优先,也就是存在读进程时,写进程会被延迟。

这种算法的缺点在于:只要有一个读进程活跃,随后而来的读进程都将被允许访问文件。这种方式会导致写进程 可能长时间等待,且存在写进程"饿死"的情况

改进上述程序,若要求写进程又看,也就是当有读进程正在读共享文件的时候,有写进程请求访问,这时应禁止后续读进程的请求,等到已在共享文件的读进程执行完毕,立即让写进程执行,只有在无写进程执行的情况下才允许读进程才只允许。

写优先:

int count = 0;	//用以记录当前读者数量 
semaphore mutex = 1;//保证修改count变量时互斥的标记 
semaphore rw = 1;	//用于保证读者和写者互斥的访问文件 
semaphpre w = 1;	//用于实现写优先 
writer(){ 
    while(1){     	
        P(w);	//在无写进程请求时进入 	
        P(rw);	//互斥访问共享文件  		
        critical section,writing;    	
        V(rw);	//释放共享文件     	
        V(w); 
    } 
} 
reader(){ 
    while(1){     	
        P(w); 	
        P(mutex);	//互斥访问count变量,确认有多少度进程  		
        if(count==0)	//当第一个读进程读共享文件时    		
            P(rw);	//阻止写进程      	
        count++;	//读者计数器+1       	
        V(mutex);	//退出访问互斥变量count        	
        V(w);        	
        reading;         	
        P(mutex);	 	
        count--;  		
        if(count==0)	//当最后一个读进程读完共享程序    		
            V(rw);	//允许写进程写      	
        V(mutex); 
    }         
}  

读者-写者问题有一个关键特征,即有一个互斥访问量count(用来控制读者-写者两组进程之间互斥的),因此遇到一个不太好解决的同步互斥时,可以采用互斥访问的计数器count能否解决问题。

三、哲学家进餐的问题

同时拿左边的筷子或者同时纳右边的筷子都会发生死锁。

为了防止死锁,有两种方案

1.可以约束至多允许4(共5名)名哲学家同时进餐,当且仅当哲学家两边的筷子都可用时,才允许他抓起筷子

semaphore chopstick[5]={1,1,1,1,1} 
semaphore mutex = 4; 
Pi(){ 
    while(1){     
        if(mutex>0){  		
            P(chopstick[i]);    	
            P(chopstick[(i+1)%5]);      	
            eat;       	
            V(chopstick[i]);        	
            V(chopstick[(i+1)%5]);         	
            think;          	
            mutex--; 
        } else { 	
            P(chopstick[i]);    	
            P(chopstick[(i+1)%5]);      	
            eat;       	
            V(chopstick[i]);        	
            V(chopstick[(i+1)%5]);         	
            think; 
        } 
    }     
}

2.等于哲学家顺序编号,要求奇数号哲学家先拿左边的筷子,然后再拿右边的筷子,而偶数号哲学家刚好相反

semaphore chopstick[5]={1,1,1,1,1} 
semaphore mutex = 1; 
Pi(){ 
    do{ 	
        P(mutex);  		
        P(chopstick[i]);    	
        P(chopstick[(i+1)%5]);     	
        V(mutex);      	
        eat;       	
        V(chopstick[i]);        	
        V(chopstick[(i+1)%5]);         	
        think; 
    }while(1);
}

四、吸烟者问题

假设有一个系统有三个吸烟者进程和一个供应者进程。每个吸烟者不停的卷烟并抽掉它,但要卷起并抽掉一支烟,抽烟者需要有三种材料:烟草、纸和胶水。三个抽烟者中,一个拥有烟草、一个拥有纸、一个拥有胶水。供应者进程无限地提供三种材料,供应者每次将两种材料放到桌子上,拥有剩下那种材料的抽烟者卷一根烟并抽掉它,并给供应者一个信号告诉已完成,此时供应者就会将另外两种放在桌子上(以此重复)

int random; 
semaphore offer1 = 0;	//定义信号量对应烟草和纸组合资源 
semaphore offer2 = 0;	//定义信号量对应烟草和胶水组合资源 
semaphore offer3 = 0;	//定义信号量对应纸和胶水组合的资源 
semaphore finish = 0;	//定义信号量表示抽烟是否完成 
P1() {			//供应者    
    while(1){ 
        random=任意一个随机整数; 	
        random = random % 3;  	
        if(random==0)   	
            V(offer1);    
        else if(random==1)    	
            V(offer2);     
        else      	
            V(offer3);      //任意两种材料放在桌子上      
        P(finish); 
    } 
} 
P2(){ 
    while(1){ 	
        P(offer3);  		//拿纸和胶水,卷成烟、抽掉    	
        V(finish); 
    } 
} 
P3(){ 
    while(1){ 	
        P(offer2);  		//拿烟草和胶水,卷成烟、抽掉    	
        V(finish); 
    } 
} 
P4(){ 
    while(1){ 	
        P(offer1);  		//拿烟草和纸,卷成烟、抽掉    	
        V(finish); 
    }          
}
2.5 死锁

死锁的概念

所谓的死锁,是指多个进程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进

死锁产生的原因

(1)系统资源竞争:通常系统中拥有的不可剥夺资源,其数量不足以满足多个进程运行的需要,使得进程在运行过程中,会因 争夺资源而陷入僵局。

(2)进程推进顺序非法

(3)死锁产生的4个必要条件

  • 互斥性
  • 不可剥夺性
  • 请求并保持
  • 循环等待

死锁的处理策略

死锁的处理策略有3种

  • 死锁预防
  • 避免死锁
  • 死锁的检测及接触

死锁预防

设置某些限制条件,破坏产生死锁的4个必要条件 中的一个或几个,以防止发生死锁。

  • 破坏互斥条件

    • 临界资源改造为可共享使用的资源
    • 缺点:可行性不高,很多时候无法破坏互斥条件
  • 破坏不可剥夺条件

    • 一、申请的资源不到满足时,立即释放拥有者的所有资源
    • 二、申请的资源被其他进程占用时,由操作系统协助剥夺(考虑优先级)
    • 缺点:实现复杂;剥夺资源可能导致部分工作失效;复杂申请和释放导致系统开销大;可能导致饥饿
  • 破坏请求和保持条件

    • 运行前分配好所有资源,之后一直保持
    • 缺点:资源利用率低,可能导致饥饿
  • 破坏循环等待条件

    • 给资源编号,必须按编号从小到大的顺序中
    • 缺点:不方便增加新设备;会导致资源浪费,用户编程麻烦

死锁避免

  • 系统安全状态:允许进程动态地申请资源,但系统在进行资源分配之前,应先计算此次资源分配的安全性。但是并非所有不安全算法都是死锁状态,但是当系统进入不安全状态后,便可能进入死锁状态;反之,只要系统处于安全状态,系统就可以避免死锁。
  • 银行家算法

死锁检测和解除

  • 资源分配图

  • 死锁定理

  • 死锁解除

    • 资源剥夺法。挂起某些死锁进程,并抢占它所有的资源,将这些资源分配给其他的死锁进程。但是,应防止被挂起的进程长时间得不到资源而处于资源匮乏状态
    • 撤销进程法。强制撤销部分甚至全部死锁进程,并剥夺这些进程的资源。撤销的原则可以按进程优先级和撤销进程代价的高低进程。
    • 进程回退法。让一(或多)个进程回退到足以回避死锁的地步,进程回退时资源释放资源而非被剥夺。要求系统保持进程的历史信息,设置还原点。
  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_之桐_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值