操作系统笔记——概论、进程、线程(王道408)

前言

学校OS课程的知识和408有一定的重叠,但是还不太够,因此我又一次打开了王道的OS课程。

这个笔记同理,只记最关键的内容和思考,直接针对408,基础性的概念性的知识以视频为主。

计算机系统概述

OS的基本概念

在这里插入图片描述
OS提供的服务:

  1. 用户级别:
    • GUI,就是windows
    • 命令接口
      • 联机:有交互性,即cmd命令行
      • 脱机:批处理,即.bat文件
  2. 程序员级别:
    • 系统调用:OS的api
    • 系统调用可以通过c语言的操作系统库函数调用,但是c语言本质上比系统调用还高一级

在这里插入图片描述
异步的前提是并发,程序之间交叉前进,即走走停停,无法预知。

OS的发展历程

在这里插入图片描述

  1. 手工阶段,缺点:
    • 独占
    • 人太慢,机器速度逐渐加快,人拖累机器
  2. 单通道批处理
    • 优:提升预处理速度,不拖累机器
    • 缺:独占
  3. 多通道批处理
    • 优:解决独占,实现并发,提升效率
    • 缺:无交互能力
  4. 分时系统
    • 优:解决交互能力,用户“看起来”独占
    • 缺:没有优先级
  5. 实时系统
    • 优:解决了优先级响应问题,及时可靠

OS的运行机制

在这里插入图片描述
OS内核相当于OS的管理员,因此特权指令只能是内核执行。

平时用户的特权调用,操作都是向管理员申请,而不是亲力亲为。

两个状态的切换:

  1. 升级:特权指令触发中断(硬件),OS响应中断的时候进入核心态
  2. 降级:OS主动修改PSW让出控制权(软件)
    • 修改PSW的指令,本身就是特权指令

在这里插入图片描述

中断和计组第5章衔接

涉及到进程之间的协调,就一定要OS接入,进而需要系统调用。

在这里插入图片描述

需要注意,陷入指令是用户态指令(请求),接下来才会因为内中断进入核心态(执行)

在这里插入图片描述

OS体系结构

在这里插入图片描述

我们OS学的功能,可以放在内核,也可以放在用户,这就形成了大内核和微内核的区别。

微内核暴露的接口多,易于维护和扩展,但是沟通成本大,要反复调用。

在这里插入图片描述

在这里插入图片描述

  1. 分层结构
    • 类似于计网的层次结构,结构清晰,通病是效率偏低
  2. 模块化
    • 主模块分离,模块之间分离,平等
      • 优点:可以同时开发,且效率不错
      • 缺点:模块间的图关系很难把握
    • 动态可加载模块。
      • 可加载说白了就是插件,有没有都不影响运行,因此可以动态加载,比如驱动
  3. 宏内核和微内核
    • 微内核相当于一个服务器,中转不同模块之间的`消息传递
  4. 外核
    • 外核可以提供一些高级的资源(未经抽象的资源)分配方式
    • 内核分配的资源都是抽象的,虚拟化的,比如虚拟地址,外核可以直接分配物理地址,在一些需要频繁跳跃的场景,外核直接分配一片连续空间效果会很好。当然,外核也负责保证安全。
    • 跳过虚拟步骤,就相当于跳过了映射层,可以提高效率,缺点是复杂。

OS引导

在这里插入图片描述

简单来说,就是开机扫ROM就可以把操作系统拉起来,但是具体还是要分几步走:

  1. 扫ROM,启动BIOS,自检
  2. 读磁盘的MBR,获取分区
  3. 从活动分区(C盘)中读PBR,获取C盘根目录
  4. 通过目录找到操作系统的程序,拉到内存中,完成OS启动

这四步环环相扣,前一个获取了信息,后一步才能根据此信息行动。

而第4步用的程序,位置一般在C:/Windows/Boot/下面。

虚拟机

在这里插入图片描述

  1. 第一类VMM,相当于传统OS的加强版,直接运行在硬件上
    • 虚拟OS看起来像一个OS,也有内核,但是实际上还是用户态,因此一个特权指令实际上要经过一次虚拟的系统调用+一次真正的系统调用。
    • 迁移性差,因为直接和硬件耦合
  2. 第二类VMM,是寄居在宿主OS之上的,分为两部分
    • 用户态的VMM和宿主的应用程序是共存的
    • 核心态的VMM是以驱动的形式存在的,持续运行在内核态

在这里插入图片描述

进程和线程

进程和线程基础

进程

在这里插入图片描述

PCB,记录进程元数据:

  1. 描述信息。用于区分进程,PID,UID
  2. 进程控制和管理信息。和进程运行状态有关
  3. 资源分配清单。
    • 资源是进程外部的,而数据段是程序产生的内部数据
  4. 处理器相关信息。寄存器上下文

进程特征:

  1. 并发性和异步是一起的
  2. 结构性指的是每个进程的结构都一样,都是PCB+数据段+程序段

进程状态

在这里插入图片描述

进程控制

在这里插入图片描述

在这里插入图片描述

创建原语分为4步:

  1. PCB创建和初始化
  2. 分配资源(此时进入就绪态)
  3. 插入就绪队列

撤销是逆过程:

  1. 剥夺所有资源,清理子进程
  2. 删除PCB

在这里插入图片描述

阻塞和唤醒,是可逆的过程

  1. 修改PCB:可逆,所以现场要保护起来
  2. 切换队列:把PCB放到对应队列中

无论是阻塞还是进程切换,都要剥夺CPU,而进程有一些内容还存在寄存器中,这些寄存器上下文就是保护现场要做的工作,是中断隐指令的内容。

在这里插入图片描述

进程通信

在这里插入图片描述

  1. 共享储存
    • 把两个进程的虚拟储存,映射到同一个物理储存区域
    • 因此要互斥访问
  2. 消息传递
    • 每一个进程都有一个消息队列
    • 直接通信:A进程把消息直接挂到B进程消息队列里
    • 间接通信:以信箱为中介,B要主动去信箱取出A发来的消息
  3. 管道通信
    • 联系生产者消费者,管道其实就是一个循环队列,是个内存缓冲区
    • 管道互斥访问,因此是半双工(像水管)

线程

在这里插入图片描述

在这里插入图片描述

  1. TCB:Thread CB
  2. 一个进程内部,线程之间资源共享
    • 因此切换成本很小,其就是为了频繁切换,提高并发性而生的。
  3. 一个程序的多线程可以放到不同进程中
    • 多核CPU的超线程
    • 但是这样就无法共享资源了,各用个的

在这里插入图片描述

线程可以理解为剥离掉公用资源后,剩下的相互独立的部分

因此线程的内容比较少,切换的时候只需要保证TCB里面的一些寄存器上下文就可以,比进程少很多。

线程实现

在这里插入图片描述

在这里插入图片描述

用户级线程,适用于早期OS没有线程管理功能的时候:

  1. 本质上是用户自己用代码(线程库)管理线程的调度
    • 在OS看来,只有一个进程而已
    • 线程的调度是纯用户态行为,这种调度非常简单(线程库的逻辑)
  2. 优缺点
    • 优:不需要系统调用,高效
    • 缺:假线程,代码在一个线程卡住,实际上都卡住了

在这里插入图片描述

内核级线程,这是经典的OS负责的线程:

  1. 优点:真并发
  2. 缺点:核心压力大

因此内核级线程又分出多种模式:

  1. 一对一
    • 这其实和内核级线程一样
  2. 多对一:多个用户级线程对应一个内核线程,而CPU只能看到内核线程
    • 这个模式比较鸡肋,和用户级线程一样
  3. 多对多:多个用户级线程,映射到多个内核线程
    • 这才是真神
    • 可以把用户线程切分成3块,每一块都用多对一模型映射到一个线程,整体上三个部分有序工作,互相之间不会拖累,兼顾了效率和并发性

在这里插入图片描述

CPU调度

调度的层次

在这里插入图片描述

作业调度,作业≈一个程序,储存的位置是外存,作业调度≈程序启动
低级调度,针对进程,切换CPU

中级调度,和作业调度一样都是在内外存之间的,区别如下:

  1. 作业调度是比较彻底,就是启动和终止
  2. 中级调度类似于休眠(挂起),暂时放到外存(手机的扩展内存技术就是这样实现的)
    • 引入7状态模型,对就绪和阻塞分别增加对应的挂起状态
    • 在挂起状态(外存里),也可以实现阻塞到就绪的转变
    • 从运行态可以跳过就绪态,直接跳到挂起

在这里插入图片描述

进程调度细节

在这里插入图片描述
进程调度时机:

  1. 主动
  2. 被动。说白了就是被抢占
    • 中断处理(中断隐指令)和原子操作,都会关中断,因此不会被打断
    • OS内核处理内核临界区的时候,权限高,不可被打断

区分一下:

  1. 狭义进程调度。在CPU空闲的时候,抓一个就绪态进程激活
  2. 进程切换。剥夺一个运行的进程,换成另外一个进程
    • 两个操作都要恢复新现场
    • 相比于调度,切换额外要做的操作是保护旧现场

广义的进程调度,可能包含了进程切换这一过程

在这里插入图片描述

最后提一嘴调度程序,我们说,调度是由OS控制的,本质上就是软件,那么说白了,负责调度的管理者,还是一个程序。

这个程序什么时候会运行呢?

  1. 如果是非抢占的时候,就是异步的运行,在特殊情况才运行(创销,阻塞唤醒)
  2. 抢占式的,那么就要定期巡查,决定一个CPU是否应该抢占

调度算法

评价指标

在这里插入图片描述

  1. 周转时间=等待+处理时间
  2. 带权周转时间
    • 本质上是个比例值,可以衡量等待时间在周转时间中的占比
    • 越大,则等待越久
  3. 等待时间。
    • 执行之前的时间,执行起来后等IO的时间不算
    • 作业的等待时间是在成为进程之前的那段时间
    • 进程的等待时间就是创建态+就绪态的那段时间
  4. 响应时间
    • 和等待时间类似,但是针对的只是一种请求,比如键盘,鼠标
批处理调度算法

在这里插入图片描述

在这里插入图片描述

对于非纯计算程序,IO时间不算等待,因此还要抛去IO操作的部分。

FCFS(其实就是FIFO),之所以对短作业不利,就是因为短作业的带权周转时间会很大,这代表其体验很差。

加粗样式

短=Short,即SJF,SPF

具体计算,要分为三部分:

  1. 还没来的作业
  2. 作业池中的作业
  3. 正在运行的作业

正在运行的作业只能在作业池中挑选,而不能是还没有进入作业池的,因此第一个任务只能是P1,即使他时间很长。

SJF分两种:

  1. 非抢占式的SJF如上
    • 比较简单,顺序操作
    • 只需要在作业完成时,分析作业池即可
  2. 抢占式的SJF(SRTN
    • 如果有一个新的作业来了,那么就有可能比当前正在运行的作业的剩余时间短,此时就把作业替换回作业池中(记得标注剩余时间)
    • 具体做题的时候,比非抢占式要额外多分析新作业来的时间点

在这里插入图片描述

  1. 题目区分细节
    • 默认非抢占,但是比较模糊
    • 考虑到抢占,SRTN的平均周转时间肯定是最少的
    • 如果默认作业没有到达顺序的先后之分,那么非抢占SJF=SRTN
  2. SJF对长作业不利,无论是否抢占,都可能造成饥饿现象

在这里插入图片描述

HRRN用到响应比指标,综合了等待时间和处理时间,在保证了优先级的前提下,修复了SJF饥饿的问题。

分析思路和SJF类似,都是分成三部分,HRRN为非抢占式的,所以只在CPU空闲的时候对作业池进行分析。

注意,其等待时间是从到达开始计数的。

交互式调度方法

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

RR(时间片轮转)

  1. 轮转本身很简单
    • 建立一个就绪队列,每一个时间片出队,处理队头对应的进程
    • 时间片完了以后就插到队尾
  2. 难在新任务来的时候,会改变队列结构
    • 新任务来了,也是插在队尾
    • 如果A任务的时间片完了,此时B任务刚来,这二者的顺序以B为先,毕竟新来的要照顾一下(注意,这个照顾顺序只针对想要同时入队的两个任务,前面早就在队里的任务仍然在前面)
  3. 如果任务处理完,时间片还没用完,会直接终止当前时间片
    • 很正常很人性化的设计,干等着没意思

因此,整个分析过程需要考虑的也是处理完以及新任务刚来的两种时刻。

严格来说,RR不会剥夺正在执行的时间片,但是先来先插到队尾的这种逻辑,以及规定时间片用完就剥夺,这两个操作具有抢占性,所以我们规定RR是抢占式的(剥夺的来源是时间片本身,而不是其他进程)

时间片太大,就会退化为FCFS,太小则切换开销占时间片的比例就太大了(类似于流水线那个感觉),效率降低。

在这里插入图片描述

优先级调度算法。优先数越高,就越排在前面激活。

此方法分为抢占式和非抢占式。
分析节点参考SJF。

在这里插入图片描述

多级反馈队列调度,这个是666,真神

我们直接分析一下其性质,相当复杂,但是又相当合理:

  1. 队列内部是RR,队列之间是抢占
  2. 关于优先级
    • 新手保护:刚来的,优先级最高
    • 耗时降级:如果耗时长,每完整执行一个时间片,优先级就会降低一级,直到降无可降
    • 被剥夺不降级:被剥夺严格来说不算执行完一个时间片,因此不降级,只是按RR原则放到了队尾。不过还会有补偿,新获得的时间片又是一个完整的时间片
  3. 关于轮转
    • 优先高级:高一级的队列为空,才对下一级进行RR遍历(有抢占性,如果高一级来了新的,则立马切到高一级)
    • 保障低级:越低级,时间片越长。虽然低优先级低人一等,但是总得让人家执行完,所以低优先级的时间片反而会更多,一旦轮到了,就可以持续执行很长时间(当然,如果你不争气,执行不完那就继续降级,又或者被强占,总之还是低人一等)

优缺点分析:

  1. 公平:FCFS
  2. 快响应:RR+优先级调度
  3. 短进程优先:SPF
  4. 自动优先级
    • 如果想要保证任务的高优先级,可以尽量让其不降级,比如如果进程因为IO主动放弃时间片,此时我们不认为其执行完毕,因此不降级,这样就保证了IO的高优先级
  5. 缺点:仍然不是绝对公平,因此会造成饥饿

在具体实践中,为了防止饥饿出现,可能一个队列会分配固定的时间上限,如果超过这个界限,还是要切换队列的,不能一直卡在高优先级队列。
此外,不同队列内部的策略还可以不一样。

在这里插入图片描述

同步与互斥

基本概念

在这里插入图片描述

异步就是无序性
同步就是有序性,A一定在B前。

遵循的原则,总结起来就是,忙可以先等一下,等久了就撤(让权),但是你不能让我等太久,过一段时间我还得拿回权利并且进入临界区。

互斥

互斥软件实现

在这里插入图片描述
在这里插入图片描述

单标志法是最原始的轮询

本质上,turn代表谦让以及指定,刚开始就指定一个进程,而一个进程执行完以后又会指定另外一个进程。
但是其问题在于,临界区使用权和CPU占用权是分裂的,让给你用,并不代表你就能用,即空闲也进不去,违背“空闲让进”

比如此时标记0进程能用临界区,但是此时CPU时间片属于1,此时while循环就会持续1个时间片,一边是忙等,另一边是空闲没人用

在这里插入图片描述

双标志法,把谦让逻辑变成了,将指定变成了排他逻辑,占用逻辑。

占用和释放,只修改自己的占用标记,不去指定他人。

双标志先检查,检查和上锁不连贯,此时,按照1526顺序,A还没上锁,B就通过检查了,就会同时进入临界区,即“忙”也能进去,违反了忙则等待。

为了修正,出现了双标志后检查法,这种方法先把临界区标记了,再检查,那么就可以确保别人拿不到临界区(缺德做法),但是这很明显不靠谱,会出现都用不上的情况,按照1526顺序,AB都标记,则会排他,那么AB就都会卡在检查阶段。

此时,即使临界区空闲,AB谁也不让着谁,谁也用不上,这违背了“空闲让进”,而且一直卡死,违背了“有限等待”

在这里插入图片描述

在这里插入图片描述

peterson算法,综合了单标志和双标志后检查法

标志代表意愿,turn代表谦让,最后被谦让的那一个,就可以获得使用权。

以1627举例,2先谦让一下,但是7后面有谦让一下,于是2就勉为其难的进入了,和现实的套路一模一样。
本质上,turn同一时间有且只能有一个值,因此同一时间只能有一个进程跳出循环,且必有一个进程跳出循环。

非常牛逼的思路,但是仍然优缺点,因为进程仍然是忙等状态,即违反了“让权等待”,但是已经是成本最低的了,这个操作可以通过时间片轮转来剥夺。

互斥硬件实现

在这里插入图片描述
关中断的缺点:

  1. 多处理机,关不了其他CPU,进程可以借助其他CPU访问临界
  2. 仅内核。因为这个操作给用户太危险

在这里插入图片描述

TestSet的操作,说白了就是用old检查,并对lock进行上锁,这是一个原子过程,检查和上锁是一气呵成的。
如果lock原来就是true,那么再上也无所谓
如果lock原来是false,那么就可以同时实现上锁+退出循环,不用担心被打断

上面这个代码只是模拟,实际上是硬件TSL指令实现的,是原子的,而软件编程是无法达到这个效果的。
缺点和peterson一样,都是忙等,不满足“让权等待”原则

还有一个Swap指令(Exchange,XCHG指令),和TSL基本一样的逻辑和特性

互斥锁(自旋锁)

互斥锁是一种思想,和mutex操作很像,就是申请和释放。

但是其申请过程是忙等的,所以TSL,swap指令都是自旋锁,单标指法其实也是自旋锁,申请过程都具有原子性

当然,正如单标志法哪里说的,忙等其实不完全忙等,时间片没了就退出(单处理器没有RR,所以就是彻底忙等)。甚至说有时候反而有意外效果,等待的时候不用切换上下文,有时候成本反而低。

在这里插入图片描述

信号量

信号量机制

在这里插入图片描述

整形信号量,说白了就是双标志先检查,但是P和V是原语,可以保证不会同时进入

但是因为底层还是一个循环,仍然会忙等,不满足让权等待。

在这里插入图片描述

记录型信号量引入阻塞队列,解决了忙等现象

  1. 原来是忙等,现在发现资源不够就丢到阻塞队列里。
    • 极限情况为0,-1后为负,此时是第一个进程阻塞
  2. 如果资源够了,且阻塞队列里有进程,就唤醒
    • 极限情况为-1,+1后为0,此时把阻塞队列里最后一个进程唤醒
信号量实现互斥同步

在这里插入图片描述

semaphore mutex = 1 :代表记录型信号量,有等待队列

互斥比较简单,mutex=1,PV夹住临界区。

在这里插入图片描述

同步是前V后P,用一个信号量关联两个进程,等V操作执行完后,P才能执行下去。

给定一个拓扑图,只需要把每一个前驱后继关系都用一个信号量定义一下即可。
之后每一个节点都是一个进程,把边定义的前驱后继关系写到进程里面即可:

  1. 入边写P
  2. 出边写V

在这里插入图片描述

经典信号量问题

生产者消费者——基本的分析思路

在这里插入图片描述

需要注意一个细节就是,P操作是不可互换的,因为mutex只夹临界区,夹得多了就会出问题(死锁)
V操作可以互换,因为V操作一定不会被卡住

在这里插入图片描述

多生产者多消费者——多种生产者

在这里插入图片描述

首先是最简单的两组关系:

  1. apple和orange各自有一对同步关系
  2. plate关系:关键在于盘子,盘子是双方的一个中介,并不能单看父或者母,要把父母统一为一方,把子女统一为另一方
    在这里插入图片描述

具体实现如下,三个同步信号量,这里将plate设置为“还可以放的空间”,因此初值为1

因此,整体就实现了一个PV结构,每一个进程,都是有P有V。
mutex实际上可有可无,因为我们这里的资源上限为1,已经相当于mutex的作用了,但是如果盘子空间变成2,就得加mutex了,否则就可能发生覆盖现象。

在这里插入图片描述

如果反过来呢,plate=盘中可用水果数量,会出问题,比如dad,此时就是V(apple),V(plate),可以看到这个进程是没有P的,也就是说不会被阻塞,他可以一直释放。

所以我们这里还可以总结出一条生产者消费者问题中,隐性的要求,就是PV一定是要成环的,相互制约,一个进程只有V无P,必然是有问题的,在我们制定信号量意义的时候,也应该考虑构造一个PV环结构。

吸烟者问题——多功能生产者

在这里插入图片描述

一个多功能生产者,给多个单功能消费者提供原料,同步关系如下

在这里插入图片描述

再拓展一下思路,如果finish定义为1呢?

那么就要把生产者的P操作放在最开始,消费者是不变的,仍然可以正常运行(因为仍然是PV环,而且PV关系没有变)

分析一下是否需要mutex,因为只有一个生产者,所以缓冲区最多有1个元素,不会出问题。
但是呢,如果有n生产者,此时因为生产者是V在前的,第一次生产不受阻塞,所以就可能会让缓冲区里存在n个元素,所以这种写法其实不好,如果是按照我那个P在前的写法,即使是有多个生产者进程,只要规定finish=1,就只能有一个元素被生产

哲学家进餐问题——连续申请多个资源

这个问题本身不难,难在如何解决死锁。

哲学家进餐问题的死锁情况为,每个哲学家都只P了一半,都卡在了第二个P上。
本质上,哲学家进餐是连续申请多个资源,如果申请的途中被卡了,而且是集体卡顿,那就死锁了。

所以解决哲学家死锁,就要从这些领域入手:

  1. 最多让n-1个哲学家同时进餐,这样就一定可以保证有1个哲学家不会被卡死
    • 可以用一个值为n-1的信号量限制
  2. 限定哲学家拿资源的顺序,强制哲学家进行两两竞争
    • 比如指定奇数哲学家先左边的,偶数先拿右边的,那么这一对哲学家必然是两个必有一个阻塞,而另一个没被阻塞的哲学家,在另一边不存在竞争,一定可以吃到
  3. 保证哲学家多个P操作的原子性
    • 在一连串P外面加个mutex即可
    • 即使一个哲学家被卡,其他哲学家也不可能是P操作被卡,即其他哲学家在吃饭,这是暂时的,可以恢复的

在这里插入图片描述

读者写者问题——优先级互斥问题

在这里插入图片描述

代码分为4个版本:

  1. 单进程读/写(全部互斥)
  2. 读者优先
  3. 读写公平
  4. 写者优先

这四个版本,2实现了多读者同读,234逐步提高写者的优先级,具体的思想如下:

  1. 插队逻辑:通过if语句,可以制造插队,提高优先级
    • 副作用是多进程可以同读同写
    • 读者优先利用了这个副作用(其本意只是提高优先级,顺带实现同读)
  2. 抵消插队逻辑:在保留插队逻辑副作用(同读)的基础上,抵消插队带来的优先级效果
    • 在插队逻辑外面加一个信号量即可(设为w)
    • 注意,对w而言,高优先级的进程,w是覆盖所有代码的,低优先级的进程,w仅覆盖进入区以及插队逻辑,由此,w提高优先级和插队逻辑的优先级效果就互相抵消了
    • 这种思路必须要基于插队逻辑才行,因为w并不是针对临界区的管控,至少还得有一个信号量(rw)管控临界区
  3. 控制同时读/写:在插队逻辑的前提下,选择性的控制是否可以同读同写
    • 读者优先中,直接对临界区信号量(rw)加插队逻辑,利用了插队的副作用,无伤实现同读
    • 写者优先不可以这么做,所以需要把插队逻辑外提到非临界区信号量(w),即使可以插队,新来的也得一起卡在临界区外面

在这里插入图片描述

这是最基础的读者优先结构,这个结构务必理解透彻了。

  1. rw:直接对临界区上锁,副作用是会造成读读互斥
  2. 插队逻辑(消除读读互斥并提升r的优先级)
    • if判断:现在加了判断,使得读者里面,只有第一个和最后一个读者需要维护锁,其他情况下,只要有读者在读,新来的就可以直接读
    • mutex:令count判断部分原子化,保护count变量

在这里插入图片描述

读写公平如上,是在读者优先的前提下修改的,仍然保证了读者不互斥的特性
但是使用信号量w额外增加了读者对写者的反制能力,说白了就是用w抵消了读者的插队能力。

这个w加的位置非常巧妙:

  1. 对于写者来说,w是覆盖了临界区的,也就是说,可以造成写者互斥的效果
  2. 对于读者来说,w只是卡在了最开始的进入区,这样就不会造成读者互斥
  3. w和rw使得读者和写者可以相互钳制
    • 读者在进入区,则写者卡在w
    • 读者在临界区(获取rw,释放w),则写者可以进一步卡在rw(已经获取了w)
    • 写者在进入区(获取w,卡在rw),其余读者/写者已经进不了进入区了(卡在w)
    • 写者在临界区,同写者在进入区

如果要实现写者优先呢?
还是插队逻辑,在读写公平的前提下,给写者增加一个插队逻辑

为了防止出现同写情况,需要将插队逻辑外提到w上。

  1. 不能像读者那样插。读者是可以共同读的,写者不行,所以不能给rw加插队逻辑
  2. 考虑给w加插队逻辑,这样,写者可以源源不断的到达“进入区”
    • 实际上,这个操作就是修改了读写公平里的这句描述:其余读者/写者已经进不了进入区了(卡在w),给写者开了个后门
  3. 为什么不能像读优先那样,直接照搬写一个写优先?
    • 因为读优先是可以同读的,但是写无法同写,所以要外提插队逻辑,所以一定是不能照搬的
    • 写优先还可以爆改一下,反正都写优先了,把读的插队逻辑去掉也是可以的,当然这样就不能同读了
semaphore rw=1; //读写公用临界区信号量
int rcount=0; //读者插队逻辑
semaphore rmutex=1;

semaphore w=1; //用于提升write的优先级
int wcount=0; //写者插队逻辑
semaphore wmutex=1;

reader(){
	while(1){
	P(w);//抵消插队逻辑优先级
	
	P(rmutex);//保护rcount
	if(rcount==0)//插队逻辑
		P(rw);
	rcount++;
	V(rmutex);
	
	V(w);//注意,抵消插队逻辑优先级的时候,被抵消方的V(w)插在临界区前
	
	写文件//临界区
	
	P(rmutex);
	rcount--;
	if(rcount==0)
		V(rw);
	V(rmutex);
	}
}

writer(){
	while(1){
	
	P(wmutex);
	if(wcount==0)//w对写者加入插队逻辑
		P(w);
	wcount++;
	V(wmutex);
	
	P(rw);//公用临界区信号量
	读文件//临界区
	V(rw);
	
	P(wmutex);
	wcount--;
	if(wcount==0)
		V(w);
	V(wmutex);
	}
}

代码分为4个版本:

  1. 单进程读/写(全部互斥)
  2. 读者优先
  3. 读写公平
  4. 写者优先

这四个版本,2实现了多读者同读,234逐步提高写者的优先级,具体的思想如下:

  1. 插队逻辑:通过if语句,可以制造插队,提高优先级
    • 副作用是多进程可以同读同写
    • 读者优先利用了这个副作用(其本意只是提高优先级,顺带实现同读)
  2. 抵消插队逻辑:在保留插队逻辑副作用(同读)的基础上,抵消插队带来的优先级效果
    • 在插队逻辑外面加一个信号量即可(设为w)
    • 注意,对w而言,高优先级的进程,w是覆盖所有代码的,低优先级的进程,w仅覆盖进入区以及插队逻辑,由此,w提高优先级和插队逻辑的优先级效果就互相抵消了
    • 这种思路必须要基于插队逻辑才行,因为w并不是针对临界区的管控,至少还得有一个信号量(rw)管控临界区
  3. 控制同时读/写:在插队逻辑的前提下,通过控制插队的信号量,选择性的控制是否可以同读同写
    • 如果把插队逻辑加在临界区信号量上,就会造成同读/写,外提则不会
    • 读者优先中,直接对临界区信号量(rw)加插队逻辑,利用了插队的副作用,无伤实现同读
    • 写者优先不可以这么做,所以需要把插队逻辑外提到非临界区信号量(w),即使可以插队,新来的也得一起卡在临界区外面

管程

PV操作和生产消费过程混在一起,耦合度高,容易出错

因此直接把控制互斥同步的部分剥离出来,封装成类(管程):

  1. 在管程内部定义变量和初始化
  2. 在管程内部定义方法,实现同步机制
  3. 至于mutex互斥,管程通过方法的互斥来实现,同一时间只能有一个进程调用管程
    • 编译器会自动实现,也就是说你只需要定义同步就可以

在这里插入图片描述

死锁

死锁概述

在这里插入图片描述

死锁的条件:

  1. 前提是争抢(互斥)
  2. 其次是占着不放(不剥夺)
  3. 不仅不放,还要持续地请求别的资源(请求和保持条件)
    • 换句话说就是,在只获取了部分资源的前提下,还要获取更多资源
  4. 满足这三个大前提,一旦成环(循环等待条件),则死锁

预防死锁——破坏死锁条件

在这里插入图片描述

  1. 破坏互斥。这个思路看着就不靠谱
  2. 破坏不剥夺。进程要反复切换,开销大,而且持续剥夺会导致饥饿
    • 方案一:主动退位
    • 方案二:OS协助剥夺
  3. 破坏保持和请求。
    • 保持和请求本质上是因为只获取了一部分资源,不得不继续请求剩余资源,那么我们干脆一次性给到位再让他启动,否则就干脆不给,没有中间态。
    • 很显然利用率低,饥饿
  4. 破坏循环等待。
    • 给资源编号,规定一个进程申请资源的编号是递增的
    • 众多进程中,必然有一个进程掌握已有的最大编号的资源(比如下图的7号资源)
    • 这个进程需要的资源编号肯定比在场所有进程资源的编号都大(至少是8),也就是说这个大编号进程要的资源,只可能是空闲的,即使其他进程卡死,这个进程一定也可以执行下去
    • 很显然,不方便,浪费
      在这里插入图片描述

避免死锁——单步动态预测

看下来可以发现,死锁预防的方法缺陷都很大。

银行家算法是一个动态预测的方法,其实和前面那个破坏保持和请求条件的思路类似,保证资源够用,具体做法如下:

  1. 每次分配之前,我都要确保分配之后仍然是安全状态。
    • 所谓的安全状态,就是分配了以后,我仍然有足够的资源让一个进程彻底执行完毕
  2. 这是做最坏的打算,只要我每一次分配都是在安全状态上,那就确保不会发生死锁。这是一种非常保守的思路。

当然,有的情况是最保守的策略都无法解决的,那就死锁是必然的,要从其他地方找问题(如下图)

在这里插入图片描述

检测和解除死锁

在这里插入图片描述

上面介绍的预防和避免,都比较简单,死锁检测通过软件的思路,设计算法去对图结构进行分析,可以得到好的全局分析结果。

在这里插入图片描述

给定一个资源分配图,图中有4类元素,注意点如下:

  1. 资源里面的点数代表资源最大出度
    • 点数-资源已有出度=可用资源数量
  2. 进程的出度代表申请的资源数

具体做的时候,就是找出能够先执行完的进程,执行完将资源释放,然后滚雪球化简资源分配图,直到所有进程执行完毕。

能够先执行完的进程怎么找呢?就是去计算一下,可用资源数量是否满足进程申请的资源数
以上图举例,P1申请1个资源,R2剩2-1=1个资源,所以P1可以执行下去,之后逐步化简就好

如果化简不完,最后有剩余的一个环,那么就代表这些进程构成了死锁循环,针对性的剥夺就好了。
这就是软件全局分析的好处,精准。

解除死锁的方法辨析:

  1. 资源剥夺法。
    • 单纯剥夺资源,进程只是挂起(歇一会),这样可能导致饥饿
  2. 撤销进程法
    • 直接remake死锁进程,啥都没了,可能造成浪费(白干了)
  3. 进程回退法
    • 类似于git的回滚,回到一个可以完全消除所有边的状态
    • 这个理想很好,实际上很难实现,因为要记录回退点

至于对哪个进程动手,归根结底就是对优先级低的动手,让出机会给高优先级任务。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

亦梦亦醒乐逍遥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值