操作系统复习(持续更新中)

一、操作系统的定义

  操作系统(OS,Operating System)是配置在计算机硬件上的第一层软件,是对硬件系统的首次扩充。它在计算机系统中占据了重要地位,汇编程序、编译程序、数据库管理系统等系统软件,都依赖于操作系统的支持,需要取得操作系统的服务。

二、操作系统在计算机系统中的地位
     从人机交互看,OS是用户与机器的接口,提供良好的人机界面,方便用户使用计算机,在整个计算机系统中具有承上启下的地位。

三、操作系统的主要功能

  为什么需要操作系统?可以很简单的会带,因为为了方便使用者使用计算机,所以需要操作系统,如果没有操作系统,使用计算机将会无比的困难,因为机器只能识别0,1二进制码,而普通人不可能去通过二进制码而计算机打交道,所以必须需要操作系统,这样普通用户才能很好的使用计算机,比如windows操作系统,Linux操作系统,它们都使得我们使用计算机变得非常的简单。既然需要在计算机中装操作系统,那么操作到底有什么作用呢。

  3.1 OS作为用户与计算机硬件系统之间的接口(用户视角

  用户可以通过如下三种方式使用计算机。

  ① 命令方式;OS提供了命令接口,用户可输入命令取得操作系统的服务,并控制用户程序的运行。

  ② 系统调用方式;OS提供了系统调用,用户可在应用程序中调用系统调用,来实现与操作系统的通信,并取得它的服务。

  ③ 图形、窗口方式;这应该是用户最熟悉的一种方式,只需要通过鼠标简单的点击就能够实现与操作系统之间的通信,并取得它的服务。

  OS作为接口示意图如下。

  

  3.2 OS作为计算机系统资源的管理者(系统视角

  OS管理的计算机系统资源如下。

  ① 处理器;用于分配和控制处理器。

  ② 存储器;负责内存的分配与回收。

  ③ I/O设备;负责I/O设备的分配与操纵。

  ④ 文件管理;负责文件的存取、共享和保护。

  3.3 OS实现了对计算机资源的抽象

  若在计算机上没有安装任何软件,那么用户需要面向硬件接口进行编程,那是相当的痛苦,为了方便使用硬件设备,则覆盖了一层I/O设备管理软件,该软件提供Read/Write接口,用户通过此接口就可以使用该硬件设备进行数据的输入和输出了,这就方便了很多。还可以覆盖一层文件管理软件并提供相应的操作接口,用户则可以用户接口去操作文件,而不用去了解具体的细节。OS就是由这些软件组成,其屏蔽了具体的硬件的细节,提供统接口供用户调用,通过该接口就可以轻松的访问操纵硬件资源。

四、操作系统的特征

  操作系统最重要的特征有并发、共享、虚拟和异步。其中,并发是最基本的特征,其他三个特征都是以并发为前提的。

  4.1 并发性  

  提到并发,就自然而然的会提到并行,两者到底有怎样的区别呢。

  并行性是指两个或多个事件在同一时刻发生。

  并发性是指两个或多个事件在同一时间间隔发生。

  有两者的概念可知,在单处理器计算机上,多道程序会交替运行,是并发的;但是每一个时刻最多只有一道程序运行,是并行的。而在多处理器计算机上,同一时刻会有多到程序同时运行,是并行的。

  用户编写好的程序交给计算机运行,从用户的角度看,计算机运行的是程序,但是,实际上,在计算机中运行的是线程。因为程序时静态实体,不能被处理器调用独立运行,更不能与其他程序并发执行。实际上,计算机会为每个程序建立进程,然后为每一个进程建立线程,处理器调度线程,从而到达运行程序的效果。

  而进程与线程也是一个容易混淆的概念。进程是操作系统分配资源的基本单位,线程是独立运行和独立调度的基本单位,线程基本上不占据资源,一个进程的多个线程共享该进程的资源。关于进程与线程,之后还会具体的介绍。

  4.2 共享性

  共享是指操作系统中的资源可供内存中多个并发执行的进程(线程)共同使用,这种资源的共同使用就叫做资源共享。而根据各种资源的不同属性,进程对资源利用的方式也不相同,目前实现资源共享的方式有如下两种。

  1. 互斥共享方式

  互斥共享是指当资源被一个进程A占用时,其他想用使用该资源的进程B就只能等待,只有进程A使用完该资源后,进程B才能够使用该资源,这种共享方式就叫做互斥式共享,把这种资源叫做临界资源或独占资源。如打印机就属于临界资源。

  2. 同时访问方式

  某一资源在一段时间内可由多个进程"同时"访问,这种"同时"是宏观上的,在微观上,这些进程可能是交替对该资源进行访问,磁盘设备就是这类资源。

  4.3 虚拟技术

  虚拟就是通过某种技术把一个物理实体转变为若干个逻辑上的对应物。物理实体是实际存在的,逻辑上的对应物是虚拟的,现在主要有两种虚拟技术,时分复用和空分复用技术。

  1. 时分复用技术

  时分复用技术可以用来实现虚拟处理机、虚拟设备等,用来提高资源的利用率。

  虚拟处理机技术。虚拟处理机技术借助多道程序设计技术,即为每一道程序建立一个进程,让多道程序并发执行,从而达到分时使用一台计算机,这台计算机能够同时为多个用户服务,使得每个终端用户都认为有一个处理机专门为他服务。

  虚拟设备技术。通过虚拟设备技术将一台物理I/O设备虚拟为多态逻辑上的I/O设备,并允许用户占用一台逻辑上的I/O设备,这样可以允许在一段时间内由一个用户访问的设备变为在一段时间内允许多个用户同时访问的共享设备。

  2. 空分复用技术

  空分复用技术可以用来实现虚拟磁盘、虚拟存储等,用来提高资源利用率。

  虚拟磁盘技术。通过虚拟磁盘技术可以将一个硬盘虚拟为多台虚拟磁盘,这样使用起来既安全又方便。如将一块硬盘划分为C、D、E等逻辑盘。

  虚拟存储器技术。利用存储器的空闲空间来存放程序,提高内存利用率。还可以通过虚拟存储器技术(实质上是时分复用技术)在逻辑上扩大存储器的容量,它可以使得一道程序通过时分复用技术在远小于它的内存空间中运行。

  时分复用或空分复用中逻辑设备数为N,那么每台虚拟设备速速必然等于或低于物理设备速度的1/N。

  4.4 异步性

  在多道程序环境下,允许多个进程并发执行,但只有进程在获得所需的资源后方可执行,如正在执行的进程A提出打印请求,需要使用打印机,但是此时进程B正在使用打印机,故此时进程A只能等待并且放弃处理机资源,当进程B使用完打印机后,并且分配处理机才能运行,可以看出,进程的执行通常不是一气呵成的,而是走走停停的方式。进程以不可预知的速度向前推进,此即进程的异步性。异步性也是操作系统的一个重要特征。

五、发展历史

        批处理系统,Batch Processing Systems。Jobs 在内存或者外存里,内存始终有一个job在运行,操作系统负责在结束后加载下一个开始运行(我们将加载到内存并运行的程序为进程,process)。问题在于如果运行时发生I/O,CPU就会停下来等I/O完成。

        多道批处理系统,Multiprogramming Batch Systems。在批处理系统基础上,当当前job发生I/O时,操作系统负责让CPU转而运行另一个job。问题在于,没有考虑和用户的交互,响应时间较长。

        分时系统,Time Sharing Systems。将CPU时间划分为很小的时间片,操作系统负责安排各个job轮流运行。由于切换频率很高,从而用户通过输入设备向计算机发出指令时可以收到实时的回复。
分时系统本身也是一种多道(multiprogramming)系统,即允许多个job并发(concurrently)执行。但是不是批处理(batch)系统。

六、CPU 的两种工作模式(用户态、内核态)

用户态(User Mode)

  1. 定义: 用户态是指程序在非特权级别下运行的模式。大多数应用程序和进程都在用户态运行。
  2. 特权级别: 用户态的特权级别低,不能直接访问硬件和关键系统资源。
  3. 权限限制: 在用户态,程序只能执行非特权指令,无法直接进行I/O操作、内存管理等。这些操作需要通过系统调用请求内核的帮助。
  4. 目的: 用户态的设计目的是保护操作系统内核和其他进程的安全,防止应用程序直接干扰系统的关键功能。
  5. 切换方式: 当用户态程序需要执行特权操作时,它会发出系统调用请求,导致CPU从用户态切换到内核态。

内核态(Kernel Mode)

  1. 定义: 内核态是指操作系统内核在特权级别下运行的模式。内核态有权访问所有硬件资源和执行所有指令。
  2. 特权级别: 内核态的特权级别高,可以执行所有CPU指令,包括特权指令。
  3. 权限: 在内核态,操作系统内核可以直接访问硬件、内存和其他关键资源,进行进程管理、内存管理、I/O操作等。
  4. 安全性: 内核态运行的代码必须非常可靠,因为任何错误都可能导致整个系统崩溃。
  5. 切换方式: 当内核完成特权操作后,会将控制权返回给用户态程序,切换回用户态。

切换过程

  1. 用户态到内核态: 这种切换通常由系统调用引发。用户程序通过执行系统调用指令来请求内核服务,此时CPU会切换到内核态,操作系统内核接管控制权。
  2. 内核态到用户态: 当内核完成请求的操作后,它会将控制权交还给用户程序。这通常是通过一个特定的返回指令(如iretsysret)来实现的,恢复用户态的执行上下文。

重要性

  1. 安全性: 通过区分用户态和内核态,可以有效地防止用户程序直接访问或修改系统的关键资源,保护系统的稳定性和安全性。
  2. 稳定性: 内核态代码的错误可能导致整个系统崩溃,因此必须格外小心。而用户态程序的错误通常只会影响自身,不会破坏整个系统。
  3. 效率: 虽然用户态和内核态的切换有一定的开销,但这种设计可以最大限度地利用CPU的特权指令,提高系统的整体性能。
七、特权指令和非特权指令 

特权指令(Privileged Instructions)

  1. 定义: 特权指令是只能在内核态(Kernel Mode)下执行的指令。这些指令通常涉及硬件和系统资源的管理,如内存管理、I/O操作、中断管理等。
  2. 权限要求: 只有在内核态下,CPU才能执行这些指令。如果在用户态(User Mode)下尝试执行特权指令,会引发异常或陷阱,操作系统内核会捕获并处理这种违规操作。
  3. 功能:
    • 中断控制: 使能或禁用中断。
    • I/O操作: 直接访问和控制I/O设备。
    • 内存管理: 修改页表、设置段寄存器等。
    • 处理器状态修改: 改变处理器状态、模式或特权级别。
  4. 例子:
    • CLI (Clear Interrupt Flag): 禁用中断。
    • STI (Set Interrupt Flag): 使能中断。
    • HLT (Halt): 使处理器进入休眠状态,直到下一个中断。
    • IN/OUT: 进行I/O端口的读写操作。

非特权指令(Non-Privileged Instructions)

  1. 定义: 非特权指令是在用户态和内核态都可以执行的指令。这些指令不涉及对关键系统资源的直接访问和控制。
  2. 权限要求: 用户态和内核态都可以执行这些指令,不会引发权限异常。
  3. 功能:
    • 算术操作: 加减乘除等基本算术运算。
    • 逻辑操作: 位与、位或、位非、异或等逻辑运算。
    • 数据传输: 内存与寄存器之间的数据传输。
    • 控制转移: 无条件跳转、条件跳转、子程序调用和返回。
  4. 例子:
    • ADD: 进行加法运算。
    • MOV: 数据传输指令。
    • JMP: 无条件跳转。
    • CALL/RET: 子程序调用和返回。

关键区别

  1. 执行权限:

    • 特权指令: 仅在内核态下执行,用户态执行会引发异常。
    • 非特权指令: 在用户态和内核态均可执行。
  2. 系统资源访问:

    • 特权指令: 直接访问和控制硬件、内存管理单元、中断系统等关键资源。
    • 非特权指令: 通常不涉及直接对硬件或关键资源的控制。
  3. 安全性:

    • 特权指令: 设计为只有内核能够执行,以保护系统的稳定和安全。
    • 非特权指令: 设计为应用程序可以安全执行,不会影响系统的整体稳定性。

通过将指令分为特权和非特权,操作系统能够有效地管理资源和权限,确保用户程序不能直接干扰或破坏系统的关键部分。这种机制是现代操作系统安全模型的重要组成部分。

八、操作系统的接口(Command Interpreter、GUI、system call)

命令解释器(Command Interpreter)

  1. 定义: 命令解释器,也称为命令行界面(CLI,Command Line Interface),是用户通过命令行输入文本命令来与操作系统交互的接口。
  2. 功能:
    • 执行命令: 允许用户输入和执行系统命令,例如文件操作(复制、删除、重命名等)、程序运行和脚本执行。
    • 脚本编写: 用户可以编写批处理文件或脚本(如Shell脚本)来自动化任务。
    • 系统管理: 提供管理系统的工具,例如进程管理、用户管理和网络配置等。
  3. 例子:
    • Windows: Command Prompt (cmd.exe), PowerShell
    • Unix/Linux: Bash, Zsh, KornShell (ksh)
    • MacOS: Terminal (使用Bash或Zsh等)

图形用户界面(GUI, Graphical User Interface)

  1. 定义: 图形用户界面是一种通过图形元素(如窗口、图标、按钮等)来与用户交互的界面,用户可以通过鼠标、键盘和触摸屏等输入设备进行操作。
  2. 功能:
    • 直观操作: 提供一个直观和用户友好的界面,使用户能够通过点击、拖放等操作来执行任务。
    • 多任务处理: 允许用户同时操作多个窗口和应用程序,提高工作效率。
    • 丰富的视觉反馈: 提供图形化的状态显示和反馈,增强用户体验。
  3. 例子:
    • Windows: Windows操作系统自带的GUI(如Windows 10, Windows 11)
    • MacOS: macOS的Aqua界面
    • Linux: GNOME, KDE Plasma, XFCE等桌面环境

系统调用(System Call)

  1. 定义: 系统调用是操作系统提供的一组函数,允许用户程序请求操作系统内核提供服务,如文件操作、进程管理、内存管理等。系统调用是程序与操作系统内核交互的接口。
  2. 功能:
    • 文件操作: 打开、关闭、读写文件,获取文件信息等。
    • 进程管理: 创建、终止进程,进程间通信,进程调度等。
    • 内存管理: 分配和释放内存,内存映射等。
    • 设备管理: 访问和控制硬件设备。
  3. 例子:
    • POSIX标准: open(), read(), write(), fork(), exec(), wait(), mmap()
    • Windows API: CreateFile(), ReadFile(), WriteFile(), CreateProcess(), VirtualAlloc()

总结

  • 命令解释器: 提供基于文本的交互方式,适合高级用户和系统管理员进行复杂操作和脚本编写。
  • 图形用户界面: 提供直观的图形化交互方式,适合一般用户进行日常操作,提高易用性和用户体验。
  • 系统调用: 提供程序与操作系统内核交互的低级接口,是实现高层功能和服务的基础。

这三种接口共同构成了操作系统的用户和程序交互体系,满足了不同层次和不同类型用户的需求。

进程

进程的定义

我们编写的代码只是一个存储在硬盘的静态文件,通过编译后就会生成二进制可执行文件,当我们运行这个可执行文件后,它会被装载到内存中,接着 CPU 会执行程序中的每一条指令,那么这个运行中的程序,就被称为「进程」(Process)

现在我们考虑有一个会读取硬盘文件数据的程序被执行了,那么当运行到读取文件的指令时,就会去从硬盘读取数据,但是硬盘的读写速度是非常慢的,那么在这个时候,如果 CPU 傻傻的等硬盘返回数据的话,那 CPU 的利用率是非常低的。

做个类比,你去煮开水时,你会傻傻的等水壶烧开吗?很明显,小孩也不会傻等。我们可以在水壶烧开之前去做其他事情。当水壶烧开了,我们自然就会听到“嘀嘀嘀”的声音,于是再把烧开的水倒入到水杯里就好了。

所以,当进程要从硬盘读取数据时,CPU 不需要阻塞等待数据的返回,而是去执行另外的进程。当硬盘数据返回时,CPU 会收到个中断,于是 CPU 再继续运行这个进程。

进程 1 与进程 2 切换

这种多个程序、交替执行的思想,就有 CPU 管理多个进程的初步想法。

对于一个支持多进程的系统,CPU 会从一个进程快速切换至另一个进程,其间每个进程各运行几十或几百个毫秒。

虽然单核的 CPU 在某一个瞬间,只能运行一个进程。但在 1 秒钟期间,它可能会运行多个进程,这样就产生并行的错觉,实际上这是并发

并发和并行有什么区别?

一图胜千言。

并发与并行

进程与程序的关系的类比

到了晚饭时间,一对小情侣肚子都咕咕叫了,于是男生见机行事,就想给女生做晚饭,所以他就在网上找了辣子鸡的菜谱,接着买了一些鸡肉、辣椒、香料等材料,然后边看边学边做这道菜。

突然,女生说她想喝可乐,那么男生只好把做菜的事情暂停一下,并在手机菜谱标记做到哪一个步骤,把状态信息记录了下来。

然后男生听从女生的指令,跑去下楼买了一瓶冰可乐后,又回到厨房继续做菜。

这体现了,CPU 可以从一个进程(做菜)切换到另外一个进程(买可乐),在切换前必须要记录当前进程中运行的状态信息,以备下次切换回来的时候可以恢复执行。

所以,可以发现进程有着「运行 - 暂停 - 运行」的活动规律。

#进程的状态及其变化

在上面,我们知道了进程有着「运行 - 暂停 - 运行」的活动规律。一般说来,一个进程并不是自始至终连续不停地运行的,它与并发执行中的其他进程的执行是相互制约的。

它有时处于运行状态,有时又由于某种原因而暂停运行处于等待状态,当使它暂停的原因消失后,它又进入准备运行状态。

所以,在一个进程的活动期间至少具备三种基本状态,即运行状态、就绪状态、阻塞状态。

进程的三种基本状态

上图中各个状态的意义:

  • 运行状态(Running):该时刻进程占用 CPU;
  • 就绪状态(Ready):可运行,由于其他进程处于运行状态而暂时停止运行;
  • 阻塞状态(Blocked):该进程正在等待某一事件发生(如等待输入/输出操作的完成)而暂时停止运行,这时,即使给它CPU控制权,它也无法运行;

当然,进程还有另外两个基本状态:

  • 创建状态(new):进程正在被创建时的状态;
  • 结束状态(Exit):进程正在从系统中消失时的状态;

于是,一个完整的进程状态的变迁如下图:

进程五种状态的变迁

再来详细说明一下进程的状态变迁:

  • NULL -> 创建状态:一个新进程被创建时的第一个状态;
  • 创建状态 -> 就绪状态:当进程被创建完成并初始化后,一切就绪准备运行时,变为就绪状态,这个过程是很快的;
  • 就绪态 -> 运行状态:处于就绪状态的进程被操作系统的进程调度器选中后,就分配给 CPU 正式运行该进程;
  • 运行状态 -> 结束状态:当进程已经运行完成或出错时,会被操作系统作结束状态处理;
  • 运行状态 -> 就绪状态:处于运行状态的进程在运行过程中,由于分配给它的运行时间片用完,操作系统会把该进程变为就绪态,接着从就绪态选中另外一个进程运行;
  • 运行状态 -> 阻塞状态:当进程请求某个事件且必须等待时,例如请求 I/O 事件;
  • 阻塞状态 -> 就绪状态:当进程要等待的事件完成时,它从阻塞状态变到就绪状态;

如果有大量处于阻塞状态的进程,进程可能会占用着物理内存空间,显然不是我们所希望的,毕竟物理内存空间是有限的,被阻塞状态的进程占用着物理内存就一种浪费物理内存的行为。

所以,在虚拟内存管理的操作系统中,通常会把阻塞状态的进程的物理内存空间换出到硬盘,等需要再次运行的时候,再从硬盘换入到物理内存。

虚拟内存管理-换入换出

那么,就需要一个新的状态,来描述进程没有占用实际的物理内存空间的情况,这个状态就是挂起状态。这跟阻塞状态是不一样,阻塞状态是等待某个事件的返回。

另外,挂起状态可以分为两种:

  • 阻塞挂起状态:进程在外存(硬盘)并等待某个事件的出现;
  • 就绪挂起状态:进程在外存(硬盘),但只要进入内存,即刻立刻运行;

这两种挂起状态加上前面的五种状态,就变成了七种状态变迁(留给我的颜色不多了),见如下图:

七种状态变迁

导致进程挂起的原因不只是因为进程所使用的内存空间不在物理内存,还包括如下情况:

  • 通过 sleep 让进程间歇性挂起,其工作原理是设置一个定时器,到期后唤醒进程。
  • 用户希望挂起一个程序的执行,比如在 Linux 中用 Ctrl+Z 挂起进程;

#进程控制块(PCB)

在操作系统中,是用进程控制块process control block,PCB)数据结构来描述进程的。

那 PCB 是什么呢?打开知乎搜索你就会发现这个东西并不是那么简单。

知乎搜 PCB 的提示

打住打住,我们是个正经的人,怎么会去看那些问题呢?是吧,回来回来。

PCB 是进程存在的唯一标识,这意味着一个进程的存在,必然会有一个 PCB,如果进程消失了,那么 PCB 也会随之消失。

PCB 具体包含什么信息呢?

进程描述信息:

  • 进程标识符:标识各个进程,每个进程都有一个并且唯一的标识符;
  • 用户标识符:进程归属的用户,用户标识符主要为共享和保护服务;

进程控制和管理信息:

  • 进程当前状态,如 new、ready、running、waiting 或 blocked 等;
  • 进程优先级:进程抢占 CPU 时的优先级;

资源分配清单:

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

CPU 相关信息:

  • CPU 中各个寄存器的值,当进程被切换时,CPU 的状态信息都会被保存在相应的 PCB 中,以便进程重新执行时,能从断点处继续执行。

可见,PCB 包含信息还是比较多的。

每个 PCB 是如何组织的呢?

进程队列

通常是通过链表的方式进行组织,把具有相同状态的进程链在一起,组成各种队列。比如:

  • 将所有处于就绪状态的进程链在一起,称为就绪队列
  • 把所有因等待某事件而处于等待状态的进程链在一起就组成各种阻塞队列
  • 另外,对于运行队列在单核 CPU 系统中则只有一个运行指针了,因为单核 CPU 在某个时间,只能运行一个程序。

那么,就绪队列和阻塞队列链表的组织形式如下图:

就绪队列和阻塞队列

除了链接的组织方式,还有索引方式,它的工作原理:将同一状态的进程组织在一个索引表中,索引表项指向相应的 PCB,不同状态对应不同的索引表。

一般会选择链表,因为可能面临进程创建,销毁等调度导致进程状态发生变化,所以链表能够更加灵活的插入和删除。

#进程的控制

我们熟知了进程的状态变迁和进程的数据结构 PCB 后,再来看看进程的创建、终止、阻塞、唤醒的过程,这些过程也就是进程的控制。

01 创建进程

操作系统允许一个进程创建另一个进程,而且允许子进程继承父进程所拥有的资源。

创建进程的过程如下:

  • 申请一个空白的 PCB,并向 PCB 中填写一些控制和管理进程的信息,比如进程的唯一标识等;
  • 为该进程分配运行时所必需的资源,比如内存资源;
  • 将 PCB 插入到就绪队列,等待被调度运行;

进程家族树

进程家族树(Process Family Tree)表示操作系统中进程之间的父子关系。每个进程都有一个父进程,可能还有一个或多个子进程。整个系统的进程结构可以用一棵树来表示,其中:

  • 根节点:系统启动时创建的第一个进程,通常是 init 进程(在现代 Linux 系统中可能是 systemd)。该进程的 PID(进程标识符)通常是 1。
  • 父进程:任何进程创建的子进程。创建者为父进程,创建的为子进程。
  • 子进程:由父进程创建的新进程。

进程创建

进程创建是指操作系统通过某种机制创建一个新进程的过程。在 Unix 类系统中,进程创建主要通过 fork()exec() 系统调用实现。

  1. fork():创建一个新进程。新进程是现有进程的一个副本,称为子进程。子进程获得了一个新的 PID。

    • 调用成功时,父进程获得子进程的 PID,子进程获得 0。
    • 在子进程中运行的代码与父进程中的代码相同,除了 fork() 的返回值不同。
  2. exec():在当前进程的上下文中执行一个新程序。exec() 替换当前进程的内存空间、代码段、数据段等,执行新的程序。

    • exec() 族包括 execl(), execp(), execle(), execvp() 等函数。

通常,fork()exec() 结合使用:首先通过 fork() 创建子进程,然后在子进程中使用 exec() 执行新程序。

02 终止进程

进程可以有 3 种终止方式:正常结束、异常结束以及外界干预(信号 kill 掉)。

当子进程被终止时,其在父进程处继承的资源应当还给父进程。而当父进程被终止时,该父进程的子进程就变为孤儿进程,会被 1 号进程收养,并由 1 号进程对它们完成状态收集工作。

终止进程的过程如下:

  • 查找需要终止的进程的 PCB;
  • 如果处于执行状态,则立即终止该进程的执行,然后将 CPU 资源分配给其他进程;
  • 如果其还有子进程,则应将该进程的子进程交给 1 号进程接管;
  • 将该进程所拥有的全部资源都归还给操作系统;
  • 将其从 PCB 所在队列中删除;

03 阻塞进程

当进程需要等待某一事件完成时,它可以调用阻塞语句把自己阻塞等待。而一旦被阻塞等待,它只能由另一个进程唤醒。

阻塞进程的过程如下:

  • 找到将要被阻塞进程标识号对应的 PCB;
  • 如果该进程为运行状态,则保护其现场,将其状态转为阻塞状态,停止运行;
  • 将该 PCB 插入到阻塞队列中去;

04 唤醒进程

进程由「运行」转变为「阻塞」状态是由于进程必须等待某一事件的完成,所以处于阻塞状态的进程是绝对不可能叫醒自己的。

如果某进程正在等待 I/O 事件,需由别的进程发消息给它,则只有当该进程所期待的事件出现时,才由发现者进程用唤醒语句叫醒它。

唤醒进程的过程如下:

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

进程的阻塞和唤醒是一对功能相反的语句,如果某个进程调用了阻塞语句,则必有一个与之对应的唤醒语句。

#进程的上下文切换

各个进程之间是共享 CPU 资源的,在不同的时候进程之间需要切换,让不同的进程可以在 CPU 执行,那么这个一个进程切换到另一个进程运行,称为进程的上下文切换

在详细说进程上下文切换前,我们先来看看 CPU 上下文切换

大多数操作系统都是多任务,通常支持大于 CPU 数量的任务同时运行。实际上,这些任务并不是同时运行的,只是因为系统在很短的时间内,让各个任务分别在 CPU 运行,于是就造成同时运行的错觉。

任务是交给 CPU 运行的,那么在每个任务运行前,CPU 需要知道任务从哪里加载,又从哪里开始运行。

所以,操作系统需要事先帮 CPU 设置好 CPU 寄存器和程序计数器

CPU 寄存器是 CPU 内部一个容量小,但是速度极快的内存(缓存)。我举个例子,寄存器像是你的口袋,内存像你的书包,硬盘则是你家里的柜子,如果你的东西存放到口袋,那肯定是比你从书包或家里柜子取出来要快的多。

再来,程序计数器则是用来存储 CPU 正在执行的指令位置、或者即将执行的下一条指令位置。

所以说,CPU 寄存器和程序计数是 CPU 在运行任何任务前,所必须依赖的环境,这些环境就叫做 CPU 上下文

既然知道了什么是 CPU 上下文,那理解 CPU 上下文切换就不难了。

CPU 上下文切换就是先把前一个任务的 CPU 上下文(CPU 寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。

系统内核会存储保持下来的上下文信息,当此任务再次被分配给 CPU 运行时,CPU 会重新加载这些上下文,这样就能保证任务原来的状态不受影响,让任务看起来还是连续运行。

上面说到所谓的「任务」,主要包含进程、线程和中断。所以,可以根据任务的不同,把 CPU 上下文切换分成:进程上下文切换、线程上下文切换和中断上下文切换

进程的上下文切换到底是切换什么呢?

进程是由内核管理和调度的,所以进程的切换只能发生在内核态。

所以,进程的上下文切换不仅包含了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源。

通常,会把交换的信息保存在进程的 PCB,当要运行另外一个进程的时候,我们需要从这个进程的 PCB 取出上下文,然后恢复到 CPU 中,这使得这个进程可以继续执行,如下图所示:

进程上下文切换

大家需要注意,进程的上下文开销是很关键的,我们希望它的开销越小越好,这样可以使得进程可以把更多时间花费在执行程序上,而不是耗费在上下文切换。

发生进程上下文切换有哪些场景?

  • 为了保证所有进程可以得到公平调度,CPU 时间被划分为一段段的时间片,这些时间片再被轮流分配给各个进程。这样,当某个进程的时间片耗尽了,进程就从运行状态变为就绪状态,系统从就绪队列选择另外一个进程运行;
  • 进程在系统资源不足(比如内存不足)时,要等到资源满足后才可以运行,这个时候进程也会被挂起,并由系统调度其他进程运行;
  • 当进程通过睡眠函数 sleep 这样的方法将自己主动挂起时,自然也会重新调度;
  • 当有优先级更高的进程运行时,为了保证高优先级进程的运行,当前进程会被挂起,由高优先级进程来运行;
  • 发生硬件中断时,CPU 上的进程会被中断挂起,转而执行内核中的中断服务程序;

以上,就是发生进程上下文切换的常见场景了。

线程的实现

因为有两种系统模式,用户态和内核态,所以也有两种模式下的线程。用户态中实现的线程叫用户线程,内核中实现的线程叫内核线程。

  • 用户线程(User Thread:在用户空间实现的线程,不是由内核管理的线程,是由用户态的线程库来完成线程的管理;
  • 内核线程(Kernel Thread:在内核中实现的线程,是由内核管理的线程。

那么,这还需要考虑一个问题,用户线程和内核线程的对应关系。

首先,第一种关系是多对一的关系,也就是多个用户线程对应同一个内核线程:

多对一

第二种是一对一的关系,也就是一个用户线程对应一个内核线程:

一对一

第三种是多对多的关系,也就是多个用户线程对应到多个内核线程:

多对多

用户线程如何理解?存在什么优势和缺陷?

用户线程是基于用户态的线程管理库来实现的,那么线程控制块(Thread Control Block, TCB 也是在库里面来实现的,对于操作系统而言是看不到这个 TCB 的,它只能看到整个进程的 PCB。

所以,用户线程的整个线程管理和调度,操作系统是不直接参与的,而是由用户级线程库函数来完成线程的管理,包括线程的创建、终止、同步和调度等。

用户级线程的模型,也就类似前面提到的多对一的关系,即多个用户线程对应同一个内核线程,如下图所示:

用户级线程模型

用户线程的优点

  • 每个进程都需要有它私有的线程控制块(TCB)列表,用来跟踪记录它各个线程状态信息(PC、栈指针、寄存器),TCB 由用户级线程库函数来维护,可用于不支持线程技术的操作系统;
  • 用户线程的切换也是由线程库函数来完成的,无需用户态与内核态的切换,所以速度特别快;

用户线程的缺点

  • 由于操作系统不参与线程的调度,如果一个线程发起了系统调用而阻塞,那进程所包含的用户线程都不能执行了,(本质是因为只对应一个内核线程,不能在内核线程中切换
  • 当一个线程开始运行后,除非它主动地交出 CPU 的使用权,否则它所在的进程当中的其他线程无法运行,因为用户态的线程没法打断当前运行中的线程,它没有这个特权,只有操作系统才有,但是用户线程不是由操作系统管理的。
  • 由于时间片分配给进程,故与其他进程比,在多线程执行时,每个线程得到的时间片较少,执行会比较慢;

以上,就是用户线程的优缺点了。

那内核线程如何理解?存在什么优势和缺陷?

内核线程是由操作系统管理的,线程对应的 TCB 自然是放在操作系统里的,这样线程的创建、终止和管理都是由操作系统负责。

内核线程的模型,也就类似前面提到的一对一的关系,即一个用户线程对应一个内核线程,如下图所示:

内核线程模型

内核线程的优点

  • 在一个进程当中,如果某个内核线程发起系统调用而被阻塞,并不会影响其他内核线程的运行;
  • 分配给线程,多线程的进程获得更多的 CPU 运行时间;

内核线程的缺点

  • 在支持内核线程的操作系统中,由内核来维护进程和线程的上下文信息,如 PCB 和 TCB;
  • 线程的创建、终止和切换都是通过系统调用的方式来进行,因此对于系统来说,系统开销比较大;

以上,就是内核线程的优缺点了。

最后的轻量级进程如何理解?

轻量级进程(Light-weight process,LWP)是内核支持的用户线程,一个进程可有一个或多个 LWP,每个 LWP 是跟内核线程一对一映射的,也就是 LWP 都是由一个内核线程支持,而且 LWP 是由内核管理并像普通进程一样被调度

在多对多模型中,LWP起到了用户线程和内核线程之间的桥梁作用。在一对一和多对一模型中,不需要LWP,用户线程可以直接映射到内核线程或共享一个内核线程。

在大多数系统中,LWP与普通进程的区别也在于它只有一个最小的执行上下文和调度程序所需的统计信息。一般来说,一个进程代表程序的一个实例,而 LWP 代表程序的执行线程,因为一个执行线程不像进程那样需要那么多状态信息,所以 LWP 也不带有这样的信息。

在 LWP 之上也是可以使用用户线程的,那么 LWP 与用户线程的对应关系就有三种:

  • 1 : 1,即一个 LWP 对应 一个用户线程;
  • N : 1,即一个 LWP 对应多个用户线程;
  • M : N,即多个 LWP 对应多个用户线程;

接下来针对上面这三种对应关系说明它们优缺点。先看下图的 LWP 模型:

LWP 模型

1 : 1 模式

一个线程对应到一个 LWP 再对应到一个内核线程,如上图的进程 4,属于此模型。

  • 优点:实现并行,当一个 LWP 阻塞,不会影响其他 LWP;
  • 缺点:每一个用户线程,就产生一个内核线程,创建线程的开销较大。

N : 1 模式

多个用户线程对应一个 LWP 再对应一个内核线程,如上图的进程 2,线程管理是在用户空间完成的,此模式中用户的线程对操作系统不可见。

  • 优点:用户线程要开几个都没问题,且上下文切换发生用户空间,切换的效率较高;
  • 缺点:一个用户线程如果阻塞了,则整个进程都将会阻塞,另外在多核 CPU 中,是没办法充分利用 CPU 的。

M : N 模式

根据前面的两个模型混搭一起,就形成 M:N 模型,该模型提供了两级控制,首先多个用户线程对应到多个 LWP,LWP 再一一对应到内核线程,如上图的进程 3。

  • 优点:综合了前两种优点,大部分的线程上下文发生在用户空间,且多个线程又可以充分利用多核 CPU 的资源。

调度

进程都希望自己能够占用 CPU 进行工作,那么这涉及到前面说过的进程上下文切换。

一旦操作系统把进程切换到运行状态,也就意味着该进程占用着 CPU 在执行,但是当操作系统把进程切换到其他状态时,那就不能在 CPU 中执行了,于是操作系统会选择下一个要运行的进程。

选择一个进程运行这一功能是在操作系统中完成的,通常称为调度程序scheduler)。

那到底什么时候调度进程,或以什么原则来调度进程呢?

TIP

我知道很多人会问,线程不是操作系统的调度单位吗?为什么这里参与调度的是进程?

先提前说明,这里的进程指只有主线程的进程,所以调度主线程就等于调度了整个进程。

那为什么干脆不直接取名线程调度?主要是操作系统相关书籍,都是用进程调度这个名字,所以我也沿用了这个名字。

#调度时机

在进程的生命周期中,当进程从一个运行状态到另外一状态变化的时候,其实会触发一次调度。

比如,以下状态的变化都会触发操作系统的调度:

  • 从就绪态 -> 运行态:当进程被创建时,会进入到就绪队列,操作系统会从就绪队列选择一个进程运行;
  • 从运行态 -> 阻塞态:当进程发生 I/O 事件而阻塞时,操作系统必须选择另外一个进程运行;
  • 从运行态 -> 结束态:当进程退出结束后,操作系统得从就绪队列选择另外一个进程运行;

因为,这些状态变化的时候,操作系统需要考虑是否要让新的进程给 CPU 运行,或者是否让当前进程从 CPU 上退出来而换另一个进程运行。

另外,如果硬件时钟提供某个频率的周期性中断,那么可以根据如何处理时钟中断 ,把调度算法分为两类:

  • 非抢占式调度算法挑选一个进程,然后让该进程运行直到被阻塞,或者直到该进程退出,才会调用另外一个进程,也就是说不会理时钟中断这个事情。
  • 抢占式调度算法挑选一个进程,然后让该进程只运行某段时间,如果在该时段结束时,该进程仍然在运行时,则会把它挂起,接着调度程序从就绪队列挑选另外一个进程。这种抢占式调度处理,需要在时间间隔的末端发生时钟中断,以便把 CPU 控制返回给调度程序进行调度,也就是常说的时间片机制

#调度原则

原则一:如果运行的程序,发生了 I/O 事件的请求,那 CPU 使用率必然会很低,因为此时进程在阻塞等待硬盘的数据返回。这样的过程,势必会造成 CPU 突然的空闲。所以,为了提高 CPU 利用率,在这种发送 I/O 事件致使 CPU 空闲的情况下,调度程序需要从就绪队列中选择一个进程来运行。

原则二:有的程序执行某个任务花费的时间会比较长,如果这个程序一直占用着 CPU,会造成系统吞吐量(CPU 在单位时间内完成的进程数量)的降低。所以,要提高系统的吞吐率,调度程序要权衡长任务和短任务进程的运行完成数量。

原则三:从进程开始到结束的过程中,实际上是包含两个时间,分别是进程运行时间和进程等待时间,这两个时间总和就称为周转时间。进程的周转时间越小越好,如果进程的等待时间很长而运行时间很短,那周转时间就很长,这不是我们所期望的,调度程序应该避免这种情况发生。

原则四:处于就绪队列的进程,也不能等太久,当然希望这个等待的时间越短越好,这样可以使得进程更快的在 CPU 中执行。所以,就绪队列中进程的等待时间也是调度程序所需要考虑的原则。

原则五:对于鼠标、键盘这种交互式比较强的应用,我们当然希望它的响应时间越快越好,否则就会影响用户体验了。所以,对于交互式比较强的应用,响应时间也是调度程序需要考虑的原则。

五种调度原则

针对上面的五种调度原则,总结成如下:

  • CPU 利用率:调度程序应确保 CPU 是始终匆忙的状态,这可提高 CPU 的利用率;
  • 系统吞吐量:吞吐量表示的是单位时间内 CPU 完成进程的数量,长作业的进程会占用较长的 CPU 资源,因此会降低吞吐量,相反,短作业的进程会提升系统吞吐量;
  • 周转时间:周转时间是进程运行+阻塞时间+等待时间的总和,一个进程的周转时间越小越好;
  • 等待时间:这个等待时间不是阻塞状态的时间,而是进程处于就绪队列的时间,等待的时间越长,用户越不满意;
  • 响应时间:用户提交请求到系统第一次产生响应所花费的时间,在交互式系统中,响应时间是衡量调度算法好坏的主要标准。

说白了,这么多调度原则,目的就是要使得进程要「快」。

#调度算法

不同的调度算法适用的场景也是不同的。

接下来,说说在单核 CPU 系统中常见的调度算法。

01 先来先服务调度算法

最简单的一个调度算法,就是非抢占式的先来先服务(First Come First Serve, FCFS)算法了。

FCFS 调度算法

顾名思义,先来后到,每次从就绪队列选择最先进入队列的进程,然后一直运行,直到进程退出或被阻塞,才会继续从队列中选择第一个进程接着运行。

这似乎很公平,但是当一个长作业先运行了,那么后面的短作业等待的时间就会很长,不利于短作业。

FCFS 对长作业有利,适用于 CPU 繁忙型作业的系统,而不适用于 I/O 繁忙型作业的系统。

02 最短作业优先调度算法

最短作业优先(Shortest Job First, SJF)调度算法同样也是顾名思义,它会优先选择运行时间最短的进程来运行,这有助于提高系统的吞吐量。

SJF 调度算法

这显然对长作业不利,很容易造成一种极端现象。

比如,一个长作业在就绪队列等待运行,而这个就绪队列有非常多的短作业,那么就会使得长作业不断的往后推,周转时间变长,致使长作业长期不会被运行。

03 高响应比优先调度算法

前面的「先来先服务调度算法」和「最短作业优先调度算法」都没有很好的权衡短作业和长作业。

那么,高响应比优先 (Highest Response Ratio Next, HRRN)调度算法主要是权衡了短作业和长作业。

每次进行进程调度时,先计算「响应比优先级」,然后把「响应比优先级」最高的进程投入运行,「响应比优先级」的计算公式:

从上面的公式,可以发现:

  • 如果两个进程的「等待时间」相同时,「要求的服务时间」越短,「响应比」就越高,这样短作业的进程容易被选中运行;
  • 如果两个进程「要求的服务时间」相同时,「等待时间」越长,「响应比」就越高,这就兼顾到了长作业进程,因为进程的响应比可以随时间等待的增加而提高,当其等待时间足够长时,其响应比便可以升到很高,从而获得运行的机会;

TIP

很多人问怎么才能知道一个进程要求服务的时间?这不是不可预知的吗?

对的,这是不可预估的。所以,高响应比优先调度算法是「理想型」的调度算法,现实中是实现不了的。

04 时间片轮转调度算法

最古老、最简单、最公平且使用最广的算法就是时间片轮转(Round Robin, RR)调度算法

RR 调度算法

每个进程被分配一个时间段,称为时间片(Quantum),即允许该进程在该时间段中运行。

  • 如果时间片用完,进程还在运行,那么将会把此进程从 CPU 释放出来,并把 CPU 分配给另外一个进程;
  • 如果该进程在时间片结束前阻塞或结束,则 CPU 立即进行切换;

另外,时间片的长度就是一个很关键的点:

  • 如果时间片设得太短会导致过多的进程上下文切换,降低了 CPU 效率;
  • 如果设得太长又可能引起对短作业进程的响应时间变长。将

一般来说,时间片设为 20ms~50ms 通常是一个比较合理的折中值。

05 最高优先级调度算法

前面的「时间片轮转算法」做了个假设,即让所有的进程同等重要,也不偏袒谁,大家的运行时间都一样。

但是,对于多用户计算机系统就有不同的看法了,它们希望调度是有优先级的,即希望调度程序能从就绪队列中选择最高优先级的进程进行运行,这称为最高优先级(Highest Priority First,HPF)调度算法

进程的优先级可以分为,静态优先级和动态优先级:

  • 静态优先级:创建进程时候,就已经确定了优先级了,然后整个运行时间优先级都不会变化;
  • 动态优先级:根据进程的动态变化调整优先级,比如如果进程运行时间增加,则降低其优先级,如果进程等待时间(就绪队列的等待时间)增加,则升高其优先级,也就是随着时间的推移增加等待进程的优先级

该算法也有两种处理优先级高的方法,非抢占式和抢占式:

  • 非抢占式:当就绪队列中出现优先级高的进程,运行完当前进程,再选择优先级高的进程。
  • 抢占式:当就绪队列中出现优先级高的进程,当前进程挂起,调度优先级高的进程运行。

但是依然有缺点,可能会导致低优先级的进程永远不会运行。

06 多级反馈队列调度算法

多级反馈队列(Multilevel Feedback Queue)调度算法是「时间片轮转算法」和「最高优先级算法」的综合和发展。

顾名思义:

  • 「多级」表示有多个队列,每个队列优先级从高到低,同时优先级越高时间片越短。
  • 「反馈」表示如果有新的进程加入优先级高的队列时,立刻停止当前正在运行的进程,转而去运行优先级高的队列;

多级反馈队列

来看看,它是如何工作的:

  • 设置了多个队列,赋予每个队列不同的优先级,每个队列优先级从高到低,同时优先级越高时间片越短
  • 新的进程会被放入到第一级队列的末尾,按先来先服务的原则排队等待被调度,如果在第一级队列规定的时间片没运行完成,则将其转入到第二级队列的末尾,以此类推,直至完成;
  • 当较高优先级的队列为空,才调度较低优先级的队列中的进程运行。如果进程运行时,有新进程进入较高优先级的队列,则停止当前运行的进程并将其移入到原队列末尾,接着让较高优先级的进程运行;

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

看的迷迷糊糊?那我拿去银行办业务的例子,把上面的调度算法串起来,你还不懂,你锤我!

办理业务的客户相当于进程,银行窗口工作人员相当于 CPU。

现在,假设这个银行只有一个窗口(单核 CPU ),那么工作人员一次只能处理一个业务。

银行办业务

那么最简单的处理方式,就是先来的先处理,后面来的就乖乖排队,这就是先来先服务(FCFS)调度算法。但是万一先来的这位老哥是来贷款的,这一谈就好几个小时,一直占用着窗口,这样后面的人只能干等,或许后面的人只是想简单的取个钱,几分钟就能搞定,却因为前面老哥办长业务而要等几个小时,你说气不气人?

先来先服务

有客户抱怨了,那我们就要改进,我们干脆优先给那些几分钟就能搞定的人办理业务,这就是短作业优先(SJF)调度算法。听起来不错,但是依然还是有个极端情况,万一办理短业务的人非常的多,这会导致长业务的人一直得不到服务,万一这个长业务是个大客户,那不就捡了芝麻丢了西瓜

最短作业优先

那就公平起见,现在窗口工作人员规定,每个人我只处理 10 分钟。如果 10 分钟之内处理完,就马上换下一个人。如果没处理完,依然换下一个人,但是客户自己得记住办理到哪个步骤了。这个也就是时间片轮转(RR)调度算法。但是如果时间片设置过短,那么就会造成大量的上下文切换,增大了系统开销。如果时间片过长,相当于退化成 FCFS 算法了。

时间片轮转

既然公平也可能存在问题,那银行就对客户分等级,分为普通客户、VIP 客户、SVIP 客户。只要高优先级的客户一来,就第一时间处理这个客户,这就是最高优先级(HPF)调度算法。但依然也会有极端的问题,万一当天来的全是高级客户,那普通客户不是没有被服务的机会,不把普通客户当人是吗?那我们把优先级改成动态的,如果客户办理业务时间增加,则降低其优先级,如果客户等待时间增加,则升高其优先级。

最高优先级(静态)

那有没有兼顾到公平和效率的方式呢?这里介绍一种算法,考虑的还算充分的,多级反馈队列(MFQ)调度算法,它是时间片轮转算法和优先级算法的综合和发展。它的工作方式:

多级反馈队列

  • 银行设置了多个排队(就绪)队列,每个队列都有不同的优先级,各个队列优先级从高到低,同时每个队列执行时间片的长度也不同,优先级越高的时间片越短
  • 新客户(进程)来了,先进入第一级队列的末尾,按先来先服务原则排队等待被叫号(运行)。如果时间片用完客户的业务还没办理完成,则让客户进入到下一级队列的末尾,以此类推,直至客户业务办理完成。
  • 当第一级队列没人排队时,就会叫号二级队列的客户。如果客户办理业务过程中,有新的客户加入到较高优先级的队列,那么此时办理中的客户需要停止办理,回到原队列的末尾等待再次叫号,因为要把窗口让给刚进入较高优先级队列的客户。

可以发现,对于要办理短业务的客户来说,可以很快的轮到并解决。对于要办理长业务的客户,一下子解决不了,就可以放到下一个队列,虽然等待的时间稍微变长了,但是轮到自己的办理时间也变长了,也可以接受,不会造成极端的现象,可以说是综合上面几种算法的优点。


临界资源
一段时间内仅允许一个进程使用的资源称为临界资源。
(Critical Resource)

如:打印机、共享变量。

互斥原则,mutual exclusion
如果进程Pi在其临界区内执行,那么其他进程都不能在其临界区执行
推进原则,Progress
如果没有进程在其临界区内执行,并且有进程需要进入临界区,那么
只有那些不在剩余区执行的进程可以参与选择,以便确定谁能下次进
入临界区,而且这种选择不能无限推迟。
有限等待,bounded waiting
从一个进程作出进入临界区的请求,直到这个请求允许为止,其他进
程允许进入临界区的次数具有上限
v为负数说明有进程在等着,所以执行v可以唤醒进程
现有一个p,才能执行一个v

产生死锁的原因和必要条件

  死锁是指多个进程在运行过程中因为争夺资源而造成的一种僵局,当进程处于这种僵局状态时,若无外力作用,他们都将无法再向前推进。

  5.1 产生死锁的原因

  产生死锁的原因可以归结为如下两点

  ① 竞争资源,当系统中供多个进程共享的资源,如打印机、公用队列等,其数目不足以满足诸进程的需要时,会引起诸进程对资源的竞争而产生死锁。

  ② 进程间推进顺序非法,进程在运行过程中,请求和释放资源的顺序不当,也会导致进程死锁。

  竞争资源引起的死锁,竞争的资源可以分为可剥夺性资源(某进程在获得这类资源后,该资源可以再被其他进程或系统剥夺,如处理机和内存资源)和不可剥夺性资源(当某进程获得这类资源后,再不能强行收回,只能在进程用完后自行释放,如磁带机、打印机等)。

  ① 竞争非剥夺性资源引起的死锁,在系统中配置的非剥夺性资源,由于它们的数量不能满足诸进程运行的需要,会使进程在运行过程中,因争夺这些资源而陷入僵局。如系统中只有一台打印机和一台磁带机供进程p1和p2使用,假定p1占用了打印机,p2占用了磁带机,若p1继续要求磁带机,则p1会阻塞,p2又要求打印机,则p2也将阻塞,两个进程都在等待对方释放出自己所需的资源,但它们又不能释放出自己已占有的资源,以致进入死锁状态。

  ② 竞争临时性资源而引起的死锁,对于打印机这类可以重复使用的资源称为永久性资源,而临时性资源则是由一个进程产生,被另一个进程使用短暂时间后便无用的资源,其也可能会引起死锁。若s1、s2、s3为临时性资源,进程p1产生s1,又要求从p3接受s3,进程p3产生s3,又要求从p2接受s2,p2产生s2,又要求从s1接受消息,如果消息通信的顺序如下

  p1: request(s3);release(s1);

  p2: request(s1);release(s2);

  p3: request(s2);release(s3);

  则可能会发生死锁。

  进程推进顺序不当引起的死锁,由于进程在运行中具有异步性特征,当进程推进顺序不合法时就可能引起死锁。如进程p1运行request(r2),此时r2被p2占有,所以p1将阻塞,当p2运行request(r1),此时r1被p1占有,所以p2将阻塞,此时,就发生了死锁。

  5.2 产生死锁的必要条件

  死锁发生必须具有下列四个必要条件

  ① 互斥条件,指进程对所分配到的资源进行排他性使用,即在一段时间内某资源只由一个进程占用,如果此时还有其他进程请求该资源,则请求者只能等待,直至占有该资源的进程用完释放。

  ② 请求和保持条件,指进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源又已经被其他进程占用,此时请求进程阻塞,但又对自己已获得的其他资源保持不放。

  ③ 不剥夺条件,指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。

  ④ 环路等待条件,指在发生死锁时,必然存在一个进程-资源的环形链,即进程集合{p0,p1,p2,...,pn}中的p0正在等待p1占用的资源,p1在等待p2占用的资源,...,pn正在等待p0占用的资源。

  5.3 处理死锁的基本方法

  为保证系统中诸进程的正常运行,应事先采取必要的措施,来预防发生死锁,在系统中已经出现死锁后,则应该及时检测到死锁的发生,并采取释放措施来解除死锁。处理死锁的方法可归结如下四种。

  ① 预防死锁,该方法通过设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或几个,来预防死锁。其是一种比较容易实现的方法,已经被广泛使用,但由于所施加的限制条件往往太严格,因而可能会导致系统资源利用率和系统吞吐量降低。

  ② 避免死锁,该方法也是事先预防的策略,但它并不需要事先采取各种限制措施去破坏产生死锁的四个必要条件,而是在资源的动态分配过程中,用某种方法去防止系统进入不安全状态,从而避免发生死锁。

  ③ 检测死锁,该方法并不需要事先采取任何限制措施,也不必要检查系统是否已经进入不安全区,而是允许系统在运行过程中发生死锁,但可通过系统所设置的检测机构,及时检测死锁的发生,并精确地确定与死锁有关的进程和资源,然后,采取适当措施,从系统中将已发生的死锁清除掉。

  ④ 解除死锁,这是与检测死锁相配套的一种措施,当检测到系统中已经发生死锁时,需将今晨从死锁状态中解脱出来,常用的实施办法是撤销或挂起一些进程,以便回收一些资源,再将这些资源分配给已处于阻塞状态的进程,使之转为就绪状态,以继续运行。

六、预防死锁的方法

  预防死锁和避免死锁这两种方法都是通过施加某些限制条件,来预防发生死锁,两者主要差别在于:为预防死锁所施加的限制条件较严格,往往会影响进程的并发执行,而未避免死锁所施加的限制条件就相对宽松,这给进程的运行提供了较宽松的环境,有利于进程的并发执行。

  6.1 预防死锁

  预防死锁是使四个必要条件中的第2、3、4个条件之一不能成立,来避免发生死锁,对于条件1,则应该加以保证。

  ① 摒弃请求和保持条件,系统规定所有进程在开始运行之前,都必须一次性地申请其在整个运行过程中所需的全部资源,若系统有足够的资源,则可分配给进程,这样,进程在运行期间都不会提出资源请求,从而摒弃了请求条件,若系统有一种资源无法满足某进程的要求时,即使其他资源都空闲,也不分配给该进程,而让其等待,在等待中不会占用任何资源,也摒弃了请求和保持条件。其优点在于简单、易于实现和安全,缺点为资源严重浪费,降低了资源利用率,进程不能保证及时运行。

  ② 摒弃不剥夺条件,进程是逐个提出对资源的要求的,当一个已经保持了某些资源的进程,再提出新的资源请求而不能立即得到满足时,必须释放它已经保持了的所有资源,带以后需要时再重新申请,这意味着某一进程已经占用的资源,在运行过程中会被暂时的释放掉,也可认为被剥夺了,从而摒弃了不可剥夺条件。其缺点在于比较复杂并且付出很大的代价,可能反复申请和释放资源,致使进程的执行无限地推迟,延长了进程周转时间,增加系统开销,降低了吞吐量。

  ③ 摒弃环路等待条件,系统将所有资源按类型进行线性排队,并赋予不同的序号,所有进程对资源的请求必须严格按照资源序号递增的次序提出,这样,在所形成的资源分配图中,不可能再出现环路,因而摒弃了环路等待条件。其缺点在于由于为系统中各类资源所分配的序号相对稳定,这就限制了新类型设备的增加,并且作业(进程)使用各类资源的顺序与系统规定的顺序不同,造成对资源的浪费,同时也会限制用户简单自主的编程。

  6.2 系统安全状态

  只要系统能够始终处于安全状态,便可避免发生死锁。在避免死锁的方法中,运行进程动态地申请资源,但系统在进行资源分配之前,应先计算此次资源分配的安全性,若此次分配不会导致系统进入不安全状态,则将资源分配给进程,否则,令进程等待。  

  安全状态指的是系统能够按某种进程顺序(p1,p2,p3,...,pn),来为每个进程pi分配其所需资源,直至满足每个进程对资源的最大需求,使每个进程都可顺利地完成。如果系统无法找到这样一个安全序列,则称系统处于不安全状态。虽然并非所有的不安全状态都必然会转为死锁状态,但当系统进入不安全状态后,便有可能进入死锁状态,只要系统处于安全状态,系统便可避免进入死锁状态,因此,避免死锁的实质在于,系统在进行资源分配时,如何使系统不进入不安全状态。

  6.3 利用银行家算法避免死锁

死锁避免-银行家算法_哔哩哔哩_bilibili

【操作系统期末复习】6分钟让你明白银行家算法_哔哩哔哩_bilibili

银行家算法是为了避免死锁,死锁产生的根本原因就是多个进程对共享资源的竞争导致互相等待无法向前推进,其中有一点是进程推进顺序非法,所以必须要考虑初始分配情况,否则算法无意义。而实际上,如果资源足够多,各个进程不会因为等待对方放弃而无法向前推动,就不会发生死锁,也就不用考虑避免。

  1. 银行家算法的数据结构  

  ① 可利用资源向量Available,这是一个含有m个元素的数组,其中每一个元素代表一类可利用的资源数目,初始值为系统所配置的改类全部可用资源的数目,Available[j] = k,表示系统中Rj资源又k个。

  ② 最大需求矩阵Max,这是一个n * m 的矩阵,定义了系统中n个进程中的每一个进程对m类资源的最大需求,Max[i,j] = k,表示进程i需要Rj类资源的最大数目为k。

  ③ 分配矩阵Allocation,是一个n * m 的矩阵,定义了系统中每一类资源当前已经分配每一进程的资源数,Allocation[i,j] = k,表示进程i已经分得Rj类资源的数目为k。

  ④ 需求矩阵Need,是一个n * m的矩阵,表示每一个进程尚需的各类资源数。Need[i,j] = k,表示进程i还需要Rj类资源k个。

  Need[i, j] = Max[i, j] - Allocation[i, j]

  2. 银行家算法

  设Requesti进程Pi的请求向量,若Requesti[j] = K,表示进程Pi需要Rj类资源K个,当Pi发出资源请求后,系统按照下述步骤进行检查

  ① 如果Requesti[j] <= Need[i, j],转向②,否则,因为它所需要的资源数已超过它所宣布的最大值,出错。

  ② 如果Requesti[j] <= Available[j],转向③,否则,表示尚无足够资源,Pi需要等待。

  ③ 系统尝试把资源分配给进程Pi,并修改数据结构中的数值:

  Available[j] = Available[j] - Requesti[j];

  Allocation[i, j] = Allocation[i, j] + Requesti[j];

  Need[i, j] = Need[i, j] - Requesti[j];

  ④ 系统执行安全性算法,检查此次资源分配后系统是否处于安全状态,若安全,才正式将资源分配给进程Pi,以完成本次分配,否则,将本次的尝试分配作废,恢复原来的资源分配状态,让进程Pi等待。

  3. 安全性算法

  系统所执行的安全性算法可描述如下

  ① 设置工作向量Work,表示系统可提供给进程继续运行所需的各类资源数目,它含有m个元素,在执行安全算法开始时,Work = Available;

  ② 设置Finish,表示系统是否有足够的资源分配给进程,使之运行完成,开始时Finish[i] = false,当有足够资源分配给进程时,再令Finish[i] = true;

  ③ 从进程集合中找到一个能满足下述条件的的进程:Finish[i] = fasle;Need[i, j] <= Work[j],若找到,进入④,否则,执行⑤

  ④ 当进程Pi获得资源后,可顺利执行,直至完成,并释放出分配给它的资源,执行 Work[j] = Work[j] + Allocation[i, j]; Finish[i] = true;进入步骤③

  ⑤ 如果所有的进程的Finish[i] = true都满足,则表示系统处于安全状态,否则,系统处于不安全状态。

在银行家算法的安全性检测过程中,确实存在可能选择错误进程的情况,从而导致系统进入不安全状态。这是因为在选择满足条件 Finish(i)==falseFinish(i) == falseFinish(i)==false 且 Needi≤WorkNeed_i \leq WorkNeedi​≤Work 的进程时,不同的选择可能会导致不同的系统状态。

具体来说,假设在某次搜索中有两个进程 A 和 B 都满足条件 Finish(A)==falseFinish(A) == falseFinish(A)==false 且 NeedA≤WorkNeed_A \leq WorkNeedA​≤Work,以及 Finish(B)==falseFinish(B) == falseFinish(B)==false 且 NeedB≤WorkNeed_B \leq WorkNeedB​≤Work。

  1. 选择进程 A:
    • 尝试给进程 A 分配资源,使其运行完成,释放资源并更新系统状态。
    • 更新后的系统状态可能导致没有其他进程能够继续运行,即进入不安全状态。
  2. 选择进程 B:
    • 尝试给进程 B 分配资源,使其运行完成,释放资源并更新系统状态。
    • 更新后的系统状态可能允许更多的进程继续运行,最终所有进程都能够完成,即进入安全状态。

这表明,银行家算法中的安全性检测确实可能存在选择错误进程的情况,这种选择可能会导致系统进入不安全状态。为了避免这种情况,安全性检测过程需要尝试所有可能的选择路径,确保找到一个可以使系统保持安全状态的路径。如果任何选择路径都不能保证系统的安全状态,那么系统当前的状态就是不安全的。

总结来说,银行家算法的安全性检测过程是一个分支搜索过程,必须考虑所有可能的选择路径,才能确保系统的安全性。因此,在某次搜索中选择了错误的进程可能会导致系统进入不安全状态,这种情况是可能存在的。

在银行家算法中,如果在尝试分配资源并检查系统状态时发现系统进入了不安全状态,算法会采取回滚操作来确保系统安全。具体步骤如下:

  1. 回滚资源分配: 如果分配资源后检测到系统进入了不安全状态,算法会撤销刚才的资源分配。也就是说,系统恢复到资源分配前的状态,包括进程的资源分配情况和可用资源数量。

  2. 尝试其他进程: 撤销分配后,算法会继续尝试其他满足条件的进程。即回到寻找下一个满足 Finish(i)==falseFinish(i) == falseFinish(i)==false 且 Needi≤WorkNeed_i \leq WorkNeedi​≤Work 的进程,并重新进行资源分配尝试和安全性检查。

  3. 重新检测安全状态: 对每一个新的资源分配尝试,再次进行安全性检测。如果找不到任何可以安全分配资源的进程,系统会认为当前资源分配请求无法满足,需要进程等待更多资源释放。

具体步骤如下:

  1. 保存当前状态: 保存当前资源分配情况、进程状态和可用资源数量。
  2. 分配资源并更新状态: 尝试给某个满足条件的进程分配资源,更新系统状态。
  3. 安全性检测: 执行安全性检测算法。如果系统状态不安全:
    • 回滚操作: 恢复之前保存的状态,即撤销分配。
    • 尝试下一个进程: 尝试给下一个满足条件的进程分配资源,重复以上步骤。
  4. 循环完成: 如果找到一个分配方案使得系统保持安全状态,则完成资源分配。如果没有找到任何安全的分配方案,则拒绝当前资源分配请求,进程进入等待状态。

通过以上步骤,银行家算法确保系统不会进入不安全状态,保障系统的稳定性和安全性。

七、死锁的检测与解除

视频:    3分钟学会资源分配图化简。_哔哩哔哩_bilibili

  7.1 死锁的检测

  当系统为进程分配资源时,若未采取任何限制性措施,则系统必须提供检测和解除死锁的手段,系统必须要做到

  ① 保存有关资源的请求和分配信息

  ② 提供一种算法,以利用这些信息来检测系统是否已进入死锁状态。

  资源分配图用圆圈代表一个进程,用方框代表一类资源,由于一种类型的资源可以有多个,用方框中的一个点代表一类资源中的一个资源。

  说明:p1进程获得了两个r1资源,请求一个r2资源,p2进程获得一个r1资源和一个r2资源,并且又申请了一个r1资源。

  利用资源分配图可以检测系统是否处于死锁状态。

  ① 在资源分配图中,找到一个既不阻塞又不独立的进程结点pi,在顺利情况下,可获得所需资源而继续运行,直至运行完毕,再释放其所占用的全部资源。

  ② p1释放资源后,p2可获得资源继续运行,执行p2完成后又释放其所占用的资源。

  ③ 在进行一系列化简后,若能消去图中所有的边,使得所有的进程结点都成为孤立结点,则称该图是可以完全简化的,若不能通过任何过程使改图完全简化,则改图是不可完全简化的。

  所有的简化顺序,都将得到相同的不可简化图,S为死锁状态的充分条件是:当且仅当S状态的资源分配图是不可完全简化的,该充分条件称为死锁定理。

  7.2 死锁的解除

  当发现有死锁情况时,需要立即把他们从死锁状态中解脱出来,常用两种解除死锁的方法

  ① 剥夺资源,从其他进程剥夺足够数量的资源给死锁进程,以解除死锁状态。

  ② 撤销进程,使全部死锁进程都夭折掉,或者按照某种顺序逐个撤销进程,直至有足够的资源可用,使死锁状态消除为止。

  • 18
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值