【操作系统】第二章 进程与线程

第二章 进程与线程

2.1 进程与线程

一、进程的概念和特征

1.概念

在多道程序环境下,允许多个程序并发执行,此时他们会失去封闭性。为此引入进程的概念,以便更好的描述和控制程序的并发执行,实现操作系统的两个基本特性——并发性和共享性

为了使参与并发执行的每个程序都可独立的运行,还必须为之配置一个专门的数据结构——进程控制块(PCB)。系统利用PCB来描述进程的基本情况和运行状态,进而控制和管理进程。而由程序段、相应数据段和PCB三部分构成了进程实体。进程是进程实体运行的过程,是系统进行资源分配和调度的一个独立单位。所谓创建进程,实际上是创建进程实体中的PCB,撤销进程,撤销的是进程实体中的PCB,因此,PCB是进程存在的唯一标志

进程定义:

  1. 进程是程序的一次执行的过程
  2. 进程是一个程序及其数据在处理机上顺序执行时所发生的活动
  3. 进程是具有独立功能的程序在一个数据集合上运行的过程,他是系统资源分配和调度的一个单位
2.特征

进程的特征是对比单个程序的顺序执行提出的,是对进程管理的基本要求

  • 动态性:进程是程序的一次执行,具有一定的生命周期,是动态的产生、变化和消亡的,动态性是进程的基本特性
  • 并发性:多个进程实体同时存在于内存中,在一段时间内可以同时执行
  • 独立性:进程实体是一个能够独立运行、独立获得资源和独立接受调度的基本单位
  • 异步性:由于进程的相互制约,使得进程是按照各自独立、不可预知的速度向前推进的。

二、进程的状态与转换

进程在其生命周期内,一般有以下五种状态,前三种为进程的基本状态

  • 运行态:正常的运行状态
  • 就绪态:进程获得了除处理器外的一切所需资源,等待分配处理机
  • 阻塞态:进程正在等待某一事件而停止运行,比如等待某资源可用(比如说打印机)或者在等待用户的输入输出。这时即使处理机空闲也无法向下运行
  • 创建态:正在创建,尚未转到就绪态。创建进程步骤如下:申请空白PCB,并且向PCB中写入用于控制和管理进程的信息;然后为该进程分配运行所需资源,最后把该进程转为就绪态,并且插入到就绪队列。
  • 结束态:正在回收资源和释放资源的进程状态。

其中的关系转换图如下:

三、进程的组织

进程是一个独立的运行单位,其中最核心的为进程控制块(PCB)

1.进程控制块

进程创建的时候,操作系统为他创建一个PCB,该结构以后会常驻内存,任何时候都可存取,在进程结束时删除。

进程控制块的作用:当操作系统准备调度某进程运行时,会从中查出该进程的现在状态和优先级;

在调度到某进程后,要根据其PCB所保存的处理机信息,设置该进程恢复运行的现场;

进程在运行过程中,当需要和与之合作的进程同步、通信或者访问文件的时候,也要访问PCB;

进程由于某种原因暂停运行时,需要将其断点的处理机环境保存在PCB中。

还有许多作用尚未列出…

进程控制块的主要包含以下几项内容:

1.进程描述信息2.进程控制和管理信息3.资源分配清单4.处理机相关信息

在一个系统中,通常存在许多进程PCB,为了方便调度和管理,需要将各个进程的PCB组织起来,常用的方法为链接方式索引方式。连接方式将同一状态的PCB链接成一个队列,不同状态对应不同队列。索引方式将同一状态的进程组织在一个索引表汇总,索引表指向相应的PCB,不同的状态对应不同的索引表。

2.程序块

能被进程调度到CPU执行的程序段代码,程序可以被多个进程共享,即多个进程运行同一程序。

3.数据段

进程运行中需要的原始数据,以及运行过程中产生的数据还有结果数据

四、进程控制

在操作系统中,进程控制的程序段是一种源语

进程的创建

允许一个进程创建另一个进程,此时进程创建着为父进程,被创建的进程为子进程。子进程可以继承父进程所有资源,子进程撤销的时候会归还资源给父进程,父进程撤销时一带撤销其子进程。操作系统创建一个新的进程的过程称之为创建原语,如下:

  1. 为新进程分配进程标识号,并且申请空白PCB。PCB有限,申请失败则创建失败
  2. 为进程分配其运行所需的资源,这些资源可以从父进程或者系统中获得。如果资源不足,则处于创建态并等待资源分配
  3. 初始化PCB,主要包括初始化标志信息,初始化处理机状态信息和初始化处理机控制信息,并且设置有优先级。
  4. 如果进程就绪队列能够接纳新进程,则将新进程插入到就绪队列,等待调度。

进程的终止

引起进程终止的主要事件有:1.正常结束 2.异常结束 3.外界干预,比如操作员或者父进程请求终止

操作系统终止进程的过程称为终止原语,如下:

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

进程的阻塞或者唤醒

正在执行的进程,如果请求系统资源失败或者等待某操作完成,进程可以通过调用阻塞原语是的自己由运行态转化为阻塞态。阻塞时进程自身的一种主动行为,因此也只有处于运行态的进程可以转化为阻塞态,阻塞原语如下:

  1. 找到被阻塞进程的标识号对应的PCB
  2. 如果该进程为运行态,则保护其运行现场,并且将他们转化为阻塞态
  3. 将PCB插入到相应事件的等待序列,将处理机释放

当被阻塞的进程的资源到位了,或者目标事件出现了,有关进程则会自己调用唤醒原语:

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

五、进程的通信

进程通信是指进程之间的信息交换,PV操作室低级通信方式,而高级的通信方式可以以较高的效率传输大量数据的通信方式,有以下三类

1.共享存储

通信的进程之间有一块可以直接访问的共享空间,通过对这片共享空间进行读写操作实现进程之间的信息互换。共享存储有两种方式:低级的共享是基于数据结构的共享;高级共享是基于存储区的共享。

2.消息传递

进场之间的数据交换以格式化消息为单位。进程通过操作系统提供的发送信息和接收信息的原语进行数据交换,这是目前最广泛使用的进程通信方式,在微内核中,微内核与各个服务器之间的通信就采用了消息传递机制。该机制可以很好的支持多处理机系统、分布式系统和计算机网络。

消息传递有两种通信方式:

  1. 直接通信方式:发送进程直接把消息发送给接收进程,接收进程从消息缓冲队列中接收信息
  2. 间接通信方式:发送消息的进程将消息发送给某个中间实体,接收进程从中间实体之中提取信息,这种中间实体又称为信箱。
3.管道通信

管道通信是一种特殊的方式,所谓管道是指用于连接一个读进程和一个写进程的特殊共享文件。将管道提供输入的发送进程以字符流的方式将大量数据送入管道,而接收管道输出的接收进程则从管道中读取数据。为了协调双方通信,管道机制必须要提供以下三方面的协调能力:互斥、同步和确认对方存在。

管道只能采用半双工通信,要实现父子进程互动通信需要定义两个管道

共享存储方式再对共享空间操作的时候必须要先确认没有其他进程在操作。但是管道通信只要管道内有数据写进程就不会写入数据,所以省去了判断步骤。

六、线程及其概念

进程的目的是更好的使多道程序并发执行,而引入线程的目的是见效程序在并发执行的时候所付出的时空开销,提高操作系统并发性能

对线程的最简单理解为“轻量级的进程”。它是一个基本的CPU执行单元,也是程序执行流的最小单元。线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点必不可少的资源,但是可以和同属一个进程的其他线程共享进程拥有的资源。

  1. 线程是一个轻型实体,每个线程都有一个唯一的标识符以及线程控制块
  2. 不同的线程可以执行相同的程序,同一个服务程序被不同用户调用时,操作系统把它创建为不同的线程
1.线程和进程的比较

1.调度:传统操作系统中有资源和独立调度的基本单位是进程,每次调度进行上下文切换,开销会很大。而在同一进程内线程互相切换开销则小的多。

2.并发性:不仅进程之间可以并发执行,而且一个进程内的线程也可以并发执行,甚至不同进程中的线程也能并发执行,从而使得操作系统有更好的并发性

3.拥有资源:线程只拥有少量运行所需的资源,而又可以共享进程的资源,这种特性使得线程之间的切换的时空开销比较小。

4.独立性:每个进程都有独立的内存空间和资源,不允许其他进程访问。而同一进程内的不同线程是为了提高并发性而设立的,因此他们会共享内存空间和资源。

5.系统开销:创建和撤销进程的时候,系统要为之分配和回收内存和资源,因此会导致开销。在切换进程的时候涉及到进程上下文切换,而切换线程只需要保存和设置少量寄存器的内容,开销很少。

6.支持多处理机系统:如果只有单线程进程,则该进程只能运行在一个处理机上,而引入线程后,拥有多个线程的进程可以在多个处理机上运行

2.线程的状态与转换

线程在运行时具有三种状态:执行状态、就绪状态、阻塞状态

3.线程的组织和控制

(1)线程控制块

系统为每个线程配置一个线程控制块(TCB),用于记录控制和管理线程的信息。TCB包括:1.线程标识符、2.一组寄存器、3.线程运行状态:保存程序运行状态 4.优先级 5.线程专有存储区 6.堆栈指针

(2)线程的创建

线程也是有生命期的。用户程序启动的时候,通常只有一个初始化线程在执行,其主要功能在于新建新线程。在创建新线程的时候,需要利用一个线程创建函数进行创建

(3)线程的终止

4.线程的实现方式

(1)用户级线程

用户级线程中,有关线程管理的所有工作都是在用户空间由应用程序完成的,内核是意识不到的。因为内核不清楚用户进程中有多少个线程,因此对于每个进程依然是分配一个时间片,一个只有一个用户级线程的进程的运行速度是一个有100个用户及线程的进程的100倍。

这样做的优点在于:1.线程切换不需要转换到内核空间,节省了开销 2.进程可以根据自身需求选择不同的线程调度算法,更灵活 3.用户级线程和平台无关,是程序的一部分,可移植性好

缺点在于:系统调用阻塞一个用户级线程的时候,会将同一进程下的所有用户级线程阻塞,而内核级线程可以单独阻塞一个线程。主要原因是内核无法意识到用户级线程,只能以进程为单位进行阻塞;同样地,内核意识不到用户级线程也导致不能将同一进程下的多个用户级线程分配给不同的处理机,因此无法发挥多处理机的效率优势

(2)内核级线程

内核级线程管理的所有工作也是在内核空间内实现的。内核空间也为每一个内核级线程设置了一个线程控制块,这也是内核可以意识到内核级线程的原因(用户级线程的控制块在用户空间)

优点如下:1.能发挥多处理机的优势 2.一个线程被阻塞,可以直接让同一进程的其他线程接管处理机,而不是进行开销较大的进程切换 3.内核支持线程具有很小的数据结构和堆栈,线程切换比较快

缺点如下:1.同一进程内的线程切换需要从用户态切换到核心态,系统开销大。

(3)组合方式

有些系统使用两者组合的方式。在组合方式中,内核支持多个内核级线程的建立,同时支持用户程序建立用户级线程。一些内核级线程对应一个或者多个用户级线程,这时用户级线程可以时分复用内核级线程,从而使得吸收了两者优点又克服了两者缺点。

在线程实现方式中,通过线程库建立和管理线程是常用方法,可以为程序员提供管理线程的API。

5.多线程模型

有些系统同时支持用户级线程和内核级线程,由于用户级线程和内核级线程的连接方式的不同,分为了以下三种多线程模型

  1. 多对一模型。将多个用户级线程映射到一个内核级线程,线程的调度和管理在用户空间完成,当用户线程需要访问内核的时候才映射到内核级线程中,但是每次只允许一个线程映射。优点在于线程管理是在用户空间进行的,效率较高。缺点在于如果一个线程访问内核时阻塞,会导致其他线程也无法访问内核
  2. 一对一模型。优点是一个进程被阻塞不会影响到其他进程,并发性能好。缺点是每创建一个用户级线程就要创建一个内核级线程,开销大。
  3. 多对多模型,将n个用户级线程映射到m个内核级线程上(m<n),克服了多对一的并发性问题,又避免了一对一模型的大开销

2.2 处理机调度

一、调度的概念

1.调度的基本概念呢

在多道程序系统中,进程数量往往多于处理机数量,因此需要进程争用处理机。处理机调度是对处理机进行分配,按照一定的算法将处理机分配个进程

2.调度的层次

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

名称作用特点
高级调度(作业调度)从外存中的后备队列的作业中挑选作业分配内存等资源是内存和辅存之间的调度
中级调度(内存调度)将暂时不能运行的程序调出外存,又称为挂起;具备了运行条件后又将进程调入内存,并且使得进程就绪。目的是提高内存利用率和系统吞吐量
低级调度(进程调度)从就绪队列中选取进程分配处理机最基本的调度,频率高

3.三级调度的联系

  1. 作业调度为进程活动做准备,进程调度使得进程活动
  2. 中级调度将暂时不能运行的挂起,处于两个调度之间
  3. 调度层级越高频率越低

二、调度的目标

不同的调度算法具有不同特性,可以实现不同的目的,为了评价不同的算法性能,人们提出了许多标准,比如:

  1. CPU利用率:
  2. 系统吞吐量:表示单位时间内CPU完成的作业数量
  3. 周转时间:从作业提交到作业完成所经历的时间
  4. 等待时间:进程处于等待处理机的时间点和
  5. 响应时间:用户提交请求到系统首次产生响应所花费的时间

三、调度效率的衡量标准

  1. CPU利用率: C P U 利用率 = C P U 有效工作时间 有效工作时间 + 空闲等待时间 CPU利用率=\frac{CPU有效工作时间}{有效工作时间+空闲等待时间} CPU利用率=有效工作时间+空闲等待时间CPU有效工作时间
  2. 系统吞吐量:单位时间内CPU完成作业的数量。长作业需要消耗更长时间,降低系统吞吐量,短作业反之
  3. 周转时间:从作业提交到作业完成所经历的时间。周转时间=作业完成时间-作业提交时间
    平均周转时间 = ( 作业 1 周转时间 + 作业 2 周转时间 + . . . . . + 作业 n 周转时间 ) / n 平均周转时间=(作业1周转时间+作业2周转时间+.....+作业n周转时间)/n 平均周转时间=(作业1周转时间+作业2周转时间+.....+作业n周转时间)/n
    带权周转时间是指作业周转时间和作业实际周转时间的比值:
    带权周转时间 = 作业周转时间 作业实际运行时间 带权周转时间=\frac{作业周转时间}{作业实际运行时间} 带权周转时间=作业实际运行时间作业周转时间
  4. 等待时间:指的是进程处于等待处理机的时间之和。是影响用户满意度的重要条件。
  5. 响应时间:指用户提交请求到系统首次响应所用的时间。在交互式系统中比较重要。

四、调度的实现

1.调度程序(调度器)

在操作系统中用于调度和分派CPU的组件称为调度程序,由三部分组成

(1)排队器:将系统中的所有就绪进程按照一定策略排成一个或者多个队列,以便调度程序选择。

(2)分派器:根据调度程序所选的进程,将其从就绪队列中取出,将CPU分配给他。

(3)上下文切换器:在对处理机进行切换的时候,会发生两对上下文切换操作:第一对,将当前的进程的上下文保存到其PCB中,装入分派程序的上下文,运行分派程序选中新进程;第二对,移出分派程序的上下文,将新选中的CPU现场信息装入处理机的各个寄存器中。

2.调度的时机、切换与过程

调度程序是操作系统内核程序,请求调度的事件发生后才运行调度程序,而调度程序调度了新的就绪程序后,才会进行进程切换。这三件事一般应该顺序执行,但是有些情况不允许马上进行调度和切换,比如:

  • 在处理中断的过程中。中断处理很复杂,并且是系统工作的一部分,应该等到中断处理完毕后再调度或切换进程。
  • 进程在操作系统内核临界区中。进入临界区后,需要独占式访问,理论上必须加锁以防止其他并行进程进入。
  • 其他需要完全屏蔽中断的原子操作。在原子操作中,连中断都要拼屏蔽,更别说进程调度了。

应发起调度的时机:

  • 发生了引起调度条件并且当前进程无法继续执行的时候,为非剥夺调度
  • 中断处理结束或者自陷结束后,返回到被中断的用户态程序执行现场前,若置上请求调度标志,则可以立马进行调度和切换

3.进程调度方式

进程调度的方式指的是某个进程在处理机上执行的时候,如果有某个更重要的进程需要处理,应该如何分配处理机,通常有两种处理方式:

(1)非抢占调度方式:如果有更紧急的进程出现,也等当前进程执行完毕或者阻塞的时候才出让处理机。优点实现简单、系统开销小,但响应时间慢,适用于多数批处理系统

(2)抢占调度方式:如果有更紧急的进程出现,则允许暂停当前进程,将处理机分配给紧急进程。

4.闲逛进程

如果系统中没有就绪进程,就会执行闲逛进程,并在执行过程中测试中断。闲逛进程优先级最低,不会被阻塞。

五、典型的调度算法

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

最简单的调度算法,既可用于作业调度又可以用于进程调度。该算法每次从就绪队列中选择最先进入该队列的进程,将处理机分配个他,直到运行完毕或者由于某些愿意阻塞了。

FCFS算法属于不可剥夺算法,从表面上是公平的,但是如果一个长作业先到达系统,就会使得后面的短作业需要等待很长时间,因此难以胜任分时系统和实时系统的主要调度策略。但是他时常被结合在其他调度策略中使用。比如在优先级调度策略中,对于相同优先级的作业一般采用FCFS。

FCFS的特点是算法简单,但是效率比较低,对长作业比较有利但是对短作业不利

2.短作业优先(SJF)调度算法

短作业/进程优先调度算法是从后备队列中选择一个或者若干个预估运行时间最短的作业,将他们调入内存运行,而在进程调度上也同理。该算法的优点在于:平均等待时间、平均周转时间最少。

但是也有不可忽视的缺点:算法对长作业不利,该算法中长作业周转时间会变长,如果不断的有短作业进入,会导致长作业一直不被处理,这种现象被称为饥饿。该算法完全不考虑作业的紧迫程度,因此不能保证紧迫作业会被立即处理。作业是根据预估运行时间排序的,而用户可能会有意缩短作业预估运行时间。

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

优先级调度算法既可以用于作业调度也可以用于进程调度。该算法中的优先级用于描述作业的紧迫程度。在作业调度中,会选择优先级最高的作业创建进程并且放入就绪队列;而在进程调度中,会将处理机分配个优先级最高的进程。

根据新的更高优先级的进程能否抢占正在执行的进程,可以讲调度算法分为非抢占式优先级调度算法抢占式优先级调度算法。而根据进程创建后优先级是否可以改变又可以分为静态优先级动态优先级

进程优先级的设计参考以下原则:系统进程>用户进程,交互型进程>非交互型进程,IO型进程>计算型进程

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

该算法主要用于作业调度,是对FCFS和SJF的一种综合,同时考虑了作业的等待时间和预估运行时间,通过计算响应比求出最高响应比的作业优先推入就绪队列

响应比 R   p = ( 等待时间 + 要求服务时间 ) 要求服务时间 响应比R~p = \frac{(等待时间+要求服务时间)}{要求服务时间} 响应比R p=要求服务时间(等待时间+要求服务时间)

根据公式可以知道,作业等待时间相同,那么要求服务时间越短响应比越高,有利于短作业;而要求服务时间相同的时候,等待时间越长其响应比越高。并且只要长作业等到足够长时间,响应比就会上升,最终也会被处理,从而避免了饥饿现象。

5.时间片轮转调度算法

主要是用于分时系统,在该算法中,系统将所有就绪进程按照FCFS排成就绪队列,逐个获得处理机使用权,但是引入了分片的概念,在时间片耗尽之后,会强制将中断当前进程,使得当前进程回到队尾排队,将处理机使用权交给下一个进程。类似于一堆小朋友排队吃饭,每个小朋友只能在饭桌前吃1分钟,1分钟后自动回到队尾重新排队,吃饱的自动离队。

6.多级队列调度算法

该算法在系统中设置多个就绪队列,可以将不同类型或者性质的进程固定分配到不同的就绪队列中,每个队列可以试试不同的调度算法,因此系统针对不同用户进程的需求,很容易可以提供多种调度策略,更为灵活。多级队列调度算法一般和其他调度算法混用

7.多级反馈队列调度算法

该算法是时间片轮转调度算法和优先级调度算法的综合,通过动态调整进程优先级和时间片大小,以兼顾多方面的系统目标。该算法的实现思想如下:

  1. 设置多个就绪队列,并且为每个队列设置优先级
  2. 赋予各个队列的时间片大小不一致,优先级越高的时间片越大,方便高优先级进程分配到更多处理机使用时间。
  3. 每个队列采用FCFS。

六、进程切换

进程的创建撤销和IO操作,都是利用系统调用来进入内核的。任何进程都是在操作系统内核的支持下运行的。

  1. 上下文切换

    切换CPU到另一个进程需要保存当前进程状态并且恢复另一个进程的状态,这个称为上下文切换。上下文切换是指某一时刻CPU寄存器和程序寄存器的内容。进行切换的时候,旧进程的状态会保存在其PCB中,然后加载新进程的上下文

    上下文切换的实质是处理机从一个进程转到另一个进程上执行,其流程如下:

    • 挂起一个进程,保存CPU上下文,包括程序计数器和其他寄存器
    • 更新PCB信息
    • 把进程PCB移动到对应队列
    • 选择新的进程执行, 更新PCB状态
    • 跳转到该进程的PCB种程序计数器所指位置执行
    • 恢复处理机上下文
  2. 上下文切换消耗:

    切换需要消耗可观的CPU时间

  3. 上下文切换和模式切换

    用户态和内核态之间的切换称为模式切换,模式切换时,CPU逻辑上可能还是在执行同一进程。但是上下文切换只发生在内核态,并且改变当前运行的进程

2.3 同步与互斥

一、基本概念

1.临界资源

一次只能给一个进程使用的资源称之为临界资源。许多物理设备属于临界资源,比如打印机。临界资源的访问必须互斥的进行,在每个进程中,访问临界资源的那段代码称为临界区。为了保证临界资源的正确使用,可以将临界资源的访问过程分为四个部分:

  • 进入区:进程在进入去检查是否可以进入临界区,如果可以就设置正在访问临界区的标志
  • 临界区:进程中访问临界资源的代码
  • 退出区:将正在访问临界区的标志清除
  • 剩余区:代码的其他部分

2.同步

同步又称直接制约关系,指的是为了完成任务而建立联系的两个或者多个进程,在某些位置上为了协调而相互等待、传递信息产生的制约关系。比如计算进程A等待输入进程B的输入信息而阻塞挂起,等到进程B输入信息后才被唤醒。

3.互斥

互斥又称为间接制约关系。当一个进程进入临界区使用临界资源的时候,另外一个进程必须等待。

为了进制两个进程同时进入临界区,同步机制必须遵守以下原则:

  • 空闲让进:临界区空闲可以允许一个请求进入临界区的进程立即进入
  • 忙则等待:已有进程进入临界区的时候,其他进程只能等待
  • 有限等待:等待的进程必须在有限的时间内进入临界区,避免进程饥饿
  • 让权等待:进程不能进入临界区的时候,应释放处理器,防止进程忙等

二、实现临界区互斥的基本方法

软件实现方法

1.算法一:单标志法

使用公共整型变量turn用于指示被允许进入临界区的进程编号。两个进程需要交替访问临界区,如果一个进程不打算访问临界区了,则会导致另外一个进程长时间等待,违背了“空闲让进”原则。也就是没办法连续执行数次同一个进程,只能数个进程轮流使用一个资源。

//P0进程

while (turn!=0){};

critical section; //临界区

turn = 1;

remainder section; //剩余区

//P1进程

while(turn!=1){};

critical section; //临界区

turn = 0;

remainder section;//剩余区

2.算法二:双标志先检查法

设置一个布尔型数组flag[],数组各个元素用来标记各进程是否想要进入临界区。每个进程在进入临界区之前首先检查收否有别的进程想要进入临界区,如果没有,则将自己对应的标志位flag[i]置为true。这种算法不需要交替进入,但是P1和P2可能同时进入临界区,也就是P1检查完flag数组后,正准备改变标志位的时候被中断,转而执行P2,此时P1的flag尚未为true,P2会认为没有进程要进入临界区,那么就会将自身的flag置为true并且开始执行。这违背忙则等待原则

//P1进程

while (flag[j]);

flag[i]=true;

critical section;

//P2进程

while(flag[i]);

flag[j]=true;

critical section;

3.算法三:双标志后检查法
和算法二相比,先检查,后上锁
可能会相互谦让导致谁也进不了临界区,违背了空闲让进有限等待原则

//P1进程

flag[i]=true;

while(flag[j]);

critical section;

flag[i]=false;

remainder section;

//P2进程

flag[j]=true;

while(flag[i]);

critical section;

flag[j]=false;

remainder section;

算法四:皮特森算法
结合双标志法和单标志法,如果双方都想要进入临界区,那么则会尝试谦让。这会使得最后执行谦让的进程P让出临界区执行资源,如果此时另外的进程P也要使用临界区资源,则会直接开始执行;如果P发现没人想要使用临界资源,也就是让了也没人要,那么自身就会开始执行临界区资源。皮特森算法遵循了空闲让进、忙则等待、有限等待三个原则,但是未遵循让权等待原则

//P1进程

flag[i]=True;turn=j;

while(flag[j] && turn==j);

critical section;

flag[i]=false;

remainder section;

//P2进程

flag[j]=True;turn=i;

while(flag[i] && turn==i);

critical section;

flag[j]=false;

remainder section;

硬件实现方法

1.中断屏蔽法

CPU只在中断发生的时候进行进程切换。因此最简单的实现方法,直接让处理机进行关中断操作,程序在临界区执行的时候不允许被中断,需要一气呵成的完成,从而实现互斥访问。但是该方法不适用于多处理机,因为单个处理机进行了关中断,但是其他处理机还是处于开中断状态,因此还是会有来自其他处理机的进程闯入临界区。不适用于用户态,因为开关中断是内核态操作,将如此关键的操作交由用户处理十分危险

2.硬指令方法

TestAndSet指令(TSL指令):这是一条原子操作指令,只能一气呵成地完成而不允许中断,其功能是读出标志后把该标志设为真,功能描述如下:

boolean TestAndSet(boolean *lock){

​ boolean old;

​ old=*lock;

​ *lock =true; // 无论是否已经上锁,都将锁置为true,以便在锁释放的瞬间可以抢占到锁;而如果是上锁状态再将lock置为true将不会产生任何影响

​ return old; //返回值,值如果是false则会跳出循环执行临界区内容;如果是true将会继续执行循环内容

}

在使用TestAndSet的例子如下:

while TestAndSet(&lock){};

临界区代码段;

lock = false;

进程其他代码

可以看出,使用TSL的时候,当临界资源被其他进程占用时(即lock=true时),则当前进程不会往下执行;一旦临界资源被释放,则当前进程会立马上锁并且执行代码

Swap指令:该指令的功能为交换两个字的内容,功能描述如下 :

Swap(boolean *a, boolean *b){

​ boolean temp;

​ temp = *a;

​ *a = *b;

​ *b =temp;

}

Swap指令的使用例子如下:

key=true;

while (key != false)

​ Swap(&lock, &key);

进程临界区代码段;

lock=false;

进程的其他代码;

Swap是一种简单互斥模型。使用时先将key=true,并且使用while循环不断交换key和lock的值,一旦lock的值为false,会将lock=true;key=false并且跳出循环,接着执行临界区代码。

硬件方法的优点在于:适用于任意数目的进程,不管是单处理机还是多处理机;简单、容易验证正确性;支持进程内有多个临界区。

缺点在于:进入等待区的时候要耗费处理机时间,不能实现让权等待,有的进程可能一直选不上而导致进程饥饿

三、互斥锁

解决临界区最简单的工具是互斥锁。一个进程在进入临界区的时候获得锁,而在退出临界区的时候释放锁。

acquire(){

​ while(!available){ };

​ available = false;

}

release(){

​ available=true;

}

acquire和release必须是原子操作。互斥锁的缺点在于忙等待。在没有获得临界资源的时候进程依旧占用处理机

四、信号量

信号量机制是一种功能强大的机制,可以用于解决互斥和同步问题,信号量只能被两个标准原语wait(S)和signal(S)访问,这两个操作又被称为P操作和V操作。原语是只能一气呵成执行完成,不可被中断的,这是通过关中断/开中断指令实现的

1.整型信号量

整型信号量只能进行初始化、P操作和V操作

wait(S){

​ while(S<=0){ };// 如果不足,则循环等待
​ S–; // 如果资源足够,则占用一个资源

}

signal(S){
​ S++; //释放资源
}

使用例子:
如果有一个打印机,则其中一个进程的执行如下:

int S = 1;
wait(S);
使用打印机
signal(S);

该操作依然不满足让权等待原则,会出现盲等。

2.记录型信号量

记录型信号量是一种满足让权等待原则的进程同步机制。拥有一个代表资源数目的变量value和一个记录等待资源分配的进程的链表L

typedef struct{
	int value;	//剩余资源数
	struct process *l;	//等待队列
}semaphore;

void wait(semaphore S){
	S.value--;	//占用一个资源
	if (S.value < 0){	//如果临接资源不够
		block(S.L);	//将该进程阻塞,并且挂在该临界资源的等待队列上
	}
}

void signal(semaphore S){
	S.value++;	//释放资源
	if (S.value <= 0)	// 如果value<=0,证明等待队列非空
		wakeup(S.L);	// 唤醒队列头的进程,将新释放的资源赋予给他
}

wait()和signal()分别用于请求和释放一类资源。当s.value<=0的时候,也就是表示该类资源已经分配完毕了,则当前进程会使用block原语阻塞自身,主动放弃处理机,符合了让权等待的原则

3.利用信号量实现同步

实现同步主要是要实现如下情形:进程B执行到某一位置的时候阻塞,等待当进程A执行到一定位置的时候,进程B才可以继续执行。进程同步是要让各个并发的进程有序地推进,让本来异步的各个进程有序进行。进程同步的信号量的P操作和V操作一般不在同一个进程中。

// P2进程的代码段4必须要在P1的代码段2执行完成后才能并发执行,实现如下
semaphore s = 0;
P1(){
	section 1;
	section 2;
	signal(s);
	section 3;
}

P2(){
	wait(s);
	section 4;
	section 5;
	section 6;
}
4.利用信号量实现进程互斥

有时候我们需要限制访问某个临界区代码或者临界区资源的进程数量,这可以通过信号量机制实现,信号量mutex表示的是进入临界区的名额

semaphore mutex = 1;

P1(){
	...
	P(mutex);
	critical section;	// 临界区代码段
	V(mutex);
	...
}

P2(){
	...
	P(mutex);
	critical section;
	V(mutex);
	...
}

进程互斥的信号量的P操作和V操作一般在同一个进程中。这使得每时每刻访问临界区代码段的进程只有一个。对于不同的临界区我们要学会设置不同的信号量

5.利用信号量实现前驱关系

使用进程的同步机制,可以实现前驱关系。
在这里插入图片描述

在做这一类题目时,一般过程如下:
1.关系分析,找出各个进程之间的同步和互斥关系

五、经典同步问题

1.生产者-消费者问题

生产者和消费者对缓冲区的访问是互斥关系,不然会导致变量出错,由于只有生产者生产出资源后消费者才能消费,他们又是同步关系。因此需要两个信号量,一个负责控制同步,一个负责控制互斥。

互斥P操作必须放置在同步P操作之后,如果调换次序,会导致死锁,这样是因为进程会先进行了互斥上锁之后,再判断同步条件是否满足,一旦同步条件不满足,那么将会死锁。但是互斥V和同步V操作是可以交换的

当临界区只有一个临界资源的时候,同步操作会兼有互斥属性,因此不需要互斥信号量。在同步中,P和V是位于不同的进程的。

semaphore mutex = 1; //互斥
semaphore full = 0;// 同步信号量,表示产品数量
semaphore empty = n;	// 表示空闲缓冲区的数量

producer(){
	while(1){
		P(empty);
		P(mutex);
		//放入缓冲区
		V(mutex);
		V(full);
	}
}

consumer(){
	while(1){
		P(full);
		P(mutex);
		//从缓冲区拿走一个产品
		V(mutex);
		V(empty);
		// 使用产品
	}
}	
2.多生产者-多消费者问题

多种生产者和多种消费者被联系到了同一个缓冲区中,每种生产者生产出来的资源不一样,每种消费者所需的资源也不一样。在该问题中,需要注意覆写问题。指的是两个不同类型的生产者对着同一块缓冲区进行写入,这样会导致后来者将先行者的数据覆盖。为此,缓冲区应该需要互斥的访问,而生产者和消费者的关系又是一种同步关系,因此也是同步互斥问题。

semaphore mutex = 1;
semaphore apple = 0;
semaphore orange = 0;
semaphore plate = 1;

dad(){
	while(1){
		// 准备一个苹果
		P(plate);
		P(mutex);
		// 将苹果放入盘子
		V(mutex);
		V(apple);
	}
}

mom(){
	while(1){
		// 准备一个苹果
		P(plate);
		P(mutex);
		// 将苹果放入盘子
		V(mutex);
		V(orange);
	}
}

dauchter(){
	while(1){
		P(apple);
		P(mutex);
		// 盘中取走苹果
		V(mutex);
		V(plate);
		// 吃掉苹果
	}
}

son(){
	while(1){
		P(orange);
		P(mutex);
		// 盘中取走苹果
		V(mutex);
		V(plate);
		// 吃掉苹果
	}
}

如果缓冲区的容量为1,那么同步操作也可以实现互斥,因为每一时刻只会有一个进程同时访问缓冲区。如果缓冲区大于1,那么必须设置一个互斥信号量mutex来进行互斥

3.读者-写者问题

要求:

  • 允许多个读者可以同时对共享文件执行读操作
  • 同一时间只允许一个写者往文件中写信息
  • 任意写者在完成写操作之前不允许其他读者或者写者工作
  • 写操作之前,应该让已有的读者和写者全部退出

写者问题和任何的进程互斥,因此需要一个互斥信号量rw,用于保证读者和写者之间的互斥访问。这样又引出一个问题,如果占用rw就不允许其他进程进入,则不满足允许多个读者访问的要求。因此需要新增一个计数器count,用于记录当前有多少个读者在访问文件,当count等0的时候才释放rw,而count不等于0的时候占用rw,读进程只有在count=0的时候才需要考虑rw是否被占用,count不为零的时候可以直接访问共享文件。

另外还有一个问题,如果在一个读者P1完成第一个if(count==0)的判断并且进行了P(rw),尚未执行count++的时候,另一个读者P2中断了P1,并且进行判断且执行P(rw)(因为此时count依旧等于0),那么P2进程将会被rw阻塞,因为P1已经为rw上锁了。所以还需要一个mutex互斥信号量用来保证读者在更新count的时候不被中断。

semaphore rw=1;
int count = 0;	//记录有几个读进程在访问文件
semaphore mutex = 0;

weiter(){
	while(1){
		P(rw)
		// 写文件
		V(rw)
	}
}

reader(){
	while(1){
		P(mutex)
		if(count==0)
			P(rw);
		count++
		V(mutex)
		// 读文件
		P(mutex)
		count--;
		if(count==0)
			V(rw);
		V(mutex)
	}
}

该算法只要有读者在读,那么写者都无法进行写,会一直阻塞,因此这是读进程优先的,可以再设置一个信号量w=1,用于实现先来先服务

在这里插入图片描述

4.哲学家进餐问题

在这里插入图片描述
该问题的特点是每个进程需要同时持有两个以及以上的同样的临界资源,这会导致死锁的发生

  1. 五个哲学家进程和他的左右邻居对中间的筷子都是互斥关系
  2. 如果分配不当,如果每个人都同时拿起他左手边的筷子,那么将会没有任何哲学家可以进餐,从而造成死锁
  3. 信号量设置:定义互斥信号量数组c[5]={1,1,1,1,1},并且对哲学家按照0~4号进行编号,哲学家i左边的筷子编号为i,右边的为(i+1)%5

在这里插入图片描述

semaphore c[5]={1,1,1,1,1};

Pi(){
	while(1){
		P(c[i]);	
		P(c[(i+1)%5]);
		// 吃饭
		V(c[i]);
		V(c[(i+1)%5]);
	}
}

但是如果在每个哲学家拿起左边的筷子后就进行中断,就会导致每个人都无法正常进行吃饭,而陷入死锁

1.可以限制最多只允许资格哲学家同时进餐,那么至少有一个哲学家是可以取得两个筷子的

semaphore c[5]={1,1,1,1,1};
int count = 0;
Pi(){
	while(1){
		if (count<5){
			P(c[i]);	
			P(c[(i+1)%5]);
			count++;
		}
		// 吃饭
		V(c[i]);
		V(c[(i+1)%5]);
		count--;
	}
}

2.可以要求奇数号哲学家先拿左边筷子,再去拿右边的,而偶数号刚好相反,那么就可以避免占有一支等待另外一支的情况

semaphore c[5]={1,1,1,1,1};
Pi(){
	while(1){
		if(i%2==0){
			P(c[(i+1)%5]);
			P(c[i]);
			// 吃饭
			V(c[(i+1)%5]);
			V(c[i]);
		}
		else{
			P(c[i]);	
			P(c[(i+1)%5]);	
			// 吃饭
			V(c[i]);
			V(c[(i+1)%5]);
		}
	}
}

3.只有在一个哲学家左右两个筷子都可用的时候,才可以拿筷子。如果有哲学家Pi拿筷子拿到一半的时候被阻塞(缺少另外一根筷子),那么将不会有其他哲学家也进行拿筷子操作,而此时Pi则会等待到缺少的筷子被释放后,进行吃饭操作。

semaphore c[5]={1,1,1,1,1};
semaphore mutex = 1;
Pi(){
	while(1){
		P(mutex)
		P(c[i]);	
		P(c[(i+1)%5]);
		V(mutex)
		// 吃饭
		V(c[i]);
		V(c[(i+1)%5]);
	}
}
5.吸烟者问题

问题描述:一个系统中有三位吸烟者进程和一个供应者进程,每个吸烟者不断卷烟并且抽掉他。但是要卷起一支烟需要三样材料,并且三个抽烟者各持有三样材料中的一种。供应者会源源不断地为不同的吸烟者提供他们所需要的材料。这也是一种生产者消费者问题,但是其临界资源从一种变成了3种。

吸烟者问题为单个可生产多种产品的生产者问题提供一个思路。其中采用自增和取余结合的方式,使得各个吸烟者可以轮流吸烟。吸烟者问题也是同步互斥问题。

在这里插入图片描述
如果缓冲区的大小大于1,那么将要设置互斥信号量

semaphore offer1=0;
semaphore offer2=0;
semaphore offer3=0;
semaphore finish = 0;
int i = 0;	

provider(){
	while(1){
		if(i==0){
			//将组合1放在桌上
			V(offer1);
		}else if (i==1){
			// 将组合二放在桌上
			V(offer2);
		}else if(i==2){
			// 将组合3放在桌上
			V(offer3);
		}
		i = (i+1)%3;
		P(finish); 
	}
}

smoke1(){
	while(1){
		P(offer1);
		// 从桌上取得材料,卷烟抽掉
		V(finish);
	}
}

smoke2(){
	while(1){
		P(offer2);
		// 从桌上取得材料,卷烟抽掉
		V(finish);
	}
}

smoke3(){
	while(1){
		P(offer3);
		// 从桌上取得材料,卷烟抽掉
		V(finish);
	}
}

六、管程

使用信号量机制使得编写程序困难,并且容易出错。因此引出了管程,一种高级的同步机制,使得管理进程同步和互斥更加简单。

组成部分

  1. 局部于管程的共享数据结构
  2. 对该数据结构定义一组操作过程和函数
  3. 对局部于管程的共享数据进行初始化的语句
  4. 管程的部分

基本特征
1.管程内的局部数据只能被管程内的局部函数访问
2.一个进程只有通过调用管程内的函数/过程才能进入管程访问共享数据
3.每次只允许一个进程在管程内执行某个过程/函数

管程在逻辑上类似于面向对象语言的类,类内的数据结构只有类内的函数可以访问。这样子,只要将临界资源放入到管程内,那么其他进程只可以通过调用管程函数才可以访问临界资源,而只需要限制访问管程的数量,就可以实现同步和互斥。一般每次只允许一个进程在管程内执行某个内部函数。

管程的互斥是通过限制访问管程内临界资源的进程数量来实现的,管程的同步是在管程内部的函数实现的。

在这里插入图片描述
Java中使用synchronized关键字实现管程机制。

2.4 死锁

一、死锁的概念

1.死锁的定义

在多道程序系统中,由于多个进程的瓶阀执行,可能会带来死锁问题。所谓死锁,指的是进程因为竞争资源而造成的一种僵局,在无外力作用的情况下,程序无法继续向前推进。就如同一条单行道上两边都有车驶来,将这条单行道堵死了。

2.死锁产生的原因

(1)系统资源竞争

对于不可剥夺的资源进行竞争可能导致死锁,比如进程A占用了打印机,但是进程B却占用了输入设备,则两个进程都无法完成打印工作。

(2)进程推进顺序非法

进程在运行过程中请求和释放资源不当会导致死锁,比如说生产者-消费者问题中,互斥信号量和同步信号量设置的先后顺序错误会导致死锁。

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

死锁的产生必须满足以下四个条件:

  • 互斥条件:进程对所分配的资源排他性使用
  • 不剥夺条件:进程所获的的资源在使用完毕前,不允许其他进程强行争夺
  • 请求并保持条件:进程已经保持了至少一个条件,但是有提出了新的资源请求,但是该关键资源被其他进程占。此时进程阻塞,但是对于自身拥有的条件也不释放
  • 循环等待条件:存在进程资源循环等待链,链中的每个进程获得资源的同时被下一个进程所请求。

二、死锁处理策略

死锁的处理策略主要分为三种:

  • 死锁预防:设置某些限制条件,破坏四个必要条件中的一个,比如哲学家进餐问题中,限制每次最多4个哲学家进餐。
  • 避免死锁:在资源的动态分配中,用某种方法防止系统进入不安全状态。比如说哲学家进餐问题中,只允许奇数或者偶数号的哲学家同时拿起筷子。
  • 死锁的检测和接触:通过系统的检测机构及时检测出死锁的发生,并且采取措施解除死锁。
1.死锁预防
2.死锁避免

死锁避免是在资源动态分配的过程中,防止系统进入不安全状态,以避免发生死锁的方法。

系统安全状态

安全状态指的是系统能够按照某种进程推进顺序为每个进程分配所需的资源,知道满足每个进程对资源的最大需求,使得每个进程都可以顺序完成,如果找不到这种安全序列,则称系统处于不安全状态。

银行家算法

将操作系统视作银行家,将进程视作为客户,资源就是银行家管理的资金。进程运行之前先声明对各个资源的需求量,当进程申请资源的时候,先测试该进程已占用的资源树和本次申请的资源数之和是否超过该进程的需求量,然后在测试系统现存资源是否能满足该进程需要的最大资源量。

3.死锁检测和解除

1.资源分配图

系统死锁可以利用资源分配图表示,圆圈表示一个进程,框表示一类资源,而框中的圆圈数量表示资源数目。从进程到资源的边称为请求边,代表进程请求该资源分配;从资源到进程的边称之为分配边,表示资源被分配到了进程中。

2.死锁定理

通过以下定理,可以知道当前是否死锁:

  1. 在资源分配图中,找到即不孤立也不阻塞的进程节点(也就是该节点所需资源可以被系统满足,而且有边与之相连),并且将与该边相连的边全部去除,并且返还占用资源(取得所需资源后执行,释放占用的资源).
  2. 在第一步中释放的资源,可以唤醒某些因等待资源被阻塞的进程,变回非阻塞进程,然后继续执行1中的步骤。如果图中所有的边都可以通过这种方法去除,则称该图是可完全简化的。如果图是不可完全简化的,则该种情况是死锁的。

3.死锁的解除

  • 资源剥夺法:挂起死锁的进程,并且剥夺它已有的资源,将这些资源分配其他死锁进程。但是被挂起的进程可能因为长时间得不到资源而处于进程饥饿的状态
  • 撤销进程法:强制撤销部分甚至全部死锁进程并且剥夺这些进程占有的资源,但是可能导致原来已经运行到一半的程序推到重新运行,资源利用率低。可以按照进程优先级、已经执行的时长、还要多久完成 和撤销进程的代价的高低进行先后撤销
  • 进程回退法:让一个或者多个进程回退到足以回避死锁的地步,进程回退的时候会自然释放资源。这要求系统保持进程的历史信息,并且设置还原点。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值