操作系统(四)进程和线程的调度算法、同步互斥、通信、死锁等

前言

本文主要涉及操作系统的简介、硬件结构、内存管理、进程管理、文件系统、设备管理等内容,可以作为学习操作系统的辅助文本记录。撰写本文的目的主要是针对操作系统整体做一个相对完整的梳理,以便后续回顾之用。
本文是第四篇,讲述操作系统的进程和线程的调度算法、同步互斥、通信、死锁等
第一篇:操作系统(一)基础知识及操作系统启动
第二篇:操作系统(二)内存管理的基础知识
第三篇:操作系统(三)进程和线程的基础知识

处理机调度

  • 从就绪队列中挑选下一个占用CPU运行的进程
  • 从多个可用的CPU中挑选就绪进程可以使用的CPU资源

调度时机

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

  • 从就绪态 -> 运行态:当进程被创建时,会进入到就绪队列,操作系统会从就绪队列选择一个进程运行;

  • 从运行态 -> 阻塞态:当进程发生 I/O 事件而阻塞时,操作系统必须选择另外一个进程运行;

  • 从运行态 -> 结束态:当进程退出结束后,操作系统得从就绪队列选择另外一个进程运行;

  • 非抢占系统

    • 当前进程主动放弃CPU时
  • 可抢占系统

    • 中断请求被服务例程响应完成时

    • 当前进程被抢占

      进程的时间片用完

      进程从等待切换到就绪

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

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

调度原则:

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

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

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

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

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

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

系统利用效率:

  • CPU 利用率:调度程序应确保 CPU 是始终匆忙的状态,这可提高 CPU 的利用率;
  • 系统吞吐量:吞吐量表示的是单位时间内 CPU 完成进程的数量,长作业的进程会占用较长的 CPU 资源,因此会降低吞吐量,相反,短作业的进程会提升系统吞吐量;

用户角度:

  • 周转时间:周转时间是进程运行+阻塞时间+等待时间的总和,一个进程的周转时间越小越好;
  • 等待时间:这个等待时间不是阻塞状态的时间,而是进程处于就绪队列的时间,等待的时间越长,用户越不满意;
  • 响应时间:用户提交请求到系统第一次产生响应所花费的时间,在交互式系统中,响应时间是衡量调度算法好坏的主要标准。

调度策略

  1. 响应时间目标:
  • 减少响应时间

    及时处理用户的输入请求,尽快将输出反馈给用户

  • 减少平均响应时间的波动

    在交互系统中,可预测性比高差异低平均更重要

  • 低延迟调度改善了用户的交互体验

  1. 吞吐量目标:
  • 增加吞吐量

    减少开销(操作系统开销,上下文切换)

    系统资源的高效利用(CPU,I/O设备)

  • 减少等待时间

  • 操作系统需要保证吞吐量不受用户交互的影响

  1. 公平性目标

调度算法

1. 先来先服务(FCFS)调度算法(First Come First serve)
  • 依据进程进入就绪状态的先后顺序排列

    进程进入等待或结束状态时,就绪队列中的下一个进程占用CPU

  • 优点:

    • 简单
  • 缺点:

    • 进程的平均等待时间波动较大,短进程可能排在长进程后面
  • I/O资源和CPU资源的利用率较低

2. 最短作业优先调度算法(Shortest Job First)
  • 选择就绪队列中执行时间最短进程占用CPU进入运行状态,这有助于提高系统的吞吐量

    就绪队列按照预期的执行时间进行排序

  • 优点:

    • 该算法具有最优平均周转时间。(短的先执行,平均下来大家等待的时间都很低)
  • 缺点:

    • 可能导致饥饿

      连续的短进程流会使长进程无法获得CPU资源

    • 需要预知未来

      如何预估下一个CPU计算的持续时间

      简单的办法:询问用户

      • 用户欺骗从而杀死相应进程
      • 用户不知道怎么办

      用历史的执行时间来预估未来的执行时间

3.最高响应比优先算法(Highest Response Ratio Next, HRRN)

每次进行进程调度时,先计算「响应比优先级」,然后把「响应比优先级」最高的进程投入运行,「响应比优先级」的计算公式:
优先权 = 等待时间 + 要求服务时间 要求服务时间 优先权 = \frac{等待时间+要求服务时间}{要求服务时间} 优先权=要求服务时间等待时间+要求服务时间

  • 这是在短进程优先算法上的基础上的改进,避免饥饿;
  • 不可抢占
  • 关注了进程的等待时间
  • 防止某一进程被无限期推迟
  • note: 但由于进程要求服务的时间是不可获知的,因此该调度算法是一个理想型的调度算法。
4. 时间片轮转算法(Round Robin, RR)

时间片:分配CPU资源的基本时间单位

  • 时间片结束时,按照FCFS算法切换到下一个就绪进程

  • RR算法开销

    • 额外的上下文切换
  • 时间片过大

    • 等待时间过长;
    • 极限情况下退化为FCFS
  • 时间片太小

    • 反应迅速,但产生大量上下文切换
    • 大量上下文切换开销影响到系统吞吐量
  • 时间片长度选择

    • 经验规则:维持上下文切换开销处于1%以内。
    • 20~50ms
5. 多级队列调度算法(MultiLevel queue, MQ)
  • 就绪队列被划分为多个独立的子队列
  • 每个队列拥有自己的调度策略
  • 队列间的调度
    • 固定优先级
    • 时间片流转
6. 最高优先级调度算法(Highest Priorty First, HPF)

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

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

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

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

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

7. 多级反馈队列调度算法(MultiLevel Feedback Queue)
  • 进程可以在不同队列之间移动的多级队列算法
    • 时间片的大小可以随优先级级别的增加而增加
    • 如果进程在当前的时间片没有完成,则降到下一个优先级
  • 特征
    • CPU密集型进程的优先级下降很快(时间片会分的很大,从而切换的开销相对变小)
    • I/O密集型进程停留在高优先级(每一次的执行时间很短,时间片用不完)
8. 公平共享调度算法(FSS, Fair Share Scheduling)
  • FSS控制用户对系统资源的访问

    • 一些用户组比其他用户组更加重要

    • 保证不重要的组无法垄断资源

      未使用的资源按比例分配

      没有达到资源使用率目标的组获得更高的优先级

调度算法总结
  1. 先来先服务----不公平,平均等待时间较差
  2. 短进程优先算法—不公平,但平均周转时间最小;需要精确预测计算时间;可能导致饥饿
  3. 最高响应比优先算法----基于短进程优先改进;不可抢占
  4. 时间片轮转----公平,平均等待时间较差,但交互性好
  5. 最高优先级—保证优先级高的先运行,可能导致低优先级饥饿
  6. 多级反馈队列----多种算法的集成
  7. 公平共享—公平是第一要素

实时调度

实时操作系统:正确性依赖时间和功能两方面的操作系统

实时操作系统的性能指标:时间约束的及时性(deadlines)(在规定时间完成任务);速度和平均性能相对不重要;

实时操作系统的特性:时间约束的可预测性,(可以知道在什么情况下,时间约束是可以达到的)。

实时任务: 一次计算、一次文件读取、一次信息传递等等

周期实时任务: 一系列相似的任务

硬实时(硬时限,Hard deadline):

  • 错过任务时限会导致灾难性或者非常严重的后果
  • 必须验证,在最坏情况下能够满足时限

软实时(软时限,Soft deadline):

  • 通常能够满足任务时限,如有时不满足,则降低要求;
  • 尽力保证满足任务时限

**可调度性:**表示一个实时操作系统能够满足任务时限要求

  • 需要确定实时任务的执行顺序
  • 静态优先级调度
  • 动态优先级调度
1. 速度单调调度算法(RM, Rate Monotonic)
  • 通过周期安排优先级
  • 周期越短优先级越高
  • 执行周期最短的任务
2. 最早截止时间优先算法(EDF,Earliest Deadline)
  • 截止时间越早优先级越高
  • 执行截止时间最早的任务

多处理器调度

多处理机调度的特征:

  • 多个处理机组成一个多处理机系统
  • 处理机之间可负载共享

对称多处理器(SMP,Symmetric multiprocessing)调度

  • 每个处理器运行自己的调度程序
  • 调度程序对共享资源的访问需要进行同步

我们把一个进程到底放到哪一个处理器上运行?

  • 静态进程分配

    • 进程从开始到结束都被分配到一个固定的处理机上执行
    • 每个处理机有自己的就绪队列
    • 调度开销小
    • 各处理机可能忙闲不均
  • 动态进程分配

    • 进程在执行时可分配到任意空闲处理机执行
    • 所有处理机共享一个公共的就绪队列
    • 调度开销大
    • 各处理机的负载都是均衡的

优先级反置(Priority Inversion)

操作系统中出现高优先级进程长时间等待低优先级进程所占用资源的现象
基于优先级的可抢占调度算法会存在优先级反置

处理:

  • 占用资源的低优先级进程继承申请资源的高优先级进程的优先级
  • 优先级天花板协议(priority ceiling protocol)
    • 占用资源进程的优先级和所有可能申请该资源的进程的最高优先级相同

同步互斥

  • 独立的线程:

    • 不和其他线程共享资源或状态
    • 确定性:一输入状态决定结果
    • 可重现:一 能够重现起始条件,I/O
    • 调度顺序不重要
  • 合作线程:

    • 在多个线程中共享状态

    • 不确定性

    • 不可重现

    • 不确定性和不可重现意味着 bug 可能是间歇性发生的

  • 进程间合作的原因:

    • 进程 / 线程,计算机 / 设备需要合作

    • 优点 1:共享资源

      一台电脑,多个用户
      一个银行存款余额,多台 ATM 机
      嵌入式系统(机器人控制:手臂和手的协调)

    • 优点 2:加速
      I/0 操作和计算可以重叠
      多处理器 一 将程序分成多个部分井行执行

    • 优点 3:模块化
      将大程序分解成小程序
      以编译为例,gcc 会调用 cpp, cc1, cc2. as, ld,使系统易于扩展

线程之间是可以共享进程的资源,比如代码段、堆空间、数据段、打开的文件等资源,但每个线程都有自己独立的栈空间。线程是调度的基本单位,进程则是资源分配的基本单位。

竞争条件(race condition,当多线程相互竞争操作共享变量时,由于运气不好,即在执行过程中发生了上下文切换,我们得到了错误的结果,事实上,每次运行都可能得到不同的结果,因此输出的结果存在不确定性(indeterminate

互斥(mutual exclusion): 一个进程占用资源,其他进程不能使用

死锁(deadlock): 多个进程各占部分资源,形成循环等待

饥饿(starvation): 其他进程可能轮流占用资源,一个进程一直得不到资源

原子操作(Atomic Operation): 指一次不存在任何中断或失败的操作

原子操作指令:

  • 现代CPU体系结构都提供一些特殊的原子操作指令

  • 测试和置位(Test-and-Set)指令

    • 从内存单元中读取值
    • 测试该值是否为1(返回真或假)
    • 内存单元值设置为1
  • 交换指令(exchange)

    交换内存中的两个值

同步

所谓同步,就是并发进程/线程在一些关键点上可能需要互相等待与互通消息,这种相互制约的等待与互通信息称为进程/线程同步

临界区(critical section): 进程中访问临界资源的一段需要互斥执行的代码,任何时刻只允许一个进程在这其中执行。

进入区(entry section): 检查可否进入临界区的一段代码,如可进入,设置相应”正在访问临界区“标志

退出区(exit section): 清除"正在访问临界区"标志

剩余区(remainder section): 代码中的其余部分

同步的方法
临界区的访问规则
  • 空闲则入:没有进程在临界区时,任何进程可以进入
  • 忙则等待:有进程在临界区时,其他进程均不能进入临界区
  • 有限等待: 等待进入临界区的进程不能无限期等待
  • 让权等待(可选):不能进入临界区的进程,应释放CPU(如转换到阻塞状态)
临界区的实现方法
  • 禁用中断
  • 软件方法
  • 更高级的抽象方法

1. 禁用硬件中断

  • 没有中断、没有上下文切换,因此没有并发

    硬件将中断处理延迟到中断被启用之后

    现代计算机体系结构都提供指令来实现禁用中断

  • 禁用中断后,进程无法被停止

2. 基于软件的同步解决方法

Peterson算法

Dekkers算法

  • 复杂:需要两个进程间的共享数据项
  • 需要忙等待:浪费CPU时间

3. 更高级的抽象方法

锁(lock):是一个抽象的数据结构,二进制变量(锁/解锁)

锁单独介绍。

常用的三种同步实现方法

  • 禁用中断(仅限于单处理器)
  • 软件方法(复杂)
  • 原子操作指令(单处理器或多处理器均可)

使用加锁操作和解锁操作可以解决并发线程/进程的互斥问题。

任何想进入临界区的线程,必须先执行加锁操作。若加锁操作顺利通过,则线程可进入临界区;在完成对临界资源的访问后再执行解锁操作,以释放该临界资源。

  1. 自旋锁(Spin lock)

    使用TS指令实现自旋锁,线程在等待时需要消耗CPU时间;

  2. 无忙等待锁

    在获取不到锁的时候,不用自旋;而是将当前进程/线程放入到锁的等待队列,然后执行调度程序,把CPU让给其他进程/线程执行。

信号量(semaphore)和管程

信号量

多线程并发导致资源竞争

信号量是操作系统提供的一种协调共享资源访问的方法。

  • 信号量是一个抽象数据类型

    • 由一个整型(sem)变量和两个原子操作组成

    • P()(Prolaag,荷兰语,尝试减少)

      sem减1

      如sem < 0. 进入等待否则继续

    • V() (Verhoog, 荷兰语,增加)

      sem加1

      sem$\leq$0,唤醒一个等待进程

  • 信号量是被保护的整数变量

    初始化完成后,只能通过P()和V()操作修改

    由操作系统保证,PV操作是原子操作

  • 信号量的分类

    • 二进制信号量:资源数目为0或1;
    • 资源信号量:资源数目为任何非负值
  • 用信号量实现临界区的互斥访问

    每类资源设置一个信号量,初值为1;

    一个线程进入,信号量值为0,可以执行;

    第二个线程进入,信号量为-1,等待。

    必须 成对使用PV操作

  • 用信号量实现条件同步

    每个条件同步设置一个信号量,初值为0

    有个线程想要申请,信号量变为-1,等待;

    等到另一个线程执行了对应信号量的V操作,变为0;

    等待的线程就可以继续执行。

  • 不能避免 死锁

管程

管程是一种用于并发编程的概念,它提供了一种结构化的方法来管理共享资源和协调并发执行的程序。管程通常由一个包含共享数据和用于操作这些数据的过程(也称为条件变量)组成,它可以帮助程序员更容易地编写并发程序,避免出现竞争条件(race condition)、死锁等问题。

作用:

  1. 管程可以提供对共享资源的访问控制,避免多个线程同时对共享资源进行修改而导致的数据不一致性问题。
  2. 管程可以通过条件变量来协调多个线程的执行顺序,使得线程能够按照特定的逻辑顺序进行交互和同步。

进程的通信(IPC, Inter-Process Communication)

每个进程的用户地址空间都是独立的,一般而言是不能互相访问的,但内核空间是每个进程都共享的,所以进程之间要通信必须通过内核。

进程通信是进程进行通信和同步的机制

  • IPC提供两个基本操作:

    发送操作:send(message)

    接收操作:receive(message)

  • 进程通信流程

    • 在通信进程间建立通信链路
    • 通过send/receive交换信息
  • 进程链路特征

    物理:共享内存、硬件总线等

    逻辑:逻辑属性等

通信方式

  1. 直接通信

通信链路的属性:

  • 自动建立链路
  • 一条链路恰好对应一对通信进程
  • 每对进程之间恰好只有一个链接存在
  • 链接可以是单向的,但通常是双向的

进程必须正确的命名对方

  • send(P, message) - 发送信息到进程P
  • receive(Q, message) - 从进程Q接收信息
  1. 间接通信
  • 通过操作系统维护的消息队列实现进程间的消息接收和发送

    • 每个消息队列都有唯一的标识
    • 只有共享了相同消息队列的进程才能够通信
  • 通信链路的属性

    • 只有共享了相同消息队列的进程,才建立连接;
    • 连接可以是单向或双向
    • 消息队列可以与多个进程相关联
  • 通信流程

    • 创建新的消息队列
    • 通过消息队列发送和接收消息
    • 销毁消息队列
  • 基本通信操作:

    send(A, message) - 发送消息到队列A

    receive(A, message) - 从队列A接收消息

  • 进程通信可以划分为阻塞(同步)非阻塞(异步)

    阻塞通信:

    ​ 阻塞发送:发送者在发送消息后进入等待,直到接收者成功收到

    ​ 阻塞接收:接收方在请求接收消息后进入等待,直到成功接收到消息

    非阻塞通信:

    ​ 非阻塞发送:发送者在消息发送后,可以立即进行其他操作;

    ​ 非阻塞接收:接收者在请求接收消息后,如果没有消息发送过来,接收者接收不到任何消息

通信链路缓冲

  • 进程发送的消息在链路上可能有三种缓冲方式:
    • 0容量:发送方必须等待接收方
    • 有限容量:通信链路缓冲队列满时,发送方必须等待
    • 无限容量:发送方不需要等待,任何时候都可以发

信号(signal)

进程通信的具体方法

信号:

进程间的软中断通知和处理机制

​ 如:SIGKILL,SIGSTOP, SIGCONT等

  • 信号的接收处理

    • 捕获(Catch):执行进程指定的信号处理函数;

    • 忽略(Ignore):执行操作系统指定的缺省处理

      如:进程终止,进程挂起等

    • 屏蔽(Mask):禁止进程接收和处理信号

      可能是暂时的(当处理同样类型的信号)

  • 不足:传送信息量小,只有一个信号类型

对于异常情况下的工作模式,就需要用「信号」的方式来通知进程。

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

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

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

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

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

sigkill和sigstop在linux操作系统中有什么区别和联系?

Sigkill和Sigstop都是Linux系统中的信号,但它们的含义和使用场景是不同的。

Sigkill信号的编号为9,表示强制杀死一个进程。当一个进程收到Sigkill信号时,该进程会立即停止运行,没有任何机会来处理该信号。实际上,Sigkill信号是操作系统保留的信号,无法被程序捕获或忽略,因为该信号会立即终止进程。

Sigstop信号的编号为17,表示暂时停止一个进程的执行。当一个进程收到Sigstop信号时,该进程会被挂起,直到接收到Sigcont信号后才能继续运行。与Sigkill信号不同,Sigstop信号可以被程序捕获和处理,以便在进程被挂起前执行一些清理操作。

通常情况下,我们应该尽量避免使用Sigkill信号来结束进程,因为这样可能会导致数据丢失或者其他不可预知的后果。而Sigstop信号则可以用于进程的调试或者控制,比如让某个进程暂停一段时间,以便我们进行相关操作。

管道(pipe)

进程通信的另一具体方法

  • 管道:进程间基于内存文件的通信机制
  • 进程不知道(不关心)管道的另一端

|这个就是Linux命令中的管道表示,他的功能它的功能是将前一个命令(ps auxf)的输出,作为后一个命令(grep mysql)的输入,从这功能描述,可以看出管道传输数据是单向的,如果想相互通信,我们需要创建两个管道才行。

ps auxf | grep mysql

同时,我们得知上面这种管道是没有名字,所以「|」表示的管道称为匿名管道,用完了就销毁。

管道还有另外一个类型是命名管道,也被叫做 FIFO,因为数据是先进先出的传输方式。

我们可以看出,管道这种通信方式效率低,不适合进程间频繁地交换数据

其实,所谓的管道,就是内核里面的一串缓存。从管道的一段写入的数据,实际上是缓存在内核中的,另一端读取,也就是从内核中读取这段数据。另外,管道传输的数据是无格式的流且大小受限。

另外,对于命名管道,它可以在不相关的进程间也能相互通信。因为命令管道,提前创建了一个类型为管道的设备文件,在进程里只要使用这个设备文件,就可以相互通信。

命名管道突破了匿名管道只能在亲缘关系进程间的通信限制,因为使用命名管道的前提,需要在文件系统创建一个类型为 p 的设备文件,那么毫无关系的进程就可以通过这个设备文件进行通信。另外,不管是匿名管道还是命名管道,进程写入的数据都是缓存在内核中,另一个进程读取数据时候自然也是从内核中获取,同时通信数据都遵循先进先出原则,不支持 lseek 之类的文件定位操作。

消息队列

消息队列:由操作系统维护的以字节序列为基本单位的间接通信机制

每个消息(Message)是一个字节序列

相同标识的消息组成按照先进先出顺序组成一个消息队列(MessageQueues)

消息队列是保存在内核中的消息链表,在发送数据时,会分成一个一个独立的数据单元,也就是消息体(数据块),消息体是用户自定义的数据类型,消息的发送方和接收方要约定好消息体的数据类型,所以每个消息体都是固定大小的存储块,不像管道是无格式的字节流数据。如果进程从消息队列中读取了消息体,内核就会把这个消息体删除。

消息队列生命周期随内核,如果没有释放消息队列或者没有关闭操作系统,消息队列会一直存在,而前面提到的匿名管道的生命周期,是随进程的创建而建立,随进程的结束而销毁。

消息这种模型,两个进程之间的通信就像平时发邮件一样,你来一封,我回一封,可以频繁沟通了。

但邮件的通信方式存在不足的地方有两点,一是通信不及时,二是附件也有大小限制,这同样也是消息队列通信不足的点。

消息队列不适合比较大数据的传输,因为在内核中每个消息体都有一个最大长度的限制,同时所有队列所包含的全部消息体的总长度也是有上限。在 Linux 内核中,会有两个宏定义 MSGMAXMSGMNB,它们以字节为单位,分别定义了一条消息的最大长度和一个队列的最大长度。

消息队列通信过程中,存在用户态与内核态之间的数据拷贝开销,因为进程写入数据到内核中的消息队列时,会发生从用户态拷贝数据到内核态的过程,同理另一进程读取内核中的消息数据时,会发生从内核态拷贝数据到用户态的过程。

共享内存

共享内存是把同一个物理内存区域同时映射到多个进程的内存地址空间的通信机制。这样这个进程写入的东西,另外一个进程马上就能看到了,都不需要拷贝来拷贝去,传来传去,大大提高了进程间通信的速度。

进程:

​ 每个进程都有私有内存地址空间

​ 每个进程的内存地址空间需要明确设置共享内存段

线程:同一个进程中的线程总是共享相同的地址内存空间

优点:快速、方便地共享数据

不足:必须使用额外的同步机制来协调数据访问,以避免还没有写完就有别的进程来读

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 即可;

根据创建 socket 类型的不同,通信的方式也就不同:

  • 实现 TCP 字节流通信: socket 类型是 AF_INET 和 SOCK_STREAM;
  • 实现 UDP 数据报通信:socket 类型是 AF_INET 和 SOCK_DGRAM;
  • 实现本地进程间通信: 「本地字节流 socket 」类型是 AF_LOCAL 和 SOCK_STREAM,「本地数据报 socket 」类型是 AF_LOCAL 和 SOCK_DGRAM。另外,AF_UNIX 和 AF_LOCAL 是等价的,所以 AF_UNIX 也属于本地 socket;

避免死锁

死锁

由于竞争资源或者通信关系,两个或更多线程在执行中出现,永远相互等待只能由其他进程引发的事件

当两个线程为了保护两个不同的共享资源而使用了两个互斥锁,那么这两个互斥锁应用不当的时候,可能会造成两个线程都在等待对方释放锁,在没有外力的作用下,这些线程会一直相互等待,就没办法继续运行,这种情况就是发生了死锁

  • 资源分类

    • 可重用资源(Reusable Resource)

      资源不能被删除且在任何时刻都只能有一个进程使用

      进程释放资源后,其他进程可以重用

      示例:I/O设备,数据库、处理器等

      可能出现死锁:占用部分资源而等待另一资源

    • 消耗资源(Consumable resource)

      资源创建和销毁

      消耗资源示例:中断、信号等

      可能出现死锁,进程间相互等待接收对方的消息

  • 出现死锁的必要条件

    • 互斥:任何时刻只能有一个进程使用一个资源实例
    • 持有并等待:进程保持至少一个资源,并正在等待获取其他进程持有的资源
    • 非抢占:资源只能在进程使用后自愿释放
    • 循环等待

死锁的处理方法

  • 死锁预防(Deadlock Prevention)

    破坏四个必要条件之一,确保系统永远不会进入死锁状态

  • 死锁避免(Deadlock Avoidance)

    在使用之前判断,只允许不会出现死锁的进程请求资源

  • 死锁检测和恢复(Deadlock Detection & Recovery)

    在检测到运行系统进入到死锁状态后,进行恢复

  • 由应用进程处理死锁

    通常操作系统忽略死锁,包括UNIX等

1. 死锁预防: 限制申请方式

采用某种策略,限制并发进程对资源的请求,使系统在任何时刻都不满足死锁的必要条件

  • 互斥:将互斥的共享资源封装成可以同时访问

  • 持有并等待:进程请求资源时,要求他不持有任何其他资源(必须一次性申请自己需要所有的资源,但资源利用率低)

  • 非抢占:如进程请求不能立即分配的资源,就释放自己已经占有的资源

  • 循环等待:对资源排序,要求进程按顺序请求资源使用资源有序分配法,来破环环路等待条件

    线程 A 和 线程 B 获取资源的顺序要一样,当线程 A 是先尝试获取资源 A,然后尝试获取资源 B 的时候,线程 B 同样也是先尝试获取资源 A,然后尝试获取资源 B。也就是说,线程 A 和 线程 B 总是以相同的顺序申请自己想要的资源。那么假如线程A获取了资源A之后,线程B尝试获取资源A的时候就会发现资源A被占用,互斥使其等待,破坏了环路等待条件,死锁不会发生。

2. 死锁避免

利用额外的先验信息,在分配资源时判断是否会出现死锁,只在不会死锁时分配资源

  • 要求进程声明需要资源的最大数目
  • 限定提供与分配的资源数量,确保满足进程的最大需求
  • 动态检查资源分配状态,确保不会出现环形等待
银行家算法(Banker’s Algorithm)
  • 银行家算法是一个避免死锁产生的算法。以银行借贷分配策略为基础,判断并保证系统处于安全状态

    • 客户在第一次申请贷款时,声明所需最大资金量,在满足所有贷款要求并完成项目时,及时归还;

    • 在客户贷款数量不超过银行拥有的最大值时,银行家尽量满足客户要求;

    • 类比

      银行家 – 操作系统

      资金 — 资源

      客户 – 申请资源的线程

  • 数据结构

    n = 线程数量,m = 资源类型数量

    Max(总需求量): n *m的矩阵;

    Available(剩余空闲量): 长度为m的向量

    Allocation(已分配量):n*m矩阵;

    Need(未来需要量): n*m的矩阵

3. 死锁检测和恢复(Deadlock Detection & Recovery)
  • 允许系统进入死锁状态

  • 维护系统的资源分配图

  • 定期调用死锁检测算法来搜索图中是否存在死锁

  • 出现死锁时,用死锁恢复机制进行恢复

  • 死锁检测算法的使用:

    死锁多久可能会发生?多少进程需要回滚

  • 进程终止:终止所有的死锁进程,一次终止一个进程直到死锁消除

  • 终止顺序:

    进程的优先级;进程已经运行的时间和还需运行的时间;进程已占用的资源;进程完成需要的资源

小结

本节主要操作系统的进程和线程的调度算法、同步互斥、通信、死锁等。
本主要文参考:

  1. 《操作系统》–清华大学网课
  2. 小林coding

如果您觉得我写的不错,麻烦给我一个免费的赞!如果内容中有错误,也欢迎向我反馈。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值