深入理解os-线程与进程


1. 什么是操作系统

操作系统是计算机科学中非常重要的概念,我准备从以下这四个方面讲述

第一点,操作系统的本质,操作系统本质上是一款软件,它工作在用户和硬件之间,它负责统筹调度硬件资源,同时分配资源给内部运行的进程,负责这些进程的调度,在这些进程的运行之上,为用户提供软件的功能。

第二点,操作系统的内核功能,操作系统的内核功能非常复杂,但是主要可以划分为内存管理、进程管理、文件系统管理等核心功能。

第三点,操作系统的用户接口功能,操作系统为用户提供了一组接口,用户可以基于这些接口与操作系统中的软件进行交互,而软件则是通过内核调用等方式,调用底层的硬件设备的功能,以这种方式完成对计算机的使用

第四点,操作系统的特征,它通常是提供了GUI,用户可以通过GUI来完成对操作系统的操作,通过这种操作,能够使得用户很容易的对计算机进行操作。

2. 什么是系统调用?

系统调用是程序员访问内核系统的一个手段,它是操作系统提供了一个接口,它能够让用户程序以用户态的身份去执行内核态的操作,而在这个过程中,在内核态中执行的操作的程序,实际上是交给了操作系统内置的程序。

这样做,既可以保证操作系统的安全性,又提供了手段给用户进行功能的访问

所谓安全性,就是指用户程序并不能直接以程序代码对内核中的空间以及资源进行修改,而是在操作系统内置的程序中,传入用户所需要访问的资源参数,由操作系统内置的程序去完成这个任务

从而保证了操作系统的安全性,不会因为用户对空间以及资源的随意修改而导致系统崩溃

比如说,用户直接修改了内存中别的程序的代码段,这时候如果是操作系统下的话就会报段错误,撤销此次操作,如果是用户自己操作的话,很有可能真的修改成功,导致操作系统崩溃

什么是用户态?什么是内核态?

用户态:在此状态下,用户只能够读取用户空间中的数据,对用户空间中的数据进行读写操作

内核态:在此状态下,可以操作系统底层的资源,例如执行I/O、设备的连接与释放、内存的释放、回收、整理,进程通信管理,等

3. 线程和进程有什么区别?

要理解进程,就要理解我们的进程是怎么产生的,如果说程序是静态的,那么进程就是程序动起来后的结果

程序在编写完毕后,只是存储在我们的磁盘上,而当我们使用相应的编译器,将其翻译成二进制可执行文件并且运行之后,这时候原本静态的程序代码就变成了进程了

首先,程序指令会被加载到内存中,然后操作系统生成PCB,分配相关资源,然后通过CPU执行指令,这个过程就是程序变为可调度的进程,然后被调度上CPU运行的全部过程

一个进程中可以有多个线程,多个线程共享进程的堆和代码段等这些资源,但是每个线程有自己的程序计数器,虚拟机栈和本地方法栈

进程的PCB

  • 进程描述信息

进程标识符(PID):标识各个进程,每个进程都有一个并且唯一的表示

用户标识符(UID):进程归属的用户,用户标识符主要为共享和保护服务

  • 进程控制和管理信息

进程当前状态,如 new、ready、running、waiting 或 blocked 等;

进程优先级:进程抢占 CPU 时的优先级;

资源分配清单:

  • 有关内存地址空间或虚拟地址空间的信息,所打开文件的列表和所使用的 I/O 设备信息。

CPU 相关信息:

  • CPU 中各个寄存器的值,当进程被切换时,CPU 的状态信息都会被保存在相应的 PCB 中,以便进程重新执行时,能从断点处继续执行。
  • 进程是资源分配与调度的基本单位,而线程是CPU调度的基本单位
  • 进程拥有一个完全独立的资源平台,而线程间的资源大部分是共享的,而只有对于寄存器以及栈这些所必须独享的资源才不共享

为什么这些资源必须共享?这是为了确保线程安全,当多线程操作的时候,我们希望:

  • 线程在切换后恢复,能够接着之前的状态继续执行,而当线程执行的时候,肯定会对寄存器的值进行修改,为了保证线程的状态恢复,我们在进行线程的切换之前必须完成寄存器的保存
  • 线程的堆栈,线程可以调用函数,因此它必须维护自己的堆栈,来确保当前函数的执行状态没有被篡改
  • 线程能够减少并发执行的时间和空间开销

这主要体现在

①线程的创建时间比进程要快,这是因为线程是在进程的内部创建的,它不涉及到一些重量级资源的分配与调度,比如说进程要创建的话,首先需要分配内存,进行进程的调度等,线程在创建的时候对这些重量级的资源是不会重新分配的,而是共享这些资源

②线程的终止时间比进程的终止时间还要快,还是资源的问题,由于进程占用的资源通常是重量级的,因此在进行内存回收等操作的时候,由于相关算法的执行,进程的释放过程是比较缓慢的,而线程的终止不涉及到这些资源,因此效率较高

③线程的上下文切换效率高于进程的上下文切换,首先进程中有一个叫做页表的概念,进程的执行需要进行虚拟内存空间的转换,这意味着同一个进程的线程都是具有同一个页表的,那么在切换的时候不需要切换页表,但是进程的切换是必须切换页表的。

为什么必须要切换页表

这是虚拟内存的机制,首先讲讲虚拟内存,虚拟内存是一种地址映射技术,它能够将磁盘上的地址映射为每个进程属于自己的、私有的、地址连续存放的虚拟内存,而通常为了提高空间利用率,我们通常分配得到的内存空间在内存上或者在磁盘上的分配都不会是连续的,因此为了记住这些地址,在进程的内部记录了页表,通过页表就能够找到这个进程所私有的那一块虚拟内存在哪了

④由于线程之间的资源是共享的,因此线程之间的通信就变得极为简单,只需要在线程的共享空间中写入数据即可,不需要通过内核来交换数据

4. 进程有哪几种状态?

进程可以粗略地分为五种状态,他们分别是新建态就绪态运行态阻塞态终止态

这里讲一下各种状态的定义以及各种状态间是如何转换的

首先进程的PCB被创建,此刻就标志着进程被新建了,进入新建态,然后此时进程还缺了一样东西内存空间,当系统中的内存充足的时候,进程就会被分配内存,这时候,进程除了CPU以外的资源就都得到了,然后此时进程就进入了就绪态

就绪态的进程可以随时被分派器调度上CPU,此时就进入了运行态

  • 当进程的操作指令唤起了操作系统的内核中的需要阻塞的接口的时候,此时就成为了阻塞态,这时候它必须等待事件的发生,通常是以中断的方式通知CPU,然后将该进程解除阻塞,然后进入就绪态
  • 当进程的时间片到,或者是因为更高优先级的进程到来的时候,这时候进程被强迫让出CPU,然后进入了就绪态,等待下一次调度

最终,进程完成了全部的指令序列,进入终止态,终止过程如下:

  • 查找所需要停止的PCB
  • 如果处于执行的状态,那么就立即终止执行,然后将CPU资源让出
  • 如果还有子进程,将子进程托管给1号进程
  • 将该进程所拥有的全部资源都归还给操作系统;
  • 将其从 PCB 所在队列中删除;

挂起态

内存的容量是十分有限的,当内存不足以支撑起这么多的进程所需要的内存空间的时候,这时候操作系统就会执行调度。

注意,这里交换的数据是内存驻留集(驻留集是指在请求分页存储管理器中,给进程分配的物理块大小集合),而PCB是还是留在内存中的

  • 就绪挂起态:这种状态有两种来源
    • 当操作系统中有太多的就绪态进程的时候,这时候就会调度一部分的进程进入就绪挂起态,将内存空间回收,将进程的数据保存到磁盘
  • 阻塞挂起态:当操作系统空间不足时,这时候可以调度一部分不用的阻塞进程进入磁盘,此时就成为了阻塞挂起态
    • 注意,此时如果阻塞没有完成,而内存充足的话,也可以解除挂起,将进程修改为阻塞态

当阻塞挂起态收到事件完成的信号后,此时阻塞挂起态的进程就进入就绪挂起态

5. 进程间的通信方式

要了解这个问题,首先要知道进程间为什么不能直接交换数据

这是因为进程的空间是相互隔离的,在一般情况下,一个进程不能够直接访问到另外一个进程的空间

但是内核空间是每个进程都可以共同访问的同一块共享区域,因此,如果要实现进程间通信,可以从内核空间入手

6.1 管道方式

管道方式实际上就是一种经典的BIO方式,在linux下,我们通常这样使用

mkfifo mypiple
echo "hello" > mypiple
#这里将数据写入了管道
#但是这时候输入端被阻塞住了,这是因为它在等待数据的读取

我们新开一个终端

cat < mypiple

此时管道的数据被读取完毕,两个终端恢复到接收指令的状态

这种方式效率很低,不适合进程间的频繁数据交换,当然,它的好处,就是非常的简单

管道的底层创建原理

匿名管道的创建,需要通过以下操作系统提供的系统调用

int pipe(int fd[2])

这里它表示创建一个匿名管道,并且它会返回两个文件描述符,一个管道的读取端描述符fd[0]和一个管道的写入段描述符fd[1],这个匿名管道并不存在于文件系统中,而是存在于内存中

实际上,这个所谓的管道就是在内存中的一段缓冲,我们可以通过fd来对这段缓存进行写入和读取,然而:

目前我们所做的这些管道操作,都是在同一个进程内部的,也就是说,这个fd只有,你这个进程能够看到,其他进程是看不到这个fd的

然而,我们可以通过fork的操作,派生子进程,然后子进程就会得到完全相同的一份fd

那么既然有了这个fd,就可以往管道里面写数据和读数据了

这样一来,两个进程就可以进行通信了

同时,在这种情况下,一般这个通信只能是半双工的,因为如果读写双方同时写入数据,数据就会错乱

在Shell中执行A | B的时候,那么Shell就是父进程了,然后如果要完成A发送到B,那么就是要fork两个子进程A和B,因此,我们就可以得出一个结论:

一条指令中有n个|,就会有n+1个子进程,然而这样是会有很大的浪费的

首先,有一些文件描述符是用不上的,因此白白造成开销,如果父进程的内存占用很高,由于fork,那么会造出来相同的一份,造成巨大的内存浪费。

分析原因:对于命名管道,它由于在文件中创建了实体,既然是这样的话,在不同的进程内部就可以通过系统调用IOL来读取这个文件,而匿名管道是只存在于内存中的缓冲区,因此只能够通过父子进程这样的手段来得到文件描述符

6.2 消息队列方式

消息队列是经典的消息传递的方式,其本质上是生产者-消费者的模型

在Linux中,消息队列是保存在内核中的消息链表,在发送数据的时候,会分成一个个的数据单元,消息的发送方和接收方要固定好消息体的格式,所以每个消息都是固定大小的数据块,而不像是无序的字节
如果消息队列中的数据被读取了,那么内核就会把这个消息体给删除

消息队列的生命周期是随内核的,当内核不释放消息队列时,那么直到系统关闭或者宕机的时候消息队列才会被释放,消息队列的优点是消息队列不能够做到实时收发,它必须先将数据从用户空间拷贝到内核中,而且必须拷贝完一个完整的块后,消费者才能从内核中拷贝到用户空间

同时消息队列也不能够存储大占用的数据,比如在Linux中定义了MSGMAXMAGMNB,他们分别定义了消息最大长度和消息队列的最大长度

6.3 共享内存

消息队列的读取和写入的最大问题是存在拷贝问题一次I/O意味着至少两次拷贝,一份数据被复制了两次,有3份一模一样的数据在存储设备中,这是极大的浪费,同时由于涉及到内核空间的读写,还有一个用户态和内核态的切换问题,这样的话就导致了用户态与内核态之间的切换开销,可见原生的消息队列的性能是非常差的

共享内存技术:简单来说,就是基于虚拟内存机制,将不同虚拟地址空间,映射到同一块物理内存,这样的话两个进程对自己的虚拟内存进行操作,实际上改变的是共享区域中的内存数据,

6.4 信号量

注意到共享内存的读写,它实际上涉及到了一个临界资源的访问问题,所谓临界资源就是多个进程/线程都能够读取到的一段共享数据,那么在并发的情况下,就会导致并发下的数据安全问题

为了防止多进程竞争共享资源,一般有悲观锁和乐观锁的实现机制,乐观锁是基于一种机器原子指令CAS实现的,而悲观锁的实现方式通常有信号量

基于信号量实现的互斥,初始化为1

其通常的使用方式是在对资源进行访问时,进行加锁

  • 如果信号量为0,那么就将其设置为-1
  • 如果信号量为-1,那么这个线程将被阻塞,并且被注册到监视这个信号量的监视器所对应的同步队列中

当退出对资源的访问的时候,进行解锁

  • 如果信号量修改完后为0,那么就唤醒同步队列阻塞器中一个线程

基于信号量实现的同步

同步,就是要保证某个进程(A)要在某个进程(B)之前执行

比如说,给信号量初始化为0,B要取数据,那么B被阻塞,只有当A生产数据后,这个信号量不为-1的时候,B才被唤醒

同理,当信号量达到最大值,这时候A就要被阻塞,当信号量为-1的时候,这时候将A解除阻塞

6.5 信号机制

在 Linux 操作系统中, 为了响应各种各样的事件,提供了几十种信号,分别代表不同的意义。我们可以通过 kill -l 命令,查看所有的信号:

信号是进程间通信机制中唯一的异步通信机制,因为可以在任何时候发送信号给某一进程,一旦有信号产生,我们就有下面这几种,用户进程对信号的处理方式。

1.执行默认操作。Linux 对每种信号都规定了默认操作,例如,上面列表中的 SIGTERM 信号,就是终止进程的意思。

2.捕捉信号。我们可以为信号定义一个信号处理函数。当信号发生时,我们就执行相应的信号处理函数。

3.忽略信号。当我们不希望处理某些信号的时候,就可以忽略该信号,不做任何处理。有两个信号是应用进程无法捕捉和忽略的,即 SIGKILLSEGSTOP,它们用于在任何时候中断或结束某一进程。

6.6 Socket

前面提到的管道、消息队列、共享内存、信号量和信号都是在同一台主机上进行进程间通信,那要想跨网络与不同主机上的进程之间通信,就需要 Socket 通信了。

实际上,Socket 通信不仅可以跨网络与不同主机的进程间通信,还可以在同主机上进程间通信。

我们来看看创建 socket 的系统调用:

int socket(int domain, int type, int protocal)

三个参数分别代表:

  • domain 参数用来指定协议族,比如 AF_INET 用于 IPV4、AF_INET6 用于 IPV6、AF_LOCAL/AF_UNIX 用于本机;
  • type 参数用来指定通信特性,比如 SOCK_STREAM 表示的是字节流,对应 TCP、SOCK_DGRAM 表示的是数据报,对应 UDP、SOCK_RAW 表示的是原始套接字;
  • protocal 参数原本是用来指定通信协议的,但现在基本废弃。因为协议已经通过前面两个参数指定完成,protocol 目前一般写成 0 即可;

针对 TCP 协议通信的 socket 编程模型

  • 服务端和客户端初始化socket,获得文件描述符
  • 服务端调用bind,将绑定IP地址和端口
  • 服务端调用listen,创建监听队列,当有请求到来的时候,将请求添加到队列中
  • 客户端发起connect,向服务端的地址和端口发起连接请求
  • 服务端调用accpet,返回用于传输的socket的文件描述符
  • 客户端调用write写入数据,服务端调用read写入数据
  • 客户端断开连接时,会调用 close,那么服务端 read 读取数据的时候,就会读取到了 EOF,待处理完数据后,服务端调用 close,表示连接关闭。

服务端存在两个socket,第一个是监听socket,第二个是连接socket,而这些连接socket都是各不相同的

针对 UDP 协议通信的 socket 编程模型

UDP 是没有连接的,所以不需要三次握手,也就不需要像 TCP 调用 listen 和 connect,但是 UDP 的交互仍然需要 IP 地址和端口号,因此也需要 bind。

对于 UDP 来说,不需要要维护连接,那么也就没有所谓的发送方和接收方,甚至都不存在客户端和服务端的概念,只要有一个 socket 多台机器就可以任意通信,因此每一个 UDP 的 socket 都需要 bind。

另外,每次通信时,调用 sendto 和 recvfrom,都要传入目标主机的 IP 地址和端口。

针对本地进程间通信的 socket 编程模型

本地 socket 被用于在同一台主机上进程间通信的场景:

  • 本地 socket 的编程接口和 IPv4 、IPv6 套接字编程接口是一致的,可以支持「字节流」和「数据报」两种协议;
  • 本地 socket 的实现效率大大高于 IPv4 和 IPv6 的字节流、数据报 socket 实现;

对于本地字节流 socket,其 socket 类型是 AF_LOCAL 和 SOCK_STREAM。

对于本地数据报 socket,其 socket 类型是 AF_LOCAL 和 SOCK_DGRAM。

本地字节流 socket 和 本地数据报 socket 在 bind 的时候,不像 TCP 和 UDP 要绑定 IP 地址和端口,而是绑定一个本地文件,这也就是它们之间的最大区别

6. 线程间的同步的方式

线程间的同步一般有三种机制

  • 基于信号量:信号量是一种有效记录数据被拥有的次数的数据结构,一般来说,它的初始值是当前有多少个线程可以操作这些资源,获取信号量失败的线程将被加入到队列中等待
  • 基于互斥:互斥也可以基于信号量进行实现,只要控制只有一个线程能够获取这个资源即可,其基本原理是:只有拥有操作这个临界资源的对象(一般是锁)才可以资源进行读取
  • 基于事件机制:通过通知和唤醒,通过事件唤醒机制来实现,这种事件机制,通常用于避免CPU忙等的情况下使用,比如说当一个I/O事件发生的时候,这时候通知发起I/O操作的那个线程挂起,不再参与CPU的调度,然后当I/O完成后,内核向挂起的线程发出一个信号,线程恢复执行,亦或者是不立即挂起,等到相关计算操作完成后,在转为挂起状态

7. 进程的调度算法

7.1 先来先服务算法

先来先服务算法是非抢占式的调度算法,它根据进程被调度为就绪态的顺序来确定在CPU上运行的优先级,越早被插入到就绪队列中,就越早被调度,完成任务。

先来先服务算法类似于公平锁的机制,但是它不利于紧急任务的执行,而且对于IO频繁的进程,这个进程的执行效率会很低

为什么?假设目前一个进程需要频繁的进行IO操作,那么一旦发生IO发生阻塞,在解除阻塞的时候会被插入到就绪队列的队尾,在这种情况下,如果一个进程在CPU上只运行很短一段时间,又要重新排队,使得这个进程运行进度缓慢。因此,FCFS利于CPU密集型作业,而不利于IO密集型作业

7.2 最短作业优先调度算法

最短作业优先调度算法的机制是优先选择那些运行时间短的进程来运行,这有利于提高系统的吞吐量,因为能够较快的处理更多的进程任务,接收更多的进程任务

但是这种调度算法有一个非常经典的缺陷,就是由于存在插队现象,那么就可能导致运行时间长的进程饥饿,假设这个运行时间长的进程是紧急任务,就可能导致用户的使用不佳的情况。

7.3 最短剩余时间优先算法

这种算法的关键点在于最短剩余,它关键是会将新进程的运行时间与当前运行进程的剩余运行时间进行对比,如果新进程的运行时间比当前运行进程的剩余运行时间更短,那么就将新进程调度上CPU

7.4 最高响应比优先算法

最高响应比优先算法中,有几个关键指标需要先了解

  • 等待时间:进程没有在CPU上运行的时间总和
  • 要求服务的时间:进程在CPU上运行的时间总和

响应比的计算公式 = (等待时间+要求服务的时间)/要求服务的时间

那么为什么说最高响应比优先算法能够同时权衡短作业和长作业呢?

假设,当前的两个进程的等待时间相同,那么响应比就取决于要求服务的时间,当要求服务的时间越短,那么就越有可能被选中,这就照顾了短作业

当等待时间相同,我们应当尽快的让短进程完成并且退出,这样就能够提高系统的吞吐量

假设,当前的两个进程的要求服务时间相同,那么响应比就取决于等待服务的时间,当需要等待的时间越长,那么就越有可能可能被选中,这就照顾了长作业

当要求服务时间相同,我们应该尽可能让之前长时间等待的进程到CPU上执行计算任务,推进任务进程,否则的话就会导致系统响应慢的问题,而等待时间短的进程因为阻塞的时间短,相对能够获得更多的CPU时间

7.5 时间片轮转算法

时间片轮转是基于时钟中断实现的,在这种机制下,每个进程都被分配了一定的时间片

  • 当进程运行到时间片结束,触发分派器的调度,切换进程上CPU
  • 当进程运行到阻塞,而时间片未结束,将会导致进程进入阻塞态,本次轮转时间片被浪费,进程下次调度时被插入到就绪队列的尾部

7.6 多级反馈队列

多级反馈队列调度算法是时间片轮转算法和最高优先级算法的综合

  • 「多级」表示有多个队列,每个队列优先级从高到低,同时优先级越高时间片越短。

  • 「反馈」表示如果有新的进程加入优先级高的队列时,立刻停止当前正在运行的进程,转而去运行优先级高的队列;

  • 设置了多个队列,赋予每个队列不同的优先级,每个队列优先级从高到低,同时优先级越高时间片越短

  • 新的进程会被放入到第一级队列的末尾,按先来先服务的原则排队等待被调度,如果在第一级队列规定的时间片没运行完成,则将其转入到第二级队列的末尾,以此类推,直至完成;

  • 当较高优先级的队列为空,才调度较低优先级的队列中的进程运行。如果进程运行时,有新进程进入较高优先级的队列,则停止当前运行的进程并将其移入到原队列末尾,接着让较高优先级的进程运行;

可以发现,对于短作业可能可以在第一级队列很快被处理完。对于长作业,如果在第一级队列处理不完,可以移入下次队列等待被执行,虽然等待的时间变长了,但是运行时间也会更长了,所以该算法很好的兼顾了长短作业,同时有较好的响应时间。

总结,多级反馈队列是一种动态调整进程运行优先级和运行时间的调度策略

这种策略设置了多条队列,每条队列有这样的特点,队列的优先级越高,则队列的时间片越短

进程被插入队列时,首先会被插入到第一级队列,如果是短任务,那么就能够很快完成

但是无法完成,那么就会被插入到第二级队列,只有当较高优先级的队列的任务完成后,才会去执行下一级队列中的任务

8. 什么是死锁

死锁指的是这样一种情况,在多个进程/线程并发的时候,由于对于临界资源的控制不合理,导致多个进程互相等待对方的资源被释放,但是这些资源的释放前提是对方所持有的资源被释放,由此就造成了进程间的互相等待,最终导致进程任务无法取得下一步进展

9. 死锁的四个必要条件

死锁的四个必要条件是:

对临界资源的控制是互斥的:它说的是当一个线程正在访问一个资源的时候,其他线程因为资源被占有而被阻塞

占用而且等待:进程申请资源的时候,由于进程执行任务需要将所有的资源都准备好,因此在申请资源的时候不会释放已经占用的资源

不可抢占:进程在完成任务之前,对于资源的控制是不可抢占的,不能够由其他进程强行剥夺

互相等待:各个进程互相等待对方的资源释放,形成了一个循环等待资源的链条

所谓必要条件,就是指这些条件如果发生了,不一定死锁,但是如果发生了死锁,这些条件一定成立,如果这些条件不成立,那么一定不可能死锁。

10. 解决死锁的方法

我认为解决死锁的方法的切入点有两个,死锁发生是在进程的运行中,那么我们可以在进程的运行前和进程的运行中做手脚,从而解决死锁问题

在进程的运行前

之所以会发生死锁,这是因为进程在运行时并发需要申请资源,如果我们能够,因此一种方法是在进程运行前提前分配好资源,这种解决策略本质上是破坏了占有并且等待的必要条件,所以是死锁预防

在进程的运行时

之所以会发生死锁,是因为多个线程抢占同一个资源,并且当这个资源被抢占了之后,会导致死锁发生的可能,因此当系统执行例如银行家算法之类的算法检测到此次的分配可能导致死锁发生,就不分配资源,这种情况下也是不会发生死锁,但是它是在进程运行时也就规避了死锁的发生而不分配资源,但是这种算法并不会破坏四个必要条件,所以是死锁避免

另外一种情况,系统允许死锁的发生,但是通过定期检测死锁的存在,比如说通过绘制资源分配图,快速定位到死锁然后将进程撤销,资源释放,死锁解除

这个策略涉及到两个策略:死锁检测死锁解除,这两个策略通常是配套使用才能解决死锁问题

11. 死锁的预防

死锁的预防旨在通过破坏四个必要条件来完成预防

预防的方法通常是通过破坏后三个条件,第一个条件由于要完成线程安全的控制,因此无法避免

先来讲讲破坏占用且等待的条件,等待的根本原因是需要在进程运行时申请资源,这种解决方案是禁止在运行时神资源,而是在进程运行时就给它分配好资源,这样的话就不存在在进程运行时申请资源

破坏不可抢占的条件是:在资源申请新的资源的时候,必须释放已经获取的资源,本进程需要的时候才主动申请。

破坏循环等待的条件:通过层次分配策略,这种层次分配策略严格规定了线程获取资源的顺序,比如说有两个资源A和B,有线程T1和T2,T1需要A和B,那么必须是先申请A再申请B,T2也是如此,在这种情况下,当T1获取了资源之后,T2就无法获取资源了必须等待A将资源全部释放,这样的话避免了循环链条的出现

12. 死锁的避免

死锁的避免是允许四个条件同时发生,但是通过资源的合理调控,就能够避免资源的分配不合理

常见的死锁避免算法有银行家算法

配资源前,先计算此次分配是否会导致系统处于不安全状态。若安全则分配,否则不分配

安全状态:存在一个由所有进程(P1,P2,…,Pn)构成的安全序列(如P3,P1,P4,…),如果系统按照这个顺序为每个进程分配所需要的资源,直至该进程声明的最大资源需求量,则可使每个进程都运行完

不安全状态:系统中不存在任何一个安全序列,使得所有进程都能够运行完,这就是不安全状态

简单来说,银行家算法就是一种预分配的策略,它在分配的时候,会先将用户进程发起的资源申请做一个计算,如果资源能够使得有一个进程分配资源序列完成工作,那么就认为是安全的,否则是不安全的。

13. 死锁的检测

对资源的分配加以限制可以 预防和避免 死锁的发生,但是都不利于各进程对系统资源的充分共享。解决死锁问题的另一条途径是 死锁检测和解除 (这里突然联想到了乐观锁和悲观锁,感觉死锁的检测和解除就像是 乐观锁 ,分配资源时不去提前管会不会发生死锁了,等到真的死锁出现了再来解决嘛,而 死锁的预防和避免 更像是悲观锁,总是觉得死锁会出现,所以在分配资源的时候就很谨慎)。

这种方法对资源的分配不加以任何限制,也不采取死锁避免措施,但系统 定时地运行一个 “死锁检测” 的程序,判断系统内是否出现死锁,如果检测到系统发生了死锁,再采取措施去解除它。

14. 死锁的解除

死锁的解决通常有四种办法

  • 最暴力的方法,重启操作系统,直接重置当前所有进程的运行状态,这种情况下,就会导致其他没有发生死锁的进程也受到影响
  • 仅撤销那些涉及到死锁的进程组,但是可能付出很大的代价,比如说有些进程已经计算了很长时间了,但是强制撤销掉了这些进程
  • 仅可能少的撤销进程,通过撤销死锁进程组内的部分进程,将撤销死锁的负面影响降到最低,这种方式是随机的,逐个进行撤销,直到死锁的解除
  • 不撤销进程,撤销进程占用的资源,直到死锁解除

文章作者: 穿山甲
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 穿山甲 !
  目录