操作系统 - 进程和死锁详细解析

操作系统

1、进程

1.1、什么是进程(Process)和线程(Thread)?有何区别?

进程是具有一定独立功能的程序关于某个[数据集]合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位线程是进程的一个实体是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行。

进程与应用程序的区别在于应用程序作为一个静态文件存储在计算机系统的硬盘等存储空间中,而进程则是处于动态条件下由操作系统维护的系统资源管理实体。

1.2、父子进程之间的关系:

孤儿进程: 父进程生成子进程,但是父进程比子进程先结束;子进程会变成孤儿进程,由系统1号init 进程进行接管。init 进程接管后,在该孤儿进程结束的时候,负责“收尸”,回收系统资源及进程信息。

僵尸进程: 子进程已经退出,但是没有父进程回收它的资源(父进程生成的子进程,但是子进程比父进程先挂掉,如果父进程没有收回它的资源时,那么子进程挂掉后就变成了僵尸进程;应该尽量避免产生僵尸进程,可以在父进程调用wait 或者waitpid 进程回收)。

原文链接:https://blog.csdn.net/qq_28840229/article/details/79844763

父进程和子进程、init进程之间的关系:
子进程是通过fork()函数创建的,相当于父进程的一个复制品,子进程和父进程除了子进程复制了父进程还有什么关系呢?

1.子进程和父进程属于同一进程组,父进程为进程组组长;
2.父进程退出子进程未退出的情况下,子进程会变成孤儿进程有init进程收养并转为后台进程;
3.子进程退出父进程为退出但父进程未及时进行回收,子进程就会成为僵尸进程;

用程序来看一下父子进程之间的关系
子进程和父进程属于同一进程组,父进程为进程组组长

原文链接:https://blog.csdn.net/weixin_48755527/article/details/120751549

扫描二维码关注公众号,回复: 14852402 查看本文章

1.3、进程间的通信方式

  • 管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
  • 有名管道 (named pipe) : 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
  • 信号量( semophore ) : 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
  • 消息队列( message queue ) : 消息队列是消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
  • 信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
  • 共享内存( shared memory )共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。

1.4、进程和线程切换时的区别

我们都知道线程切换的开销比进程切换的开销小,那么小在什么地方?切换的过程是怎样的?无论是在多核还是单核系统中,一个CPU看上去都像是在并发的执行多个进程,这是通过处理器在进程间切换来实现的。

  • 操作系统实现这种交错执行的机制称为上下文切换。
  • 操作系统保持跟进程运行所需的所有状态信息,这种状态,也就是上下文,它包括许多信息,例如PC和寄存器文件的当前值,以及主存的内容

在任何一个时刻,单处理器系统都只能执行一个进程的代码。
当操作系统决定要把控制权从当前进程转移到某个新进程时,就会进行上下文切换,即保存当前进程的上下文,恢复新进程的上下文,然后将控制权传递到新进程,新进程就会从上次停止的地方开始

深入计算机系统一书中对上下文切换的表达如下:
  如果现在有两个并发的进程:外壳进程和hello进程。开始只有外壳进程在运行,即等待命令行上的输入,当我们让他运行hello程序时,外壳通过调用一个专门的函数,即系统调用,来执行我们的请求,系统调用会将控制权传递给操作系统。操作系统保存外壳进程的上下文,创建一个新的hello进程及其上下文,然后将控制权传递给新的hello进程。hello进程终止后,操作系统恢复外壳进程的上下文,并将控制权传回给他,外壳进程将继续等待下一个命令行输入。

1.上下文切换

内核为每一个进程维持一个上下文。上下文就是内核重新启动一个被抢占的进程所需的状态。包括一下内容:

  • 通用目的寄存器
  • 浮点寄存器
  • 程序计数器
  • 用户栈
  • 状态寄存器
  • 内核栈
  • 各种内核数据结构:比如描绘地址空间的页表,包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表。

2.进程切换

系统中的每个程序都是运行在某个进程的上下文中的。上下文是由程序正确运行所需的状态组成的,这个状态包括存放在存储器中的程序的代码和数据,他的栈,通用目的寄存器的内容,程序计数器,环境变量以及打开文件描述符的集合。所以进程切换就是上下文切换

那回到最开始的问题,进程切换和线程切换有什么区别?

当然这里的线程指的是同一个进程中的线程。要想正确回答这个问题,需要理解虚拟内存。

3.虚拟内存

虚拟内存是操作系统为每个进程提供的一种抽象,每个进程都有属于自己的、私有的、地址连续的虚拟内存当然我们知道最终进程的数据及代码必然要放到物理内存上,那么必须有某种机制能记住虚拟地址空间中的某个数据被放到了哪个物理内存地址上,这就是所谓的地址空间映射,那么操作系统是如何记住这种映射关系的呢,答案就是页表。

每个进程都有自己的虚拟地址空间,进程内的所有线程共享进程的虚拟地址空间

现在我们就可以来回答这个面试题了。

进程切换和线程切换的区别

最主要的一个区别在于进程切换涉及虚拟地址空间的切换而线程不会。因为每个进程都有自己的虚拟地址空间,而线程是共享所在进程的虚拟地址空间的,因此同一个进程中的线程进行线程切换时不涉及虚拟地址空间的转换。

为什么虚拟地址空间切换会比较耗时呢?

现在我们已经知道了 进程都有自己的虚拟地址空间 ,把虚拟地址转换为物理地址需要查找页表,页表查找是一个很慢的过程,因此通常使用Cache来缓存常用的地址映射,这样可以加速页表查找,这个cache就是TLB(translation Lookaside Buffer,我们不需要关心这个名字只需要知道TLB本质上就是一个cache,是用来加速页表查找的)。

由于每个进程都有自己的虚拟地址空间,那么显然每个进程都有自己的页表,那么当进程切换后页表也要进行切换,页表切换后TLB就失效了,cache失效导致命中率降低,那么虚拟地址转换为物理地址就会变慢,表现出来的就是程序运行会变慢,而线程切换则不会导致TLB失效,因为线程线程无需切换地址空间,因此我们通常说线程切换要比较进程切换快,原因就在这里。

2、死锁(deadlock)

2.1、什么是死锁?

在两个或者多个并发进程中如果每个进程持有某种资源而又等待其它进程释放它们现在保持着的资源,在未改变这种状态之前都不能向前推进,称这一组进程产生了死锁。通俗的讲就是两个或多个进程无限期的阻塞、相互等待的一种状态。

2.2、死锁产生的四个条件

(有一个条件不成立,则不会产生死锁)

  • 互斥条件:一个资源一次只能被一个进程使用
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得资源保持不放
  • 不剥夺条件:进程获得的资源,在未完全使用完之前,不能强行剥夺
  • 循环等待条件:若干进程之间形成一种头尾相接的环形等待资源关系

2.3、死锁的处理策略—预防死锁、避免死锁、检测和解除死锁

一、死锁的处理策略——预防死锁
(一)破坏互斥条件

互斥条件:只有对必须互斥使用的资源的争抢才会导致死锁。
如果
把只能互斥使用的资源改造为允许共享使用
,则系统不会进入死锁状态。比如: SPOOLing技术。操作系统可以采用 SPOOLing 技术把独占设备在逻辑上改造成共享设备。比如,用SPOOLing技术将打印机改造为共享设备…

该策略的缺点:并不是所有的资源都可以改造成可共享使用的资源。并且为了系统安全,很多地方还必须保护这种互斥性。因此,很多时候都无法破坏互斥条件。

SPOOLing技术

SPOOLing 技术是用于将 I\O 设备进行虚拟化的技术,这个技术可不像 CPU 的虚拟化能欺骗我们人类,它是专门用于欺骗进程的。就拿打印机举栗吧,我就买了一台打印机,但此时我打开了 word 和 pdf,想要打印 word 和 pdf 中的内容;此时计算机中有2个进程,word 进程和 pdf 进程,这两个进程都认为自己拥有一个打印机,那么是否此时我作为计算机的主人就拥有2台打印机了呢?当然不是啊,我又不是睁眼瞎,我就看到了一台打印机啊~~~这就是通过虚拟化技术欺骗了2个无知的进程。

具体的实现

道理我都懂,那么怎么实现欺骗进程的目的呢(也就是怎么实现SPOOLing技术呢)?

  1. SPOOLing 技术首先需要提供统一的调用接口,每一个进程都可以调用该接口,这样在进程看了自己是拥有该设备的。
  2. 需要将磁盘设备和内存作为缓冲区,磁盘设备上的缓冲区称作,而内存上的缓存区被称作缓冲区
  3. 需要一个专门的输入输出进程来实现对 I/O 设备的读写数据

请添加图片描述

以向 I/O 设备写数据为例子,做出概念图,如下所示:

请添加图片描述

首先某一个进程(例如 word)调用了统一的接口,然后进入内核。内核例程负责将 word 想要打印的内容做成一个打印申请表,将这个申请表放入打印输出队列中(这个队列在输出井中)。然后由输出进程从打印队列中取打印申请表根据表格内容将用户数据从磁盘中取出放入内存输出缓冲区,然后再输出到 I/O 设备中。输出进程会不断的查看打印输出队列,直到队列为空,则输出进程被阻塞
请添加图片描述

(二)破坏不剥夺条件

不剥夺条件:进程所获得的资源在未使用完之前,不能由其他进程强行夺走,只能主动释放。
破坏不剥夺条件:
​ ①、方案一:**当某个进程请求新的资源得不到满足时,它必须立即释放保持的所有资源,**待以后需要时再重新申请。也就是说,即使某些资源尚未使用完,也需要主动释放,从而破坏了不可剥夺条件。
​ ②、方案二:当某个进程需要的资源被其他进程所占有的时候,可以由操作系统协助,将想要的资源强行剥夺。这种方式一般需要考虑各进程的优先级(比如:剥夺调度方式,就是将处理机资源强行剥夺给优先级更高的进程使用)
该策略的缺点:
​ ①、实现起来比较复杂。
​ ②、释放已获得的资源可能造成前一阶段工作的失效。因此这种方法一般只适用于易保存和恢复状态的资源,如CPU。
​ ③、反复地申请和释放资源会增加系统开销,降低系统吞吐量。
​ ④、若采用方案一,意味着只要暂时得不到某个资源,之前获得的那些资源就都需要放弃,以后再重新申请。如果一直发生这样的情况,就会导致进程饥饿

(三)破坏请求和保持条件
  • 请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源又被其他进程占有,此时请求进程被阻塞,但又对自己已有的资源保持不放。

可以采用静态分配方法,即进程在运行前一次申请完它所需要的全部资源,在它的资源未满足前,不让它投入运行。一旦投入运行后,这些资源就一直归它所有,该进程就不会再请求别的任何资源了。
缺点:
有些资源可能只需要用很短的时间,因此如果进程的整个运行期间都一直保持着所有资源,就会造成严重的资源浪费,资源利用率极低。另外,该策略也有可能导致某些进程饥饿。

(四)破坏循环等待条件

​循环等待条件:存在一种进程资源的循环等待链,链中的每一个进程已获得的资源同时被下一个进程所请求。

  • 可采用顺序资源分配法。首先给系统中的资源编号,规定每个进程必须按编号递增的顺序请求资源,同类资源(即编号相同的资源)一次申请完。原理分析:一个进程只有已占有小编号的资源时,才有资格申请更大编号的资源。按该策略的缺点:
    ①、不方便增加新的设备,因为可能需要重新分配所有的编号;
    ②、进程实际使用资源的顺序可能和编号递增顺序不一致,会导致资源浪费;
    ③、必须按规定次序申请资源,用户编程麻烦。
二、死锁的处理策略——避免死锁
(一)什么是安全序列、安全序列、不安全状态、死锁的联系
  • 所谓安全序列,就是指如果系统按照这种序列分配资源,则每个进程都能顺利完成。只要能找出一个安全序列,系统就是安全状态。当然,安全序列可能有多个。
    如果分配了资源之后,系统中找不出任何一个安全序列,系统就进入了不安全状态。这就意味着之后可能所有进程都无法顺利的执行下去。
  • 当然,如果有进程提前归还了一些资源,那系统也有可能重新回到安全状态,不过我们在分配资源之前总是要考虑到最坏的情况。
  • 如果系统处于安全状态,就一定不会发生死锁。如果系统进入不安全状态,就可能发生死锁(处于不安全状态未必就是发生了死锁,但发生死锁时一定是在不安全状态)。因此可以在资源分配之前预先判断这次分配是否会导致系统进入不安全状态,以此决定是否答应资源分配请求。这也是“银行家算法”的核心思想。
(二)银行家算法 --避免死锁
  • 银行家算法是荷兰学者 Dijkstra 为银行系统设计的,以确保银行在发放现金贷款时,不会发生不能满足所有客户需要的情况。后来该算法被用在操作系统中,用于避免死锁。 核心思想:在进程提出资源申请时,先预判此次分配是否会导致系统进入不安全状态。如果会进入不安全状态,就暂时不答应这次请求,让该进程先阻塞等待。
  1. 实现步骤

    分配+回收已经分配的。以此类推,共五次循环检查即可将5个进程都加入安全序列中,最终可得一个安全序列。该算法称为安全性算法。可以很方便地用代码实现以上流程,每一轮检查都从编号较小的进程开始检查。实际做题时可以更快速的得到安全序列。

安全性算法步骤:

  • ①、检查当前的剩余可用资源是否能满足某个进程的最大需求,如果可以,就把该进程加入安全序列,并把该进程持有的资源全部回收。
  • ②、不断重复上述过程,看最终是否能让所有进程都加入安全序列。
    系统处于不安全状态未必死锁,但死锁时一定处于不安全状态。系统处于安全状态一定不会死锁。
三、死锁的处理策略——检测和解除

如果系统中既不采取预防死锁的措施,也不采取避免死锁的措施,系统就很可能发生死锁。在这种情况下,系统应当提供两个算法:
①死锁检测算法:用于检测系统状态,以确定系统中是否发生了死锁。
②死锁解除算法:当认定系统中已经发生了死锁,利用该算法可将系统从死锁状态中解脱出来。

(一)死锁的检测

​ 为了能对系统是否已发生了死锁进行检测,必须:
​ ①用某种数据结构来保存资源的请求和分配信息;
​ ②提供一种算法,利用上述信息来检测系统是否已进入死锁状态。

有向边图消去

(二)死锁的解除

一旦检测出死锁的发生,就应该立即解除死锁。
补充:并不是系统中所有的进程都是死锁状态,用死锁检测算法化简资源分配图后,还连着边的那些进程就是死锁进程
解除死锁的主要方法有:

  • ①、资源剥夺法。挂起(暂时放到外存上)某些死锁进程,并抢占它的资源,将这些资源分配给其他的死锁进程。但是应防止被挂起的进程长时间得不到资源而饥饿。
  • ②、 撤销进程法(或称终止进程法) 。强制撤销部分、甚至全部死锁进程,并剥夺这些进程的资源。这种方式的优点是实现简单,但所付出的代价可能会很大。因为有些进程可能已经运行了很长时间,已经接近结束了,一旦被终止可谓功亏一篑,以后还得从头再来。
  • ③、 进程回退法 。让一个或多个死锁进程回退到足以避免死锁的地步。这就要求系统要记录进程的历史信息,设置还原点。

猜你喜欢

转载自blog.csdn.net/weixin_43673156/article/details/124331606
今日推荐