操作系统概念(第九版)唐杰译
第一章:导论
一、操作系统的功能
1、几个概念
操作系统:是管理计算机硬件的程序
计算机系统:(四个组件)硬件,操作系统,应用程序,用户
硬件:cpu,内存,I/O 设备
2、几个角度看操作系统的功能
用户:操作系统主要方便用户使用
系统角度:操作系统是一个资源分配器,它应该考虑如何为各个程序和用户分配资 源,以便计算机系统能高效公平的运行。
操作系统是一个控制程序,它管理用户程序的执行,防止计算机资源的不当使用, 特别注意I/O设备的运行和控制。
操作系统是一直运行在计算机上的程序(内核),还有两类其他程序,系统程序和 应用程序。
我的观点:操作系统是为了让用户更好使用计算机硬件和软件资源的一个应用程序。
二、计算机系统的组成
1、计算机系统的运行
计算机系统主要包括一个或多个CPU和若干设备控制器,通过公用总线相连。
中央处理器,磁盘控制器,USB控制器,图形适配器,内存
开机过程:
开机运行引导程序(简单,一般在固件,ROM)
引导程序必须知道如何加载操作系统并开始执行系统。
内核加到内存后运行,也会启动一些系统进程。
然后等待事件的发生,事件可以通过硬件或者软件(例如系统调用)来通知。
2、存储结构
内存,也叫随机访问内存(RAM),计算机运行的大多数程序都位于可读写的内存。
冯.诺依曼体系结构,指令执行周期:
从内存取指令->存到至指令寄存器-> 解码 -> 获取内存数据存到寄存器-> 执行运算 -> 可能把结果写回内存
存储金字塔:
寄存器 - 高速缓存 - 内存 - 固态硬盘 - 硬盘 - 光盘 - 磁带
3、I/O 结构
设备控制器管理某一特定类型的设备,维护这一定量的别地缓存存储,和一组特定 用途的寄存器。
操作系统为每个设备控制器提供一个设备驱动程序,负责设备控制器,为操作系统 的其他部分提供统一的设备访问接口。
三、计算机系统的体系结构
1、单处理器系统
单处理器系统只有一个主CPU,以便执行一个通用指令集,还有一些专用处理器, 执行有限指令集。
2、多处理器系统
多处理器系统,也称多核系统,有两个或多个紧密通信的CPU,他们共享总线,时 钟,内存,外设等。
2.1 主要的优点
增加吞吐量:
通过增加处理器的数量,可以在更短期的时间完成更多的工作,但是不能以N被速 度与单CPU,因为存在资源竞争。
规模经济:
多处理可以共享外设,大量存储空间和电源供给,因此比相同核数的单处理器系统 要便宜。
增加可靠性:
多处理器在某个处理器停止工作的时候,还是可以运行的,但是单处理器不行。
2.2 分类
非对称处理器:(主从关系)
一个主处理器控制系统,其他处理器向主处理器要任务或做预先规定的任务。
对称多处理器:(SMP)
每个处理器都参与完成操作系统的所有任务。注意每个处理器都有自己寄存器和高 速缓存,但是共享相同的物理内存。
2.3 多核设计
把多个计算核集成到一个芯片,它比多个单核更加高效,因为单片通信比多片通信 更加高效,而且消耗的电源更低。
3、集群系统
由两个或者多个独立的系统组成,共享存储,采用LAN连接或者更快的内部连接。
高可用性:这个意味着集群中的一个或多个系统出了故障,仍可以继续提供服务。
非对称集群:一台机器处于热备份,而另一台运行程序。热备份主机监视活动服务 器。
对称集群:两个或多个主机都运行应用程序,并互相监视。
提供高性能计算:集群所有计算机可以同时并发执行一个应用程序,但是程序需要 专门编写。这个又叫并行计算,将一个程序分成多个部分,每个部分可以并行地运 行在这些计算机或者他们的各个核上。
四、操作系统的结构
操作系统具有多道程序能力,多道程序设计,通过调度作业,是的CPU总有一个 执行作业,从而提高CPU的利用率。
操作系统在内存中同时保持多个任务,从里面可以选择一个执行。
磁盘有一个作业池,包括磁盘上的,或者等待被分配内存的所有进程。
分时系统:
分时系统要求计算机系统是可以交互的。
分时系统允许多个用户同时共享一台计算机。
分时系统采用CPU调度和多道程序设计,为每个用户体统一小部分的计算资源。
同时多个作业要加载到内存:作业调度
同时多个任务等待执行:CPU调度
分时系统保持合理的响应时间。
交换:把进程从磁盘调入或者从内存调出磁盘
虚拟内存:实现一个作业不必完全存储在内存中
五、操作系统的执行
操作系统是中断驱动的,如果没有进程需要执行,I/O设备需要服务,那么操作系 统在静静地等待事件的发生。
1、事件
事件总是由中断和陷阱引起的,对于每个中断,操作系统有不同的代码来处理,中 断程序用于处理中断。
2、双重模式个多重模式的执行
通过一个模式位来表示当前模式:内核模式(0) 和用户模式(1)
用户模式:
执行用户的应用时
内核模式:(内核模式,特权模式)
执行系统调用或系统服务的时候
一旦有陷阱,从用户模式切换到内核模式。
双模式的用处:
防止操作系统和用户程序受到 错误用户程序的影响。
原理:
将可能引起损害的机器指令作为特权指令,而特权指令只有在内模式才允许执行的 指令。如果用户模式想执行特权指令,则硬件不会执行,并以陷阱的形式通知操作 系统。
特权指令:切换到用户模式,I/O控制,定时器管理,中断管理等
支持虚拟化技术的CPU有一种单独模式,用于表示虚拟机管理器是否正在控制系 统。
3、定时器
操作系统应该维持控制CPU,防止用户进程陷入死循环,或者不调用系统服务时, 不把控制返回给操作系统。
在将控制交给用户前,操作系统可以设置好定时器,以便产生中断。定时器可以防 止用户程序运行时间过长。
六、进程管理
程序是一个被动实体,如同存储在磁盘上的文件内容
进程是一个主动实体。单线程进程有一个程序计数器,多线程有多个程序计数器。
进程是系统的工作单元
操作系统进程管理的主要工作:
- 在CPU上调度进程和线程
- 创建和删除用户进程和系统进程
- 挂起和重启进程
- 提供进程的同步机制
- 提供进程的通信机制
七、内存管理
操作系统内存管理主要工作:
- 记录内存那部分被使用和被谁使用
- 决定哪些进程会被调入或调出内存
- 根据需要分配或者释放内存空间
八、存储管理
操作系统对存储设备的物理属性进行了抽象,并定义了逻辑存储单元,即文件。操 作系统通过映射文件到物理媒介,并通过存储设备访问文件。
1、操作系统对文件的管理
- 创建和删除文件
- 创建和删除目录,以便组织文件
- 提供文件和目录的操作原语
- 映射文件到外存
- 备份文件到稳定存储介质
2、操作系统对存储器的管理
- 空闲存储空间的管理
- 存储空间的分配
- 硬盘的调度
3、高速缓存
高速缓存,信息通常存储在一个存储内存中,使用时,会临时复制到更快的存 储系统,即告诉缓存。当需要特定信息时,先检测高速缓存是否有,在去查询 内存。
高速缓存大小有限,高速缓存的管理设计比较重要,例如高速缓存的大小与置 换策略。
多处理器环境下,确保高速缓存的一致性
4、I/O 系统
I/O 子系统的几个组件:
- 缓冲、高速缓存等内存管理组件
- 设备驱动器的通用接口
- 特定设备的驱动程序
九、保护与安全
保护是一种机制,用于控制进程或用户访问计算机系统资源。
例如,内存寻址硬件确保一个进程只能访问自己的地址空间,定时器可以确保没有进程可以一直占用CPU不放。设备控制寄存器不能被用户访问,因此保护了各种外围设备的完整性。
保护和安全要求系统能够区分所有用户。
十、内核数据结构
主要用到了列表,堆栈,队列,树,哈希表,位图等等。
当需要表示大量资源的可用性时,通常采用位图,例如磁盘空间的管理。
十一、计算环境
移动计算:就是智能手机和平板电脑的计算
分布式计算:通过网络互联的一组计算机系统,可供用户访问系统维护的各个资源
C-S 计算:客户机-服务系统的模式
对等计算:P2P,每个结点都可以是服务器或客户机。
云计算:通过网络提供计算、存储甚至应用程序服务的功能。
第二章操作系统结构
一、操作系统的服务
操作系统提供环境以便执行程序。
用户界面:
有命令行界面,批处理界面,图形用户界面
程序执行:
系统能够加载程序到内存,并运行,程序能够结束执行,包括正常或者不正常。
I/O操作:
程序可能需要I/O,操作系统必须提供手段以便执行I/O。
文件系统操作:
程序需要读写文件和目录,创建或删除文件目录,等等,操作系统有权限管理,根 据文件所有者,允许或拒绝文件的访问。
通信:
一个进程需要和另一个进程交换信息。可以是同一个计算机,也可以是不同计算机。
常用的两种通信方式:
共享内存,消息交换
错误检测:
操作系统许哟啊不断的检测错误和更正错误。例如CPU或者内存硬件错误,I/O设 备出错,用户程序出错(算术溢出,非法地址访问,占用CPU时间太长),对于每 种错误,采取实当的错误处理。
资源分配:
当多个作业运行的时候,每个都应分配资源,操作系统管理许多不同类型的资源如 CPU,内存,文件存储等等。
记账:
操作系统需要记录用户使用资源的类型和数量。
保护与安全:
当多个独立进程并发执行时,一个进程不应干预其他进程或操作系统本身。
二、用户与操作系统的界面
1、命令行解析程序
解析程序又叫外壳(shell)
功能:获取并执行用户指定的下一条命令。
2、图形用户界面
采用基于鼠标的视窗和菜单系统,桌面图标代表一些程序,文件,或者系统功能。
3、界面选择
一般高级用户比较喜欢用命令行界面,事实上,有些不常用的功能只能通过命令行 来使用,对于重复性的任务,命令行界面更加容易,因为可以把命令写成外壳脚本 使用。
三、系统调用
系统调用提供操作系统服务接口。
其实,每个应用程序,都可能涉及很多的系统调用,例如复制一个文件到另一个到 文件。
系统调用顺序:
获取文件名称
写提示输出到屏幕
接受输入
获取输出文件名称
…
打开输入文件
如果文件不存在,停止
…
1、系统调用的形式
对于大多数程序语言,提供了很多系统调用的接口,以链接到操作系统的系统调用。
通常每个系统调用都有一个相关的数字。
2、系统调用参数传递
三种形式:
(1)通过寄存器传参,参数少时
(2)通过存在内存的块或表传递,而块的地址通过寄存器传递
(3)通过堆栈的形式传递参数,不限制参数的数量和长度。
3、系统调用的类型
六大类:
进程控制、文件管理、设备管理、信息维护、通信、保护
3.1 进程控制
执行程序能够正常或异常停止(end,abort)
可以加载执、行程序(load、execute)
创建进程、终止进程(fork,exit)
获取进程属性,改变进程属性
等待时间 (sleep)
等待事件、信号事件
3.2 文件管理
创建、删除、打开、读、写(create,delete,open,read,wirte)
能够获取文件的属性:例如,文件名,文件类型、记账信息
3.3 设备管理
进程执行,需要一些资源,如内存、磁盘驱动、文件等等
进程需要请求,如果有,操作系统就可以分配,否则程序应要等待。
设备资源:物理设备、虚拟设备,(请求和释放request,release)
3.4信息维护
例如获取当前时间(time),日期(date),还有的别操作系统版本,内存或磁盘可 用量等等。
3.5通信
消息传递模型,通信进程通过相互交换消息来传递信息。
每个进程都有一个标识,进程名,可以通过get_processid 得到。
通常通信源叫客户机,接受后台程序称为服务器。通过read_message 和
write_message 交换信息。
共享内存模型,通过读写共享区域的数据来交换信息。
共享内存要求两个或多个(参与的进程)都同意共享才能成立的,同时要保证不会 同时对同一个地方进行写,这个后面的互斥机制可以实现。
线程默认共享内存。
3.6 保护
通过系统调用set_permission、get_permission 来设置资源(文件和磁盘)的权 限。系统可以运行或拒绝特定用户访问某些资源。
4、常见系统程序
文件管理、状态信息、文件修改、程序语言支持、程序加载和执行、通信、后台服务等等。
四、操作系统的设计与实现
高层目标:批处理、分时、单用户、多用户、分布式、实时
1、需求分析
用户目标:
便于使用、易于学习、使用、可靠、安全、快速
系统目标:
易于设计、实现和维护、灵活、可靠、高效
2、机制和策略
机制和策略分离,机制决定如何做,策略决定做什么。
举个例子:实现一个机制,能够给应用程序赋予一个优先权的机制。
采用机制和策略分离,可以支持I/O密集型,或者CPU密集型
他们的区别,就是实现的形式不一样,例如CPU调度是一个机制,但可以采用各 种算法调度。
3、实现
可以采用各种语言写,汇编、c语言、python
采用高级语言的好处是:代码写得快,更为紧凑,易于理解和调试
代码容易移值到其他硬件
缺点:会降低响应速度和增加存储
关键部分:中断处理器、I/O管理器、内存管理器、CPU调度器,可以找出瓶颈用汇 编语言程序来代替。
五、操作系统结构
1、分层法
把操作系统分成若干层,对于每一层,包含数据结构和一组可为高层调用的程序集,而且可以调用低层的操作。
主要优点:
简化了结构和调试
因为每层只能调用低层的功能和服务。调试可以自底往上,一层一层确认无误。
每层为高层隐藏了一定的数据结构和操作。
难点:
难以合理定义每一层,如存储驱动程序和CPU调度器的关系,互为上下
效率可能会稍微差,因为系统调用需要执行更长的时间,从上层调用,可能需要一 层接一层调用,传递参数,参数有时可能存在被改的风险。
2、微内核
通过把不必要的的部件从内核删除,当作系统级或用户级程序来实现。
内核服务:提供进程与内存管理、通信的功能。
优点:易于扩展操作系统,所有新的服务,都可以通过在用户空间内增加。
容易从一个硬件平台移值到另一个硬件平台,有很好的安全性和可靠性。
3、模块化
目前操作系统最佳设计方法:采用可加载的内核模块
设计思想:内核提供核心服务,而其他服务可以在内核运行时动态实现。
动态链入服务比直接添加到内核好,因为后者需要重新编译内核,例如可将CPU调度和内存管理算法建立在内核,但通过加载模块,支持不同文件系统。
如solaris系统
有7种类型可加载模块
调度类,文件系统、可加载系统调用,可执行格式,streams模块,其他模块,设备和总线驱动程序。
4、混合系统
Mac OSx
采用分层模式,图形用户界面,应用层序服务环境,内核环境(Mach,BSD),内核扩展
Mach 有内存管理,远程过程调用,进程间通信,线程调度等服务。
BSD 支持命令行窗口,网络,文件系统等
IOS,Android
六、操作系统的调试、生成(略)
七、系统的引导
引导:加载内核以启动计算机的过程
引导程序:可以定位内核,并加载到内存执行。
有的计算机系统复杂点:
两步:简单引导程序,引入一个复杂的引导程序,后者再加载内核。
开机启动时:指令寄存器会加载某个预先定义的内存位置,从该位置开始执行,那 个位置就是引导程序的位置所在,一般位于ROM。
具有引导分区的磁盘叫引导盘,也叫系统盘。
第三章 进程
一、进程的概念
个人总结:进程就是正在运行的程序和相关内容的一个统称。
1、进程
进程包括 程序代码(文本段或代码段)、当前活动(如程序计数器的值,处理器寄 存器的内容等)、栈数据(临时数据,如函数参数,返回地址等)、数据段、堆空间。
2、程序和进程的联系
程序本身不是进程,程序只是被动的实体,如存储在磁盘上的可执行文件。
进程是活动实体,当一个可执行文件被加载到内存时,这个程序就成为进程。
两个进程可以与同一程序相关联,但是他们被当作两个独立的执行序列,例如可以运行多个电子邮件副本,同一个用户打开多个web主页。
但是,他们虽然文本段相同,数据、堆和栈都不同。
3、进程的状态
5个状态:
- 新的:进程正在创建
- 就绪:进程等待分配处理器
- 运行:指令正在执行 -> 中断-> (就绪)
- 等待:进程等待某个事件的发生(I/O完成或收到信号)->就绪
- 终止:进程已经完成了执行。
4、进程控制块
操作系统采用 进程控制块(PCB)表示每个进程。
主要内容:
进程状态:新的、就绪、运行、等待、停止
程序计数器:表示进程将要执行的下一条指令地址
CPU寄存器:通常有累加器、索引寄存器、栈寄存器、通用寄存器等,发生中 断时,这些状态信息和程序计数器一起保存。
CPU调度信息:包括进程的优先级调度队列的指针等
内存管理信息:一般包括基地址、界限寄存器的值、页表、段表等信息
记账信息:CPU时间、实际使用时间、时间期限、记账数据等。
I/O状态信息:分配给进程的I/O 设备列表、打开文件的列表
支持线程的系统,PCB还包括:每个线程的信息
二、进程调度
分时系统的目的就是在进程之间快速切换CPU,以便用户可以在程序运行之间能与其交互。
进程调度器在不断地选择一个可用的进程到CPU执行。
1、调度队列
- 作业队列:当进程进入系统时,这个队列包含系统内所有进程。
- 就绪队列:驻留在内存、就绪的、等待运行的进程
- 设备队列: 等待特定I/O 设备的的进程,每个设备都有自己的设备列表。
一个运行的进程:
可能发出I/O 请求,被放到I/O 队列
创建子进程,并等待其停止
被中断强制释放CPU,并放回到就绪队列。
当进程终止时、PCB和相关资源都会被释放。
2、调度程序
3种类型:
长期调度程序:(作业调度程序)
从磁盘中选择进程加载到内存,以便执行
短期调度程序(CPU调度程序)
从准备执行的进程中选择,分配给CPU
中期调度程序:
将进程从内存(或CPU竞争)中移出,从而降低多道程序的程度。
此后进程可以被再次调入,从中断处继续执行,这种方案叫交换。
区别和联系:
前两种主要是执行的频率
短期调度要经常为CPU选择新的进程,可能每100ms执行一次,因此短期调度程序必须足够快。
长期调度执行不那么频繁,主要是控制多道程序程度(内存中进程的数量)。如果多道程序稳定,就入进程等于离开进程速率,因此只有进程离开时才会调度。
两类进程:
I/O密集型;
执行I/O 比执行计算的时间花费更多时间
CPU密集型:
很少I/O请求,大部分时间用于执行计算
长期调度要选择I/O密集型和CPU密集型的合理进程组合,避免两极化。
关于交换:为了改善进程的合理组合,或者由于内存的需求改变导致过度使用内存,需要释放内存,就有必要进行交换。
三、进程运行
1、进程创建
进程创建的时候,肯恶搞创建多个新的进程,创建进程称为父进程,新进程称为子 进程。对进程的识别采用唯一的进程标识符pid。
对与linux系统,进程init,pid永远为1,是所有用户进程的父进程。
2、资源共享
父进程在子进程直接分配资源或者共享资源(如内存或文件),限制了子进程只能 使用父进程的资源,可以防止创建进程过多导致系统超载。
父进程可能向子进程传递初始化数据,有的还会传递资源。
3、等待or 并发
父进程创建子进程之后:
父进程与子进程并发进行
父进程等待,直到某个或者全部子进程执行完。
新进程的地址空间
子进程是父进程的复制品,(与父进程拥有相同的程序和数据)
子进程加载另一个新的程序。
4、创建函数 fork
通过调用fork可以创建新的进程。地址空间与父进程相同,父子进程都继续执行 fork 后面的指令。
子进程继承了父进程的权限、调度属性、以及某些资源。
返回值:对于父进程,返回的是子进程的pid,而子进程返回的是0.
进程可以调用exec()来加载新进程,替代进程的空间。
父进程如果没事,可以通过调用wait()把自己移出就绪队列,直到子进程停止
5、进程终止
进程可以通过调用exit()显示请求操作系删除自身,进程终止。所有进程资源, 如物理和虚拟内存,文件,等都会被释放。
当然父进程可以通过子进程的pid显式终止子进程。
原因一般很多:
- 子进程使用了超过分配的资源
- 分配给子进程的任务,不需要执行了
- 父进程正在退出,操作系统不允许无父进程的子进程运行。(级联终止)
进程终止时,操作系统会释放其资源,但是进程表条目还在的,直到父进程调用 wait(),因为进程表包含了进程的退出状态。
僵尸进程:进程已经终止,单父进程没有调用wait()。所有的进程都会过渡到这种 状态,不过一般都是短暂的而已。
孤儿进程:如果其父进程没有调用wait()就退出了的子进程。
init进程当作所有孤儿进程的父进程,会定期调用wait(),收集孤儿进程的退出状 态,以便释放孤儿进程的标识和进程表条目。
四、进程间通信
操作系统内的进程并发执行可以是独立的,也可以是协作的。
独立进程:一个不能影响其他进程或不能受其他进程影响的进程。
协作进程: 一个能影响其他进程或能受其他进程影响的进程。
1、进程协作的好处
信息共享:多个用户可能对同一样的信息感兴趣,应能并发访问这些信息。
计算加速:快速完成任务,可以吧任务分成几个子任务并行执行。
模块化:把系统功能分块,某些功能独立分成一个进程或者线程。
方便:即使单个用户可以同时执行多个任务。
2、两种通信方式
共享内存和消息传递。
共享内存:建立起一块供协作进程共享的内存区域,进程通过向此区域读出或写入 数据来交换信息。
消息传递:通过在协作进程间交换消息来实现通信。
比较:
对于分布式系统,消息传递比共享内存更容易实现
共享内存要快于消息传递,因为消息传递经常要调用系统调用,需要消耗更多的消 息以便内核介入。
3、共享内存系统
采用共享内存的进程间通信,需要通信进程建立共享内存区域。
通常共享区域驻留在创建共享内存段的进程地址空间,需要利用它进行通信的进程需要将其附属到自己的地址空间。
他们确保不会向同一个位置同时写入数据。
示例:生产者与消费者
生产者和消费者之间需要一个缓冲区,以便被生产者填充和消费者清空。
缓冲区的类型:
无界缓冲区:没有限制缓冲区的大小,消费者必须等待新的项,生产者总可以产生 新项。
有界缓冲区:有固定大小的缓冲区,如果空,消费者必须等待,如果满,则生产者 必须等待。
当in +1 == out 就是满了
4、消息传递系统
消息传递,可以通过互相交换信息来实现相互通信。
依赖操作:
send(msg); recieved(message)
4.1 通信的方式类别
直接通信/间接通信
同步/异步通信
自动/显示缓冲
4.2 直接通信
直接通信,需要每个进程必须明确指定通信的接收方或者发送方。
如send(Q , msg)
Receive(Q , msg)
通信链路属性:
链路自动建立
链路只与两个进程相关
每对进程之间只有一个链路
上面的寻址方式是对称的,发送方和接收方必须指定
非对的变形也有,只需要指定接收方,接受进程通过id变量获取发送方进程名 称。
4.3 间接通信
**通过邮箱或端口来发送和接收消息。**邮箱可以抽象为一个对象,进程可以向其 中存放消息,也可以删除消息,每个邮箱有唯一的标识。
两个进程共享一个邮箱才能通信。
Send(A ,msg), receive(A,msg)//发送到A或从A接收消息
链路特点:
两个进程共享同一个邮箱才能建立链路
一个链路可以与两个或多个进程关联
两个通信进程之间可有多个不同链路,一个链路对应一个邮箱
邮箱的所有者:进程或者系统。
如果邮箱为进程拥有(邮箱是进程地址空间的而一部分),那么需要区分进程 的所有者(从邮箱接受消息)和使用者(从邮箱发送消息)。
5、同步
消息传递可以试阻塞的(同步的),非阻塞的(异步的)
因此send和receive 有记中模式
阻塞发送:发送进程阻塞,直到消息被接收进程或邮箱接收
非阻塞发送: 发送消息,并恢复操作。
阻塞接收:接收进程阻塞,直到有消息可用
非阻塞接收:接收进程可以接收到一个有效消息或空消息。
6、缓存
不管通信是直接的还是间接的,交换的信息总是驻留在临时队列中。
三种队列:
零容量:队列最大长度为0,也就是发送者应阻塞
有限容量:队列长度为n,队列未满时,可以继续存放消息,否则发送者应当阻塞
无限容量:不管多少消息都可以存放,发送者不用阻塞
五、C/S 通信
1、套接字
一个套接字由一个IP地址和一个端口号组成。(应用层与传输层的一个接口)
有面向连接的服务(TCP),面向无连接的服务(UDP)
Server:选定一个服务端口,监听是否有连接
Client:根据服务器的ip地址和端口好,建立套接字
请求连接
连接成功即可发送数据了。
2、远程过程调用(RPC)
RPC允许客户调用远程主机的过程,RPC系统调用通过调用适当的存根,并且传 递远程过程参数。
问题:
不同的数据表示(大端小端),用统一的外部数据表示。
避免重复执行:采用正好一次的策略,可以为每个消息附加一个时间戳,服务器对 所处理消息保留一个足够长的时间戳历史,就可以查询消息是否已经出现过。
如何知道服务器端口:
一、可以预先固定
二、通过先于月老(matchmaker)打交道,确认通信端口
3、管道
3.1 普通管道
创建:pipe(int fd[] )
如果生产者和消费者的模式,生产者向管道的写入端写,消费者从管道的读出端读。
只允许单向通信
普通管道只能由创建进程所访问,父进程创建管道来和子进程通信。
因为管道是一个特殊类型的文件,因此子进程也继承了父进程的管道。
只有进程互相通信管道才存在,一旦完成通信,就不存在了。
3.2 命名管道
双向通信,不需要父子关系,当建立一个管道后,多个进程可以通过它通信。
通信完成还是继续存在,除非显式删除。
它只允许半双工传输,也就是说,如果想数据在两个方向传输,就用两个管道。
对于unix,命名管道为FIFO,通过makfifo()创建。
此外通信进程应该位于同一个机器,否则用上面的套接字。
第四章多线程编程
一、概念
1、线程
线程:线程是进程中的一个实体,是CPU调度和分派的基本单位。
内容:
线程ID,程序计数器、寄存器组、栈空间
它与同一进程的其他线程共享代码段、数据段和其他操作资源。
2、多线程的优点
响应性:如果程序部分阻塞或执行冗长操作,它还是可以继续执行,增加对用户的响应度。
资源共享:线程默认共享他们所属进程的内存和资源。代码和数据共享的有点是,允许一个应用程序在同一地址空间有多个不同线程的活动。
经济:进程创建所需的内存和资源分配非常昂贵,线程能共享进程的资源,因此创建和切换线程更加经济。
可伸缩性:对于多处理器,多线程可以并行执行,但是如何单线程,就算再多CPU也只能在其中一个上运行。
3、并发与并行
并行系统是指可以同时执行多个任务。
并发系统是指支持多个任务,允许所有任务都能取得进展。
Amdahl 定律:
S 为应用程序串行的一部分
加速比 <= 1/( S + (1-S)/N ), N 是多核处理器的核数。 当N无穷的时收敛于1/S
4、多线程编程的挑战
识别任务,能够分清哪些任务是可以独立的,以便在多核并行执行
平衡:根据任务的价值不同,分配不同的核数
数据分割:把任务数据合理划分
数据依赖:如果任务存在依赖关系,必须确保任务执行是同步的
测试和调试
5、并行类型
数据并行:把数据分配到不同的计算核,执行相同的操作。
任务并行:把任务分配到多个计算核,数据和操作可能都不同。
一般都混合使用
二、多线程模型
1、用户线程和内核线程
用户层的线程叫用户线程,位于内核之上,无需内核支持
内核层的线程叫内核线程,由操作系统来支持和管理
2、线程模型
2.1多对一模型:
多个用户级线程到一个内核线程。
特点:
如果一个线程执行阻塞系统调用,那么整个进程都会阻塞。
任一时间只有一个线程可以访问内核,那么多个线程也不能并行运行在多核系统。
2.2 一对一模型
一个用户线程映射到一个内核线程。
特点:
运行一个线程执行阻塞系统调用时,另一个线程能够继续执行。(更好的并发性)
多个线程可以并行地运行在多核处理器上。
由于创建内核线程的开销会影响应用程序的性能,所以限制了支持线程的数量
3.3 多对多模型
多路复用多个用户线程映射到多个或相对少的数量的内核线程数。
前者:
多对一,内核一次只能调度一个线程,所以并未增加并发行。
一对一,开发人员要小心不能在应用程序开太多线程。
多个内核线程可以在多处理器并发执行,当一个线程阻塞系统调用时,内核可以调 度另一个线程执行。(用户线程可以换)
改进:允许多对多的同时,也允许特定用户线程绑定一个内核线程,这个叫双层模 型
4、线程库
线程库为程序员提供创建和管理线程的API,可以分为两种,用户空间没有内核支持的库,调用只是一种本地调用。由操作系统支持的内核级库,调用是系统调用。
创建策略:
异步线程,就是父线程创建了子线程之后,马上恢复执行自己的任务,与子线程并发执行。
同步线程,就是父进程创建子线程后,等待子线程完成了,才继续执行任务,又叫分叉-连接策略。
5、多线程编程
(1)初始话线程属性
(2)定义线程要运行的功能(函数)注意参数传递是指针类型
(3)创建线程,获取tid
(4)join 等等线程结束
6、线程池
存在问题:
(1)频繁创建线程还是需要不少时间,任务完成,资源需要被释放
(2)如果无限制线程的数量,大量并发时,那么将可能耗尽系统资源
创建线程池:
在进程开始时创建一定数量的线程,加到池中等待工作,如果接收到请求,就从唤醒池中一个线程去服务。一旦完成,返回线程池中等待工作。
优点:
- 用现有的线程完成服务请求比创建一个线程快
- 线程池可以限制可用线程总的数量,保护了不能支持大并发量的线程服务。
- 将执行任务从创建任务中分离,可以采用不同策略去运行任务,例如定期执行。
三、多线程问题
1、系统调用的fork() 与exec()
对于一个线程,调用了fork之后,马上调用exec(),那么就仅仅复制调用线程。
否则复制所有线程。
因为,如果调用exec的话,将会有一个新的程序代替整个进程,包括所有线程。
2、信号处理
信号机制:
信号由特定事件发生而产生的
信号被传递到某个进程
信号一旦收到就应该处理
同步信号:同一个进程产生的信号,发送给同一个进程
异步信号:某进程接收到其他进程产生的信号
信号处理程序:
缺省的信号处理程序,处理时,由内核来运行。
用户定义的信号处理程序
单线程好处理,但多线程有点复杂
多线程信号传递:
传递信号到信号所适用的线程
传递信号到进程内的每个线程
传递信号到进程内的某些线程
规定一个特定线程以接收所有信号
当然,传递方法,主要取决于信号类型,如同步信号,就要传到产生这个信号的线程,终止进程,就是传递给所有线程。
3、线程撤销
线程撤销:在线程完成任务前终止线程
目标线程的撤销形式:
异步撤销:一个线程立刻终止目标线程
延迟撤销:目标线程不断检查它是否应终止,这允许它检查是否处于安全的撤销点。
因为有些情况,如资源已经分配给已撤销的线程,或它正在更新与其他线程正在共享的数据,是很困难的。
4、线程本地存储
有时线程需要一些本线程独立的数据,称为线程本地存储(TLS)
区别于局部变量,局部变量只有单个函数可见,但是,TLS可以在多个调用函数可见。类似于静态变量,但是对于每个线程都是独特的。
5、调度程序激活
在多对多模型设计中,在用户和内核线程之间增加一个中间数据结构,称为轻量级进程(LWP),表现为虚拟处理器,以便调度运行用户线程。
一个LWP和一个内核线程相连,也只有内核线程才能通过操作系统调度以便运行于物理处理器。如果一个内核线程阻塞,LWP也会阻塞。
调度器激活:用户线程库和内核之间的一种通信方案。
原理:内核提供一组虚拟处理器给应用程序,应用程序可以调度用用户线程到任何一个可用虚拟处理器。
内核将特定事件通知给应用程序,这个叫回调。
就是当一个线程要阻塞时,内核向应用程序发出一个回调,并分配一个新的虚拟处理器,让应用程序运行一个回调处理程序,保存要阻塞线程的状态,并释放占有的处理器,调用一个适合的线程在新的虚拟调度处理器运行。
当阻塞线程等待的事件完成后,内核触发另一个回调,通知先前阻塞的线程可以运行了,内核可能分配一个新的虚拟处理器或抢占一个用户线程的虚拟处理器运行对应的回调处理程序,运行阻塞线程。
一句话,就是当线程阻塞后,可以调度新的线程,避免浪费LWP资源。
第五章 进程调度
一、基本概念
1、调度
当多个进程同时存在于内存时,如果一个进程正在等待I/O,操作系统就把CPU交 给另一个进程。
调度程序:
当CPU空闲时,操作系统就从就绪队列中选择一个进程来执行,这个采用短期调 度程序。
2、抢占调度
CPU调度的情况:
抢占的调度
- 从运行状态切换到等待状态(I/O请求,wait())
- 当一个进程终止
非抢占的调度
- 从运行状态到就绪状态(中断)
- 从等待状态到就绪状态(I/O完成)
非抢占调度:一但某个进程分配到CPU,该进程会一直使用CPU,直到它终止或 切换到等待状态。
抢占调度:一个进程正在使用CPU的时候,另一个进程随时可以抢占CPU.
抢占有风险:抢占调度可能会导致竞争,例如两个进程共享数据,一个进程正在修 改时,它抢占运行,那么肯能会读到不一致的数据。如果这些是重要的内核数据入 (I/O)队列,而内核需要读取相同的数据和结构,那就混乱了。
解决:在进行上下文切换的时候,等待系统调用的完成,或者I/O阻塞的发送。
3、调度程序
功能:
- 切换上下文
- 切换到用户模式
- 跳转到用户程序的合适位置,以便重新启动程序
调度程序应当尽可能快,把调度程序停止一个进程到启动另一个进程所需要的时间 称为调度延迟。
二、调度准则
衡量调度算法的基本特征
CPU使用率:应使CPU尽可能忙碌,轻负荷(40%),重负荷(90%)
吞吐量:单元时间内进程完成的数量,对于长进程可能1个/时,短进程可能10/秒
周转时间:进程从提交到完成的时间段,周转时间为所有时间段之和,包括等待进入内存、就绪队列中等待、CPU上执行、I/O 执行。
等待时间:就是进程在就绪队列等待所花时间的总和。
响应时间:从进程提交请求到产生第一响应的时间。
我们的目标就是最大化CPU利用率和吞吐量,最小化周转时间、等待时间和响应时间。
三、调度算法
1、先到先服务(FCFS)
非抢占的
思想:按照到达顺序来服务
优点:简单容易实现
缺点:平均等待时间往往很长
2、最短作业优先调度(SJF)
抢占或非抢占
思想:作业长度最短的优先,相同按照FCFS
更为恰当的表示:最短下次CPU执行,其实我觉得主要是因为,一个进程可能要执行的时间很长,可能需要多次分配个CPU,所以这里就是表示,最短的下次CPU执行。
优点:SJF是最优的,原理:因为把短的调度程序往前调,短进程的等待时间减少大于 大进程等待时间的增加。
缺点:SJF真正的困难是如何知道下次CPU执行的长度。有时可以把用户提交时,进 程时限作为长度,SJF经常用于长期调度。
CPU执行的测量长度常常用指数平均值来预测。
SJF的抢占算法:
如果新进程需要的时间比当前未完成进程所剩余的时间小,允许抢占先执行。
抢占SJF又叫最短剩余时间优先。
3、优先级调度
可以是抢占和非抢占的
思想:优先级高的先运行
当然一般用数字表示,至于用小数还是大数表示优先级高,没有规定。
优先级的定义可以分为内部和外部,内部定义可以采用测量数据来定义,如时限,内存要求,打开文件数量,IO时间与CPU时间比等等。外部一般是赞助部门,其他因素。
问题: 无穷阻塞和饥饿,因为优先级较低的进程可能一直都无法执行。
解决:老化,就每隔一定时间,循环提高所有进程的优先级,最高的循环回到最低。
4、轮转调度
抢占的
轮转(RR),专门为分时系统设计的。
思想:将一个小时间单元定义为时间量或时间片,通常10-100ms,CPU循环调度每个进程,且每个进程执行的时间不能超过一个时间片。
缺点:平均等待时间都比较长,上下文切换消耗比较多时间。
5、多级队列调度
多级队列算法将就绪队列分成多个单独队列,根据进程的属性如内存大小、进程优先级、进程类型 ,永久分配到某个队列。每个队列可以有自己的调度算法如RR,FCFS.
队列之间也有调度,通常采用固定的优先级抢占调度,只有高级队列为空,才能调度低级调度队列的进程。
另一种形式也可以为:对队列之间采用时间片比例。
6、多级反馈队列调度
多级反馈队列算法运行进程在队列之间进行迁移,例如过多使用CPU时间的进程会迁移到低级队列,因此,IO密集型和交互进程放在更高优先级队列。低级优先队列中等待时间过长的进程会被移到更高优先级队列。这样形式的老化,可以防止饥饿问题的产生。
多级队列调度程序的调度参数:
- 队列的数量
- 每个队列的调度算法
- 何时升级到高级队列
- 何时降级到低级队列
- 进程该进入哪个队列
四、线程调度
支持线程的操作系统,内核级线程才是操作系统所调度的,用户级线程由线程库来管理。
1、竞争范围
进程竞争范围(PCS),系统线程库调度用户级线程,以便在LWP上运行。
系统竞争范围(SCS),操作系统调度内核线程到物理CPU。
PSC一般采用优先级调度,调度程序选择运行具有最高优先级的、可运行程序。
用户级线程的优先级由程序员设置的。
五、多处理器调度
1、多处理器调度方法
非对称多处理,让一个处理器处理所有调度决定、iO处理及其他活动,其他处理器 只执行用户代码。
对称多处理(SMP),每个处理器自我调度,所有进程可以在相同的就绪对咧,也 可以有自己私有的就绪队列。
2、处理器的亲和性
一个进程从一个处理器调到另一个处理器,需要设置第一个的缓存内容无效,然后 对第二个处理器内容重新填充。这个缓存的无效和填充代价比较大。
处理器的亲和性:大多SMP视图让一个进程运行在同一个处理器上。
形式:软亲和性,操作系统视试图保持进程运行在同一个处理器(但不保证),也 可以迁移到其他处理器。
硬亲和性,允许进程运行在某个处理器子集上。
3、负载平衡
负载平衡设法将负载平均分配到SMP系统的所有处理器。对于每个处理器一个私 有的可执行进程的队列,负载平衡是必须的,对于公共队列系统,就不是很有必要。
负载平衡的方法:
推迁移:一个定期任务检查每个处理器的负荷,如果如果发现不平衡,就从一个超 载处理器推到空闲或不太忙的处理器。
拉迁移:空闲处理器从一个忙的处理器拉一个等待任务时,发生拉迁移。
六、实时CPU调度
实时操作系统的调度
软实时操作系统:不保证会调度关键实时进程,只保证这类型进程会优先于非关键 进程。
硬实时系统:一个任务必须在它的截至期限前完成。(主要是后面的两种调度)
1、最小延迟
事件延迟:当事件发生到事件得到服务的这段时间称为事件延迟。
两种延迟影响实时系统的性能
中断延迟:从CPU收到中断到中断处理程序开始的时间。
中断发生-操作系统完成当前指令-确定中断类型-保存进程状态-执行中断服务程序(ISR) 执行这些任务的时间之和就是中断的延迟。
调度延迟:调度程序从停止一个进程到启动一个进程所需要的时间。
调度延迟包括冲突阶段和调度阶段(冲突是指抢占内核中运行的任何进程,释放高优先级进程需要的,低优先级进程占有的资源)
2、调度算法
2.1优先权调度
当一个实时进程需要CPU时,立即相应。支持抢占的基于优先级的算法。
下面两种是属于硬实时系统调度算法
有几个前提:
这些进程都是周期性的,也就是定期需要CPU
- 有固定的处理时间t
- CPU处理的截至期限d
- 处理的周期p
- 周期任务的速率是1/p
2.2单调速率调度
这个是抢占式的,静态优先级的策略。优先级为周期的倒数,也就周期越短的,优先级越高。
这个算法是最优的,如果一组进程不能由此算法调度,也不能由任何其他静态优先级算法来调度。
示例:
哈哈哈
2.3 最早截至期限优先调度
最早截至期限优先(EDF)调度根据截至期限动态分配优先级。
要求:一个进程可运行时,它应向系统公布截止期限要求。
EDF调度不要求进程是周期的,也不要去执行的长度是固定的,唯一就是进程变得可运行时,要宣布它的截至日期。
理论上这种调度是可以满足CPU利用率100%,但是上下文切换和中断处理需要消耗点时间代价的。
实例:
哈哈哈
2.4 比例分享调度
就是在不同的应用程序,按照一定的比例分配时间。如果总的股数是T,剩余股数是R,如果一个进程需要N股,小于R就可以分配时间,否则拒绝调度。
第六章 同步
协作进程能与系统内的其他进程相互影响,可以通过共享逻辑地址空间(代码和数据),也可以通过文件或消息来共享数据。
一、基本概念
1、竞争条件
多个进程并发访问和操作同一个数据并且执行结果与特定访问顺序有关,叫竞争条件。
2、临界区问题
2.1 临界区
对于n个进程,每个进程都有一个代码段,执行该区代码时可能修改公共变量、更 新一个表、写一个文件等,这个区域就叫临界区。
没有两个进程可以在他们的临界区内同时执行。
2.2 临界区问题
设计一个协议以便协作进程。划分四个区:
进入区:每个进程要进入临界区前,应请求许可。
临界区:见上面
退出区:恢复一些跟进入临界区相关的变量
剩余区:进程其他无关代码区
2.3 临界区问题解决方案三大要求:
- 互斥:进程Pi在临界区执行时,其他进程都不能在其临界区执行。
- 进步:如果没有进程在临界区内执行,如有进程要进入临界区,那么选择那些进程进入临界区。
- 有限等待:从一个进程请求进入临界区到允许为止,其他进程进程允许的次数 具有上限。
3、操作系统的临界区问题
两种常用方法
抢占式内核:允许处于内核模式的进程被抢占
非抢占式内核:处于内核模式的进程不允许被抢占,一直运行,直到退出内核模式、 阻塞、或自愿放弃CPU.
二、Peterson 解决方案
这是一个基于软件的临界区问题解决方案
针对两个进程来解决的,Pi,Pj i = 1-j
1.变量
两个关键变量:
turn:表示轮到哪个进程
flag[2]: 当flag[i] 为true, 且turn == i 时,i进程就可以进入临界区。
2、过程
对于进程i,当它想进入临界区时,先设置falg[i] = 1,表示请求进入临界区。
设置turn=j,通过设置为对方,试探如果对方flag[i]=1,就让对方先进入,否则,自己进入。
执行完,设置flag[i] = 0 ,表示自己退出临界区。
3、满足三个条件
互斥成立:因为就是两个flag都为true,但是turn只能为i或j,所以只有一个能 进入临界区。
进步:如果没有在临界区,就是flag为0,如果其中一个设置为1,就可以立刻进 入。
有限等待:如果两个同时请求,一个执行,而另一个只需等待1次,下一次就可以 轮到。
三、硬件同步
单处理器器,可以简单解决:在修改共享变量的时候,只要禁止中断出现就好。这种一般往往是非抢占式内核采用。
多处理器:多处理器如果采用禁止中断会很耗时的,因为消息要传递到所有处理器。消息传递会延迟进入临界区,降低系统效率,而且如果时钟通过系统时钟中断来更新,那么也会受到影响。
1、特殊硬件指令,用于检测和修改某个字的内容,或用于原子地交换两个字(作为不可中断的指令),用一个指令test_and_set()来抽象它。
有一个全局变量lock,初始化为false。
进入临界区前,先获取lock,如果为false,就可以进入,并且把lock设置为true。
如果为真,一直等等。
退出临界区,把lock设置为false,以便其他进程可以进去。
所以,加锁和释放锁都是同一个进程操作的。
不足:这个算法满足互斥,并没有满足有限等待。
改进:把进程组织成一个队列,释放锁的时候,循环寻找下一个在等待的进程。
四、互斥锁
前面的硬件解决方案比较复杂,不能为程序员直接使用,所以操作系统设计员设计了已经软件工具。最简单的就是互斥锁:
1、原理:一个进程想进入临界区,就先通过acquire()获得锁,然后通过release()释放锁。
每个互斥锁有一个布尔变量available,表示锁是否可用。
实现如下:
可以看到,如果锁是可用,调用acquire会成功,否则它就一直阻塞,直到锁被释放。
2、缺点
它需要忙等待,因此这种类型的锁,又叫做自旋锁,进程在不断地旋转,等待锁变得可用。前面的test_and_set()也有这个问题。对于单CPU,这样等待比较浪费CPU的周期。
3、优点
自旋锁,进程在等待锁时,没有山下文切换(可能需要相当长的时间),如果锁用 的时间较短的话,自旋锁还是比较有用的。
自旋锁一般用于多处理系统,因为一个线程可以在一个处理器旋转,其他线程在其 他处理器运行。
五、信号量
信号量比互斥锁的功能更加强大,提供更高级的方法。
信号量S是一个含有一个整型变量和PCB队列的一个结构体类型变量,只能通过连个标准原子操作wait(), signal() 来访问。
wait 最初是P(荷兰语proberen 测试),signal 最初是V (荷兰语 verhogen增加)
同样,wait() 和 signal() 的操作需要不能被中断的支持。
1、信号量的类型
二进制信号量:值只能为0或1,因为跟互斥锁类是,对于不支持互斥锁的系统, 可以用二进制信号量来代替。
计数信号量:值不受限制,可以用于控制访问具有多个实例的某种资源,需要资源 是调用wait,释放资源时调用signal,当信号量计数为0时,将阻塞该等待资源 的进程,放到等待队列里面。
信号量可以用与同步两个并发的进程(之前的处理,感觉好像没有这个功能)。
要求:当进程1执行完之后才执行进程2.可以如下操作
P1:
S1;
Signal(synch);
P2:
Wait(synch);
S2;
2、信号量的实现
2.1原理
为了克服忙等待,每个信号量附属一个列表。如果一个进程调用wait,发现信号量 value<=0时,不是忙等待,而是阻塞自己,把自己加入到信号量的等待队列,并 切换成等待状态,以便把CPU调度给别的进程。
当有进程执行signal时,如果value<=0,就wakeup 队列的一个进程。从等待 状态,变到就行状态,等待CPU调度。
2.2 信号量值
信号量的值可以为负数,但是具有忙等定义下的信号量不能为负值。
如果为负值,它的绝对值就表示等待它的进程的数目。
2.3 信号量操作必须是原子执行的
对于同一信号量,没有两个进程可以同时执行操作wait()和signal()。这是一 个临界区问题。
解决方案:
单处理器:可以简单地通过禁止中断来实现原子操作。
多处理器器:如果采用中断,则每个处理器都必须被禁止,这样比较困难而且影响 性能。所以一般SMP都提供其他加锁技术,例如test_and_set,自旋锁等。
由此可见,我们并没有完全取消忙等待,而是把忙等待从进入去移到了临界区(因 为signal和wait操作属于临界区问题),但是这个指令一般比较短,所以忙等待 发生的比较少,需要时间也短。
3、死锁和饥饿
当两个或多个进程无限等待一个事件的发生,而这个事件只能由这些等待进程的其 中之一来产生,那么这些进程的状态叫死锁。
例子如下:
无限阻塞或饥饿:如果进程无限等待信号量,如果对信号量的列表采用LIFO,来 删除进程,那么就可能产生无限阻塞。
4、优先级反转
情景:如果有三个进程等级为L M H ,且L进程占有R资源,H也想要R资源,
且R可能是一些重要的内核数据,那么H不会抢占,但是M如果可以运行了,就 可以抢占L,导致H要等待更长时间。
解决:为了防止这种抢占,采取优先级反转策略,对于任何等级的的进程,正在访 问资源的进程可以暂时获得最高级等级,这样就不会被抢占,当用完资源后,还原 等级。
5、对比总结互斥和信号量
互斥锁变量值只能为0或1,不能很好的解决多资源问题,但能够实现互斥。
互斥锁是基于软件实现的。
互斥锁存在忙等待问题。
互斥锁的两个函数,也需要原子地执行,所以通常采用前面的硬件机制来实现。
信号量的值,可以是多个值,可以处理多个实例的资源问题。
信号量可以实现,让进程同步。
信号量有一个等待队列,可以一定程度避免忙等待。
信号量的操作也需要原子地执行,因为需要依赖前面的硬件机制或者自旋锁来辅助。
因此信号量其实也一定的忙等待,但是时间一般很短。
六、经典同步问题
1、生产者-消费者
一个整型变量n,表示最多能够放n条消息
Mutex 一个互斥信号量;
Empty = n 表示剩余空间
Full = 0 表示消息总数
2、读者-作者问题
对于共享数据,有的进程只需要读,有的进程需要更新(读和写),前者叫读者,后者叫作者。
2.1 第一读者问题
读者不应保持等待,除非作者已经获得权限使用对象。也就是,只要没有作者在访问,读者都可以读。
2.2第二读者作者问题
一旦作者就绪,,作者会纪念可能快地执行,也就是,如果有作者在等待访问对 象,那么不会有新的读者可以读。
第一读者-作者的实现:
rw_mutex = 1: 读写锁,主要是给作者用,也是第一个读者和最后一个读者使用,因为如果有一个读者在读,那么作者必须等待,直到最后一个读者读完。
mutex 用于确保read_count=0 的变化互斥。
2.3 读写锁
有的系统提供读写锁,申请读的时候,获取读模式的读写锁,申请写的时候,获 取写模式的读写锁,但是只有一个进程可以获取写模式的读写锁,作者进程需要互 斥访问。
适用情况:容易识别哪些进程只需要读共享数据和只写共享数据
读者进程比作者进程多很多。
3、哲学家就餐问题
解决一个给多个进程之间分配多个资源,但不会出现死锁和饥饿的问题。
5个哲学家,5根筷子
每个筷子当作一个信号量,那么只有两个筷子同时拿起才可以吃饭,吃完会释放。 它保证了两个邻居不能同食,但是,会出现死锁,因为当所有哲学家同时拿起左边 的筷子。大家就无限等待了。
解决:
最多做4个哲学家
只有两根筷子都可用才能拿起(必须在临界区拿起)
非对称方案:单号先拿起左边的,双号先拿起右边的,也就是让相邻两个人,竞争 同一个筷子,谁先得,谁可能有饭吃。
七、管程
信号量的缺陷:
如果信号量不按先wait 后 signal的顺序,将会导致多个进程进入临界区。
如果信号量不小心用wait来代替了mutex,会出现死锁。
如果省略了wait和signal其中一个,都可能发生违反互斥或死锁的事件。
也就是说,如果程序员没有正确使用信号量来解决临界区问题,还是容易产生各种错误的。
因此引入了更高级的同步工具,管程
1、管程的概念
管程类型是属于ADT类型,即封装了一些数据和对其操作的一组函数。
管程包括一组变量,用于定义这一类型的实例状态,也包括操作这些变量的函数。而且里面的操作都是互斥的。
管程确保每一次只有一个进程在管程内处于活动状态(可以同时有多个进程,只有一
个在运行,其他都挂起)
结构:
1.1条件变量
要处理某些同步问题,可以定义一个或多个类型的condition变量。
每个条件变量只有,两种操作wait和signal
注意这里的wait 和signal 跟前面的信号量有所不同。
x.wait() 所有调用这个操作的进程,都会被自动挂起。(知道条件不满足,自己主 动挂起)
而信号量的是通过wait去试探,请求看能否进入临界区。
x.signal() 会恢复其中一个挂起的进程,这是在x变量下的进程运行的,一旦完成 都会调用的。如果没有相应的挂起进程,那就没什么作用。
但是信号量的signal始终会影响信号量的状态。
1.2唤醒进程
对于P调用了x.signal() ,如果Q 挂在x上,那么有两种选择
唤醒并等待:进程P等待直到Q离开进程,或者等待另一个条件
唤醒并继续:进程Q等待直到P离开,或者等待另一个条件(看似更合理些)
2、哲学家问题的管程解决方案
这个要求每个哲学家先pickup()然后再putdown()
这里能够保证哲学家不会同时用餐,且不会出现死锁。
但是会出现哲学家饿死,因为self[i]的不到满足时,把自己挂起的时候,没有人会去释放他,可以把self[i] 改成是每个筷子的条件变量,当的不到满足时,哲学家把自己挂载那个筷子上,其他哲学家用完该筷子,就会调用对应的signal。
3、采用信号量管程的实现
关键信号量:
mutex: 控制整个管程内的互斥,进入管程需要wait,离开后需要调用signal
或者理解为,执行管程里面的操作要先wait后signal(后面的实现是这种)
next:信号量,初始化为0,唤醒进程可以通过next来挂起自己,next其实是管程内执行的统一队列,所有要在里面操作的进程都要在这排队,条件变量的也是,所以它维护了互斥操作。
next_count:整型变量,计算挂在next的进程数
因此每个操作函数的结构如下:
If(next_count>0) 这个主要是,针对,如果管程还有挂起的进程,就先让他们执行完,最后才释放metex
条件变量的实现:
第一段个上面异曲同工,注意x_sem 和 x_count 初始都为0,x.wait()的实现入下:
记住不是进程一进来请求x变量就调用它,而是如果得不到满足(通过其他变量来判断)才调用。
对于signal,只要每个跟x相关的进程结束都会调用
signal(x_sem) 就是从x_sem 队列释放它,但是通过wait(next);让它到next那里排队。
4、管程的重启
就对于挂在同一个条件变量x的进程,当调用x.signal 时,要选择一个进程启动,当然可以用FCFS,但是也可以支持优先级(条件等待)。例如,如果是对一个资源的访问,可以根据访问时间长短进程,排优先级,通过wait(param) 的参数排序。
5、管程存在的问题
没进程可能没有获得访问权限,访问资源
进程获得资源后,不释放
进程没有获得资源,但是请求释放
进程请求同一个资源两次,中途不释放
这些都是一些访问资源操作不当的情况,我觉得可以通过把那种访问和释放封装在一个函数,那么只要访问,就必释放,可以减少这种错误的出现。
第七章 死锁
一、资源的使用
进程使用资源的顺序:
1、申请:进程请求资源,如果不能立刻被允许,那么申请进程应等待,直到获得该资 源为止。
2、使用:进程对资源进程操作
3、释放:进程释放资源
当一组进程内的每一个进程都在等待一个事件,而这些事件只能由这一组进程的另一个进程引起,那么这组进程就处于死锁状态。
物理资源(打印机,磁带驱动器,内存空间,CPU周期)
逻辑资源(信号量,互斥锁,文件等)
二、死锁的特征
1、四个必要条件
- 互斥:至少有一个资源处于非共享模式,即一次只能被一个进程使用
- 占有并等待:一个进程占有者至少一个资源,并等待另一个资源,而该资源被 其他进程所占有。
- 非抢占:资源不能被抢占,只能是进程在完成任务后自动释放。
- 循环等待:加入有一组进程P0 -
Pn,那么P0 等待的资源被P1占有,P1等 待的别P2占有,如此类推。
2、资源分配图
图由两个顶点集组成:
P ={所有活动进程的集合}, R={所有资源类型的集合}
Pi -> Rj 表示进程i请求 j资源
Rj -> Pi 表示把资源分配给进程i
每个资源都是只有一个实例,那么如果存在环,那么环上的进程就产生了死锁。
每个资源不止一个实例,那么如果存在环,那么环上的进程可能产生死锁。
3、死锁的处理方法
有三种:(预防,检测恢复,忽视)
- 通过协议来预防或避免死锁,确保系统不会进入死锁。
- 允许系统进入死锁,然后检测恢复。
- 忽视这个问题,认为死锁不会发生。
死锁预防:至少确保死锁的必要的条件的其中之一不成立。
死锁避免:利用操作系统知道有关进程的申请资源和使用资源信息,判断每个申请 是否允许还是延迟。
系统将考虑到:现有可用资源,已分配资源,每个进程将来申请和释放的资源。
三、死锁预防
确保至少一个死锁必要条件不成立
1、互斥
共享资源不要求互斥访问,因此不会发生死锁,但是有些资源本身是互斥的没 办法,例如互斥锁。
2、持有且等待
一个进程申请一个资源时,不能占有其他资源。
两种可行协议:
每个进程执行前,一次性申请所有资源。
显然这种资源利用率比较低,因为许多资源可能已经被分配,但是很长时间没 有被使用。
一个进程仅当其没有资源时,可以申请资源。然鹅它要申请更多其他资源时,必须释放现已有的所有资源。
这种可能会发生饥饿,因为一个进程可能需要很多资源,但是它所需要的资源 可能至少有一个已经分配给其他进程,可能会永久等待。
3、非抢占
使其可以抢占,当一个进程持有一些资源,并申请另一个不能立刻被分配的资 源,那么它所持有的资源可以被抢占,然后被抢占资源可以添加到等地啊的资 源列表,只有当该进程获得原有资源和申请的新资源看,才可以重新执行。
4、循环等待
确保不成立的一个方式是:
对所有资源类型进行一个完全排序,要求每个进程按递增的顺序来申请资源。
当进程拥有Ri时,只有当Rj 大于所有Ri,才可以申请Rj,或者显示释放所有资源 R,Ri>=Rj,再申请Rj。
四、死锁避免
由于死锁避免有一定的副作用:设备使用率低和系统吞吐率低
死锁避免需要额外的信息,即如何申请资源,一般需要知道资源分配的状态:可用 资源,已分配的资源,进程的最大需求。
1、安全状态
如果系统能够按一定的顺序为每个进程分配资源,仍然避免死锁,那么系统的状态是安全的。
一个安全序列<P1,P2,… Pn>,对于每个进程Pi仍需要申请的资源数必须小于所有Pj(j<i)进程所占有的资源数+当前可用资源数,那么这个序列就是安全序列。
核心思想:
当一个进程申请一个可用资源时,如果分配后仍处于安全状态,就可以立即分配,否则,让进程等待。
现在这种方式与没有使用死锁避免算法的系统相比,资源使用率可能会更低。
两种死锁避免算法
2、资源分配图法
适用于每个资源类型只有一个实例:
增加一个需求边,Pi->Rj 用虚线表示,Pi将来可能会申请资源Rj
避免思想:
若个Pi申请Rj资源,把边修改成Rj -> Pi ,如果存在环(包含虚线),那么系统 就是处于非安全状态,不能分配该资源,否者处于于安全序列,可以分配。
环检测算法,需要n2复杂度,n为进程数。
3、银行家算法
资源分配图法,不适用于多个实例,银行家算法适用,但是效率不如资源分配图法
3.1 需要的数据结构
Available[m] , 表示每种资源可用实例数,available[i] = k 表示资源i有k个实例
Max[n][m],表示每个进程需要资源的最大数 Max[i][j]=k 进程i需要j资源最大数量为k
Allocation[n][m],表示每个进程已经分配的数量,allocation[i][j] = k 进程i分配了j 资源的数目为k
Need[n][m],表示每个进程仍需要的资源数,need[i][j]=k,进程i仍需要资源j的数量为k
约定:对于数组X <= Y 当且仅当所有Xi <= Yi .
3.2 安全算法
目的:用于检查当前系统状态是否安全
1、初始化
Work = Available,Finish[n]=false ,表示所有进程都在运行
2、依次寻找适当的进程i,满足
a.Finish[i] == false 进程i还在运行
b.Need[i]<=Work 进程i需要的资源小于可用资源
如果没有满足的条件,就转到第四步
3、Work += Allocation[i], finish[i]=true,模拟结束i进程,回收i进程资源
4、遍历所有i,如果finish[i]==true 都成立,则系统是安全的,否则不安全。
显然这个算法的代价是mxn2 的数量级。
3.3 资源请求算法
1、判定请求资源Requesti <= needi 是否成立,不成立结束,因为申请超过需求的资源。
2、判断Requesti<=available 是否成立,不成立就要等待,因为没有足够的资源可用。
3、上面都通过后,系统假装分配资源给进程i,修改对应的数据
Available -= requesti
Allocationi += requesti
Needi -= requesti
然后调用上面的安全算法,判定是否系统还处于安全状态,如果是,可以分配,否则不能分配。
示例:
五、死锁检测
如果一个系统不提供死锁预防,也不提供死锁避免算法,那么他需要提供:
检查系统是否出现死锁的算法
从死锁中恢复的算法。
1、对于资源都是单个实例
把资源图变换成等待图:
通过删除所有资源类型结点,并合并适当的边(同方向合并,否则取消)。
如下:
然后如果等待图出现一个环,那么系统就死锁,所以系统需要维护一个等待图,并 定期调用检测算法,其中检查算法的复杂度为n2.
2、多实例资源
采用银行家算法的安全算法,如果最后有finish[i] = false 那么系统就出现死锁,而且为false的那些进程发生了死锁。
六、死锁恢复
1、进程终止
终止所有死锁进程。但是这个代价比较大,例如一些进程运行了较长时间,又必须重新执行。
一次终止一个进程,直到消除死锁循环为止。这个开销也相当大,因为终止一次,又必须调用一次检查算法。
终止进程也很讲究,我们应该终止造成最小代价的进程。
可以根据进程的优先级、运行时间、资源数量等等来衡量。
2、资源抢占
通过不断抢占某些进程的资源以便给其他进程的资源使用,直到死锁破坏为止。
如同死锁预防的抢占式。
三个问题:
- 选择牺牲进程:尽可能使得代价最小,可以通过进程拥有的资源数量,进程运行的时间等来衡量。
- 回滚:要把牺牲进程回滚到某个安全状态,以便从改状态重启进程。
- 饥饿:保证资源不会总是从同一个进程抢占,应该确保一个进程有一个有限次数被选择为牺牲进程。可以把回滚次数作为代价因素之一。
第八章 内存管理策略
内存是由一个很大的字节数组组成,每个字节都有自己的地址。
一、基本硬件
CPU可以直接访问内存或处理器内置的寄存器,机器指令可以采用内存地址作参数,但是不能用磁盘的地址作为参数。
1、保护
单独的进程内存空间可以保护进程而不互相影响,通过两个寄存器来界定合法地址的范围。
基地址寄存器:含有该进程最小的合法物理地址。
界限寄存器:指定了范围的大小。
保护的实现原理:CPU硬件对用户产生的地址与寄存器的地址比较,如果在范围内就合法,否者陷入操作系统,当作致命错误处理。
2、寄存器值的管理
只有操作系统通过特殊的特权指令,开可以加载基地址寄存器和界限地址寄存器,因此只有操作系统可以修改,用户程序不能修改。
多任务时,操作系统进行上下文切换时,应将一个进程的寄存器状态存到内存,然后从内存调入下一个进程的上下文到寄存器。
3、地址绑定
程序的二进制可执行文件,一般存放在硬盘,需要执行时才会调入内存,程序执行前一般经过一些步骤,把指令和数据绑定到存储器的地址:
编译时:如果知道进程驻留地址,那可以生成绝对代码
加载时:如果编译时不知道驻留何处,就生成重定位代码,绑定延迟到加载时进行。
执行时:一个进程会从一个内存段移到另一个内存段,就会需要把绑定延迟到执行时。
4、地址空间
CPU生成的地址通常成为逻辑地址,内存单元看到的地址通常称为物理地址。
程序生成所有逻辑地址的集合叫逻辑地址空间。
这些逻辑地址对应的所有物理地址的集合叫物理地址空间。
从虚拟地址到物理地址的映射通过内存管理单元(MMU)硬件设备来实现的。
一般是物理地址 = 虚拟地址+基地址寄存器的值(重定位寄存器)
5、动态加载
由于受限于内存的大小,程序有时不能一下子全部加载到内存,可以采用动态加载的方式,即一个程序只有在需要调用时才会加载。所有程序都以重定位加载格式保存在磁盘上。先执行主程序,调用程序需要时再加载。
6、动态链接
动机:对于常用的系统库,每个程序如果需要用到相关程序,那么每个程序加载到内存时,都有一个系统库的副本,那么这样非常浪费磁盘空间和内存空间。
所以就有了动态链接:
动态链接库为系统库,可以链接到用户程序,以便运行。
原理:对于每个程序的二进制映像内,每个库程序的引用都有一个存根。这个存根是一下段代码,用来指出如何定位在内存驻留的库程序,或者如果不在,入股从磁盘加载到内存。找到库程序后,存根会用库和程序地址来替换自己,并开始执行程序。
因此,每个需要库程序的进程,都只需要同一个库代码副本就行了。
动态链接,也用于库的的更新
7、交换
从进程暂时不用执行时,而内存需要释放空间,就可以把相关进程从内存交换到备份存储。
交换既可以是换入:从磁盘调度进程到内存
也可以是换出:从内存调度到磁盘
但是,交换的代价比较大,传输时间基本是s级别的。
正常情况下,一般不交换,当内存空闲内存低于某个阈值,就启用交换,当数量增加了,就停止交换。
二、连续内存分配
(程序连续不分开)
内存通常分成两个区域:一个用于驻留操作系统,一个是用于用户进程的。由于中断向量常放于低内存,所以,操作系统也放在低内存。
1、内存分配
1.1采用固定分区法
将内存分为多个固定大小的分区,每个分区只能包含一个进程。因此多道程序受限 于分区数。
不好,主要是浪费空间,因为进程需要的空间可能大小不一。
1.2可变分区
操作系统把内存也是分成一个个块,但是这些块大小不一,这里称为孔。因此内存 就是一个有这不同大小的孔的集合。
对于操作系统:要维护两个列表,一个是可用块的列表和一个需要分配空间的进程队列。
分配:通常,如果没有足够大的空可以分配,那么进程就需要等待,如果孔太大, 就分成两块,一块分配个新进程,一个还回到孔集合。
1.3孔分配问题
首次适应:从头开始查找,分配首个足够大的孔
最优适应:分配最小的足够大的孔,应该要遍历整个列表去查找。
最差适应:分配一个最大的孔。
评价:首次适应和最优适应在执行时间和利用空间方面都好于最差适应。但他们两 在空间利用上难分伯仲。首次适应最快。个人觉得,最差适应容易造成更多的外部 碎片。
1.4碎片
外部碎片:就是内存有很多不连续的孔,单独分配给进程不够大,但是合并其他就 可以利用。最坏的情况是,没两个进程之间有一个空闲块。
首次适应和最优适应算法都会产生外部碎片。
内部碎片:如果把内存按照固定大小的块来分配内存,那么进程需要的空间和块的 大小的空间之差,成为内部碎片,因为它是属于某个进程的,不能被利用。
外部碎片的解决办法:
1、紧缩,通过移动内存内容,把空闲的空间合并起来。但是这种紧缩不是都可行, 对于重定位是静态的就不能移动,动态的还行,移动后改变基地址寄存器值。 而且移动的开销也不小。
2、允许进程的逻辑地址空间(不应该时物理地址空间吗?)不连续,这样只要有物理 内存可用,就可以分配内存。
三、分段
(程序分段,内存也分段)
程序员更希望看到内存是由一组不同长度的段组成,这些段之间没有顺序。
分段就是支持这种用户视图的管理方案。
1、基本概念
逻辑地址地址空间由一组段组成,每个段都有名称和长度。用户通过段名称和段偏移来指定一个地址。
一个C编译器经常会自动构造下面几个段:
- 代码
- 全局变量
- 堆
- 每个线程使用的栈
- 标准的C库
2、分段硬件
我们现在能够通过二维地址来引用程序对象,但是物理地址还是一维的,所以需要一个把二维地址映射到一维物理地址的机制
2.1通过段表来实现
段表的每个条目都有段基地址和段界限,段基地址表示该段在内存开始的物理地 址。段界限指定该段的长度。
2.2 段表的使用
每个逻辑地址由两部分组成:段号s和段偏移d。段号作为段表的索引,逻辑地址 的偏移d,应该位于0和段界限之间,如果不是,就陷入操作系统。如果d合法, 那么基地址值+段偏移就是实际物理地址。
如下图:
四、分页
分段允许物理地址空间是非连续的,分页可以提供这种优势,而且分页避免了外部碎片和紧缩,而分段不可以。
分页也避免了将不同大小的内存块匹配到交换空间的麻烦问题,因为磁盘也采用分页。
对于分段,当代码和数据需要换出时,应在备份存储找到空闲的空间,备份存储同样有与内存相关的碎片问题。
目前大多数操作系统都采用分页机制,实现分页需要操作系统和硬件的协作。
1、基本方法
将物理内存分为固定大小的块,成为帧或者页帧。将逻辑内存也分为同样大小的块,称为页或者页面。备份存储划分为固定大小的块,大小和单个或多个内存帧一样。
这样,逻辑地址空间完全独立于物理地址空间,所以一个进程可以有64位的地址空间,但是物理内存小于64位的地址空间。
2、映射机制
页表:包含每个页所在物理内存的基地址。
CPU生成的每个地址分成两部分:页码(p)和页偏移(d)。页码作为页表的索引,找到对应的基地址与页偏移的组合,形成了物理内存地址。
页大小一般由硬件来决定,为2的幂,从512字节到1G不等。选择2的组要好处是方便将逻辑地址转换成页码和偏移。
设逻辑地址空间为2m字节,页大小为2n字节,那么逻辑地址的高m-n位表示页码,低n位表示页偏移。
p为页表索引,d作为页的偏移。、
3、分页的缺点
随着进程需要的空间越大,页表需要的空间也越大,但会随着页的变大而减少。
所以页码大小的选择也很关键。
分页还是有内部碎片的,因为如果需要的空间比页小,就出现了内部碎片。
分页增加了上下文切换的时间,因为还有页表这些信息要切换。
对于32位CPU,一般一个页表的条目是4字节长,也就是一个32位的条目可以指向2^32个物理帧的任意一个。
4、操作系统管理内存
操作系统应该要知道哪个物理帧分配或者未分配,这些信息保存在一个帧表的结构,每个条目对应一个帧,表示空闲还是占有,如果占用,被哪个进程的哪个页占用。
操作系统为每个进程维护一个页表的副本,如果用户执行系统调用,提供地址参数,那么这个地址能够形成正确的物理地址。
5、页表实现机制
有的操作系统为每个进程分配一个页表,页表指针存在进程控制块里面。这样进行 上下文切换时,可以切换到正确的页表。
把页表存在内存,并将页表基地址寄存器(PTBR)指向页表,改变页表只需要改 变这个寄存器值就行,大大减少上下文切换时间。
显然,访问内存的一个自己需要2次内存的访问(1次用于查找页表条目,1次用 于字节),内存访问的速度就减半了。
6、TLB
TLB又叫转换表缓冲区,用于缓存页表字段的,以提高访问物理内存的速度。
TLB的条目有两个部分组成:键和值。其实就是页码和帧码对。
6.1 工作原理
当CPU产生一个逻辑地址后,发送页码到TLB,如果找到这个页码,就直接用对应的帧码访存。
如果不在(不命中),就去内存访问页表,得到帧码后,可以直接用来访问内存,同时,把页码和帧码添加到TLB,下次访问同样的页码即很快命中。
6.2替换策率:
因为TLB不大,所以很容易满,当满的时候,会选者一个替换,一般有LRU(最近最少使用),轮转替换,随机替换这些策略。
有些TLB允许条目固定下来,例如一些重要内核代码的条目。
6.3 地址空间标识符(ASID)
有些TLB条目还保存地址空间标识符,唯一标识每一个进程,为进程提供进程地址空间的保护。TLB视图解析虚拟页码,确保当前运行的进程ASID与虚拟页码对应的ASID一样。如果不匹配,就不明中,因为对于多道程序,可能有相同的页码号,但是不同的进程映射的物理地址是不一样的。
如果不支持ASID,那么每次上下文切换的时候,TLB就会被刷新(flush或删除),确保不会有进程使用错误的地址转换。
7、保护策略
在页表项提供一个位表示可读可写或者只读。所以如果一个进程对某个页的操作跟该位的模式不合就会陷入操作系统。
页表一般还提供一个有效-无效位。当该位有效时,改制表示相关页在进程的逻辑地址空间,否者不在。
8、共享页
如果多个进程有共同的共享代码和各自的数据段,那么他们的共享代码页可以映射到相同的物理也,这样可以大大节省内存的空间。这样的代码又叫可重入代码,或纯代码(这样的代码是不能自我修改的)。
五、页表结构
多级页表的重要性:
现在计算机系统一般支持2^32 - 2^64 的逻辑地址空间,假如页大小为4KB,那么页表条目也有100w(2^20)多个,如果一个条目4个字节,那么需要4MB的地址空间。
这个成本是挺大的。
1、二级页表
前提:
假设,一个32位逻辑地址空间,4K大小的页,那么高20位为页码,低12位为页偏移。对页表再分页,就是10位页码和10位页偏移(原因是一个页表4KB,只能容纳1K个4字节的条目)
一个逻辑地址就分成如下:
其中p1是用来访问一级页表的索引,p2用来访问2级页表的页偏移。
地址由外向内转换,这种方案又叫向前映射页表。
对于64位逻辑地址系统,分二级,那么一级页表就有2^42
个条目,所以要分成多个级别,分三级,2^34(16GB),同样不合理
因此,对于64为的架构,分成页表通常认为是不适当的。
2、哈希页表
常用于处理大于32位的地址空间,采用虚拟页码作为哈希值,哈希页表的每一个条目都包括一个链表,用于解决哈希冲突的。
每个元素有三个值:虚拟页码,映射帧码,指向链表下一元素指针。
用于64位的地址空间的一个哈希变体,采用聚簇页表。
3、倒置页表
整个系统只有一个页表,对于每个真正的内存也或帧,倒置页表才有一个条目。现如这种方式,页表的条目不会太多,当然为了区分不同进程,需要有一个pid标识。
这种的而缺点是,虽然减少了存储每个页表的内存空间,但是增加了查找页表所需要的时间。
第九章虚拟内存管理
虚拟内存技术允许执行进程不必完全处于内存,因此程序可以大于物理内存。
一、背景
很多情况,不需把完整的程序加载到内存,如下:
- 程序通常有处理异常的代码,但是一般很少发生
- 数据、链表等分配的空间多余实际需要值
- 即使需要整个程序,但也不一定同时需要整个程序,可以动态加载
主要的好处:
程序不再受物理内存的可用量限制
每个用户程序占有较少的物理内存,增加多道程序的程度。
加载或交换进程到内存的时间会更少,用户程序会运行更快。
1、虚拟内存
从每个进程的角度,都有一个从0到n的连续逻辑地址空间。n取决与操作系统的 位数。
同样对这个虚拟内存进行分页,通过内存管理单元MMU将逻辑页映射到内存的物 理帧。
2、虚拟内存的好处(主要是共享)
- 将逻辑内存和物理内存分开,用户不需考虑内存是实际如何分配,就当作自己有一个独立的内存空间
- 通过将共享对象映射到虚拟地址空间,系统库可以为多个进程共享。尽管对于每个进程来说,感觉库只是自己内存空间的一部分。
- 允许进程共享内存,每个进程可以把某部分空间映射到某一段共享内存。
- 系统调用fork()创建进程时,可以共享页面,从而快速创建进程。
二、请求调页
进程从磁盘加载到内存运行,有两种策略:
- 将整个程序加载到物理内存
- 仅需要某些页面时才加载。
后者这种技术又叫请求调页。
1、工作原理
进程一般驻留在磁盘上,当进程需要进行时,它被交换到内存,不过不是整个进程,采用的是惰性交换器。除非需要某个页面,否则不会交换到内存。
交换器一般操纵的是进程,调页程序值涉及进程的页面。
如此一来,调页程序可以避免读入那些不使用的页面,也减少了交换时间和所需的物理内存空间。
2、硬件支持
此时的页表每个条目同样有两个字段,物理帧号,有效-无效位
如果有效位为v,就说明页面在内存中,如果为i,表示页面无效(不在进程的逻辑地址空间)或者有效但在磁盘上。对于无效条目,可以简单标记无效或者包含磁盘上的页面地址。
3、缺页处理
当进程试图访问一个尚未调入内存的页面,就会出现缺页错误,陷入操作系统,调用缺
页处理程序
(1)检查进程的内部表,确认该引用是有效的还是无效的内存访问
(2)如果无效,终止程序,如果有效但是尚未调入页面,就立刻调入。
(3)找到一个空闲帧(从空闲帧链表上得到)
(4)调度一个磁盘操作,把所需要的页面读到刚刚分配的帧
(5)磁盘传输完成,修改进程的内部表和页表,指示该页已经处于内存。
(6)重新启动被陷阱中断的指令。
三、写时复制
1、Fork
系统调用fork,为子进程创建了一个父进程地址空间的副本,复制属于父进程的页面。(物理帧复制的意思吧)。
考虑到很多子进程创建后,调用系统调用exec(),那么复制父进程页面就很没有必要,所以可以采用一种写时复制,允许父进程和子进程最初共享相同的页面工作,且这些页面标记为写时复制,如果任何一个进程写入共享页面,就创建一个共享页面的副本。
这样就可以既可以工作,又可以保留修改不影响别的进程。
2、vfork(virtual memory fork)
不同于写时复制,调用vfork时,父进程被挂起,子进程使用父进程的地址空间,因此子进程的修改,对于恢复后的父进程是可以看见的。因此对于子进程创建后,立即调用exec,可以调用vfork,因为没有复制页面。
四、页面置换
如果内存同时驻留很多程序,就会导致内存过度分配,那么进程容易发生缺页错误,因此操作系统会通过交换一些进程,以释放他们所有帧,降低多道程度,需要页面置换。
1、基本页面置换
如果没有空闲帧,那么查找当前不使用的一个帧,释放它:将其内容交换到交换空间,并修改页表和相关表,以表示该页不在内存中。然后把缺少的页调入。
(1)找到需要页面的磁盘位置
(2)找到一个空闲帧:
a.如果有空闲帧,那么就使用它
b.如果没有,就使用页置换算法选者一个牺牲页
(3)将需要的亚目读入空闲帧,修改页表和帧表
(4)从发生缺页的位置,继续执行用户进程。、
注意到如果没有空闲帧,需要有两个页面的传输(一出一入),实际上加倍了缺页错误的处理时间,相应的增加了访问时间。
增加修改位/脏位来减少这种开销。每个页面或帧都有一个修改位,如果该帧从调入到当前被修改过,就修改那个修改位,否则不修改。
那么对于没被修改过的页,或者(只读页),如果要换出去,就不用写出了,因为外面有同样的副本。
请求调页主要要处理两个问题:
帧分配算法和页面置换算法。
通过引用串来模拟每种置换算法的性能
2、FIFO页面置换
算法思想:当替换页面时,将选择最旧的页面替换。并不需要记录确却的时间,通过一个先进先出队列管理就行。
性能:共计15次缺页,这个有时候不理性,比如某个页很早被初始,但它还是经常被使用,这个会导致很多的缺页发生。
Belady异常:
把上面的串改为这个,如果分配4个帧(缺页10次),3帧(缺页9次)
随着分配帧的数量(分配每个进程的数量)增加,缺页的错误率可能会增加。
3、最优页面置换(OPT)
算法思想:置换最长时间不会被使用的页面。
通过遍历后面的引用,确定那个不会被调用
性能:9次缺页错误,这个算法具有最低的缺页错误率,而且不会遭受belady异常。
这个算法比较难实现,因为需要知道引用串的未来知识。(因此只能有一个近似的)
4、LRU页面置换
算法思想:OPT是根据页面将来使用的时间来决定,用最近代替未来,那就是替换最近最长时间没有被使用的页。
性能:12次缺页错误,是个不错的策略,LRU也没哟Belady异常。但是怎么实现呢?
4.1实现策略
采用计数器:
每个页表条目附加一个计数器,每引用依次,+1,那么替换的时候,就看哪个最小就替换哪个。
队列实现:
页面每被引用依次,就放到队首,那么最少引用的肯定就是队尾的那个,直接替换队尾的,节省了搜索时间,就是每次更新有点费时。
5、帧分配
全局置换:允许一个进程从所有帧集合中选择一个置换帧,而不管它是否已经分配給其他进程。
局部置换:要求每个进程只能从自己分配的帧,选择替换。
五、系统抖动
对于没有分配足够帧的进程,很容易产生缺页错误,必须置换某个帧,但是其他帧都在使用,然后替换之后,又很被引用,就导致快速发生缺页错误。
**这种高度页面调度活动,称为抖动。**如果一个进程调页时间多余它执行时间,那么这个进程就在抖动。
原因1:可能是多道程序过多,因为这个时候,CPU利用率会降低,低了之后,可能CPU调度程序会增加程序的调入,进而加剧。
解决:降低多道程度。
原因2:页置换算法的问题,
解决:可以通过局部置换算法或这优先权置换算法限制系统的抖动。例如LRU。
第十章 存储管理
一、磁盘
磁盘或硬盘都是计算机系统提供大量的外存。
由许多盘片叠成,每个盘片表面逻辑地划分圆形的磁道,每个磁道划分出扇区,扇区的大小一般为512KB。所有盘片的同一个磁道的集合叫柱面。
磁头附在磁臂上,磁臂将所有磁头作为一个整体一起移动。
RPM:高速旋转磁盘每分钟旋转数,一般在60-250次之间
传输速率:驱动器和计算机之间的数据流的速度
磁盘访问时间:
寻道时间:移动磁臂到所要柱面需要的时间
旋转延迟:旋转磁道(磁臂)到所要开始扇区的时间
传输时间:用来传输数据的时间
二、磁盘调度
操作系统需要让磁盘驱动器具有较快的访问速度和较宽的磁盘带宽。
寻道时间:磁臂移动磁头到包含目标扇区的柱面的时间。
旋转延迟:磁盘旋转目标扇区到磁头下的额外时间。
磁盘带宽:传输字节总数除以服务请求开始到结束的总时间。
如果磁盘驱动器或者控制器空闲,对于磁盘IO请求可以立即处理,否则添加到请求队列里面。
因此涉及一个磁盘队列调度的问题。
1、FCFS调度
就是先来先服务,如下图所示,这种情况,可能波动会比较大。
2、SSTF调度
最短寻道时间优先,选者里当前磁道最短寻道时间的请求,但是这个可能还是会有 比较多的重复路。
它的本质跟SJF一样,同样会导致一些请求饥饿。
3、SCAN调度
扫描算法:磁臂从磁盘的一端开始,向另一端移动;一过每一个柱面,如果有请求,就处理,移到另一端之后,然后反向处理。这个的一个好处是,可以节省重复的路,但是如果请求集中在中间,那么走到两端就很昂非时间。
它又叫电梯算法,因为先处理向上请求,然后处理向下请求。
4、C-SCAN
改进的SCAN ,循环扫描,从一个端到另一个端,沿路处理,返回立刻返回到左端,不处理。
5、LOOK调度
这个也分LOOK 和 C-LOOK,但是不同的是,他们左右移动的时候,指挥移动到最远请求的地方为止,不一定要走到两端。
一般SSTF是比较常见的,比FCFS有更好的性能,对于磁盘负荷比较大的系统,SCAN 或C-SCAN 表现更好,因为他们没有出现饥饿。
三、RAID结构(磁盘冗余阵列)
使用RAID主要原因是高可靠性和高数据传输速率,而不是经济原因。
可靠性:通过引入冗余,重复每个盘的信息,那么一个盘坏了,还有备份,这种技术叫镜像。
并行处理:把数据分散在多个磁盘上,最简单的方式采用数据分条
- 位级分条:吧每个字节分散到多个磁盘上,例如把一个字节分配到8个磁盘,第i位放在第i个盘上。
- 块级分条:文件的块可以分散在多个磁盘上;
好处:通过负载平衡,增加了多个小访问的吞吐量
降低了大访问的相应时间。
根据不同的冗余程度,可以分为以下层级:
假定没给磁盘的吞吐率为S,传输速率为R,N 为总的磁盘数
1、RAID level0:
具有块分条,没有任何冗余
2、RAID level1:
每个磁盘,有一个对应的镜像
读磁盘,可以选择其中一个读,写两个都要写
容量:只能为N/2
可靠性:允许每一对磁盘的其中一个损坏
吞吐量:(连续)读吞吐两最大为N/2 * S
(随机)读速率可以达到 N · R
(随机)写N/2 * R 因为要两个磁盘同时写
3、RAID level2:
采用奇偶位错误校验码,有n个奇偶校验为磁盘,每个字节对应一个奇偶校验位。
如果是奇校验,那么如果1的个数为奇数校验位为0,否则为1.
4、RAID level3:
采用一个校验磁盘,位交错奇偶校验,校验位是前面磁盘的同一个位置1的个数的 校验。
5、RAID level4:
块级交错奇偶校验
容量:N-1
可靠性:只能容忍一个存储磁盘的损坏
吞吐量:连续或随机读都是N-1 * S
连续写 N*R ,随机写 N/2
随机写差的原因是,有写冲突,因为一次只能写一个奇偶校验盘,虽然存储盘可以并行
6、RAID level5:
旋转校验(块交错分布式奇偶校验)
这个是右对齐,还有一个是左对齐的。
注意S 和R 是针对数据来说的,所以为什么R-4 的Random read 是N-1 因为只有N-1个盘有数据,而R-5 的Random read 是N 因为每个盘都有数据。
四、文件系统管理
假设我们把磁盘分成4KB的一个个小块,我们的磁盘大小为256KB,那么如下:
因为磁盘大部分空间用来保存用户数据,所以我们把8-63的所有块作为用户数据区域
因为需要有一些结构来跟踪每一个数据块的信息,我们采用inode这样的结构来表示。那么磁盘需要为这些inode table预留一些空间,我们放在3-7 block
我们仍然需要一些结构来表示inode或data block 是否已经被分配还是空闲的
这里采用位图来描述:一个是data bitmap 一个是inode bitmap
最后第一个超级块:包含一些描述文件系统的信息,如inode的数量80(因为假设一个inode256字节),data blok的数量56,Inode开始的位置 begin at 3 .
细化图:
寻找某个inode的具体扇区计算公式:
Inumber是inode的序号,blockSize 是一个块的大小,因此计算blk是计算出在哪个block
然后加上inode偏移地址/扇区大小,得到真正所在的扇区。因为磁盘一般按照扇区读写的。Data block 也如此计算。
2、间接指针
为了支持更大的文件,即一个数据不能装下一个文件(大于4KB),那么可以采用多级指针,一个indoe指向的数据块,存储的是指向data block的指针。
那么在本样例,一个data block可以容纳1024个pointer,所以,采用间接指针(假设一个inode有12个直接指针和1个间接指针),那么一个inode可以管控的数据大小是(12+1024)4KB的数据,约4M的文件
二级指针为:(12+10241024)*4KB 越4G的文件
3、访问文件的过程
其实目录也相当于文件,所以一个目也有一个对应的inode
root/foo/bar
注意到,读了bar之后,要从新写bar inode 是因为,indoe会记录一些访问信息。
典型的inode结构:
完结撒花(写了我一周的时间,虽然基本每天都是上午10点才起床,哈哈哈)
继计网后,有一篇3w+的作品。 - 2019/8/5