操作系统因为翘课太多只能自学啊~~
而且计算机基础知识是很重要的,必须要认真对待才是(那为啥翘课?翘课学前端去了…)
不管怎么说,还是做个笔记方便日后复习
(图片均出自中国大学MOOC或bilibili视频截图)
随便写点(这个部分可以跳过)
计算机系统:处理机+外围设备
处理机:包括CPU,主存储器、输入输出设备
CPU:中央处理器+控制核心
(下文有时会把处理机称为CPU,见谅)
(后面网上一查才发现,好多人都把处理机和处理器混淆了)
中央处理器:运算单元+控制单元
主存储器
外围设备:输出设备 输入设备 存储设备 网络通信设备(比如鼠标键盘等)
总线:Bus,是CPU,内存,输入输出设备传输信息的共用通道;外围设备通过相应的接口电路再与总线相连接,从而形成了计算机硬件系统
存储器的层次:
外围设备及其控制:
总线的类型:
前置概念
(引用自CSDN博主「快乐的一只小喵喵」的原创文章)
程序计数器(PC,Program counter)
用于存放指令的地址。为了保证程序(在操作系统中理解为进程)能够连续地执行下去,CPU必须具有某些手段来确定下一条指令的地址。当执行一条指令时,首先需要根据PC中存放的指令地址,将指令由内存取到指令寄存器中,此过程称,为“取指令”。与此同时,PC中的地址或自动加1或由转移指针给出下一条指令的地址。此后经过分析指令,执行指令。完成第一条指令的执行,而后根据PC取出第二条指令的地址,如此循环,执行每一条指令。
指令寄存器(IR,Instruction Register)
用来保存当前正在执行的一条指令。是临时放置从内存里面取得的程序指令的寄存器,用于存放当前从主存储器读出的正在执行的一条指令。当执行一条指令时,先把它从内存取到数据寄存器(DR,Data
Register)中,然后再传送至IR。指令划分为操作码和地址码字段,由二进制数字组成。为了执行任何给定的指令,必须对操作码进行测试,以便识别所要求的操作。指令译码器就是做这项工作的。指令寄存器中操作码字段的输出就是指令译码器的输入。操作码一经译码后,即可向操作控制器发出具体操作的特定信号。
> 通用寄存器(GR,General register)
通用寄存器可用于传送和暂存数据,也可参与算术逻辑运算,并保存运算结果。除此之外,它们还各自具有一些特殊功能。通用寄存器的长度取决于机器字长,汇编语言程序员必须熟悉每个寄存器的一般用途和特殊用途,只有这样,才能在程序中做到正确、合理地使用它们。
————————————————
一种较为简单的指令执行步骤:
1.取指:根据PC从存储器或高速缓冲存储器中取指令到IR
2.解码:解译IR中的指令来决定其执行行为
3.执行:连接到CPU部件,执行运算,产生结果并写回,同时在CC里设置运算结论标志;跳转指令操作PC,其他指令递增PC值
用户的程序并不能够使用全部的计算机指令(比如app总是要你授权就是这个道理),计算机核心资源的特殊指令会被保护起来,它们只能被操作系统程序使用
至于特权指令是怎么被实现的,就要提到处理器模式:
计算机一般设置0 1 2 3四种模式
0是操作系统内核
1是系统调用
2是共享库程序
3是用户程序
数值越大权限越低
tip:现代操作系统很多都只是用了0和3两个模式,内核模式和用户模式(下图以红色的括号为准)
嘛。。。其实就是字面意思嘛。。。
就是程序执行过程中,遇到突发状况,就暂时终止处理机上当前程序的运行,转去处理突发事件,处理完成之后再回到刚才暂停的地方继续执行 这个过程,就是中断
中断是并发的基础
中断源
那引起中断的事件就称为中断源
它可能是处理器执行机器指令引起的,比如除数为0,操作数溢出等算数异常或者其他异常
对于简单的问题,处理器会简单处理然后报告用户;也可以由用户编写的中断续元程序处理
但是复杂的问题(比如I/O中断,来源于外围设备报告I/O状态的中断事件)(比如打印机打印一般了发现缺纸了)
就需要人工干预了
终于进入主题。由于学校的视频资源给的好像不是很系统,所以我决定自己找视频再结合教材作书摘一样地写
定义
Operating system,即OS,是配置在计算机硬件上的第一层软件,也就是说,操作系统是计算机硬件的第一次扩充
目的
管理硬件,提高利用率和系统吞吐量,具体实现了以下四个方面
名称 | 内容 |
---|---|
方便性 | 没有操作系统的话用户只能用机器语言操作了 |
有效性 | 合理组织计算机工作流程,提高效率 |
可扩充性 | 方便地增加新的功能和模块 |
开放性 | 很好地应用于网络环境 |
功能
不同的场合下身份不同,一共可以充当三个角色
身份 | 功能 |
---|---|
资源的管理者 | 管理 处理机、 存储器、 文件、 设备 |
用户的服务者 | 向用户提供接口(定义见后续内容) |
硬件的扩充者 | 扩充机器 |
管理处理机、存储器、文件和设备
接口
学过开发的大家应该清楚什么意思。
这里指的是用户和计算机硬件之间的接口,即用户操控计算机硬件资源的途径
以下是三种主要的接口
类型 | 内容 |
---|---|
命令接口 | 允许用户直接使用 |
程序接口 | 允许用户通过程序间接使用 |
GUI | 现代操作中最流行的图形用户接口(比如windows,这里我们不讨论GUI) |
其中,命令接口又可以分为:
类型 | 内容 |
---|---|
联机命令接口 | 用户说一句,系统做一句 |
脱机命令接口 | 用户说一堆,系统做一堆 |
然后,程序接口具体是:
由一组系统调用组成(系统调用 就是 程序接口,有的地方也叫广义指令)
一共有四个特征:并发 共享 虚拟 异步
其中 并发 和 共享 是最基本的特征, 二者互为存在条件
并发
并发是两个或多个事件在同一时间间隔内发生
这些事件在宏观上是同时发生,但是微观上是交替发生的
这是书上的话,其实就是说
并发是一个时间段里面多个事件先后发生
(并且不是一贯到底的)
(如果时间段比较短的话,就像是同时完成了多个事件)
另外,并发的基础是中断
这里有一个容易混淆的概念:并行
两个或多个事件在同一时刻同时发生
那么问题来了——为什么是并发而不是并行
对于计算机来说,并行不是明显更高效吗,为什么操作系统的特征是并发呢?
主要是硬件限制,以前单核的CPU,同一时刻只能执行一个程序,所以就需要并发;
现在呢,保底都是四核了,也就是同一时刻四个程序,但是现在用电脑同一时刻才几个程序?所以还是会有并发…
共享
共享指的是资源共享,是指系统中的资源可供内存中多个并发执行的进程使用
资源共享的方式
方式 | 内容 |
---|---|
互斥共享 | 一个时间段内只能给一个进程访问 |
同时共享 | 一个时间段内由多个进程轮流访问 |
同时共享 和 并发 类似,也只是宏观上的同时,微观上是几个程序轮流占用
(当然有些时候也真的是同时,比如一边打游戏一边听歌,扬声器可以同时播放两者的音乐)
并发和共享的关系
单核CPU下,没有并行这种操作,那要是并发都没有,计算机隔很久才执行一个程序,那共享从何而来?
另外,要是没有共享性质,极短的事件内,一个资源不能被两个或者多个程序访问,那么并发也无法继续进行
所以二者是相互依存
虚拟
把一个物理上的实体变为若干个逻辑上的对应物;实体是实际存在的,对应物是用户感受到的
举例1:空间的虚拟
一个程序需要放入内存并且给它分配cpu才能执行
那么比如运行一个游戏要10G内存,但是我们的电脑只有4G的内存,但是我们就是可以让它运行,好像我们电脑的内存不止10G的样子
这个例子中,4G内存就是实体,10G是用户感受到的,也就是逻辑上的对应物体
这里涉及到的技术叫做 空分复用术
举例2:时间的虚拟
单核CPU的计算机 看上去 似乎可以“同时”执行多个程序(体现为 并发),可是它的原理是什么呢?
这里涉及到的技术叫做时分复用术
(这里也看出,如果没有并发,就不需要(时间上的)虚拟;如果没有虚拟,就难以并发)
异步
在多道程序环境下,允许多个程序并发执行,但是资源有限,每个进程的执行不是一贯到底(进程是并发的),而是走走停停(轮流使用一段时间资源),以不可预知的速度向前推进
说到异步啊…最开始学前端学到async和ajax的时候接触到了异步,当时我以为的异步实际上是并行(因为当时学的时候,有人说异步是生活中的同步,同步才是生活中的异步)…所以我之前一直理解错了…
我们学习的范围包括:
1.手工操作系统
2.批处理操作系统(包括单道和多道)
3.分时操作系统
4.实时操作系统
5.网络操作系统
6.分布式操作系统
7.个人计算机操作系统
发展顺序自上而下,每一个阶段都是为了解决上一代的主要缺点
其中重点掌握2 3 4,1 5 6 7了解即可
1.手工操作系统
其实这个都算不上操作系统
当时的程序员用纸带打孔的方式 表示0和1 进行编程
流程大概是:
纸带A(代码)–>纸带机–>计算机(计算)–>纸带机–>纸带B(运行结果)
计算机速度很快,但是啊纸带机很慢,整个流程中大部分时间都在等待纸带机读取数据
这种流程有两个主要缺点:
计算机有很多时间在等待 ,
一次只能有一个用户占用资源(机器)
上述两点概括为 人机速度矛盾
2.1批处理操作系统:单道
这个阶段引入了脱机输入/输出技术,并且引入监督程序(操作系统的雏形)负责控制作业的输入输出
流程大概是(对称的,看一半就行)
纸带->纸带机->外围设备->磁带->计算机->磁带->外围设备->纸带机->纸袋
其实就是引入了外围设备和磁带,外围设备可以把数据快速存到磁带里面,然后磁带内的数据可以被计算机快速处理,输出同理
这个过程中,监督程序负责调控 计算机读取磁带内数据
缺点是
只有一个程序能运行,CPU依旧有很多时间在等待I/O,资源利用率还是不高
2.2批处理操作系统:多道
流程同上,但是每次可以使用多个磁带向计算机输入数据了
引入了中断技术,并发由此出现,操作系统正式诞生
(再次强调,中断是并发的基础)
缺点
响应时间长,没有人机交互能力(用户提交之后就只能等,不能手动控制调度)
3.分时操作系统
引入了时间片的概念(就是一个较小的时间段,大概是一个定值)
计算机以时间片为单位轮流为各个用户/作业服务,各个用户可以通过终端与计算机进行交互
举例
比如时间片是0.01s,那么每个任务轮流执行0.01s,在1s内可能会完成多个任务,给用户的感受就是多个“同时”完成
缺点
没有优先级,完全公平地处理任务,不能分轻重缓急
4.实时操作系统
引入了优先级
在分时操作系统的基础上,能够优先响应一些紧急任务(它们不需要时间片排队)
分类(了解即可)
类型 | 内容 |
---|---|
硬实时系统 | 必须在绝对严格的固定时间内完成处理 |
软实时系统 | 能够接受偶尔违反时间规定 |
5-7其他几种操作系统(了解即可)
网络操作系统
实现网络中各种资源的共享和各台计算机之间的通信
分布式操作系统
有分布性和并行性,由多台计算机分工,工作可以分布在这些计算机上并行、协同完成
个人计算机操作系统
比如windows,方便个人使用
两种指令
这里的指令是指机器语言指令(高级语言翻译过来的)
两种指令主要是:特权指令和非特权指令
类型 | 内容 |
---|---|
特权指令 | 不允许用户随意使用 |
非特权指令 | 允许用户随意使用 |
出现这种分类的原因正如其名——有的指令可能造成的影响较大(比如删库?),不允许用户随便使用,即“特权”
但是,计算机怎么识别当前是否可以执行特权指令呢?
这就是接下来将要提到的处理器状态了
两种处理器状态
处理器状态分为 用户态(目态) 和 核心态(管态)
类型 | 内容 |
---|---|
用户态(目态) | 此时处理器不可执行特权指令 |
核心态(管态) | 此时处理器可以执行特权指令 |
这是利用程序状态字寄存器PSW中的某标志位来标识当前处理器处于什么状态。(比如可以让用户态为0,核心态为1)
上述内容体现在处于不同状态的程序上
两种程序
也就是内核程序和应用程序,正如其名,不再多言
(当然,如果提到“管态程序”、“核心态程序”这些等价的概念时,也要反应得过来才是)
是计算结配置上的底层软件,是操作系统最基本、最核心的部分
就是常常听到的什么四核八核十六核CPU那个核
大概是这么一个地位:
时钟管理
实现计时的功能
中断
前面说并发的时候提到过
负责实现中断机制(并发的前提)
分类
广义的中断的分类标准主要根据信号的来源——CPU的内部还是外部
类型 | 内容 |
---|---|
内中断(异常、例外、陷入/陷阱) | 指令中断的则为自愿中断;硬件故障或者软件中断的则为强迫中断 |
外中断(中断) | 一般是人工干预或外围设备请求导致的 |
所以狭义的中断实际上是外中断
运作过程
配合时钟管理的计时器,发出中断信号
中断处理是内核的一部分,所以中断发生时,CPU进入核心态
(中断是 用户态 转为 核心态 的唯一途径)
(而核心态 转为 用户态 的途径是执行一个特权指令,将PSW设置为“用户态”)
中断发生后,当前运行的进程暂停,接下来操作系统内核将对其进行处理
那么具体一点就是:
执行每个指令后,CPU都会检测是否有外部中断信号: 如果没有,那么执行下一个指令;
如果有,那么保存即将被中断的CPU环境(相当于游戏存档,方便下次继续),然后进行中断处理。
原语
一种特殊的程序
处于操作系统最底层,最接近硬件
运行时间短、调用频繁
具有原子性(就是数据库事务提到的那个)
系统资源管理
包括进程管理、存储器管理和设备管理等
内核分类
有不同的分类标准,比如:
类型 | 内容 |
---|---|
单内核 | 内核中各个部件混杂的形态 |
大内核 | 在微内核基础上,还包括进程、存储器、设备管理等 |
微内核 | 结构性部件与功能部件分离 |
混合内核 | 微内核和单内核的折中,较多组件再核心态中运行 |
外内核 | 尽可能减少内核的软件抽象化和传统微内核的消息传递机制 |
这里主要讨论 大内核 和 微内核
大内核
把操作系统主要模块都作为系统内核,运行在核心态(管态)
优点:高性能(一直是核心态不用切换)
缺点:代码庞大,结构乱,难维护
(这个缺点是很多早期计算机技术的通病)
微内核
内核只保留基本功能
优点:内核功能少,结构清晰,方便维护
缺点:需要频繁地在核心态和用户态之间切换,低性能
就是程序接口(严格意义上说操作系统是由一组系统调用组成的),上面有提到过
操作系统提供给应用程序(编程人员)使用的接口。可以理解为一种可供应用程序调用的特殊函数,应用程序可以请求系统调用来获得操作系统的服务。
进程就是一个程序地执行过程
当一个程序被放到内存中,才能够被CPU处理,才能执行
(我们双击exe文件,就是把程序放入内存的操作)
其中,程序的定义是一个指令序列
而进程可以从不同角度进行定义,比如这些:
程序段,数据段,PCB(进程控制块)三部分组成了进程实体(也叫进程映像,简称为进程)
进程是程序的一次执行过程
进程是一个程序及其数据在处理机上顺序执行时所发生的活动
进程是具有独立功能的程序在数据集合上运行的过程,它是系统进行资源分配和调度的一个独立单位
进程是进程实体的运行过程,是系统进行资源分配和调度的一个独立单位
程序段,数据段,PCB三者都在内存中:
程序段、数据段分别是CPU中存放程序代码和所需数据的地方,
PCB则存储了程序段和数据段在CPU中的地址等信息
严格地说,进程实体是静态的,进程是动态的
PCB的存在时进程存在的唯一标志
一个系统中,会存在众多PCB。必须用适当的形式来组织管理他们。
主要有两种组织方式:
方式 | 内容 |
---|---|
链接方式 | 按照进程状态将PCB分为多个队列;操作系统持有指向各个队列的指针 |
索引方式 | 根据进程状态的不同,建立几张索引表;操作系统持有各个索引表的指针 |
链接方式
拥有执行指针,指向当前处于运行态(执行态)的进程的控制器PCB;
拥有就绪队列指针,指向当前处于就绪态的进程;
拥有阻塞队列指针,指向当前处于阻塞太的进程,很多操作系统会因为阻塞原因不同而划分多个阻塞队列;
索引方式
和链接方式类似,拥有执行指针和阻塞队列指针
并且拥有就绪表指针,指向一张就绪索引表的地址
五大特征
特征 | 内容 |
---|---|
动态性 | 进程是程序的一次执行过程,是动态地产生、变化和消亡 |
并发性 | 内存中存在多个进程实体,可以并发执行 |
独立性 | 进程是能够独立地运行、获取资源、接受调度的基本单位 |
异步性 | 各个进程以独立的、不可预知的速度向前推进。操作系统需要提供“进程同步机制”来解决问题 |
结构性 | 每个进程都由PCB、结构段、数据段组成 |
其中,动态性是进程的最基本特征
三大状态
状态 | 内容 |
---|---|
运行态 | 占有CPU并且在CPU上运行 |
就绪态 | 已经具备运行条件,但是由于没有闲置CPU,暂时不能运行 |
阻塞态(等待态) | 因为等待某一件事暂时不能运行 |
以上是基本的状态
进程还能被细分为更多的状态,比如
其他状态 | 内容 |
---|---|
创建态 | 进程正在被创建,操作系统为进程分配资源、初始化PCB |
终止态 | 进程正在从系统中被撤销,操作系统将会回收资源、撤销PCB |
挂起态 | 还可以细分为就绪挂起态和阻塞挂起态,是在内存不够的时候,被调度(后面会说)到外存(硬盘;这是和阻塞的最大区别)暂时保存的状态 |
状态转换
还是比较好懂,大概内容如图所示
三态四转换模型
五态六转换模型
内存不够可以理解为内存中的就绪/阻塞队列满了,只能先放到外存
五态转换详细过程的如下
但是上图也看出了一个问题:
就绪态的任务在CPU修改PCB状态为执行之后,进入执行态,执行态开始请求,进入阻塞态,请求完成,阻塞态回归到就绪态——问题就出现了,这个时候,PCB的状态依旧是执行!
为了解决这个问题,利用了原语
原语的实现
具有原子性的操作,不允许中断
原语是通过关中断指令和开中断指令实现的:
在原语代码执行前,先执行关中断指令,之后代码执行不会引起中断;
在原语代码执行后,再执行开中断指令,此后都将执行中断处理;
进程的创建
有两种常见的进程创建方式:
发生引起创建进程的事件
用户登录、应用请求、作业调度和提供服务等
执行创建原语
1.申请空白PCB
2.为新进程分配所需资源
3.初始化PCB
4.将PCB插入就绪队列
进程的终止
也有两种常见的进程终止方式:
发生引起进程终止的事件
任务正常结束、异常结束或被外界干预
执行撤销原语
1.从PCB集合中找到终止进程的PCB
2.如果进程正在执行,则将其占有的CPU剥夺然后分配给其他进程
3.终止其所有子进程
4.将该进程拥有的所有资源归还给父进程或操作系统
5.删除其PCB
注意,撤销原语可以直接让进程变为终止态——无论该进程正处于何种状态
进程的阻塞
还是两种方式:
发生引起阻塞进程的事件
需要等待 系统分配资源 或 其他与之合作的进程完成工作
执行阻塞原语
1.找到阻塞进程对应的PCB
2.保护进程运行环境,将PCB信息修改为阻塞态,暂停该进程
3.将PCB插入相应事件的等待队列
进程的唤醒
依旧是两种方式:
发生引起唤醒进程的事件
等待结束
执行唤醒原语
1.在事件等待队列中找到PCB
2.将PCB从等待队列中移除
3.将PCB插入就绪队列,等待被调度
阻塞和唤醒其实就是等待和等待结束的意思
另外,阻塞和唤醒一定成双成对出现
进程的切换
还是两种:
发生引起切换进程的事件
1.当前时间片用完
2.有更高优先级的进程到达
3.当前进程主动阻塞
4.当前进程终止
执行切换原语
1.将运行环境存入当前进程的PCB
2.该PCB移入相应的队列
3.选择另一个进程执行,并更新其PCB
4.根据PCB中信息恢复进程需要的运行环境
其实无论哪种原语,其作用无非以下三点:
1.更新PCB信息
1.1所有进程控制原语一定会修改进程状态标志
1.2剥夺当前进程的CPU使用权必然要保存其运行环境
1.3某进程回复运行前必然要恢复其运行环境
2.将PCB插入合适的队列
3.分配/回收资源
由于进程是分配系统资源的基本单位,所以各个进程的内存地址相互独立
进程通信即进程之间的信息交换,但是为了保证安全性,一个进程不能直接访问其他进程的地址空间,所以进程通信会通过以下方式实现
方式1:共享存储
开辟一个共享空间,用来进程间传递信息
进程对这个空间的访问必须是互斥的,这由操作系统提供的工具实现(同步互斥工具,如P/V操作)
实现原理
两种…
类型 | 内容 |
---|---|
基于数据结构 | 低级通信方式,速度慢,限制多,每次只能传格式固定的某种数据结构,如长度为10的串 |
基于存储区 | 高级通信方式,速度快,在内存中开辟共享存储区,存放的位置由进程决定 |
方式2:管道通信
实现原理
“管道”指的是用于连接读写进程的一个共享文件,又叫pipe文件,其实就是在内存中开辟的一个大小固定的缓冲区
tips:
1.管道只能采用半双工通信(可以正向传输也可以逆向传输,但是同一时间只能朝一个方向传输;双向同时通信则需要两个管道)
2.各个进程对管道的访问是互斥的(哪怕是同向也一样)
3.其中,数据以字符流的形式传输,读写操作是用write()和read()程序接口(系统调用)实现的。
4.管道没写满,就不允许读;如果没读空,就不允许写(会被阻塞)
5.数据一旦被读出,就从管道中清除,无法再次使用(这意味着最多一个读进程,否则可能出现读取错误)
方式3:消息传递
进程之间的数据交互以格式化的信息为单位
通过操作系统提供的发送消息/接受消息两个原语进行数据交换
上述的**“格式化的信息”**指的是消息头+消息体
(类似于请求/响应的头/体)
名称 | 内容 |
---|---|
消息头 | 发送进程ID,接受进程ID,消息类型,消息长度等格式化信息 |
消息体 | 消息的内容等等 |
实现原理
类型 | 内容 |
---|---|
直接通信 | 消息会被直接挂到接受进程的缓冲队列上 |
间接通信 | 消息先发送到中间实体“信箱”,所以间接通信也称“信箱通信”(可以通过消息头的信息确定收发对象) |
背景
上古之世,未有进程之说,企鹅和企鹅音乐竟然无法同时运作!
先贤鬼斧神工以创进程,自此以后,企鹅和企鹅音乐才得以“同时”运作…
再后来,随着科技发展,企鹅作为一个进程却可以同时进行文字传输,视频播放,文件传输等等操作
单个进程是如何做到同时进行多项任务的?
众所周知,传统的进程只能串行(顺序)执行一系列程序(传统进程是程序执行流的最小单位),无法达到“同时”的效果
所以引入了线程的概念:
一个进程会包含多个线程,各个线程独立的、并发的运行,CPU轮流为每个线程服务。
这样一来,线程是程序执行流的基本单位,可以看做轻量级的进程
和进程类似,拥有TCB(线程控制块)等
和子进程的区别
1.线程是进程的一部分,一个没有线程的进程可以看做单线程
2.引入线程的概念后,进程是(除了CPU以外的)资源分配的基本单位,而线程是程序执行(调度)的基本单位。线程几乎没有系统资源。
3.进程有独立的地址空间,而多个线程总是共享地址空间和公共变量
影响和意义
1.同一个进程内的线程切换不需要切换进程环境,系统开销减小
2.线程可以并发,提高了并发度
方式1:用户级线程
User-Level Thread(ULT)
用户可以感知到多个线程,但是操作系统内核只能感知到进程
tips:
用户级线程是由 应用程序 通过 线程库 实现的
所有的线程管理都由应用程序负责
用户级线程中,线程切换可以在用户态下完成,无需操作系统干预
方式2:内核级线程(内核支持线程)
Kernel-Level Thread,KLT
即操作系统内核可以感知到的线程,线程切换需要在核心态下进行
将ULT和KLT混合使用(当然并不是每个操作系统都支持这么做)
将n个用户级线程映射到m个内核级线程上(n >= m)
这里,分配CPU资源的单位是内核级线程(毕竟内核只看得见它们啊…)
如上图所示的情况,哪怕是4核CPU,也只能并行两个用户级线程
混合线程的思路,引出了多线程模型
模型1:多对一模型
概述
多个用户级线程映射到一个内核级线程
优点
用户级线程的切换可以在用户态完成,无需切换核心态,开销小,效率高
缺点
当一个(内核级)线程被阻塞后,整个进程都会被阻塞,并发度低;
并且,多个线程不可以在多核CPU上并行。
模型2:一对一模型
概述
一个用户级线程映射到一个内核级线程
优点
一个(内核级)线程阻塞,不影响线程,并发性强;
可以在多核CPU上并行。
缺点
一个用户进程会占用多个内核级线程,并且切换操作需要在核心态完成,
线程的管理成本高,开销大。
概述
n个用户级线程映射到m个内核级线程(n>=m)
优/缺点
是模型1和模型2的中和,并发度居中,开销和管理成本也居中
在多道程序系统中,进程的数量总是多于CPU个数,不可能并行所有进程。
所以需要CPU调度,从就绪队列中按照一定的算法选择一个进程并将处理机分配给它运行,以实现进程的并发执行
高级调度(作业调度)
按照一定原则,从后备队列中的作业中筛选一个或多个作业,为之分配资源、建立PCB,使它们获得竞争处理机的权力
后备队列是因为有时候用户提交的作业过大,无法一次性全部放入内存,所以先放到处于外存的后备队列,等操作系统调度安排
高级调度是外存(辅存,硬盘)与内存之间的调度,每个作业只调入一次(此时为之建立PCB),调出一次(作业结束菜调出)
主要是解决调入的问题,由操作系统决定何时调入
tips:
作业
是一个比程序/进程更加宽泛的概念,不仅包含通常的程序和数据,还配有一份程序说明书,系统根据说明书对进程进行控制
中级调度(内存调度)
把暂时不能运行的进程调度到外存等待,直到其重新具备了运行条件才再次调入内存
与高级调度的区别是这个可以多次调入调出,把进程放到外存等待(此时状态变为挂起,PCB放到内存中的挂起队列),给其他进程空间
这样做提高了内存的利用率和系统吞吐量
低级调度(进程调度)
按照某种方法和策略,从就绪队列中选取一个进程,将处理机分配给它
进程调度是操作系统中最基本的一种调度,频率较高,几十毫秒便会执行一次
时机
进程调度即低级调度
什么时候需要进程调度
1.当前运行的进程主动放弃处理机
进程正常终止、运行过程中发生异常而终止、进程主动请求(阻塞)
2.当前运行的进程被动放弃处理机
该进程的时间片用完、由优先级更高的进程进入就绪队列、有更紧急事件需要处理
什么时候不能进程进程调度
(对于大部分操作系统而言)
1.处理中断的过程中(据说是因为中断处理很复杂,与硬件关联大…阿巴阿巴)
2.进程处于操作系统内核程序临界区(但是在普通临界区是可以进行的)
3.在原子操作中(比如原语),不可中断
方式
类型 | 内容 |
---|---|
非剥夺调度方式(非抢占方式) | 只允许进程主动放弃处理机。就算有优先级更高的任务到达,当前任务依旧会继续执行,知道该进程终止或者主动发出申请进入阻塞态 |
剥夺调度方式(抢占方式) | 当一个进程在处理机上执行时,有优先级更高的进程到达,当前进程立刻暂停,处理机将被分配给更优先级更高的进程 |
优劣
非剥夺调度方式(非抢占方式):
实现简单,系统开销小
但是无法实现紧急任务
适合早期的批处理系统
剥夺调度方式(抢占方式):
能优先处理紧急任务,也可以以时间片轮流执行
适合分时操作系统、实时操作系统
切换与过程
狭义的进程调度 与 进程切换 的区别
狭义的进程调度指的是,从就绪队列中选中一个要运行的进程这一行为。
进程切换是指,一个进程让出处理机,由另一个进程占用处理机的过程。
进程切换主要完成了
1.保存原来运行的进程中数据(保存到PCB)
2.恢复新的进程的数据(从PCB读取)
而广义的进程调度包含 选择一个进程 和 进程切换 两个步骤(这也是我们通常所说的进程调度)
tips:
进程切换是有代价的,需要消耗时间,过于频繁必将拉低效率
CPU利用率
即 忙碌时间/总时间
如某个作业需要在CPU上运行5s,再用打印机输出5s,之后再执行5s,那么利用率就是10/15
(这里打印机的利用率是5/15)
系统吞吐量
即 完成的作业量/总时间
比如完成10道作业,花费100秒,则系统吞吐量为
10/100 = 0.1
周转时间
即 作业提交到作业完成的用时
(站在用户的视角看待)
tip:
实际运行时间是指占用处理机的时间(不包括输入输出的时间)
带权周转时间
(作业完成时间 - 作业提交时间)/ 作业实际运行时间
即 周转时间 / 作业实际运行时间
对于周转事假相同的两个作业,实际运行时间长的作业在相同时间内被服务的时间更多,带权周转时间更小,用户满意度更高
对于实际运行时间相同的两个作业,周转时间短的带权周转时间更小,用户满意度更高
等待时间
进程/作业处于等待处理机状态时间之和。
对于进程来说,等待时间是指进程建立之后等待被服务的时间之和,但是等待I/O的期间也算是被服务,所以不计入等待时间
对于作业来说,还要加上作业在外存后备队列的等待时间
响应时间
指从用户提交请求到首次产生响应所用的时间
响应比
(等待时间+要求服务时间) / 要求服务时间
举个例子
进程 | 到达时间 | 运行时间 |
---|---|---|
P1 | 0 | 7 |
P2 | 2 | 4 |
0时刻:只有P1在就绪队列,P1占用处理机
7时刻:P1主动放弃处理机,就绪队列中有P2,其响应比是(5 + 4)/4=2.25
前置概念:饥饿
作业/进程长期得不到服务(意思是可能一直被插队而无法得到处理,不是说需要等待的时间较长)
先来先服务FCFS
First Come First Serve,先来先服务
属性 | 内容 |
---|---|
思想 | 公平(分先来后到) |
规则 | 按照作业/进程的先后顺序进行服务 |
用于作业/进程调度 | 用于作业调度:考虑哪个作业先到达后备队列;用于进程调度:考虑哪个进程先到达就绪队列 |
是否为抢占 | 非抢占 |
优缺点 | 优点:公平、简单;缺点:不利于排在长作业/进程后的短作业/进程用户体验 和 长作业/进程的运行 |
是否会产生饥饿现象 | 不会 |
短作业优先SJF
Shortest Job First,最先服务(服务时间)最短的
用于进程就是Shortest Process First
属性 | 内容 |
---|---|
思想 | 追求最少的平均等待时间、平均周转时间、平均带权周转时间 |
规则 | 从已经到达的作业/进程中,选择服务时间最短的优先服务 |
用于作业/进程调度 | 用于作业调度:考虑哪个作业先到达后备队列;用于进程调度:考虑哪个进程先到达就绪队列 |
是否为抢占 | 非抢占 |
优缺点 | 优点:“最短的”(不严谨地说)平均等待/周转时间;缺点:不利于厂作业,可能导致饥饿,难以做到真正的短作业优先 |
是否会产生饥饿现象 | 会 |
最短剩余时间优先SRTN
Shortest Remaining Time Next,是短作业优先算法SJF的抢占版本
高响应比优先HRRN
属性 | 内容 |
---|---|
思想 | 综合考量作业进程的等待时间和要求服务的时间 |
规则 | 每次调度优先考虑响应比最高的 |
用于作业/进程调度 | 同上 |
是否为抢占 | 非抢占 |
优缺点 | 优点:综合考量了…;缺点:(不明显) |
是否会产生饥饿现象 | 不会(随着等待时间变长,响应比也会越来越大,被调用的几率就越来越大) |
时间片轮转RR
Round-Robin
需要注意,如果一个任务剩余的时间小于一个时间片,那么它会在完成时主动放弃处理机,然后发生调度,其他就绪态的任务开始执行
这种调度方式常用于分时系统
属性 | 内容 |
---|---|
思想 | 公平 |
规则 | 轮流执行一个时间片 |
用于作业/进程调度 | 用于进程调度(只有作业放入内存建立相应的进程后才能被分配处理机时间片) |
是否为抢占 | 抢占(原理是中断) |
优缺点 | 优点:响应快、公平;缺点:高频的进程切换开销太大、公平 |
是否会产生饥饿现象 | 不会 |
优先级调度
优先级越高越先执行(注意优先级和优先数不一样,优先数还得看题目的定义)
tips:
1.优先级也有静态和动态两种,顾名思义
2.系统进程>用户进程,前台进程>后台进程
3.操作系统更偏好(优先处理)I/O型进程(也叫I/O繁忙型进程,与I/O相对的是计算型进程,也叫CPU繁忙型进程)。这样设计是因为I/O操作可以和CPU并行工作,如果让I/O繁忙的进程优先运行的话,那么就能让I/O设备尽早投入工作,资源利用率,系统吞吐量都会得到提升
| 属性 | 内容 |
|–|--|
| 思想 | 轻重缓急 |
| 规则 | 先执行优先级高的 |
| 用于作业/进程调度 | 用于进程和作业调度,甚至还会用于之后会提到的I/O调度 |
| 是否为抢占 | 两种都有 |
| 优缺点 | 优点:能分清轻重缓急;缺点:可能会有高频的切换和饥饿现象 |
| 是否会产生饥饿现象 | 会 |
那么问题来了
这么多算法,各有千秋但是也各有缺点,所以,能不能有一个集大成者出现呢?
多级反馈队列调度算法
属性 | 内容 |
---|---|
思想 | 其他算法的综合 |
规则 | 1.设置多级就绪队列,各级队列优先级从高到低,时间片从小到大 2.新进程到达时进入第一级队列,按照FCFS等待分配时间片;若时间片用完进程还未结束,那么进程进入下一级队列队尾(如果此时已经是最低一级的队列,那么重新放到本队列末尾) 3.只有第K级队列为空时,才会为K+1级队列的进程分配时间片 |
用于作业/进程调度 | 用于进程 |
是否为抢占 | 抢占 |
优缺点 | 优点:综合性强,适应多种情况 缺点:可能导致饥饿 |
是否会产生饥饿现象 | 会(短进程不断,并且在一个时间片内可以被处理完,那么已经被降级的进程就得不到处理) |
进程同步
众所周知,进程具有异步性。异步性是指,各并发执行的进程以自己独立的、不可预知的速度向前推进。
但是在读写进程的时候,写 一定要发生在 读 的前面,这种情况就需要解决异步性带来的问题——这便是进程同步要讨论的
进程同步 也叫 直接制约关系,它是为了完成某种任务而建立的两个或多个进程,这些进程因为需要在某些位置上协调它们的工作次序而产生的制约关系。进程间的直接制约关系就是源于它们之间的相互合作。
进程互斥
一个资源在同一时间内只能被一个进程使用
对临界区的互斥访问,从逻辑上可以分为以下四个部分:
do{
entry section; //进入区:负责检测
critical section; //临界区
exit section; //退出区
remainder section; //剩余区
}
区域 | 内容 |
---|---|
entry section进入区 | 负责检查是否可以进入临界区,若可进入,则应设置正在访问临界资源的标指(可以看作上锁),阻止其他进程进入临界区 |
critical section临界区 | 也叫临界段。就是访问临界资源的那段代码 |
exit section退出区 | “解锁” |
remainder section剩余区 | 做一些其他处理 |
tips:
进入区和退出区是 负责实现互斥 的代码段
遵循了四个原则:
原则 | 内容 |
---|---|
空闲让进 | 临界区空闲时,允许 一个请求进入临界区的进程 立刻 进入临界区 |
忙则等待 | 当已经有进程进入临界区的时候,其他试图进入这里的进程必须等待 |
有限等待 | 等待时间不能是无限的(不能饥饿) |
让权等待 | 当进程不能进入临界区时,应该立即释放处理机,防止进程忙等待 |
1.单标志法
两个进程在访问完临界区之后,把临界区的权限交给另一个进程。也就是每个进程进入临界区的权限只能被另一个进程授予
违背了空闲让进原则
2.双标志先检查法
设置一个布尔型数组(习惯性叫flag),数组中每个元素用来标记进程想进入临界区的意愿。比如flag[0] = true表示0号元素想要进入,每个进程进入临界区之前都要检查当前有没有别的进程想要进入临界区。
如果没有,那么就把自身对应的表示设为true
3.双标志后检查法
双标志先检查法的改版。前一个算法(先检查)的问题是,先“检查”后“上锁”,但是这两个操作无法一气呵成,因此导致了两个进程同时进入临界区。
所以机智的人类就提出了先上锁后检查的操作
然后又发现,这不是会导致两个都进不去吗(注意这是饥饿,不是死锁)
4.peterson算法
在双标志检查法的基础上,如果出现了几个进程争着进入临界区的情况,那么让进程变得“谦让”
方法是,依旧设置一个flag数组表示自身的意愿,但是额外地增加一个turn值表示愿意谦让哪个进程(比如turn=1,就是在发生冲突时,愿意把机会让给1号进程)
如果按①⑥②⑦⑧的顺序执行
P0谦让,然后P1不甘落后又谦让,并且执行⑧循环等待,所以最终P0先使用
1.中断屏蔽法
利用 开/关中断指令 实现(和原语一样,在某进程开始访问临界区到结束访问为止都不允许被中断,也就不能发生进程切换,因此也不可能发生两个进程同时访问临界区的情况)
优点:简单高效
缺点:不适用于多处理机;只适用于操作系统内核进程,不适用于用户进程(因为开/关中断指令,只能运行在内核态,这组指令如果能让用户随意使用会很危险)
2.TestAndSet指令
简称TS指令,也有叫做TestAndSetLock指令的,简称TSL指令
他们是通过硬件控制做到的,暂不细究
优点:实现简单;适用于多处理机环境;
缺点:不满足“让权等待”
Swap指令
也叫Exchange简称XCHG指令,同上,暂不细究
优点:实现简单;适用于多处理机环境;
缺点:不满足“让权等待”
分为两大类,整型信号量 和 记录型信号量
主要思想是,设置一个变量(即信号量),可以用这个量表示系统中某种资源的数量
常用到一对原语:wait(S)和signal(S),分别简称为P、V(来自荷兰语),操作其中S是信号量
整形信号量
只支持三种操作:初始化、p、v
注意这里的while只是为了方便理解,实际上当资源不够的时候,进程会被挂起而中断
优点:检查和上锁一气呵成,避免了并发异步导致的问题
缺点:不满足让权等待原则,会发生忙等
记录型信号量
即在整型信号量的基础上,用记录型数据结构表示的信号量
至于signal那里,
由于wait保证了,在没资源的情况下,进程会进入阻塞队列(block),
所以
value为正数的时候表示空闲资源数量,
为负数的时候其绝对值表示阻塞队列中进程的数量
生产者消费者问题
系统中有一组生产者进程和一组消费者进程,生产者进程每次生产一组数据放入缓冲区,消费者每次从缓冲区拿出一组数据使用。
生产者消费者共用一个初始为空、大小为n的缓冲区。
只有当缓冲区不满的时候,生产者才能生产,否则进入阻塞直到收到缓冲区不满的信号才被唤醒,重新进入就绪队列;
消费者只有当缓冲区不为空的时候,才能拿出使用,否则阻塞知道收到缓冲区不为空的信号才被唤醒,重新进入就绪队列;
规则很简单,但是存在问题:
如果并发(或者并行)环境下,有多个生产者,它们几乎同时检测了缓冲区,都发现还没满,结果两个都试图写入,结果其中一个生产者写入之后就已经满了,其他的生产者就写入失败发生写入错误
解决这个问题之前,我们先要分析
1.缓冲区是临界资源,需要互斥访问
2.缓冲区满的时候,消费者先取,生产者后放,是一种同步关系;缓冲区为空的情况同理
我们再来分析一手:
互斥(其实就是先V后P):
1.盘子相当于缓冲区,是临界资源,需要互斥访问
同步:
2.父亲放苹果后,女儿才能取苹果,是同步关系。母亲和儿子同理。
3.只有盘子为空,父母才能放,也是同步关系。
不会怎么样。因为缓冲区大小为1,并且存在三个同步信号量,这三个同步信号量中同一时刻最多有一个为1。因此在任何时刻,最多只有一个进程的P操作不会被阻塞,从而顺利进入临界区
(但是还是设置一个吧,保险
分析技巧
其实是站在缓冲区的角度看问题,而不是站在进程上看问题
这里桌子是缓冲区,但是注意容量应该是1!桌上摆了两个东西但是不能看作是2,应该 把两个东西看做捆绑出现的一种组合!
所以有三种组合:纸+胶水、烟草+胶水、烟草+纸
互斥:
1.桌上只能摆放一种组合,需要互斥访问
同步:
1.桌上有组合一,那么第一个抽烟者取走
2.桌上有组合二,那么第二个抽烟者取走
3.桌上有组合三,那么第三个抽烟者取走
4.抽烟完毕,供应者提供下一组材料
另外,还需要设置一个变量来使得三个抽烟者轮流抽烟(如果是随机给材料的情况那么就不用了)
伪代码如下(建议把P(finish)放到前面去,这样更符合表达)
读写(带计数器的互斥)问题
这个问题我也深有体会,在CSDN写博客的时候,如果对同一个文字打开多个编辑窗口,分别写入不同的内容发布,最后以后者的内容为最终内容。
分析一手:
由于这里出现了两种类型的进程,所以不能够再从缓冲区出发了
互斥:
1.写进程和写进程互斥
2.写进程和读进程互斥
初步设想
由于会出现无法一气呵成的情况,count还没自增就一起进去了,所以我们发现读进程之间还是得互斥的来
(3.读进程也得先后读取,主要是先后上锁)
但是我们还忽略了一个问题就是,写进程优先级比读进程高,那么从实际角度来说,应该是内容更新更为重要,而这种可能导致写进程饥饿(因为最后一个读进程负责解锁,所以读进程可以一直插队到写进程前)的算法不太好,所以我们进一步优化为:
哲学家进餐问题
上述伪代码的问题是,由于异步和并发,可能出现,每个人依次拿起自己左边的筷子,然后就出现死锁,所有人都没法进行下一步于是全体尬住乐
所以我们还得先分析一手:
互斥:
1.每只筷子都是临界资源,要互斥访问
同步:
1.需要组织一定的先后顺序,比如设置一个初值为4的同步信号量 或者 让每个人互斥地取筷子
在管程出现之前,使用信号量机制较为普遍,但是这样很容易出错
管程是一种特殊的软件模块,属于一种高级的同步机制,由
1.共享数据结构
2.初始化数据结构的语句
3.一组用来访问数据结构的过程(函数)
如果感觉不容易理解,那么可以把管程看做是PV操作的封装,这样一来直接调用就不容易出错
管程的基本特征
1.外部进程/线程只能通过管程提供的特定入口才能访问共享数据
2.每次仅允许一个进程在管程内执行某个内部过程
补充
各个进程必须互斥访问管程的特性,是由编译器实现的
可以在管程设计条件变量及等待/环形操作来解决同步问题
并发环境下,各进程竞争资源而造成的一种互相等待其他进程占有的资源,导致各个进程都阻塞、都无法进一步推进的现象
死锁、饥饿、死循环的区别
先来看看各个名词的定义
名称 | 定义 |
---|---|
死锁 | 各个进程互相等待对方手里的资源,导致各个进程阻塞、都无法推进 |
饥饿 | 由于长期得不到想要的资源,某个进程无法向前推进 |
死循环 | 某个进程一直执行,没有跳出某个循环(有时是bug,有时是有意为之) |
再来分析一下区别
名称 | 区别(特点) |
---|---|
死锁 | 1.两个或者两个以上的进程 2.一定处于阻塞态 |
饥饿 | 1.不限数目 2.可能是 阻塞态 或 就绪态 |
死循环 | 1.不限数目 2.前两个是管理者(OS)的问题,死循环是被管理者的逻辑问题引发的(除非有意为之) 3.是运行态 |
死锁的四个必要条件
死锁发生必须同时满足四个条件:
条件 | 内容 |
---|---|
互斥条件 | 对必须互斥使用的资源的争抢会导致死锁 |
不剥夺条件 | 进程保持的资源只能主动释放,不能由其他进程强行夺走 |
请求和保持 | 保持某些资源不放的同时,请求别的资源 |
循环等待 | 存在进程资源的循环等待链 |
这里需要注意
死锁一定有循环等待,但是循环等待未必是死锁
即 循环等待是死锁的必要不充分条件
如果同类资源数大于1,即使循环等待,也未必死锁;
但是如果系统中每类资源都只有一个,那么循环等待就是死锁的充分必要条件了
死锁发生的时候
概括就是资源分配不合理时就可能导致死锁,主要是以下三种情况:
1.对系统资源的竞争
2.进程推进顺序非法
3.信号量使用不当
核心思想是破坏死锁产生的四个必要条件中的一个或者几个
破坏互斥条件
如果把只能互斥使用的资源改造为允许共享使用,则系统不会进入死锁状态。比如:SPOOLing技术。
SPOOLing技术可以把独占设备在逻辑上改造为共享设备
这里由于涉及的硬件知识过多,所以我们不用深究该技术原理,只需要明白其过程:
比如进程1和进程2同时访问打印机,在使用SPOOLing之前会发生阻塞;使用该技术之后,宏观上看就不会阻塞,两个请求被“同时”接受了(或许是类似并发的技术吧)
缺点
1.并不是所有资源都能被改造为共享资源。
2.并且为了系统安全,很多地方还必须保护这种互斥性。因此,很多时候都无法破坏互斥条件。
破坏不剥夺条件
我们有几种方案可供选择
方案一
当某个进程请求新的资源得不到满足,那使它立即释放保持的资源,待以后需要时再重新申请。
方案二
大概某个进程需要的资源被其他进程所占有的时候,可以由操作系统协助,将想要的资源强行剥夺。这种方式一般要考虑各个进程的优先级。(比如剥夺调度方式,就是将处理及资源强行剥夺给优先级更高的进程使用)
缺点
1.实现起来比较复杂
2.释放已经获得的资源可能导致前一阶段的工作失效。因此这种方法一般只适用于容易保存和恢复的资源,比如CPU
3.反复地申请和释放资源会增加系统开销,降低系统吞吐量。
4.如果采用方案一,意味着只要暂时得不到某个资源,之前获得的那些资源就需要放弃,以后再重新申请。如果一直发生这种情况,就会发生进程饥饿。
嘶。看完第4点才意识到,原来饥饿中提到的“得不到处理”是指处理完毕!
破坏请求和保持
采用静态分配法,即进程在运行前一次申请完它所需要的全部资源,在它资源未满足之前不开始运行。一旦请求完毕,这些资源一直归它所有,那么它就不会再请求资源,也就不会发生死锁。
如果需要的资源都有,那它不争不抢(感觉也破坏了互斥条件),当然不会死锁呀~
缺点
虽然简单,但是也有很明显的缺点呀:
1.如果整个运行期间都一直保持所有资源,就会造成严重的资源浪费而且利用率可能很低
2.可能导致其他进程饥饿
(比如有A类进程需要资源1,B类进程需要资源2,C类进程需要资源1和资源2。如果由源源不断的A或B进程,那么C就会饥饿)
破坏循环等待
说实话这个看上去很简单,但是我还是看得有点懵…
采用顺序资源分配法,首先给系统资源编号,规定每个进程必须按编号递增的顺序请求资源,同类资源(即编号相同的资源)一次申请完毕
原理
一个进程只有已经占有小编号的资源时,才有资格申请更大编号的资源。
按照这种规则,已经持有大编号的进程不可能逆向地申请小编号的资源,从而就不会产生循环等待现象。(懵逼,这说的啥看不懂啊(#°Д°))
缺点
1.不方便增加新的设备,因为可能需要重新分配所有编号
2.一个进程实际使用资源的顺序可能和编号递增顺序不一致,这样会导致资源浪费
3.必须按照规定次序申请资源,用户编程麻烦((@_@;)这又是为啥啊)
用某种方法防止系统进入不安全的状态,比如银行家算法
安全序列
先看图
提炼一下:
安全序列
如果按照这种序列分配资源,那么每个进程都能顺利完成。安全序列可能有多个
不安全状态
如果没有任何安全序列,那么就进入不安全状态。不安全状态可能会因为进程资源的提前归还,而重新进入安全状态
(死锁一定发生在不安全状态,但是不安全状态不一定死锁)
安全状态
在安全状态下一定不会发生死锁
上述内容可以得出
银行家算法
没错,这个算法又是DJ老哥提出的,一开始真的是为了解决银行贷款时避免资金不够的情况。
在资源分配之前,预先判断这次分配是否会导致系统进入不安全状态,以此来决定是否分执行这次分配
此后继续检测(不检测已经加入安全序列的进程)
通俗地说就是,如果它我分配了之后它能够顺利完成,它就会归还——那就分配,然后把它加入安全序列,之后继续。
(类似贪心?)
考试中用笔算,注意技巧:如果现有资源可以分别满足几个进程,那么直接把它们全部扔进安全序列(反正可以先后完成)
至于计算机怎么实现…这个对于数学不好的人(比如我)简直劝退
这一部分有兴趣了解一下就行…
允许死锁发生,不过操作系统会检测出死锁,然后采取某种措施解除死锁
当然,这是 死锁预防措施 或 死锁避免措施 没有被系统启用(或者失效了)的情况下的处理方式
两个算法
1.死锁检测算法
用于检测系统状态,以确定系统中是否发生了死锁
方式
1.用某种数据结构(资源分配图)来保存资源的请求和分配信息
2.提供一种算法,利用上述信息来检测系统是否进入了死锁
(注意,分配边表示已经分配了多少)
对于上图中P1 P2 R1 R2构成的资源分配图
首先看P1,因为P1的请求可以满足,那么和P1相连的边可以全部移除
然后看P2,也是同理,与之相连的边可以全部移除
这样一来所有的边都被移除了,这种资源分配图就是可完全简化的,这说明这是安全的,没有发生死锁;否则就是已经发生了死锁,而最终还有相连边的进程就是发生死锁的进程
其实上述内容就是
死锁定理
如果某时刻资源分配图是不可完全简化的,那么此时系统处于死锁
2.死锁解除算法
发生死锁时,解除死锁。有几种方法:
1.资源剥夺法
挂起某些死锁进程,抢占其资源,并且将其资源分配给其他资源。但是要防止被挂起的进程发生饥饿。
2.撤销进程法
也叫终止进程法。强制撤销部分甚至全部死锁进程,并剥夺这些进程的资源。(这种方式简单粗暴,但是代价可能巨大,比如一个进程运行很久已经快结束了结构又被撤销直接白干了)
3.进程回退法
让一个或多个死锁进程回退到足以避免死锁的地步
(这需要系统记录进程的历史信息,设置还原点)
有了方法,现在还要讨论对哪个进程出手
我们一般从一下几个方面进行考虑:
1.进程优先级(干掉优先级低的)
2.已经执行时间(干掉执行时间少的)
3.剩余执行时间(干掉剩余时间多的)(和上面那个有点矛盾)
4.已经使用多少资源(先干掉资源用的多的)
5.交互式进程还是批处理式进程(先干掉批处理进程,让用户感受好些)
…
内存是用于存放数据的硬件。程序执行前需要先放到内存中才能被CPU处理。
内存单元
即每一个内存地址对应的存储空间,至于存储空间的大小,分两种情况:
1.按字节编址
每个内存单元大小为1字节(byte,b)
2.按字长编址
每个内存单元大小为1字(每个字大小是2字节)
冷知识
1K = 2^10
1M = 2^20,
1G = 2^30
所以4G内存表示,内存中可以存储4*230字节——如果是按字节编址,也就有4*2030个地址,即2^32个地址
逻辑地址 与 物理地址
其实就相当于直接路径和相对路径这样的,很好理解
比如实际的地址(物理地址)是666,把666当成0,那么667就是1
编辑、编译、链接 和 装入
编辑:高级语言写代码
编译:高级语言编译为(机器语言)多个目标模块,分散在多个逻辑地址段中
链接:多个模块组装成一个装入模块,有一个完整的逻辑地址段
装入:把装入模块装入内存中,有完整的物理地址
首先是三种装入:
绝对装入
在编译时,如果知道程序放到内存中的哪个位置,编译程序将产生绝对地址的目标代码。装入程序按照装入模块中的地址,将程序和数据装入内存。
概述为:编译时产生绝对地址
比如你知道大小为装入模块要从物理地址为100的地方开始存放,那就直接装入100
只适用于单道程序环境,且通常是编译或汇编环节给出绝对地址(其实也可以由程序给)
静态重定位
又称可重定位装入,装入模块中的地址是逻辑地址,直至真正装入时逻辑地址换为物理地址。程序运行期间无法移动。
概述为:装入时将逻辑地址转为物理地址。
这是早期多道批处理操作系统采用的
动态重定位
又称动态运行时装入。编译、链接后的装入模块的逻辑地址都是从0开始的。装入程序(负责把装入模块装入的程序)把装入模块装入内存之后,并不会把逻辑地址转为物理地址,而是把地址转换推迟到程序真正要执行时才进行。因此,装入内存后所有的地址依然是逻辑地址。这种方式需要一个重定位寄存器的支持(进行的操作是物理地址+目标逻辑地址)。
概述为:运行时将逻辑地址转为物理地址
通过重定位寄存器,可以将程序分配到不连续的内存中(意思是每段只有部分的程序代码,这也是虚拟的体现,这样使用户能够使用一个比存储空间大得多的地址空间)
这也是现代操作系统普遍采用的
再来看看三种链接:
静态链接
在程序运行之前,将各目标模块及它们所需要的库函数连接成一个完整的可执行文件(也就是装入模块),之后不再拆分。
装入时动态链接
将各目标模块装入内存时,边装入边链接。
运行时动态链接
在程序执行中需要该目标模块时,才对他进行链接。其优点时便于修改和更新,便于实现对目标模块的共享。
管理内容大概分为以下3个方面:
1.内存空间的分配与回收
2.从逻辑上对内存空间进行扩充
3.地址转换(逻辑地址和物理地址的转换)
4.内存保护
专门说一下
内存保护
只允许各个进程访问自己拥有的内存,保证各个进程互不干扰
方式一
cpu中有上下限寄存器,存放进程的上下限地址,进程访问地址时,cpu据此检测是否越界
方式二
采用重定位寄存器(也叫基址寄存器) 和 界地址寄存器(也叫限长寄存器),分别存放起始物理地址和最大逻辑地址,同样检测是否越界
早期计算机额内存很小,比如IBM的第一台PC机器,最大就1MB内存。所以得想办法解决一下内存过小从而导致程序无法顺利运行的情况——覆盖技术因此诞生
覆盖
思想是将程序分为多个段(多个模块)
常用的段常驻内存,不常用的段在需要时调入内存
这样就使得1mb的内存可以运行2mb甚至更大程序!
这其中,内存被划分为两个区域
区域名称 | 数量 | 简介 |
---|---|---|
固定区 | 1个 | 常驻内存的段就被放入这里,在运行结束之前不会调出 |
覆盖区 | 若干个 | 选取可能放入这个区域中最大的段的内存 |
橙色是常驻区,BC是不会被同时调用的两个段,分别是D、EF的前驱
这种技术
优点:由程序员声明覆盖结构,操作系统自动完成覆盖
缺点:对用户不透明,增加了编程负担
交换技术
思想是内存空间紧张时,系统将内存中某些进程暂时换出外存,把外存中某系已经具备运行条件的进程换入内存(进程在内存与外存间动态调度)
没错,就是挂起与就绪之间的转换的中级调度(内存调度)
1.至于如何交换呢?
一般来说,具有交换功能的操作系统中,通常把磁盘空间分为 文件区 和 对换区两个部分。
文件区主要用于存放文件,追求存储空间的利用率,因此对文件区的空间管理采用离散匹配方式;
对换区只占很小的空间,被换出的进程数据就存放在对换区。由于对换的速度直接影响到系统的整体速度,因此通常对换区采用连续分配方式。
(总之,对换区的I/O速度比文件区要快)
2.那么什么时候发生交换?
交换通常在内存紧张时进行,在内存充裕时暂停。比如在许多进程运行时发生缺页,就说明内存紧张,此时可以换出一些进程。
3.换出哪些进程?
优先换出阻塞进程。可以换出优先级低的进程;为了防止优先级低的进程在被调入内存后很快又被换出,有的系统还会考虑进程在内存的驻留时间。(注意PCB常驻内存)
由于时间紧迫,这里开始先水一点了,后面再细细地补上
连续分配
指为用户进程分配一个连续的内存空间
固定分区分配
把整个内存的用户区域(对换区)分为n个大小固定的分区
动态分区分配
动态分区分配又称为可变分区分配,在进程装入内存时,根据进程动态地建立分区
动态分区分配算法
这个算法是为了解决一个问题:在动态分配过程中,当很多歌空闲分区都能满足需求时,应该选择哪个分区进行分配?
这里举例四种动态分区分配算法:
1.首次适应算法
每次从低地址开始查找,找到第一个满足的空闲分区
查找方式是通过空闲分区链或者空闲分区表来查找的(其实就是空闲的内存的首地址和大小等信息会以顺序或者链式存储,可以据此遍历)
2.最佳适应算法
和首次适应算法类似,不过存储空闲分区地址的数据结构将以递增的形式存储,所以空间越大的分区排在越后面
这样一来每次都能找到大小最接近的,使得内存利用率很高
可是问题也很明显,每次使用,都可能会留下非常小的空间(比如10mb的程序占用了11mb内存,空闲的1mb难以利用),这种空间叫做内部碎片
内部碎片多了之后,利用率又下来了
3.最坏适应算法
和最佳适应算法相反,最坏适应算法存储是递减的
主要是想着防止小碎片产生,那我就每次用最大的空间不就好了?(10mb程序占用100mb内存,还有90mb这不是很容易用出去吗)
但是问题是,可能导致后面来的大进程没有空间
4.邻近适应算法
递增存储并且是环形结构,每次查找从上一次查找结束的位置开始继续查找
上一节中提到的算法,虽然各有千秋,但是无一例外地都可能产生碎片
(虽然可以使用紧凑技术来解决碎片问题,但是其时间开销太大)
所以出现了基本分页存储管理
就是把内存分为一个个大小相等的小分区,再按照分区大小把进程拆分为一个个小部分
每一个分区就是一个“页框”,或者叫“页帧”、“内存块”、“物理块”,每个页框都有一个编号,即“页框号”(或者叫“内存块号”之类的),从0开始
tips:内存可能不能整除页框大小,所以最后一个页面可能没有一个页框那么大,因此页框不能太大,否则可能产生较大的内部碎片
装入
类似于一个程序的链接装入过程
其中涉及到一些关于 页号 和 页内偏移量的计算
页号 = 逻辑地址 / 页面长度
页内偏移量 = 逻辑地址 % 页面长度
另外,为了方便计算页号、页内偏移量,页面大小一般用2的整数幂
页表
为了能够知道进程中每个页面在内存中存放的位置,操作系统要为每个进程建立一张页表
(完了卧槽啊,吃个饭回来发现电脑重启了,一下午的笔记就没了,7k字一个字一个字码上去的啊,淦哦,缺失的内容先简单写一下了,心态有点炸)
基本概念
核心思想是把进程分成很多大小相等的段,内存也分为与进程等大的段,然后前者叫做页面,后者叫页框,它们之间的对应关系用页表来存,页表也放在页框中
1.页面:简称页,也就是进程的分段
2.页框:大小和页面相同,也叫内存块的(有一堆名字),是内存的分段
3.页表:记录了页面和页框的对应关系,其中每一项叫做页表项
4.页号:页表中某个页面的编号,其值为 逻辑地址 / 页面大小
5.页面偏移量:即 逻辑地址位数 % 页面大小(页面长度)的值
6.逻辑地址构成 = 页号 + 页内偏移量
(比如30位的逻辑地址 = 10位页号 + 20位页内偏移量;
如果有m位页号,那么说明一个进程中最多有m个页面;
如果有k位页内偏移量,那么一个页面的大小是2k个内存单元)
7.页面大小:就是一个页面的大小,是2的整数幂
基本地址变换机构
基本地址变换机构通过页表将逻辑地址转为物理地址
通常会在系统中设置一个页表寄存器(PTR),存放在内存中的起始地址F和页表长度M。地址变换的过程如下:
1.进程未执行时,页表的 起始地址F 和 页表长度M 放在进程控制块PCB中,当进程被调度时候,操作系统内核将F和M放到寄存器中
2.根据页表中记录的逻辑地址计算出 页号 和 页内偏移量
3.与PTR里面的数据进行比较,如果页号大于等于页表长度,那么发生越界,内中断;如果小于,那么判定合法,进入下一步。
4.根据页号查询页表,找到对应的页框号
5.用页框号和页内偏移量得到物理地址
6.访问目标单元
这个过程中,访问了两次内存
(访问内存开销比较大,所以减少访问内存的次数可能会得到优化)
带有快表的地址变换机构
局部性原理
先看如下代码:
int i = 0;
int arr[100];
while(i < 100)
{
arr[i ++] = i;
}
时间局部性
如果执行了程序中某条指令,那么不久后这条指令可能被再次执行;
如果某个数据被访问过,那么不久后该数据可能再次被访问;
空间局部性
如果某个存储单元被访问,那么不久之后其附近的存储单元也可能被访问。(比如很多具有连续存储这一性质的数据)
至于为什么叫局部
我个人的理解是,限制在某一个时间段,限制在某一个空间内,这就是局部。
有什么意义
其实上述内容暗示我们,一个较短的时间段内,进程只有一部分被访问了。这就说明我们每次只需要访问一部分就能运行一个进程,那么就没有必要一次性存储全部的内容。
我们上面提到过,访问内存开销大,那么我们尽量减少访问内存的次数。
这样重复的访问内存,却只是找同样的东西,那么我们能不能把这个东西保存到内存外面,下次访问就不需要从内存里面找了?
快表
又称联想寄存器(TLB) 或者 地址变换高速缓存,是一种访问速度比内存快很多的高速缓冲存储器,用来存放当前访问过(当前指的是当前运行的进程)的若干页表。
与之对应的慢表通常指的是内存中的页表
在加入快表后,地址变换过程变为:
(黑色斜体字表示新加入的步骤)
1.进程未执行时,页表的 起始地址F 和 页表长度M 放在进程控制块PCB中,当进程被调度时候,操作系统内核将F和M放到寄存器中
2.根据页表中记录的逻辑地址计算出 页号 和 页内偏移量
3.与PTR里面的数据进行比较,如果页号大于等于页表长度,那么发生越界,内中断;如果小于,那么判定合法,进入下一步。
3.查询快表,如果查询成功(命中),就进入第5步;失败则进入第4步
4.根据页号查询页表,找到对应的页框号,并把该页表项中的页号和页框号放入快表中
5.用页框号和页内偏移量得到物理地址
6.访问目标单元
上述过程中,如果第3步命中,那么地址变换就只需要访问一次内存
可以根据以下例题感受效率的变化
tips:
至于第3步为什么会有查询失败的情况,有两种可能:
1.第一次访问某个页号,快表中没有存储(快表一开始是空的)
2.快表满了,装不下全部数据,所以就出现访问过也查找失败的情况(因为快表造价要贵点,空间比较小。不)(然要主存干啥,干脆全用快表了)
其实主要还是二级页表…多级同理…
单级页表存在的问题
首先看一个计算
首先是告诉我们这个系统支持32位逻辑地址,然后页面大小是4KB = 212位,结合前面的知识(逻辑地址位数 = 页面大小位数 +
页号位数),我们知道页号位数 = 32 - 12 = 20位,
然后页号就是页面的编号嘛,所以有220个页面,一个页面在页表中都有一个对应的页表项,那么就有220个页表项,题目告知一个页表项大小为4B,那么整个页表的大小就是222B。
这时候,不要忘了页框和页面等大,所以一个页框也是4KB = 212B,于是乎,页框个数就是 222/ 212 =
210个 ,也就意味着,要分配1024个连续的页框来存储页表,这将是不小的开销
所以存在两个问题:
1.页表必须连续存放,当页表很大时,存放起来比较困难(最起码开销很大吧)
2.由于存在局部性原理,所以让整个页表常驻内存并不明智
为了解决问题1,大佬们提出
二级页表
将页表分组,使得每一个内存块正好可以放入一个分组(比如页面大小4KB,页表项4B,每个页框可以存放1K个页表项,所以每1K个页表项分为一组,然后离散地存入各个页框)
此后,为了记录这些离散存储的页表组,我们再建一张页表,称为页目录表,或者外层页表、顶层页表等等
需要注意的几个细节:
让我鹦鹉学舌来一手(主要是为了巩固,加深印象)
40位逻辑地址就是240B,而一个页面是4KB = 212B,那么可以容纳228个页面,也就是有228个页号,因为按字节编制,所以页号有28位。
页面和页框等大,都是212B,一个页表项是4B,那么一个页面可以容纳210个页表项,因此一个页表项对应的页号是10位。
由于各级页表的大小不能超过一个页面的容量,所以一级最大就是10位,那么对于28位的页号来说就是10 + 10 + 8三级页表才能完成存储
但是n级页表的缺陷也很明显:
(不考虑快表)需要访问n+1次内存
至于问题2,后面细说,这里先大概了解
大佬们提出了虚拟存储技术,就是在内存只放一部分,然后访问的时候如果内存中没有那么就产生缺页中断(内中断),从外存中调进来就可以了
与分页存储的最大区别就是离散分配时,所分配的地址空间的基本单位不同:
进程的地址空间指的是,按照程序自身的逻辑关系划分为若干个段,每个段都有一个段名(在低级语言中,程序员使用段名来编程),每段从0开始编址。
段成为内存分配的单位,每个段在内存中占据连续的空间,但是各段之间可以不相邻。
(这里先暂停一下,十月底再更新,先学算法去了)