操作系统
一、 操作系统基本概念
1. 操作系统的概念
操作系统(Operating System,OS)是指控制和管理整个计算机的硬件和软件资源(OS是系统资源的管理者),并合理地组织调度计算机的工作和资源的分配;以提供给用户和其他软件方便的接口和环境(向上层提供方便易用的服务);它是计算机系统中最基本的系统软件(最接近硬件的一层软件)。
操作系统的功能:
- 文件管理。
- 存储管理。执行一个程序需要将它放入内存中
- CPU管理。程序的运行就是相应的进程被CPU处理。
- 设备管理。
操作系统的目标是向上层提供更方便易用的服务。(类似于封装的思想,将一些硬件功能封装成简单易用的服务,是用户能封方便地使用计算机,用户无需关心底层硬件的原理,只需要对OS发出命令即可)
- GUI图形化用户接口。用户使用图形界面进行操作系统。
- 操作系统提供的命令接口
- 联机命令接口(交互式命令接口)。例如终端
- 脱机命令接口(批处理命令接口)。例如.sh,是一系列命令的综合。
- 程序接口。可以在程序中进行系统调用的程序接口。普通用户无法使用,只能通过代码间接使用。例如Java输出“Hello,World”时,底层就使用操作系统提供的系统调用,通过系统调用操作硬件。(系统调用类似于函数调用,是应用程序请求操作系统服务的唯一方式)
没有安装任何软件支持的计算机称为裸机。在裸机上安装的操作系统,可以提供资源管理功能和方便用户的服务功能。安装了OS的机器成为扩展机器(extended machine)/虚拟机(virtual machine)。操作系统实现了对硬件的拓展:将CPU、内存、磁盘、显示器、键盘等硬件合理地组织起来,让硬件能够互相协调配合,实现更多的复杂功能
2. 操作系统的特征
- 并发性
并发:指两个或多个事件在同一时间间隔内发生。这些事件宏观上是同时发生的,但微观上是交替发生的。
并行:指两个或多个事件在同一时刻发生。
操作系统的并发性指计算机系统中“同时”运行着多个程序,这些程序宏观上看是同时运行着的,而微观上看是交替运行的。
- 单核CPU:同一时刻只能执行一个程序,各个程序只能并发执行
- 多核CPU:同一时刻可以执行多个程序,多个程序可以并行地执行
操作系统是伴随着“多道程序技术”一起诞生的。
- 共享性
共享即资源共享,指系统中资源可供内存中多个并发执行的进程共同使用。它分为两种资源共享方式:
- 互斥共享。一个时间段只允许一个进程访问该资源
- 同时共享。一个时间段允许多个进程“同时”访问该资源。所谓的“同时”往往是宏观上的,而在微观上,这些进程可能是交替地对该资源进行访问的(即分时共享)
并发性和共享性互为存在条件。并发性指系统同时运行多个程序,如果失去并发性,则系统中只有一个程序正在运行,则共享性失去存在的意义。共享性指系统中的资源被多个并发的进行共同使用,如果失去共享性,不同程序不能同时访问资源,就无法实现同时发送文件,也就无法并发
- 虚拟性
虚拟是指一个物理实体映射为若干个对应的逻辑实体--分时或分空间。
- 分时技术。例如CPU按时按分给不同进程
- 分空间。例如内存安地址分给不同资源
- 异步性
异步是指进程的执行顺序和执行时间的不确定性。由于操作系统的并发性,进程的运行速度不可预知,多个进程并发执行,时走时停,不可预知每个讲程运行推讲快慢
3. 操作系统的分类
- 手工操作阶段。这一阶段,用户既是程序员,又是操作员,是计算机专业人员,编程语言
为机器语言,输入输出设备为纸带或卡片。用户独占所有资源 - 早起批处理
- 联机批处理。联机是指1/O设备与主机直接连接方式,用户输入一批作业到输入设备,主机处理后,数据成批输出。
- 脱机批处理。增加一台不与主机直接相连而专门于IO设备交换信息的卫星机
“批处理”指的是输入输出的方式,而“单道程序”和“多道程序”指的是外设与主机之间的数据传输方式。
- 多道批处理系统
- 单道批处理系统:每次只调一个用户作业程序进入内存并运行。一道程序从运行到结束,独占所有的资源。
- 多道批处理系统:每次可调多个作业程序进入内存并运行。宏观上并行:进入系统的几道程序均在运
行。微观上串行:轮流使用CPU。
-
分时操作系统
计算机以时间片为单位轮流为各个用户/作业服务,各个用户可通过终端与计算机进行交互。
主要优点:请求可以被即时响应,解决了人机交互问题。允许多个用户同时使用一台计算机,并且用户对计算机的操作相互独立,感受不到别人的存在
缺点:不能优先处理一些紧急任务。 -
实时操作系统
实时操作系统能接受外部信号后及时处理,并且要求在规定时限内处理完成事件,它的特点是及时性和可靠性。
有点:能够优先相应一些紧急任务,某些紧急任务不需要时间片排队
4. 操作系统的运行机制
一条高级语言的代码翻译过来可能会对应多条机器指令,CPU程序运行的过程其实就是CPU执行一条一条的机器指令的过程。
应用程序与内核程序
普通编写的是应用程序,而编写操作系统写的是“内核程序”,由很多内核程序组成了“操作系统内核”,或简称内核(Kernel)
,也就是操作系统最重要最核心的部分,也是最接近硬件的部分,用于管理计算机。
特权指令
操作系统内核作为“管理者”,会设置“特权指令”,如:内存清零指令。这些指令影响重大,只允许操作系统内核来使用。在CPU设计和生产的时候就划分了特权指令和非特权指令,因此CPU执行一条指令前就能判断出其类型。
如何只让内核运行特权指令?
CPU有两种状态,内核态和用户态
- 处于内核态时,说明此时正在运行的是内核程序,此时可以执行特权指令
- 处于用户态时,说明此时正在运行的是应用程序,此时只能执行非特权指令
CPU程序状态字寄存器(PSW)区分来年两种功能状态。(其中有个二进制位,1表示“内核态”,0表示“用户态”)
内核态和用户态的切换?
- 内核态➡️用户态:执行一条特权指令,修改PSW的标志位“用户态”,这个动作意味着操作系统
将主动让出CPU使用权 - 用户态➡️内核态:由“中断”引发,硬件自动完成内核态,触发中断信号意味着操作系统将强行夺
回CPU的使用权
5. 中断和异常
中断是让操作系统内核夺回CPU使用权的唯一途径
中断的类型
- 内中断,也称为异常。中断信号来源于CPU内部,与当前执行的指令有关。CPU在执行指令时会检查是否有异常发生。
- 陷阱、陷入(trap)
- 故障(fault)
- 终止(abort)
- 外中断,也称中断。与当前执行的指令无关,中断信号来源于CPU外部。每个指令周期末尾,CPU都会检查是否有异常发生。
- 时钟中断
- IO中断请求
不同的中断信号,需要用不同的中断处理程序来处理。当CPU检测到中断信号后,会根据中断信号的类型去查询“中断向量表”,以此来找到相应的中断处理程序在内存中的存放位置。
6. 系统调用
操作系统作用户和计算机硬件之间的接口,需要向上提供一些简单易用的服务。主要包括命令接
口和程序接口。其中,程序接口由一组系统调用组成。
“系统调用”是操作系统提供给应用程序(程序员)使用的接口,可以理解为一种可供应用
程序调用的特殊函数,应用程序可以通过系统调用来请求获得操作系统内核的服务。
应用程序通过系统调用请求操作系统服务。而系统中各种共享资源都由操作系统内核统一掌控,因此凡是与共享资源有关的操作(如存储分配、IO请求、文件管理),都必须有系统调用的方式向内核提出服务请求,有内核代替完成,这样可以保证系统的稳定性和安全性,防止用户非法操作。系统调用的主要功能
- 设备管理。完成设备的 请求/释放/启动 等功能
- 文件管理。完成文件的 读/写/创建/删除 等功能
- 进程控制。完成进程的 创建/撒销/阻塞/唤醒 等功能
- 进程通信。完成进程之间的 消息传递/信号传递 等功能
- 内存管理。完成内存的 分配/回收 等功能
7. 操作系统的体系结构
操作系统的体系结构:
- 宏内核(大内核)。
将操作系统的主要功能模块都作为系统内核,运行在核心态。如Linux,Unix
- 优点:高性能
- 缺点:内核代码庞大,结构混乱,难以维护
- 微内核
只把中断、原语、进程通信等最基本的功能保留在内核。进程管理、文件管理、设备管理等功能以用户进程的形式运行在用户态。如Windows NT。
- 优点:内核功能少,结构清晰,方便维护
- 缺点:需要频繁地在核心态和用户态之间切换,性能低
- 分层结构
内核分多层,每层可单向调用相邻的、更低一层提供的接口。
- 优先:便于调试和验证,自底向上逐层调试验证;易扩充和易维护,各层之间调用接口清晰固定
- 缺点:仅可调用相邻低层,难以合理定义各层的边界;效率低,不可跨层调用,系统调用执行时间长
- 模块化
将内核划分为多个模块,各模块之间相互协作。每个模块具有某方面的管理功能,并规定好各模块间的接口,使各模块之间能通过接口进行调用。还可以进一步将各模块细分为若干个具有一定功能的子模块,同样也规定好各子模块之间的接口。把这种设计方法称为模块-接口法内核。内核=主模块+可加载内核模块
- 主模块:只负责核心功能,如进程调度、内存管理
- 可加载内核模块:可以动态加载新模块到内核,而无需重新编译整个内核
优点:
- 模块间逻辑清晰易于维护,确定模块间接口后即可多模块同时开发
- 支持动态加载新的内核模块(如:安装设备驱动程序、安装新的文件系统模块到内核),增强
OS适应性 - 任何模块都可以直接调用其他模块,无需采用消息传递进行通信,效率高
缺点: - 模块间的接口定义未必合理、实用
- 模块间相互依赖,更难调试和验证
- 外核
内核负责进程调度、进程通信等功能,外核负责为用户进程分配未经抽象的硬件资源,不需要操作系统进行内存映射。且由外核负责保证资源使用安全
优点:
- 外核可直接给用户进程分配硬件资源,使用户进程可以更灵活的使用硬件资源
- 减少了虚拟硬件资源的“映射层”,提升效率
缺点: - 降低了系统的一致性,使系统变得更复杂
8. 引导程序
操作系统的引导程序(boot),让用户开机的时候,让操作系统运行起来。它主要分为4步:
- CPU从一个特定的主存地址开始,取指令,执行ROM(BIOS,基本输入输出系统,不会随着断电而丢失)中的引导执行(先进行硬件自检再开机)
- 将磁盘中的第一块也被称为主引导记录MBP,读入内存,执行磁盘引导程序,扫描分区表
- 从活动分区(又称为主分区,即安装了操作系统的分区例如C盘)读入分区引导记录PBR,执行其中的程序
- 从根目录下找到完成的操作系统初始化程序(启动管理器)并执行,完成开机
9. 虚拟机
虚拟机:使用虚拟化技术,将一台物理机器虚拟化为多台虚拟机器(Virtual Machine, VM),每个虚拟机器都可以独立运行一个操作系统
| 第一类VMM | 第二类VMM | |
|---|---|---|
| 对物理资源的控制 | 直接运行在硬件之上,能直接控制和分配物理资源 | 运行在宿主 OS之上,依赖于宿主OS为其分配物理资源 |
| 资源分配方式 | 在安装Guest OS时,VMM要在原本的硬盘上自行分配存储空间,类似于“外核"的分配方式,分 配未经抽象的物理硬件 |
GuestOS 拥有自己的虚拟磁盘,该盘实际上是Host OS 文件系统中的一个大文件。GuestOS分配到的内存是虚拟内存 |
| 性能 | 性能更好 | 性能更差,需要宿主系统作为中介 |
| 可支持的虚拟机数量 | 更多,不需要和 Host OS 竞争资源,相同的硬件资源可以支持更多的虚拟机 | 更少,Host OS 本身需要使用物理资源,HostOS 上运行的其他进程也需要物理资源 |
| 虚拟机的可迁移性 | 更差 | 更好,只需导出虚拟机镜像文件即可迁移到另一台 HostOS上,商业化应用更广泛 |
| 运行模式 | 第一类VMM运行在最高特权级(Ring 0),可以执行最高特权的指令。 | 第二类VMM部分运行在用户态、部分运行在内核态。GuestOS 发出的系统调用会被 VMM 截获,并转化为 VMM 对 HostOS 的系统调用 |
二、进程
1. 进程的概念
程序:静态的,就是存放在磁盘里的可执行文件,时一系列指令的集合
进程:是动态的,是程序的一次执行过程,是系统进行资源分配和调度的一个独立单位
进程的组成包括
- PCB (进程控制块),进程运行时的信息被保存的一个数据结构PCB(process Control Block)中,也叫进程控制块
- 进程描述信息
- 进程标识符 PID
- 用户标识符 UID
- 进程控制和管理信息
- CPU、磁盘、网络流量使用情况统计...
- 进程当前状态:就绪态/阻塞态/运行态...
- 资源分配清单
- 正在使用哪些文件、哪些内存区域、哪些 I/O 设备
- 处理机相关信息
- 如 PSW、PC 等等各种寄存器的值(用于实现进程切换)
- 进程描述信息
- 程序段。程序的代码
- 数据段。运行过程中产生的各种数据(如:程序中定义的变量)
PCB是给操作系统用的,而程序段、数据段是给进程自己用的
进程的特征
- 动态性:进程是程序的一次执行过程,是动态地产生、变化和消亡的。
- 并发性:内存中有多个进程实体,各进程可并发执行。
- 独立性:进程是能独立运行、独立获得资源、独立接受调度的基本单位。
- 异步性:各进程按各自独立的、不可预知的速度向前推进,操作系统要提供“进程同步机制”来解决异步问题。
- 结构性:每个进程都会配置一个 PCB。结构上看,进程由程序段、数据段、PCB 组成。
2. 进程的状态
- 进程的状态
- 创建态:进程正在被创建,操作系统为进程分配资源、初始化PCB。
- 就绪态:已经具备运行条件,但由于没有空闲CPU,而暂时不能运行。
- 运行态:占有CPU,并在CPU上运行。
- 阻塞态:因等待某一事件而暂时不能运行。请求等待某个事件的发生(如等待某种系统资源的分配,或者等待其他进程的响应)。在这个事件发生之前,进程无法继续往下执行
- 终止态:进程正在从系统中撤销,操作系统会回收进程拥有的资源、撤销PCB。
运行态 -> 阻塞态:是一种进程自身做出的主动行为
阻塞态 -> 就绪态:不是进程自身能控制的,是一种被动行为
- 进程的组织方式
进程PCB中,会有一个变量 state 来表示进程的当前状态。如:1表示创建态、2表示就绪态、3表示运行态..为对的各个进程进行统一的管理,操作系统会将各个进程的PCB组织起来。
链式方式:操作系统通过链式队列管理进程,按照进程状态将PCB分为多个队列,操作系统持有指向各个队列的指针
索引方式:操作系统给各种状态的进程建立索引表,每一个索引表的表项指向PCB,操作系统持有指向各个索引表的指针
3. 进程的控制
进程控制的主要功能是对系统中的所有进程进行管理,它具有创建新进程、撤销已有进程、实现进程状态转换等功能。也就是实现进程的状态转换。
原语
原语是一种特殊的程序,它的执行具有原子性,即程序的运行不能中断。
如何实现原语执行的原子性?
通过“关中断指令”和“开中断指令”这两个特权指令实现原子性,阻止外部中断信号的执行。
正常情况:CPU每执行一条指令都会理性检查是否有中断信号需要处理,如果有则暂停当前运行的这段程序,转而执行相应的中断处理程序。而CPU执行了CPU执行了关中断指令之后,就不再例行
检查中断信号,直到执行开中断指令之后才会恢复检查。
进程控制相关的原语
- 创建:操作系统创建一个进程的原语,创建态->就绪态
- 原语:申请空白PCB;为新进程分配所需的资源、;初始化PCB;将PCB插入就绪队列
- 切换:切换进程的状态,就绪态 -> 运行态 或者 运行态 -> 就绪态
- 原语:将进程运行信息存入PCB;PCB移入相应队列;选择另一个进程执行,并更新其PCB;根据PCB恢复新进程所需的运行环境
- CPU中会有很多寄存器,用来存放程序运行过程中所需要的某些数据,例如:
- PSW程序状态寄存器:保存进程状态用户态/内核态
- PC程序计数器:存放下一条指令的地址
- IR指令寄存器:存放当前正在执行的指令
- 通用寄存器:存放一些必要信息
- 引起进程切换的事件:当前进程时间片到、有更高优先级的进程到达
- 阻塞:引起进程的阻塞,运行态->阻塞态
- 原语:找到要阻塞的进程对应的PCB;保护进程运行现场,将PCB状态信息设置为“阻塞态”,暂时停止进程运行;将PCB插入相应事件的等待队列
- 引起阻塞的事件:需要等待系统分配某种资源;需要等待相互合作的其他进程完成工作
- 唤醒:唤醒阻塞的进程,阻塞态 -> 就绪态
- 原语:在事件等待队列中找到PCB;将PCB从等待队列移除,设置进程为就绪态;将PCB插入就绪队列,等待被调度
- 引起唤醒的事件:等待的事件发生
- 撤销:终止一个进程, 就绪态/阻塞态/运行态 -> 终止态
- 原语:从PCB集合中找到终止进程的PCB;若进程正在运行,立即剥夺CPU,将CPU分配给其他进程;终止其所有子进程;将该进程拥有的所有资源归还给父进程或操作系统;删除PCB
- 引起进程终止的事件:正常结束;异常结束;外接干预
无论哪个进程控制原语,要做的无非三类事情:
- 更新PCB中的信息
- 将PCB插入合适的队列
- 分配/回收资源
4. 进程间通信
进程间通信(Inter-Process Communication,IPC),是指进程之间产生数据交互。
为什么进程通信需要操作系统支持?
进程是分配系统资源的独立单位,各个进程拥有的内存地址空间相互独立,为了保证安全,一个进程不能直接访问另一个进程的地址空间。
进程通信的三种方式
- 共享存储
设置一个共享内存区域,并映射到进程的虚拟地址空间,用于进程通信。为避免出错,各个进程对共享空间的访问应该是互斥访问的。共享存储分为两种:
- 基于存储区的共享。操作系统在内存中划出一块共享存储区提供给进程,数据的形式、存放位置都由通信进程控制,而不是操作系统,这种共享方式灵活性很高,速度快,是一种高级通信方式。
- 基于数据结构的共享。类似于全局变量,共享空间里只能放一个固定格式的数据结构。比如int[10],这种共享方式速度慢、限制多,是一种低级通信方式
注:通过增加“页表项/段表项”,将同一片共享内存区映射到各个进程的地址中。
2. 消息传递
进程间的数据交换以格式化的消息(Message)为单位。进程通过操作系统提供的“发送消息/接收
消息”两个原语进行数据交换。
消息传递分为:
- 直接通信方式。消息发送的进程,要点名接受进程的ID,通过原语实现数据发送与接收。
- 间接通信方式。通过“信箱”作为中间实体间接地通信,又称为信箱通信方式。(进程可通过系统调用申请一个邮箱)。允许多个进程往同一信箱里发送消息,也可以多个进程从同一个信箱里接受消息。
- 管道通信
管道(pipe)是一个特殊的共享文件,其实就是在内存中开辟一个大小固定的内存缓冲区,用于数据共享。管道的读写是先进先出的FIFO。
管道只能采用半双工通信,即某一时间内只能单向的传输(只能写/只能读)
如果要实现全双工通信,即双向实时通信,则需要设置两个管道。
注意要点:
- 各进程必须互斥访问管道,这由操作系统实现。
- 当管道写满时,写进程将阻塞,知道读进程将管道中的数据取走,即可唤醒写进程。
- 当管道读空时,读进程将堵塞,知道写进程往管道内中写入数据,即可唤醒读进程。
- 管道中的数据一旦被读出,就彻底消失,因此,当多个进程同时读同一个管道时会错乱。通常的解决方法:
- 一个管道允许多个写进程,一个读进程。
- 允许多个写进程,多个读进程,但是系统会让各个读进程轮流从管道中读数据。
4. 信号
信号量(Semaphore),实现进程间的同步和互斥
信号(Signal),实现进程间通信
信号:用于通知进程某个特定事件已经发生,进程收到一个信号后,对信号进行处理。
Linux操作系统定义了30种信号类型,
- 信号发送和保存
信号信息用不少于Nbit的位向量保存的,对应N种信号。
- 进程之间,进程自己可以发送信号
- 内核进程也可以给用户进程发送信号
- 进程之间互相发送的信号类型时有限制的
-
信号的处理的时机
当进程从内核态转为用户态时(如系统调用返回,中断处理返回),例行检查是否有待处理的信号,如果有,则处理信号。
-
信号如何处理方式
- 处理信号时,操作系统为此类信号执行缺省(默认)的信号处理程序;某些信号默认忽略不做处理
- 执行进程为此类信号设置用户自定义的信号处理程序,此程序覆盖默认的信号处理程序
- 信号处理程序结束后,通常会返回进程的下一条指令继续执行,除非信号处理程序将进程阻塞或终止了
- 一旦处理了某个信号,就将此类的pending重置为0
- 若重复收到同类信号,将此信号简单地丢弃,因为同类信号仅有单独1bit记录
- 当同时收到多个不同类的信号时,通常先处理更小的信号
信号和异常有什么关系?
信号可以作为异常的配套机制,让进程对操作系统的异常处理进行补充。
在进程运行过程中,某些特殊事件可能发生异常,操作系统内核负责捕获并处理异常:
- 有些异常可由内核全部处理(如缺页异常)
- 有些异常无法由内核完成全部处理,可能需要用户进程配合,此时就可以用信号机制与异常机制相互配合。(如:在Linux中,发生除以0异常时,内核的异常处理程序会向用户进程发送SIGFPE信号。SIGFPE信号的默认处理程序会将进程终止并转储内存(闪退崩溃);当然进程可以自定义SIGFPE信号处理程序)
三、线程
1. 线程的概念
线程是程序执行的最小单元,是一个基本的CPU执行单元。进程内各线程之间可以并发。提供系统的并发度
进程是程序的一次执行,引入线程后,进程是系统资源分配的基本单元,线程是调度的基本单元
线程的属性:
- 线程是处理机调度的单位
- 多CPU计算机中,各个线程可占用不同CPU执行
- 每个线程都有一个线程ID、线程控制快TCB
- 线程也就有就绪、阻塞、运行三种基本状态
- 线程几乎不拥有系统资源,系统资源分配给进程
- 同一进程的不同线程间共享进程资源
- 由于共享内存地址空间,同一进程中的线程间的通信无需系统干预
- 同一进程中线程切换,不会引起进程切换
- 不同进程中线程切换,会引起进程切换
- 切换同进程内的线程,系统开销小
- 切换进程,系统开销大
2. 线程的实现方式
线程的实现方式
- 用户级线程
用户感知到线程的存在,而操作系统意识不到线程的存储,用户级线程就是从用户视角能看到的线程。例如早起的操作系统Unix,只支持进程不支持线程,线程是由线程库实现的。
- 用户级的线程是由应用程序通过线程库实现,所有线程管理工作由应用程序负责,包括线程切换
- 用户级的线程中,线程切换在用户态完成,无需系统干预
优点:用户级线程的切换在用户空间即可完成,不需要切换到核心态,线程管理的系统开销小,效率高
缺点:当一个用户级线程被阻塞后,整个进程都会被阻塞,并发度不高。并且操作系统给进程分配在一个CPU中,多个线程不可在多核处理机上并行运行。
- 内核级线程
内核级线程又称内核支持的线程,就是从操作系统内核视角能意识到线程存在。线程的管理由操作系统内核完成。线程的调度、切换等工作都由内核负责,因此内核级线程的切换必然需要在内核态下完成。操作系统回给每个内核线程建立相应的线程控制块TCB,通过TCB对线程进行管理
优点:当一个线程被阻塞后,别的线程还可以继续执行,并发能力强。多线程可在多核处理机上并行执行。
缺点:一个用户进程会占用多个内核级线程,线程切换由操作系统内核完成,需要切换到用户态,因此线程管理的成本高,开销大。
在支持内核级线程的系统中,根据用户级线程和内核级线程的映射关系,可以划分为几种多线程模型
-
一对一模型。进程的每一个用户级线程映射到一个内核线程。进程的用户级的线程和对应的内核级线程的数量相同
- 优点:当一个线程被阻塞后,别的线程还可以继续执行,并发能力强。多线程可在多核处理机上并行执行。
- 缺点:一个用户进程会占用多个内核级线程,线程切换由操作系统内核完成,需要切换到核心态,因此线程管理的成本高,开销大。
-
多对一模型。多个用户级线程映射到一个内核级线程。且一个进程只被分配一个内核级线程。
- 优点:用户级线程的切换在用户空间即可完成,不需要切换到核心态,线程管理的系统开销小,效率高
- 缺点:当一个用户级线程被阻塞后,整个进程都会被阻塞,并发度不高。多个线程不可在多核处理机上并行运行
-
多对多模型。用户级线程映射到m 个内核级线程(n>=m)。每个用户进程对应 m 个内核级线程。内核级线程中可以运行任意一个有映射关系的用户级线程代码,只有m个内核级线程中正在运行的代码逻辑都阻塞时,这个进程才会阻塞
- 优点:克服了多对一模型并发度不高的缺点(一个阻塞全体阻塞),又克服了一对一模型中一个用户进程占用太多内核级线程,开销太大的缺点。
- 缺点:每个功能的线程没有隔离,不便于管理,系统复杂度高
操作系统只管理内核级线程,内核级线程才是处理机分配的单位。
3. 线程的状态与控制
线程的状态
线程的控制
每个线程创建是建立TCB,系统通过将多个TCB组织成线程表来管理
四、系统调度
1. 调度的概念
当有一堆任务要处理,但由于资源有限,这些事情没法同时处理。这就需要确定某种规则来决定处理任务的顺序,这就是调度
调度的三个层次:作业调度(高级)、内存调度(中级)、进程调度(低级)
- 作业调度
作业调度类似于程序调度,让操作系统启动一个程序。操作系统决定程序启动的顺序
作业调度:按一定的规则从外存的作业后备队列中挑选一个作业掉入内存,并创建进程。每个作业只调入一次,调出一次。作业调入会建立PCB,调出时撤销PCB
-
进程调度(低级调度)
进程调度:按照某种策略从进度队列中选取一个进程执行,也就是分配给CPU。进程调度是操作系统中最基本的一种调度。,进程就由就绪态变为运行态。进程调度的频率很高,一般几十毫秒一次,保证所有进程宏观并行执行。 -
内存调度(中级调度)
内存不够时,可将某些进程的数据调到外存,等内存空闲或者进程需要运行时在重新调入内存。
暂时调到外存等待的进程状态称为挂起状态。被挂起的进程PCB会被组织成挂起队列。
内存调度: 按照某种策略决定将哪个处于挂起状态的进程重新调入内存。
一个进程可能会被多次调出、调入内存,因此内存调度的频率要比作业调度更高。
进程的挂起状态和进程的七状态模型
暂时调到外存等待的进程状态称为挂起状态(suspend)。而挂起状态又可以进一步细分为就绪挂起和阻塞挂起两种状态。
五状态模型
七状态模型。根据挂起状态的细分,进程的状态又可以细分为7状态模型。进入挂起状态因为内存空间不够,需要等待内存空闲。
挂起和阻塞/就绪的区别?
两种状态都是暂时不能获得CPU的服务
- 挂起时进程调到外存去等待
- 阻塞/就绪进程还在内存中
有的操作系统还会把就绪挂起和阻塞挂起分为两个挂起队列,甚至会根据阻塞原因不同再把阻塞挂起进程进一步细分为多个队列。
2. 进程调度
进程调度就是从就绪队列中选择一个进程分配CPU资源。
进程调度的情况
- 进程主动放弃CPU
- 进程正常终止
- 运行过程中发生异常终止
- 进程主动请求阻塞(如等待IO)
- 进程被动放弃CPU
- 进程的时间片用完
- 有更紧急的事需要处理,比如中断
- 有更高级的进程进入就绪队列
不能进程调度的情况
- 在处理中断的过程中,中断处理过程复杂,与硬件密切相关,很难做到在中断处理过程中进行进程切换。
- 进程在操作系统的内核临界区中。
- 临界资源:进程需要互斥访问的资源
- 临界区:访问临界资源的代码
- 内核临界区:内核中访问某种资源的,比如内核的某种数据结构。
- 在原子操作过程中。原子操作不可中断。
进程调度的方式
- 非剥夺调度方式(非抢占方式)
只允许进程主动放弃CPU,在运行中即便有更紧急的任务,当前进程依然会继续使用CPU,知道该进程终止或主动进入阻塞态。
- 优点:实现简单,系统开销小
- 缺点:但是无法及时处理紧急任务,适合于早期的批处理系统
- 剥夺调度方式(抢占方式)
当一个进程正在处理机上执行时,如果有一个更重要或更紧迫的进程需要使用处理机,则立即暂停正在执行的进程,将处理机分配给更重要紧迫的那个进程。
- 优点:可以优先处理更紧急的进程,也可实现让各进程按时间片轮流执行的功能(通过时钟中
断)。适合于实时操作系统、分时操作系统
狭义的进程调度和进程切换的区别?
狭义的进程调度。单纯只从就绪队列中选择一个要运行的进程。选择的这个进程可以是CPU刚暂停执行的进程,这时候可以直接执行。也可能是另一个新进程,这时就需要进程切换。
广义的进程调度。包含了进程选择和进程切换两个步骤。
进程切换的过程主要完成了:
- 对原来运行进程各种数据的保存
- 对新的进程各种数据的恢复
注意:进程切换是有代价的,过于频繁的进程调度,会使得整个系统的效率降低。使系统大部分时间都花在了进程切换上,而真正用于执行进程的时间減少。
3. 进程调度器/调度程序
- 调度程序
调度程序决定了哪个进程可以运行,运行多久。进程状态的切换由调度程序完成。以下都可能唤醒调度程序
- 创建新进程
- 进程退出
- 进程阻塞
- 中断发生(可能唤醒某些阻塞进程)
调度程序触发时机
非抢占调度,只有进程阻塞或退出才会触发调度程序
抢占调度,每个时钟中断或k个时钟中断才会触发调度程序
调度程序处理的对象
不支持内核级线程的操作系统,调度程序的处理对象是进程
支持内核级线程的操作系统,调度程序的处理对象是内核线程,进程作为资源分配的单位,线程是调度的单位。
闲逛进程
没有其他就绪进程时,运行闲逛进程(idle)。闲逛进程的特性:
- 优先级最低
- 可以是0地址指令(不需要缓存,不需要访问CPU的寄存器),占用CPU执行一个完整的周期(指令周期末尾例行检查中断)
- 能耗低
3. 进程调度算法
3.1 调度算法的评价指标
- CPU利用率。指CPU忙碌的时间占总时间的比例
CPU利用率 = CPU忙碌时间/总时间 - 系统吞吐量。单位时间内完成的作业的数量
$系统吞吐量=总共完成了多少道作业/总共花了多少时间$
eg:某计算机系统处理完10道作业,共花费100秒,则系统吞吐量为?
10/100 =0.1 道/秒
- 周转时间。作业从被提交给系统开始,到作业完成为止所花费的是时间。
$周转时间 = 作业完成时间-作业提交时间$
它包括四个部分:后三项在一个作业的整个处理过程中,可能发生多次。
- 作业在外存后备队列上等待作业调度的时间
- 进程在就绪队列上等待进程调度的时间
- 进程在CPU上执行的时间
- 进程等待IO操作完成的时间。
平均周转时间。对于操作系统来说,所有作业周转时间的平均值
- 等待时间。进程/作业处于等待被服务的时间之和,等待CPU处理,等待时间越长用户满意度越低。
对于进程来说,等待时间就是从进程建立的就绪态之后,等待被CPU执行的时间之和,在等待IO处理不算,因为等待IO也会被IO设备服务。
对于作业来说。作业的等待时间不仅包括建立进程后的等待时间,还要加上作业在外存后备队列等待内存调度的时间。
平均等待时间。所有进程/作业等待时间的平均值。
- 响应时间。指用户从提交请求到首次产生响应所用的时间。
3.2 进程/作业调度算法
注:比起早期的批处理操作系统来说,由于计算机造价大幅降低,因此之后出现的交互式操作系统(包括分时操作系统、实时操作系统等)更注重系统的响应时间、公平性、平衡性等指标。而这几种算法恰好也能较好地满足交互式系统的需求。因此这三种算独适合用于交互式系统。(比如UNIX)
- 先来先调度算法 FCFS
算法思想。公平角度
算法规则。按照作业/进程到达的先后顺序进行服务,等待时间长的优先调度。
用于作业/进程调度。 都有
- 进程调度时。考虑哪个进程先到达就绪队列
- 作业调度时。考虑哪个进程先到达外村的后备队列。
是否可抢占?非抢占式算法,等进程/作业主动放弃时才进行调度
优点:公平、算法实现简单
缺点:排在长作业(进程)后面的短作业需要等待很长时间,带权周转时间很大,对短作业来说用户体验不好。即,FCFS算法对长作业有利,对短作业不利
是否会导致饥饿(某进程/作业长期得不到服务)? 不会。等待被处理。
- 短作业优先 SJF
算法思想。最求最少的等待时间,最少的平均周转时间、最少的平均带权周转时间
算法规则。每次调度时选择当前已到达且运行时间最短的进行调度,作业/进程服务时间最短的优先得到服务。
用于作业/进程调度。 都有,用于进程调度时称内“短进程优先(SPF, Shortest Process First)算法”
是否可抢占?SJF和SPF是非抢占式的算法。但是也有抢占式的版本—一最短剩余时间优先算法 (SRTKN, Shortest Remaining Time Next)
抢占式短作业优先算法。每当有进程加入就绪队列改变时就需要调度,如果新到达的进程剩余时间比当前运行的进程剩余时间更短,则由新进程抢占处理机,当前运行进程重新回到就绪队列。另外,当一个进程完成时也需要调度
对比非抢占式的短作业优先算法,显然抢占式的评价指标更低
优点:“最短的”平均等待时间、平均周转时间
缺点:不公平。对短作业有利,对长作业不利。可能产生饥饿现象。另外,作业/进程的运行时间是由用户提供的,并不一定真实,不一定能做到真正的短作业优先
是否会导致饥饿?
会。会。如果源源不断地有短作业/进程到来,可能使长作业/进程长时间得不到服务,产生“饥饿”现象。如果一直得不到服务,则称为“饿死”
- 高响应比优先 HRRN
算法思想。要综合考虑作业/进程的等待时间,又要考虑要运行的时间
算法规则。在每次调度时先计算各个任务的响应比, 选择响应比最高的任务为其服务响应比=等待时间+运行时间/运行时间
用于作业/进程调度。 都可以
是否可抢占?非抢占式算法。因此只有当前运行的作业/进程主动放弃CPU(正常/异常完成,或主动阻塞)时,才需要调度,才需要计算响应比
优点:综合考虑了等待时间和运行时间。对于长作业来说,随着等待时间越来越久,其响应比也会
越来越大,从而避免了长作业饥饿的问题
- 等待时间相同时,运行时间短的优先(SJF 的优点)
- 运行时间相同时,等待时间长的优先(FCFS 的优点)
是否会导致饥饿? 不会
- 时间片轮转调度算法 RR
算法思想。 公平地、轮流地各个进程股务,让每个进程在一定时间隔内都可以得到响应
算法规则。 按照各进程在就绪队列的到达顺序,让各个进程轮流执行一个时间片。若进程未在一个时间片内执行完,则剥夺处理机,将进程重新放到就绪队列队尾重新排队。
用于作业/进程调度。 用于进程调度。进程才会被分配运行的时间片,进程(具体来说是线程)是调度的基本单位
是否可抢占? 若进程未能在时间片内运行完,将被强行剥夺处理机使用权,不是等到进程主动放弃调度,因此时间片轮转调度算法属于抢占式的算法。由时钟装置发出时钟中断来通知CPU时间片已到
优点:公平;响应快,适用于分时操作系统;
缺点:缺点:由于高频率的进程切换,因此有一定开销;不区分任务的紧急程度。
是否会导致饥饿? 不会,轮流服务
时间片太大太小的影响?
- 如果时间片太大,使得每个进程都可以在一个时间片内就完成,则时间片轮转调度算法退化为先来先服务调度算法,并且会增大进程响应时间。因此时间片不能太大。
- 另一方面,进程调度、切换是有时间代价的(保存、恢复运行环境),因此如果时间片太小,会导致进程切换频繁,系统会花大量的时间来处理进程切换,从而导致实际用于进程执行的时间比例减少。可见时间片也不能太小。
- 一般来说,设计时间片时要让切换进程的开销占比不超过1%
- 优先级调度算法
算法思想。 随着计算机的发展,特别是实时操作系统的出现,越来越多的应用场景需要根据任务的紧急程度来决定处理顺序
算法规则。 每个任务设置优先级,调度时选择优先级最高的调度
用于作业/进程调度。 都有。甚至,还会用于在之后会学习的IO调度中
是否可抢占? 抢占式、非抢占式都有
- 非抢占式只需在进程主动放弃处理机时进行调度即可
- 抢占式还需在就绪队列变化时,检查是否会发生抢占。例如每次调度时选择当前已到达且优先级最高的进程。当前进程主动放弃处理机时发生调度。另外,当就绪队列发生改变时,即有先任务到来时也需要检查是会发生抢占。
优点:用优先级区分紧急程度、重要程度,适用于实时操作系统。可灵活地调整对各种作业/进程的偏好程度。
缺点:若源源不断地有高优先级进程到来,则可能导致饥饿
是否会导致饥饿? 会
补充:
- 就绪队列未必只有一个,可以按照不同优先级来组织。也可以把优先级高的进程排在更靠近队头的位置
- 根据优先级是否可以动态改变,可将优先级分为静态优先级和动态优先级两种。
- 静态优先级:创建进程时确定,之后一直不变。
- 动态优先级:创建进程时有一个初始值,之后会根据情况动态地调整优先级。可以从追求公平、提升资源利用率等角度考虑,比如根据等待时间调整、IO密集型提升优先级
- 优先级级别
- 通常系统进程高于用户进程
- 前台进程优先级高于后台进程
- 操作系统更偏好IO繁忙型进程,而不是CPU繁忙型进程。IO设备和CPU可以并行工作。如果优先让IO繁忙型进程优先运行的话,则越有可能让IO设备尽早地投入工作,则资源利用率、系统吞吐量都会得到提升
- 多级反馈队列调度算法
算法思想。 对其他调度算法的折中权衡
算法规则。- 设置多级就绪队列,各级队列优先级从高到低,时间片从小到大
- 新进程到达时先进入第1级队列,按FCFS原则排队等待被分配时间片,若用完时间片进程还未结束,则进程进入下一级队列队尾。如果此时己经是在最下级的队列,则重新放回该队列队尾
- 只有第k级队列为空时,才会k+1级队头的进程分配时间片
用于作业/进程调度。 进程调度
是否可抢占? 抢占式的算法。在k级队列的进程运行过程中,若更上级的队列(1~k-1级)中进入了一个新进程,则由于新进程处于优先级更高的队列优先执行,因此新进程会抢占处理机,原来运行的进程放回k级队列队尾。
优缺点。对各类型进程相对公平(FCFS的优点);每个新到达的进程都可以很快就得到响应(RR的优点);短进程只用较少的时间就可完成(SPF的优点);不必实现估计进程的运行时间(避免用户作假);可灵活地调整对各类进程的优先级,比如CPU密集型进程、IO密集型进程(拓展:可以将因IO而阻塞的进程重新放回原来优先级的队列,这样IO型进程就可以保持较高优先级)
是否会导致饥饿? 会,被降级为敌优先级的进程可能长期得不到服务
- 多级队列调度算法
系统中按进程类型设置多个队列,不同队列可采用不同的调度策略。
- 例如系统进程采用优先级调度、交互式队列采用RR、批处理对立采用FCFS
队列之间可采取固定优先级或时间片划分。 - 例如:时间片划分例如三个队列分配时间为50%、40%、10%,固定优先级例如高优先级优先执行。
3.3 多处理机调度。
单处理机调度:单个CPU,根据调度算法决定哪个进程调度执行
多处理机调度:多个CPU,需要考虑:
- 调度算法。决定哪个就绪进程调度运行
- 分配CPU。决定进程在哪个CPU执行
多处理机的调度目标:
- 负载均衡。让每个CPU都同等忙碌
- CPU亲和性。尽力让一个进程调度到同一个CPU上运行,主内存访问延迟是L1服务器的100倍以上,避免了CPU数据迁移带来的时间损耗。
多处理机调度方案:
- 公共就绪队列
所有CPU共享同一个就绪进程队列(位于内核区)。CPU空闲时运行调度程序从公共就绪队列中选择
一个进程运行。每个CPU访问公共就绪队列时需要上锁,确保互斥访问。
优点:天然实现负载均衡
缺点:各个进程频繁换CPU运行,亲和性不好
如何提升处理的亲和性?
- 软亲和。由进程调度程序尽量保证“亲和性”
- 硬亲和。由用户进程通过系统调用,主动要求操作系统分配固定的CPU,”确保“亲和性”
- 私有就绪队列
每个CPU都有一个私有就绪队列。CPU空闲时运行调度程序,从私有就绪队列中选择一个进程运行。
如何实现负载均衡?
Push策略。一个特定的周期性检查系统每个处理器的负载,如果负载不平衡,就从忙碌CPU的就绪队列中“推”一些就绪进程到空闲CPU的就绪队列
pull策略。每个CPU运行调度程序时,周期性检查自身负载与其他CPU负载。如果一个CPU负载很低,就从其他高负载CPU的就绪队列中“拉”一些就绪进程到自己的就绪队列
亲和性?
- 私有就绪队列天然地实现了“处理机亲和性”
- 硬亲和:为了负载均衡时不破坏亲和性,用户可以通过系统调用执行CPU执行,保证硬亲和
五、同步与互斥
5.1 同步和互斥的概念
异步。异步性是指,各并发执行的进程以各自独立的、不可预知的速度向前推进。
同步。同步亦称直接制约关系,为完成某种任务而建立多个进程,这些进程需要按照工作次序执行。
读进程和写进程并发地运行,由于并发必然导致异步性,而实际应用中又必须按某种顺序来执行,又必须按照“写数据 读数据”。如何解决这种异步问题,就是“同步”所讨论的内容。
互斥。一个时间段内只允许一个进程访问临界资源
临界资源。资源被多个进程共享,一个时间段内只允许一个进程使用的资源
临界资源互斥访问原则
- 空闲让进。临界区空闲时,可以允许一个请求进入临界区的进程立即进入临界区;
- 忙则等待。当已有进程进入临界区时,其他试图进入临界区的进程进行等待;
- 有限等待。对请求访问的进程,应保证能在有限时间内进入临界区(保证不会饥饿);
- 让权等待。当进程不能进入临界区时,应立即释放该资源和CPU,防止进程忙等待。
5.2 进程互斥的实现方式——软件
目标是实现进程互斥访问临界区
- 单标志法
进入临界区前检查是否有其他进程在访问,没有则进入。只检查不上锁。每个进程进入临界区的权限只能被另一个进程赋予,进程访问完临界区后会把使用临界区的权限交个另一个进程。
优点:可以实现“同一时刻最多只允许一个进程访问临界区”
缺点:不遵循空闲让进原则
int trun = 0; //表示当前允许进入临界区的进程号//P0进程
while (turn != 0); //检查是否有进程在临界区
临界区;
turn = 1;//P1进程
while (turn != 1); //检查是否有进程在临界区
临界区;
turn = 0;
- 双标志先检查
算法思想:设置一个布尔型数组flag[],标记各进程想进入临界区的意愿,比如flag[0]=ture意味着0号进程P0现在想要进入临界区。每个进程在进入临界区之前先检查当前有没有别的进程想进入临界区,如果没有后上锁,则把自身对应标志设置为 true,之后开始访问临界区。
缺点:
- 违反了忙则等待原则。检查和上锁不是原子性的,进程可能同时上锁,可能导致两个进程同时进入临界区的问题。
- 无法实现临界区互斥访问
bool flag[2]; // 表示进入临界区意愿的数组 flag[0] = false; flag[1] = false; // 刚开始设置为两个进程都不想进入临界区// P0 进程:
while (flag[1]); // ① 检查。有其他进程访问临界区,P0 就一直循环等待
flag[0] = true; // ② 上锁
访问临界区
flag[0] = false; // 释放锁
// P1 进程:
while (flag[0]); // ⑤ 检查。有其他进程访问临界区,,P1 就一直循环等待
flag[1] = true; // ⑥ 上锁
访问临界区
flag[1] = false; // 释放锁
;
- 双标志后检查
算法思想:设置一个布尔型数组flag[],标记各进程想进入临界区的意愿,每个进程在进入临界区之前先上锁,把自身对应标志后设置为 true,再检查当前有没有别的进程想进入临界区,之后开始访问临界区。
bool flag[2]; // 表示进入临界区意愿的数组 flag[0] = false; flag[1] = false;// P0 进程:
flag[0] = true; // 上锁
while (flag[1]); //检查。检查是否有其他进程访问临界区
访问临界区
flag[0] = false; 释放锁
// P1 进程:
flag[1] = true; // 上锁
while (flag[0]); //检查。检查是否有其他进程访问临界区
访问临界区
flag[1] = false; 释放锁
缺点:违背了空闲让进和有限等待原则,双方进程可能都上锁,检查时发现对方都要访问临界区,则死锁一直等待,会因各进程都长期无法访问临界资源而产生“饥饿”现象。
- Peterson算法
先检查,如果双方都争着想进入临界区,那可以让进程谦让,后运行的进程等待
优点:解决了互斥问题。遵循了空闲让进、忙则等待、有限等待三个原则
缺点:没有实现让权等待原则。
// 共享变量(两个进程都能看到) bool flag[2] = {false, false}; // flag[i] = 进程i想进入临界区 int turn; // turn = 现在该谁谦让flag[0] = true; // 我想进 flag[1] = true; // 我想进
turn = 1; // 但我先让你 turn = 0; // 但我先让你//******* 检查,如果有其他进程访问临界区,则谦让。*******
while (flag[1] && turn == 1 while (flag[0] && turn == 0
{ {
// 等待... // 等待...
} }// 进入临界区 // 进入临界区
flag[0] = false; // 我用完了 flag[1] = false; // 我用完了
- Peterson算法+让权等待
Peterson如果有别的进程在访问临界区,则反复检查条件,CPU一直被它占用,但却没有做任何有意义的工作**。这叫做忙等待(Busy Waiting / 自旋等待)
让权等待:等待的进程主动挂起(阻塞态),释放CPU给其他进程,等临界区被释放时再唤醒。
5.3 进程互斥的实现方式——硬件
- 中断屏蔽方法
与原语的实现方式相同,利用开/关中断指令,在某进程开始访问临界区到结束访问,不允许被中断,也就不能发生进程切换,实现临界区的互斥访问。
优点:简单、高效
缺点:
- 不使用多CPU系统。关中断指令只作用于当前CPU,其他CPU内的进程可能同时访问临界区
- 只适用于操作内核进程,不适用于用户进程。因为开关中断指令只能运行在内核态。
关中断;
临界区访问
开中断;
- TestAndSet(TS指令/TSL指令)
TS指令是用硬件实现的,实现上锁和检查,执行的过程不允许被中断,只能一气呵成。
优点:实现简单,无需像软件实现方法那样严格检查是否会有逻辑漏洞;适用于多处理机环境
缺点:空转。缺点:不满足“让权等待”原则,暂时无法进入临界区的进程会占用CPU并循环执行TSL指令,从而导致“忙等”。
bool lock = false; // 共享锁,false=空闲,true=被占用// 这是硬件实现的,不可被打断。检查并上锁
bool TestAndSet() {
boolean old = lock; // 读出旧值
lock = true; // 上锁
return old; // 返回旧值
}用它实现互斥:
while (TestAndSet())
{ // 返回 true 说明锁已被占,继续等
}// 返回 false 说明抢到锁了,进入临界区
lock = false; // 用完释放
- Swap指令(XCHG指令)
有的地方也叫 Exchange 指令,或简称 XCHG指令。
Swap 指令是用硬件实现的,执行的过程是原子性的。
优点:实现简单,无需像软件实现方法那样严格检查是否会有逻辑漏洞:适用于多处理机环境
缺点:空转。
bool lock = false; // 共享锁,false=空闲// 进程想进入临界区:
bool key = true; // 每个进程自己的 key,初始为 true
while (key == true) //检查,如果自己想临界区,并上锁
{
Swap(&lock, &key); // 把 key 和 lock 对换
}// key 变成 false 了,说明抢到锁,进入临界区
lock = false; // 用完释放
5.4 互斥锁
互斥锁解决临界区访问问题。使用原子操作的函数来获取锁和释放锁,函数acquire获得锁,release释放锁。一个进程在进入临界区时应获得锁;在退出临界区时释放锁
优点:等待期间不用切换进程上下,多处理器系统中,若上锁的时间短,则等待代价很低
缺点:互斥锁的主要缺点是忙等待,如果有多个进程访问临界区,其他进程访问时,剩余进程就循环等待知道时间片用完才释放CPU资源。违反了让权等原则。
适用于多处理器系统,不适用于单处理器系统。
需要连续循环忙等的互斥锁,也称为自旋锁(spin lock),如 TSL指令、swap指令、单标志法
5.5 信号量
用信号量机制,实现检查、上锁的原子性
信号量其实就是一个变量(可以是一个整数,也可以是更复杂的记录型变量),可以用一个信号量
来表示系统中某种资源的数量。通过使用操作系统提供的一对原语来对信号量进行操作,从而很方便的实现进程互斥、进程同步。
原语是一种特殊的程序段,其执行只能一气呵成,不可被中断。原语是由关中断/开中断指令实现
的。
一对原语:wait(S)原语和 signal(S)原语,wait和signal是原子性的。括号里的信号量S其实就是函数调用时传入的一个参数。
wait、signal 原语常简称为P、V操作
信号量分为:整型信号量和记录型信号量
1. 整型信号量
整型信号量。用一个整数型的变量作为信号量,用来表示系统中某种资源的数量。
与普通整数变量的区别:对信号量的操作只有三种:初始化、wait、singal
存在的问题:不满足“让权等待”原则,会发生“忙等”
int S = 1; // 初始化整型信号量s,表示当前系统中可用的打印机资源数void wait (int S) { // wait 原语,相当于“进入区”
while (S <= 0); // 如果资源数不够,就一直循环等待
S = S - 1; // 如果资源数够,则占用一个资源
}void signal (int S) { // signal 原语,相当于“退出区”
S = S + 1; // 使用完资源后,在退出区释放资源
}// 进程P0:
...
wait(S); // 进入区,申请资源
使用打印机资源... // 临界区,访问资源
signal(S); // 退出区,释放资源
...
2. 记录型信号量
整型信号量的缺陷是存在“忙等”问题,因此人们又提出了记录型信号量,即用记录型数据结构表
示的信号量。
struct { int value; // 剩余资源数 Queue < Thread > waitList; // 等待队列(阻塞队列) }// == == = wait 原语:申请资源 == == =
wait(RecordSemaphore S) {
S.value - -; // 先把资源数减1,表示请求一个单位的资源
if (S.value < 0) { // 减完发现是负数,说明没有剩余资源了
block(S.waitlist)
}
}// == == = signal 原语:释放资源 == == =
signal(RecordSemaphore S) {
S.value + +; // 先把资源数加1(表示我用完了)
if (S.value <= 0) { // 等待队列里还有人
wakeup(S.waitlist) // 唤醒它,它从阻塞态变为就绪态
}
}
如何解决忙等?
- 操作结构包含:资源数;阻塞队列
- wait操作。如果剩余资源数不够,使用block原语使进程从运行态进入阻塞态,并把放入到信号量S的阻塞队列中
- signal操作。进程使用完资源后释放,还有别的进程在等待这种资源,感使用wakeup 原语唤醒阻塞队列中的一个进程,该进程从阻塞态变为就绪态
记录型信号量的优点:
- 实现了进程的互斥访问临界区
- 遵循了“让权等待”原则,不会出现“忙等”现象。
5.6. 信号量机制实现进程互斥和同步
1 信号量机制实现进程互斥
- 分析并发进程的关键活动,划定临界区
- 设置互斥信号量 mutex,初值1。对不同的临界资源需要设置不同的互斥信号量。
- 在进入区 wait(mutex)一—申请资源
- 在退出区 signal(mutex)一一释放资源
P、V操作必须成对出现。缺少P(mutex)就不能保证临界资源的互斥访问。缺少 V(mutex) 会导致资源永不被释放,等待进程永不被唤醒。
先wait后singal,先加锁后释放
/*信号量机制实现互斥*/ semaphore mutex=1; //初始化信号量P1(){
wait(mutex); // 使用临界资源前需要加锁
临界区代码段
signal(mutex); // 使用临界资源后需要解锁
}
P2(){
wait(mutex);
临界区代码段.
signal(mutex);
}
2 信号量机制实现进程同步
进程同步:要让各并发进程按要求有序地推进。
用信号量实现进程同步:
- 并发访问临界区
- 分析什么地方需要实现“同步关系”?,访问临界区的代码必须保证“一前一后”执行的两个操作
- 设置同步信号量S,初始为0
- 必须在前执行的进程,之后执行 signal操作
- 必须在后执行的进程,之前执行 wait操作。
先生产后消耗,前signal后wait,前V后P
要求“代码4”要基于P1 的“代码1”和“代码2”的运行结果才能执行。
/*信号量机制实现同步*/ semaphore S = 0; // 初始化同步信号量,初始值为0P1() {
代码1;
代码2;
signal(S); // S++。 表示有可用资源。如果有进程在阻塞队列中等待,则唤醒
代码3;
}P2() {
wait(S); // S--。 如果S>0,表示有资源继续运行,如果S=0,则会执行bloack阻塞
代码4;
代码5;
代码6;
}
3 信号量机制实现前驱关系
代码要求按如下前驱图所示的顺序来执行:实每一对前驱关系都是一个进程同步问题(需要保证一前一后的操作)
实现:
- 要为每一对前驱关系各设置一个同步信号量
- 在“前操作”之后执行signal V操作
- 在“后操作”之前执行wait P操作
S1() { S2() { S3() { S4() { S5() { S6() {
S1; P(a); P(b); P(c); P(d); P(e);
V(a); S2; S3; S4; S5; P(f);
V(b); V(c); V(g); V(e); V(f); P(g);
} V(d); } } } S6;
} }
4. 生产者消费者问题
生产者消费者问题
系统中有一组生产者进程和一组消费者进程,生产者进程每次生产一个产品放入缓冲区,消费者
进程每次从缓冲区中取出一个产品并使用。
如何实现?
互斥关系:缓冲区是访问,是临界区需要互斥
同步关系:
- 空闲缓冲区。消费者消耗完产品,先增加空闲缓冲区,生产者后消耗空闲缓冲区
- 产品数量。生产者生产产品,先增加产品,消费者后消耗产品
int mutex = 1; //互斥信号量,实现对缓冲区的互斥访问
int empty = n; //同步信号量,表示缓冲区空闲的数量
int full = 0; //同步信号量,表示产品的数量
//生产者 producter() { while(1) { wait(empty) //消耗一个空闲缓冲区wait(mutex) //实现互斥访问
生产
signal(mutex)signal(full) //生产一个产品
}
}//消费者
consumer() {
while(1) {
wait(full) //消耗一个产品wait(mutex) //实现互斥访问
消耗
signal(mutex)
signal(full) //增加一个空闲缓冲区
}
}
5. 多生产者-多消费者问题
桌子上有一只盘子,每次只能向其中放入一个水果。爸爸生产苹果,妈妈生产橘子,女儿消费苹果,儿子消费橘子。只有盘子空时,爸爸或妈妈才可向盘子中放一个水果。仅当盘子中有自己需要的水果时,儿子或女儿可以从盘子中取出水果。
互斥关系:父亲、母亲对缓冲区的互斥访问
同步关系:
- 父亲前生产苹果,女儿后消费
- 母亲前生产,儿子后消费
- 缓冲区为空,才能生产水果。“盘子为空”这个事件可以由儿子或女儿触发,事件发生后才允许父亲/母亲放水果
如何实现?
int mutex = 1; //实现互斥访问盘子(缓冲区)
int apple = 0; //盘子中有几个苹果
int orange = 0; //盘子中有几个橘子
int plate = 1; //盘子中还可以放多少个水果
dad() { while(1) { 生产苹果 wait(plate) //消耗缓冲区盘子wait(mutex) //互斥访问
把苹果放入盘子
signal(mutex)signal(apple) //生产苹果
}
}mom() {
while(1) {
生产橘子
wait(plate) //消耗缓冲区盘子wait(mutex) //互斥访问
把橘子放入盘子
signal(mutex)signal(orange) //生产苹果
}
}daughter() {
while(1) {
wait(apple) //消耗苹果wait(mutex)
消耗
signal(mutex)signal(plate) //增加缓冲区数量,盘子为空
}
}son() {
while(1) {
wait(orange) //消耗橘子wait(mutex)
消耗
signal(mutex)signal(plate) //增加缓冲区数量,盘子为空
}
}
6. 读者写者问题
有读者和写者两组发进程,共享一个文件,
- 允许多个读者可以同时对文件读;(与消费者进程不同,读进程在读数据后并不会将数据清空,并不会改变数据。因此多个读者可同时访问共享数据)
- 同时只允许一个写者往文件中写信息;
- 写在完成前,不允许其他读或写;
- 写在执行前,必须等读和写全部退出。 读进程与写进程同时共享数据,可能导致读出的数据不一致的问题
互斥关系:
- 写进程和写进程之间,互斥访问共享文件
- 写进程和读进程之间,也要实现互斥
int rw = 1; //文件锁:控制读写互斥
int count = 0. //当前正在读的读者数量。读进程不需要互斥读文件,所以只有第一个读进程时才会在读之前加锁
int countMutex = 1. //保护 count 变量的锁
int wmutex = 1. //排队锁:防止写者一直等不到。
//解决的问题:由于只有count == 0 最后一个读进程负责读完了才能解锁rw,所以如果一直有读进程,则写进程就会一直阻塞等大。 wmutex保证实现多个读进程在等待读时,写进程不会被阻塞
writer() { while(1) { wait(wmutex) // 排队(和读者公平竞争入场资格wait(rw) //互斥访问共享文件
写文件
signal(rw) //写完了解锁signal(wmutex).
}
}reader() {
while(1) {
// -------- 进入前 --------
wait(wmutex) //先排队,和写者公平竞争保证。//实现只有第一个读进程才会对共享文件加锁,实现读写互斥、多个读进程同时读
wait(countMutex) //读进程互斥访问count
if(count == 0){ //只有第一个读进程才会在读之前加锁
wait(rw) //互斥访问共享文件,加锁
}
count++
signal(countMutex) // 排队完成,释放排队锁signal(wmutex)
读文件...
// -------- 离开后 --------
wait(countMutex) //各读进程互斥访问count
count--
if(count == 0) { //由最后一个读进程负责读完了〝解锁"共享文件
signal(rw)
}
signal(countMutex)
}
}
结论:在这种算法中,连续进入的多个读者可以同时读文件;写者和其他进程不能同时访问文件;写者不会饥饿,但也并不是真正的“写优先”,而是相对公平的先来先服务原则。有的书上把这种算法称为读写公平法
为什么要有 count?
多个读者可以同时读,不能每个读者都去抢 rw 锁(那样就变成读者之间也互斥了)。所以约定:
第一个进来的读者 → 负责抢 rw 锁(挡住写者)
最后一个离开的读者 → 负责还 rw 锁(放开写者)
中间的读者 → 直接读,不管锁
为什么要有 countMutex?
count++ 和 count-- 本身不是原子操作,两个读者同时执行会出错,所以需要一把小锁专门保护 count。
为什么要有 wmutex?
没有它会发生写者饥饿:读者源源不断进来,count 永远不为 0,rw 锁永远不释放,写者永远等不到。
加了 wmutex 之后,读者进入前必须先在 wmutex 排队,写者也在同一个队里排,大家公平竞争,写者不会被无限插队。
7. 哲学家进餐问题
一张圆桌上坐着5名哲学家,每两个哲学家之间的桌上摆一根筷子,桌子的中间是一碗米饭。
- 哲学家可以思考和进餐。
- 哲学家思考时,并不影响他人。
- 哲学家饥饿时:试图分别两次拿起左、右两根筷子
- 如果筷子已在他人手上,则需等待
- 同时拿起两根筷子才可以开始进餐
- 当进餐完毕后,放下筷子继续思考
哲学家问题引出了死锁现象。临界资源分配不当时,每一个哲学家都在等待其他哲学家释放资源,而都不会释放已有的资源,就会无限等待
如何解决死锁的发生?
- 可以对哲学家进程施加一些限制条件,比如最多允许四个哲学家同时进餐。这样可以保证至少有一个哲学家不会阻塞。
- 要求奇数号哲学家先拿左边再拿右边的筷子,而偶数号哲学家刚好相反。用这种方法可以保证如果相邻的哲学家同时想吃饭时就会有一个阻塞。那么只会有其中一个可以拿起第一只筷子,另一个会直接阻塞。这就避免了占有一支后再等待另一只的情况。
3. 仅当一个哲学家左右两支筷子都可用时才允许他抓起筷子。各哲学家拿筷子这件事必须互斥的执行。即要拿就两根一起拿,拿不到就一根都不拿,不允许出现"拿了一半在等"的状态
int chopstick[5] = {1,1,1,1,1} //表示5个筷子的互斥访问。并对哲学家按0~4编号,哲学家i左边的筷子编号为i,右边的筷子编号为(i+1)%5。 int mutex = 1 //互斥的取筷子pi() {
while(1) {
P(mutex)
P(chopstick[i])
P(chopstick[(i+1)%5])
V(mutex)
吃饭
V(chopstick[i])
V(chopstick[(i+1)%5])
}
}
5.7 管程
信号量实现互斥与同步存在的问题:编写程序困难、易出错。而管程是一种简便的PV操作,使用了封装思想,实现进程的同步/互斥
管程是一种实现进程同步机制的一种特殊软件模块,组成:
- 共享数据结构
- 对该数据结构进行操作的一组过程(函数)
- 对共享数据的初始化
- 管程的名字
管程的基本特征:
- 管程的数据只能被管程的过程(函数)访问,私有访问。
- 进程只能通过管程的过程才能访问管程的共享数据
- 每次只允许一个进程执行管程
管程默认的实现:
- 管程默认仅允许一个进程执行,实现进程互斥访问
- 管程中包含了等待/唤醒操作,解决同步问题
1. 管程解决成产者消费者问题
synchronized关键字:这个函数同一时间段内只能被一个线程调用
class Monitor { condition full, empty; // 条件变量用来实现同步(排队) int count = 0; // 缓冲区中的产品数synchronized void insert(Item item) { // 把产品item放入缓冲区
if (count N) {
wait(full);
}
count++;
insert_item(item);
if (count 1) {
signal(empty);
}
}synchronized Item remove() { // 从缓冲区中取出一个产品
if (count 0) {
wait(empty);
}
count--;
if (count N-1) {
signal(full);
}
return remove_item();
}
}producer() {
while(1) {
item = 生产一个产品;
Monitor.insert(item)
}
}consumer() {
while(1) {
item = Monitor.remove(item)
消费item
}
}
5.8 死锁
死锁:每个进程都占用资源,又互相等待对方的资源,导致各进程都阻塞,无法向前推进
- 至少两个以上的进程
- 进程一定处于阻塞态
饥饿:由于长期得不到想要的资源,某进程无法向前推进的现象。
- 可能只有一个进程发生饥饿
- 进程可能是阻塞态(等待IO设备),也可能是就绪态(等待CPU)
死循环:某进程执行过程中一直跳不出某个循环的现象。 - 可能只有一个进程发生死循环
- 进程是运行态(CPU上执行)
死锁和饥饿问题是由于操作系统分配资源不合理导致的,而死循环是由代码逻辑错误导致的。
死锁产生的必要条件
产生死锁必须同时满足一下四个条件,只要其中任一条件不成立,死锁就不会发生。
- 互斥。对临界资源的互斥访问。只有对必须互斥使用的资源的争抢才会导致死锁
- 不剥夺条件。进程的资源只能主动释放,不能由其他进程剥夺
- 请求和保持。进程已经持有资源并对已有的资源保持不放,又等待其他进程的资源
- 循环等待。存在一种进程资源的循环等待链,链中的每一个进程己获得的资源同时被下一个进程所请求。
注意:发生死锁时一定有循环等待,但是发生循环等待时未必死锁(循环等待是死锁的必要不充分条件)
死锁问题的处理策略
- 预防死锁。破坏死锁产生的四个必要条件中的一个或几个。
- 避免死锁。用某种方法防止系统进入不安全状态,从而避免死锁(银行家算法)
- 死锁的检测和解除。允许死锁的发生,不过操作系统会负责检测出死锁的发生,然后取某种措、施解除死锁。
1. 死锁的处理策略——预防死锁
- 破坏互斥条件。
- 将临界资源改造为可共享使用的资源。(如SPOOLing技术,将独占的设备在逻辑上改造成共享的)
缺点:并不是所有的资源都可以改造成可共享使用的资源。并且为了系统安全,很多地方还必须保护这种互斥性。因此,很多时候都无法破坏互斥条件。
- 破坏不可剥夺条件
- 申请的资源得不到满足时,立即释放拥有的所有资源
- 申请的资源被其他进程占用时,由操作系统剥夺(考虑优先级)
缺点:
- 实现复杂。
- 释放已获得的资源可能造成前一阶段工作的失效。因此这种方法一般只适用于易保存和恢复状态的资源,如CPU。
- 反复地申请和释放资源会增加系统开销,降低系统吞吐量。
- 若采用方案一,意味着只要暂时得不到某个资源,之前获得的那些资源就都需要放弃,以后再重新申请。如果一直发生这样的情况,就会导致进程饥饿。
- 破坏请求和保持条件
- 运行提前分配好所有需要的资源,之后一直保持。即进程在运行前一次申请完它所需要的全部资源,在它的资源未满足前,让它投入运行。一旦投入运行后,这些资源就一直归它所有,该进程就不会再请求别的任何资源。
缺点:
- 一直保持资源资源利用率低
- 可能导致等待的进程饥饿
- 破坏循环等待条件
给资源编号,规定每个进程必须按编号递增的顺序请求资源,同类资源(即编号相同的资源)一次申请完。
缺点:
- 不方便增加新设备;
- 会导致资源浪费;小编号的资源申请后长时间未使用。
- 用户编程麻烦
2. 死锁的处理策略——避免死锁
安全序列:一个进程的执行顺序 P₁ → P₂ → … → Pₙ。按这个顺序依次分配资源,每个进程都能拿到它所需的全部资源、顺利运行完、然后释放所有占有的资源。
安全状态:系统中至少能找出一个安全序列。
不安全状态:分配了资源后,一个安全序列都找不出来。不安全状态 ≠ 死锁
| 状态 | 是否死锁 |
|---|---|
| 安全状态 | 一定不会死锁 |
| 不安全状态 | 可能死锁,也可能不死锁。 不安全状态也可能因为某进程提前释放资源而自动恢复到安全状态 |
银行家算法:在进程提出资源申请时,先预判此次分配是否会进入不安全状态。如果会进入不安全状态,就暂时不答应这次请求,让该进程先阻塞等待。
例如:系统中有5个进程P0~P4,3种资源 R0~R2,初始数量为(10,5, 7),每个进程的需求和已分配的资源如下,判断此时系统是否处于安全状态?
资源总数(10, 5, 7),剩余可用资源(3,3,2)
方法一:
依次检查剩余可用资源(3, 3,2)是否能满足各进程的需求。
- 可满足P1需求,将P1加入安全序列,并更新剩余可用资源值为(5, 3,2)
- 依次检查剩余可用资源(5, 3, 2)是否能满足剩余进程(不包括已加入安全序列的进程)的需求。可满足P3需求,将P3加入安全序列,并更新剩余可用资源值为(7,4,3)
- 依次检查剩余可用资源(7,4, 3) 是否能满足剩余进程(不包括已加入安全序列的进程)的需求⋯•
以此类推,找出一个安全序列.{P1,P3,P0,P2,P4},共五次循环检查即可将5个进程都加入安全序列中,最终可得一个安全序列。该算法称安全性算法。
方法二:
- (3, 3,2)可以满足P1,P2,因此可把P1、P3先加入安全序列。剩余资源为(2, 0, 0) +(2, 1, 1)+(3, 3, 2)=(7, 4, 3)
- (7,4,3)可以满足剩余序列,因此可以找到一个安全序列
3. 死锁的处理策略——检测和消除
检测是否发生死锁,设计某种算法当发生死锁时从死锁状态解除
资源分配图:
- 进程结点:对应一个进程
- 资源结点:对应一类资源,一类资源可能有多个
- 请求边。进程结点——>资源结点:表示进程想申请几个资源(每条边代表一个)
- 分配边。资源节点——>进程结点:表示已经为进程分配了几个资源(每条边代表一个)
检测死锁算法
1)在资源分配图中,找出既不阻塞又不是孤点的进程Pi(即找出一条有向边与它相连,且该申请资源数量小于等于系统中空闲资源数量。如P1)。消去它所有的请求边和分配变,使之称为孤立的结点。
2) 进程Pi所释放的资源,根据1)中的方法进行一系列简化后若能消去途中所有的边,则称该图是可完全简化的。
死锁:如果某时刻系统的资源分配图是不可完全简化的,那么此时系统死锁、
解除死锁
用死锁检测算法化简资源分配图后,还连着边的那些进程就是死锁进程,如P1,P2
解除死锁的主要方法有:
- 资源剥夺法。挂起(暂时放到外存上)某些死锁进程,并抢占它的资源,将这些资源分配给其他的死锁进程。但是应防止被挂起的进程长时间得不到资源而饥饿。
- 撤销进程法(或称终止进程法)。强制撤销部分、甚至全部死锁进程,并剥夺这些进程的资源。这种方式的优点是实现简单,但所付出的代价可能会很大。因为有些进程可能已经运行了很长时间,已经接近结束了,一旦被终止可谓功亏一篑,以后还得从头再来。
- 进程回退法。让一个或多个死锁进程回退到足以避免死锁的地步。这就要求系统要记录进程的历史信息,设置还原点。
选择解除死锁进程的逻辑:
- 进程优先级。不重要优先
- 已执行多长时间。执行越短优先
- 进程的执行时间。执行时间少优先
- 进程已经使用的资源。越少优先
- 进程时交互式还是批处理式?批处理优先
六、内存
6.1 内存管理的概念
内存可存放数据。程序执行前需要先放到内存中才能被CPU处理
内存中也有一个一个的“小房间”,每个小房间就是一个“存储单元”
- 如果计算机“按字节编址”,则每个存储单元大小为 1字节,即 1B,即8个二进制位
- 如果字长为16位的计算机“按字编址”,则每个存储单元大小为1个字;每个字的大小为16个二进制位
内存管理的作用
- 内存空间的分配与回收
- 内存空间的扩充(实现虚拟性)。可以向用户提供一个比存储空间大得多的地址空间
- 地址转换。操作系统负责实现逻辑地址到物理地址的转换。
- 绝对装入:编译器负责地址转换(单道程序阶段,无操作系统)
- 可重定位装入:装入程序负责地址转换(早期多道批处理阶段)
- 动态运行时装入:运行时才进行地址转换,寄存器转换
- 存储保护:保证各进程在自己的内存空间内运行,不会越界访问
- 设置上下限寄存器
- 利用重定位寄存器、界地址寄存器进行判断
6. 2. 内存地址的转换
0. 进程的内存映像
1. 指令的工作原理
代码的执行要翻译成CPU能识别的指令。这些指令会告诉CPU应该去内存的哪个地址读/写数据,
这个数据应该做什么样的处理。
编译后指令的地址都是从0开始的,程序经过编译、链接后生成的指令中指明的是逻辑地址(相对地址),即:相对于进程的起始地址而言的地址
2. 物理地址和逻辑地址如何转换?
1. 绝对装入
在编译时,如果知道程序将放到内存中的哪个位置,编译程序将产生绝对地址的目标代码装入程序按照装入模块中的地址,将程序和数据装入内存。
只适用于单道程序环境。灵活性差
2. 静态重定位(可重定位装入)
- 设定编译、链接后的装入模块的地址都是从0开始的,指令中使用的地址、数据存放的地址都是相对于起始地址而言的逻辑地址。
- 装入内存时,对地址进行“重定位”,将逻辑地址变换为物理地址(地址变换是在装入时一次完成的)。
特点:
- 所需的地址空间是连续的
- 一个作业装入内存时,必须分配其要求的全部内存空间
- 在运行期间就不能再移动
3. 动态重定位(动态运行时装入)
- 设定编译、链接后的装入模块的地址都是从0开始的,指令中使用的地址、数据存放的地址都是相对于起始地址而言的逻辑地址
- 装入内存后,并不会立即把逻辑地址转换为物理地址,等待运行时重定位寄存器把地址转换为物理地址(绝对地址)
特点:
- 可将程序分配到不连续的存储区中;
- 在程序运行前只需装入它的部分代码
- 在程序运行期间,根据需要动态申请分配内存;
- 允许程序在内存中发生移动,便于程序段的共享
- 虚拟性。可以向用户提供一个比存储空间大得多的地址空间
6.3 内存管理方案——空间的分配与回收
连续分配:指用户进程分配的必须是一个连续的内存空间。
非连续分配:用户进程分配的可以是一些分散的内存空间。
内部碎片。分配给某进程的内存区域中,如果有些内存部分没有用上。
外部碎片。是指内存中的某些空闲分区由于太小而难以利用。
1. 单一连续分配
在单一连续分配方式中,内存被分为系统区和用户区。
- 系统区通常位于内存的低地址部分,用于存放操作系统相关数据;
- 用户区用于存放用户进程相关数据。
内存中只能有一个用户程序,用户程序独占整个用户区空间。
优点:实现简单;无外部碎片;可以采用覆盖技术扩充内存;不一定需要采取内存保护(
缺点:只能用于单用户、单任务的操作系统中;有内部碎片;存储器利用率极低。、
2. 固定连续分配
于是将整个用户区划分为若干个固定的分区,在每个分区中只装入一个进程,这样就形成了最早的、最简单的一种可运行多道程序的内存管理方式。
优点:实现简单,无外部碎片。
缺点:
- 当用户程序太大时,可能所有的分区都不能满足需求,此时不得不采用覆盖技术来解决,但这又会降低性能;
- 会产生内部碎片,内存利用率低。
固定分区分配又分为:
- 分区大小相等。缺乏灵活性,但是很适合用于用一台计算机控制多个相同对象的场合
- 分区大小不等。增加了灵活性,可以满足不同大小的进程需求。根据常在系统中运行的作业大小情况进行划分(比如:划分多个小分区、适量中等分区、少量大分区)
如何记录内存中各个分区的大小和使用情况呢?
分区说明表。操作系统需要建立一个数据结构来实现各个分区的分配与回收。了解各个分区的使用情况。每个表项对应一个分区,通常按分区大小排列。每个表项包括对应分区的大小、起始地址、状态(是否己分配)。
当某用户程序要装入内存时,由操作系统内核程序根据用户程序大小检索该表,从中找到一个能满足大小的、未分配的分区,将之分配给该程序,然后修改状态为“己分配”。
3. 动态分区分配
动态分区分配又称为可变分区分配。这种分配方式不会预先划分内存分区,而是在进程装入内存时,根据进程的大小动态地建立分区,分区的大小等于进程的需要的大小。因此系统分区的大小和数目是可变的。
没有内部碎片,但有外部碎片
紧凑技术。如果内存中空闲空间的总和本来可以满足某进程的要求,但由于进程需要的是一整块连续的内存空间,因此这些“碎片”不能满足进程的需求。可以通过紧凑(拼凑,Compaction)技术来解决外部碎片。
- 通过将内存中的进程进行挪位置,将空闲空间合并成一个大的空闲空间
- 适合动态重定位的方式进行逻辑地址的映射
- 涉及中级调度,将进程在内存和外存中换入换出。
- 重新进入内存后需要将进程的地址重新替换掉
系统要用什么样的数据结构记录内存的使用情况?
使用空闲分析表或者空闲分区列记录内存中的空闲分区
- 空闲分析表
每个空闲分区对应一个表项。表项中包含分区号、分区大小、分区起始地址等信息
- 空闲分析链
每个分区的起始部分和末尾部分分别设置前向指针和后向指针,存储前后分区的地址。起始部分处还可记录分区大小等信息
当很多个空闲分区都能满足需求时,应该选择哪个分区进行分配?
把一个新作业装入内存时,须按照一定的动态分区分配算法,从空闲分区表(或空闲分区链)中选出一个分区分配给该作业。
动态分区算法分为:首次适应、最佳适应、最坏适应临近适应
如何进行分区的回收操作?
内存回收:进程结束内存回收后,如果发现有一些内存是相邻的,便把相邻的分区合并
4. 基本分页存储管理
内存分为分页框。将内存空间分为一个个大小相等的分区,每个分区就是一个页框(页框=页帧=内存块=物理块=物理页面)。每个页框有一个编号,即页框号(页框号=页帧号=内存块号=物理块号=物理页号),页框号从0开始。
进程分为页。操作系统将进程的逻辑地址空间也分为与页框大小相等的一个个部分,每个部分称为一个页或页面。每个页面也有一个编号,即页号,页号也是从0开始。
操作系统的进程分配。进程的每个页分别放入内存的页框中。也就是说,进程的页面与内存的页框有一一对应的关系。以页框为单位为各个进程分配内存空间
页表。用来记录页框和页的一一对应关系。为了能知道进程的每个页面在内存中存放的位置,操作系统要为每个进程建立一张页表。注:页表通常存在PCB(进程控制块)中
- 一个进程对应一张页表
- 每个页表项由页号和块号组成
- 页表块号记录的是内存的块号,或叫做页内偏移量。而不是内存的起始地址。
- J号内存的起始地址 = J的内存块号 ✖️ 内存块的大小
- 进程的每个页面对应一个页表项
- 页表记录进程页面和实际存放的内存块之间的映射关系
页表寄存器。通常会在系统中设置一个页表寄存器(PTR),存放页表在内存中的起始地址F和页表长度M。
- 进程未执行时,页表的始址 和 页表长度放在进程控制块(PCB)中
- 当进程被调度时,操作系统内核会把它们放到页表寄存器中。
每个页表项应该多大,占几个字节?
页表的大小。
Eg:假设某系统物理内存大小为4GB,页面大小为 4KB,则每个页表项至少应该为多少字节?
- 内存块大小=页大小=4KB=212B
- 4GB的内存总共会被分 232/212=220个内存块
- 内存块号的范围应该是0~220-1
- 内存块号至少要用 20 bit 来表示
- 至少要用3B(字节)来表示块号。 3✖️8 = 24bit
- 由于页号是隐含的,因此每个页表项占3B,存储整个页表至少需要3✖️(n+1)字节
页号的大小。页表项连续存放,因此页号可以是隐含的,不占存储空间
将进程地址空间分页之后,操作系统该如何实现逻辑地址到物理地址的转换?
虽然进程的各个页面是离散存放的,但是页面内部是连续存放的,如果要访问逻辑地址A的物理地址,则:
- 确定逻辑地址 A 对应的进程页号 P
- 找到页号 P 在内存中对应的页框(查页表),确认内存中的起始地址
- 确定进程逻辑地址 A 的页内偏移量 W,也就是地址偏移量
逻 辑 地 址 对 应 的 物 理 地 址 页 号 在 内 存 中 的 起 始 地 址 页 内 偏 移 量
页 号 逻 辑 地 址 页 面 长 度 ( 取 除 法 的 整 数 部 分 )
页 内 偏 移 量 逻 辑 地 址 页 面 长 度 ( 取 除 法 的 余 数 部 分 )
Eg:在某计算机系统中,页面大小是50B。某进程逻辑地址空间大小为200B,则逻辑地址110对应
的页号、页内偏移量是多少?
页面大小刚好是2的整数幂有什么好处?
- 逻辑地址的拆分更加迅速。如果每个页面大小为 2KB,用二进制数表示逻辑地址,则末尾K位即为页内偏移量,其余部分就是页号。因此,如果让每个页面的大小为2的整数幂,计算机硬件就可以很方便地得出一个逻辑地址对应的页号和页内偏移量,而无需进行除法运算,从而提升了运行速度。
- 物理地址的计算更加迅速。将二进制表示的页号和页内偏移量拼接起来,就可以得到最终的物理地址。
- 传统算法。根据页号可以查询页表,而页表中记录的只是内存块号,而不是内存块的起始地址!J号内存块的起始地址=J* 内存块大小,需要费时的乘法运算
- 传统算法。根据页号可以查询页表,而页表中记录的只是内存块号,而不是内存块的起始地址!J号内存块的起始地址=J* 内存块大小,需要费时的乘法运算
4.2 基于快存的分页存储管理
快表,又称联想寄存器(TLB),是一种访问速度比内存快很多的高速缓存(TLB不是内存!),用来存放最近访问的页表项的副本,可以加速地址变换的速度。与此对应,内存中的页表常称为慢表。
4.3 二级页表
页表必须连续存放,因此当页表很大时,需要占用很多个连续的页框。
没有必要让整个页表常驻内存,因为进程在一段时间内可能只需要访问某几个特定的页面。
- 在需要访问页面时才把页面调入内存(虚拟存储技术)。可以在页表项中增加一个标志位,用于表示该页面是否已经调入内存
- 若想访问的页面不在内存中,则产生缺页中断(内中断),然后将目标原面从外存调入内存
可将长长的页表进行分组,使每个内存块刚好可以放入一个分组。要为离散分配的页表再建立一张页表,称页目录表,或称外层页表,或称顶层页表
5. 基本分段存储管理
进程的地址空间:按照程序分为若干个段,每个段都有一个段名,每段从0开始编址
内存分配规则:以段为单位进行分配,每个段在内存中占据连续空间,但各段之间可以不相邻。
段表。记录段在内存中的位置。记录了段号、段的长度、内存地址。
页的大小固定且由系统决定。段的长度却不固定,决定于用户编写的程序。
- 页是信息的物理单位。分页的主要目的是为了实现离散分配,提高内存利用率。分页仅仅是系统管理上的需要,完全是系统行为,对用户是不可见的。
- 段是信息逻辑的单位。分段的主要目的是更好地满足用户需求。一个段通常包含着一组属于一个逻辑模块的信息。分段对用户是可见的,用户编程时需要显式地给出段名。
分页的用户进程地址空间是一维的,页表号和内存地址
分段的用户进程地址空间是二维的,段号、内存地址、段的长度
分段比分页更容易实现信息的共享和保护。该功能段用来判断缓冲区此时是否可访问。允许所有生产者、消费者进程共享访问
访问一个逻辑地址需要几次访存?
- 分页(单级页表):第一次访存—一查内存中的页表,第二次访存—一访问目标内存单元。总共两次
访存 - 分段:第一次访存一一查内存中的段表,第二次访存一一访问目标内存单元。总共两次访存
6. 段页式存储管理
将进程按逻辑模块分段,再将各段分页(如每个页面4KB),再将内存空间分为大小相同的内存块
段表、页表
段表记录了:段好、页表长度、页表的内存号
页表记录了:页号、进程的内存号
逻辑地址转换?
- 由逻辑地址得到段号、页号、页内偏移量
- 段号与段表寄存器中的段长度比较,检查是否越界
- 由段表始址、段号找到对应段表项
- 根据段表中记录的页表长度,检查页号是否越界
- 由段表中的页表地址、页号得到查询页表,找到相应页表项
- 由页面存放的内存块号、页内偏移量得到最终的物理地址
- 访问目标单元
访问一个逻辑地址所需访存次数?
三次。第一次—查段表、第二次——查页表、第三次——访问目标单元
可引入快表机构,以段号和页号为关键字查询快表,即可直接找到最终的目标页面存放位置。引入快表后仅需一次访存
6.4 内存空间扩充技术
用来解决“程序大小超过物理内存总和”的问题
1. 覆盖技术
将程序分多个段。常用的段常驻内存和不常用的段。内存中分为一个“固定区”和若干个“覆盖区”。需要常驻内存的段放在“固定区”中,调入后就不再调出(除非运行结束),不常用的段在需要时调入内存
按照自身逻辑结构,让那些不可能同时被访问的程序段共享同一个覆盖区。必须由程序员声明覆盖结构,操作系统完成自动覆盖。
缺点:对用户不透明,增加了用户编程负担。
2. 交换技术
进程在内存和外存(磁盘)之间动态调度。内存空间紧张时,系统将内存中某些进程暂时换出外存,把外存中某些已具备运行条件的进程换入内存
涉及中级调度/内存调度,就是要决定将哪个处于挂起状态的进程重新调入内存
-
应该在外存的什么位置保存被换出的进程?
- 具有对换功能的操作系统中,通常把磁盘空间分为文件区和对换区两部分。
- 文件区。主要用于存放文件,主要追求存储空间的利用率,因此对文件区空间的管理采用离散分配方式;对换区空间只占磁盘空间的小部分,被换出的进程数据就存放
- 对换区。由于对换的速度直接影响到系统的整体速度,因此对换区空间的管理生要追求换入换出速度,因此通常对换区采用连续分配方式。对换区的I/O速度比文件区的更快。
-
什么时候应该交换?
交换通常在许多进程运行且内存吃紧时进行,而系统负荷降低就暂停。例如:在发现许多进程运行时经常发生缺页,就说明内存紧张,此时可以换出一些进程;如果缺页率明显下降,就可以暂停换出。 -
应该换出哪些进程?
可优先换出阻塞进程;可换出优先级低的进程;为了防止优先级低的进程在被调入内存后很快又被换出,有的系统还会考虑进程在内存的驻留时间⋯
注意:PCB 会常驻内存,不会被换出外存
覆盖和交换的区别?
- 覆盖是在同一个程序或进程中的
- 交换是在不同进程(或作业)之间的
3. 虚拟存储技术
6.5 虚拟存储技术
1. 虚拟内存
在操作系统的管理下,在用户看来似乎有一个比实际内存大得多的内存,这就是虚拟内存
传统存储管理方式的特征、缺点
一次性:作业必须一次性全部装入内存后才能开始运行。这会造成两个问题:导致大作业无法分配内存;只有少量作业能运行,
驻留性:一旦作业被装入内存,就会一直驻留在内存中,直至作业运行结束。
虚拟内存技术基于局部性原理
- 时间局部性:如果行了程序中的某条指令,那么不久后这条指令很有可能再次执行
- 空间局部性:一旦程序访问了某个存储单元,在不久之后,其附近的存储单元也很有可能被访问。(因为很多数据在内存中都是连续存放的,并且程序的指令也是顺序地在内存中存放的)
虚拟内存的定义
- 基于局部性原理,在程序装入时,可以将程序中很快会用到的部分装入内存,暂时用不到的部分留在外存,就可以让程序开始执行。
- 在程序执行过程中,当所访问的信息不在内存时,由操作系统负责将所需信息从外存调入内存,然后继续执行程序
- 若内存空间不够,由操作系统负责将内存中暂时用不到的信息换出到外存。
虚拟内存有一下三个主要特征:
多次性:无需在作业运行时一次性全部装入内存,而是允许被分成多次调入内存。
对换性:在作业运行时无需一直常驻内存,而是允许在作业运行过程中,将作业换入、换出。
虚拟性:从逻辑上扩充了内存的容量,使用户看到的内存容量,远大于实际的容量。
虚拟内存技术,允许一个作业分多次调入内存。如果采用连续分配方式,会不方便实现。因此,
虚拟内存的实现需要建立在离散分配的内存管理方式基础上。即分页存储管理、分段存储管理、段页式存储管理
实现虚拟内存需要操作系统要提供请求调页(请求调页)功能和页面置换(段置换)功能
2. 请求分页存储管理
请求分页存储管理与基本分页存储管理的主要区别:
- 在程序执行过程中,当所访问的信息不在内存时,由操作系统负责将所需信息从外存调入内存,然后继续执行程序。(请求调页)
- 若内存空间不够,由操作系统负责将内存中暂时用不到的信息换出到外存。(页面置换)
- 需要在页表中修改新增的页面的表项
请求分页的页表字段
- 状态位:是否已调入内存
- 访问字段:可记录最近被访问过凡次,或记录上次访问的时间,供页面置换算法选择换出页面时参考
- 修改位:页面调入内存后是否被修改过。有的页面没有被修改过,就不用再浪费时间写回外存。有的页面修改过,就需要将外存中的旧数据覆盖
- 外存地址:页面在外存中的存放位置
缺页中断
缺页中断是因为当前执行的指令想要访问的目标页面未调入内存而产生的,因此属于内中断
一条指令在执行期间,可能产生多次缺页中断。
缺页中断机构
假设此时要访问逻辑地址=(页号,页内偏移量)=(0,1024)
在请求分页系统中,每当要访问的页面不在内存时,便产生一个缺页中断,然后由操作系统的缺页中断处理程序处理中断。此时缺页的进程阻塞。放入阻塞队列,调页完成后再将其唤醒,放回就绪队列。
- 如果内存中有空闲块,则为进程分配一个空闲块,将所缺页面装入该块,并修改页表中相应的页表项。
- 如果内存中没有空闲块,则由页面置换算法选择一个页面淘汰,若被置换的页面在内存期间被修改过,则要将其写回外存。未修改过的页面不用写回外存。
地址变换机构
- 找到页表项是需要检查页面是否在内存中
- 若页面不再内存中,需要请求调页
- 若内存空间不够,还需换出页面
- 页面调入内存后,需要修改相应页表项
3. 页面置换算法
请求分页存储管理若内存空间不够,由操作系统负责将内存中暂时用不到的页面换出到外存。页面置换算法决定选择哪个页面换出
页面的换入、换出需要磁盘IO,会有较大的开销因此好的页面置换算法应该追求更少的缺页率
3.1. 最佳置换算法(OPT)
优先淘汰最长时间内不会被访问的页面
实现:每次选择淘汰的页面将是以后永不使用,或者在最长时间内不再被访问的页面,这样可以保证最低的缺页率。
缺页率最小,性能最好;
但无法实现,操作系统无法提前预判页面访问序列
3.2. 先进先出置换算法(FIFO)
优先淘汰最先进入内存的页面
实现方法:把调入内存的页面根据调入的先后顺序排成一个队列,需要换出页面时选择队头页面即可。队列的最大长度取决于系统次进程分配了多少个内存块。
实现简单;但性能很差,
可能出现Belady异常。当为进程分配的内存块越多时,缺页次数不减反增的异常现象。
3.3.最近最久未使用置换算法(LRU)
优先淘汰最近最久没访问的页面
实现方法:赋予每个页面对应的页表项中,用访问字段记录该页面自上次被访问以来所经历的时间t。当需要淘汰一个页面时,选择现有页面中t值最大的,即最近最久未使用的页面。
在手动做题时,若需要淘汰页面,可以逆向检查此时在内存中的几个页面号。在逆向扫描过程中最后一个出现的页号就是要淘汰的页面。
性能很好;但需要硬件记录t
支持,算法开销大
3.4.时钟置换算法(CLOCK)/最近未用算法NRU
置换最近没有被访问过的。循环扫描各页面。第一轮淘汰访问位=0的,并将扫描过的页面访问位改1。若第一轮没选中,则进行第二轮扫描。
简单的CLOCK算法实现方法:为每个页面设置一个访问位,再将内存中的页面都通过链接指针链接成一个循环队列。当某页被访问时,其访问位置1。当需要淘汰一个页面时,只需检查页的访问位。
如果是0,就选择该页换出:如果是1,则将它置 0,暂不换出,继续检查下一个页面,若第一轮扫
描中所有页面都是1,则将这些页面的访问位依次置为0后,再进行第二轮扫描(第二轮扫描中一定会有访问位为0的页面,因此简单的CLOCK 算法选择一个淘汰页面最多会经过两轮扫描)
实现简单,算法开销小;
但未考虑页面是否被修改过。
3.5.改进型的时钟置换算法
淘汰最近没有被访问过+没有被修改过的页面
简单的时钟置换算法仅考虑到一个页面最近是否被访问过。事实上,如果被淘汰的页面没有被修改过,就不需要执行IO操作写回外存。只有被淘汰的页面被修改过时,才需要写回外存。
因此,除了考虑一个页面最近有没有被访问过之外,操作系统还应考虑页面有没有被修改过。在其他条件都相同时,应优先淘汰没有修改过的页面,避免IO操作。这就是改进型的时钟置换算法的思想。
若用(访问位,修改位)的形式表述,则
第一轮:从当前位置开始扫描到第一个(0,0)的帧用于替换。本轮扫描不修改任何标志位
第二轮:若第一轮扫描失败,则重新扫描,查找第一个(0,1)的帧用于替换。本轮将所有扫描过的帧访问位设为0
第三轮:若第二轮扫描失败,则重新扫描,查找第一个(0,0)的帧用于替换。本轮扫描不修改任何标志位
第四轮:若第三轮扫描失败,则重新扫描,查找第一个(0, 1)的帧用于替换。
由于第二轮已将所有帧的访问位设为0,因此经过第三轮、第四轮扫描一定会有一个帧被选中,因此改进型CLOCK置换算法选择一个淘汰页面最多会进行四轮扫描
算法开销较小,性能也不错
4. 页面分配策略
驻留集:指请求分页存储管理中给进程分配的内存块的集合。在采用了虚拟存储技术的系统中,驻留集大小一般小于进程的总大小。
- 驻留集太小。缺页频繁,系统要花大量的时间来处理缺页,实际用于进程推进的时间很少;
- 驻留集太大。又会导致多道程序并发度下降,资源利用率降低。
所以应该选择一个合适的驻留集大小。
驻留集分配策略:
- 固定分配:操作系统为每个进程分配一组固定数目的内存块,在进程运行期间不再改变。即,驻留集大小不变
- 可变分配:先每个进程分配一定数目的内存块,在进程运行期间,可根据情况做适当的增加或减少。即,驻留集大小可变
发生缺页进行页面置换时,页面置换的范围是什么?
- 局部置换。发生缺页时只能置换进程自己的内存块
- 全局置换:可以将操作系统保留的空闲内存块分配给缺页进程,也可以将别的进程持有的内存块置換到外存,再分配给缺页进程。
页面分配、置换的组合
全局置换意味着一个进程拥有的内存块数量必然会改变,因此不可能是固定分配
-
固定分配局部置换:系统每个进程分配一定数量的内存块,在整个运行期间都不改变。若进程在运行中发生缺页,则只能从该进程在内存中的页面中选出一页换出,然后再调入需要的页面。
缺点是:很难确定。很难在刚开始就确定应每个进程分配多少个内存块才算合理。 -
可变分配全局置换:刚开始会每个进程分配一定数量的内存块。操作系统会保持一个空闲内存块队列。当某进程发生缺页时,从空闲内存块中取出一块分配给该进程;若已无空闲内存块,则可选择一个未锁定的页面换出外存,再将该内存块分配给缺页的进程。采用这种策略时,只要某进程发生缺页,都将获得新的内存块,仅当空闲内存块用完时,系统才选择一个未锁定的页面调出。
被选择调出的页可能是系统中任何一个进程中的页,因此这个被选中的进程拥有的内存块会减少,缺页率会增加。 -
可变分配局部置换:刚开始会为每个进程分配一定数量的内存块。当某进程发生缺页时,只允许从该进程自己的内存块中选出一个进行换出外存。如果进程在运行中频繁地缺页,系统会为该进程多分配几个内存块,直至该进程缺页率趋势适当程度;反之,如果进程在运行中缺页率特别低,则可适当减少分配给该进程的内存块。
可变分配全局置换:只要缺页就给分配新内存块
可变分配局部置换:要根据发生缺页的频率来动态地增加或减少进程的内存块
调页策略,决定什么时候调入所需的页面
-
预调页策略。根据局部性原理,一次调入若干个相邻的页面可能比一次调入一个页面更高效。但如果提前调入的页面中大多数都没被访问过,则又是低效的。因此可以预测不久之后可能访问到的页面,将它们预先调入内存,但目前预测成功率只有50%左右。故这种策略主要用于进程的首次调入,进程运行前调入,由程序员指出应该先调入哪些部分。
-
请求调页策略:进程在运行期间发现缺页时才将所缺页面调入内存。由这种策略调入的页面一定会被访问到,但由于每次只能调入一页,而每次调页都要磁盘IO操作,因此IO开销较大。
从何处调入页面?
外存(磁盘)分为对换区和文件区
- 对换区。读/写速度更快,采用连续分配方式
- 文件区。读/写速度更慢,采用离散分配方式
-
系统拥有足够的对换区空间:页面的调入、调出都是在对换区进行,这样可以保证页面的调入、调出速度很快。
在进程运行前,需将进程相关的数据从文件区复制到对换区。 -
系统缺少足够的对换区空间:凡是不会被修改的数据都直接从文件区调入,由于这些页面不
会被修改,因此换出时不必写回磁盘,下次需要时再从文件区调入即可。
对于可能被修改的部分,换出时需写回对换区,下次需要时再从对换区调入。
抖动颠簸现象
刚刚换出的页面马上又要换入内存,刚刚换入的页面马上又要换出外存,这种频繁的页面调度行为称为抖动,或颠簸。
产生抖动的主要原因是进程频繁访问的页面数目高于可用的物理块数(分配给进程的内存块不够)
颠簸解决方案
页面分配的内存块少,发生颠簸;多,则降低系统并发度和利用率,则提出工作集
驻留集:指请求分页存储管理中给进程分配的内存块的集合。
工作集:指在某段时间间隔里,进程实际访问页面的集合。
一般来说,驻留集大小不能小于工作集大小,否则进程运行过程中将频繁缺页。
运行过程中操作系统会设置窗口尺寸,根据“窗口尺寸”来算出工作集。
例:某进程的页面访问序列如下,窗口尺寸为4,
工作集大小可能小于窗口尺寸,实际应用中,操作系统根据工作集大小调整内存块的分配,根据工作集大小给进程分配若干内存块。如:窗口尺寸为5,经过一段时间的监测发现某进程的工作集最大为3,那么说明该进程有很好的局部性,可以给这个进程分配3个以上的内存块即可满足进釋的运行需要。
5. 内存映射文件
内存映射文件—一操作系统向上层程序员提供的文件的系统调用。请求操作系统将文件映射到进程的虚拟地址空间
- 方便程序员访问文件数据
- 方便多个进程共享一个文件
优点:
- 程序员编程更简单,已建立映射的文件,只需按访问内存的方式读写即可
- 文件数据的读入/写出完全由操作系统负责,IO效率可以由操作系统负责优化
七、文件
文件的逻辑结构
文件的物理结构
如何为文件分配磁盘块?
虚拟文件系统
向上层用户进程提供统一标准的系统调用接口,屏蔽底层具体文件系统的实现差异
八、IO设备
操作系统的设备管理
IO控制方式
IO软件层次顺序
IO系统/IO核心子系统
假脱机技术
九、磁盘
进程请求磁盘的调度顺序











































































































































浙公网安备 33010602011771号