文章目录
第一章 计算机系统概述
1.1 基本构成
计算机有四个主要的结构化部件:
处理器: 用来控制计算机的操作,执行数据处理功能
内存: 存储我们的数据和程序
输入/输出模块: 也就是在计算机内部和我们的外部进行数据的移动,比如说将内存中的数据显示到显示屏上
系统总线: 在各个部件之间进行通信,比如说进行数据的传输
1.2微处理器的发展
微处理器,是指用一片或少数几片大规模集成电路组成的中央处理器,它的发明为台式计算机和便携式计算机带来了一场硬件革命
今天,微处理器不仅成为了最快的通用处理器,还发展成为了多处理器,每个芯片上面容纳了多个处理器,每个处理器上有多层大容量缓存,并且多个处理器之间共享内核的执行单元
为了满足便携式设备的需求,传统微处理器正在被片上系统所取代
1.3指令的执行
指令的执行可以分为两个阶段,一个是取指,另一个是执行,在每个指令周期开始时,处理器从存储器中取一条指令,取完后除非在某种情况下,否则处理器在每次取指后总是会递增PC,指向下一条指令存放的地址,执行完后继续取指,就这样不断的循环,直到我们关机或出现故障的时候程序执行才会停止
1.4中断
中断最初是用于提高处理器效率的一种手段,那为什么能提高处理器效率呢,假设现在一台打印机发出的打印的请求,那我们的程序只有在打印机完成打印后才能继续执行,但是打印机的运行速度相较于CPU很慢,就会导致浪费了时间
但是我们知道,打印机要完成一个工作要有一个准备阶段,而我们中断机制是指在打印机完了准备阶段后发出中断请求,然后CPU保存现在程序的信息,去指向打印机的请求,这比我们单纯等待打印机完成准备打印阶段花费的时间要少
也就是说没有中断我们需要等待打印机准备和执行的时间,而有了中断后我们就只需要在打印机准备好后再进行工作
由于中断随时会发生,所以在取指阶段和执行阶段之间要加上一个中断阶段来判断是否有I/O设备发出中断请求,如果没有中断就继续执行指令的下一个阶段,如果有中断,那么当前程序暂停,执行中断返回后再执行当前程序,这就导致我们的指令周期变成了下面这张图
前面我们介绍的只是单个中断,但是在实际的系统运行中往往不止一个中断发生,往往会有多个中断同时发生,那么当多个中断同时发生的时候我们该怎么处理呢
第一种就是正在处理一个中断的时候,禁止处理其它中断,也就是只有当前中断处理完成后才能处理其它设备发出的中断
但是这种方法的缺点是,未考虑相对优先级和时间限制的要求,例如,当来自通信线的输入到达的时候,可能需要快速接收以便腾出空间让处理下一批输入,如果在下一批输入到达的时候一开始的输入还没有处理完,那么就可能丢失数据
所以,为了防范这种情况的发生,就有了第二种方法,第二种方法是定义中断优先级,假设某个中断优先级比当前正在执行的中断的优先级更高,那么就执行中断优先级高的
1.5存储器的层次结构
为什么要用怎么多层呢,只用一层不好吗,当然不行,因为我们要求的是容量大,速度快,价格便宜,单单某一级的存储器无法满足我们的要求,所以我们才采用多级的结构来尽量满足我们的要求
在这个结构当中,某一些存储器是易失的,也就是一旦断电我们存储的数据都会消失,与之相对的就是非易失的,像我们的磁盘就是非易失的,断电后数据依然存在
1.6高速缓存
为何要用高速缓存呢,因为长期以来,处理器速度的提升比访问存储器速度的提升要快,这就带来了一个问题,那就是可能出现数据的传输跟不上指令的执行速度,这就会对处理器造成性能浪费
如果我们采用和处理器速度相同的存储器进行存储数据,成本又很高,承担不起,所以我们采用了高速缓存
高速缓存利用的程序的局部性原理,也就是在某一段时间内,可能程序只用到某段连续的内存,比如说我们的for循环操作,所以我们可以将这些将来很可能用到的数据提前放到比当前存储器速度快的上一级存储器当中,这样处理器就能更加充分的利用,在现代CPU中,往往有多级高速缓存
那高速缓存如何设计才能最大程度的发挥它的功能呢
当新的块读入高速缓存中的时候,将由映射函数确定这个块将占用哪个高速缓存单元,如果当所有单元都满的时候,我们需要使用置换算法来置换将来访问可能性最小的块。
并且当某个块被修改的时候,我们需要再这个块的数据被换出高速缓存前的时候将其写回进内存,写策略规定何时发生存储器写操作,一种极端情况是当块更新的时候就写回,另一种极端情况是当块被置换时才写回
1.7直接内存存取
执行I/O操作的技术有三种:可编程I/O,中断驱动I/O和直接内存存取(DMA)
可编程I/O就是说当处理器遇到I/O相关指令的时候,I/O模块执行相关动作,但是这时处理器不会去执行其它指令,而是每隔一段时间就检查I/O状态,确认是否完成,如果没有完成继续等待
这种方式会让CPU等待过长时间,会降低系统运行的效率和性能
第二种是中断驱动,就类似于我们直接的中断操作,当遇到I/O相关操作的时候发送命令给I/O,让I/O准备数据,期间处理器继续执行指令,当I/O准备完成后再执行I/O程序
第三种就是直接内存存取,这种方式只需要处理器发送命令给DMA模块,然后DMA控制I/O自行处理完所有程序并且自行传输数据,不需要处理器来传输数据,前两种都靠处理器来传输数据
但是要注意这种方式需要跟处理器抢占总线,当DMA模块正在传输数据的时候,处理器无法使用总线
1.8多处理器和多核计算机组织结构
多处理器的发展离不开我们对性能的要求,随着技术的发展和硬件价格的下降,计算机的设计者们找到了越来越多的并行处理机会,很常用的方法就是添加处理器
对称多处理器(SMP)
对称多处理器交互的基本单元可以是单个元素,而不像松散耦合多处理器系统的交互单元通常是一条消息或者一个文件,这就使得对称多处理器的进程之间可以进行高度的协作
其实多处理器结构有很多相对于单处理器结构的优势,在书上有详细的写,这里不多做讲解
在现代计算机中,处理器通常至少有专用的一级高速缓存,高速缓存的使用带来了新的设计问题,由于每个本地高速缓存包含一部分内存的映像,如果修改了当前高速缓存中的一个字,就会使得该子在其他高速缓存中变得无效,但是这种问题不是用操作系统解决的,而是用硬件解决的
多核计算机
我们首先要了解多核和多处理器的区别:多核CPU在同一芯片上集成多个处理核心,提高性能并允许同时处理多个任务。相比之下,多处理器是通过主板上的多个物理CPU实现并行处理,成本较低但效率不如多核
其中一个例子就是英特尔酷睿i7-990X
第二章 操作系统概述
2.1操作系统的目标和功能
操作系统是应用程序和计算机硬件之间的接口,并且操作系统也是一个程序,方便我们更好的使用硬件而不需要了解硬件的具体原理
它有三个目标:
方便: 操作系统使计算机更易于使用
有效: 操作系统允许以更加有效的方式使用计算机系统资源
扩展能力: 在构造操作系统时,应允许在不妨碍服务的前提下,有效地开发,测试和引入新的系统功能
作为用户/计算机接口的操作系统
在现在我们学初学编程很容易,只需要学一些相关的语法就能编写程序,比如说C语言,了解语法就可以编写程序,那我们是否思考过程序是怎样调用硬件的呢
当然是用我们C语言自带的函数,这些函数就变成了我们与硬件沟通的接口,这些接口的内部也是一些系统程序,这些系统程序我们可以将其当成操作系统,我们传入参数给函数,函数通过操作系统调用硬件,最后返回结果
简单来说,操作系统通常提供以下几个方面的服务:
程序开发: 简单来说就是提供工具帮助我们开发程序
程序运行: 当我们想要运行程序的时候,操作系统将我们的指令和数据加载到内存并且初始化一些资源
I/O设备访问: 每个设备都有不同,所以对应的操作也会不同,操作系统提供了一个统一的接口以便让我们更好的操作设备
文件访问控制: 避免我们在写程序的时候不小心修改了关键文件的信息,操作系统提供了文件的保护机制
系统访问: 就是访问我们的硬件,保护我们的硬件,避免程序直接访问硬件造成某些影响或者破坏,并且有时还要解决资源竞争时的冲突问题
错误检测和响应: 当计算机系统运行的时候可能会发生各种各样的错误,这个时候操作系统必须提供响应使错误对我们程序的影响最小。响应可以是终止引起错误的程序,重试或简单地给应用程序报告错误
记账: 统计我们对资源的利用率,并且统计相关数据,这对我们将来增加功能或者调整系统又帮助
上图也指明了典型计算机系统中的三种重要接口:
指令系统体系结构,程序二进制接口,程序编程接口
作为资源管理器的操作系统
操作系统在某种角度看是在控制数据的移动和处理,但是这种控制机制和我们日常生活中说的控制机制有所不同,日常生活中控制器独立于控制对象,比如说热水控制器和烧水装置,但是操作系统不一样,因为操作系统归根结底也是一组程序,也是存储在硬件之内的,所以它不是独立于硬件的
这个不同主要有两方面:
操作系统是由处理器执行的一段程序或一组程序
操作系统经常会释放控制,并且必须依赖处理器才能恢复控制
如图显示了操作系统的一部分在内存中,包括内核程序和当前正在使用的其他操作系统程序,内核程序包含操作系统最常用的功能
操作系统的易扩展性
重要的操作系统不断发展,原因如下:
硬件升级和新型硬件的出现,新的服务,纠正错误
2.2操作系统的发展史
串行处理
早期的计算机需要程序员直接与计算机硬件打交道,因为当时没有操作系统,但是这种系统会导致两个主要问题:
1.调度: 当时如果想要使用装置,需要预订,并且通常是以半个小时为单位,但是有时候就会出现预订了一个小时但是实际上程序只用了45分钟就完成的工作,剩下的15分钟计算机是空闲的,还有的时候在程序的运行过程中遇到了问题就会导致程序中止,你原来分配的时间就会不够用,所以在还未完成目标前就不得不停止使用
2.准备时间: 当我们写了一个称为作业的程序想要运行的时候,就需要向内存中保存我们的程序,并且还要完成一系列准备工作,这一系列工作的每个步骤都可能需要安装或拆卸磁带,或准备卡片组,如果这个过程发生错误,就只能重新开始,所以我们会花费大量的时间在程序的准备工作当中
简单批处理系统
这个方案的中心思想是使用一个称为监控程序的软件,通过使用这类操作系统,用户不再直接访问机器,而是由操作员将我们一个个称为作业的程序放在输入设备上,供监控程序使用,程序处理完后,监控程序自动加载下一个程序
我们可以从下面两个视角来理解这个方案是如何工作的
监控程序视角:
像图中的监控程序称为常驻监控程序,也就是总是存储在内存中的,其它的监控程序则是在我们执行程序的时候才载入,在执行用户程序的时候,监控程序将控制权交给我们的用户程序,在用户程序结束的时候将控制权返还给监控程序
处理器角度:
从某个角度看,处理器执行存储的监控程序,当遇到用户程序的时候就会遇到监控程序的分支,然后执行用户程序,所以所谓的控制权的转移只不过是看当前执行的是什么程序,如果是监控程序控制权就是交给监控程序,如果是用户程序控制权就交给用户程序
但是这种模式也有一定的缺点,一部分内存交付给监控程序;监控程序消耗了一部分机器时间,这些都构成的系统开销
多道批处理系统
上面那种模式依然会使处理器大部分时间处于空闲状态,因为I/O设备的运行速度相对于处理器速度太慢了,就会导致机器大部分时间都在等待I/O设备运行,在上面那种模式我们可以理解为机器运行完一条指令后才执行另一条指令,那么我们是否可以在I/O设备准备数据的时候处理其它指令呢,当然可以,这就引出了多道程序设计或多任务处理
但是多道处理系统依赖某些计算机硬件,尤其是支持I/O中断和直接存储器访问(DMA)的硬件
并且多道处理系统比单道处理系统的设计更加复杂,并且需要内存管理来管理我们需要运行的程序,并且有时候还需要调度算法
分时系统
为何需要分时系统,因为在当时计算机资源很珍贵,并且有时候会有多个用户申请使用计算机,那么就需要将机器的使用时间拆分,供用户使用,但是由于人的反应时间相对较慢,所以需要设计良好的系统响应时间,可以让一个系统同时供多人使用并且这些用户感觉不到互相干扰,这提升了我们的使用效率
第一个分时操作系统(CTSS)的运行机制如下
每隔0.2s产生一个中断,在每个时钟中断处,操作系统恢复控制权,并将处理器分配给另一个用户,这种当前用户被抢占,新用户载入的技术叫做时间片技术,并且当新的程序载入的时候,会保留老用户的程序状态便于以后恢复
2.3主要成就
操作系统开发中的4个重要理论进展
进程,信息保护和安全,内存管理,调度和资源管理
进程
这是操作系统设计的核心
计算机系统的发展有三条主线:多道程序批处理操作,分时和实时事务系统
想要设计出能够协调各种不同活动的系统软件很困难,因为在系统中任何时刻都有很多作业正在运行,每个作业又有不同的步骤这就导致有一些不常见的错误只有在特定的情况下才发生,而我们又很难把每种可能都枚举出来,并且当产生错误的时候,我们也不清楚是硬件还是软件的问题
一般而言,产生这类错误的主要原因有四个:
不正确的同步: 假设一个程序启动了一个I/O读操作,在继续进行前需要其它的信号,但是设计不正确的信号机制可能会导致信号丢失或接受到重复信号
失败的互斥: 当多个用户试图同时使用一个共享资源的时候如果控制不当可能会发生错误
不确定的程序操作: 当某个程序的结果只依赖于该程序的输入时,当别的程序与该程序交替执行的时候当存放输入的内存区被当前程序修改的时候可能会造成不可预测的结果
死锁: 假设有两个程序需要两个I/O设备执行,其中一个程序获得了一个设备的控制权,另一个程序获得了另一个设备的控制权,这就会导致机器卡在这里,无法继续往下走,如果没有外力的改变就一直卡着
要解决这些问题就需要一种系统级的方法来监控处理器中不同程序的执行,进程的概念为此提供了基础,进程由三个部分组成
一段可执行的程序
程序所需要的相关的数据
程序的执行上下文
执行上下文是根本,也称进程状态,是操作系统用来管理进程需要的内部数据,上下文包括的正确执行进程的所有信息,也还包括的操作系统使用的信息
进程索引寄存器表示当前正在控制处理器的的进程在进程表中的索引,程序计数器指向该进程中下一条指令,基址寄存器表示当前进程的起始地址,界限寄存器保存该区域的大小,这几个寄存器能够保护内部进程不会相互干扰
当A进程中断的时候,所有寄存器的内容被记录在其执行的上下文环境中,当恢复A进程的时候,恢复上下文并且继续执行A程序
因此,进程被当作数据结构来实现,任何时候整个进程的状态都包含在其上下文中
最后要介绍的是线程,本质上,进程分解为多个并发的线程,这些并发的线程相互协作执行,完成进程的工作
内存管理
操作系统担负着5项存储器管理职责:
进程隔离: 操作系统必须保护独立的进程,防止互相干扰
自动分配和管理: 也就是分配我们给作业的存储空间,我们不需要手动分配
支持模块化程序设计: 程序员能够定义程序模块,并且动态的创建,销毁和修改蘑模块的大小
保护和访问控制: 当一个程序访问另一个程序的存储空间的时候,操作系统需要判断是否能进行操作,也就是保护程序的完整性
长期存储: 许多应用程序需要在计算机关机后长时间地保存信息
典型情况下,操作系统使用虚存和文件系统机制来满足要求
虚存机制允许程序以逻辑方式访问存储器,而不需要考虑物理内存上可用的空间数量
我们通过分页机制来使得内存被充分的利用,在分页系统中,进程由许多固定大小的块组成,这些块称为页,程序通过虚地址访问字,虚地址由页号和页中的偏移量组成,进程的每页可以置于内存中的任何地方,分页系统提供了程序中使用的虚地址和内存中的实地址或物理地址之间的动态映射
当一个进程的执行的时候,一部分页会载入内存中,若需要访问的某页不在内存中,存储管理硬件会在检测它后,安排载入缺页,这一配置称为虚存
如果我们程序使用的是虚地址访问,那么地址转换硬件会将我们提供的虚地址映射成真实的内存地址,如果访问的虚地址不在内存中,那么就会从外存中换入需要的数据块
信息保护和安全
与操作系统相关的大多数安全和保护问题可以分为4类:
可用性: 保护系统不中断
保密性: 保护用户不能读取未授权访问的数据
数据完整性: 保护数据不被未授权修改
认证: 涉及用户身份的正确认证和消息或数据的合法性
调度和资源管理
资源分配和调度都必须考虑3个因素:
公平性: 也就是给进程提供几乎平等的访问机会
有差别的响应性: 对于某些进程操作系统会优先响应
有效性: 提高效率
短程队列存储的是在内存中并等待处理器可用随时准备运行的进程组成,任何一个这样的进程都可在下一步使用处理器,但是选择哪个进程取决于短期调度器或分派器
长程队列是等待使用处理器的新作业列表,操作系统通过把长程队列中的作业转移到短程队列中,向系统中添加作业的任务
出现一个中断时或服务调用时,就会请求短期调度器选择一个进程执行
2.4现代操作系统的特征
现代操作系统加入了很多不同的方法和设计要素,大致分为以下几类
微内核体系结构,分布式操作系统,多线程,面向对象设计,对称多处理
直到最近,多数操作系统都只有一个单体内核,操作系统提供多数功能都由这个大内核来提高,这个大内核是作为一个进程实现的。微内核体系结构只给内核分配一些最基本的功能,包括地址空间,进程间通信和基本的调度,其它的服务则运行在用户模式且与其他应用程序类似的进程提供,这些进程科根据特定的应用和环境需求进行定制,有时也称这些进程为服务器
多线程技术是指把执行一个应用程序的进程划分为可以同时运行的多个线程,线程和进程的区别如下:
线程顺序执行可以中断,处理器可以转到另一个线程,进程则是由一个或多个线程和相关系统资源的集合
对称多处理比单处理体系结构具有更多的优势:
性能:运行效率更高
可用性:在单处理系统下如果处理器出现问题就无法运行了,而多处理系统下单个处理器损坏系统可以继续运行,只是性能有所降低
增量增长:用户可以添加额外的处理器来增强系统的功能
可扩展性:生产商可根据系统配置的处理器数量,提供各种不同价格的产品
多线程和对称多处理这两个方式时互补的,一起使用会更加有效
面向对象设计用于给小内核增加模块化的扩展
2.5容错性
容错性是指系统或部件在发生软/硬件错误时,能够继续正常运行的能力
可用性定义为系统能够有效服务用户请求的时间段
错误
错误分为以下几类:
永久性错误,临时性错误,一瞬性错误,间歇性错误
一般来说,系统的容错性时通过增加冗余度来实现的,冗余的实现有以下几种方法
空间(物理)冗余,时间冗余,信息冗余
操作系统机制
操作系统软件中采用了许多技术来提高容错性,比如以下的方法
进程隔离,并发控制,虚拟机,检测点和回滚机制
2.6多处理器和多核操作系统设计考虑因素
多处理器操作系统的关键设计问题如下
并发进程或线程,调度,同步,内存管理,可靠性和容错性
多核操作系统设计考虑因素
考虑因素包含上面系统的所有设计问题,但是我们需要关注其潜在的并行规模问题
可用从三个层次开发其潜在的并行能力:首先是每个核内部的硬件并且,其次是处理器层次上的潜在并行能力,最后是应用程序的并行能力
这里介绍一下两种常用的策略:
应用层并行
大体上将应用划分为多个可用并行执行的子任务,主要是通过GCD实现
虚拟机方式
给一个进程分配多个核而不是一个核分配多个进场,这样能够避免很多由任务切换及调度引起的开销,这种方式的机理如下:早期的计算机,一个程序运行在一个单独的处理器上,而多道程序设计的出现,使得每个应用程序都好像运行在一个专用的处理器上,为了管理进程,操作系统需要一块收保护的空间,以避免用户核程序的干扰,这就出现的内核核用户模式的区别,但是随着虚拟处理器的出现,使得处理器之间进行切换产生的开销页开始增加,所以我们可用考虑抛弃内核模式核用户模式的区别,让操作系统成为管理程序,让应用程序自己负责资源管理,操作系统为应用程序分配处理器和内存资源
2.7微软Windows系统简介
操作系统组织架构
Windows的体系结构是高度模块化的,从理论上讲,任何模块都可用移动,升级或替换,而不需要重写整个系统或其标准应用程序编程接口
Windows的内核模式组件包括以下类型:
执行体: 包括操作系统核心服务
内核:控制处理器的执行
硬件抽象层:使各种硬件对内核来说看上去是相同的
设备驱动:用来扩展执行体的动态库
窗口和图形系统:实现GUI函数
以下是对每个执行体模块的简单描述:
I/O管理器:提供应用程序访问I/O设备的一个框架
高速缓存管理器:使最近访问过的磁盘数据驻留在内存中,以实现快速访问
对象管理器:创建,管理和删除执行体对象和用于表示诸如进程,线程和同步对象资源的抽象数据类型
即插即用管理器:决定并加载特定设备的驱动
电源管理器:调整各种设备间的电源管理
安全访问监控程序:强制执行访问确认和审核产生规则
虚存管理器:管理虚存地址,物理地址和磁盘上的页面文件
进程/线程管理器:创建,管理和删除对象,跟踪进程和线程对象
配置管理器:负责执行和管理系统注册表
本地过程调用机制:针对本地进程,在服务和子系统间进行通信的一套跨进程的高效过程调用机制
用户模式几次呢 Windows支持4种基本的用户模式进程:
特殊系统进程:管理系统所需的用户模式服务
服务进程:打印机后台管理程序,事件器记录,与设备驱动协作的用户模式构件
环境子系统:提供不同的操作系统个性化设置
用户应用程序:为充分利用而为用户提供可执行程序和动态链接库
客户-服务器模型
这种模式是分布式计算中的一种常用模型
这种模型的优点如下:
简化了执行体,可用很容易地加入新的API
提高了可靠性,单个客户的失败不会是操作系统其余部分崩溃
为应用程序与服务间通过RPC调用进行通信提供了一致的方法,且灵活性不受限制
为分布式计算提供了适当的基础
Windows对象
尽管Windows的内核是用C语言写的,但是其采用的设计原理与面向对象设计密切相关,Windows使用的面向对象的重要概念如下:
封装,对象类和实例,继承,多态性
Windows中的对象可用有名称也可用无名称
Windows同步使用处理器时所用的两类对象如下:
分派器对象: 执行体对象的子集,线程可用在该类对象上等待,以控制基于线程的系统操作的分发与同步
控制对象: 内核组件用来管理不受普通线程调度控制的处理器操
2.8传统的UNIX系统
在传统UNIX内核中,用户程序既可以直接调用操作系统服务,也可通过库程序调用操作系统服务
系统被划分为两个主要部分:一个关心进程控制,另一个关系文件管理和I/O
但是这种传统UNIX系统被设计在单一处理器上运行,缺乏保护数据结构免受多个处理器同时访问的能力,并且它的内核不通用,不可扩展,不能重用代码,并且不是模块化的,所以想要增加新的功能就会很麻烦并且会增加很多新的代码
2.9现代UNIX系统
典型的现代UNIX内核具有如图的结构,它有一个以模块化方式编写的小核心软件,该软件提供许多操作系统进程需要的功能和服务;每个外部圆圈表示相应的功能及以多种方式实现的接口
Linux操作系统
大多数UNIX内核都是单体的,单体内核指把所有的功能放在一堆,相对于我们的只有一个main函数,这就导致如果想增加或者删除功能,要修改很多东西,并且所有模块和例程都需要重新链接,重新安装
Linux中这个问题尤其尖锐,因为Linux的开发时全球性的,是由独立程序员组成的松散组织完成的
尽管Linux未采用微内核的方法,但由于其特殊的模块结构,因而也具有很多微内核方法的优点,Linux是由很多模块组成的,这些模块可由命令自动加载和卸载,这些相对独立的块称为可加载模块
Linux可加载模块有两个重要特征:
动态链接: 当内核已在内存中并正在运行时,内核模块可被加载和链接到内核
可堆叠模块: 模块可按层次结构排列
动态链接简化了配置任务,节省了内核所占的内存空间
通过可堆叠定义模块间的依赖关系的优点如下:
1.多个类似模块多大相同代码,可移入单个模块当中,因此降低了重复性
2.内核可确保所需模块存在,避免卸载其他运行模块所依赖的模块,并在加载新模块时一同加载所需要的其他模块
内核组件
这只是一个简洁的的体系结构图
主要内核组件的简要介绍如下:
信号:内核使用信号来向进程提供信息
系统调用:进程通过系统调用来请求系统服务
进程和调度器:创建,管理,调度进程
虚存:为进程分配和管理虚存
文件系统:为文件,目录和其他文件相关对象提供一个全局的分层命名空间,并提供文件系统函数
网络协议:为用户的TCP/IP协议套件提供套接字接口
字符设备驱动:管理向内核一次发送/接受1字节数据的设备,如终端,调制解调器和打印机
块设备驱动:管理以块为单位向内核发送/接受数据的设备,如各种形式的外存
网络设备驱动:管理网卡和通信端口
陷进和错误:处理CPU产生的陷进和错误
物理内存:管理实际内存中的内存页池,并为虚存分配内存页
中断:处理来自外设的中断
Android
Android操作系统时为触屏移动设备设计的基于Linux的操作系统,也是最流行的手机操作系统
Android软件体系结构
Android时一个包括操作系统内核,中间件和关键应用的软件栈,我们应将Android视为一个完整的软件栈,而非单个操作系统
应用
与用户直接进行交互的所有应用都是应用层的一部分,包括一套通用的应用程序
应用框架
应用框架提供高级构建块,为程序员开发程序提供标准化的访问接口,旨在简化组件的复用,一些关键的应用框架组件如下:
活动管理器:管理应用的生命周期
窗口管理器:底层界面管理器的抽象
包管理器:安装和删除应用
电话管理器:允许与电话,短信和彩信服务交互
内容接口:这些函数将封装应用间需要共享的应用数据,如联系人数据
资源管理器:管理应用程序的资源,如本地化字符串和位图
视图系统:提供用户界面UI元素
位置管理器:允许开发人员使用基于位置的服务
通知管理器:触发规定事件时提醒用户
XMPP:管理应用间的通信
系统库
应用框架下面的一层由两部分组成:系统库和运行库,系统库组件时用C或C++编写的实用系统函数集,可被Android系统的各个组件使用
部分关键系统库如下:
界面管理器,OpenGL,媒体框架,SQL数据库,浏览器引擎,仿生LibC
Android Runtime
每个Android应用都通过字节的Dalvik虚拟机实例,以自己的进程运行
这个组件包含一组核心库
Linux内核
Android系统的内核与linu的内核非常相似,但不完全相同,Android系统依赖于Linux内核来提供核心的系统服务
Android系统体系架构
从应用开发者的角度来看Android系统体系架构包含了如下几层:
应用和框架,Binder IPC,Android 系统服务,硬件抽象层,Linux内核
活动
活动时单个可视用户界面组件,包括菜单选项,图标和复选框等
电源管理
Android在Linux内核中增加了两个提升电源管理能力的新功能:报警和唤醒锁
第三章 进程描述和控制
3.1什么是进程
进程的两个基本元素是程序代码和代码相关联的数据集合,进程执行的任意时刻,都可由如下元素来表征
标识符:用来区分其他进程
状态:表示程序的运行状态
优先级:相对于其他程序的优先顺序
程序计数器:程序中即将执行的下一条指令的地址
内存指针:包括程序代码和进程相关数据的指针,以及与其他进程共享内存块的指针
上下文数据:进程执行时处理器的寄存器中的数据
I/O状态信息:包括显式I/O请求,分配给进程的I/O设备和被进程使用的文件列表等
记账信息:包括处理器时间总和,使用的时钟数总和,时间限制,记账户等
上述列表信息存放在一个称为进程控制块的数据结构中,控制块由操作系统创建和管理,因为进程控制块包含了充分的信息,因此可以中断一个进程的执行,并在后来恢复进程的执行
因此我们可以说进程由程序代码和相关数据及进程控制块组成
3.2进程状态
列出为进程执行的指令序列,可描述单个进程的行为,这样的序列称为进程轨迹,给出各个进程轨迹的交替方式,就可以描述处理器的行为
两状态进程模型
进程有两个状态,即运行态和未运行态,所以我们可以用一个队列来安排进程的执行,图中的队列中的每项都指向某个特定进程的指针
被中断的进程转移到等待进程队列中,或在进程结束或取消时销毁它,在任何情形下,分派器均从队列中选择一个进程来执行
进程的创建和终止
将一个新进程添加到正被管理的进程集时,操作系统需要建立用于管理该进程的数据结构,并且在内存中给它分配地址空间,这些行为构成了一个新进程的创建过程
在现在的操作系统中,允许一个进程引发另一个进程的创建很有用,比如说服务器进程可以为它处理的每个请求产生一个新进程,当操作系统为另一个进程的显式请求创建一个进程时,这个动作就称为进程派生
当一个进程派生另一个进程时,前一个称为父进程,被派生的进程称为子进程
图中列出了一些最常见的进程中止原因,比如说在我们的个人计算机中,用户可以自己选择结束一个应用程序,这些行为最终导致给操作系统发出一个服务请求,以终止发出请求的进程
五状态模型
对于可运行的进程,处理器以一种轮转的方式操作,也就是给每个进程一定的执行时间,然后进程返回队列,阻塞情况除外,但是,这种实现不合适我们的实际运行,因为存在一处于非运行但已就绪等待执行的进程,同时还存在另外一些处于阻塞态等待I/O操作结束的进程。
所以解决该问题一种较好方法时,将非运行态分为两个状态:就绪态和阻塞态
运行态:进程正在执行
就绪态:进程做好了准备,只要有机会就开始执行
阻塞/等待态:进程在某些事情发生前不能执行
新建态:刚刚创建的进程,操作系统还未把它加入可执行进程组
退出态:操作系统从可执行进程组中释放出的进程
新建态和退出态对进程管理非常有用,操作系统可以分为两步定义新进程。首先,操作系统做一些必需的辅助工作,并分配进程所需要的全部表格,此时,进程处于新建态,这意味着操作系统已经执行了创建进程的必需动作,但还未创立进程,进程处于新建态时,程序保留在外存中,通常保留在磁盘中
进程退出分为两步,首先先是终止进程,与其相关的表和其他信息会临时被操作系统保留,因此给辅助程序提供了提取所需信息的时间,在提取了所需的信息后,操作系统会从系统中删除该进程
根据我们前面的分析,我们可以改进之前的排队图,加入阻塞队列,当一个事件发生的时候,所有位于阻塞队列中等待该事件的进程都被放到就绪队列中,这种情况意味着这时操作系统必须扫描整个阻塞队列,搜素那些等待该事件的进程
但是在大型操作系统中,队列中可能有几百甚至几千个进程,此时拥有多个队列将会很有效,一个事件可以对应一个队列,并且按照优先级分派进程,那么改进后的排队器如图片的右边所示
被挂起的进程
交换的需要 前面介绍的三个基本状态提供了一种为进程行为建立模型并知道操作系统实现的系统方法,并且许多实际的操作系统都是按照着三种状态具体构建的
但是呢,我们可以证明向模型中增加其他状态时也是合理的,为了说明好处,我们需要考虑一个未使用虚存的系统,这表示每个执行的进程必须完全载入内存
根据之前的分析我们知道,我们机器运行的时候大部分时间都花费在I/O活动上,因为I/O活动远慢于我们的处理器,所以我们的处理器会处于空闲状态,这种问题我们有几种解决方案,一种是扩充内存,但是我们知道扩充内存也意味着价格会增加,另一种方法是交换,当内存中不存在就绪态的进程时,操作系统就把阻塞的进程换出到磁盘中的挂起队列,即从内存中“踢出”的进程队列,操作系统此后要么从挂起队列中取出另一个进程,要么接受一个新进程的请求,将其放入内存运行
但是交换是I/O操作,这可能导致问题更加恶化,但又由于磁盘I/O一般是系统中最快的I/O,因此交换通常会提高性
要使用交换,我们必须在行为模型中增加另一个状态:挂起态,当内存中的所有进程都处于阻塞态时,操作系统可把其中的一个进程置为挂起态,并将其转移到磁盘,此时内存所释放的空间就可被调入的另一个进程使用
书上讲了很多状态转换的例子,可以去看看,这里不多讲述
挂起的原因有很多,比如说为了提供更多的内存空间以便调入以就绪/挂起态进程,或增加分配给其他就绪态进程的内存,还比如说操作系统为了监视系统活动,记录其各种资源的使用情况,也会挂起程序,再比如说操作系统发现问题或者怀疑其存在问题,就会挂起进程,死锁就是其中的一个例子,下面的表格给出了进程挂起的原因
3.3进程描述
我们知道操作系统控制计算机系统内部的事件,为处理器执行进程进行分配和调度,那么要控制进程并且管理资源,操作系统需要哪些信息
操作系统的控制结构
普遍采用的方法是,操作系统构造并维护其管理的每个实体的信息表
内存表用于跟踪内存(实存)和外存(虚存),内存的某些部分为操作系统保留,剩余部分供进程使用,内存表必须包含如下信息:
分配给进程的内存
分配给进程的外存
内存块或虚存块的任何保护属性,如哪些进程可以访问某些共享内存区域
管理虚存所需要的信息
尽管图中我们给出了4种不同的表,但这些表必须以某种方式链接或交叉引用,因为内存,I/O,文件时代表进程而被管理的,因此进程表中必须有对这些资源的直接或间接引用。
操作系统如何知道创建表的,这就是通过人的帮助或者一些自动配置软件产生,也就是我们开机的时候自动配置软件给操作系统提供的相关的信息
进程控制结构
操作系统在管理和控制进程时,首先要知道进程的位置,其次要知道进程的属性(如进程ID,进程状态)
进程位置 一个进程至少应有足够的内存空间来保存其程序和数据,此外,程序的执行通常涉及用于跟踪过程调用和过程间参数传递的栈,最后还有与每个进程相关的许多属性,以便操作系统控制该进程,通常,属性集称为进程控制块,程序,数据,栈,和属性的集合称为进程映像
进程映像的位置取决于所用的内存管理方案,在操作系统管理进程的时候,其进程映像至少应有一部分位于内存,而要执行进程,需要将整个进程载入内存中,在CTSS中,当进程被换出时,部分进程映像可能仍保留在内存中,用于跟踪进程
在分页机制中,操作系统维护的进程表必须给出每个进程映像中的每页的位置,因为如果进程映像包含多个块,则这些块可能在不同页中
进程属性 复杂的多道程序系统需要关于每个进程的大量信息,该信息可以保留在进程控制块中
进程控制块信息分为三类:
进程标识信息,进程状态信息,进程控制信息
实际上,对于操作系统中的进程标识符来说,每个进程都分配了一个唯一的数字标识符,否则就必须有一个映射,以便操作系统可以根据进程标识符定位相应的表,除进程标识符外,还给进程分配了一个用户标识符,用于说明拥有该进程的用户
处理状态信息 是处理寄存器的内容组成。运行一个进程时,进程的信息一定会出现在寄存器中。中断进程时,必须保存该寄存器的所有信息,以便进程恢复执行时可以恢复所有这些信息。
注意,所有处理器设计都包括一个或一组称为程序状态字的寄存器,它包含所有状态信息,比如Intel x86处理器中的状态字存储在EFLAGS寄存器中
进程控制信息 是操作系统控制和协调各种活动进程所需的额外信息
这是进程映像在虚存中的结构,图中每个进程映像的地址范围看起来是连续的,但实际情况可能并非如此,具体取决于内存管理方案和操作系统组织控制结构的方式
进程控制块的作用
进程控制块是操作系统中最重要的数据结构,每个进程控制块都包含操作系统所需进程的所有信息,操作系统的每个模块都可以读取和修改它们
但是这带来了一个重要的设计问题,主要是保护相关的问题,具体表现为两个问题:
一个例程中的错误可能会破坏进程控制块,进而破坏系统对受影响进程的管理能力
进程控制块结构或语义中的设计变化可能会影响到操作系统中的许多模块
这些问题可要求操作系统中的所有例程都通过一个处理程序例程来解决,即处理程序例程的人物权是包含进程控制块,且是读写这些块的唯一仲裁程序。
3.4进程控制
执行模式
在继续讨论操作系统管理进程的方式之前,需要区分通常与操作系统相关联的处理器执行模式和用户程序相关联的处理器执行模式,分为特权模式和非特权模式
非特权模式通常称为用户模式,因为用户程序通常在该模式下运行;特权模式称为系统模式,控制模式或内核模式
使用这两种模式主要是保护操作系统和重要的操作系统表不受用户程序的干扰
这就引出了两个问题:处理器如何知道它正在什么模式下执行?模式如何变化?
对第一个问题,程序状态字中通常存在一个指示执行模式的位,该位会因事件的改变而变化,比如实现64位IA-64体系结构的Intel Itanium处理器中,就有一个包含2位CPL字段处理器状态寄存器(PSR),级别0是最高特权级别,级别3是最低特权级别
进程创建
当操作系统基于某种原因决定创建一个新进程时,会按如下步骤操作:
1.为新进程分配一个唯一的进程标识符
2.为进程分配空间
3.初始化进程控制块
4.设置正确的链接
5.创建或扩充其他数据结构
进程切换
何时切换进程 进程切换可在操作系统当前正运行进程中获得控制权的任何时刻发生
首先考虑系统中断,大多数操作系统都会区分两种系统中断:一种为中断,另一种称为陷阱,前者与外部事件有关,后者则是当前运行的进程产生错误或异常导致的
对于普通中断,控制权首先转给中断处理器,中断处理器完成一些基本辅助工作后,再将控制权交给已发生的特定中断相关的操作系统例程
对于陷阱,操作系统则确定错误或异常状态是否致命,致命时,对当前正运行进程置为退出态,并切换进程,不致命时,操作系统可以会尝试恢复程序,也可能会切换进程,或继续当前运行的进程
最后,操作系统可被来自正执行程序的系统调用激活,例如用户进程执行了一个请求I/O操作的指令,这时该调用会转移到作为操作系统代码一部分的一个例程
模式切换
当中断信号出现的时候,处理器会将程序计数器置为中断处理程序的开始地址,并从用户模式切换到内核模式,以便中断处理代码包含特权指令
然后将已中断进程的上下文保存到已中断程序的进程控制块中,然后如果继续下一步
假设中断与I/O事件有关,则中断处理程序检查错误条件,若发生了错误,则中断处理程序给最初请求I/O操作的进程发一个信号,若是时钟中断,则处理程序把控制权交给分派器,由分派器将控制权传递给另一个进程,因为给当前运行进程分配的时间片已用尽
进程状态的变化
显然,模式切换与进程切换是不同的,模式切换可以在不改变运行态进程的情况下出现,此时保存和恢复上下文的开销很少,但是如果进程切换状态,则操作系统必须使环境产生实质性的变化
3.5操作系统的执行
我们根据学习知道操作系统也是一段代码,也是程序,那操作系统是一个进程吗?如果是,如何控制它?这些问题使得人们提出了大量的设计方法
无进程内核
在许多老操作系统中,传统且通用的一种方法是在所有进程外部执行操作系统内核,如图所示,在这么模式下,进程这一概念仅适用于用户程序,而操作系统代码则是在特权模式下单独运行的实体
在用户进程内运行
较小的计算机(比如我们的PC,工作站)的操作系统通常采用另一种方法,即在用户进程的上下文种执行所有操作系统软件
这种图片展示的是操作系统是用户调用的一组例程,任何时刻操作系统都管理着n个进程映像,这些映像不仅包括图中列出的区域,还包括内核程序的程序,数据和栈区域
图中展示的是上面那种策略下的典型进程映像结构,当进程在内核模式下运行时,单独的内核栈用于管理调用/返回,操作系统代码和数据位于共享地址空间中,并被所有用户进程共享
在这种情况下,如果发生中断,陷进或系统调用时,不需要切换进程,只是在同一进程中切换模式
操作系统完成操作后,需要继续运行当前程序,也是只需要切换模式即可,不会导致进程切换的惩罚,这是这种方式的优点
基于进程的操作系统
这种方式是把操作系统作为一组系统进程来实现,主要的内核功能被组织为独立的进程,同样,此时存在一些任何进程之外执行的进行切换代码
这种方法有几个优点。首先,它利用了鼓励使用模块化操作系统的程序设计原理,可使模块间的接口最小且最简单。其次,有些非关键操作系统功能可简单地用独立的进程来实现,例如前面提及的监视各种资源利用率和系统中用户进程进展状态的程序。第三,把操作系统作为一组进程来实现时,在多处理或多机环境中很有用,因此此时为提高性能,有些操作系统服务可传送到专用的处理上执行
3.6UNIX SVR4进程管理
UNIX采用了我们前面那种在用户进程内运行的那种方式,该模型中操作系统大部分都在用户进程环境执行,UNIX使用了两类进程,即系统进程和用户进程,系统进程在内核模式下运行,执行操作系统代码来实现管理功能和内部处理。用户进程则在用户模式下运行并执行用户程序和实用程序。
进程状态
两个UNIX休眠态对应于我们之前讲的两个阻塞态,不同之处如下:UNIX采用两个运行态表示进程是在用户模式下执行还是在内核模式下执行
UNIX区分两种状态,即内存中的就绪态和被抢占态,被抢占是指当内核完成任务并准备将控制权返回给用户程序时,如果出现了一个已就绪并具有较高优先级的进程那么就会抢占当前进程,支持优先级高的进程
进程描述
UNIX中的进程是一组相当复杂的数据结构,这些数据结构为操作系统提供管理进程和分派进程所需的全部信息
进程映像中的元素分为三部分:用户级上下文,寄存器上下文和系统级上下文,如下图所示
用户级上下文包含用户程序的基本元素,它可直接由已编辑的目标文件生成,进程未运行时,处理器状态信息保存在寄存器上下文区域中
系统级上下文 包含操作系统管理进程所需的其余信息,它由静态部分和动态部分组成,静态部分的一个元素时进程表项,它实际上是由操作系统维护的进程表的一部分,每个进程一个表项,进程表项包含内核总可访问的进程控制信息
进程表项和用户区的区别反映了UNIX内核总在某些进程上下文中执行的事实,多数时候,内核都处理与该进程相关的部分
系统级上下文的第三个静态部分是由内存管理系统使用的本进程区表,最后内核栈式系统级上下文的动态部分
进程控制
UNIX中的进程创建时由内核系统调用fork()实现的,一个进程发出一个fork请求时,操作系统执行如下功能:
1.在进程分配一个唯一进程标识符
2.为子进程分配一个唯一进程标识符
3.复制父进程的进程映像,但共享内存除外
4.增加父进程所拥有文件的计数器,反映另一个进程现在也拥有这些文件的事实
5.将子进程置为就绪态
6.将子进程的ID号返回给父进程,将0值返回给子进程
3.7小结
现代操作系统中最基本的构件是进程,操作系统的基本功能是创建,管理和终止进程
要执行进程管理功能,操作系统必须维护每个进程的描述,包括执行进程空间和一个进程控制块
在整个生命周期内,进程总是在一些状态之间转换,最重要的状态有就绪态,运行态和阻塞态
正运行进程可被进程外发生并被处理器识别的中断事件打断,或被执行操作系统的系统调用打断
第四章 线程
4.1进程和线程
在迄今为止的讨论中,进程具有如下两个特点:
资源所有权:进程包括存放进程映像的虚拟地址空间
调度/执行:进程执行时采用一个或多程序的执行路径,不同进程的执行过程会交替进行
这两个特点是独立的,因此操作系统应能分别处理它们,为区分这两个特点,我们通常将分配的单位称为线程或轻量级进程,而拥有资源所有权的单位称为进程或任务
多线程
多线程是指操作系统在单个进程内支持多个并发路径的能力,每个进程中仅执行单个线程的传统方法称为单线程方法
在多线程环境中,进程定义为资源分配单位和一个保护单元,与进程相关联的有:
容纳进程映像的虚拟地址空间
对处理器,其他进程,文件和I/O资源的受保护访问
一个进程可能有一个或多个线程,每个线程有:
一个线程执行状态
未运行时保存的线程上下文;线程可视为在进程内运行的一个独立程序计数器
一个执行栈
每个线程用于局部变量的一些静态存储空间
与进程内其他线程共享的内存和资源的访问
在多线程环境中,进程仍然只有一个与之相关联的进程控制块和用户地址空间,但每个现在会有许多单独的栈和一个单独的控制块
因此,进程中的所有线程共享进程的状态资源,所有现场都驻留在同一块地址空间中,并可访问相同的数据,当某个线程改变了内存中的一个数据项时,其他线程在访问这一数据项时会看到这一变化
这里讲一下在单用户处理系统中使用线程的4个例子:
前台和后台工作: 这种方案允许程序在前一条命令完成前提示输入下一条命令,因此通常会使用户感到应用程序的响应速度有所提高
异步处理: 程序中的异步元素可用线程来实现
执行速度: 在多处理系统中,同一进程中的多个线程可同时执行,这一,即使一个线程在读取数据时被I/O操作阻塞,另一个线程仍然可以继续运行
模块化程序结构: 涉及多种活动或多种输入/输出源和目的的程序,更容易使用线程来设计和实现
在支持线程的操作系统中,调度和分派是在线程基础上完成的,因此大多数与执行相关的信息可以保存在线程级的数据结构中,但是有些活动会影响进程中的所有线程,因此操作系统必须在进程级对它们进行管理
线程的功能
线程状态 与进程一样,线程的主要状态有运行态,就绪态和阻塞态,一般来说挂起态对线程没有意义
有四种与线程状态改变的基本操作:
派生: 在典型情况下,在派生一个新进程时,同时也会为该进程派生一个线程
阻塞: 线程需要等待一个事件时会被阻塞,处理器转而执行另一个就绪线程
解除阻塞: 发生阻塞一个线程的事件后,会将该线程转到到就绪队列中
结束: 一个线程完成后,会释放其寄存器上下文和栈
从图中我们可以看到,当两个调用分别涉及两个不同的主机,用于获得一种组合结果,在单线程程序中,结果时按顺序获取的,因此程序必须依次等待,如果采用多线程,可明显加快程序的运行速度
在单处理上,多道程序涉及可交替执行多个进程中的多个线程,就比如图中的三个线程在处理器中交替执行
线程同步 一个进程中的所有线程共享同一个地址空间和诸如打开的文件之类的其他资源,一个线程对资源的任何修改都会影响同一进程中其他线程的环境,因此需要同步各种线程的活动,以便它们互不干扰且不破坏数据结构
4.2线程分类
用户级和内核级线程
线程分为两大类,即用户级线程(ULT)和内核级线程(KLT),后者又称为内核支持的线程或轻量级进程
用户级线程
在纯ULT软件中,管理线程的所有工作都由应用程序完成,内核意识不到线程的存在
这是通过线程库设计成多线程程序,线程库时管理用户级线程的一个例程包,它含有创建和销毁线程的代码,在线程间传递消息和数据的代码,调度线程执行的代码,以及保存和恢复线程上下文的代码
使用ULT而非KLT的优点如下:
1.所有线程管理数据结构都在一个进程的用户地址空间中,线程切换不需要内核模式特权,因此进程不需要为了管理线程而切换内核模式,进而节省了两次状态转换
2.调度因应用程序的不同而不同,一个应用程序可能更适合简单的轮转调度算法,而另一个应用程序可能更适合基于优先级的调度算法
3.ULT可在任何操作系统中运行,不需要对底层内核进行修改以支持ULT,线程库时供所有应用程序共享的一组应用级函数
与KLT相比,ULT也有两个明显的缺点:
1.在典型的操作系统中,许多系统调用都会引起阻塞,因此,在执行一个系统调用时,会阻塞进程中的所有线程
2.在纯ULT策略中,多虚存应用程序不能利用多处理技术,这就导致尽管有多线程,但是一个进程中只有一个线程可以执行
现在已有解决这两个问题的方法,例如把应用程序写成一个多进程而非多线程程序,但是这却消除的线程的主要优点
另一个解决线程阻塞问题的方法时,使用一种称为“套管”的技术,这个技术的目的是把一个产生阻塞的系统调用转化为一个非阻塞的系统调用
内核级线程 在纯KLT软件中,管理线程的所有工作均由内核完成,应用级没有线程管理代码,只有一个到内核线程设施的应用编程接口(API),Windows是这种方法的一个例子
这种方法克服了ULT方法的两个缺点。首先,内核可以同时把同一个进程中的多个线程调度到多个处理器中;其次,进程中的一个线程被阻塞时,内核可以调度同一个进程中的另一个线程。KLT方法的另一个优点是,内核例程自身也可是多线程的
但是这种方式与ULT方法相比,主要缺点是:在把控制权从一个线程传送到同一个进程的另一个线程时,需要切换到内核模式
混合方法
有些操作系统提供混合ULT和KLT的方法,在混合系统中,线程创建完全在用户空间中完成,线程的调度和同步也在应用程序中进行,在这种方式中,同一个应用程序的多个线程可在多个处理器上并行地运行,某个会引起阻塞的系统调用不会阻塞整个进程
其他方案
多对多的关系
域时一个静态实体,它包含一个地址空间和一些发生/接收消息的端口
在多个域中使用一个线程目的,最初是为了给程序员提供结构化工具,例如,我们考虑一个使用I/O子程序的程序,在允许用户派生进程的多道程序设计环境中,主程序可能产生一个新进程去处理I/O,然后继续执行
实现这一应用有以下几种方法:
1.整个程序作为单个进程来实现
2.主程序和I/O子程序可作为两个独立的进程实现
3.把主程序和I/O子程序当作单个线程实现的单个活动,但为主程序和I/O子程序创建地址空间(域)。因此,在执行过程中,这个线程可在两个地址空间之间移动,操作系统可以分别管理这两个地址空间,而不会带来任何创建进程的开销
第三种方法优点甚多,对某些应用程序来说可能时最有效的解决方案
一对多的关系
在分布式操作系统领域,人们把线程当作一个可在地址空间中移动的实体兴趣浓厚,这种研究的一个著名例子是内核称为Ra的Clouds操作系统
线程创建后,会通过该进程中一个程序的入口点,开始在进程中执行,线程可从一个地址空间转移到另一个地址空间,甚至横跨机器边界,线程移动时,它须携带自身的某些信息,如控制终端,全局参数和调度指导信息
Clouds方法提供了一种隔离用户,程序员与详细分布式环境的有效方法
4.3多核和多线程
尽管这个定律看上去使得多核组织机构的前景很迷人,但是即使是一小部分串行代码也会显著影响性能
除了服务器软件外,其他类型的应用程序也可从多核系统中直接获益,因为它们的吞吐量能随着处理器核心的数量伸缩,比如下面的例子:
原生多线程应用程序,多进程应用程序,java应用程序,实例应用程序
4.4Windows8的进程和线程管理
先讲Windows8中执行应用程序的关键对象和机制,然后详细介绍如何管理进程和线程
应用程序由一个或多个进程组成,每个进程提供执行程序所需要的资源
线程 是进程中可被调度的实体,一个进程的所有线程共享其虚拟地址空间和系统资源
作业对象 允许将一组进程当作一个单元来管理,作业对象是可命名,可获得,可共享的对象,这些对象控制器相关的进程的属性。
线程池 是一个工作线程集,它可代表应用程序有效地执行异步回调,线程池主要用于减少应用程序线程的数量,并对工作线程进行管理
纤程 是必须由应用程序调度的一个可执行单元,纤程运行在调度其的线程的上下文中,每个线程可以调度多个纤程
**用户模式调度(UMS)**是应用程序用于安排自己的线程的一种轻量级机制。对于需要有效地在多处理器或多核系统上并行多线程这类高性能要求的应用程序来说,UMS模式很有用,要利用UMS的优势,应用程序不许实现调度程序组件,以便管理应用程序的UMS线程并决定何时运行这些线程
Windows进程
Windows进程的重要特点如下:
Windows进程作为对象实现
一个进程可被创建为一个新进程,或一个已有进程的副本
一个可执行的进程可包含一个或多个线程
进程对象和线程对象都内置有同步能力
图中显示了进程与其控制或使用的资源的关联方式,每个进程都被指定了一个安全访问令牌,称为进程的基本令牌,与进程相关的还有定义当前分派给该进程的虚拟地址空间的一些块,进程不能直接修改这些结构,必须依赖虚存管理器来为进程提供内存分配服务
最后,进程还包括一个对象表,表中内容为该进程所知其他对象的句柄,比如图中的进程还可以访问一个文件对和一个定义一段共享内存的段对象
进程对象和线程对象
Windows面向对象结构促进了通用进程软件的发展,Windows使用两类与进程相关的对象:进程和线程。进程是一个实体,该实体对应于拥有内存,打开的文件等资源的用户作业或应用程序;线程是顺序执行的可分派工作单元,它是可中断的,因此处理器可以切换到另一个线程
Windows创建一个进程后,会使用为Windows进程定义的,用做模板的对象类或类型来生成一个新的对象实例,并在创建对象时为其赋属性值,图中给出了进程对象中每个对线属性的定义
每个Windows进程必须包含一个执行线程,该线程可能会创建其他线程,在多处理器系统中,同一个进程中的多个线程可以并行执行,下表定义了线程对象的属性,注意线程的某些属性与进程的类似,此时,线程的这些属性值是从进程的属性值得到的
注意,线程对象的一个属性是上下文,它包括线程执行后处理器寄存器的值,该信息允许线程被挂起和恢复,此外,当线程被挂起时,可通过该线程的上下文来改变它的行为
多线程
由于不同进程中的线程可以并发执行,因此Windows支持进程间的并发性。此外,同一个进程中的多个线程可以分配给不同的处理器并同时执行。一个含有多线程的进程在实现并发时,不需要使用多进程的开销,同一个进程中的线程可通过它们的公共地址空间交换信息,并访问进程中的共享资源
具有多个线程的面向对象进程是实现服务器应用程序的有效方法
线程状态
一个已有的Windows线程处于以下6种状态之一
就绪态: 就绪线程可被调度执行,内核分派器跟踪所有就绪线程,并按优先级顺序进行调度
备用态: 备用线程已被选择下次再某个特定处理器上运行。
运行态: 内核分派器执行了线程切换后,备用线程将进入运行态并开始执行,执行过程一直持续到该线程被抢占,用完时间片,被阻塞或终止,在前两种情况下,它将回到就绪态
等待态 1.当线程被一个事件阻塞。2.为了同步自愿等待。3.一个环境子系统指引它把自身挂起时,该线程将进入等待态
过渡态: 一个线程在等待后,如果准备好运行但资源不可用时,进入该状态
终止态: 一个线程可被自身终止或被另一个线程终止,或当其父进程终止时终止
对操作系统子系统的支持
通用的进程和线程软件必须支持各种操作系统客户端的特定进程和线程结构。操作系统子系统的职责是,利用Windows进程和线程的特征来模仿相应操作系统中的进程和线程软件
进程创建从应用程序的一个创建新进程请求开始,创建进程的请求从一个应用程序发往相应的受保护子系统,该子系统又给Windows执行体发送一个进程请求,Windows创建一个进程对象并给子系统返回一个该对象的一个句柄
4.5 Solaris的线程和SMP管理
Solaris实现了一种灵活利用处理器资源的多级线程支持
多线程体系结构
Solaris使用了4个独立的线程相关概念:
进程: 普通的UNIX进程,包括用户的地址空间,栈和进程块
用户级线程: 通过线程库在进程地址空间中实现,它们对操作系统是不可见的,用户级线程(ULT)是进程内一个用户创建的执行单元
轻量级线程: 轻量级线程可视为用户级线程和内核线程间的映射,每个轻量级线程支持一个或多个用户级线程,并映射到一个内核线程,轻量级线程由内核独立调度,可在多处理器中并行执行
内核线程: 可调度和分派到系统处理器运行的基本实体
图中显示了这4个实体的关系,注意,每个轻量级线程严格对应于一个内核线程
一个进程可以值包含一个绑定到某个轻量级进程上的用户级线程,在这种情况下,它对应于穿越的一个UNIX进程,只有一个执行线程,进程中不需要并发时,应用程序可使用这种进程结构
另外,有些内核线程并未与轻量级线程进程绑定,内核通过创建,运行并销毁这些内核线程来执行特定的系统功能
动机
Solaris中采用三层线程架构(用户级线程,轻量级线程和内核线程)的原因时,辅助操作系统管理线程,并向应用程序提供清晰的接口,在执行装填,一个轻量级线程以一对一的关系绑定到一个内核线程,因此,并发和执行是在内核线程的层面上来管理的,此外,一个应用程序可以通过包含系统调用的应用程序接口来访问硬件
进程结构
Solaris基本保留了UNIX系统中的进程结构,但用一组给每个轻量级进程包含一个数据块的结构代替了处理器状态块
轻量级线程数据结构包括以下元素:
一个轻量级进程标识符
轻量级线程及支持器内核线程的优先级
一个信号掩码,告诉内核将接受哪个信号
轻量级进程未运行时所保存的用户级寄存器的值
轻量级进程的内核栈,栈中包含系统调用参数,结果和每个调用级别的错误代码
资源的使用和统计数据
指向对于内核级线程的指针
指向进程结构的指针
图中给出了线程执行状态的简化图,这些状态反映了内核线程和与之绑定在一起的轻量级进程的执行状态,这些状态如下:
就绪态: 线程可以运行,即线程准备开始执行
执行态: 线程正在处理器上执行
睡眠态: 线程被阻塞
停止态: 线程停止
僵死态: 线程已被终止
自由态: 线程资源已被释放,并等待从操作系统的线程数据结构中移除
当一个线程被一个更高优先级的线程抢占或其时间片用完时,会从执行态转为就绪态。当一个线程被阻塞时,会从执行态转为睡眠态,并且必须等待一个事件唤醒它,以便回到就绪态,当一个线程调用了一个系统调用且必须等待系统服务完成时,就会被阻塞,当一个线程的进程被终止时,该进程便进入停止态,发生这种情况可能时出于调试目的
把中断当作线程
大多数操作系统包含两种基本形式的并发活动:进程和中断,Solaris把这两个概念统一到了称为内核线程模型核用于调度并执行内核线程的一种机制中,为实现这一点,中断被转换成内核线程。
Solaris中的解决方案可总结如下:
1.Solaris使用一组内核线程来处理中断,中断线程也有其自身的标识符,优先级,上下文核栈
2.内核控制对数据结构的访问,并使用互斥原语在中断线程间进行同步,也就是说,通常用于线程核同步技术也可用于中断处理
3.中断线程被赋予更高的优先级,高于所有其他类型的内核线程
4.6Linux的进程核线程管理
Linux任务
Linux中的进程或任务由一个task_struct数据结构表示,task_struct数据结构包含以下各类信息:
状态: 进程的执行状态
调度信息: Linux调度进程所需要的信息
标识符: 每个进程都有唯一的一个进程标识符
进程间通信: Linux支持UNIX SVR4中的IPC机制
链接: 每个进程都有一个到其父进程的链接及到其兄弟进程(与它有相同的父进程)的链接,以及到所有子进程的链接
时间和计时器: 包括进程创建的时刻和进程所消耗的处理器时间总量
文件系统: 包括指向被该进程打开的任何文件的指针和指向该进程当前目录与根目录的指针
地址空间: 定义分配给该进程的虚拟地址空间
处理器专用上下文: 构成该进程上下文的寄存器和栈信息
运行:这个状态值对应于两个状态,一个运行进程要么正在执行,要么准备执行
可中断:这是一个阻塞态,此时进程正在等待一个事件(如一个I/O操作)的结束,一个可用的资源或另一个进程的信号
不可中断:这是另一个阻塞态。它与可中断状态的区别是,在不可中断状态下,进程正在等待一个硬件条件,因此不会接收任何信号
停止:该进程被中止,并且只能由来自另一个进程的主动动作恢复,例如,一个正在被调试的进程可被置于停止态
僵死:进程已被终止,但由于某些原因,在进程表中仍然有其任务结构
Linux线程
如同传统的UNIX系统那样,老版本的Linux内核不支持多线程,多线程应用程序需要一组用户级程序库来编写,以便将所有线程映射到一个单独的内核级进程中
Linux提供一种不区分进程和线程的解决方案,这种解决方案使用一种类似于Solaris轻量级进程的方法,将用户级线程映射到内核级进程,组成一个用户级进程的多个用户级线程则映射到共享同一个组ID的多个Linux内核进程上,因此这些进程可用共享文件和内存等资源,使得同一个组中的进程调度切换时不需要切换上下文
当Linux内核执行从一个进程到另一个进程的切换时,会检查当前进程的页目录地址是否与将被调度的进程的相同,若相同,则它们共享同一个地址空间,所以此时上下文切换仅仅是从代码的溢出跳转到代码的另一处
Linux命名空间
Linux中和每个进程相关联的是一组命名空间,命名空间可使一个进程拥有与其他相关命名空间下的其他进程不同的系统视图,命名空间的总体目标之一是位实现控制组提供支持,控制组是一个轻量级可视化工作,它可使得进程或进程组像是系统上的唯一进程,因此,控制组是一种形式的虚拟机,当前Linux有六种命名空间
Mount命名空间 这个命名空间为进程提供文件系统层次结构的特定试图,因此两个不同mount命名空间的两个进程会看到不同的文件系统层次结构。一个进程使用的所有文件操作仅适用于该进程可见的文件系统
UTS命名空间 UTS命名空间和Linux系统调用unmae有关,unmae调用返回当前内核的名字和信息,包括节点名和域名
IPC命名空间 IPC命名空间隔离某些进程间通信资源,如信号量,因此,并发机制由可在进程中启动IPC的程序员使用,这些进程共享相同的IPC命名空间
PID命名空间 PID命名空间隔离进程ID空间,以便不同PID命名空间中的进程拥有相同的PID
网络命名空间 网络命名空间用于隔离与网络相关的系统资源,每个网络命名空间都拥有自己的网络设备,IP地址,IP路由表,端口号等
用户命名空间 用户命名空间为其自身的UID集提供一个容器,并完全与其父进程分离。因此,当一个进程克隆一个新进程时,可为新进程指定一个新的用户命名空间,一个新的PID命名空间和所有命名空间,克隆的进程对父进程的所有资源或父进程资源和特权的子集,有使用权和特权
4.7 Android的进程和线程管理
安卓应用
Android应用是实现了某个应用的软件,每个Android应用都包含一个或多个实例,而每个实例由一个或多个4种类型的应用程序组件组成,以下是4种组件类型:
活动: 活动对应于一个用户可视化界面,例如一个电子邮件应用程序可能用一个活动来显示新邮件列表,一个活动来撰写邮件,一个活动来读取邮件
服务: 服务通常用于执行后台操作,它需要相当长的时间才能完成,对于一个应用的主要线程(即UI线程)而言,这会使得与用户直接互动的响应速度更快。例如,用于使用应用程序时,一个服务可用创建一个进程或线程来播放背景音乐,或者可以创建一个线程从网络获取数据而不阻塞用户界面和活动
内容提供器: 内容提供器是应用程序所用应用数据的一个接口,管理数据中的一类是私有数据,它只能含有内容提供器的应用程序使用,例如,记事本应用程序使用内容提供器保存笔记,管理数据中的另一类是共有数据
广播接收器: 广播接收器响应系统的广播公告,广播可来自其他应用程序,如让其他应用程序知道一些数据已被下载到设备,并可供它们使用;广播也可来自系统,如低电量警告
每个应用程序都运行在自己的专用虚拟机上,并有一个包含该应用程序及其虚拟机的进程,如下图所示
这种方法隔离了每个应用,称为沙盒程序,因此,一个应用程序在无授权许可的情况下,不能访问其他应用程序的资源,每个应用程序都视为一个有着自身独特用户ID的个人Linux用户,这些用户ID用于设置文件权限
活动
活动是一个提供用户交互的应用程序软件,如拨号,拍照,发送邮件或查看地图。每个活动都有一个可在其中绘制其用户界面的窗口,这个窗口通常会充满屏幕,也可比屏幕小而浮在其他窗口之上
前面提到过一个应用可能包含多个活动,当应用运行时,有一个活动在前台与用户进行交互,打开多个活动是按照栈的方式记录的,若用户从当前活动切换到其他活动,就会创建一个新活动并把新活动压入栈中,之前的活动会变成当前应用栈的第二个元素,用户可以通过按返回按钮或类似的功能接口,返回到最近的一个前台活动
活动状态
这个图片给出了活动的状态转换简图,一个应用可以有多个活动,每个活动的状态对应于状态转换图上的一个特定状态,启动一个新活动时,应用程序会执行一系列针对活动管理器的系统调用
结束一个应用程序
如果有太多的事情正在进行,系统可能需要恢复一些主存储器以保持响应,在这种情况下,系统会通过结束应用中的一个或多个活动来回收内存,同时终止该应用的进程
当用户返回该应用时,系统就需要重新创建那些需要调用的已被结束的活动
进程和线程
一个应用的进程和线程默认分配单个进程和单个线程,所有应用程序的组件在该应用程序的单个进程的单个线程上运行,在任何情况下,对于一个给定的应用,其所有进程和线程都在相同的虚拟机中执行
要回收负载较重系统中的内存,系统就需要结束一个或多个进程,系统会根据优先级层次结构来决定结束哪个或哪些进程,在任何给定的时刻,每个进程都处在一个特定的优先级层次结构上,处在最低优先级层次结构上的进程先结束,按降序排列的优先级层次结构如下所示:
前台进程: 用户当前正从事工作所需要的进程,在同一时间,前台进程可以有多个
可见进程: 主持一个组件的进程,它不是前台进程,但对用户始终可见
服务进程: 正在运行服务的进程,它不属于任何一个更高的类别,这样的例子有在后台播放音乐或在网络上下载数据
后台进程: 在停止态主持一个活动的进程
空进程: 不保留任何活动应用组件的进程,这种进程的唯一原因是为了缓存目的,用来减少下一次组件需要在其中运行的启动时间
4.8 MAC OS X的GCD技术
这个技术提供了一个可用线程池,设计人员可以指定程序中的部分代码为“块”,块可被独立调度和并发执行,操作系统会根据核心数量及系统的线程容量尽可能提高并发性,虽然其他操作系统也提供类似的线程池,但GCD在易用性和效率方面有质的提升
“块”是对C,C++或其他编程语言的简单扩展,使用块的目的是为了定义一个完整的工作单元,包括代码和数据,下面是一个很简单的块的定义
块可让编程人员封装复杂的函数及其参数和数据,使他们能像变量一样在程序中方便地引用和传递,块以队列的方式调度和分派,应用程序除了使用GCD提供的系统队列外,也可建立自己的私有队列,程序执行过程中遇到的块会被放入队列,GCD则利用这些队列来实现并发,顺序化和回调,使用队列比手动管理线程和锁要有效得多,例如下面这个有三个块:
对于可并发的队列,调度器会尽快将F分配给一个可用线程,接下来才分配G,最后是H,对于顺序执行队列,调度器将F分配给一个线程,只有当F完成置换才会将G分配一个线程,使用预定义线程的方法为每次请求节省了创建新线程的时间,减少了与块操作有关的延迟
除了直接调度块外,应用程序还可将一个块和队列与一个事件源关联起来,事件源可以是时钟,网络套接字或文件描述符。每当源产生一个事件时,如果关联的块不是正在执行,则会被调度执行,这样既实现了快速响应,又避免了轮询或让线程阻塞在事件源上的代价
4.9 小结
某些操作系统区分进程和线程的概念,前者涉及资源的所有权,后者涉及程序的执行,这种方法可提高性能,方便编码,在多线程系统中,可在一个进程内定义多个并发线程,实现方法是使用用户级线程或内核级线程。
用户级线程对操作系统是未知的,它们由一个在进程的用户空间中运行的线程库创建并管理。用户级线程非常高效,因为从一个线程切换到另一个线程不需要进行状态切换,但一个进程中只有一个用户级线程可以执行,如果一个线程发生阻塞,整个进程都会被阻塞。进程内包含的内核级线程是由内核维护的,由于内核认识它们,因而同一个进程中的多个线程可在多个处理器上并行执行,一个线程的阻塞不会阻塞整个进程,但从一个线程切换到另一个线程时需要进行模式转换
第五章 并发性:互斥与同步
操作系统设计中的核心问题是进程和线程的管理:
多道程序设计技术: 管理单处理系统中的多个进程
多处理器技术: 管理多处理器系统中的多个进程
分布式处理器技术: 管理多台分布式计算机系统中多个进程的执行
并发是所有问题的基础,也是操作系统设计的基础。并发包括很多设计问题,其中有进程间通信,资源共享与竞争,多个进程活动的同步以及给进程分配处理器时间等
并发会在以下三种不同的上下文中出现:
多应用程序: 多道程序设计技术允许在多个活动的应用程序间动态共享处理器时间
结构化应用程序: 作为模块化设计和结构化程序设计的扩展,一些应用程序可被有效地设计成一组并发进程
操作系统结构: 同样的结构化程序设计优点适用于系统程序,且我们已知操作系统自身常常作为一组进程或线程实现
5.1并发的原理
在单处理器多道程序设计系统,进程会被交替地执行,因而表现出一种并发执行的外部特征,即使不能实现真正的并行处理,并且在进程间来回切换也需要一定的开销,交替执行在处理效率和程序结构上还是会带来很多好处,在多处理系统中,不仅可以交替执行进程,而且可以重叠执行进程
从表面上看,交替和重叠代表了完全不同的执行模式和不同的问题,实际上,这两种技术都可视为并发处理的一个实例,并且都代表了同样的问题。在单处理器情况下,问题源于多道程序设计系统的一个基本特性:进程的相对执行速度不可预测,这就带来了下列困难:
1.全局资源的共享充满了危险:例如,如果两个进程都使用同一个全局变量,并且都对该变量执行读写操作,那么不同的读写执行顺序是非常关键的
2.操作系统很难对资源进行最优化分配
3.定位程序设计错误非常困难,这是因为结果通常是不确定和不可再现的
一个简单的例子
现在考虑一个支持单用户的单处理器多道程序设计系统,用户可以从一个应用程序切换到另一个应用程序,每个应用程序都使用同一键盘进行输入,使用同一屏幕进行输出,echo被视为一个共享过程,载入到所有应用程序的公用全局存存储区中,因此,只需使用echo过程的一个副本,从而节省空间
但是这种共享可能会带来一些问题。考虑下面的顺序:
1.进程P1调用echo过程,并在getchar返回它的值并存储与chin后立即中断,此时chin变量存储的是x
2.进程P2被激活并调用echo过程,echo过程允许得出结果,然后显示P2进程输入的字符y
3.进程P1恢复,此时chin中的值x被写覆盖,因此已丢失,进程P1显示y
因此,第一个字符丢失,第二个字符显示了两次,问题的本质在于共享全局变量chin。
如果需要保护共享的全局变量,唯一的办法是控制访问该变量的代码,如果我们定义了一条规则,即一次只允许一个进程进入echo,并且只有在echo过程运行结束后,它才对另一个进程是可用的,那么刚才讨论的那类错误就不会发生
在多处理器系统中,也同样存在保护共享资源的问题,解决的方法也是相同的,根上面单处理多道程序系统中的解决方法相同,都是控制对共享资源的访问,资源在一段时间内,只能被一个进程访问和使用
竞争条件
竞争条件发生在多个进程或线程读写数据时,其最终结果取决于多个进程的指令执行顺序
在第一个例子中,假设两个进程P1和P2共享全局变量a,在P1执行的某一时刻,它将a的值更新为1,在P2执行的某一个时刻,它将a的值更新未2,因此,两个任务竞争更新变量a,在本例中,竞争的“失败者”(即最后更新全局变量a的进程)决定了变量a的最终值
在第二个例子中,考虑两个进程P3和P4共享全局变量b和c,在某一执行时刻,P3执行赋值语句b=b+c,在另一个执行时刻,P4执行赋值语句c=b+c,两个变量的最终值取决于两个进程执行赋值语句的顺序
操作系统关注的问题
并发会带来以下的设计和管理问题,如下所示:
1.操作系统必须能够跟踪不同的进程
2.操作系统必须每个进程分配和释放各种资源
3.操作系统必须保护每个进程的数据和物理资源,避免其他进程的无意干扰
4.一个进程的功能和输出必须与执行速度(相对于其他并发进程的执行速度)无关
为理解如何解决与执行速度无关的问题,我们首先需要考虑进程间的交互方式
进程的交互
我们可以根据进程相互之间知道对方是否存在的程度,对进程间的交互方式进行分类,如下表所示
进程之间互相不知道对方的存在:这是一些独立的进程,它们不会一起工作
进程间接知道对方的存在:这些进程并不需要知道对方的进程ID,但它们共享某些对象,如一个I/O缓冲区
进程直接知道对方的存在:这些进程可通过进程ID互相通信,以完成某种活动,同样,这类进程表现出合作行为
但是实际条件并不总是像表中给出的那么清晰,例如几个进程可能既表现出竞争,又表现出合作
进程间的资源竞争 当并发进程竞争使用同一资源时,它们之间会发生冲突,竞争进程之间没有没有任何信息交换,但一个进程的执行可能会影响到竞争进程的行为,特别时当两个进程都期望访问同一个资源时,
竞争进程面临三个控制问题,首先是互斥,假设两个或更多的进程需要访问一个,比如两条进程都请求使用打印机,只能先允许一个进程使用,在执行过程中,每个进程都给该I/O设备发命令,接收状态信息,发送数据和接收数据。我们把这类资源称为临界资源,使用临界资源的那部分程序称为程序的临界区,一次只允许有一个程序在临界区中。
实施互斥产生了两个额外的控制问题,一个是死锁,例如有两个进程P1和P2,以及两个资源R1和R2,当操作系统把R1分配给P2,把R2分配给P1,每个进程都在等待另一个资源,且在获得其他资源并完成功能前,谁都不会释放自己已拥有的资源,此时这两个进程就会发生死锁
另一个控制问题是饥饿,假设有三个进程,每个进程都周期性地访问资源R,当P1拥有资源,P2和P3都被延迟,等待资源,当P1退出时,操作系统把访问授权给P3,并在P3退出时又给P1,P3和P1不断的循环访问资源,P2就会被无限地拒绝访问资源
由于操作系统负责分配资源,竞争的控制不可避免地涉及操作系统,此外进程自身需要能够以某种方式表达互斥的需求,如在使用前对资源加锁,但任何一种解决方案都涉及操作系统的某些支持,如提供锁机制,如果以进程在某个资源的临界区中,则任何试图进入临界区的进程都必须等待
进程间通过共享合作
通过共享进行合作的情况,包括进程间在互相并不确切知道对方的情况下进行交互,进程可能使用并修改共享变量而不涉及其他进程,但却知道其他进程也可能访问同一个数据,因此这些进程必须合作,以确保它们共享的数据得到正确管理,控制机制必须确保共享数据的完整性,由于数据保存在资源中,因此再次涉及有关互斥,死锁和饥饿等控制问题,唯一的区别是可用按两种不同的模式(读和写)访问数据,并且只有写操作必须保证互斥
进程间通过通信合作
典型情况下,通信可有各种类型的消息组成,发送和接收消息的原语由程序设计语言提供,或由操作系统的内核提供
由于在传递消息的过程中进程间未共享任何对象,因而这类合作不需要互斥,但仍然存在死锁和饥饿问题
互斥的要求
要提供对互斥的支持,必须满足以下要求:
1.必须强制实施互斥:在与相同资源或共享对象的临界区有关的所有进程中,一次只允许一个进程进入临界区
2.一个在非临界区停止的进程不能干涉其他进程
3.绝不允许出现需要访问临界区的进程被无限延迟的情况
4.没有进程在临界区中时,任何需要进入临界区的进程必须能够立即进入
5.对相关进程的执行速度和处理器的数量没有任何要求和限制
6.一个进程驻留在临界区中的时间必须是有限的
满足这些互斥条件的方法有很多种,第一种方法是让并发执行的进程承担这一责任,这类进程需要与另一个进程合作,而不需要设计语言或操作系统提供任何支持来实施互斥,我们把这类方法称为软件方法,尽管这种方法已被证明虎增加开销并存在缺陷
第二种方法涉及专用机器指令,这种方法的优点是可用减小开销,但却很难成为一种通用的解决方案
5.2 互斥:硬件的支持
中断禁用
在单处理器机器中,并发进程不能重叠,只能交替,因此,为保证互斥,只需保证一个进程不被中断即可,可通过如下方法实施:
由于临界区不能被中断,故可以保证互斥(也就是中断其他进程,只留处在临界区的进程),但是这种方法的代价非常高,由于处理器被限制只能交替执行程序,因此执行的效率会明显降低,另一个问题是,这种方法不能用于多处理体系结构中
专用机器指令
在多处理配置中,几个处理器共享对内存的访问,在这种情况下,不存在主/从关系,处理器间的行为是无关,表现出一种对等关系,处理器之间没有支持互斥的中断机制
这里给出两种最常见的指令:
比较和交换指令 这个指令的一个版本使用一个测试值检查一个内存单元,如果这个内存单元的当前值是测试值,就用一个新的的值取代,否则保持不变,如果返回值与测试值相同,则表示内存单元已被更新,整个比较和交换功能按原子操作进行,即它不接受中断,那为什么比较后要进行交换呢,就像你去超市买东西,看到货架上有货和拿走商品必须同时完成,不然过一会这个货可能就没了,你就买不到了
这个指令的另一个版本返回一个布尔值:交换发生时为真,否则为假,几乎所有的处理器家族都支持该指令的某个版本,且多数操作系统都利用该指令支持并发
唯一可以进入临界区的进程时符合我们条件的(也就是看比较和交换指令的返回值),所有试图进入临界区的其他进程进入忙等待模式。
术语忙等待 或自旋等待指的是这样一种技术:进程在得到临界区访问权之前,它只能继续执行测试变量的指令来得到访问权,除此之外不能做任何其他事情
机器指令方法的特点 使用专用机器指令实施互斥有以下优点:
适用于单处理器或共享内存的多处理器上的任意数量的进程
简单且易于证明
可用于支持多个临界区,每个临界区可以用它自己的变量定义
但也有一些严重的缺点:
使用了忙等待: 因此,当一个进程正在等待进入临界区时,它会消耗处理器时间
可能饥饿: 当一个进程离开一个临界区且有多个进程正在等待时,选择哪个等待进程时任意的,因此某些进程可能会被无限地拒绝进入
可能死锁: 考虑单处理器上的下列情况。进程P1执行专用指令并进入临界区,然后P1被中断并把处理器让给具有更高优先级的P2,若P2试图使用同一资源,由于互斥机制,它将被拒绝访问,因此,它会进入忙等待循环,但是,由于P1比P2的优先级低,因此它将永远不会被调度执行
5.3 信号量
图中是常用的并发机制
在解决并发进程问题的过程中,第一个重要的进展是1965年的Dijkstra的论文,他参与了了一个操作系统的涉及,这个操作系统设计为一组合作的顺序进程,并为支持合作提供了可靠的机制
基本原理如下:两个或多个进程可以通过简单的信号进行合作,可以强迫一个进程在某个位置停止,直到它接收到一个特定的信号,任何复杂的合作需求都可以通过适当的信号结构得到满足,为了发信号,需要使用一个称为信号量的特殊变量,要通过信号量s传送信号,进程须执行原语semSignal(s),要通过信号量接收信号,进程必须执行原语semWait(s);若相应的信号仍未发送,则阻塞进程,直到发送完为止
为达到预期效果,可把信号量视为一个值为整数的变量,整数值上定义了三个操作:
1.一个信号量可以初始化为非负数
2.semWait操作使信号量减1,若值变成负数,则阻塞执行semWait的进程,否则进程继续执行
3.semSignal操作使信号量加1,若值小于等于零,则被semWait操作阻塞的进程解除阻塞
除了这三个操作外,没有任何其他办法可以检查或操作信号量
对这三个操作的解释如下:开始时,信号量的值未零或者正数,若值为正数,则它等于发出semWait操作后可立即执行的进程的数量,若值为零,则发出semWait的下一个进程会被阻塞,此时信号量的值变为负值,之后,每个后续semWait操作都会使得信号量的负值更大,该负值等于正在等待解除阻塞的进程的数量,在信号量为负值的情形ia,1每个semSignal操作都会将等待进程中的一个进程接触阻塞
这个给出了信号量定义的三个重要结论:
通常,在进程对信号量减1之前,无法提前直到该信号量是否会被阻塞
当进程对一个信号量加1之后,会唤醒另一个进程,两个进程继续并发允许,而在一个单处理系统中,同样无法知道哪个进程会立即继续允许
向信号量发出信号后,不需要知道是否有另一个进程正在等待,被解除阻塞的进程数要么没有,要么是为1
下图给出了关于信号量原语更规范的定义,semWait和semSignal原语被假设是原子操作
下面这个图定义了称为了二元信号量的更严格的形式,二元信号量的值只能是0或1,可有下面三个操作定义
1.二元信号量可以初始化为0或1
2.semWaitB操作检查信号的值,若值为0,则进程执行semWaitB就会受阻,若值为,则将值改为0,并继续执行该进程
3.semSignalB操作检查是否有任何进程在该信号上受阻,若有进程受阻,则通过semWaitB操作,受阻的进程会被唤醒;若没有进程受阻,则值设置为1
理论上,二元信号量更易于实现,且可以证明它和普通信号具有同样的表达能力,为了区分这两种信号,非二元信号量也常称为计数信号量或一般信号量
与二元信号量相关的一个概念是互斥锁。互斥是一个编程标志位,用来获取和释放一个对象。二元信号量和互斥量的关键区别在于,为互斥量枷锁的进程和为互斥量解锁的进程必须是同一个进程,相比之下,可能由某个进程对二元信号量进行加锁操作,而由另一个进程为其解锁
不论是计数信号还是二元信号量,都需要使用队列来保存于信号量上等待的进程,那进程按照什么顺序从队列中移除?,最公平的策略是先进先出,被阻塞时间最久的进程最先从队列释放,采用这一策略定义的信号量称为强信号量,而没有规定进程从队列中移出顺序的信号量称为弱信号量
互斥
图中给出了一种使用信号量s解决互斥问题的方法,设由n个进程,用数组P(i)表示,所有进程都需要访问共享资源。每个进程进入临界区执行semWait(s),若s值为负,则进程被阻塞,若值为1,则s被减为0,进程立即进入临界区,由于s不在为正,因而其他进程都不能进入临界区
信号量一般初始化为1,这一第一个执行semWait(s)的进程可立即进入临界区,并把s的值置为0,接着任何试图进入临界区的其他进程,都将发现第一个进程忙,因此被阻塞,把s的值置为-1。可以有任意数量的进程试图进入,每个不成功的尝试都会使s的值减1,当最初进入临界区的进程离开时,s增1,一个被阻塞的进程被移除等待队列,至于就绪态,这样,当操作系统下一次调度时,它就可以进入临界区
生产者/消费者问题
现在分析并发处理中最常见的一类问题:生产者/消费者问题。这个访问通常描述如下:有一个或多个生产者生产某种类型的数据,并放置在缓冲区中;有一个消费者从缓冲区中取数据,每次取一项;系统保证避免对缓冲区的重复操作,即在任何时候只有一个主体可访问缓冲区,问题是要确保这种情况:当缓存已满时,生产者不会继续向其中添加数据;当缓存为空,消费者不会从中移走数据,我们将讨论该问题的多种解决方案,以证明信号量的能力和缺陷
首先假设缓冲区是无限的,且是一个线性的元素数组,可以用抽象的术语定义如下的生产者和消费者函数:
上面这种是用索引in和out来实现,但是这种方法要确保消费者在开始之前应该确保生产者已经生产
现在用二元信号量来实现这个系统
这种方法很直观,一开始s置为1,然后生产者在任何时候都可以向缓存中增加数据,在添加前执行semWaitB(s),让s变为0,保证其他消费者和生产者无法在操作过程中访问缓冲区,然后生产完数据后执行semSignalB(s),让s重新变为1,同时,在生产者在临界区中时候,将n的值增1,若n=1,则表示在本次生产前缓冲区是空的,生产者执行semSignalB(delay)告诉消费者有数据生成,可以取数据,消费者就使用semWaitB(delay)等待生成出第一个项目,然后在自己的临界区中取到这一项并将n减1,如果生产者总能保持在消费者之前工作,即n将总为正,则消费者很少会被阻塞在信号量delay上,因此,生产者和消费者都可以正常运行
但这个程序仍有缺陷,主要是在这个地方
消费者执行semSignalB(s)后,s为1,如果这个时候生产者使用了临界区添加数据后,n会从0变成1,s还是1,delay也是1然后下面的判断条件不成立,进入下一轮循环,在下一轮循环执行完semSignalB(s)后如果没有生产者进行生成的话,那么这时n是0,因为前面的n- -,这时判断条件成立,delay为0,然后n=0,s=1,继续循环(因为s是1),那么在循环一遍后,n变成-1了,这时就出现问题了,可能会导致死锁(这一段我理解的可能有问题,推荐大家还是去读一些别人的解释好一点)
解决这个的问题的方法是引入一个辅助变量,比如下面这段程序引入了m,就不会出现死锁
使用一般信号量(也成为计数信号量),可得到一种更清晰的解决方法,变量n为信号量,其值等于缓冲区中的项数,假设在抄录程序的时候发生来错误,也不会影响程序
现在假设semWait(n)和semWait(s)操作偶然被颠倒,这时会产生严重甚至致命的错误,如果在缓冲区为空时消费者曾进入过临界区,那么任何一个生产者都不能向缓存中中添加数据项,系统发生死锁。这是体现信号量的微妙之处和进行正确设计的困难之处的较好示例
但是缓冲区毕竟不是无限的,所以缓冲区被视为一个循环存储器,并总是保持下面的关系
生产者和消费者函数可表示成如下形式:
这给出了使用一般信号量的解决方案,其中增加了信号量e来记录空闲空间的数量
信号量的实现
如前所述,semWait和semSignal操作必须作为原子原语实现,一种显而易见的方法时用硬件或固件实现,如果没有这些方案,那么还有很多其他方案,问题的本质是互斥:任何时候只有一个进程可用semWait和semSignal操作控制一个信号量,可以使用Dekker算法或Peterson算法,这比如伴随着处理开销
另一种选择是使用一种硬件支持互相互斥的方案,例如比较和交换指令和中断方法,如下图所示
对于单处理系统,在semWait或semSignal操作期间是可以禁用中断的,因为这些操作的执行时间相对很短,因此这种方式是合理的
5.4管程
信号量为实施互斥和进程间的合作,提供了一种原始但功能强大且灵活的工具,但是,使用信号量设计一个正确的程序是很困难的,难点在于semWait和semSignal操作可能分布在整个程序中,而很难看出信号量上的这些操作所产生的整体效果
管程是一种程序设计语言结构,它提供的功能和信号量相同,但更易于控制,管程还被作为一个程序库实现,这就允许我们用管程锁定任何对象,对类似于链表之类的对象,可以用一个锁锁住整个链表,也可每个表用一个锁,还可为表中的每个元素用一个锁
使用信号的管程
管程是由一个或多个过程,一个初始化序列和局部数据组成的软件模块,其主要特点如下:
1.局部数据变量只能被管程的过程访问,任何外部过程都不能访问
2.一个进程通过调用管程的一个过程进入管程
3.在任何时候,只能有一个进程在管程中执行,调用管程的任何其他进程都被阻塞,以等待管程可用
前两个特点让人联想到面向对象软件中对象的特点。的确,面向对象操作系统或程序设计语言很容易把管程作为一种具有特殊特征的对象来实现
通过给进程强加规定,管程可以提供一种互斥机制:管程中的数据变量每次只能被一个进程访问。因此,可以把一个共享数据结构放在管程中,从而对它进行保护,如果管程中的事件代表某些资源,那么管程为访问这些资源提供了互斥机制
要进行并发处理,管程必须包含同步工具。例如,假设一个进程调用了管程,且当它在管程中时必须被阻塞,直到满足某些条件,这就需要一种机制,使得该进程不仅被阻塞,而且能释放这个管程,以便某些其他的进程可以进入,以后,当条件满足且管程再次可用时,需要恢复该进程并允许它在阻塞点重新进入管程
管程通过使用条件变量来支持同步,有些条件变量包含在管程中,并且只有在管程中才能被访问。有两个函数可以操作条件变量:
cwait(c):调用进程的执行在条件c上阻塞,管程现在可被另一个进程使用
csignal(c):恢复执行在cwait之后因某些条件被阻塞的进程,若有多个这样的进程,选择其中一个,若没有这样的进程,什么都不做
注意,管程的wait和signal操作和信号量不同,如果管程中的一个进程发信号,但没有在这个条件变量上等待的任务,则丢弃这个信号
尽管一个进程可以通过调用管程的任何一个过程进入管程,但我们仍可视管程有一个入口点,保证一次只有一个进程可以进入。其他试图进入管程的进程不被阻塞并加入等待管程可用的进程队列中。当一个进程在管程中时,它可能会通过发送cwait(x)把自己暂时阻塞在条件x上,随后它被放入等待条件改变以重新进入管程的进程队列中,在cwait(x)调用的下一条指令开始恢复执行
若在管程中执行的一个进程发现条件变量x发生来变化,则它发发送csignal(x),通知相应的条件队列已改变
我们在这里举一个例子,管程模块boundedbuffer控制着用于保存和取回字符的缓冲区,管程中有两个条件变量(使用结构cond声明):缓冲区中至少有增加一个字符的空间时,notfull为真;缓冲区中至少有一个字符时,notempty为真
生产者可以通过管程中的过程append向缓冲区中增加字符,它不能直接访问buffer,该过程首先检查条件notfull,以缺点缓冲区是否还有可用空间,如果没有,执行管程的进程在这个条件上被阻塞。其他的某个进程现在可用进入管程,此后,当缓冲区不再满时,被阻塞的进程可以从队列中移出,重新激活并恢复处理,消费者函数类似
这个例子表明,与信号量相比,管程担负的责任不同,对于管程,它构造了自己的互斥机制,程序员必须把适当的cwait和csignal原语放在管程中,而在使用信号量的情况下,执行互斥和同步都是程序员负责
在这个程序中,进程执行csignal函数后立即退出管程,若未执行,则发送该信号的进程被阻塞,从而使管程可用,并将其放入紧急队列中,因为该进程已被执行一部分
对于信号量,在管程中的同步函数可能会发生错误,例如,若省略管程中任何一个csignal函数,则进入相应条件队列的进程将被永久阻塞,管程优于信号量之处在于,所有同步机制都被限制在管程内部,这不但易于验证同步的正确性,并且易于检测出错误,若一个管程都被正确地编写,则所有进程对受保护资源的访问都是正确的;而对于信号量,只有当所有访问资源的进程都被正确编写时,资源访问才是正确的
使用通知和广播的管程
Hoare关于管程的定义要求在条件队列中至少有一个进程,当另一个进程为该条件产生csignal时,立即运行队列中的一个进程,因此产生csignal的进程必须立即退出管程,或阻塞在管程上
这种方法有两个缺陷:
1.若产生csignal的进程在管程内还未结束,则需要两次额外的进程切换;阻塞这个进程需要一次切换,管程可用时恢复这个进程又需要一次切换
2与信号相关的进程调度必须非常可靠,必须立即激活来自相应条件队列中的一个进程,调度程序必须确保在激活前没有其他进程进入管程,否则进程被激活的条件又会改变
Mesa语言有一种不同的管程机制,这种方法克服来上面的缺陷,并之处许多有用的扩展,在Mesa中,csignal原语被cnotify取代,cnotify可以这样解释:当一个正在管程中的进程执行cnotify(x)时,会使得x条件队列得到通知,但发信号的进程继续。通知的结果是在将来合适且处理器可用时恢复执行位于条件队列头的进程,但是,由于不能保证在它之前没有其他进程进入管程,因而这个等待进程必须重新检查条件,比如更改后的代码如下
这里的if语句被while循环取代,作为回报,它不在有额外的进程切换,且对等待进程在cnotify之后什么运行没有任何限制
与cnotify原语相关的一个有用的改进是,给每个条件原语关联一个监视计时器,不论条件是否被通知,等待时间超时的一个进程将被设置为就绪态,激活该进程后,它检查相关条件,如果条件满足则继续执行,超时可以防止如下情况发生:当某些其他进程在产生相关条件的信号之前失败时,等待该条件的进程被无限制地推迟执行而处于饥饿状态
由于进程是接到通知而非强制激活的,因此可给指令表增加一条cbroadcast原语。广播可以使所有在该条件上等待的进程都置于就绪态,当一个进程不知道有多少进程将被激活时,这种方式非常方便。此外当一个进程难以准确判断将激活哪个进程时,也可使用广播。
Lampson/Redell管程(也就是这个while管程)优于Hoare管程(前面的if管程)的原因时,这种方法的错误较少,在这种方法中,由于每个过程在收到信号后都检查管程变量,且由于使用了while结构,一个进程不正确广播或发信号,不会导致收到信号的程序出错,收到信号的程序将检查相关的变量,如果期望的条件得不到满足,它会继续等待
这个管程的另一个优点是,它有助于在程序结构中采用更加模块化的地方,例如,考虑一个缓冲区分配程序的实现,为了在顺序的进程间合作,必须满足两级条件:
1.保持一致的数据结构。管程强制实施互斥,并在允许对缓冲区的另一个操作之前完成一个输入或输出操作
2.在1级条件的基础上,为该进程加上足够的内存,完成其分配请求
在Hoare管程中,每个信号会传达1级条件,同时携带一个隐含消息“我现在有足够的空闲字节,能够满足特定的分配请求”,因此,该信号隐式携带2级条件,如果后来程序员改变了2级条件的定义,则需要重新编写所有发信号的进程,如果程序员改变了对任何特定等待进程的假设(即等待一个稍微不同的2级不变量),则可能需要重新编写所有发信号的进程。这样就不是模块化的结构,且当代码被修改后可能会引发同步错误,每当对2级条件做很小的改动时,程序员就必须记得去修改所有进程,而对于Lampson/Redell管程,一次广播可以确保1级条件并携带2级条件的线索,每个进程将自己检查2级条件,故不会产生错误的唤醒,因此,2级条件可以隐藏在每个过程中,而对Hoare管程而言,2级别条件必须由等待者带到每个发信号的进程的代码中,这违反了数据抽象和进程间的模块化原理
5.5消息传递
进程交互时,必须满足两个基本要求:同步和通信。为实施互斥,进程间需要同步;为实现合作,进程间需要交互信息,提供这些功能的一种方法是消息传递,消息传递还有一个优点,即它可在分布式系统,共享内存的多处理器和单处理器系统中实现
消息传递系统有多种形式,消息传递的实际功能以一对原语的形式提供:
send(destination,message)
receive(source,message)
这是进程间进行消息传递所需的最小操作集。一个进程以消息的形式给另一个指定的目标进程发送信息,进程通过执行receive原语接收消息,receive原语中指明发送消息的源进程和消息
表中列出来与消息传递系统相关的一些设计特点
同步
两个进程间的消息通信隐含着某种同步的消息:只有当一个进程发送消息后,接收者才能接收消息
考虑send原语,首先,一个进程执行send原语有两种可能:要么发送进程被阻塞直到这个消息被目标进程接收到,要么不阻塞。类似的,一个进程发出receive原语后,也有两种可能:
1.若一个消息在此之前已被发送,则该消息被接收并继续执行。
2.若没有正等待的消息,则该进程被阻塞直到所等待的消息到达,或该进程继续执行,放弃接收的努力
因此,发送者和接收者都可阻塞或补阻塞,通常有三种组合,但任何一个特定系统通常有三种组合,但任何一个特定系统通常只实现一种或两种组合:
阻塞send,阻塞receive:发送和接收者都被阻塞,直到完成信息的传递,这种情况有时也称为会和,它考虑到了进程间的紧密同步
无阻塞send,阻塞receive:尽管发送者可以继续,但接收者会被阻塞直到请求的消息到达。这可能是最有用的一种组合,它允许一个进程给各个目标进程尽快的发送一条或多条消息
无阻塞send,无阻塞receive:不要求任何一方等待
对大多数并发进程设计任务来说,无阻塞send是最自然的,例如,无阻塞send用于请求一个输出操作,它允许请求进程以消息的形式发出请求,然后继续,但是这样有一个潜在的危险:错误会导致进程重复产生消息。这可能会消耗系统资源,包括处理器时间和缓冲区空间,进而损害其他进程和操作系统。同时这增加了程序员的负担,由于必须确定消息是否收到,因而进程必须使用应答消息,以证实收到了消息
对大多数并发程序设计任务来说,阻塞receive原语是最自然的。通常,请求一个消息的进程都需要这个期望的信息才能继续执行下去,但若消息丢失,或者一个进程在发送预期的消息之前失败,则接收进程会无限期地阻塞,这个问题可以使用无阻塞receive来解决,但该方法的危险是,如果消息在一个进程已执行与之匹配的receive之后发送,则该消息将被丢失,其他可能的方法是允许一个进程在发出receive之前检测是否有消息正在等待,或允许进程在receive原语中确定多个源进程
寻址
显然,在send原语中确定哪个进程接收消息很有必要,大多数实现允许接收进程指明消息的来源
在send和receive原语中确定目标或源进程的方案分为两类:直接寻址和间接寻址,send原语包含目标进程的标识号,而reveive原语有两种处理方式。一种是要求进程显式地指定源程序,因此该进程必须事先指定希望来自哪个进程的消息,这种方式对于处理并发进程间的合作非常有效,另一种是不可能指定所期望的源进程,例如打印机,这类应用使用隐式寻址更为有效
另一种常用的方法是间接寻址。此时,消息不直接从发送者到接收者,而是发送到一个共享数据结构,该结构有临时保存消息的队列组成,这些队列通常称为信箱,两个通信进程中,一个进程给合适的信箱发送消息,另一个进程从信箱中获得这些信息
间接寻址通过解除发送者和接收者之间的耦合关系,可更灵活的使用消息,它们之间的关系可以是一对一,多对一,一对多或多对多。一对一,一对一关系允许在两个进程间建立专用的通信链路,隔离它们间的交互,多对一关系对客户-服务器间的交互非常有用,一个进程给许多其他程序提供服务,这是信箱称为一个端口;一对多关系适用于一个发送者和多个接收者,它对于一组进程间广播一条消息或某些信箱的应用程序非常有用。多对多关系可让多个服务进程对多个客户进程提供服务
进程和信箱的关联可以是静态的,也可以是动态的。端口常常静态的关联到一个特定的进程上,也就是说,端口被永远地创建并指定到该进程当有很多发送者时,发送者和信箱的关联可以是动态的
一个相关的问题是信箱的所有权问题,对于端口来说由接收进程创建,因此,撤销一个进程时候,其端口也会随之销毁,对于通用的信箱,操作系统可提供一个创建信箱的服务,这样信箱就可视为由创建它的进程所有,这时它们也同该进程一起终止;或视为操作系统所有,这时销毁信箱就需要一个显式命令
消息格式
消息的格式取决于消息机制的目标,以及该机制是运行在一台计算机上还是运行在分布式系统中,对某些操作系统而言,设计者会优先选用定长的短消息来减小处理的存储和开销,需要传递大量数据时,可将数据放到一个文件中,然后让消息引用该文件,更为灵活的一种方法时使用变长消息
图片这里给出了操作系统支持的变长消息的典型格式,分为两部分:一部分是包含相关信箱的消息头和包含实际内容的消息体
排队原则
最简单的排队原则是先进先出,但是仅有这一原则是不够的,一个替代原则是允许指定消息的优先级,即根据消息的类型来指定或由发送者指定;另一个替代原则是允许接收者检查消息队列并选择下一次接收哪个消息
互斥
图中给出了用于实施互斥的消息传递方式,假设使用阻塞receive原语和无阻塞send原语,且一组并发进程共享信箱box,该信箱可供所有进程在发送和接收消息时使用,并初始化为一个无内容的消息,希望进入临界区的进程首先试图接收一条消息,若信箱为空,则阻塞该进程
,一旦进程获得消息,它就执行其临界区,然后把消息放回信箱,因此,消息函数可视为在进程之间传递的一个令牌
上面的解决方案设有多个进程并发地执行接收操作,因此:
若有一条消息,则它仅传递给一个进程,而其他进程被阻塞
若消息队列为空,则所有进程被阻塞,一条消息可用时,仅激活一个阻塞进程,并得到这条消息
图中给出了解决有界缓冲区生产者/消费者问题的另一种方法,它利用了消息传递的能力,除了传递信号外,它还传递数据,它使用了两个信箱,当生产者产生数据后,数据将作为消息发送到信箱mayconsume,只要改信箱中有一条消息,消费者就可以开始消费,缓冲区的大小由全局变量capacity确定,信箱mayproduce最初填满空消息,空消息的数量等于信箱的容量,每次生产使得mayproduce中的消息数减少,每次消费使得mayproduce中的消息数增多
这种方法非常灵活,可以由多个生产者和消费者,只要它们都访问这两个信箱即可
5.6 读者/写者问题
这个问题的定义如下:存在一个多个进程共享的数据区,该数据区可以时一个文件或一块内存空间,甚至可以是一组寄存器;有些进程只读取这个数据区中的数据。此外,还必须满足一下条件:
1.任意数量的读进程可同时读这个文件
2.一次只有一个写进程可以写文件
3.若一个写进程正在写文件,则禁止任何读进程读文件
生产者/消费者问题不能视为特殊读者/写者问题,答案是不能,生产者不仅要写,它还要读队列指针,同样的,消费者不仅读,还要调整队列指针(也就是写),现在分析读者/写者问题的两种解决方案
读者优先
这里给出了使用信号量的一种解决方案,它给出了一个读进程和一个写进程的实例,该方案无需修改就可用于多个读进程和写进程的情况,写进程非常简单,信号量wsem用于实施互斥。读进程也使用wsem实施互斥,但为了允许多个读进程,没有读进程正在读时,第一个试图读的读进程需要在wsem上等待,当至少已有一个读进程在读时,随和的读进程无需等待,可以直接进入,readcount用于记录读进程的数量,信号量x用于确保readcount被正确地更新
写者优先
在前面的解决方案中,读进程具有优先权,但是写进程有可能处于饥饿状态
对于读进程,还需要一个额外的信号量。在resm上不允许建造长队列,否则写进程无法跳过这个队列,因此只允许一个读进程在resm上排队,而所有其他读进程在等待rsem前,在信号量z上排队
图中给出了另一种解决方案,这种方案赋予写进程优先权,并通过消息传递来实现,这种情况下,有一个访问共享数据区的控制进程,其他要访问这个数据区的进程给控制进程发送请求消息,若请求得到同意,则会收到应答消息“OK”,并通过“finished”消息表示访问完成,控制进程备有三个信箱,每个信箱存放一种它可能接收到的消息
要赋予写进程优先权,进程控制就要线服务于写进程,后服务于读请求消息,此外,必须要实施互斥,要实现互斥,需要使用变量count,它被初始化为一个大于可能的读进程数的最大值,在该例中,我们取其值为100,控制器的动作可总结如下:
若count>0,则无读进程正在等待,可以有也可能没有活动的读进程
若count=0,则唯一未解决的请求时写请求,允许该写进程继续执行并等待“finished”消息
若count<0,则一个写进程已发出一条请求,且正在等待消除所有活动的读进程
5.7 小结
现代操作系统的核心时多道程序设计,多处理器和分布式处理器,这些方案和操作系统设计技术的基础都是并发。当多个进程并发执行时,不论是在多处理器系统的情况下,还是在单处理器多道程序系统中,都会出现冲突和合作问题
并发进程可按多种方式进行交互,互斥指的是,对一组并发进程,一次只有一个进程能够访问给定的资源或执行给定的功能
支持互斥的第二种方法要使用专用机器指令,这种方法能降低开销,但由于使用了忙等待,效率较低
支持互斥的另一种方法是在操作系统中提供相应的功能,其中最常见的两种技术是信号量和消息机制,信号量用于在进程间发信号,能很容易地实施一个互斥协议,消息对实施互斥很有用,还为进程间的通信提供了一种有效的方法
第六章 并发:死锁和饥饿
6.1 死锁原理
死锁定义为一组相互竞争系统资源或进行通信的进程间的“永久”阻塞。当一组进程中的每个进程都在等待某个事件,而仅有这组进程中被阻塞的其他进程才可触发该事件,就称这组进程发生了死锁。死锁是永久性的,并无有效的通用解决方案
下面描述设计进程和计算机资源的死锁。称为联合进程图
假设P不同时需要两个资源,则两个进程有下面的形式:
不论两个进程的相对时间安排如何,总不会发生死锁
可重用资源
资源通常分为两类:可重用资源和可消耗资源,可重用资源是指一次仅供一个进程安全使用且不因使用而耗尽的资源,使用后会释放这些单元供其他进程再次使用,比如内存和外存
如果交替执行这两个进程时会发生死锁,如下所示:
这看起来更像程序设计错误而非操作系统设计人员的问题,由于并发程序设计非常具有挑战性,因此这类死锁的确会发生,而发生的原因通常隐藏在复杂的程序逻辑中,因此检测非常困难,处理这类死锁的一个策略是,给系统设计施加关于资源请求顺序的约束
可重用资源死锁的另一个例子是内存请求,假设可用的分配空间为200KB,如下所示
若事先并不知道请求的存储空间总量,则很难通过系统设计约束来处理这类死锁,解决这类特殊问题的最好办法是,使用虚存有效地消除这种可能性
可消耗资源
生产进程创造资源,消费进程得到资源后资源不再存在
考虑下面的进程对,其中的每个进程都试图从另一个进程接收消息,然后再给那个进程发送一条消息
这种死锁错误比较微妙,因此难以发现,此外,罕见的事件组合也会导致死锁,因此只有当程序使用了相当长的一段时间甚至几年后才可能出现这类问题
不存在解决所有类型死锁的有效策略,下图的表中概况了已有方法中最重要的那些方法的要素:检测,预防和避免
资源分配图
资源分配图是有向图,它说明了系统资源的进程和状态,其中每个资源和进程用节点表示
图中从可消耗资源节点中的点到一个进程的边表示进程是资源生产者,图中c是一个死锁的例子,两个进程同时请求对方已经占用的资源会导致死锁,而d不会死锁,因为有多个资源可供分配
这种情况也会导致死锁,这种不是两个进程彼此拥有对方所需资源的简单情况,而是存在进程和资源的还,因此导致了死锁
死锁的条件
死锁有三个必要条件:
1.互斥。一次只有一个进程可以使用该资源
2.占用且等待。当一个进程等待其他进程时,继续占用已分配的资源
3.不可抢占。不能强行抢占进程已有的资源
在很多情况下,这些条件很容易满足,但是这三个条件都只是死锁存在的必要条件而非充分条件,要产生死锁,还需要第四个条件:
4.循环等待。存在一个闭合的进程链,每个进程至少占有此链条中下一个进程所需的一个资源,如上面的两个死锁例子
第四个条件实际上是前三个条件的潜在结果,即假设前三个条件存在,那么可能发生的一系列事件会导致不可解的循环等待,因此,这四个条件一起构成了死锁的充分必要条件
总结如下:
处理死锁的方法有三种。一种是采用某种策略消除条件1-4中的某个条件的出现,来预防死锁,二是基于资源分配的当前状态来做动态选择来避免死锁,三是试图检测死锁的存在并从死锁中恢复。下面按序讨论每种方法
6.2 死锁预防
简单地讲,死锁预防策略是试图设计一种系统来排除发生死锁的可能性。死锁预防方法分为两类:一类是间接死锁预防方法,即防止前面列出的三个必要条件中的任何一个条件的发生;另一类是直接死锁预防方法,即防止循环等待的发生,下面具体分析这四个条件
互斥
在列出的4个条件中,第一个条件不可能禁止,如果需要对资源进行互斥访问,那么操作系统就必须支持互斥
占有且等待
为了预防这个条件,可以要求进程一次性地请求所有需要的资源,并阻塞这个进程直到所有请求都同时满足,这种方法有两个方面的低效性。首先,一个进程可能被阻塞很长时间,为了满足其资源请求。而实际上,只要有一部分资源,它就可以继续至少。其次,分配给一个进程的资源可能会在相当长的一段时间不会被该进程使用,且不能被其他进程使用。另一个问题是一个进程可能事先并不知道它所需要的所有资源
不可抢占
预防不可抢占的方法有几种,首先,占用某些资源的一个进程进一步申请资源时若被拒绝,则该进程必须释放其最初占有的资源,必要时可再次申请这些资源和其他资源。其次,一个进程请求当前被另一个进程占有的一个资源时,操作系统可以抢占另一个进程,要求它释放资源。只有在任意两个进程的优先级都不同的条件下,后一种方案才能预防死锁
循环等待
循环等待条件可通过定义资源类型的线性顺序来预防。若一个进程已分配了R类型的资源,则其接下来请求的资源只能是排在R类型之后的资源。
位证明这种策略的准确性,我们给每种资源类型指定一个下标。当i<j时,资源Ri排在Rj前面。那么进程B获得Rj时,就不能请求Ri,因为i<j。
循环等待的预防方法可能是低效的,因此它会使进程执行速度变慢,且可能在没有必要的情况下拒绝资源访问
6.3死锁避免
解决死锁问题的另一种方法是死锁避免。它和死锁预防的差别很小。在死锁预防中,约束资源请求至少可破坏四个死锁条件中的一个条件,但会导致低效的资源使用和低效的进程执行。死锁避免则相反,它允许三个必要条件,但通过明智地选择,可确保永远不会到达死锁点,因此死锁避免和死锁预防相比,可允许更多的并发
这里给出了两种死锁避免方法:
若一个进程的请求会导致死锁,则不启动该进程
若一个进程增加的资源请求会导致死锁,则不允许这一资源分配
进程启动拒绝
考虑一个有着n个进程和m种不同类型资源的系统。定义以下向量和矩阵:
那我们就可以定义一个死锁避免策略:若一个新进程的资源需求会导致死锁,则拒绝启动这个新进程。仅当满足下面的的式子时,才启动新进程
也就是说,只有满足所有当前进程的最大请求量及新的进程请求时,才会启动该进程。这个策略不是最优的,因为它假设了最坏的情况:所有进程同时发出它们的最大请求
资源分配拒绝
资源分配拒绝策略,又称为银行家算法。首先需要定义状态和安全状态的概念。考虑一个系统,它又固定数量的进程和固定数量的资源,任何时候一个进程可能分配到零个或多个资源。系统的状态时当前给进程分配的资源情况。安全状态指至少有一个资源分配序列不会导致死锁,不安全状态指非安全的一个状态
该策略能确保系统中的进程和资源总处于安全状态。进程请求一组资源时,假设同意该请求,因此改变了系统的状态,然后确定结果是否仍处于安全状态。如果是,同意这个请求,如果不是,阻塞该进程直到同意该请求后系统状态仍然是安全的
死锁避免策略并不能确切地预测死锁,它仅是预料死锁的可能性并确保永远不会出现这种可能性
死锁避免的优点是,无须死锁预防中的抢占和回滚进程,且与死锁预防相比限制较少。但是,它在使用中也有许多限制:
必须事先声明每个进程请求的最大资源。
所讨论的进程必须是无关的,即它们的执行顺序必须没有任何同步要求的限制。
分配的资源数量必须是固定的。
在占有资源时,进程不能退出
6.4 死锁检测
死锁预防策略非常保守,它们通过限制访问资源和在进程上强加约束来解决死锁问题。死锁检测策略则完全相反,它不限制资源访问或约束进程行为。对于死锁检测来说,只要有可能,就会给进程分配器所请求的资源。
死锁检测算法
死锁检测可以频繁地在每个资源请求发生时进行,也可进行得少一些,具体取决于死锁的可能性。在每次请求资源时检查死锁有两个优点:可以尽早地检测死锁情况;算法相对比较简单,然而这种频繁的检测会耗费相当多的处理器时间
死锁检测使用了上节定义的Allocation矩阵和Availabel向量,此外还定义了一个请求矩阵Q,表示需要的资源。这个算法主要是一个标记未死锁进程的过程,最初,所有进程都是未标记的,然后执行下列步骤:
当且仅当这个算法的最终结果有未标记的进程时,才存在死锁,下面举个例子
由于P4没有分配到资源,标记P4
令W=(0 0 0 0 1)
进程P3的请求小于等于W,因此标记P3,并令W=W+(0 0 0 1 0)=(0 0 0 1 1),就相对于给能够执行进程资源,进程执行完后释放资源,然后二者相加,算出总的可用资源数量
不存在其他能够执行的进程,因为P1和P2要使用R3,R2,而我们的W没有相关的资源
算法的结果时P1和P2未标记,表明这两个进程是死锁的
恢复
检测到死锁后,就需要某种策略来恢复死锁。下面按复杂度递增的顺序列出可能的方法:
1.取消所有的死锁进程。这是操作系统中最常采用的方法
2.把每个死锁进程回滚到前面定义的某些检查点,并重新启动所有进程。此时,要求在系统中构建回滚和重启机制,这种方法的风险是原来的死锁可能再次发生。但是并发进程的不确定性通常能保证不会发生这种情况
3.连续取消死锁进程直到不再存在死锁。所选取消进程的顺序应基于某种最小代价原则
4.连续抢占资源直到不再存在死锁,这同样需要使用一种基于代价的选择方法
对于3,4,选择原则因采用如下之一:
目前为止消耗的处理器时间最少
目前为止产生的输出最少
预计剩下的时间最长
目前为止分配的资源总量最少
优先级最低
6.5 一种综合的死锁策略
解决死锁的所有策略都各有优缺点,与其将操作系统机制设计为只采用其中的一种策略,不如在不同情况下使用不同的策略,下面提出了一种方法:
把资源分成几组不同的资源类
为预防在资源类之间由于循环等待产生死锁,可使用前面定义的线性排序策略
在一个资源类中,使用该类资源最适合的算法
我们考虑如下的资源类:
可交换空间: 进程交换所用外存中的存储块
进程资源: 可分配的设备,如磁带设备和文件
内存: 可按页或按段分配给进程
内部资源: 诸如I/O通道
在每一类中。可采用如下策略:
可交换空间: 要求一次性分配所有请求资源来预防死锁,若直到最大存储需求,则这个策略是合理的,死锁避免也是可能的
进程资源: 对于这类资源,死锁避免策略通常是很有效的,采用资源排序的预防策略也是可能的
内存: 对于内存,基于抢占的预防是最合适的策略。当一个进程被抢占后,它仅被换到外存,释放空间以解决死锁
内部资源: 可用使用基于资源排序的预防策略
6.6 哲学家就餐问题
这个问题是有五个人吃东西,但是吃东西时需要两把叉子,我们需要设计一套算法,算法必须保证互斥(两个人不同同时使用同一把叉子),同时还要避免死锁和饥饿,下面给出解决方案
基于信号量的解决方案
每位哲学家首先拿起左边的叉子,然后拿起右边的叉子,在哲学家吃完后,就把叉子放回到桌子上,这个解决方案会导致死锁,如果五个人同时拿就不行
为避免死锁的危险,我们可以考虑增加一位服务员,他只允许4位哲学家同时进入餐厅,因而保证至少有一位哲学家可以拿到两把叉子
基于管程的解决方案
这种方案定义了一个含有5个条件变量的向量,每把叉子对应一个条件变量,这些条件变量用来标识哲学家等待的叉子可用情况。另外,用一个布尔向量记录每把叉子的可用情况。如果至少有一把叉子不可用,那么哲学家进程就会在条件变量的队列中等待。这可让其他哲学家进程进入管程
6.7 UNIX并发机制
UNIX为进程间的通信和同步提供了各种机制,这里只介绍最重要的几种:
管道,信号量,消息,信号,共享内存
管程,消息和共享内存提供来进程间传送数据的方法,而信号量和信号则用于触发其他进程的行为
管道
UNXI对操作系统开发最重要的贡献之一就是管道,管道时一个环形缓冲区,它允许两个进程以生产者/消费者的模型进行通信。因此,这是一个先进先出队列,由一个进程写,由另一个进程读
管道在创建时获得一个固定大小的字节数。当一个进程试图往管道中写时,如果有足够空间,则立即执行写请求;否则被阻塞。类似地,一个读进程试图读取的字节数多于当前管道中的字节数多于当前管道中的字节数时,被阻塞,否则立即执行读请求
管道分为两类:命名管道和匿名管道。只有具有“血缘”关系的进程才可共享匿名管道,而不相关的进程只能共享命名管道
消息
消息是有类型的一段文本。每个进程都有一个与之相关练的消息队列,其功能类似于信箱
消息发送者指定每个发送的消息的类型,类型可被接收者用作选择的依据。接收者可按先进先出的顺序接收消息,或按类型接收信息。当进程试图给满队列发送信息时会被阻塞。同样的,进程试图从一个空队列读取消息时也会被阻塞
共享内存
共享内存是UNXI所提供的进程间通信手段中速度最快的一种。这是虚存中由多个进程共享的一个公共内存块。进程读写共享内存所用的机器指令,与读写虚存空间的其他部分所用的指令相同。每个进程有一个只读或读写的权限。互斥约束不属于共享内存机制的一部分,但必须由使用共享内存的进程提供
信号量
UNXI的信号量系统调用是对第五章中定义的原语的推广,在这些原语之上可同时进行多个操作,且增量和减量操作的值可用大于1。内核自动完成所有需要的操作,在所有操作完成前,任何其他进程都不能访问该信号量
信号量由如下元素组成:
信号量的当前值
在信号量行操作的最后一个进程的进程ID
等待该信号量的值大于当前值的进程数
等待该信号量的值为零的进程数
与信号量相关联的是阻塞在该信号量上的进程队列
信号量实际上是以集合的形式构建的,一个信号量集合中有一个或多个信号量,系统调用允许同时设置集合中所有信号量的值。此外,系统调用把一系列信号量操作作为参数,每个操作定义在集合中的一个信号量上
这个对信号量的推广为进程的同步与协作提供了很大的灵活性
信号
信号是用于向一个进程通知发生异步事件的机制。信号类似于硬件中断,但没有优先级,即内核公平地对待所有信号。对于同时发生的信号,一次只给进程一个信号,而没有特定的次序
进程间可以互相发送信号,内核也可在内部发送信号,信号的传递是通过修改信号要发送到的进程所对应的进程表中的一个域来完成的。只有进程在被唤醒继续运行时,或进程准备从系统调用中返回时,才处理信号
6.8Linux内核并发机制
Linux包含了其他UNIX系统中出现的所有并发机制,包括管道,消息,共享内存和信号。Linux还支持一种特殊类型的信号–实时信号。
实时信号于标准的UNIX信号相比有三个主要不同点:
支持按优先级顺序排列的信号进行传递
多个信号能进行排队
在标准信号机制中数值和消息只能视为通知,不能发送给目标进程,但实时信号机制可以将数值随信号一起发送过去
此外,Linux还包含了一套丰富的并发机制,这套机制是专门为内核模式线程准备的
原子操作
Linux提供了一组操作来保证对变量的原子操作,原子操作执行时不会被打断或干扰。在单处理器上,线程一旦启动原子操作,则从操作开始到结束的这段时间内,不能中断线程。
Linux中定义了两种原子操作:一种是针对整数变量的整数操作,另一种是针对位图中某一位的位图操作。如下表所示,这些操作在Linux支持的任何计算机体系结构中都必须事先。在某些体系结构中,这些原子操作具有对应的汇编指令。其他体系结构通过锁住内存总线的方式来保证操作的原子性
对于原子整数操作,定义了一个特殊的数据类型,原子整数操作仅能用在这个数据类型上,而其他操作不允许用在这个数据类型上。这会带来一些好处:
1.在某些情况下不受竞争条件保护的变量,不能使用原子操作
2.这种数据类型的变量能够避免被不恰当的非原子操作使用
3.编译器不能错误地优化对该值的访问(比如使用别名而不使用正确的内存地址)
4.这种数据类型的实现隐藏了与计算机体系结构相关的差异
原子位图操作操作由指针变量指定的任意一块内存区域的位序列中的某一位,因此没有和原子整数操作中等同的数据结构
自旋锁
在Linux中保护临界区的常用技术是自旋锁。在同一时刻,只有一个线程能获得自旋锁。其他任何试图获得自旋锁的线程将一直进行尝试,直到获得了该锁。
任何线程进入临界区前都必须检查该整数。若值为0,则线程将该值设置为1,然后进入临界区,若该值非0,则该线程继续检查该值,直到它为0。但这有一个缺点,即锁外面的线程会以忙等待的方式继续执行。
基本自旋锁 基本的自旋锁有如下4个版本:
普通(plain):在临界区代码不被中断处理程序执行或禁用中断的情况下,可以使用普通自旋锁。它不会影响当前处理器的中断状态
irq:中断一直被启用时,可以使用这种自旋锁
irqsave:不知道在执行时间内中断是否启用时,可以使用这个版本。获得锁后,本地处理器的中断状态会被保存,该锁释放时会恢复这一状态
bh:发生中断时,相应的中断处理器只处理最少量的必要工作。一段我们称之为下半部的代码执行中断相关工作的其他部分,因此允许尽快地启用当前的中断。bh自旋锁用来禁用和启用下半部,以避免与临界区冲突
程序员知道需要保护的数据不会被中断处理程序或下半部访问时,使用普通自旋锁,否则就需要使用合适的非普通自旋锁
自旋锁在单处理系统和多处理系统中的实现是不同的。对于单处理器系统,必须考虑如下因素:是否关闭内核抢占功能
读写自旋锁
读写自旋锁机制允许在内核中达到比基本自旋锁更高的并发度。这个自旋锁允许多个线程同时以只读的方式访问同一数据结构,只有当一个线程想要更新数据结构时,才会互斥地访问该自旋锁。
相对于写者而言,读写自旋锁对于读者更有利,只要至少存在一个读者拥有该锁,写者就不能抢占该锁
信号量
Linux在用户级提供了和UNIX对应的信号量接口。在内核内部,Linux提供了供自身使用的信号量的具体实现,即内核中的代码能够调用内核信号量。 内核的信号量不能通过系统调用直接被用户程序访问。内核信号量是作为内核内部函数实现的,因此比用户可见的信号量更加高效
Linux在内核中提供三种信号量:二元信号量,计数信号量和读写信号量
二元信号量和计数信号量 这个信号量与第五章描述的信号量功能是相同的,down和up分别对应于semWait和semSignal函数
Linux提供了三个版本的down操作。
down操作对应于传统的semWait操作,该函数既可以用于计数信号量上的操作,也可以用于二值信号量上的操作。
down_interruptible函数允许因down操作而被阻塞的线程在此期间接收并响应内核信号。若线程被唤醒,则函数会增加信号量并返回错误代码。这告知线程对信号量操作的调用已取消。
down_trylock函数可在不被阻塞的同时获得信号量。信号量可用时,就可获得它。
读写信号量 它把用户分为读者和写者;它允许多个并发的读者,但仅允许一个写者。对读者使用的是一个计数信号量,对写者使用的是一个二元信号量
6.8.4 屏障
在有些体系结构中,编译器或者处理器硬件为了优化性能,可能会对源代码中的内存访问重新排序。重新排序的目是优化对处理器指令流水线的使用,也包括相应的检查,以便保证不违法数据依赖性。
为了保证指令执行的顺序,Linux提供了内存屏障设施,对于屏障操作,有两点需要注意:
1.屏障与机器指令相关。
2.屏障相关的操作指明编译器和处理器的行为。在编译方面,屏障操作指示编译器在编译期间不要重新排序指令。在处理器方面,屏障操作指示流水线上在屏障前面的任何指令必须必须在屏障后面的指令开始执行之前提交
barrier()操作时mb()操作的一个轻量级版本,它仅控制编译器的行为
对于SMP结构,这些指令定义为我们通常所说的内存屏障;但对于UP结构,它们都仅作为编译器屏障
6.9 Solaris线程同步原语
除UNIX的并发机制外,Solaris还支持4种线程同步原语:
互斥锁,多读者单写者锁,信号量,条件变量
Solaris在内核中为内核线程实现这些原语,同时在线程库中也为用户级线程提供这些原语,下图显示了这些原语的数据结构。
原语的初始化函数填充这些数据结构的一些成员。创建一个同步对象后,实际上只能执行两个操作:进入(获得锁)或释放(解锁)。内核和线程库中没有实施互斥和防止死锁的机制。线程试图访问一块应被保护的数据或代码,但未使用正确的同步原语时,这种访问会发生。线程加锁了一个对象,但在对象解锁时失败时,内核不会采取任何行动。所有同步原语都要求有一个硬件指令来在原子操作中测试和设置对象。
6.9.1 互斥锁
互斥锁用于保护资源。加锁互斥量的线程与解锁互斥量的线程必须是同一个线程。一个线程通过执行原语试图获得一个互斥锁,若不能获得,则根据互斥对象中保存的特定信息来决定阻塞动作。默认的阻塞策略是一个自旋锁。还有一个基于中断的阻塞机制可供选择
6.9.2 信号量
6.9.3 多读者/单写者锁
这个锁允许多个线程以只读权限访问被锁保护的对象,它还允许在排斥所有读线程后,一次有一个线程作为写者访问该对象。写者获得锁的状态为write lock;所有试图读或写的线程都必须等待。一个或多个读线程获得该锁后的状态为read lock。原语如下:
6.9.4 条件变量
条件变量用于等待一个特定的条件为真,它必须和互斥锁联合使用。原语如下:
6.10 Windows 7的并发机制
Windows提供了线程间的同步,并把它作为对象结构中的一部分。最重要的同步方法包括执行体分派器对象,用户模式临界区,轻量级读写锁,条件变量和锁无关操作。分派器对象利用了等待函数。下面先介绍等待函数,随后介绍同步方法
6.10.1 等待函数
等待函数允许线程阻塞其自身的执行。等待函数只有在特定的条件满足后才会返回。等待函数的类型决定了所使用的标准。当等待函数被调用时,它会检查等待的条件是否已满足。如果条件不满足,那么调用的线程就会进入等待状态。在等待条件满足的期间,它不会占用处理器时间。最简单的等待函数类型是在单个对象上等待的函数
当下列条件之一满足时,函数就会返回:
特定对象处于有信号状态
出现了超时,超时间隔可设置为INFINITE,以便指定等待不会超时
6.10.2 分派器对象
windows执行体实现同步的机制时分派器对象族,表中给出了同步对象的简单描述
表中的前5个对象类型主要用来支持同步,而其他对象类型具有其他用途,但也可以用于同步。
每个分派器对象实例既可处于有信号状态,也可处于无信号状态。线程可以阻塞在一个处于无信号状态的对象上,当对象进入有信号状态时线程就会被释放。这种机制非常简单。线程使用同步对象句柄向Windows执行体发出一个等待请求。对象进入有信号状态时,Windows执行体释放一个或全部等待在该分派器对象上的线程对象
事件对象 用于将一个信号发送给线程,表示某个特定事件已发生。
互斥对象 用来确保对资源的互斥访问,同一时间只允许一个线程对象获得访问权,因而互斥对象在功能上和二值信号量类似。当互斥对象进入有对象状态时,只能触发一个在互斥信号上等待的线程。
和互斥对象类似,信号量对象可在多个进程中被线程共享,Windows信号量是一个计数信号量,本质上,可等待的计时器对象会在适当的时间或时间间隔产生信号。
6.10.3 临界区
临界区提供了与互斥对象类似的同步机制,不同的是,临界区只能用在单个进程的线程中。事件对象,互斥对象和信号量对象也能用于单进程的应用程序中,但临界区为互斥同步提供了更快,更高效的机制。
进程负责分配临界区使用的内存区域。进程中的线程在使用它之前要使用相关的函数来初始化临界区。线程使用相关函数来进行对临界区的一些列操作
临界区使用一个复杂但精巧的算法来获取互斥量。如果是多处理器系统,其代码将会试图获取一个自旋锁,如果进程持有临界区的时间很短,那么就很有效。如果在合适的循环次数之后仍不能获得自旋锁,系统将使用一个分派器对象阻塞该进程。分派器对象仅作为万不得已时的手段使用
6.10.4 轻量级读写锁和条件变量
Windows Vista中增加了用户模式的读写锁。与临界区一样,仅当试图使用自旋锁时,读写锁才进入内核进入阻塞。称它为轻量级的原因是,读者写者通常只需要一个指针大小的内存空间。
使用轻量级读写锁时,进程要声明一个相关类型的变量,并调用相关函数对其进行初始化。线程通过调用函数可以获得轻量级读写锁,而调用其他函数可以释放锁
Window Vista中还增加了条件变量,进程必须声明一个相关类型的变量,并且调用函数进行初始化。条件变量能和临界区或轻量级读写锁一起使用,因而有两种调用方法,它们在特定的条件的下睡眠并以原子操作的方式释放特定的锁
有两种唤醒方法,它们分别唤醒一个或所有睡眠的的线程,条件变量的用法如下:
1.获得互斥锁
2.当某一个函数为FALSE时,调用其它函数
3.执行受保护的操作
4.释放该锁
6.10.5 锁无关同步机制
Windows同样严重依赖于内部锁操作来进行同步。内部锁操作使用硬件机制来确保内存中的位置只会由单独的一个原子操作读,写或修改。
许多同步原语的实现中都使用了内部锁操作,但这些内部操作锁同样可供程序开发人员在那些不希望使用软件锁来实现同步的场景中使用。这种优势在于,当一个线程拥有锁时,即使时间片块用完,也可以不从CPU上换出。因此这不会阻塞其它的线程运行
使用内部锁操作可实现更加复杂的锁无关原语
6.11 Android进程间通信
Linux内核包含很多用于进程间通信的机制,但Android系统在IPC中并未用到上述机制,而是在内核中新增了一个连接器。连接器提供了一个轻量级的远程程序调用功能,在内存和事务处理方面非常高效,非常适合嵌入式系统
连接器被用来传递两个进程之间的交互,进程组件发起一个调用,调用传递给连接器,连接器将其传递给目标组件,目标进程返回的结果提供链接器传递给发起调用的进程
通常,RPC是指位于不同机器上的两个进程之间的调用/返回交互,但在Android系统中,RPC机制运行在同一系统上的不同虚拟机中
连接器使用的通信方法是ioctl系统调用,这是一种针对特定设备的多用途系统调用,可以用来接入一些伪设备驱动。ioctl调用包括可执行的参数形式的命令和一些适当的变量
图中展示了使用连接器的典型过程。提供服务的进程会创建多个线程以便能并发处理多个请求,每个线程通过阻塞ioctl来通知服务器,交互过程如下:
1.客户端组件通过带参数的调用来请求服务
2.调用唤醒代理线程,代理把调用转化为连接器驱动中的一个事务,代理实现的过程称为数据编组,它把高层应用数据结构转换为一个邮包,这个邮包是承载消息的容器。
3.连接器向目标线程发送一个信号,将它从ioctl调用阻塞中唤醒,并将邮包交付给目标进程的存根组件
4.存根组件的作用是数据编出,它把从连接器事务中接收的邮包重新组装成最高层的应用数据结构,接着使用与客户端组件发出的完全相同的调用来访问服务组件
5.被调用的服务组件将适当的结果返给存根
6.存根将返回结果打包成一个用来回复的邮包,然后通过ioctl调用提交给连接器
7.连接器唤醒正在进行ioctl调用的客户端代理线程,让其接收事务处理的返回数据
8.代理线程将回复邮包进行解包,并将结果返回给发出服务请求的客户端组件
6.12 小结
死锁是指一组争用系统资源的进程或互相通信的进程被阻塞的现象,这种阻塞是永久性的,除非操作系统采取某些非常规行动。死锁可能涉及可重用资源或可消耗资源。
处理死锁通常有三种方法:预防,检测和避免。预防通过确保不满足死锁的一个必要条件来避免发生死锁。操作系统总是同意资源请求时,需要进行死锁检测。操作系统必须周期地检查死锁,并采取行动打破死锁。死锁避免涉及分析新的资源请求,以确保它是否会导致死锁,且仅当不可能发生死锁时才同意该请求
第七章 内存管理
在单道程序设计系统中,内存划分为两部分:一部分供操作系统使用,另一部分供当前正在执行的程序使用。在多道程序设计系统中,必须在内存中进一步细分出用户部分,以满足多个进程的要求。细分的任务由操作系统动态完成,称为内存管理
有效的内存管理在多道程序设计系统中至关重要,下表介绍了我们要讨论的关键术语
7.1 内存管理的需求
在研究与内存管理相关的各种机制和策略时,清楚内存管理要满足的需求非常有用。内存管理的需求如下:
重定位,逻辑组织,保护,物理组织,共享
7.1.1 重定位
在多道程序设计系统中,可用的内存空间通常被多个进程共享。通常情况下,程序员事先并不知道在某个程序执行期间会有其他哪些程序驻留在内存中。此外,我们还系统提供一个巨大的就绪进程池,以便把活动进程换入或换出内存,进而使处理器的利用率最大化。程序换出到磁盘中后,下次换入时要放到与换出前相同的内存区域中会很困难。我们需要把进程重定位到内存的不同区域
因此,我们事先并不知道程序会放到哪个区域,并且我们必须允许程序通过交换技术在内存中移动。这关系到一些与寻址相关的技术问题。如下图所示,该图描述了一个进程映像,为简单起见,假设该进程映像占据了内存中一段相邻的区域。处理器硬件和操作系统软件比较能以某种方式把程序代码中的内存访问转换为实际的物理内存地址,并反映程序在内存中的当前位置
7.1.2 保护
每个进程都应受到保护,以免被其他进程有意或无意地干扰。因此,该进程以外的其他进程中的程序不能未经授权地访问该进程的内存单元。在某种意义上,满足重定位的需求增大了满足保护需求的难度。由于程序在内存的位置不可预测,因而在编译时不可能检查绝对地址来确保保护。此外,大多数程序设计语言允许在运行时进行地址的动态技术。因此,必须在运行时检查进程产生的所有内存访问,以确保它们只访问分配给该进程的内存空间
通常,用户进程不能访问操作系统的任何部分,不论是程序还是数据。注意,内存保护需求必须由处理器而非操作系统来满足,因而操作系统不能预测程序可能产生的所有内存访问。因此,只能在指令访问内存时来判断这个内存访问是否违法,要实现这一点,处理器硬件必须具有这个能力
7.1.3 共享
任何保护机制都必须具有一定的灵活性,以允许多个进程访问内存的同一部分。合作完成同一个任务的进程可能需要共享访问相同的数据结构。因此,内存管理系统在不损害基本保护的前提下,必须允许对内存共享区域进行受控访问。支持重定位的机制也支持共享
7.1.4 逻辑组织
计算机系统中的内存总是被组织成线性的地址空间,且地址空间由一系列字节或字组成。尽管这种组织方式类似于实际的机器硬件,但不符合程序构造的典型方法。大多数程序被组织成模块,某些模块是不可修改的,某些模块包含可用修改的数据。若操作系统和计算机硬件能够有效地处理以某种模块形式组织的用户程序与数据,则会带来很多好处:
1.可以独立地编写和编译模块,系统在运行时解析从一个模块到其他模块的所有引用
2.通过适度的额外开销,可以为不同的模块提供不同的保护级别(只读,只执行)
3.可以引入某种机制,使得模块可被多个进程共享
最易于满足这些需求的工具是分段,它也是本章将要探讨的一种内存管理技术
7.1.5 物理组织
如第一章所述,计算机存储器至少要组织成两级,即内存和外存。内存提供快速的访问,成本也相当较高。此外,内存是易失性的,即它不能提供永久性存储。外存比内存慢且便宜,且通常是非易失性的。因此,大容量的外存可用于长期存储程序和数据,较小的内存则可用于保存当前使用的程序和数据。
在这种两级方案中,系统主要关注的是内存和外存之间信息流的组织,我们可以让程序员负责组织这一信息流,但由于以下两方面的原因,这种方式是不切实际的:
1.供程序和数据使用的内存可能不足。程序员必须采用覆盖技术来组织程序和数据,覆盖是指不同的模块被分配到内存中的同一块区域,主程序负责在需要时换入或换出模块。即使有编译器的帮助,覆盖还是很浪费程序员时间
2.在多道程序设计环境中,程序员不知道可用空间的大小及位置
所以,在两级存储器间移动信息的任务由系统负责
7.2 内存分区
内存管理的主要操作是处理器把程序装入内存中执行,内存管理涉及一种称为虚存的复杂方案,虚存基于分段和分页两种基本技术。我们在考虑虚存技术前,先考虑不涉及虚存的简单技术,比如分区技术,简单分页和简单分段
7.2.1 固定分区
操作系统占某些固定部分,其余部分给用户使用。管理空间的最简单方案就是对它分区,以形成若干边界固定的区域
分区大小 我们可以以每个区大小相等来分区,也可以以每个区大小不等来分区。若分区已满并且没有进程处于就绪态或运行态,则操作系统就可以换出一个进程的所有分区,并装入另一个进程,使得处理器有事可做
使用大小相等的固定分区有两个难点:
- 程序可能太大导致不能放到一个分区中。此时,程序员必须使用覆盖技术设计程序,使得在任何时候该程序只有一部分需要放到内存中。当需要的模块不在时,用户程序必须把这个模块装入程序的分区,覆盖该分区中的任何程序和数据
- 内存的利用率很低。任何程序,即使很小,也需要占据一个完整的分区,假设存在一个长度小于2MB的程序,当它被换入时,仍占据一个8MB的分区。这种存在导致的内存空间浪费,这种现象称为内部碎片
使用大小不等的分区可缓解这两个问题,但不能完全解决这两个问题,但是这会使得内部碎片更少
放置算法
对于大小相等的分区策略,进程在内存中的放置非常简单,只要存在可用的分区,进程就能装入分区,对于大小不等的分区策略,把进程分配到分区有两种方法,最简单的方法就是把每个进程分配到能够容纳它的最小分区中(这里假定知道一个进程最多需要的内存大小)。在这种情况下,每个分区需要维护一个调度队列,用于保存换出的进程。这种方法的优点是可使每个分区内部浪费的空间最少
尽管从单个分区的角度来看这种技术是最优的,但从整个系统来看却不是最佳的,假设有一个空的16MB分区,但是却没有12-16MB之间的进程,即使有更小的进程可以分配,但16MB的分区仍会保持闲置,所以一种更可取的方法是为所有进程只提供一个队列,当需要把一个进程装入内存时,选择可以容纳该进程的最小可用分区
但是不固定大小的分区也存在以下缺点: - 分区的数量在系统生成阶段就已经缺点,因而限制了进程的数量
- 由于分区的大小是在系统生成阶段事先设置的,因而小作业不能有效地利用分区空间。在事先知道所有作业的大小情况下,这种方法也许是合理的,但大多数情况下这种技术非常低效
目前几乎没有场合固定分区方法
7.2.2 动态分区
为了克服固定分区的缺点,人们提供了一种动态分区的方法,在现代,这种方法已被很多更先进的内存管理技术所取代。使用这种技术的一个重要操作系统是IBM主机操作系统。
对于动态分区,分区长度和数量是可变的。进程装入内存时,系统会给它分配一块与其所需容量完全相等的内存空间,比如下面图片这个例子,一开始它使用64MB的内存。我们可以发现当进程越来越多的时候,空洞也就越来越多了,这是因为当有空洞时不管它大小如何也会将其当作一个分区,这样做的结果就是分区越来越多,分区的容量越来越小,内存利用率也就下降。这种现象称为外部碎片
克服外部碎片的一种技术是压缩:操作系统不时地移动进程,使得进程占用的空间连续,并使所有空闲空间连成一片,也就是压缩多个连续小空间成一个大空间,但是这种技术的困难在于其花费的时间太多了,并且会浪费处理器时间,另外,压缩需要动态重定位的能力,也就是说,必须能够把程序从内存的一块区域移动到另一块区域,且不会使程序中的内存访问无效
放置算法
由于内存压缩非常费时,因而操作系统需要巧妙地把进程分配到内存中,以便盖住空洞,如果内存有多个足够大的空闲块,那么操作系统必须确定要为此进程分配哪个空闲块
可供考虑的放置算法有三种:最佳适配,首次适配和下次适配。这三种算法都在内存中选择大于等于进程的空闲块。差别在于:最佳适配选择与要求大小最接近的块;首次适配从头开始扫描内存,选择大小足够的第一个可用块;下次适配从上一次放置的位置开始扫描,选择下一个大小足够的块
下图的例子给出了各种放置的示例
各种方法的好坏取决于发生进程交换的次序及这些进程的大小。但是我们依然可以得出一些一般性结论。首次适配算法不仅是最简单的,而且通常也是最好的和最快的。下次适配算法通常比首次适配差,且常常会在内存的末尾分配空间,导致通常位于存储空间末尾的最大空闲存储块很快分裂为小碎片。另一方面,首次适配算法会使得内存的前端出现很多小空闲分区。最佳适配算法尽管被称为最佳,但通常性能却是最差的
置换算法 在使用动态分区的多道程序设计系统中,有时会出现内存中的所有进程都处于阻塞态的情况下。为避免时间浪费,操作系统将把一个阻塞的进程换出内存。因此,操作系统必须选择要替换哪个进程。置换算法的细节在后续讨论
7.2.3 伙伴系统
固定分区和动态分区方案都有缺陷。固定分区方案限制了活动进程的数量,动态分区的维护特别复杂,并且会引入进行压缩的额外开销。更有吸引力的一种折中方案是伙伴系统
折中方案我们可以这样描述,假设空间有64MB,如果有进程大于32MB(也就是64MB的一半),那么就将空间全部给进程,如果小于32MB但大于16MB,我们就将64MB分为两个大小相等的32MB的伙伴,其中一个伙伴给进程使用,如果进程小于16MB大于8MB,那么就继续分,将32MB的伙伴分为两个16MB的伙伴,其中一个给进程,剩下的以此类推,这样做肯定也会产生空洞。
在任何时候,伙伴系统中为所有大小为2的倍数的空洞维护一个列表,空洞可通过对半分裂i+1列表中移除,并在i列表中产生两个大小为2的i次方的伙伴,也就是一分二,也可以合成,比如两个16MB的连续空洞可以合成一个32MB的伙伴,二合一
下图给出了一个初始大小为1MB的块的例子
下图显示了释放B的请求后,伙伴系统分配情况的二叉树。叶节点表示内存中的当前分区。若两个伙伴都是叶节点,则至少需分配出去一个,否则它们将合并为一个更大的块
伙伴系统是较为合理的折中方案,它克服了固定分区和可变分区方案的缺陷。但在当前的操作系统中,基于分页和分段机制的虚存更为先进。然而伙伴系统在并行系统中有很多应用。UNIX内核存储分配中使用了一种经过改进后的伙伴系统
7.2.4 重定位
在考虑解决分区技术的缺陷之前,必须先解决在内存中放置进程的一个遗留问题。使用上图中的固定分区方案时,一个进程总可以指定到同一个分区。也就是说不论进程装入时选择哪个分区,都是选择使用指定的同一分区。在这种情况下,需要使用一个简单重定位加载器:首次加载一个进程时,代码中的相对内存访问被绝对内存地址代替
在大小相等的分区及只有一个进程队列的大小不等的分区的情况下,一个进程在其生命周期中可能占据不同的分区。此外,使用压缩时,内存中的进程也可能会发生移动。因此,进程访问的位置不是固定的。为了解决这个这个问题,需要区分几种地址类型。逻辑地址是指与当前数据在内存中的物理分配地址无关的地址,在进行访问需要转化成物理地址。相对地址是逻辑地址的一个特例,它是相对于某些已知点的存储单元。物理地址或决定地址是数据在内存中的实际位置
系统采用运行时动态加载的方式把相对地址的程序加载到内存。通常情况下,被加载进程中的所有内存访问都相对于程序的开始点。所以需要能够转化相对地址的硬件机制
图中给出了实现地址转化的一种典型方法
进程处于运行态时,有一个特殊处理器寄存器(基址寄存器),其内容是程序在内存中的起始位置。还有一个界限寄存器指明程序的终止位置,当程序被装入内存或当该进程的映像被换入时,必须设置这两个寄存器。
在进程的执行过程中会遇到相对地址,包括指令寄存器的内容,跳转或调用指令中的指令地址,以及加载和存储指令中的数据地址。每个这样的相对地址都经过处理器的两步操作。首先,基址寄存器中的值加上相对地址产生一个绝对地址;然后绝对地址与界限寄存器进行比较,如果在范围内就继续该指令的执行;否则,向操作系统发出一个中断信号。操作系统对其进行响应
7.3 分页
大小不等的固定分区和大小可变的分区技术在内存的使用上都是低效的,前者会产生内部碎片,后者会产生外部碎片。但是,如果内存被划分为大小固定,相等的块,且块相对比较小,每个进程也被分成同样大小的小块,那么进程中称为页的块可以分配到内存中称为页框的可用块。使用分页技术时,只有内部碎片,没有任何外部碎片
下图展示了页和页框的用法
如果没有足够的连续页框来保存进程D,也不会阻止操作系统加载该进程。只有空间足够,尽管不连续也可以装入,就如上图所示那样,但是我们要找到进程对应的页框,一个简单的基址寄存器是不够的,操作系统需要为每个进程维护一个页表。页表给出了该进程的每页所对应页框的位置。在分页中,给出逻辑地址后,处理器使用页表产生物理地址(页框号,偏移量)
下图给出了各个进程的页表,进程的每页在页表中都有一项,因此页表很容易按页号对进程的所有页进行索引。此外,操作系统为当前内存中未被占用,可供使用的所有页框维护一个空间页框列表
由此可见,前述的简单分页类似于固定分区,它们的不同之处在于:采用分页技术的分区相当小,一个程序可以占据多个分区,并且这些分区不需要是连续的
为了使分页方案更加方便,规定页和页框的大小必须是2的幂,以便容易地表示出相当地址。下图给出分区,分页,分段的不同情况下的寻址方式
使用页大小为2的幂的页的结果是双重的。首先,逻辑地址方案对编程者,汇编器和链接是透明的。程序的每个逻辑地址与其相对地址是一致的。其次,用硬件实现运行时动态地址转换的功能相对比较容易
在前面的例子中,逻辑地址为000001 0111011110,其页号为1,偏移量为478。假设该页驻留在内存页框6中,则物理地址页框号为6,偏移量为478,物理地址为0001100111011110,如下图所示
总之,采用简单的分页技术,内存可分成许多大小相等且很小的页框,每个进程可划分成同样大小的页;较小的进程需要较少的页,较大的进程需要较多的页;装入一个进程时,其所有页都装入可用页框中,并建立一个页表。这种方法解决了分区技术存在的许多问题
7.4 分段
细分用户程序的另一种可选方案是分段。采用分段技术,可以把程序和与其相关的数据划分到几个段中。尽管段有最大长度限制,但并不要求所有程序的所有段的长度都相等。和分页一样,采用分段技术时逻辑地址也由两部分组成;段号和偏移量
由于使用大小不等的段,分段类似于动态分区。与动态分区不同的是,在分段方案中,一个程序可以占据多个分区,并且这些分区不要求是连续的。分段消除了内部碎片,但是和动态分区一样,它会产生外部碎片。不过由于进程被分成多个小块,因此外部碎片也会很小
分页对程序员来说是透明的,而分段通常是可见的,并且作为组织程序和数据的一种方便手段提供给程序员。一般情况下,程序员或编译器会把程序和数据指定到不同的段。为了实现模块化程序设计的目的,程序或数据可能会进一步分成多个段。这种方法最不方便的地方是,程序员必须清楚段的最大长度限制
采用大小不等的段的另一个结果是,逻辑地址和物理地址间不再是简单的对应关系,类似于分页,在简单的分段方案中,每个进程都有一个段表,系统也会维护一个内存中的空闲块列表。每个段表项必须给出相应段在内存中的起始地址,还必须指明段的长度。当进程进入运行状态时,系统会把其段表的地址装载到一个寄存器中,由内存管理硬件来使用这个寄存器。
当我们找到对应的段后,找到对应的基地址并且将其与偏移量相加得到我们最后的物理地址
总之,采用简单的分段技术,进程可划分为许多段,段的大小无须相等;调入一个进程时,其所有段都装入内存的可用区域,并建立一个段表
7.5 小结
内存管理时操作系统中最重要,最复杂的任务之一。内存管理把内存视为一个资源,它可以分配给多个活动进程,或由多个活动进程共享。为有效地使用处理器和I/O设备,需要在内存中保留尽可能多的进程。此外,程序员在开发程序时最好能不受程序大小的限制
内存管理的基本工具是分页和分段。采用分页技术,每个进程被划分为相对较小的,大小固定的页。采用分段技术可以使用大小不同的块。在单独的内存管理方案中,还可结合使用分页技术和分段技术
第八章 虚拟内存
先给出一些与虚拟内存相关的定义
8.1 硬件和控制结构
通过对简单分页,简单分段与固定分区,动态分区等方式进行比较,一方面可了解二者的区别,另一方面可了解内存管理方面的根本性突破。分页和分段的两个特点是取得这种突破的关键:
1.进程中的所有内存访问都是逻辑地址,这些逻辑地址会在运行时动态地转换为物理地址,这意味着进程可在执行过程的不同时刻占据内存中的不同区域
2.一个进程可划分为许多块,在执行过程中,这些块不需要连续地位于内存中。动态运行时地址转换和页表或段表的使用使得这一点成为可能
下面介绍这种突破。如果前两个特点存在,那么在一个进程的执行过程中,该进程不需要所有页或所有段都在内存中。如果内存中保存有待取的下一条指令的所在块及待访问的下一个数据单元的所在块,那么执行至少可以暂时继续下去
现在考虑如何实现这一点。用术语“块”来表示页或段,取决于采用的是哪种基址。进程执行的任何时候都在内存的部分称为进程的常驻集。只要所有内存访问都是访问常驻集中,执行就可以顺利进行;使用段表或页表,处理器总可以确定是否如此。
处理器需要访问一个不在内存中的逻辑地址时,会产生一个中断,表明出现了内存访问故障。操作系统会把被中断的进程置于阻塞态。要想继续执行,就必需把引发故障的进程块读入内存。为此,操作系统产生一个磁盘I/O读请求。产生请求后,在执行磁盘期间,操作系统可以调度另一个进程运行。需要的块读入内存后,产生一个I/O中断,控制权交回给操作系统,而操作系统把阻塞的进程置为就绪态
但是我们使用中断可能会使得我们的效率不高。提供系统利用率的实现方法有如下两种,其中第二种的效果与第一种相比更令人吃惊:
1.在内存中保留多个进程。 由于对任何特定的进程都仅装入它的某些块,因此有足够的空间来放置更多进程,并且这会使得在任何时刻这些进程中至少有一个处于就绪态,于是处理器就得到了更有效的利用
2.进程可以比内存的全部空间还大 在这种情况下,我们可以编写比内存空间还大的程序,对程序员而言,所处理的是一个巨大的内存,程序大小与磁盘存储器相关。操作系统在需要时会自动地把进程块装入内存
由于进程只能在内存中执行,因此这个存储器称为实存储器,简称实存。但我们感觉到的是一个更大的内存,且通常分配在磁盘上,这称为虚拟内存,简称虚存。虚存支持更有效的系统并发度,并能解除用户与内存之间没有必要的紧密约束,下表总结了使用和不使用虚存情况下分页和分段的特点
8.1.1 局部性和虚拟内存
考虑一个由很长的程序和多个数据数组组成的大进程。在任何一段很短的时间内,执行可能会局限在很小的一段程序中,且可能仅会访问一个或两个数据数组。因此若加载进内存的进程只被使用了一部分进程块,那么就会带来巨大的浪费。仅装入这一小部分块可更好地使用内存。然后,若程序访问到不在内存中的数据,那么就会引发错误,告诉操作系统读取所需的块
因此,在任何时刻,任何一个进程只有一部分块位于内存中,因此可在内存中保留更多的进程并且同时也节省了时间,但是这要求操作系统必须很聪明的管理这个方案。当操作系统读取一个块时,它必须把另一块换出,如果一块正好在将要使用前换出,操作系统又必须很快将它取回。这类操作通常会导致一种称为系统抖动的情况:即处理器的大部分时间都用于交换块而非执行指令。
这类推断基于局部性原理,局部性原理描述了一个进程中程序和数据引用的集合倾向。同时,还可以对将来可能会访问的块进行猜测
局部性原理表明虚拟内存的方案是可行的。要使虚存比较实用并且有效,需要两方面的因素。首先必须有对所采用分页或分段的硬件支持;其次,操作系统必须徐有管理页或短在内存和辅助存储器之间移动的软件
8.1.2 分页
虚拟内存使得页表项更加复杂,因而每个页表项需要有一个位(P)来表示它所对应的页当前是否在内存中,若在内存中,则页还包括该页的页框号,如下图所示
页表项中所需要的另一个控制位是修改位,它表示相应页的内容从上次装入内存到现在是否已经改变
页表结构
虚拟地址又称为逻辑地址,它由页号和偏移量组成,而物理地址由页框号和偏移量组成。由于页表的长度可基于进程的长度而变化,因而不能期望再寄存器中保存它,它须在内存中且可以访问,下图给出了一种硬件实现
在大多数系统中,每个进程都有一个页表。每个进程可以占据大量的虚存空间,例如,假设每个进程的虚存空间可达2的31次方,若使用2的9次方字节的页,则意味着每个进行需要有2的22次方个页表项。显然,采用这种方法来放置页表的内存空间太大。为克服这个问题,大多数虚拟内存方案都在虚存而非实存中保存页表。这意味着页表和其他页一样都服从分页管理。一个进程正在运行时,它的页表至少有一部分在内存中,这一部分包括正在运行的页的页表项。一些处理器使用两级方案来组织大型页表。在这类方案中有一个页目录,其中的每项指向一个页表,下图给出了用于两级方案的典型例子
下图给出了这种方案中地址转换所涉及的步骤
倒排页表 前述页表设计的一个重要缺陷是,页表的大小与虚拟地址空间的大小成正比
替代一级或多级页表的一种方法是,使用一个倒排页表结构。在这种方法中,虚拟地址的页号部分使用一个简单的散列函数映射到散列表中。散列表包含指向倒排表的指针,而倒排表中含有页表项。采用这种结构后,散列表和倒排表中就各有一项对应于一个实存页而非虚存页。因此,不论有多少进程,支持多少虚拟页,页表都只需要实存中的一个固定部分。由于多个虚拟地址可能映射到同一个散列表项中,因此需要使用一种链接技术来管理这种溢出。这称为倒排的原因是,它使用页框号而非虚拟页号来索引页表项,下图给出了一种倒排页表结构
转换检测缓冲区(TLB)
每次虚拟访问都可能会引起两次物理内存访问:一次取相应的页表项,另一次取需要的数据。因此,简单的虚拟内存方案会导致内存访问时间加倍,为了克服这个问题,大多数虚拟内存方案都为页表项使用了一个特殊的高速缓存,通常称为转换检测缓冲区(TLB),这和CPU的高速缓存类似,缓冲区里包含了最近用过的页表项,由此得到的分页硬件组织如下图所示
这和CPU的高速缓存机制类似,要找一个虚拟地址对应的页表项,先检查缓冲区,如果没有检查页表,如果还没有则会产生一次内存访问故障,称为缺页。这时离开硬件作用范围,调用操作系统,由操作系统负责装入所需要的页,并更新页表。下图的流程图表明了TLB的使用
由于TLB仅包含整个页表中的部分表项,因此不能简单地把页号编入TLB的索引,相反,TLB中的项必须包含页号和完整的页表项,处理器中的硬件机制允许同时查询许多TLB页,以确定是否存在匹配的页号。对于下图,在页表中查找所用的直接映射或索引,这种技术称为关联映射。TLB的设计还须考虑TLB中表项的组织方法,以及读取一个新项时置换哪一项。这些问题是任何硬件高速缓存设计者都必须考虑的
最后,虚存机制须与高速缓存系统(内存高速缓存)进行交互,通过虚拟地址查找块时,内存系统查看TLB中是否存在匹配的页表项,如果不在,从页表读取页表项。产生一个标记和其余部分组成的实地址后,查看高速缓存中是否存在包含这个字的块。若有,则把它返回给CPU;若没有,则从内存中检索这个字,这个过程如下图所示
页尺寸
页尺寸是一个重要的硬件设计决策,需要考虑多方面的因素,其中一个因素是内部碎片。显然,页越小,内部碎片的总量越少。另一方面,页越小,每个进程需要的页的数量就越多,这就意味着更大的页表。对于多道程序设计环境中的大程序,这意味着活动进程有一部分在虚存而非内存中。因此,一次内存访问可能产生两次缺页中断;第一次读取所需的页表部分,第二次读取进程页。另一个因素是基于大多数辅存设备的物理特性,希望页尺寸比较大,从而实现更有效的数据块传送
页尺寸和分配给进程的页框数对缺页率的影响如下图所示
最后,页尺寸的设计问题与物理内存的大小和程序大小有关,当内存变大时,应用程序使用的地址空间页相应增长。此外,大型程序中所用的当代程序设计技术可能会降低进程中的局部性原理。例如
- 面向对象技术鼓励使用小程序和数据模块。它们的引用在相对较短的时间内散布在相对较多的对象中
- 多线程应用可能导致指令流和分散内存访问的突然变化
对于给定大小的TLB,当进程的内存大小增加且局部性降低时,TLB访问的命中率降低,这种情况下,TLB可能会成为一个性能瓶颈
提高TLB性能的一种方法是,使用包含更多项的更大TLB,但是,TLB的大小会影响其他的硬件设计特征。替代方法之一是采用更大的页,使TLB中的每个页表项对应于更大的存储块,但是采用较大的页可能会导致性能下降
因此,很多硬件设计者开始尝试使用多种页尺寸,并且很多微处理器体系结构支持多种页尺寸,这位有效地使用TLB提供了很大的灵活性,例如程序指令可以使用数量较少的大页来映射,而线程栈可能使用较小的页来映射,但是大多数商业操作系统仍然只支持一种页尺寸,原因是页尺寸会影响操作系统的许多特征,因此操作系统支持多种页尺寸是一项复杂的任务
8.1.3 分段
虚拟内存的含义 分段允许程序员把内存视为多个地址空间或段组成,段的大小不等,并且是动态的,内存访问以段号和偏移量的形式组成地址
对程序员而言,这种组织与非段式地址空间相比有许多优点:
1.简化了对不断增长的数据结构的处理。对事先不确定的数据结构最终的大小,段式虚存可以讲数据结构分配到它自己的段,需要时操作系统可以扩大或缩小这个段,若需要的内存空间不足,操作系统可以将其换出,后续再换入
2.允许程序独立地改变或重新编译,而不要求整个程序重新链接或重新加载,这也是使用多个段实现的
3.有助于进程间的共享。程序员可以在段中放置一个实用工具程序或一个有用的数据表,供其他进程访问
4.有助于保护。由于一个段可被构造成包含一个明确定义的程序或数据集,因而程序员或系统管理员可以更方便地指定访问权限
组织 基于分段的虚拟内存方案仍然需要段表这个设计,且每个进程都有一个唯一的段表。在这种情况下,段表项将变得更加复杂。由于一个进程可能只有一部分段在内存中,因而每个段表项中需要有一位表明相应的段是否在内存中,若在内存中,则表项还包括该段的起始地址和长度
段表项中需要的另一个控制位是修改位,它拥有表明相应的段从上次被装入内存到目前为止其内容是否改变,从内存中读一个字的基本机制如下图所示
当某个特定的进程正在运行时,有一个寄存器为该进程保存段表的起始地址。虚拟地址中的段号用于检索这个表,并查找该段起点的相应内存地址。这个地址加上虚拟地址中的偏移量部分,就形成了需要的实地址
8.1.4 段页式
分段和分页各有长处。分页对程序员是透明的,它消除了外部碎片,因而能更加有效地使用内存,并且可能开发出更精致的存储管理算法。分段对程序员是可见的,它具有处理不断增长的数据结构的能力,及支持共享和保护的能力。为了结合二者的优点,有些系统配备了特殊的处理器硬件和操作系统软件来同时支持分段和分页
在段页式系统中,地址空间被划分为许多段,段依次划分为许多固定大小的页。从程序员的角度看,逻辑地址仍然由段号和偏移量组成;从系统的角度来看,段偏移量又可以拆解为页号和页偏移量
下图给出了支持段页式的一种结构。每个进程都使用一个段表和页表,且每个进程段使用一个段表。某个特定的进程运行时,使用一个寄存器记录该进程段表的起始地址
8.1.5 保护和共享
分段有助于实现保护与共享机制,由于每个段表包括一个长度和一个基地址,因而程序一般不会访问超出该段的内存单元。为实现共享,一个段可能会在多个进程的段表中引用,分页系统也有同样的机制。但是,这种情况下由于程序的页结构和数据对程序员不可见,因此更难说明共享和保护需求,下图说明了可以实施保护关系的类型
同时也存在更高级的机制,一种常用的方案时使用下图的环状保护结构,在这种方案中,编号小的内环比编号大的外环具有更大的特权。环状系统的基本原理如下:
1.程序可以只访问驻留在同一个换或更低特权环中的数据
2.程序可以调用驻留在相同或更高特权环中的服务
8.2 操作系统软件
操作系统的内存管理设计取决于三个基本的选择:
- 是否使用虚存技术
- 是使用分页还是使用分段,或同时使用二者
- 为各种存储管理特征采用的算法
前两个选择取决于所用的硬件平台。如果没有对地址转换和其他基本功能的硬件支持,那么这些技术都无法实际使用。除去老式计算机上的操作系统外,所有重要的操作系统都提供了虚拟内存,其次,纯分段系统现在已越来越少,结合分段和分页后,操作系统所面临的大多数内存管理问题都是关于分页方面的
第三个选择属于操作系统软件领域的问题,也是本节的主题,下表列出了需要考虑的重要设计因素。在各种情况下,最重要的都是与性能相关的问题,希望使缺页中断发生的频率最小。此外,希望能够通过适当的安排,使得一个进程正在执行时,访问一个未命中的页中的字的概率最小,在下表给出的所有策略中,不存在一种绝对的最佳策略
分页环境中的内存管理任务极其复杂。此外,任何特定策略的总体性能取决于内存的大小,内存和外存的相对速度,竞争资源的进程大小和数量,以及单个程序的执行情况。所有没有绝对的最优解,只能我们处于的情景中找出相对比较好的策略
8.2.1 读取策略
读取策略决定某页何时取入内存,常用的两种方法是请求分页和预先分页。对于请求分页,只有当访问到某页中的一个单元才将该页取入内存。若内存管理其他策略比较合适,将发生下述情况:当一个进程首次启动时,会在一段实际出现大量的缺页中断;取入越来越多的页后,局部性原理表明大多数将来访问的页都是最近读取的页。随着读取的页越来越多,缺页中断的数量会降到很低
对于预先分页,读取的页并不是缺页中断请求的页。预先分页利用了大多数辅存设备的特性,这些设备有寻道时间和合理的延迟。若一个进程的页连续存储在辅存中,则一次读取许多连续的页要比隔一段时间读取一页有效。相反则这个策略是低效的
进程首次启动时,可采用预先分页策略,此时程序员须以某种方式指定需要的页;发生缺页中断时页可采用预先分页策略,由于这个过程对程序员不可见,因此更为可取
8.2.2 放置策略
放置策略决定一个进程块驻留在实存中的什么位置。对于纯分段或段页式系统,如何放置通常无关紧要。另一个关注放置问题的领域是非一致存储访问(NUMA),在NUMA中,机器中分布的共享内存可被机器的任何处理器访问,但访问某一特定物理单元所需的时间会随处理器和内存模块之间距离的不同而变化。因此,其性能很大程度上取决于数据驻留的位置和使用数据的处理器间的距离
8.2.3 置换策略
置换策略涉及许多概念,比如下面的: - 给每个活动进程分配多少页框
- 计划置换的页集是局限于那些产生缺页中断的进程,还是局限于所有页框都在内存中的进程
- 在计划置换的页集,选择换出哪一项
前两个概念都称为驻留集管理,术语”置换策略“专指第三个概念,本节讲述这方面的内容
当内存中的所有页框都被占据,且需要读取一个新页以处理一次缺页中断时,置换策略决定置换当前内存中的哪一页。根据局部性原理,最近的访问历史和最近将要访问的模式有很大的关联。因此大多数策略都基于过去的行为来预测将来的行为。必须折中考虑的是,置换策略设计得越精致,越复杂,实现它的软硬件开销就越大
页框锁定 置换策略有一个约束条件:内存中的某些页框可能是被锁定的。一个页框被锁定时,当前保存在页框中的页就不能被置换,大部分操作系统和重要的控制结构就保存在锁定的页框中。锁定时通过给每个页框关联一个”锁定”位来实现的
基本算法
不论采用哪种驻留集策略,都有一些用于选择置换页的基本算法。可以查到的置换算法包括以下几种: - 最佳(OPT)
- 最近最少使用(LRU)
- 先进先出(FIFO)
- 时钟(Clock)
OPT策略选择置换下次访问距当前时间最长的那些页,这种算法导致的缺页中断最少。由于它要求操作系统必须知道将来的事件,因此不可能实现,但是可以作为衡量其他算法性能的一种标准
LRU策略置换内存中最长时间未被引用的页。根据局部性原理,这也是最近最不可能访问到的页。实际上,LRU策略的性能接近于OPT策略,但这种方法的问题是比较难以实现。一种实现方法是给每页添加一个最后一次访问的时间戳,并在每次访问内存是更新时间戳,即使有支持这种方案的硬件,开销仍然非常大,另一种方案是维护一个关于访问页的栈,但开销同样很大
FIFO策略把分配给进程的页框视为一个循环缓冲区,并按循环的方式移动页。它需要的只是一个指针,该指针在进程的页框中循环。因此这是一种实现起来最简单的页面置换策略。如果经常出现一部分出现或数据在整个程序的生命周期中使用频率都很高的情况,若使用这个算法,则这些页需要被反复地换入和换出
近年来,试图以较小的开销接近LRU的性能,许多这类算法都是称为时钟策略的各种变体
最简单的时钟策略需要给每个页框关联一个称为使用位的附加位,当某页首次装入内存时,将该页框的使用位置为1;该页随后被访问时,其使用位也会置为1。用于置换的候选页框集被视为一个循环缓冲区,并有一个指针与之关联。当一页被置换时,该指针被置为指向缓冲区的下一个页框。需要置换一页时,操作系统扫描缓冲区,查找使用位置为0的一个页框。每当遇到一个使用位为1的页框,将其置为0。这个策略类似于FIFO,唯一不同的是,在时钟策略中会跳过使用位为1的页框。下图给出了这几种方法的例子
我们图中可以看到OPT的F数量最少,说明缺页中断的次数最少,而FIFO的次数最多,时钟策略(*号表示相应的使用位为1)比LRU多一次
下图给出了一个实验结果,该实验比较了前面讨论的4个算法:它假设分配给一个进程的页框数量是固定的
当分配的页框数量较少时,4种策略的差别非常显著,FIFO比OPT几乎差了两倍。
使用可变分配和全局或局部置换范围时,也有关于时钟算法与其他算法的比较,时钟算法的性能非常接近于LRU
增加使用的位数,可使时钟算法更有效。在所有支持分页的处理器种,内存中的每页都有一个与之关联的修改位,因此页框也与这些修改位相关联。修改位是必需的,若某页被修改,则在它被写回外存前不会被置换出。如果一起考虑使用位和修改位,那么每个页框都处于以下4种情形之一:
根据这一分类,时钟算法的执行过程如下:
1.扫描页框缓冲区,在这次扫描过程中,对使用位不做任何修改,选择遇到的第一个页框用于置换(u=0,m=0)
2.若第一步失败,重新扫描,查找u=0,m=1的页框。选择第一个遇到的置换,扫描过程中,每个跳过的页框的使用位置为0
3.若第二步失败,则指针回到最初的位置,且集合中所有页框的使用位均为0。重复第1步,并在必要时候重复第2部,这样可以查找到供置换的页框
第一步查找自被取入至今未被修改且最近未访问的页,这样的页最适合置换,并且有一个优点,即由于未被修改,它不需要写回辅存。若第一次未找到候选页,则算法再次在缓冲区中开始循环,查找最近未被访问但被修改的页。这样的页必须先写回才被置换,若第二次扫描失败,则缓冲区中的所有页框都被标记为最近未访问,执行第三次扫描
页缓冲
尽管LRU和时钟策略比FIFO更高级,但FIFO却没有像它们那样涉及复杂性和开销问题。另一个问题是,置换修改过的页,代价比置换未被修改过的页代价要大
能提高分页的性能并允许较简单的页面置换策略的一种方法是页缓冲。为提高性能,这种算法不丢弃置换出的页,而是将它分配到以下两个表之一中:若未被修改,则分配到空闲页链表中;若已被修改,则分配到修改页链表中。该页在内存中并不会物理性移动,移动的只是该页所对应的页表项,移动后的页表项在空闲页链表中或修改页链表中
空闲页表链内包含有页中可以读取的一系列页框。需要读取一页时,使用位于列表头部的页框,置换原本在那个位置的页,当未经修改的一页被置换时,它仍然在内存中,且其页框被添加到空闲页链表的尾部。与此类似,当已修改过的一页被写出和置换时,其页框也被添加到修改页链表的尾部
这些操作的一个重要特点是,被置换的页仍然留在内存中,因此,若进程访问该页,则可以迅速返回该进程的驻留集,且代价很小。实际上,空闲页链表和修改页链表充当着高速缓存的角色。修改页链表还有另外一种很有用的功能:已修改的页按簇写回,而不是一次只写一页,因此大大减少了I/O操作的数量,进而减少的磁盘访问时间
置换策略和高速缓存大小
随着内存越来越多大,应用的局部性特性也逐渐降低。作为补偿,高输出的大小也相应增加。对于使用某种形式页缓冲的系统,有可能通过为页面置换策略补充一个在页缓冲区中的页放置策略来提高高速缓存的性能。大多数操作系统通过从页缓冲区中选择一个任意的页框来放置页,且通常使用FIFO原则
8.2.4 驻留集管理
驻留集大小 对于分页式虚拟内存,在准备执行时,不需要也不可能把一个进程的所有也都读入内存。因此,操作系统必须决定读取多少页,即决定给特定的进程分配多大的内存空间。这需要考虑以下几个因素: - 分配给一个进程的内存越少,在任何时候驻留正在内存中的进程数就越多。这增加了操作系统至少找到一个就绪进程的可能性,减少了由于交换而消耗的处理器时间
- 若一个进程在内存中的页数较少,尽管有局部性原理,缺页率仍相对较高
- 给特定进程分配的内存空间超过一定大小后,由于局部性原理,该进程的缺页率没有明显的变化
基于这些因素,当代操作系统通常采用两种策略。固定分配策略为一个进程在内存中分配固定数量的页框,以供执行时使用。这一数量在最初加载时确定,可以根据进程的类型或基于程序员或系统管理员的需要来确定。对于固定分配策略,一旦在进程的执行过程中发生缺页中断,该进程的一页就必须被它所需要的页面置换
可变分配策略允许分配给一个进程的页框在该进程的生命周期中不断地发生变化。若一个进程的缺页率一直比较高,应给它多分配一些页框以减少缺页率;若一个进程的缺页率特别低,则可在不明显增大缺页率的前提下减少分配给它的页框。可变分配策略的使用和置换范围的概念紧密相关
可变分配策略看起来性能更优,但是这种方法要求操作系统评估活动进程的行为,这必然会增加操作系统的软件开销,并且取决于处理器平台所提供的硬件机制
置换范围 置换策略的作用分为全局和局部两类。这两种类型的策略都是在没有空闲页框时由一个缺页中断激活的。局部置换策略仅在产生缺页的进程的驻留页中选择,而全局置换策略则把内存中所有未被锁定的页都作为置换的候选页
置换范围和驻留集大小之间存在一定的联系。固定驻留集意味着使用局部置换策略。为保持驻留集的大小固定,从内存中移除的一页必须由同一个进程的另一页置换。可变分配策略显然可以采用全局置换策略:内存中一个进程的某一页置换了另一个进程的某一页,导致该进程的分配增加一页,而被置换的另一个进程的分配则减少一页,下面分析这三种组合
固定分配,局部范围 在这种情况下,分配给在内存中运行的进程的页框数固定。发生一次缺页中断时,操作系统必须从该进程的当前驻留页中选择一页用于置换,置换算法可以使用前面讲述过的那些算法
对于固定分配策略,需要事先确定分配给该进程的总页框数。这将根据应用程序的类型和程序的请求总量来确定。这种方法有两个缺点:总页数分配得过少时,会产生很高的缺页率,导致整个多道程序设计系统运行缓慢;分配过多时,内存中只能有很少的几个程序,处理器会有很多空闲时间,并把大量的时间花费在交换上
可变分配,全局范围 这种组织方式可能最容易实现,并被许多操作系统采用。在任何时刻,内存中都有许多进程,每个进程都分配到了一定数量的页框。典型情况下,操作系统还维护有一个空闲页框列表。发生一次缺页中断时,一个空闲页框会添加到进程的驻留集,并读入该页。因此发生缺页中的进程的大小会逐渐增大,这将有助于减少系统中的缺页中断总量
这种方法的难点在于置换页的选择,没有空闲页框可用时,操作系统必须选择一个当前位于内存中的页框进行置换。选择的置换页可以属于任何一个驻留进程,而没有任何原则用于确定哪个进程应从其驻留集中失去一页,因此,驻留集大小减小的那个进程可能并不是最适合被置换的
解决上述潜在性能问题的一种方法是使用页缓冲。按照这种方法,选择置换哪一页并不重要,因为如果在下次重写这些页之前访问到了这一页,则这一页仍可回收
可变分配,局部范围 可变分配,局部范围策略试图克服全局范围策略中的问题,总结如下:
1.当一个新进程被装入内存时,根据应用类型,程序要求或其他原则,给它分配一定数量的页框作为其驻留集。使用预先分页或请求分页填满这些页框
2.发生一次缺页中断时,从产生缺页中断的进程的驻留集中选择一页用于置换
3.不时地重新评估进程的页框分配情况,增加或减少分配给它的页框,以提高整体性能
在这种策略中,关于增加或减少驻留集大小的决定必须经过仔细权衡,且要基于对活动进程将来可能的请求的评估,这种策略会比简单的全局置换策略复杂得多,但它会产生更好的性能
这个策略的关键要素是用于确定驻留集大小的原则和变化的时间安排。比较常见的是工作集策略。尽管真正的工作集策略很难实现,但它可作为比较各种策略的标准
进程在虚拟时间t的参数为∆的工作集W(t,∆),表示该进程在过去的∆个虚拟时间单位中被访问到的页集。虚拟时间窗口越大,工作集就越大。这可用下面的关系式表示:
下图给出了一个例子,点表示工作集未发生变化的时间单位
简单讲一下这个例子,4表示存储最近访问到的四个页
该工作集同时还是一个关于时间的函数,若一个进程执行了∆个时间单位,且仅使用一页,则有|W(t,∆)|=1。若许多不同的页可以快速定位,且窗口大小允许,则工作集可增长到和该进程数N一样大。因此有如下关系
下图表明了对于固定的∆值,工作集大小随时间变化的一种方法。对于许多程序,工作集相对比较稳点的阶段和快速变化的阶段是交替出现的。当一个进程开始执行时,它访问新页的同时也逐渐建立起一个工作集。最终,根据局部性原理,该进程相对稳定在由某些页构成的工作集上。在瞬变阶段,来自原局部性阶段中的某些页仍然留在窗口∆中,导致访问新页时工作集的大小剧增。当窗口滑过这些页访问后,工作集的大小减小,直到它仅包含那些满足新的的局部性的页
工作集的概念可用于指导有关驻留集大小的策略:
1.监视每个进程的工作集
2.周期性地从一个进程的驻留集中移去那些不在其工作集中的页
3.只有当一个进程的工作集在内存中(即其驻留集包含了它的工作集)时,才可执行该进程
但是,这种工作集策略仍然存在许多问题:
1.根据过去并不总能预测将来,工作集的大小和成员都会随时间而变化
2.为每个进程真实地测量工作集是不实际的,它需要为每个进程的每次页访问使用该进程的虚拟时间作为时间标记,然后为每个进程都维护一个基于时间顺序的页队列
3.∆的最优值是未知的,且它在任何情况下都会变化
然而,这种策略的思想是有效的,许多操作系统都试图采用近似工作集策略。其中一种方法的重点不是精确的页访问,而是进程的缺页率。因此通过监视缺页率来达到类似的结果。推断方法如下:若一个进程的缺页率低于某个最小阈值,则可以给该进程分配一个较小的驻留集但并不降低该进程的性能,使得整个系统都从中受益。若一个进程的缺页率超过某个最大阈值,则可在不降低整个系统性能的前提下,增大该进程的驻留集,使得进程从中受益
遵循该策略的一种算法是缺页中断频率(PFF) 算法。该算法要求内存中的每页都有一个与之关联的使用位。某页被访问时,相应的使用位置为1;发生一次缺页中断时,操作系统记录该进程从上次缺页中断到现在的虚拟世界,这通过维护一个页访问计数器来实现。定义一个阈值F,若从上一次缺页中断到这次缺页中断的时间小于F,则把该页加到该进程的驻留集中;否则淘汰所有使用位为0的页,缩减驻留集大小。同时把其余页的使用位重置为0。使用两个阈值可对该算法进行改进:一个是用于引发驻留集增加的最高阈值,另一个是用于引发驻留集大小减小的最低阈值
使用时间间隔来度量是一种比较合理的折中。使用页缓冲对该策略进行补充时,会达到相当好的性能
但是PFF方法也有缺点。假设在局部性之间的过渡期间,快速而连续的缺页中断会导致该进程的驻留集在旧局部性中的页被逐出前快速膨胀。在内存突发请求高峰时,这会导致额外的切换和交换开销
解决这种局部性过渡问题且开销低于PFF的一种方法是可变采样间隔的工作集(VSWS) 策略。VSWS策略根据经过的虚拟时间在采样实例中评估一个进程的工作集。在采样的开始处,该进程的所有驻留页的使用位被重置;在末尾处,被访问的页设置它们的使用位,这些页在下一个区间仍保留在驻留集中,其他被淘汰出驻留集。在这个区间中,任何缺页中断都将导致该页被添加到驻留集中;因此,该区间中驻留集保持固定或增长
VSWS策略由三个参数驱动:M,采样区间的最大宽度;L,采样区间的最小宽度;Q,采样实例间允许发生的缺页中断数量
VSWS策略如下:
1.若从上次采样实例到至今的单位时间达到L,则挂起该进程并扫描使用位
2.若在这个长度L的虚拟时间区间内,发生了Q此缺页中断:
若从上次采样实例至今的时间小于M,则等待,直到经过的虚拟时间到达M时,才挂起该进程并扫描使用位
若从上次采样实例至今的时间大于等于M,则挂起该进程并扫描使用位
选择参数值,使得上次扫描后第Q此缺页中断时能正常地激活采样。另两个参数M和L为异常条件提高边界保护。VSMS策略试图通过增加采样频率,减少突然的局部性过度所引发的内存请求高峰,进而在缺页中断速度增加时,减少未使用页淘汰出驻留集的速度
8.2.5 清楚策略
与读取策略相反,清楚策略用于确定何时将已修改的一页写回辅存。通常有两种选择:请求式清除和预约式清除。对于请求式清除,只有当一页被选择用于置换时才被写回辅存;而预约式清除策略则将这些已修改的多页在需要使用它们所占据的页框之前成批写回辅存
完全使用任何一种策略都存在危险。对于预约式清除,写回辅存的一页可能仍然留在内存中,直到页面置换算法指示它被移除。预约式清除允许成批地写回页,但这并无太大意义,因为这些页的大部分通常会在置换前又被修改,并且辅存的传送能力有限,因此不应浪费在实际上不太需要的清除操作上
另一方面,对于请求式清除,写回已修改的一页和读入新页是成对出现的,这意味着发生缺页中断的进程在解除阻塞之前必须等待两次页传送,而这可能会降低处理器的利用率
一种较好的方法是结合页缓冲技术,这种技术允许采用下面的策略:只清除可用于置换的页,但去除了清除和置换操作间的成对关系。通过页缓冲,被置换页可放置在两个表中:修改表和未修改表。修改中的页可以周期性成批写出,并一道未修改表中。未修改表中的一页要么因为被访问到而被回收,要么在其页框分配给另一页时被淘汰
8.2.6 加载控制
加载控制会影响到驻留在内存中的进程数量,这称为系统并发度。加载控制策略在有效的内存管理中非常重要。如果某一时刻驻留的进程太少,那么所有进程都处于阻塞态的概率就较大,因而会有许多时间花费在交换上,如果驻留的进程太多,那么会频繁发生缺页中断,从而导致系统抖动
系统并发度
下图说明了抖动的情况
系统并发度较小的时候,由于很少出现所有驻留进程都被阻塞的情况,因此会看到处理器的利用率增长,但在到达某一点后,平均驻留集会不够用,从而缺页率迅速增加,导致处理器的利用率下降。解决这个问题有很多种途径,工作集或PFF算法都隐含了加载控制
另一种方法通过调整系统并发度,来使缺页中断之间的平均时间等于处理一次缺页中断所需的平均时间。性能研究表明这种情况下处理器的利用率达到最大
另一种方法时采用前面给出的时钟页面置换算法,使用全局范围的技术。它监视该算法中扫描页框的指针循环缓冲区的速度。速度低于某个给定的最小阈值时,表明出现了以下的情况:
1.很少发生缺页中断,因此很少需要请求指针前进
2.对于每个请求,指针扫描的平均页框数很小,表明有许多驻留页未被访问到,且均易于被置换
这两种情况下,系统并发度可以安全地增加。另一方面,如果指针的扫描速度超过某个最大阈值时,表明要么缺页率很高,要么很难找到可置换页,说明系统并发度
进程挂起 系统并发度减小时,一个或多个当前驻留进程须被挂起(换出),下面列出了6种可能性:
- 最低优先级进程:实现调度策略决策,与性能问题无关
- 缺页中断进程:很有可能是由于缺页导致无法运行,所以挂起对性能影响最小,并且该选择可以立即收到成效
- 最后一个被激活的进程:这个进程的工作集最有可能还未驻留
- 驻留集最小的进程:在将来再次装入时的代价最小,但不利于局部性较小的程序
- 最大空间的进程:可在过量使用的内存种得到最多的空闲页框
- 具有最大剩余执行窗口的进程:在大多数进程调度方案中,一个进程在被中断或放置在就绪队列末尾之前,只运行一定的时间。这近似于最短处理时间优先的调度原则
选择哪个策略取决于操作系统中许多其他设计因素以及要执行的程序的特点
8.3 UNIX和Solaris内存管理
UNIX的目标是与机器无关,因此其内存管理方案因系统的不同而不同。
SVR4和Solaris中实际上有两个独立的内存管理方案。分页系统提供了一种虚拟存储能力,但分页式虚存不适合为内核分配内存的管理。为实现这一目标,使用了内核内存分配器
8.3.1 分页系统
数据结构 对于分页式虚存,UNIX使用了许多与机器无关的数据结构,并进行了一些较小调整:
- 页表:典型情况下,每个进程都有一个页表
- 磁盘块描述符:与进程的每页相关联的是表中的项,它描述了虚拟页的磁盘副本
- 页框数据表:描述了实存中的每个页框,并以页框号为索引
- 可交换表:每个交换设备都有一个可交换表,设备的每页都在表中有一项
表中定义的大多数域都很明了。页表项中的年龄域表明自程序上次访问这一页框至今持续了多久,但这个域的位数和更新频率取决于不同的实现版本。因此,并非所有UNIX页面置换策略都会用到这个域
磁盘描述符中需要有存储域类型的原因如下:使用一个可执行文件首次创建一个新进程时,该文件只有一部分程序和数据可以装入实存。后来发生缺页中断时,装入新的一部分程序和数据。只有在第一次装入时,才创建虚存页,并为它分配某个设备中的页面用于交换。这时,操作系统被告知在首次加载程序或数据块之前是否需要清空该页框中的单元
页面置换 页框数据表用于页面置换。该表中有许多用于创建各种列表的指针。所有可用页框都链接在一起,构成一个可用于读取页的空闲页框链表。可用页的数量减少到某个阈值以下时,内核将“窃取”一些页作为补偿
SVR4中使用的页面置换算法是时钟策略的一种改进算法,称为双表针时钟算法,如下图所示
这种算法为内存中的每个可被换出的页在页表项中设置访问位。当该页被首次读取时,这一位置为0;当该页被访问以进行读或写时,这一位置为1。时钟算法中的前指针,扫描可被换出页列表中的页,并把第一页的访问位置为0。一段时间后,后指针扫描同一个表并访问位。如果该位被置为1,则略过这些页框。如果该位为0,则将这些页放置到准备置换出页的列表中
该操作需要两个参数:
- 扫描速度:双指针扫描页表的速度,单位为页/秒
- 扫描窗口:前指针和后指针之间的间隔
在引导期间,需要根据物理内存的总量为这两个参数设置默认值。扫描速度可以改变,以满足变化的条件。当空闲存储空间的总量在lotsfree和minfree两个值之间变化时,这个参数在最慢扫描速度和最快扫描速度之间线性变化。当空闲存储空间缩小时,这两个指针的移动速度加快,以释放更多的页,扫描窗口参数确定指针和后指针之间的间隔
8.3.2 内核内存分配器
内核在执行期间会频繁地产生和销毁一些小表和缓冲区,产生和销毁的每次操作都需要动态地分配内存,下面是一些例子: - 路径名转换过程可能需要分配一个缓冲区,用于从用户空间复制路径名
- allocb()例程分配任意大小的STREAMS缓冲区
- 许多UNIX实现分配僵尸结构,以便保留退出状态和已死进程的资源使用信息
- 在SVR4和Solaris中,内核在需要时动态地分配许多对象
这些块中的大多数都小于典型的机器页尺寸,因此分页机制对动态内核内存分配是低效的,SVR4使用修改后的伙伴系统
在伙伴系统中,分配和释放一块存储空间的成本比最佳适配和首次适配策略都要低。但是对内核内存管理的情况,分配和释放操作必须尽快可能地块。伙伴系统的缺点是分裂和合并都需要时间
所以就提出了伙伴系统的一种变体,称为懒惰伙伴系统,它已被SVR4采用。因为UNIX在内存请求中常常表现出稳定状态的特征,比如如果释放了一个大小为2的块,并立刻合并为4的块,则下次需要的还是2的块,所以又需要分裂,为了避免这种不必要的合并与分裂,懒惰伙伴系统推迟了合并工作,直到看上去需要合并时,才合并尽可能多的块
懒惰伙伴系统使用以下参数:
Ni:当前大小为2的i次方的块
Ai:当前大小为2的i次方且已被分配的块的数量
Gi:当前大小为2的i次方且全局空闲的块的数量:这些块可以合法合并
Li:当前大小为2都i次方且局部空闲的块的数量:这些块不可以合并,即使空闲也不能合并
这些参数存在如下关系:
Ni=Ai+Gi+Li
总体上看,懒惰伙伴系统试图维护一系列局部空闲块,只有当局部空闲块的数量超过阈值才进行合并。存在过多的局部空闲块时,可能会出现缺少空闲块的情况。大多数时候,当一个块被释放后,并不立即合并,因此记录和操作的代价很小。分配一个块时,局部空闲快和全局空闲块没有区别
为了限制局部空闲块的增长,给定大小的空闲块数量超过这一大小的已分配块数量时,才会进行合并,为实现这一方案,作者定义了一个延迟变量:
Di=Ai-Li=Ni-2Li-Gi
下图给出了这一算法
8.7 小结
要有效地使用处理器和I/O设备,就需要在内存中保留尽可能多的进程。此外,还需要解除程序在开发时对程序使用内存大小的限制
解决这两个问题的途径是虚拟内存技术。采用这个技术时,所有地址访问都是逻辑访问,并在运行时转换为实地址。这就允许进程位于内存中的任何地址,并且可随时间变化。虚拟内存技术还允许将进程划分为块。这就允许内存中不需要一定是连续的,甚至在运行时不需要改进程的所有块都在内存中
支持虚存技术的两种基本方法是分页和分段,对于分页,每个进程划分为相对较小且大小固定的页;对于分段,则可以使用大小可变的块。还可以把分页和分段组合在一个内存管理方案中
虚拟内存管理方案要求硬件和软件的支持。硬件支持由处理器提供,包括把虚拟地址动态转换为物理地址,当被访问的页或短不在内存中时产生一个中断
与操作系统支持内存管理相关的设计问题有如下几种:
- 读取策略:进程页可在请求时读取,或使用预先分页策略,按簇一次读取多页
- 放置策略:对纯分段系统,读取的段必须匹配内存中的可用空间
- 置换策略:当内存装满后,必须决定置换哪个页或哪些页
- 驻留集管理:换入一个特定的进程时,操作系统必须决定给该进程分配多少内存。这既可以在进程创建时静态分配,也可以动态地变化
- 清除策略:修改过的进程页可在置换时写出,或使用预约式清除策略,按簇一次写出多页
- 加载控制:加载控制主要关注任何给定时刻驻留在内存中的进程数量
第九章 单处理器调度
在多道程序设计系统中,内存中有多个进程。每个进程要么正在处理器上运行,要么正在等待某些事件的发生,比如I/O完成
多道程序设计的关键是调度。实际上典型的调度有4种,其中一种属于I/O调度,其他三种调度类型属于处理器调度,如下图所示
9.1 处理器调度的类型
处理器调度的目的是,以满足系统目标的方式,把进程分配到一个或多个处理器上执行
下图在进程状态转换图中结合了调度功能。创建新进程时,执行长程调度,它决定是否把进程添加到当前活跃的进程集中。中程调度是交换功能的一部分,它决定是否把进程添加到那些至少部分已在内存且可被执行的进程集中。短程调度真正决定下次执行哪个就绪进程
调度决定了哪个进程须等待,哪个进程能继续运行,因此会影响系统的性能,这可以在下图看出
该图给出了进程状态转换过程中所涉及的队列。调度属于队列管理问题,用于在排队环境中减少延迟并优化性能
9.1.1 长程调度
长程调度程序决定哪个进程可以进入系统中处理,因此它控制了系统的并发度。一旦允许进入,作或用户程序就成为进程,并添加到供短程调度程序使用的队列中。但在某些系统中,新创建的进程最初处于被换出状态。此时,它会添加到供中程调度程序使用的队列中等待调度
在批处理系统或操作系统的批处理部分中,新提交的作业会发送到磁盘,并保存在一个批处理队列中。长程调度程序运行时,从队列创建相应的进程。这时涉及两个决策,决定何时接纳进程并且决定接受哪个作业将其转变为进程
何时创建进程通常由要求的系统并发度驱动,创建的进程越多,每个进程的执行时间越少。因此为了给当前进程集提供满意的服务,长程调度程序可能会限制系统的并发度。一个作业终止时,调度程序会决定增加一个或多个新作业。此外若处理器的空闲时间片超过了某个阈值,也可能会启动长程调度程序
下次允许哪个作业进入的决策可基于简单的先来先服务(FCFS)原则,或基于管理系统性能的工具。例如,在得到信息时,调度程序可以尝试混合处理处理器密集型和I/O密集型。同样也可以根据请求的I/O资源来做出决策,进而平衡I/O的使用
对于分时系统中的交互程序,用于连接到系统的动作可能会产生创建进程的请求。在系统接受前,操作系统将接受所有的授权用户,直到系统饱和为止,这时,连接请求会得到表明系统已饱和并要求用户重新尝试的消息
9.1.2 中程调度
中程调度是交换功能的一部分。典型情况下,换入决定取决于管理系统并发度的需求,在不使用虚存的系统中,存储管理也是一个问题,因此,换入决策将考虑换出进程的存储需求
9.1.3 短程调度
短程调度程序是三种调度中使用的最频繁的调度,它精确地决定下次执行哪个进程
导致当前进程阻塞或抢占当前运行进程的事件发生时,调用短程调用程序。这类事件包括:
- 时钟中断
- I/O中断
- 操作系统调用
- 信号(如信号量)
9.2 调度算法
9.2.1 短程调度规则
短程调度的主要目标是,按照优化系统一个或多个方面行为的方式,来分配处理器时间,通常需要对可能被评估的各种调度策略建立一系列规则
常用的规则可按两个维度来分类。首先可分为面向用户的规则和面向系统的规则。面向用户的规则与单个用户或进程感知到的系统行为相关。我们希望调度策略能给各个用户提供“好”的服务。对于响应时间,可以定义一个阈值,如2秒,因此调度机制的目标是,使平均响应时间为2秒或小于2秒的用户数量最大
另一个规则是面向系统的,即其重要是处理器使用的效果和效率,比如吞吐量。我们总是系统吞吐量能达到最大,但是这一规则侧重于系统的性能,而非提供给用户的服务,因此吞吐量是系统管理员而非普通用户所关注的
面向用户的规则在所有系统中都非常重要,面向系统的规则在单用户系统中的重要性要低一些。在单用户系统中,只要系统对用户应用程序的响应时间可以接受,实现处理器高利用率或高吞吐量就不那么重要
另一个维度划分的依据是,这些规则是否与性能直接相关。与性能直接相关的规则是定量的,通常很容易度量。与性能无关的规则本质上要么是定性的,要么不易测量和分析,比如可预测性
下表总结了几种重要的调度规则,它们相互依赖,不可能同时都达到最优。因此,设计调度策略时,要在相互竞争的各种要求之间折中,并根据系统的本质和使用情况,给各种要求设定相应的权值
在大多数交互式操作系统中,不论是单用户系统还是分时系统,适当的响应时间都是关键需求
9.2.2 优先级的使用
在许多系统中,每个进程都被指定一个优先级,调度程序总是优先选择具有较高优先级的进程
这个图说明了优先级的使用,为清楚起见,简化了队列图,忽略了多个阻塞队列和挂起状态。这里给出的不是一个就绪队列,而是一组队列,这些队列按优先级递减的顺序排序:RQ0,RQ1,RQn。进行一次调度选择时,调度程序从优先级最高的队列开始。若该队列中有一个或多个进程,则使用某种调度策略选择其中的一个
纯优先级调度方案会出现的一个问题是,低优先级进程可能会长时间处于饥饿状态,一直存在高优先级就绪进程时,会出现这种情况。若不希望这样,则只能让一个进程的优先级随时间或执行历史而变化
9.2.3 选择调度策略
选择函数决定选择哪个就绪进程下次执行,这个函数可以根据优先级,资源需求或进程的执行特性来进行选择。对于最后一种情况,下面的三个参数非常重要:
w:目前为止在系统中停留的时间
e:目前为止花费的执行时间
s:进程所需的总服务时间,包括e;这个参数通常须进行估计或由用户提供
下表给出了本节所分析的各种调度策略的一些简要信息
决策模式 说明选择函数开始执行的瞬间的处理方式,通常分为两类:
- 非抢占:在这种情况下,一旦进程处于运行状态,就会不断执行直到终止,进程要么因为等待I/O,要么因为请求某些操作系统服务而阻塞自己
- 抢占:当前正在运行进程可能被操作系统中断,并转换为就绪态。一个新进程到达时,或中断发生后把一个阻塞态进程置为就绪态时,或出现周期性的时间中断时,需要进行抢占决策
对于下表的例子,下图显示了每种策略在一个周期内的执行模式
首先,每个进程的结束时间是确定的。根据这一点,可以确定周转时间(完成时间减去到达时间,书中可能有些地方写错了),根据排队模型,周转时间就是驻留时间,或这一项在系统中花费的总时间(等待时间+服务时间)。更有用的数字是归一化周转时间,它是周转时间与服务时间的比值,表示一个进程的相对延迟情况。典型情况下,进程的执行时间越长,可以容忍的延迟时间就越长。这个比值的最小值为1.0。比值越大,服务级别就越低
先来先服务 这是最简单的策略,也称为先进先出或严格排队方案。每个进程就绪后,会加入就绪队列。当前正运行的进程停止执行时,选择就绪队列中存在时间最长的进程运行
与短进程相比,FCFS更适用于长进程。考虑下面的例子
进程Y的归一化周转时间与其他进程相比不协调,一个短进程紧跟一个长进程之后到达时会发生这种情况。另一方面,进程Z的周转时间几乎是Y的两倍,但其归一化等待时间低于2.0
使用FCFS的另一个缺点是,相对于I/O密集型进程,它更有利于处理器密集型的进程。考虑一组进程,其中的一个进程多数时间都使用处理器,其他多数进程I/O操作。若一个处理器密集型进程正在运行,则所有/O密集型进程都须等待。当处理器密集型进程正在执行时,有些I/O队列中的进程会移回就绪队列,这时大多数I/O设备可能是空闲的。在当前正运行的进程离开运行态时,就绪的I/O密集型进程迅速通过运行态,并阻塞在I/O事件上。若处理器密集型进程也被阻塞,则处理器空闲,因此FCFS可能导致处理器和I/O设备都未得到充分利用
FCFS与优先级策略结合后通常能提供一种更有效的调度方法。在单处理系统中,调度程序可以维护许多队列,每个优先级一个队列,每个队列中的调度基于先来先服务原则
轮转 减少FCFS策略不利于短作业的一类简单方法是,采用基于时钟的抢占策略。这种算法周期性产生时钟中断,出现中断,正运行的进程回放置到就绪队列,然后基于FCFS策略选择下一个就绪作业运行,这种技术也称为时间片
轮转法最主要的设计问题是所用的时间段长度。若长度很短,则短作业还较快地通过系统。另一方面,处理时钟中断,执行调度和分派函数都需要处理器开销。因此,应避免使用过短的时间片。较好的想法是,时间片最好略大于一次典型交互的时间,小于这一时间时,大多数进程至少需要两个时间片,下图显示了长短对响应时间的影响。如果一个时间片比运行时间最长的进程还要长时,轮转法就会退化为FCFS
这种方法在通用的分时系统或事务处理系统中特别有效。但是这也有缺点,通常,I/O密集型进程与处理器密集型进程相比,使用处理器的时间要短。这种情况下,处理器密集型不公平地使用了大部分处理器时间,导致I/O密集型进程性能降低,使用I/O设备低效,响应时间变化较大
所以又提出了一种改进的转轮法,称为虚拟转轮法。这种方法大部分跟前面的类似,但是这种方法是新颖之处是用了一个辅助队列。进行调度决策时,辅助队列中的进程优先于就绪队列中的进程。这种方法在公平性方面确实优于转轮法
最短进程优先(SPN) 这种方法响应时间整体上有明显提高,但响应时间的波动加剧,长进程尤其如此。因此,这种方法降低了可预测性
SPN策略的难点在于需要知道或至少需要估计每个进程所需的处理时间。对于交互进程,操作系统可为每个进程保留一个运行平均值。最简单的计算方法如下
Ti是进程的第i个实例的处理器执行时间,未避免每次重新计算总和,可以把上式重写为
上式中每个实例的权值相同,即每个实例都乘以相同的常数1/n。典型情况下,我们希望给较近的实例以较大的权值。基于过去值的时间序列预测将来值的一种更为常用的技术是指数平均法
其中a是一个常数加权因子,用于确定距现在较近或较远的的观察数据的相对权值。观察值最近越小,为了更清楚地了解这一点,可将式子展开为
观测值越旧,其计算入平均值的比例越小
下图给出了系数大小与系数在展开式中的位置关系图
A的值越大,较近观测值的权值就越大。a=0.8时,几乎所有权值都给了最近的4个观测值。若a=0.2,则要计算最近8个观测值的平均值。a值接近1的优点是,平均值能迅速反映观测值的快速变化。缺点是如果观测值出现简单的波动,就会立即影响到平均值,而使用较大的a值会导致平均值急剧变化
下图比较了两个不同a值的简单平均和指数平均。这两种情况都从估计值S1=0开始。这会使得新进程的优先级更高。注意,指数平均法与简单平均法相比,能更快地跟踪进程行为的变化,且a值越大,对观测值变化的反应就越迅速
SPN的风险在于,只要持续不断地提高更短的进程,长进程有可能饥饿,并且由于缺少抢占机制,它在分时系统或事务处理环境下仍不理想
最短剩余时间(SRT) 这是在SPN中增加了抢占机制的策略,在这种情况下,调度程序总是选择预期剩余时间最短的进程。只要有新进程就绪,并且运行时间短语当前正在运行的进程,就可以抢占当前正在运行的进程。和SPN一样,调度程序在执行选择函数时,必须具有关于处理时间的估计,并具有长进程饥饿的风险
SRT不像FCFS那样偏向长进程,也不像轮转法那样产生额外的中断,因此降低了开销,另一方面,由于它必须记录过去的服务时间,因此增加了开销。从周转时间来看,SRT的性能要好于SPN,因为相对于一个正在运行的长作业而言,短作业可被立即选择并运行
最高响应比优先 对于每个单独的进程,我们希望这个值最小,并希望所有进程的平均值也最小,我们事先并不知道服务时间是多少,但可基于历史或用户和配置管理员的某些输入值来近似地估计它
这个式子中,R为响应比,w为等待处理器时间,s为预计的服务时间。进程被立即调度时,R等于归一化周转时间。R的最小值为1.0,只有第一个进入系统的进程才能达到该值
调度规则如下:当前进程完成或被阻塞时,选择R值最大的就绪进程。偏向短作业时,长进程由于得不到服务,等待的时间会不断地增加,因此比值变大,最终在竞争中赢了短进程
类似于SRT,SPN,使用最高响应比策略时需要估计预计的服务时间
反馈法 不存在各个进程相对长度的任何信息时,就不能使用SPN,SRT和HRRN方法,另一种优先考虑短作业的方法是,处罚运行时间较长的作业,也就是若不能获得剩余的执行时间,则关注已执行的时间
具体方法如下:调度基于抢占原则并使用动态优先级机制。一个进程首次进入系统中时,会放在RQ0中,当它首次被抢占并返回就绪态时,会放在RQ1中,每次被抢占都会降低其优先级。因此,新到的进程和短进程会优先于老进程和长进程。在每个队列中,除优先级最低的队列外,都使用简单的FCFS机制。进程处于优先级最低的队列中后,就不会再降低,但会重复返回该队列,直到运行结束。因此,这个队列可按轮转方式调度
这个图通过显示一个进程经过各个队列的路径,说明了反馈调度机制。这种方法称为多级反馈,表示操作系统把处理器分配给一个进程,当这个进程被阻塞或被抢占时,就反馈到多个优先级队列的一个队列中
这个方案有多个变体,其中一个较为简单的变体使用了与转轮法相同的方式-按周期性的时间间隔执行抢占。这个简单方案的一个问题是,长进程的周转时间可能会惊人地增加。事实上,新作业频繁地进入系统时,可能会出现饥饿的情况。为解决这一问题,可以按照队列来改变抢占次数。一般而言,从RQi中调度的进程允许执行q=2的i次方时间后才被抢占
即使给较低优先级分配较长的时间,长进程仍然有可能饥饿,补救方法是,当一个进程再其当前队列中等待服务的进程超过一定的时间后,就把它提升到优先级较高的一个队列中
9.2.4 性能比较
显然各种调度策略的性能是选择调度策略的关键因素之一,但是由于相关的性能取决于各种各样的因素,因而不能得到明确的比较结果。然而,我们可以通过以下分析得出一些通用的结论
排队分析 本节中采用基本的排队公式
首先我们看到,选择与服务时间无关的下一个服务项的任何调度原则都满足以下关系:
Tr为周转时间或驻留时间,是在系统中的时间,等待时间和执行时间的总和;Ts为平均服务时间,是运行状态的平均时间,p为处理器的利用率
特别的,对于一个基于优先级的调度程序,若每个进程优先级的指定与预计服务时间无关,则提高与FCFS原则相同的平均周转时间和平均归一化的周转时间。此外,抢占存在与否对这些平均值没有影响
除了转轮法和FCFS,到目前为止考虑的各种调度原则都是基于预计服务时间进行选择的,很难为这些原则开发分析模型。但是可以通过考察基于服务时间的优先级调度,可得到这类调度算法与FCFS相比较的性能
如果调度基于优先级来完成,且进程基于服务时间指定到一个优先级类,就会出现差别,下表给出了假设有两个优先级且每个类具有不同的服务时间时,所产生的公式。表中的λ表示到达率。这些结果可以推广到任何数量的优先级类中。注意,抢占式调度和非抢占式式调度的公式是不同的。对于后一种情况,会假设一个高优先级进程就绪时,立即中断低优先级进程
假设每个类中到达的进程数相同,且低优先级类的平均服务时间是高优先级类的5倍,下图给出了全部结果,通过优先选择短作业,在较高利用率的基础上,提高了平均归一化的周转时间,如果使用抢占,将会使这种提高达到最大。但要注意,整体性能并未受到太大的影响
但是,分别考虑两个优先级类时,会出现较大的差别,下图显示了高优先级短进程的结果,并假设查看一半进程具有较短处理时间时的相对性能
仿真建模 离散事件仿真可解决建模分析的某些问题,这种仿真能为许多策略建立模型,但是其缺点式结果仅适用于特定假设下的特定进程集合,尽管如此,我们仍能得到有用的观察结果
仿真包含了50000个进程,到达速率λ=0.8,平均服务间Ts=1。因此,假设处理器的利用率为λTs=0.8。我们这里仅测试一种利用率
为方便表示结果,进程按服务时间的百分比进行分组,每组有500个进程。服务时间最短的500个进程位于第一个百分点;剩余进程中服务时间最短的500个进程位于第2个百分点;以此类推,就可把各种策略的结果视为关于进程长度的函数
下面两个图给出了归一化周转时间和平均等待时间
查看周转时间会发现FCFS的性能非常差,另一方面,因为FCFS的调度与服务时间无关,因此其绝对等待时间始终是一致的。这两幅图显示了时间片为一个时间单位的转轮法。除了执行时间小于一个时间片的最短进程外,转轮法对所有进程的标准周转时间约为5,因此会公平地对待所有进程。除了最短进程外,最短进程(SPN)法的执行结果要比转轮法好。最短剩余时间法(SRT)是具有抢占机制的SPN,除了小部分的最长进程外,SRT的执行效率要比SPN好。可以看出,在所有的非抢占策略中,FCFS偏向于长进程,SPN偏向于短进程。最高响应比(HRRN)是这两种结果的折中,这一点在图中得到了证实。最后该图给出了在每个优先级队列中具有固定统一时间片的反馈调度,对于短进程,FB的执行结果非常好
9.2.5 公平共享调度
迄今为止介绍的所有调度算法,都把就绪进程集视为单个进程池,并从进程池中选择下一个要运行的进程。虽然进程池可以按照优先级划分成几个子进程池,但它们都是同构的
但是,在多用户系统中,如果单个用户的应用程序或作业能组成多个进程,就会出现传统调度程序无法识别的进程集合结构。因此,基于进程组的调度策略非常有吸引力,这种方法通常称为公平共享调度。此外,即使每个用户用一个进程表示,也能扩展到用户组。例如,在分时系统中,可能希望把某个部门的所有用户都视为同一个组中的成员,然后进行调度决策,并为每个组中的用户提供相同的服务。如果同一个部门中的大量用户登录到系统,则希望响应时间的降低主要影响到该部门的成员,而不影响其他部门的用户
术语“公平共享”表明了这类调度程序的基本原则,每个用户被指定了某种类型的权值,这个权值定义了用户对系统资源的共享,而且是作为在所有使用资源中所占的比例来体现的。特别地,每个用户被分配了处理器的共享。公平共享调度程序的目标是监视使用情况,对相对于公平共享的用户占用较多资源的用户,调度程序分配以较少的资源,相对于公平共享的用户占有较少资源的用户,调度程序分配以较多的资源
FSS(公平共享调度程序)在进行调度决策时,需要考虑相关进程阻的执行历史,以及每个进程的执行历史。系统把用户团体划分为一些公平共享组,并为每个组分配一部分处理器资源。因此可能会有4个组,每个组使用25%的处理器。这样做实际上是为每个公平共享组提供了一个虚拟系统,虚拟系统的运行速度按比例慢于整个系统
调度是根据优先级进行的,它会考虑进程的基本优先级,近期使用处理器的情况,以及进程所在组近期使用处理器的情况。优先级的数值越大,所表示的优先级越低
每个进程被分配一个基本优先级。进程的优先级会随进程使用处理器以进程所在组使用处理器而降低。对于进程组使用的情况,用平均值除以该组的权值来归一化平均值。分配给某个组的权值越大,那么该组使用处理器对其优先级的影响就越小
在下图的例子中,进程A在一个组中,进程B和进程C在第二组中
在该图中,内核按下面的顺序调度进程:A,B,A,C,A,B。因此,处理器的50%分配给进程A,剩下的50%分配给进程B和进程C
9.4 小结
操作系统根据执行的进程从三类调度方案中选择一种。长程调度决定何时允许一个新进程进入系统。中程调度是交换功能的一部分,它决定何时把一个进程的部分或全部取进内存,使得该程序能够被执行。短程调度决定哪个就绪进程下次被处理器执行
在设计短程调度程序时使用了各种各样的规则。有些规则与单个用户觉察到的系统行为有关(面向用户),其他规则查看系统满足所有用户的需求时的总效率(面向系统)。有些规则与性能的定量测度有关,另一些规则本质上是定性的。从用户的角度来看,响应时间通常是系统最重要的一个特性;从系统的角度看,吞吐量或处理器利用率是最重要的
针对所有就绪进程的短程调度决策,人们开发了多种算法:
- 先来先服务:选择等待服务时间最长的进程
- 轮转:使用时间片限制任何正运行进程只能使用一段处理器时间,并在所有就绪进程中轮转
- 最短进程优先:选择预期处理时间最短的进程,且不抢占该进程
- 最短剩余时间:选择预期剩余处理时间最短的进程
- 最高响应比优先:调度决策基于对归一化周转时间的估计
- 反馈:建立一组调度队列,基于每个进程的执行历史和其他一些规则,把它们分配到各个队列中
调度算法的选择取决于预期的性能和实现的复杂度
第十章 多处理器和实时调度
10.1 多处理器调度
当计算机系统中包含多个处理器时,在设计调度功能时会出现一些新问题,本节先简述多处理器,然后分析设计进程级和线程级调度时的不同考虑
多处理器系统分为以下几类:
- 松耦合,分布式多处理器,集群 :由一系列相对自洽的系统组成,每个处理器都有自身的内存和I/O通道
- 专用处理器 :I/O处理器是一个典型的例子。此时有一个通用的主处理器,专用处理器由主处理器控制,并为主处理器提供服务
- 紧耦合多处理器 :由一系列共享同一个内存并受操作系统完全控制的处理器组成
本节主要介绍最后一类系统,特别是与调度有关的问题
10.1.1 粒度
描述多处理器并把它和其他结构放在一个上下文中的一种较好的方式是,考虑系统中进程间的同步粒度或同步频率。根据粒度的不同,我们可以区分5类并行度,如下表所示
无约束并行性 对于无约束并行性,进程间没有显式的同步。每个进程都代表独立的应用或作业。这类并行性的一个典型应用是分时系统。每个用户都执行一个特定的应用,如字处理或电子表格。多处理器和多道程序单处理器提供相同的服务。由于有多个处理器可用,因而用户的平均响应时间很短
无约束并行性可能达到这样的性能,如果任何一个文件或信息被共享,那么单个系统必须连接到一个有网络支持的分布式系统中。另一方面,多处理器共享系统与分布式系统相比,其成本效益更高,因页允许节约使用磁盘和其他外围设备
粗粒度和极粗粒度并行性 粗粒度和极粗粒度的并行,是指在进程之间存在同步,但这种同步的级别极粗。这种情况可以简单地处理为一组运行在多道程序单处理器上的并发进程,在单处理上对用户软件进行很少的改动或不进行改动就可以提供支持
在多处理上的加速比实际上超过了仅增加所用处理器数量所期待的加速比,这是磁盘高速缓存和编译代码共享协作的结果,而这些只需要一次性的载入内存
当进程间的交互不是很频繁时,分布式系统可以提供较好的支持,单交互更加频繁时,分布式系统中的通信开销会抵消潜在的加速比,此时多处理器组织结构能提供最有效的支持
中粒度并行性 应用程序可按进程中的一组线程来有效地实现。此时,须由程序员显式地指定应用程序潜在的并行性。为了达到中粒度并行性的同步,在应用程序的线程之间,需要更高程度的合作与交互
尽管多处理器和多道单处理器都支持独立,极粗和粗粒度的并行,且基本不会对调度功能产生影响,但在处理线程调度时,我们仍然需要重新分析调度。系统对一个线程的调度可能会影响到整个应用的性能
细粒度并行性 这是与线程中的并行相比,更为复杂的使用情况,迄今为止,这仍然是一个复杂的特殊领域,存在许多不同的方法
10.1.2 设计问题
多处理器中的调度涉及三个相互关联的问题: - 把进程分配到处理器
- 在单处理器上使用多道程序设计
- 一个进程的实际分派
所用的方法通常取决于应用程序的粒度级和可用处理器的数量
把进程分配到处理器 若假设多处理器的结构是统一的,也就是说没有哪个处理器在访问内存和I/O设备时具有物理上的特别优势,那么最简单的调度方法是把处理器视为一个资源池,并按照要求把进程分配到相应的处理器,那么分配应该是静态的还是动态的
如果一个进程一直分配给同一个处理器,那么就需要为每个处理器维护一个专门的短程队列。优点是调度的开销较小,因为所有进程关于处理器的分配只进行一次。同时,使用专用处理器允许一种称为组调度的策略
静态分配的缺点是,一个处理器可能处于空闲,另一个处理器积压了许多工作。为防止这种情况发生,需要使用一个公共队列。所有进程都进入一个全局队列,然后调度到任何一个可用的处理器中。所以它可以在不同的时间于不同的处理器上执行。在紧密耦合的共享存储器结构中,所有处理器都能得到所有进程的上下文信息,因此,调度进程的开销与它被调度到哪个处理器上无关。另一种分配策略是动态负载平衡,在该策略中,线程能在不同处理器所对应的队列之间转移
我们都需要通过某种方式把进程分配给处理器,可以使用两种方法:主从式和对等时。主从式中,操作系统的核心功能总是在某个特定的处理器上运行,其他处理器执行用户程序。主处理器负责调度作业。当一个进程被激活时,如果从处理器需要服务,那么需要给主处理器发送一个请求,然后等待服务的执行。这种方法非常简单。由于处理器拥有对所有存储器和I/O资源的控制,因而可以简化冲突解决方案。但是这种方法有两个缺点:1.主处理器的失败会导致整个系统失败;2.主处理器可能称为性能瓶颈
对等式结构中,操作系统能在任何一个处理器上执行。每个处理器从可用进程池中进行自调度。这种方法增加了操作系统的复杂性,因此必须采用某些技术来解决对资源的竞争请求
当然,在二者之间还存在许多方法,比如为内核处理提供一个处理器子集。另一种方法是,基于优先级和执行历史,简单地管理内核进程和其他进程之间的需求差异
在单处理器上使用多道程序设计 传统多处理器处理的是粗粒度或无约束同步粒度。但是对于运行在多处理器系统中的中粒度应用程序,当多个处理器可用时,要求每个处理器尽可能地忙就不再那么重要。相反,我们更加关注如何为应用提供最好的平均性能
进程分派 与多处理器调度相关的最后一个设计问题是选择哪个进程运行。在单处理器上,使用优先级或基于使用历史的高级调度算法可以提高性能。考虑单处理器时,这些复杂性可能是不必要的,甚至可能起到相反的效果,而相对比较简单的方法可能会更有效,而且开销也较低
10.1.3 进程调度
在大多数传统的多处理器系统中,进程并不指定到一个专用处理器。并非所有处理器都只有一个队列,而是有多个基于优先级的队列,并都送入相同的处理器池中。在任何情况下,都可把系统视为多服务器排队结构
考虑一个双处理器系统,每个处理器的处理速率为单处理器系统中处理器处理速率的一半。进程服务时间可用来度量整个作业所需的处理器时间总量,或该进程每次准备使用处理器时所需的时间总量。对于转轮法,假设时间片的长度比上下文切换的开销大,而比平均服务时间短。结果取决于服务时间的变化。通常这种变化用变化系数Cs来度量。Cs=0对应于无变化的情况,所有进程的服务时间相等。Cs的值越大,服务时间的值变化越大
下图给出了转轮法的吞吐量和FCFS的吞吐量的比值,它是Cs的函数。注意,在双处理器情况下,调度算法间的差别很小。对于双处理器,在FCFS的情况下,一个需要长服务时间的进程很少被中断,其他进程可以使用其他处理器,下图显示了类似的结果
得出的一般结论是,对于双处理器,调度原则的选择不如在但处理器中重要。因此在单处理器系统中使用简单的FCFS原则或在静态优先级方案中使用FCFS就已足够
10.1.4 线程调度
一个应用程序可以按一组线程来实现,这些线程可以在同一个地址空间中协作和并发地执行
在单处理器中,线程可用做辅助构造程序,并且在处理过程中重叠执行I/O。由于线程切换的开销相对很小,因此很好的实现。在多处理器系统,线程可开发应用程序中真正的并行性。但是对于需要在线程间交互的应用程序(中粒度并行度),线程管理和调度的很小变化就会对性能产生重大影响
在多处理器线程调度和处理器分配的各种方案中,有4种比较突出的方法: - 负载分配:系统维护一个就绪线程的全局队列,每个处理器只要空闲就从队列种选择一个线程。而负载平衡基于一种比较永久的分配方案来分配工作
- 组调度:一组相关的线程基于一对一的原则,同时调度到一组处理器上运行
- 专用处理器分配:这种方法与负载分配方法正好相反,它通过把线程指定到处理器来定义隐式的调度。程序终止时,处理器返回总处理器池,以便分配给另一个程序
- 动态调度:在执行期间,进程中线程的数量可以改变
负载分配 这可能是最简单的方法,也是能从单处理器环境中直接移用的方法。它有以下优点: - 负载均匀地分布在各个处理器上,确保当有工作可做时,没有处理器是空闲的
- 不需要集中调度程序。一个处理器可用时,操作系统调度例程会在该处理器上运行,以选择下一个线程
- 可以使用第九章介绍的任何一种方案组织和访问全局队列
下面分析了三种不同的负载分派方案: - 先来先服务:跟第九章类似
- 最少线程数优先:就绪队列被组织成一个优先级队列,一个作业包含的未调度线程的数量最少时,给它指定最高的优先级,具有相同优先级的队列按作业到达的顺序排队,和FCFS一样,被调度的线程一直运行到完成或被阻塞
- 可抢占的最少线程数优先:最高优先级给予具有最少未被调度线程数的作业,若刚到达的作业所包含的线程数少于正在执行作业的线程数,它将抢占属于这个被调度作业的线程
对于多种作业,FCFS优于上面列出的另两种策略。此外,某些组调度通常优于加载共享
负载分配有以下缺点: - 中心队列占据了必须互斥访问的存储器区域。因此若有许多处理器同时进行查找工作,就有可能称为瓶颈,只有很少的处理器时,不是大问题,但是处理器很多时,久可能真正出现瓶颈
- 被抢占的线程可能不再同一个处理器上恢复执行,每个处理器都配备一个本地高速缓存时,缓存的效率会很低
- 如果所有线程被视为一个公共的线程池,那么一个程序的所有线程不可能同时访问处理器,一个程序的线程间需要高度合作时,所涉及的进程切换会严重影响性能
有一种改进后的负载分配技术。操作系统为每个处理器维护一个本地运行队列和一个共享的全局运行队列。本地运行队列供临时绑定在某个特定处理器上的进程使用。处理器首先检查本地运行队列,绑定的线程优于未绑定的线程。比如让处理器专门运行属于操作系统一部分的进程,又比如特定应用程序的线程能分布在许多处理器上,并通过适当的附加软件支持组调度
组调度 这个方案有如下优点: - 组内的进程相关或大致平等时,同步阻塞会减少,且可能只需要很少的进程切换,因此性能会提高
- 调度开销可能会减少,因为一个决策可以影响许多处理器和进程
对于中粒度到细粒度的并行应用程序,组调度非常必要,因为这种应用程序的一部分准备运行,而另一部未运行时,它的性能会严重下降
组调度提高应用程序性能的一种显著方式是使进程切换的开销最小。假设进程的一个线程正在执行并到达进程中另一个线程同步的某一点。若另一个线程未运行,那么第一个线程被挂起,直到其他处理器进行进程切换并得到需要的线程。那么这种切换会严重降低性能
组调度的使用引发了对处理器分配的要求。如果使用均匀的时间分配,则会严重浪费的处理资源,因为当单线程应用程序运行时,其余的处理器是空闲的。单线程应用程序有多个时,为提高处理器的利用率,可以为它们平均分配处理器时间。这种方法不可用时,另一种可供选择的的方法是根据线程数加权调度。比如有两个应用程序,一个有4个线程,另一个有1个线程,那么给有4个线程的应用程序4/5的时间,一个线程的应用程序分配1/5的时间,处理器的浪费会降低
专用处理器分配 当一个应用程序被调度时,它的每个线程都被分配给一个处理器,相应的处理器专门用于处理对应的线程,直到应用程序运行结束
这种方法看上去极端浪费处理器时间。当应用程序的一个线程被阻塞时,该线程的处理器将一直处于空闲,所以这种方法并不存在并发处理器,以下两点可以在一定程度上解释这种策略:
1.在一个高度并行的系统中,有很多处理器时,每个处理器只占系统总代价的一小部分,处理器利用率不再是衡量有效性或性能的一个重要因素
2.在一个程序的生命周期中避免进程切换会加快程序的执行速度
下表给出了一个实验结果。作者同时运行两个应用程序,在16个处理器上计算矩阵相乘和快速傅里叶变换(FTT)。每个应用程序都把它的问题划分为许多小任务,每个小任务都映射到执行该应用程序的一个线程中。程序使用的线程数量可变。应用程序定义任务并对其进行排队。应用程序从队列中取出任务并映射到一个可用的线程。线程数比任务数少时,剩余的任务留在队列中,线程完成分配给自己的任务后再选择。并发所有应用程序都可以按这种方案构造,但许多数值问题和其他一些应用可以采用这种方式处理
下表给出了每个应用程序中执行任务和线程数从1-24时,应用程序的加速比情况。该表说明,当每个应用程序的线程数量超过8,从而使得系统中的进程总数超过处理器数量时,整个应用程序的性能开始变差,因为这时线程抢占和再次调度的频率增大。过多的抢占导致许多资源的使用效率降低
作者认为,比较有效的策略是限制活跃线程的数量,使其不超过系统中处理器的数量
多处理器系统中的处理器分配问题更加类似于单处理器中的存储器分配问题,而非单处理器中的调度问题。在某一给定时刻,给一个程序分配多少个处理器,这个问题类似于在某一给定时刻,给一个进程分配多少页框。书中提出了一个类似于虚存中的工作集的术语-活动工作集。这是指,为了保证应用程序以可以接受的速度继续执行,在处理器上必须同时调度的最少数量的活动。调度活动工作集中所有元素失败时可能会导致处理器抖动,当调度需要服务的线程,导致那些服务将被用到的线程进行取消调度时,就会发生这种情况。类似地,处理器碎片指的是剩下的处理器无法支持应用程序的需要,所以处于空闲的状态,组调度和专用处理器分配的目的是避免这些问题
动态调度 某些应用程序可能提供了语言和系统工具,允许动态地改变进程中的线程数量,这就使得操作系统可以通过调整负载情况来提供利用率
操作系统负责把处理器分配给作业,每个作业通过把它的一部分可运行任务映射到线程,使用当前划分给它的处理器执行这些任务。关于运行哪个子集以及该进程被抢占时应该挂起哪个线程之类的决策,则留给单个应用程序。某些应用程序可以设计成使用操作系统的功能来达成目的
在这种方法中,操作系统的调度责任主要局限于处理器分配,并根据以下策略继续进行。当一个作业请求一个或多个处理器时:
1.如果有空闲的处理器,则使用它们来满足请求
2.否则,如果发请求的作业是新到达的,则从当前已分配了多个处理器的作业中分出一个处理器给这个作业
3.如果这个请求的任何分配都不能得到满足,则它保持未完成状态,直到一个处理器变得可用,或该作业取消了它的请求
释放了一个或多个处理器时:
1.为这些处理器扫描当前未得到满足的请求队列。给当前还没有处理器的作业分配一个处理器,然后再次扫描这个表,按FCFS原则分配剩下的处理器
对可以采用动态调度的应用程序,这种方法优于组调度和专用处理器分配,但是这带来的开销可能会抵消它的一部分性能优势
10.1.5 多核线程调度
现在广泛使用的操作系统如Windows和Linux,本质上仍以多处理系统的方式来进行多核系统的调度。这些调度程序通常主要通过负载均衡来使就绪线程均匀分布在处理器之间,以保持处理器繁忙,然而,这种策略并不能使多核架构获得性能上的好处
随着单个芯片上内核数量的增加,最小化访问片外存储器比最大化处理器利用率更优先。在最小化方面,传统且主流的方法是利用局部缓存。如下图所示
在这种芯片的架构中,每个内核都有一个独立的一级缓存,每队内核共享一个二级缓存,且所有内核共享三级缓存,不同的芯片可能不一样
当部分但非全部内核共享内存时,调度分配的方式会对性能有明显的影响。比如在上图我们假设核0和1相邻,2和3相邻,1和2不相邻。若两个线程要共享内存资源,则应将它们分配给相邻的内存来提高性能;若不需要共享内存,则应该分配到不相邻的内核来实现负载均衡
事实上,缓存共享需要考虑两方面:合作资源共享和资源抢占。合作资源共享使得多个线程可以访问相同的内存区域。这种情况下,将线程其放在相邻内核上调度合作进程是可行的(因为共享缓存)
另一种情况是,线程在相邻的内核上竞争缓存内存地址。无论使用哪种缓存置换技术,都会在一定程度上导致性能变差。抢占感知调度的目标是把线程分配到内核上并最有效地利用共享内存,进而减少对片外存储器的访问。但是这非常复杂
10.2 实时调度
10.2.1 背景
实时计算正在成为越来越重要的原则。操作系统,特别是调度程序,可能是实时系统中最重要的组件
实时计算定义为这样的一类计算:系统的正确性不仅取决于计算的逻辑结果,而且取决于产生结果的时间。我们可以通过定义实时进程或实时任务来定义实时系统。一般来说,在实时系统中,某些任务是实时任务,它们具有一定的紧急度。这类任务试图控制外部世界发生的事件,或对这些事件做出反应。通常会给某个特定任务指定一个最后期限,最后期限指定开始时间或结束时间,这类任务分为硬实时任务和软实时任务。硬实时任务是指必须满足最后期限的任务,否则会带来严重的后果。软实时任务也有一个与之关联的最后期限,但即使超过了最后期限,调度和完成这个任务仍然是有意义的
实时任务的另一个特征是,它们是周期的或非周期的。非周期任务有一个必须结束或开始的最后期限,或有一个关于开始时间和结束时间的约束条件。而对周期任务而言,这一要求可描述为每隔周期T一次
10.2.2 实时操作系统的特点
实时操作系统由如下5方面的需求表征:
- 可确定性
- 可响应性
- 用户控制
- 可靠性
- 故障弱化操作
操作系统的可确定性,在某种程度上是指它可以按照固定的,预先确定的时间或时间间隔执行操作。在实时操作系统中,进程请求服务是用外部事件和时间安排来描述的。操作系统可确定性地满足请求的程度首先取决于它响应中断的速度,其次取决于系统是否具有足够的能力在要求的时间内处理所有的请求
关于操作系统可确定性能力的一个非常有用的度量是,从高优先级设备中断至开始服务之间的延迟。在非实时操作系统中,这一延迟可以是几十到几百毫秒,而在实时操作系统中,这一延迟上限是从几微妙到1毫秒
可响应性关注的点和可确定性不同。确定性关注的是操作系统获知有一个中断之前的延迟,可响应性关注的是在知道中断之后,操作系统为中断提供服务的时间,可响应性包括以下几方面:
1.最初处理中断并开始执行中断服务例程(ISR)所需的时间总量。若ISR的执行需要一次进程切换,则需要的延迟将比在当前进程上下文中执行ISR的延迟长
2.执行ISR所需的时间总量,通常与硬件平台有关
3.中断嵌套的影响。一个ISR可能会因另一个中断的到达而中断时,服务将被延迟
可确定性和可响应性共同组成了对外部事件的响应时间。对实时系统来说,响应时间的要求非常重要,因为这类系统必须满足系统外部个体,设备和数据流强加的时间要求
用户控制 在实时操作系统中的应用通常要比在普通操作系统中广泛。在典型的非实时操作系统中,用户要么对调度没有任何控制,要么只提供概括性的指导,比如分多个优先级组。但在实时系统中,必须要运行用户细粒度地控制任务优先级。并且用户要能区分硬实时任务和软实时任务,并且要确定优先级,实时系统还允许用户指定一些特性,比如哪个进程必须常驻内存
可靠性 在实时系统中比在非实时系统中更重要。非实时系统出现故障可以通过重启来解决,但是实时系统如果出现问题可能会导致灾难性的后果,因为其是实时响应和控制事件
和其他领域一样,实时和非实时操作系统的区别仅体现在程度上。故障弱化操作是指系统故障时尽可能多地保存其性能和数据的能力,例如一个典型的传统的UNIX系统,内核数据出错时,产生故障信息,便于以后分析,并且将内存中的内容存储到磁盘中,然后终止系统执行。与之相反,实时系统将尝试改正这个问题并最小化影响,并且继续执行。我们就可以看出差别,非实时出现错误后终止运行,而实时则是继续运行并且尝试矫正错误。当实时操作系统关机时,必须维护文件和数据的一致性
故障弱化运行的一个重要特征称为稳定性。一个实时系统是稳定的,是指如果当它不可能满足所有任务的最后期限时,即使总是不能满足一些不怎么重要的任务的最后期限,那么系统也将首先满足最重要的任务的最后期限
尽管为众多实时应用设计了众多的实时操作系统,但大部分实时操作系统仍然具有以下常见功能: - 与传统操作系统相比,有着更为严格的使用优先级,以抢占式调度来满足实时性要求
- 中断延迟(一个设备从运行到中断的时间)有界且相对较短
- 与通用操作系统相比,有更精确和更可预测的时序特性
实时系统的核心时短程任务调度程序。在设计这种调度程序时,公平性和最小平均响应时间并不是最重要的,最重要的是所有硬实时任务都能在最后期限内完成(或开始),且尽可能多的软实时任务也能在最后期限内完成(或开始)
大多数当代操作系统都不能直接处理最后期限,而被设计为尽可能地对实时任务做出响应,使得在临近最后期限时,能迅速调度一个任务。从这一点看,实时应用程序在许多条件下都要求确定性的响应时间在几毫米到小于1毫秒的范围内
下图显示了多种可能性。在使用简单转轮调度的抢占式调度程序中,实时任务将加入就绪队列,等待它的下一个时间片,但是这会导致延迟难以接受。我们还可以使用优先级驱动非抢占式调度器,在这种情况下,只要当前的进程阻塞或运行结束,就可以调度这个就绪的实时任务。较慢的低优先级任务在临界时间中执行时,会导致几秒的延迟,因此这种方法也难以接受。一种折中的方法是,把优先级和基于时钟的中断结合起来。可抢占点按规则的间隔出现。出现一个可抢占点时,若有更高优先级的任务正在等待,则当前运行的任务被抢占。这就有可能抢占操作系统内核的部分任务,这类延迟约为几毫秒。尽管这种方法对某些实时应用程序已经足够,但对一些要求更苛刻的应用程序来说仍然不够,这时常采用一种称为立即抢占的方法,也就是操作系统立刻响应中断,除非系统处于临界区代码保护区中,这种方法的调度延迟相较于前面的方法更少
10.2.3 实时调度
实时调度是计算机科学最活跃的研究领域之一。本节概述各种实时调度方法,并研究两类流行的调度算法
在考察实时调度算法时,观察到各种调度方法取决于:1.一个系统是否执行可调度性分析;2.如果执行,它是静态的还是动态的;3.分析结果自身是否根据在运行是分派的任务产生一个调度或激活,基于这些考虑,作者分以下几类算法进行说明: - 静态表调度法:执行关于可行调度的静态分析。分析的结果是一个调度,它确定在运行时一个任务何时须开始执行
- 静态优先级抢占调度法:执行一个静态分析,但未制定调度,而是通过给任务指定优先级,使得可以使用传统的基于优先级的抢占式调度程序
- 基于动态规划的调度法:在运行时动态低确定可行性,而不是在开始运行前离线地确定。到达的任务仅能在满足其他时间约束时,才可被接受并执行。可行性分析的结果是一个调度或规划,可用于确定何时分派这个任务
- 动态尽力调度法:不执行可行性分析,系统试图满足所有的最后期限,并终止任何已经开始运行但错过最后期限的进程
静态调度法 适用于周期性的任务。该分析的输入为周期性的到达时间,执行时间周期性的最后结束期限和每个任务的相对优先级。调度程序试图开发一种能满足所有周期性任务要求的调度。这是一种可预测的方法,但不够灵活,因为任务要求的任何变换都需要重做调度
静态优先级抢占调度法 与前面我们讲过的非实时多道程序系统中的基于优先级的抢占式调度所用的机制相同。在非实时系统中,各种因素可能用于确定优先级。在实时系统中,优先级的分配与每个任务的时间约束相关。这种方法的一个例子是速率单调算法,它根据周期的长度来给指定任务指定静态优先级
基于动态规划的调度法 在一个任务已到达但未执行时,试图创建一个包含前面被调度任务和新到达任务的调度。如果新到达的任务能按如下方式调度:满足它的最后期限,且之前被调度的任务不会错过它的最后期限,则修改调度
动态尽力调度法 是当前许多商用实时系统所使用的算法。任务到达时,系统根据任务的特性给它指定一个优先级,并通常使用某种形式的时限调度,如最早最后期限调度。这类任务一般是非周期性的,因此不可能进行静态调度分析。而对于这类调度,直到到达最后期限或直到任务完成,我们都不知道是否满足时间约束,这是这类调度的一个主要缺点,但是易于实现
10.2.4 期限调度
人们提出了许多关于实时任务调度的合适方法,这些方法都基于每个任务的额外信息,最常见的信息包括: - 就绪时间:任务开始准备执行时的时间。对于重复或周期性的任务,这实际上是一个事先知道的时间序列,对于非周期性任务,要么事先知道时间,要么操作系统仅知道什么时候任务真正就绪
- 启动最后期限:任务必须开始的时间
- 完成最后期限:任务必须完成的时间,典型实时应用程序要么有启动最后期限,要么有完成最后期限,但不会两者都存在
- 处理时间:从执行任务直到完成任务所需的时间。某些情况下,可以提供这个世界,在另外一些情况下,操作系统会度量指数平均值
- 资源需求:任务在执行过程中所需的资源集
- 优先级:度量任务的相对重要性。硬实时任务可能具有绝对优先级。系统必须继续运行时,可为硬实时任务和软实时任务指定相关的优先级,进而指导调度程序
- 子任务结构:一个任务可分解为一个必须执行的子任务和一个可选执行的子任务,只有必须执行的子任务拥有硬最后期限
考虑最后期限时,实时调度功能可分为多个维度:下次调度哪个任务及允许哪种类型的抢占。采用最早最后期限优先的策略调度。这个结论既适用于单处理器配置,也适用于多处理器配置
另一个重要的设计问题是抢占。确定启动最后期限后,就可使用非抢占式调度程序。这种情况下,当实时任务完成必须执行的部分或关键部分后,它负责阻塞自身,以使其他实时启动最后期限能够得到满足。对于具有完成最后期限的系统,抢占策略是最适合的
下面给出一个具有完成最后期限的周期性任务调度的例子。考虑两个传感器A和B收集并处理数据的一个系统。A每20ms收集一次数据,B每50ms收集一次数据。处理A的数据需要10ms,处理B的数据需要25ms的时间
在该例中,通过在每个抢占点上优先调度与最后期限最邻近的进程,可满足系统的所有要求,由于任务是周期性的可预测的,因此可以使用静态表调度法
现在考虑处理具有启动最后期限的非周期性任务的方案,下图给出了这样一个例子,它由5个任务组成,每个任务的执行时间为20ms
一种最直接的方案是永远调度具有最早最后期限的就绪任务,并让该任务一直运行到完成,但是这种处理非周期性任务,特别是在处理有启动最后期限的非同期性任务时,是很危险的。若在任务就绪前事先直到最后期限,则可对该策略进行改进以提高性能。这种策略称为有自愿空闲时间的最早最后期限,具体操作如下:总是调度最后期限最早的合格任务,并让该任务运行直到完成。合格的任务可以是未就绪的任务,因此可能会导致即使有就绪任务,处理器仍保持空闲的情形,我对合格的理解是,有点像动态规划,找到一种能够让所有任务都能够在最后期限之前运行,就比如这个例子中先运行B可能是因为判断A如果运行则会导致B错过,所以先让处理器空闲
10.2.5 速率单调调度
为周期性任务解决多任务调度冲突的一种优秀方法是速率单调调度(RMS)。RMS基于任务的周期给它们指定优先级
在RMS中,最短周期的任务具有最高优先级,次短周期的任务具有次高优先级。若将任务的优先级视为速率的函数,则它就是一个单调递增函数,速率单调单调因此而得名,如下图所示
下图说明了周期性任务的相关参数
任务周期T,指从该任务的一个实例到达下一个实例到达之间的时间总量。典型情况下,任务周期的末端也是该任务的硬最后期限,尽管有些任务可能具有更早的最后期限。执行时间C是所需要的处理时间总量。在单处理器系统中,执行时间必须不大于其周期。若一个周期性任务总是运行到完成,即该任务的任何一个实例都不曾因为资源缺乏而被拒绝服务,则该任务的处理器利用率为U=C/T
衡量周期调度算法有效性的一个标准是,看它是否能保证满足所有硬最后期限。假设有n个任务,每个任务都有固定的周期和执行时间。要满足所有的最后期限,必须保持下面的不等式成立:
各个任务的处理器利用率的总和不能超过1,1对应于处理器的总利用率。对于RMS,下面的不等式成立:
下表给出了这个上界的一些值。任务数量增加时,调度上界收敛于ln2约等于0.693
这个式子中的上界对最早最后期限调度也成立。因此,有可能实现更大的处理器利用率,最早最后期限调度适用于更多的周期性任务,然而,RMS已被广泛用于工业应用中。对此给出了如下解释:
1.实践中的性能差别很小。利用率实际上常常能达到90%
2.大多数硬实时系统也有软实时部件,它们可以在低优先级上执行,占用硬实时任务的RMS调度中未使用的处理器时间
3.RMS易于保障稳定性。当一个系统由于超载和瞬时错误而不能满足所有的最后期限时,对一些基本任务,只要它们可调度,它们的最后期限就应得到保证。在RMS中,可以让基本任务具有较短的周期,或修改RMS优先级来说明基本任务来实现。对于最早最后期限调度,周期性任务的优先级从一个周期到另一个周期是不断变化的,这就使得基本任务的最后期限很难得到满足
10.2.6 优先级反转
当系统内的环境迫使一个较高优先级的任务等待一个较低优先级任务时,就会发生优先级反转。比如一个地优先级任务被某个资源阻塞,且一个高优先级也被同一个资源阻塞时,就会发生优先级反转,高优先级任务被置为阻塞态,低优先级任务迅速使用完资源并释放资源后,高优先级任务可能很快被唤醒,并在实时限制内完成
更加严重的一种情况成为无界限优先级反转,在这种情况下,优先级反转的持续时间不仅取决于处理共享资源的时间,还取决于其他不相关任务的不可预测行为。下面举一个探路者软件例子。探路者软件包含如下优先级递减的三个任务:
T1:周期性地检查太空船和软件的状况
T2:处理图片数据
T3:随机检测设备的状态
在T1执行完后,将计时器重新初始化为最大值。若计时器计时完毕,那么处理器终止,所有的服务都会重启。恢复过程需要一天时间。T1和T3共享了一个通用的数据结构,该数据结构由一个二元信号量s保护
下图显示了优先级反转的顺序
如果按照上述的方案来执行,那么T1最后才运行完,如果T2和T3的执行时间太长就会导致系统莫名其妙的重启,其中的数据也就没了
实际系统中用到了两种替代方法来避免无界限的优先级反转:优先级继承和优先级置顶
优先级继承 的想法是优先级较低的任务继承任何一个与其共享同一个资源的优先级较高的任务的优先级。当高优先级任务在资源上被阻塞时,立即更改优先级。资源被低优先级任务释放时,结束改变,下图显示了优先级继承的解决方案
在优先级置顶方案中,优先级与每个资源相关联,资源的优先级被设定为比使用该资源的具有最高优先级的用户的优先级要高一级。调度程序然后动态地将这个优先级分配给任务访问该资源的任务。一旦使用完资源,优先级就返回到以前的值
10.7 小结
对于紧耦合的多处理器,多个处理器可以使用同一个内存,在这种配置中,调度结构更加复杂。性能研究表明,在多处理器系统中,不同调度算法间的差别并不是很重要
实时进程或任务是指该进程的执行与计算机系统外部的某些进程,功能或事件集合有关,且为了保证有效和正确地与外部环境交互,必须满足一个或多个最后期限。实时操作系统是指能够管理实时进程的操作系统。在实时操作系统中,传统的调度算法原则不再适用,关键因素是满足最后期限。很大程度上依赖于抢占及相对最后期限进行反应的算法,适合于这种情况
第十一章 I/O管理和磁盘调度
输入/输出可能是操作系统设计中最困难的部分,由于存在许多不同的设备及这些设备的应用,因此很难有一种通用的,一致的解决方案
11.1 I/O设备
计算机系统中参与I/O的外设大体上分为如下三类:
- 人可读:适用于计算机用户间的交互,如打印机和终端。终端又包括显示器和键盘,以及其他一些可读的设备,如鼠标
- 机器可读:适用于与电子设备通信,如磁盘驱动器,USB密钥,传感器,控制器和执行器
- 通信:适用于与远程设备通信,如数字线路驱动器和调制解调器
各类设备之间有很大的差别,甚至同一类别内的不同设备之间也有很大差异。主要差别包括:
- 数据传送速率:数据传送速率可能会相差几个数量级,下图给出了一些例子
- 应用:设备用途对操作系统及其支撑设施中的软件和策略都有影响。例如用于存储文件的磁盘需要文件管理软件的支持。再如,终端既可被普通用户使用,也可被系统管理员使用。这两种使用情况隐含了不同的特权级别,而且可能在操作系统重要拥有不同的优先级
- 控制的复杂性:打印机仅需要一个相对简单的控制接口,而磁盘的控制接口则要复杂得多,这些差别对操作系统的影响,在某种程度上被控制该设备的I/O模块的复杂性所过滤
- 传送单位:数据可按字节流或字符流的形式传送,也可按更大块传送
- 数据表示:不同的设备使用不同的数据编码方式,这些差别包括字符编码和奇偶校验约定
- 错误条件:随着设备的不同,错误的性质,报告错误的方式,错误造成的后果及有效的响应范围,都各不相同
这些差异使得不管是从操作系统的角度,还是从用户进程的角度,都很难找到一种统一的,一致的I/O解决方法
11.2 I/O功能的组织
下面总结了执行I/O的三种技术:
- 程序控制I/O:处理器代表一个进程给I/O模块发送一个I/O命令;该进程进入忙等待,直到操作完成才能继续执行
- 中断驱动I/O:处理器代表进程向I/O模块发出一个I/O命令。有两种可能:若来自进程的I/O指令是非阻塞的,则处理器继续执行发出I/O命令的进程的后续指令。若I/O指令是阻塞的,则处理器执行的下一条指令来自操作系统,它将当前的进程设置为阻塞态并调度其他进程
- 直接存储器访问(DMA):一个DMA模块控制内存和I/O模块之间的数据交换。为传送一块数据,处理器给DMA模块发送请求,且只有在整个数据块传送结束后,它才被中断
下表描述了这三种技术之间的关系。在大多数计算机系统中,DMA是操作系统必须支持的主要数据传送形式
11.2.1 I/O功能的发展
随着计算机系统的发展,单个部件的复杂度和完善度也随之增加,这在I/O功能上表项得最为明显。I/O功能的发展可概况为以下阶段:
1.处理器直接控制外围设备,这在简单的微处理器控制设备中可以见到
2.增加了控制器或I/O模块。处理器使用非中断的程序控制I/O。在这一阶段,处理器开始从外部设备接口的具体细节中分离出来
3.本阶段所使的配置与阶段2相同,但采用了中断方式,处理器无须费时间等待执行一次I/O操作,因而提高了效率
4.I/O模块通过DMA直接控制存储器。仅在传送开始和结束时才需要用到处理器
5.I/O模块有一个单独的处理器,有专门为I/O设计的指令集。中央处理器指导I/O处理器执行内存中的一个I/O程序。I/O处理器在没有中央处理器干涉的情况下取指令并执行指令,这就是恶的中央处理器可以指定一系列的I/O活动,且仅在整个序列执行完成后中央处理器才被中断
6.I/O模块有自己的局部存储器,事实上其本身就是一台计算机。这这种结构可以控制许多I/O设备,并使得需要中央处理器参与的部分降到最小
我们可以看到,随着I/O的发展,中央处理器逐步从I/O任务中解脱出来,因此提高了性能。在最后两个阶段,一个主要变化是引入了可执行程序的I/O模块的概念
阶段4到阶段6中描述的的所有模块,用术语直接存储器访问是最适合的;阶段5中的I/O模块通常称为I/O通道;阶段6中的I/O模块称为I/O处理器。但是有时这两个术语同时适用于这两种情况
11.2.2 直接存储器访问
下图概况地给出了DMA逻辑。DMA单元能够模拟处理器,而实际上能够像处理器一样获得系统总线的控制权。这样做是为了利用系统总线与存储器进行双向数据传送
DMA技术工作流程如下:当处理器想要数据时,通过向DMA模块发送信息来发出命令: - 请求读操作或写操作的信号,通过在处理器和DMA模块之间使用读写控制线发送
- 相关的I/O设备地址,通过数据线传送
- 从存储器中读或向存储器中写的起始地址,在数据线上传送,并由DMA模块保存在其地址寄存器中
- 读或写的字数,也通过数据线传送,并由DMA模块保存在其数据计数寄存器中
然后处理器继续执行其他工作,I/O操作委托给DMA模块。DMA模块向存储器中传送数据块,数据不需要通过处理器。传送结束后,DMA模块给处理器发送一个中断信号。因此,只有在传送开始和结束时才会用到处理器
DMA机制可按多种方法配置,下图给出了一些可能的配置情况
第一个例子尽管配置的开销不大,但是低效的:因为每传送一个字需要两个总线周期(传送请求以及传送)
第二个例子是集成DMA和I/O功能,这可大大降低所需的总线周期数量。DMA逻辑实际上可能就是I/O模块的一部分,或可能是控制一个或多个I/O模块的一个单独模块。
第三个例子使用一个I/O总线连接I/O模块和DMA模块,这就使得DMA模块中I/O接口的数量减少到1,并提供了一种可以很容易地进行扩展的配置。第二个和第三个例子中的系统总线,仅用于DMA模块与内存交换数据以及与处理器交换控制信号。DMA和I/O模块之间的数据交换是脱离系统总线完成的
11.3 操作系统设计问题
11.3.1 设计目标
在设计I/O机制时,有两个最重要的目标:效率和通用性。效率很重要,因为I/O操作通常是计算机系统的瓶颈。与内存和处理器相比,大多数I/O设备的速度都非常低。解决该问题的一种方法是多道程序设计,多道程序设计允许在一个进程执行的同时,其他一些进程等待I/O操作。但是,即使到了计算机拥有大量内存的今天,I/O操作跟不上处理器获得的情况仍然会频繁出现。我们可以使用交换技术,但这技术本身就是一个I/O操作。因此I/O设计的主要任务就是提高I/O效率。目前,因其重要性而最受关注的是磁盘I/O
另一个重要目标是通用性。出于简单和避免错误的考虑,人们希望能用一种统一的方式处理所有设备。这意味着从两个方面都需要统一:一是处理器看待I/O设备的方式,二是操作系统管理I/O设备和I/O操作的方式。由于设备特性的多样性,在实际中很难真正实现通用性。目前所能做的就是用一种层次化,模块化的方式设计I/O操作。这种方法隐藏了大部分I/O设备底层例程中的细节,使得用户进程和操作系统高层可以通过通用函数来操作I/O设备。下面将详细讲述这种方法
11.3.2 I/O功能的逻辑结构
分层的原理是:操作系统的功能可以根据其复杂性,特征时间尺度和抽象层次来分开。把这种原理应用于I/O机制就可以得到下图所示的组织类型,组织的细节取决于设备的类型和应用程序
图中给出了三个最重要的逻辑结构。但某个特定的操作系统可能并不完全符合这些结构,但其基本原则是有效的,且大多数操作系统都通过类似的途径来组织I/O
首先考虑一种最简单的情况。本地外设以一种简单的方式进行通信,如字节流或记录流,像图中的逻辑外部设备图所示,涉及的各层如下所示:
- 逻辑I/O:把设备当作一个逻辑资源来处理,它并不关心实际控制设备的细节。逻辑I/O模块代表用户进程管理的普通I/O功能,允许用户进程使用简单命令与设备打交道
- 设备I/O:请求的操作和数据被转换为适当的I/O指令序列,通道命令和控制器指令。可以使用缓冲技术来提高利用率
- 调度和控制:I/O操作的排队,调度实际上发生在这一层,在这一层处理中断,收集并报告I/O状态。这一层是与I/O模块和设备硬件真正发生交互的软件层
通信端口的I/O结构和逻辑外部设备的几乎一样,主要差别在逻辑I/O模块被通信体现结构取代,通信结构自身也是许多层组成的,比如TCP/IP
文件系统I/O结构用到了未介绍的三层: - 目录管理:这一层,符号文件名被转换为标识符,采用标识符可以直接或间接的访问文件。并且还影响文件目录的用户操作,比如添加删除等操作
- 文件系统:这一层处理文件的逻辑结构及用户指定的操作。并且还管理访问权限
- 物理组织:就像虚存地址必须要转换为物理内存地址一样,考虑到对辅存设备的物理磁道和扇区结构,对于文件和记录的逻辑访问也必须转换为物理外存地址。辅助存储空间和内存缓冲区的分配通常也在这一层
11.4 I/O缓冲
假设某个用户进程需要从磁盘中读入多个数据块,最简单的方法就是对磁盘单元执行一个I/O命令,并等待数据传送完毕,这个等待可以是忙等待,也可以是进程被中断挂机
但是这种方法存在两个问题。首先程序被挂起,等待相对较慢的I/O完成。其次这种方法干扰了操作系统的交换决策。需要的数据必须保留在内存中,如果使用了分页机制,那么需要锁定这些页。并且不可能将进程全部换出,即使操作系统想这么做也不行。还有可能出现但进程死锁,比如进程发出一个I/O命令并且换出到磁盘并挂起,该进程被阻塞,然后I/O事件发生后,I/O操作也被阻塞,因为它等待进程被换入。为避免死锁,发出I/O请求之前,参与I/O操作的用户存储空间必须立即锁定在内存中,即使这个I/O操作正在排队,且一段时间内不被执行
同样的考虑也适用于输出操作
为避免这些开销和低效操作,为了方便起见,输入请求发出时就开始执行输入传送,输出请求发出一段时间后才开始执行输出传送,这项技术称为缓冲
在讨论缓冲前,需要区分两类I/O设备:面向块和面向流的I/O设备。面向块 是指将信息保存在块中,块的大小通常是固定的,传送过程一次传送一块。面向流 的设备以字节流的方式输入/输出数据,它没有块结构。终端,打印机,鼠标和其他指示设备及其他大多数非辅存设备,都属于面向流的设备
11.4.1 单缓冲
操作系统提供的最简单的缓冲类型是单缓冲。如下图所示,当用户进程发出I/O请求时,操作系统为该操作分配一个位于内存中的系统部分的缓冲区
对于面向块的设备,单缓冲方案可描述如下:输入传送的数据被放到系统缓冲区中。当传送完成时,进程把该块移到用户空间,并立即请求另一块,这称为预读,这个假设在大多数情况下是合理的,因为数据通常是被顺序访问。只有在处理序列的最后,才会读入一个不必要的块
相对于无缓冲的情况,这种方法通常会提高系统速度,因为用户进程可以在读取的同时进行处理,并且在读入的过程中,操作系统可以将进程换出,但这增加了操作系统的逻辑复杂度。操作系统必须记录缓冲区的情况。交换逻辑也受到影响:比如操作所涉及的磁盘和用于交换的磁盘是同一个磁盘时,当进行写操作时,操作排队等待将进程换出到同一个设备上是没有任何意义的。若试图换出进程并释放内存,则要在I/O操作完成后才能开始,而在这时,把进程换出到磁盘已经不再合适
类似的考虑也适用于面向块的输出。当准备将数据发送到一台设备时,首先把这些数据从用户空间复制到系统缓冲区,它最终是从系统缓冲区中被写出的。发请求的进程现在可以自由地继续执行,或者在必要时换出
假设T是输入一个数据块所需要的时间,C是两次输入请求之间所需的计算时间。无换出时,操作是串行执行的,每块的执行时间为T+C。有一个缓冲区时,执行时间为max[C,T]+M,其中M是把数据从系统缓冲区复制到用户内存所需的时间。在大多数情况下,使用单缓冲时每块的执行时间明显少于没有缓冲的情况
对于面向流的I/O。单缓冲方案能以每次传送一行的方式或每次传送一字节的方式使用。每次传送一行适用于滚动模式的终端。每次传送一字节适用于表格模式终端,每次击键对它来说都很重要,许多其他外设如传感器和控制器都属于这种类型
对于每次传送一行的I/O,可以用缓冲区保存单独一行数据。在输入期间用户进程被挂起,等待整行的到达。对于输出,用户进程可以把一行输出放在缓冲区中,然后继续执行。它不需要挂起,除非在第一次输出操作的缓冲区内容清空之前,又需要发送第二行输出。对于每次传送一字节的I/O,操作系统和用户进程的交互参照第五章讲述的生产者/消费者模型进行
11.4.2 双缓冲
单缓冲方案的改进版可为操作分配两个系统缓冲区,如下图所示。在一个进程向一个缓冲区中传送数据的同时,操作系统正在清空另一个缓冲区,这种技术称为双缓冲或缓冲交换
对于面向块的传送,我们可以粗略地估计执行时间为max[C,T]。因此,若C小于等于T,则有可能使面向块的设备全速运行;另一方面,若C>T,则双缓冲能确保该进程不需要等待I/O(因为缓冲区总是满的)。在任何清空下,双缓冲的性能与单缓冲相比都有所提升,但这种提升是以增加复杂性为代价的
对于面向流的输入,我们再次面临两种可选操作模式。对于每次传送一行的I/O,用户进程不需要为输入或输出挂起,除非该进程的运行速度超过了双缓冲的速度。对于每次传送一个字节的操作,双缓冲与具有两倍长度的单缓冲相比,并无特别的优势。这两种情况都采用生产者/消费者模型
11.4.3 循环缓冲
双缓冲方案能平滑I/O设备和进程之间的数据流。如果我们关注的重点是某个特定进程的性能,那么通常希望相关I/O操作能够跟得上这个进程。如果进程需要执行大量的I/O操作,那么仅有双缓冲并不够,此时通常要使用多于两个缓冲区的方案来弥补的不足
使用两个以上的缓冲区时,这组缓冲区本身会被当作循环缓冲区,如下图所示,其中每个缓冲区是这个循环缓冲区的一个单元。这就是第五章研究的有界缓冲区生产者/消费者模型
11.4.4 缓冲的作用
缓冲是用来平滑I/O需求的峰值的一种技术,但在进程的平均需求大于I/O设备的服务能力时,缓冲再多也无法让I/O设备与该进程一直并驾齐驱。但在有多个缓冲区,所有的缓冲区也终将会被填满,进程每处理一大块数据后不得不等待。但在多道程序设计环境中,当存在多种I/O活动和多种进程活动时,缓冲是提高操作系统效率和单个进程性能的一种方法
11.5 磁盘调度
当前磁盘的速度比内存慢了几个数量级,并且这一差距未来仍将继续存在。因此,磁盘存储子系统的性能至关重要。目前,许多研究正致力于如何提高磁盘存储子系统的性能。本节着重介绍一些关键问题和最重要的方法
11.5.1 磁盘性能参数
磁盘I/O的实际操作细节取决于计算机系统,操作系统以及I/O通道和磁盘控制器硬件的特性。下图给出了磁盘I/O传送的一般时序图
磁盘驱动器工作时,磁盘以某个恒定的速度旋转。磁头定位到磁道所需要的时间称为寻道时间。在任何情况下,一旦选择好磁道,磁盘控制器就开始等待,直到适当的扇区旋转到磁头处。磁头到达扇区开始位置的时间称为旋转延迟。寻道时间和旋转延迟的总和为存取时间,这是达到读或写位置所需要的时间。一旦磁头定位完成,磁头就通过下面旋转的扇区,开始执行读操作或写操作,这正是操作的数据传送部分。传输所需的时间是传输时间
在某些高端服务器系统中,使用了一种称为旋转定位感知(RPS)的技术。简单来说这个技术就是不仅看进程顺序来执行服务,而且看哪个服务能最先完成(比如在餐厅中不仅看座位来服务,有时候也会看炒菜的时间,可能有点不准确,还是看书更加权威)。当RPS失败时,也会产生一个额外延迟,因此时间轴也必须添加这个因素
寻道时间 寻道时间是将磁头臂移动到指定磁道所需要的时间。事实证明,这个时间很难减少。寻道时间由两个重要部分组成:最初启动时间,以及访问臂达到一定的速度后,横跨那些其必须跨越的磁道所需要的时间。遗憾的是,横跨磁道的时间不是磁道数量的线性函数,它还包括一个稳定时间(从磁头定位于目标磁道直到确认磁道标识之间的时间)
许多提升都来自更小更轻的磁盘部件
旋转延迟 旋转延迟是指将磁盘的待访问地址区域移动到读/写磁头可访问的位置所需要的时间
传输时间 向磁盘传送或从磁盘传送的时间取决于磁盘的旋转速度,并用如下公式表示:
T表示传输时间;b表示要传送的字节数;N表示一个磁道中的字节数;r表示旋转速度,单位为转/秒。因此,总平均存取时间表示为
其中Ts为平均寻道时间
时序比较 我们考虑两个不同的I/O操作,假设有一个典型的磁盘,其平均寻道时间为4ms,转速为7500rpm,每个磁道有500个扇区,每个扇区有512字节。假设读取一个包含2500个扇区,大小为1.28MB的文件。假设第一个I/O操作假设文件尽可能紧凑地保存在磁盘上,假设要读取的数据占据了五个相邻磁道的所有扇区,读第一个磁道的时间如下:
在后续操作中,不需要考虑寻道时间,只需要处理旋转延迟,因此,后面的磁道能在4+8=12ms内读入,那么读取整个文件只需要16+4*12=64ms=0.064s
现在考虑随机访问的情况下可得
显然,从磁盘读扇区的顺序对I/O的性能有很大的影响。在文件访问需要读或写多个扇区的情况下,我们可以对数据在扇区上的存储方式进行一定的控制。然而,即使在访问一个文件的情况,在多道程序环境中,也会出现I/O请求竞争同一个磁盘的情况。 因此,在完全随机访问的磁盘上,分析可以提高磁盘IO性能的途径是非常值得的
11.5.2 磁盘调度策略
产生性能差异的原因可以追溯到寻道时间。如果扇区访问请求包括随机旋转磁道,那么磁盘I/O系统的性能会非常低,为提高性能,需要减少在寻道时间上的时间
考虑在多道程序环境中的一种典型情况,即操作系统为每个I/O设备维护一个请求队列。队列中可能有来自多个进程的许多I/O请求。若随机地从队列中选择项目,则磁道完全是随机访问,这种情况性能最差。随机调度可用于于其他技术进行对比,以评估这些技术
下图比较了不同调度算法对I/O请求序列的性能表项。纵轴表示磁盘上的磁道;横轴表示时间,或跨越磁道的数量,在该例中,假设磁盘有200个磁道,磁盘请求队列中是一些随机请求。并且下表列出了不同算法的平均寻道长度
先进先出(FIFO) 最简单的调度是先进先出调度,它按顺序处理队列中的项目。该策略的优点是公平,每个请求都会得到处理,且按接收到的顺序处理。使用FIFO,若只有需要访问某些进程,且大多数请求都是访问簇聚的文件扇区,则有望达到较好的性能。但是,若有大量进程竞争一个磁盘,则这种技术在性能上往往接近于随机调度。因此,需要考虑一些更复杂的调度策略。下表列出了许多这类策略
优先级 对于基于优先级(PRI)的系统,有关调度的控制并不包含磁盘管理软件的控制。这种方法并不会优化磁盘的利用率,但能满足操作系统的其他目标。通常较短的批作业和交互作业的优先级较高,而较长计算时间的长作业的优先级较低。这就使得大量短作业能够迅速地通过系统,并能提供较好的交互响应时间。但是,长作业可能不得不等待过长的时间。这种策略可能会导致部分用户采用对抗的手段:把作业分成小块,以回应系统的这种策略。对于数据库系统,这类策略往往会使得性能较差
后进先出(LIFO) 优先处理最新请求的策略具有一定的价值。在事务处理系统中,由于顺序读取文件的缘故,把设备分配给最后到来的用户,可减少磁臂的运动,甚至没有磁臂运动。但是由于大量工作而一直处于忙碌状态时,明显会出现饥饿的可能性。一旦任务在队列中发出I/O请求,并从队头退出,该任务就不能再回到队头,除非前面的队列清空(这是为了减少饥饿,防止任务出去后反复进来退出,导致先进的任务一直不能运行)
上述的几个处理的调度方式仅基于队列或请求者的属性。如果调度程序知道当前轨道的位置,那么可以采用这些基于请求项的调度
最短服务时间优先 最短服务时间优先(SSTF)处理选择使磁头臂从当前位置开始移动到最少的磁盘I/O请求。因此,这个策略总是选择导致最小寻道时间的请求。当然,这并不能保证平均寻道时间最小,但能提供比FIFO更好的性能。由于磁头臂可沿两个方向移动,因此能使用一种随机选择算法解决距离相等的情况
SCAN 除FIFO外,迄今为止描述的所有策略都可能使某些请求直到整个队列为空时才可完成,这就可能导致饥饿的情况,为了避免出现这类情形,一种比较简单的方法是SCAN(扫描)算法。因其与电梯类似,故而也称电梯算法
SCAN要求磁头臂仅沿一个方向移动,并在途中满足所有未完成的请求,直到它到达这个方向上的最后一个磁道,或者在这个方向上没有其他请求为止,后一种有时称为LOOK策略。接着反转服务方向,沿相反方向扫描,同样按顺序完成所有请求
从上图我们可以看出,SCAN策略的行为和SSTF策略非常类似。但这仅是一个静态的例子,队列在这期间不会增加新的请求。即使队列动态变化,除非请求模式不符合常规,否则SCAN仍然类似于SSTF,但SCAN策略对最近横跨过的区域不公平,因此它并不像SSTF和LIFO那样能很好的利用局部性
SCAN策略偏爱那些请求接近最靠里或最靠外的磁道的作业,且偏爱最近的作业,第一个问题可通过C-SCAN策略得以避免,第二个问题可通过N步扫描策略解决
C-SCAN 这个策略把扫描限定在一个方向上。当访问到沿某个方向的最后一个磁道时,磁头臂返回到磁盘相关方向末端的磁道,并再次开始扫描,这就减少了新请求的最大延迟。对于SCAN,如果从最里面的磁道扫描到最外面的磁道的期望时间为t,则这个外设上的扇区的期望服务间隔为2t。而对于C-SCAN,该间隔为t+Smax,Smax是最大寻道时间
N步SCAN和FSCAN 对于SSTF,SCAN和C-SCAN,磁头臂可能很长一段时间内都不会移动。如果一个或多个进程对一个磁道有较高的访问频率,那么它们就可以重复请求来垄断整个设备。为了避免这种情况,所以才有了上述的两种方法
N步SCAN把磁盘请求队列分为长度为N的几个子队列,每次用SCAN处理一个子队列,在处理某个队列时,新请求必须添加到其他某个队列中。在扫描的最后,剩下的请求数小于N,则它们全在下一次扫描时处理。对于较大的N值,N步SCAN的性能接近于SCAN;当N=1时,实际上就是FIFO
FSCAN是一种使用两个子队列的策略。扫描开始时,所有请求都在一个队列中,另一个队列为空。在扫描过程中,所有新到的请求都放入另一个队列。因此,对新请求的服务延迟到处理完所有老请求之后
11.6 RAID
前面提到过,辅存性能的提高速度远低于处理器和内存性能的提高速度,这会使得磁盘存储系统可能会成为提高整个计算机系统性能的关键
所以设计人员想到并行使用多个组件可获得额外的性能提高,在磁盘存储器的情况下,这就导致了独立并行运行的磁盘阵列的开发。通过多个磁盘,多个独立的I/O请求可并行地处理,只要它们所需的数据驻留在不同的磁盘中。此外,若要访问的数据库分布在多个磁盘上,I/O请求也可并行执行
使用多个磁盘,组织数据的方法有多种,且可通过增加冗余度来提高可靠性。这就导致难以开发在多个平台和操作系统中均可使用的数据库方案。所幸的是,关于多磁盘数据库设计已形成了一个标准方案,称为独立磁盘冗余阵列(RAID)。RAID方案包括从0到6的7个级别。这些级别并不隐含一种层次关系,但表明了不同的设计体系结构。这些设计体系结构具有三个共同的特性:
1.RAID是一组物理磁盘驱动器,操作系统把它视为单个逻辑驱动器
2.数据分布在物理驱动器阵列中,这种设计称之为条带化
3.使用冗余磁盘容量保存奇偶校验信息,保证一个磁盘失效时,数据具有可恢复性
不同的RAID级别中,第二个和第三个特性的细节不同;RAID0和RAID1不支持第三个特性
RAID策略用多个小容量驱动器代替大容量磁盘驱动器,并以能同时从多个驱动器访问数据的方式来分布数据,因此提高了I/O的性能,并能更容易地增加容量
RAID特有的贡献是有效地解决了对冗余的需求。尽管RAID允许多个磁头和动臂机构同时操作,但却增大了失效的概率。所以RAID通过奇偶校验信息来从磁盘的失效中恢复所丢失的数据
下面开始分析RAID的每个级别。下表总结了RAID的7个级别。其中,I/O的性能以下面两种能力表示:数据传送的能力或移动数据的能力,以及I/O请求率或I/O请求的完成能力,因为RAID不同级别之间的性能差别主要表现在这两种能力上。下图给出了一个例子说明分别使用7种RAID方案,在无冗余的情况下需要4个磁盘的数据容量
在所介绍的7个RAID级别中,只有4个是常用的:RAID0,RAID1,RAID5和RAID6
11.6.1 RAID级别0
RAID0并不是RAID家族中的真正成员,因为它未用冗余来保护数据。但是许多应用程序都采用了这种方式,比如超级计算机上的应用,超级计算机关注的是性能和容量,可靠性没那么重要
RAID0与使用单个大磁盘相比,有明显的优点:两个不同的I/O请求为两块不同的数据时,被请求的块很有可能在不同的磁盘上,因此两个请求可以并行发出,从而减小I/O排队时间
RAID0与所有RAID一样,并不是简单把数据分部在磁盘阵列中:数据呈条状分布在所有可用磁盘中。一个条带可以是一个物理块,扇区或其他某种单元。在一个n磁盘阵列中,最初的n个逻辑条带保存n个不同磁盘的第一个条带中,假设有4个逻辑条带,那么就分布在磁盘1 2 3 4中的第一个条带中。这样布局的优点是,如果一个I/O请求由多个逻辑上连续的条带组成,那么请求就可以并行处理,大大减少I/O传输时间
RADI0时间高数据传送能力 对于需要高传送率的应用程序,必须满足两个要求:首先,高传送能力必须存在于整个路径中,其次应用程序必须产生能够有效使用磁盘阵列的I/O请求。如果请求的是大量逻辑上连续的数据,可以满足第二个要求。对于第一个要求,相较于单个磁盘的传送,并行传送可以增加有效的传送速率
RAID0时序高速I/O请求率 在面向事务的环境中,用户对响应时间的关注超过了对传送速率的关注。对于少量数据的单独I/O请求,I/O时间由磁头移动和磁盘移动决定
在事务处理环境中,每秒可能有上百条I/O请求。磁盘阵列可通过在多个磁盘中平衡I/O负载来提供较高的执行速率。这一性能还会受到条带大小的影响。如果条带相对较大,则一个I/O请求可能只包括对一个磁盘的访问,多个正在等待的I/O请求可以并行处理,因此能减少每个请求的排队等待时间
11.6.2 RAID级别1
RAID1通过临时复制所有数据来实现冗余。这种情况下,每个逻辑条带映射到两个单独的物理磁盘,因此每个阵列中的每个磁盘都有一个包含相同数据的镜像磁盘
RAID的组织有许多较好的特征:
1.读请求可由包含被请求数据的任何一个磁盘提供服务,而不管哪个磁盘的访问时间
2.写请求需要对两个相应的条带进行更新,但这可并行完成。因此,写性能由两个写操作中较慢的那个决定,但RAID1中并无写性能损失
3.从失效中恢复很简单。当一个驱动器失效时,可以从第二个驱动器访问数据
RAID1的主要缺点是成本问题,它需要两倍于所支持逻辑磁盘的空间。因此,RAID1驱动器通常用于保存系统软件和数据以及其他极其重要的文件
在面向事务处理的环境中,有许多读请求时,RAID1可以实现高I/O请求速度。在这种情况下,RAID1的性能接近于RAID0的两倍。但是,有相当一部分I/O请求是写请求时。与RAID0相比,RAID1不会有明显的性能优势。但对于那些对数据传送敏感的应用程序,且大部分I/O请求为读请求时,RAID1会提供更好的性能,如果应用程序能把每个读请求分开使所有磁盘成员都参与进来,就会提高性能
11.6.3 RAID级别2
RAID2和RAID3都使用了一种并行访问技术。在并行访问阵列中,所有磁盘成员都参与每个I/O请求的执行。通常,所有磁盘的轴心是同步的,这就使得每个磁头都处于各自磁盘中的同一位置
RAID2和RAID3的条带非常小,通常只有一个字节或一个字,RAID2对每个数据磁盘中的相应位置都计算一个错误校验码,并且这个码位保存在多个奇偶校验磁盘的相应位中
RAID2仍然相当昂贵。对一次读,所有磁盘都被同时访问到,被请求的数据以及相关的校正码被送到阵列控制器。如果发生一位错误,可以检测并改正,使得读操作的存取时间不会减慢。写操作必须访问所有数据磁盘和奇偶校验磁盘
RAID2仅是在可能发生许多磁盘错误的环境中的一种有效选择。单个磁盘和磁盘驱动器的可靠性很高时,RAID2往往会表现出矫枉过正,因而不切实际
11.6.4 RAID级别3
RAID3的组织方式相较于RAID2,只需要一个冗余磁盘。RAID3采用并行访问,数据分布在较小的条带中,RAID3为所有数据磁盘中的同一位置的集合计算一个简单的奇偶校验位,而不是错误校正码
冗余性 发生磁盘故障时,访问1奇偶校验驱动器,并从其余设备中重建数据,丢失的数据可以恢复到新驱动器上,并继续执行操作
数据重建很简单,假设X1失效,用下面公式可以恢复,X4为奇偶校验磁盘
通过公式我们可以看到,条带的数据内容可以由其他的条带来恢复,这对3-6级别也使用
磁盘失效时,在缩减模式下仍然能得到所有数据,对于读操作,丢失的数据可在运行中通过异或运算重新生成;当进行写操作时,必须为以后的重新生成维护奇偶校验的一致性,要返回到完全操作,就要替换失效的磁盘,并在新磁盘中重新生成失效磁盘的全部内容
性能 由于数据分成了很小的条带,因此RAID3能实现非常高的数据传送率。对大数据量的传送,性能的提高非常明显,另一方面,由于一次只能执行一个I/O请求,因此在面向事务处理的环境中性能并不乐观
11.6.5 RAID级别4
4-6级别使用了一种独立的访问技术,在独立访问阵列中,每个磁盘成员都单独运转,因此对请求的并行度更高,所以更适合于较高I/O请求速度的应用程序,而相对不太适合需要较高数据传送率的应用程序
对于RAID4到RAID6,数据条带相对较大。RAID4中,每个数据磁盘中相应的条带计算一个逐位奇偶校验,奇偶校验保存在奇偶校验磁盘的相应条带中
执行一个很小的I/O写请求时,会引发性能损失,因为其不仅要更新数据还要更新相对应的奇偶校验位
要计算新的奇偶校验,阵列管理软件必须读取旧用户条带和旧奇偶校验条带,然后用新数据和新近计算的奇偶校验更新这两个条带。因此,每个条带的写操作包括两次读和两次写
对于涉及所有磁盘驱动器条带的大数据量I/O写的情况,只需使用新数据位进行计算就可得奇偶校验,因此奇偶校验驱动器能和数据驱动器一起并行地进行更新,因此不需要额外的读和写
对于任何一种情况,每次写都必须包含奇偶校验磁盘,因此奇偶校验磁盘有可能成为瓶颈
11.6.6 RAID级别5
RAID5的组织类似于RAID4,不同之处在于RAID5把奇偶校验条带分布在所有磁盘中,典型的方案是循环分配。奇偶校验条带分布在所有驱动器上,可避免RAID4中一个奇偶校验磁盘的潜在I/O瓶颈问题
11.6.7 RAID级别6
RAID6方案采用了两种不同的奇偶校验计算,并保存在不同磁盘的不同块中,因此需要N+2个磁盘。上图说明了这种方案,P和Q是两种不同的数据校验算法,其中一种就是RAID4和RAID5使用的异或计算,另一种是独立数据校验算法。这就使得即便有两个包含用户数据的磁盘发生错误,也可以重新生成数据
RAID6的优点是它能提供极高的数据可用性。三个磁盘同时失效时数据才会丢失,但是另一方面,RAID6会导致严重的写性能损失,因为每次写操作都会影响两个校验块,性能测试表明,与RAID5相比,RAID6控制器会有30%以上的整体写性能损失。但是读性能与RAID5相当
11.7 磁盘高速缓存
高速缓存用于内存和存储器之间,这种原理也适用于磁盘存储器。磁盘高速缓存是内存中为磁盘扇区设置的一个缓冲区,它包含有磁盘中某些扇区的副本。当出现对特定扇区的I/O请求时,会进行检测。若在,则该请求通过高速缓存来满足,若不在,则把请求的扇区从磁盘读到磁盘高速缓存中
11.7.1 设计考虑因素
有许多设计问题需要考虑。首先就是I/O请求的资源在高速缓存中,如何将数据给进程。可以在内存中把这一块数据从高速缓存中传送到分配给该用户进程的存储空间中,或简单地使用一个共享内存,传送指向数据的指针。后一种方式节省了传输时间
第二个要解决的问题是置换策略。当一个新扇区被读入磁盘高速缓存时,必须换出一个已存在的块。这时需要一个页面置换算法。最常用的的算法是最近最少使用算法(LRU)。逻辑上,高速缓存由一个关于块的栈组成,最近访问过的块位于栈顶。当一块被访问时,从栈中的位置移到栈顶。当需要置换时,把位于栈低的那一块移出,将新来的块压入栈顶。当然,这并不需要在内存中真正移动这些块,因为有一个栈指针与高速缓存相关联
另一种可能的算法是最不常使用页面置换算法(LFU) :置换集合中访问次数最少的块。LFU可通过给每个块关联一个计数器来实现。当一个块被读入时,其计数器被指定为1;每次访问到这一块,其计数器都增1,需要置换时,选择计数器值最小的块。直觉上LFU比LRU更合适,因为LFU使用了关于每个块的更多相关信息
简单的LFU算法有以下问题:可能存在一些块,整体上看可能很少出现访问,但被访问时,可能会在一段很短的时间内出现很多重复访问,导致访问计数器的值很高。因此受局部性影响,LFU算法不是一个好的的置换算法
为克服LFU的这些缺点,提出了一种基于频率的置换算法,为简单起见,先考虑一个简化的版本。栈顶的一部分留做一个新区。出现一次命中时,被访问的块移到栈顶,若该块已在新区中,则其访问计数器不会增加,否则计数器加1。当需要置换时,访问计数器值最小,并且不在新区中的块被选择换出,如果存在多个这样的块,就选择近期最小使用的块
但是这存在以下问题:假设一个块换入到新区,并且频繁的使用,那么其计数器的值一直为1,假设访问5次后使用其它的块,那么该块就不新区了,若该块未被很快地再次访问,就很可能被置换,因为其计数器的值一直是1,但是它可能是频繁访问的块,在不久后可能又要换入,又换入到新区,这就导致一个经常使用的块被频繁的换入换出,浪费了时间
对于这个问题的进一步改进方案是,把栈分为三个区:新区,中间区和老区,如下图所示
同样的,位于新区中的块,其访问计数器不会增加,但是,只有老区中的块才符合置换条件。如果有足够大的中间区,这就使得相对比较频繁地被访问到的块,有机会增加自己的访问计数器,这种改进后的策略与简单的LRU或LFU相比,性能有明显提升
不论采用哪种特殊的置换策略,置换都可按需发生或预先发生。对前一种情况,只有当需要用到存储槽时才置换这个扇区。对后一种情况,一次可释放多个存储槽。使用后一种方法的原因与写回扇区的要求相关。如果一个扇区被读入高速缓存且仅用于读,那么当它被置换时,并不需要写回到磁盘。但是如果该扇区已被修改,那么就需要被换出之前必须写回到磁盘,这时成簇地写回并按顺序写来降低寻道时间是非常有意义的
11.7.2 性能考虑因素
高速缓存的性能问题可简化为是否可以达到某个给定的未命中率。这取决于访问磁盘的局部性行为,置换算法和其他设计因素。但是,未命中率主要是磁盘高速缓存大小的函数。下图概况了使用LRU的多个研究结果,并且给出了基于频率的置换算法的模拟研究结果。比较这两幅图,可以得出这类性能评估的风险
这些图形看上去表明LRU的性能优于基于频率的置换算法,但在使用相同高速缓存结构,但在使用相同高速缓存结构和相同访问模式时,再对它们进行比较,会发现基于频率的置换算法优于LRU。因此,访问模式的顺序和相关的设计问题,如块大小,会将对性能产生重要的影响
11.11 小结
计算机系统和外界的接口是它的I/O体系结构。I/O体系结构的设计目标是提供一种系统化的方法来控制与外界的交互,并为操作系统提供有效管理I/O所需要的信息
I/O功能通常划分为多层。较低的层处理与物理功能相关的细节,较高的层以逻辑方式处理I/O,因此,硬件参数的变化不需要影响大多数I/O软件
I/O的一个重要方面是使用缓冲区。缓冲区用I/O实用进程控制。缓冲可以平滑计算机系统内部速度和I/O设备速度间的差异。使用缓冲区还可把实际的I/O传送从应用程序进程的地址空间分离出来,这就使得操作系统能够更加灵活地执行存储管理功能
磁盘I/O对整个系统的性能影响最大。为提高其性能,最广泛的两种方法是磁盘调度和磁盘高速缓存
在任何时候,总有一个关于同一磁盘上的I/O请求的队列,磁盘调度的目的是按某种方式满足这些请求,并使磁盘的机械寻道时间最小从而提高性能。那些被挂起请求的物理布局和对局部性的考虑在调度中起着主要作用
磁盘高速缓存是一个缓冲区,它通常保存在内存中。由于局部性原理,使用磁盘高速缓存能充分减少内存和磁盘之间I/O传送的块数
第十二章 文件管理
在大多数应用中,文件都是核心元素。大多数应用程序的输入都是通过文件来实现的,实际上所有应用程序的输出都保存在文件中,因为文件便于长期存储信息以及用户或应用程序将来访问信息
12.1 概述
12.1.1 文件和文件系统
从用户的角度来看,文件系统是操作系统的重要组成部分。文件系统允许用户创建称为文件的数据集。文件拥有一些理想的属性:
- 长期存在:文件存储在硬盘或其他辅存中
- 可在进程间共享:文件有名字,具有允许受控共享的相关访问权限
- 结构:取决于具体的文件系统,一个文件具有针对某个特定应用的内部结构。此外,文件可组织为层次结构或更复杂的结构,以反映文件之间的关系
文件系统不但提供存储数据的手段,而且提供一系列对文件进行操作的功能接口。典型的操作如下: - 创建:在文件结构中定义并定位一个新文件
- 删除:从文件结构中删除并销毁一个文件
- 打开:进程将一个已有文件声明为“打开”状态,以便允许该进程对这个文件进行操作
- 关闭:相关进程关闭一个文件,以便不再能对该文件进行操作,直到该进程再次打开它
- 读:进程读取文件中的所有或部分数据
- 写:进程更新文件,要么通过添加新数据扩大文件的尺寸,要么通过改变文件中已有数据项的值
文件系统通常为文件维护一组属性,包括所有者,创建时间,最后修改时间和访问权限
12.1.2 文件结构
讨论文件时通常要用到如下4个术语:
域,文件,记录,数据库
域 是基本的数据单元。一个域包含一个值,域克通过其长度和数据类型来描述。域的长度可以是定长的或变长的,具体取决于文件的设计。对于后一种情况,域通常包含两个或三个子域:要保存的实际值,以及某些情况下的域长度。在其他情况下,域之间特殊的分隔符暗示了域的长度
记录 是一组相关域的集合,可视为应用程序的一个单元。同样记录的长度也可以是定长的或变长的,取决于设计。如果记录中的某些域是变长的,或域的数量可变,则该记录是变长的,对于变长的,每个域通常都有一个域名。对于这两种情况,整条记录通常都包含一个长度域
文件 是一组相似记录的集合,它被用户和应用程序视为一个实体,并可通过名字访问。文件有唯一的一个文件名,可被创建或删除。访问控制通常在文件级实施。在有些更复杂的系统中,这类控制也可在记录级或域级实施
有些文件系统中,文件是按照域而非记录来组织的。在这种情况下,文件是一组域的集合
数据库 是一组相关的数据的集合,其本质特征是数据元素间存在着明确的关系,且可供不同的应用程序使用。数据库自身由一种或多种类型的文件组成。通常,数据库管理系统是独立于操作系统的,尽管它可能会使用某些文件管理程序
注意,并非所有文件管理系统都会呈现本节讨论的这种结构,在UNIX或类UXIN系统中,文件的基本结构是字节流。例如一个C语言程序以文件的形式存储,而没有物理域,记录等
12.1.3 文件管理系统
文件管理系统是一组系统软件,它为使用文件的用户和应用程序提供服务。典型情况下,文件管理系统是用户或应用程序访问文件的唯一方式,它可使得用户或程序员不需要为每个应用程序开发专用软件,并为系统提供控制最重要资源的方法,它需满足以下目标: - 满足数据管理的要求和用户的需求
- 最大限度的保证文件中的数据有效
- 优化性能,包括总体吞吐量(系统角度)和响应时间(从用户的角度)
- 为各种类型的存储设备提供I/O支持
- 减少或消除丢失或破坏数据的可能性
- 向用户进程提供标准I/O接口例程集
- 在多用户系统中为多个用户提供I/O支持
第一条中,用户需求的范围取决于各种应用程序和计算机系统的使用环境。对于交互式的通用系统,最小需求集合如下:
1.能创建,删除,读取和修改文件
2.能受控地访问其他用户的文件
3.能控制允许对用户文件进行哪种类型的访问
4.能在文件间移动数据
5.能备份用户文件,并在文件受破坏时恢复文件
6.能通过名字而非标识符访问自己的文件
文件系统架构
典型的软件架构图如下图所示
不同系统有不同的组织方式,但这一组织具有相当的代表性。在底层,设备驱动程序直接与外围设备通信。其负责启动设备上的I/O操作,处理I/O请求的完成。对于文件操作,典型的控制设备是磁盘和磁带设备。设备驱动程序通常是操作系统的一部分
接下来一层称为基本文件系统 ,或物理I/O层。这是与计算机系统外部环境的基本接口。这一层处理在磁盘间或磁带系统间交换的数据块,它关注的是这些块的位置,而非数据的内容或所涉及的文件结构。基本文件系统通常是操作系统的一部分
基本I/O管理程序 负责所有文件I/O的初始化和终止。这一层需要一定的控制结构来维护设备的的操作和文件状态。基本I/O管理程序根据所选的文件来选择执行文件I/O的设备。为优化性能,它还参与调度对磁盘和磁带的访问。I/O缓冲区的指定和辅存的分配,也是在这一层实现的。这一层也是操作系统的一部分
逻辑I/O 使用户和应用程序能够访问记录。基本文件系统处理的是数据块,而逻辑I/O模块处理的是文件记录。逻辑I/O提供一种通用的记录I/O能力,并维护关于文件的基本数据
文件系统中与用户最近的一层通常称为访问方法 ,它在应用程序和文件系统以及保存数据的设备之间提供了一个标准接口。不同的访问方法反映了不同的文件结构及访问和处理数据的不同方法
文件管理功能
下图显示了文件系统的功能概况
用户通过各种命令来与文件系统进行交互。并且在执行任何操作前,文件系统必须确认和定位所选择的文件。这就要求使用某种类型的目录来描述所有文件的位置及它们的属性。此外,大多数共享系统都实行用户访问控制来保护重要的文件。用户和应用程序可以在文件上执行的基本操作是在记录级执行的。用户和应用程序把文件视为具有组织记录的某种结构,如顺序结构。因此,要把用户命令转换为特定的文件操作命令,必须采用适合于该文件结构的访问方法
虽然用户和应用程序关注的是记录,但I/O是以块为基础来完成的,因此文件中的记录必须组织成一组块序列在输出,并在输入后将各块组织起来。支持文件的块I/O需要许多功能。磁盘调度和文件分配都会影响性能的优化,因此这些功能需要放在一起考虑,此外,优化还取决于文件结构和访问方式,因此,开发性能最优的文件管理系统是相当复杂的任务
上图表明,文件管理系统作为一个单独的系统实用程序,和操作系统关注的是不同方面的内容,它们之间的共同点是对记录的处理,这种划分是任意的,不同的系统会采用不同的方法
12.2 文件组织和访问
尽管文件组织和访问方法超出了操作系统通常所考虑的范围,但是如果没有对文件组织和访问的正确评价,就不可能对文件相关的其他设计文件进行评价,所以这是必要的
本节使用的术语文件组织指文件中记录的逻辑结构,它由用户访问记录的方式确定。文件在辅存中的物理组织取决于分块策略和文件分配策略,这方面的问题将在本章后面讲述
在选择文件组织时,有以下重要原则:
快速访问,维护简单,易于修改,可靠性,节约存储空间
这些原则的相对优先级取决于将要使用这些文件的应用程序,例如,如果一个文件仅以批处理方式处理,且每次都要访问到它的所有记录,则基本无须关注用于检索一条记录的快速访问
这些原则可能会自相矛盾。例如,为了节约存储空间,数据冗余应最小;但另一方面,冗余时提高数据访问速度的一种主要手段。这方面的一个例子是使用索引
已实现或提出的文件组织有多种,本节主要介绍5中基本组织。实际系统中使用的大多数结构要么正好是这几类之一,要么是这些组织的组合。这五种组织如下,下图描述了前四种:
堆,索引文件,顺序文件,直接或散列文件,索引顺序文件
12.2.1 堆
堆是最简单的文件组织形式。数据按它们到达的顺序被收集,每条记录由一串数据组成。堆的目的仅仅是积累大量的数据并保存数据。记录可以有不同的域,或者域相似但顺序不同。因此,每个域都应能自我描述,并包含域名和值。每个域的长度由分隔符隐式地确定,要么明确地包含在一个子域中,要么是该域类型的默认长度
由于堆文件没有结构,因而对记录的访问是通过穷举查找方式进行的,也就是需要查找整个文件
当数据在处理前采集并存储时,或当数据难以组织时,会用到堆文件。当保存的数据大小和结构不同时,这种类型的文件空间使用情况很好,能较好地用于穷举,且易于修改。但是,除了这些受限制的使用外,这类文件对大多数应用都不适用
12.2.2 顺序文件
顺序文件是最常用的文件组织形式。在这类文件中,每条记录都使用一种固定的格式。所有记录都具有相同的长度,并由相同数量,长度固定的域按特定的顺序组织。由于每个域的长度和位置已知,因此只需保存各个域的值,每个域的域名和长度是该文件结构的属性
有一个特殊的域被称为关键域。关键域通常是每条记录的第一个域,它唯一地标识这条记录:此外,记录按关键域来存储:文本关键域按字母顺序,数字关键域按数字顺序
顺序文件通常用于批处理应用中,如果这类应用涉及对所有记录的处理,那么顺序文件通常是最佳的,顺序文件组织是唯一可以很容易地存储在磁盘和磁带中的文件组织
对于查询或更新记录的交互式应用,顺序文件的性能很差。在访问一个大型顺序文件中的记录时,还是会遇到相当多的处理和延迟,除此之外还有一些问题。典型情况下,顺序文件按照记录在块中的简单顺序存储,也就是说,文件在磁带或磁盘上的物理组织直接对应于文件的逻辑组织。在这种情况下,常用的处理过程是把新记录放在一个单独的堆文件中,称为日志文件或事务文件,通过周期性地执行成批更新,把日志文件合并到主文件中,并按正确的关键字顺序产生一个新文件
另一种选择是把顺序文件组织成链表的形式。一条或多条记录保存在每个物理块中。磁盘中的每个块中都含有指向下一个块的指针。新记录的插入仅涉及指针操作,而不再要求将新纪录放到某个特定的物理块位置。这种方法可以带来一些方便,但它是以增加额外的处理和空间开销为代价的
12.2.3 索引顺序文件
克服顺序文件缺点的一种常用方法是索引顺序文件,它保留了顺序文件的关键特征:记录按照关键域的顺序组织。但它增加了两个特征:用于支持随机访问的文件索引和溢出文件。索引提供了快速接近目标记录的查找能力。溢出文件类似于顺序文件中使用的日志文件,但溢出文件中的记录可根据它前面记录的指针进行定位
最简单的索引顺序结构只使用一级索引,这种情况下的索引是一个简单的顺序文件。索引文件中的每条记录由两个域组成:关键域和指向主文件的指针,其中关键域和主文件中的关键域相同。要查找某个特定的域,首先要查找索引,查找关键域值等于目标关键域值或者位于目标关键域值之前且最大的索引,然后在该索引的指针所指的主文件中的位置处开始查找
考虑一个包含100万条记录的顺序文件。要查找某个特定的关键域值,平均需要访问50万条记录。假设创建一个包含了1000项的索引,索引的关键域均匀分布在主文件中,那么我们只需要查找对索引平均查找500次就能找到对应的索引,然后再对1000条记录平均查找500次就能找到我们想要的记录,查找的开销从500000降低到了1000,性能提升了500倍
文件可按如下方式处理:主文件中的每条记录都包含一个附加域。附加域对应用程序是不可见的,它是一个指向溢出文件的一个指针。向文件中插入一条新记录时,它被添加到溢出文件中,然后修改主文件中逻辑顺序位于这条新记录之前的记录,使其包含指向溢出文件中。如果前面的记录也在溢出文件中,那么修改前面那条记录的指针。和顺序文件一样,索引顺序文件有时也按批处理的方式合并溢出文件
索引顺序文件极大地减少了访问单条记录的时间,同时保留了文件的顺序特性。为顺序地处理整个文件,需要按顺序处理主文件中的记录,直到遇到一个指向溢出文件的指针,然后继续访问溢出文件中的记录,直到遇到一个空指针,然后恢复在主文件中的访问
为提供有效的访问,可以使用多级索引,也能降低平均查找长度
12.2.4 索引文件
索引顺序文件保留了顺序文件的一个限制:基于文件的一个域进行处理。当需要基于其他属性而非关键域查找一条记录时,这两种形式的顺序文件都无法胜任。但在某些情况需要这种灵活性
为实现这一点,需要一种采用多索引的结构,成为查找条件的每个域都可能有一个索引。索引文件一般都摒弃了顺序性和关键字的概念,只能通过索引来访问记录。因此,对记录的放置位置不再有限制,只要至少有一个索引的指针指向这条记录即可。此外,还可以使用长度可变的记录
可以使用两种类型的索引。完全索引包含主文件中每条记录的索引项,为了易于查找,索引自身被组织成一个顺序文件。部分索引只包含那些有感兴趣域的记录的索引项,对于变长记录,某些记录并不包含所有的域。向主文件中增加一条新记录时,索引文件必须全部更新
索引文件大多用于对信息的及时性要求比较严格且很少会对所有数据进行处理的应用程序中
12.2.5 直接文件或散列文件
直接文件或散列开发直接访问磁盘中任何一个地址已知的块的能力。每条记录中都需要一个关键域。但是这里没有顺序排序的概念
直接文件用于基于关键字的散列。直接文件常在要求快速访问时使用,且记录的长度是固定的,通常一次只访问一条记录
12.3 B树
对于大文件或大型数据库,仅靠一个对主键进行索引的顺序文件并不能保证快速访问。为了提供更加高效的访问,通常会使用一个结构化的索引文件。这种结构最简单的情形是两层组织:上层包含顺序的指针集合,其中的每个指针指向下层的一个段。这种结构可以扩展到多与两个层次的情况,这样就形成了树状的结构。其中可能会出现一些分支短而另一些分支长的情况,导致搜素时间不均衡,所以需要一种所有分支等长的平衡树状结构来提供最佳的平均性能。B树就是这样一种结构
B树是具有以下特征的一种树状结构(如下图所示):
1.包含若干节点和叶子的一颗树
2.每个节点至少包含一个用来唯一标识文件记录的关键码,且包含多于一个指向子节点或叶子的指针。一个节点包含的关键码和指针的数量是可变的
3.每个节点的关键码数量不能超过最大关键码数量
4.每个节点的关键码按照非减次序来存储。每个关键码都对应于一个子节点,以该子节点为根的字数包含的所有关键码都小于等于当前节点的关键码,大于前一个节点的关键码,一个节点包含一个额外的最右子节点,最右子节点为根的字数所包含的所有关键码,都大于该节点包含的任意关键码。这样,每个节点的指针数量都比关键码数量多一个
最小度数为d的B树需满足以下性质:
1.每个节点最多有2d-1个关键码和2d个子女,即2d个指针
2.除根节点外,每个节点都至少有d-1个关键码和d个指针
3.根节点最少有1个关键码和2个子女
4.所有叶子都在同一层,它不包含任何信息。这是用来终止树的一个逻辑结构,实际的实现可能会有不同
5.一个包含k个指针的非叶子节点有k-1个关键码
通常情况下,具有较大分支树的B树会有较低的高度
搜索一个关键码时,需要从根节点出发,如果所需的关键码在该节点中,那么搜索结束,否则有三种情况:
1.小于该节点的关键码就沿着左边的指针去下一层
2.大于该节点的关键码就沿着右边的指针去下一层
3.在某两个相邻的关键码中间,就沿着两个关键码中间的指针去下一层
和其他树状结构相比,这种结构的优点在于,它很宽且很浅,因此搜索可以很快结束。因为它时平衡的,因此不存在相对较长的搜索过程
向B树中插入新关键码的规则必须保证B树是一颗平衡树。过程如下:
1.向树种搜索这个关键码。若该关键码不在树种,则到达底层的一个节点
2.若该节点的关键码少于2d-1,则把新的关键码按适当的顺序插入该节点
3.若该节点是满的(包含2d-1个关键码),则以该节点的中间关键码为界,把该节点分裂为两个新节点,每个新节点都包含d-1个关键码,并按步骤4把中间的关键码提升到上一层。若新关键码的值小于中间的关键码,则插入左边的新节点,否则插入右边的新节点。
最后的结果是,原始节点分裂为两个新节点,一个包含d-1个关键码,另一个包含d个关键码
4.若父节点已满,则它必须分裂,且它的中间关键码被提升到上一层,提升的节点按照步骤3的规则插入父节点
5.如果提升的过程到达了根节点是满的,那么再按照步骤3的规则插入。但在这种情况下,中间的关键码变成了变成了一个新的根节点,并且树的高度增加1
下图描述了度数d=3的B树的插入过程
12.4 文件目录
12.4.1 内容
与任何文件管理系统和文件集合相关联的是文件目录,目录包含关于文件的信息,如属性,位置和所有权。大部分这类信息,特别是与存储相关的信息,都由操作系统管理。目录本身是一个文件,它可被各种文件管理例程访问。尽管用户和应用程序也可得到目录中的某些信息,但这通常是由系统例程间接提供的
下表列出了目录通常为系统中的每个文件保存的信息。从用户的角度看,目录再用户和应用程序所知道的文件名和文件自身之间提供映射。因此,每个文件项都包含文件名。实际上所有系统都需要处理不同类型的文件和不同的文件组织,因此还需要这方面的信息。每个文件的一类重要文件信息是其存储,包括位置和大小。在共享系统中,还须提供用于文件的访问控制信息。典型情况下,用户是文件的所有者,可以给其他用户授予一定的访问权限。最后,还需要有使用信息,以管理当前对文件的使用并记录文件的使用历史
12.4.2 结构
不同系统对上述表的信息的保存方式也大不相同。某些信息可以保存在与文件相关联的头记录中,这看减少目录所需要的存储量。当然一些重要的单元须保存在目录中,如名字,地址,大小和组织
最简单的目录结构是一个目录项列表,每个文件都有一个目录项。文件名用做关键字。但当多个用户使用多个文件时,就不够用了
要立即需求,先要考虑可能在目录上执行的操作类型:
- 查找:找到文件对于的目录项
- 创建文件:创建文件时,需要增加一个目录项
- 删除文件:需要删除对应的目录项
- 显示目录:可能会请求目录的全部或部分内容。通常这个请求是由用户发出的
- 修改目录:由于某些文件属性保存在目录中,因而这些属性的变化需要改变相应的目录项
简单列表难以支持这些操作。考虑单用户的需求:用户可能有许多类型的文件,包括字处理文本文件,图形文件,电子表格等,并且用户希望以某种方便的方式组织这些文件。若目录是一个简单的顺序列表,则它对应组织文件没有任何帮助,并且强迫用户不要对这两种不同类型的文件使用相同的名字。这个问题在共享系统中会更糟。命名的唯一性成为严重问题,此外,若目录中没有内在的结构,则很难对用户隐藏整个目录的某些部分
解决这些问题的出发点是两级方案,这种情况下,每位用户都有一个目录,还有一个主目录。主目录有每个用户目录的目录项,并提供地址和访问控制信息。每个用户目录是该用户文件的简单列表。这意味着只要在每位用户的文件集合中保证名称的唯一性,文件系统就可很容易地在目录上实行访问限制,但它对于用户构造文件集合没有任何帮助
更好的方法是层次或树状结构方法,这也是普遍采用的一种方法。也有一个主目录,主目录下方有许多用户目录,用户目录又有子目录的目录项和文件的目录项,在任何一级,一个目录都可以包含子目录的目录项或文件项
当包含很多目录项时,这种组织可能会导致查找时间很长,此时最好采用散列结构
12.4.3 命名
对用户而言,要求为文件提供唯一的名称是令人难以接受的负担,特别是在共享系统中
使用树状结构目录降低了唯一名称方面的难度,我们可以通过路径名来查找文件(这和我们在电脑中查找文件的方式很像)。由于所有路径都从主目录开始,因此主目录名是隐含的。在这种情况下,多个文件可以有相同的文件名,只要保证它们的路径名唯一即可
尽管路径名会使得文件名的选择变得冗余,但要求用户在每次访问文件都拼写出完整的路径名依然困难。典型情况下,对交互用户或进程而言,总有一个当前与之相关联,通常称为工作目录。比如用户B的工作目录是Word,则原来的路径名/User_B/Word/Unit_A/ABC和在当前工作目录下的路径名Unit_A/ABC访问到的文件是医院的。交互式用户登录或创建一个进程时,默认的工作目录是用户目录。在执行过程中,用户可以在数中向上或向下浏览,进而定义不同的工作目录
12.5 文件共享
在多用户系统中,几乎总是要求允许文件在多个用户间共享。这时就会产生两个问题:访问权限和对同时访问的管理
12.5.1 访问权限
文件系统应为多个用户间广泛共享文件提供灵活的工具。文件系统应提供一些选项,使得访问某个特定文件的方式能被控制。典型情况下,用户或用户组被授予访问文件的某些权限。已使用的访问权限有很多。下面列出的是一些具有代表性的访问权限,它们可指派给某个特定用户,使之有权访问某个特定文件:
- 无:用户不知道文件是否存在。为实施这种限制,不允许用户读包含该文件的用户目录
- 知道:用户可确定文件是否存在并确定其所有者。用户可向所有者请求更多的访问权限
- 执行:用户可加载并执行一个程序,但不能复制它。私有程序通常具有这种访问限制
- 读:用户能以任何目的读文件,包括复制和执行。有些系统还可区分浏览和复制,前一种情况文件的内容可以呈现给用户,但用户却无法进行复制
- 追加:用户可给文件添加数据,通常只能在末尾追加,但不能修改或删除文件的任何内容
- 更新:用户可修改,删除和增加文件中的数据。通常包括最初写文件,完全重写或部分重写,移去所有或部分数据。有些系统还区分不同程度的更新
- 改变保护:用户可改变已授给其他用户的访问权限。通常,只有文件的所有者才具有这一权力。在某些系统中,所有者可把这项权利扩展到其他用户。为防止滥用这种机制,文件的所有者通常能指定该项权利的持有者改变哪些权限
- 删除:用户可从文件系统中删除该文件
这些权限构成了一个层次结构,层次结构中的每个权限都隐含了前面的那些权限。因此,如果某个特定的用户被授予对某个文件的修改权限,那么也被授予了修改前面的哪些权限
被指定为某个文件所有者的用户,通常是最初创建该文件的用户。所有者具有前面列出的全部权限,并且还可给其他用户授予权限。访问可以提供给不同类型的用户: - 特定用户:由用户ID指定的单个用户
- 用户组:非单独定义的一组用户。系统必须能通过某种方式了解用户组的所有成员
- 全部:有权访问该系统的所有用户。这些是公共文件
12.5.2 同时访问
如果允许多个用户追加或更新一个文件,操作系统或文件管理系统必须强加一些规范。一种蛮力方法是在用户修改文件时,允许用户对整个文件加锁。较好的控制粒度是在修改时对单个记录加锁。实际上,这正是第五章讨论的读者-写者问题。在设计共享访问能力时,必须解决互斥问题和死锁问题
12.6 记录组块
在前面讲过,记录是访问结构化文件的逻辑单元,而块是与辅存进行I/O操作的基本单位。为执行I/O,记录必须组织成块
这里需要考虑几个问题,首先,块是定长的还是变长的。在大多数系统中,块是定长的,这个简化块的组织。其次,块与记录的平均大小相比,块的相对大小是多少,一种折中方案是,块越大,可以减少I/O操作。但另一方面,若随机访问文件,且未发现任何局部性,则大块会导致未使用记录的不必要传输(因为随机访问可能只需要块的一小部分,但是传输时却要传输整个块)。但是,综合考虑顺序访问的频率和访问的局部性潜能,可以说使用大块能减小I/O传送时间,需要注意的是,大块需要更大的I/O缓冲区,因而会使得缓冲区的管理更加困难
对于给定的块大小,有三种组块方法:
- 定长组块:使用定长的记录,且若干完整记录保存在一个块中。每个块的末尾困难会有一些未使用的空间,称为内部碎片
- 变长跨越式组块:使用变长的记录,并紧缩到块中,使得块中不存在未使用的空间。但是某些记录可能会跨越两个块,两个块通过一个指针连接
- 变长非跨越式组块:使用变长的记录,但并不采用跨越方式。若下一条记录比块中剩余的未使用空间打,则无法使用这一部分,因此在大多数块中都会有未使用的空间
下图显示了这些方法,这里假设文件保存在磁盘上的顺序块中。图中假设文件大到足以跨越两个磁道。即使使用其他一些文件分配方案,结果也不会改变
定长组块是记录定长顺序文件的最常用方式。变长跨越式组块的存储效率高,并且对文件大小没有限制,但这种技术很难实现。跨越两个块的记录需要两次I/O操作,且不论如何组织,文件都很难修改。变长非跨越式组块会浪费空间,且存在记录的大小不能超过块的大小的限制
采用记录组块技术时,记录组块技术和虚存硬件会互相影响。在虚存环境中,页是传送的基本单位。页通常很小,因此对于非跨越式组块,把页当作块来处理是不现实的。因此,有些系统会组合多页,为文件传送创建一个较大的块
12.7 辅存管理
在辅存中,文件是由许多块组成的。操作系统或文件管理系统为文件分配块。这时会引发两个管理问题。首先,辅存中的空间必须分配给文件,其次必须知道哪些空间可用来进行分配,这两个任务是相关的,即文件分配采用的方法可能会影响空闲空间管理的方法。此外,文件结构和分配之间也是相互影响的
12.7.1 文件分配
文件分配涉及以下几个问题:
1.创建一个新文件时,是否一次性地给它分配所需的最大空间
2.给文件分配的空间是一个或多个连续的单元,这些单元称为分区。分区是一组连续的已分配块。在分配文件时,分区的大小应该是多少
3.为跟踪分配给文件的分区,应使用哪种数据结构或表
下面依次分析这些问题
预分配与动态分配
预分配策略要求在发出创建文件的请求时,声明该文件的最大尺寸。但对许多应用程序来说,如果不能可靠地估计文件的最大尺寸,就很难实现这种策略。此时,用户和应用程序会将文件尺寸估计的大一些,以避免出现分配的空间不够用的情形,从辅存分配的角度看,这显然是非常浪费的。因此,使用动态分配要好一些,动态分配只有在需要时才给文件分配空间
分区大小 第二个问题是分配给文件的分区大小。一种极端情况下,分配大到足以保存整个文件的分区;另一种极端情况是,磁盘空间一次只分配一块。因此,在选择分区大小是,需要折中考虑单个文件的效率和整个系统的效率。下面给出了需要折中考虑的4项内容:
1.邻近空间可以提高性能,对于面向事务的操作系统中运行的事务
2.数量较多的小分区会增加用于管理分配信息的表的大小
3.使用固定大小的分区(例如块)可以简化空间的再分配
4.使用可变大小的分区或固定大小的小分区,可减少超频分额导致的未使用存储空间的浪费
当然,这几项内容是相互影响的,必须一起考虑。因此,我们可有两种选择:
- 大小可变的大规模连续分区:能提供较好的性能。大小可变避免了浪费,且会使文件分配表较小,但这又会导致空间很难再次利用
- 块:小的固定分区能提供更大的灵活性,但为了分配,它们可能需要较大的表或更复杂的结构。邻近性不再是主要目的,主要目的是根据需求来分配块
每种选择都适用于预分配和动态分配。对于大小可变的大规模连续分区,一个文件被预分配给一组连续的块,这就消除了对文件分配表的需求,所需要的仅是指向第一块的指针和分配的块数量。一次性地所有分区需要的所有块,这意味着文件的文件分配表将保持固定大小
对于大小可变的分区,我们需要考虑空闲空间的碎片问题。这个问题再第七章讨论过。一些可能的选择策略如下: - 首次适配:从空闲列表中选择第一个未被使用但大小足够的连续块组
- 最佳适配:选择大小足够但未使用过的块中的最小一个
- 最近适配:为提高局部性,选择与前面分配给该文件的块组最为邻近的组
文件分配方法
通常有三种方法:连续,链式和索引,下表总结了每种方法的特点
连续分配 是指在创建文件时,给文件分配一组连续的块。如下图所示。这是一种使用大小可变分区的预分配策略。在文件分配表中,每个文件只需要一个表项,用于说明起始块和文件的长度。从单个文件的角度来看,连续分配是好的。对于顺序处理,可以同时读入多个快,从而提高I/O性能。同时,检索一个快也非常容易。但也存在一些问题。首先会出现外部碎片,因此很难找到空间大小足够的连续块。时常需要执行紧缩算法来释放磁盘中的额外空间,如下图所示。其次,因为是预分配,所以也会带来空间浪费的问题
与连续分配相对的另一个极端是链式分配,如下图所示。典型情况下,链式分配基于单个块。链中的每块都包含指向下一块的指针。每个文件只需要一个表项,用于声明起始块和文件的长度。块的选择很简单:任何一个空闲块都可加入链中。由于一次只需一个块,因此不必担心外部碎片的出现。这种类型的物理组织方式最适合于顺序处理的顺序文件。要选择某一块,沿着链向下,直到到达期望的块
链式分配的后果是局部性原理不再适用。若需要像顺序处理那样一次取入一个文件中的多个块,则需要对磁盘的不同部分进行一系列访问,这对于单用户系统有重大影响,也是共享系统需要关注的。为克服这个问题,有些系统会周期性地合并文件,如下图所示
索引分配 解决了连续分配和链式分配中的许多问题。对于索引分配,每个文件在文件分配表中都有一个一级索引。分配给该文件的每个分区在索引中都有一个表项。文件的索引保存在一个单独的块中,文件分配表中该文件的表项指向这一块。分配可以基于大小固定的块,也可基于大小可变的块。基于块可以消除外部碎片,而按大小可变的分区也可提高局部性。在任何一种情况下,都需要不时地进行文件整理。文件整理可以减少使用大小可变分区的索引数量,但对基于块的分配不能减少索引数量。索引分配支持顺序访问文件和直接访问文件,因而是最普遍的一种文件分配形式
12.7.2 空闲空间管理
要实现管理首先需要直到磁盘中的哪些块是可用的。因此,除文件分配表外,还需要磁盘分配表(DAT)。下面介绍一些已经实现的技术
位表 这种方法使用一个向量,向量的每一位对应于磁盘中的每一块。0表示空闲块,1表示已使用块
位表的优点是,通过它能相对容易地找到一个或一组连续的空闲块,因此其适用于前面描述的任何一种文件分配方法。位表的另一个优点是它非常小,但其长度仍然很长。一个块位图所需的存储器容量为
磁盘大小(字节数)/(8*文件系统块大小)
对于一个大小为512字节的16GB磁盘,位表会占用4MB的空间。如果能在内存中节省出这个空间,那么就把位表放在内存中。另一种方法是把位表放在磁盘中,但是4MB未被的位占用的磁盘块多,需要一个块时我们不能容忍查找这么大的磁盘空间,因此位表需要驻留在内存中
即使位表在内存中,穷举式查找也会使文件系统的性能降低,当磁盘比较满时,这个问题更严重。因此,大多数使用位表的文件系统都有一个辅助数据结构,用于汇总位表的子区域的内容。比如汇总表收集子区域中空间块的数量和连续空闲块的最大长度。当文件系统需要大量的连续块时,以通过扫描汇总表来发现适合的子区域,然后再查找这个子区域
链接空闲区 使用指向空闲区的指针和它们的长度值,可将空闲区链接在一起。由于不需要磁盘分配表,这种方法的空间开销可以忽略不计。该方法适用于所有文件分配方法。如果一次只分配一块,只要简单地选择链头上的空闲块,并调整第一个指针或长度值。如果基于可变分区进行分配,可以使用首次适配算法,同样需要调整指针和长度
但是这种方法本身也存在问题。使用一段时间后,磁盘会出现很多碎片,许多分区会变得只有一个块那么长。并且分配一个块时,在把数据写入到块中之前,需要先读这个块,找到指向新的第一个空闲块的指针。需要同时分配许多块时,会大大降低创建文件的速度。并且删除一个由许多碎片组成的文件也非常耗时
索引 索引方法把空闲空间视为一个文件,并使用一个索引表。索引应该基于可变大小的分区而非块。因此,磁盘中的空闲分区在表中都有一个表项。该方法能为所有大大文件分配方法提供有效的支持
空闲块列表 在这种方法中,每块指定一个序号,所有空闲块的序号保存在磁盘的一个保留区中,并且这个表保存在磁盘而非内存中。这是一种非常令人满意的方法,考虑下面几点:
1.磁盘上用于空闲块列表的空间小于磁盘空间的1%
2.尽管空闲块列表大到不能保存在内存中,但两种有效的技术可把该表的一小部分保存到内存中:
a.该表可视为一个下推栈,栈中靠前的数千个元素可保留在内存中。分配一个新块时,它从栈顶弹出,此时它在内存中。解除分配时,会被压入栈中。只有栈中在内存的部分满了或空了时,才需在内存和磁盘之间进行传送。因此,这种技术在大多数时候都能提供零时间的访问
b.该表可视为一个FIFO队列,队列头和队列尾的几千项在内存中。分配块时从队列中去取走第一项。取消分配时可把它添加到队列尾。只有内存中的头部分空了或内存中的尾部分满了时,才需在磁盘和内存之间传送数据
这两种方法,后台线程都可对内存中的列表慢慢地排序,因此连续分配很容易
12.7.3 卷
不同操作系统和不同文件管理系统所用的卷的概念会有不同,但从本质上讲,卷是逻辑磁盘,卷的定义如下
在最简单的情况下,一个单独的磁盘就是一个卷。通常,一个磁盘会分为几个分区,每个分区都作为单独的卷来工作
12.7.4 可靠性
考虑以下情况:
1.用户A请求给一个已有文件增加文件分配
2.该请求被批准,磁盘和文件分配表在内存中被更新,但未在磁盘中更新
3.系统崩溃,随后系统重启
4.用户B请求一个文件分配,并被分配给了一块磁盘空间,覆盖了上次分配给用户A的空间
5.用户A通过保存在A的文件中的引用,访问被覆盖的部分
当系统为了提高效率而当内存中保留磁盘分配表和文件分配表的副本时,会出现问题。为避免这类错误,请求一个文件分配时,需要执行以下步骤:
1.在磁盘中对磁盘分配表加锁,以防止在分配完成前另一个用户修改这个表。
2.查找磁盘分配表,查找可用空间。这里假设磁盘分配表的副本总在内存中,若不在,则须先读入
3.分配空间,更新磁盘分配表,更新磁盘。更新磁盘包括把磁盘分配表写回磁盘。对于链式磁盘分配,它还包括更新磁盘中的某些指针
4.更新文件分配表和更新磁盘
5.对磁盘分配表解锁
这种技术可用防止错误。但在频繁地分配较小的块时,就会对性能产生重要影响。为减少这种开销,可以使用一种批尺寸分配方案。在这种情况下,为了分配,可以先获得磁盘上的一批空闲块,而它们在磁盘上的相应部分则被标记为“已用”。使用这批块的分配在内存中进行,当这批块用完后,更新磁盘上的磁盘分配表,并获得新的一批块。若出现系统崩溃,则磁盘上标为“已用”的部分在被重新分配前,须通过某种方式清空。所用的清空技术取决于文件系统的特性
12.12 小结
文件管理系统是一组系统软件,它为使用文件的用户和应用程序提供服务,包括文件访问,目录维护和访问控制。文件管理系统通常被视为一个由操作系统提供服务的系统服务,而不是操作系统的一部分。但至少有一部分文件管理功能是由操作系统执行的
文件由一组记录组成。访问这些记录的方式决定了文件的逻辑组织,并在某种程度上决定了它在磁盘上的物理组织,如果文件主要作为一个整体处理,那么顺序文件组织是最简单,最适合的。如果对单个文件顺序访问的同时进行随机访问,那么索引顺序文件能产生最佳的性能;如果对文件的访问主要是随机访问,那么索引文件或散列文件最适合
不论选择哪种文件结构,都需要一种目录服务,这就使得文件可以按层次方式组织。这种组织有助于用户了解文件。同时也有助于文件管理系统给用户提供访问控制和其他服务
文件记录即使是固定大小的,通常也与一个物理磁盘块的大小不一致。因此,需要某种类型的组块策略。在复杂性,性能和空间加锁之间的折中决定了要使用的组块策略
任何文件管理办法的一个重要功能都是管理磁盘空间。其中部分功能是给一个文件分配磁盘块的策略。可以采用各种各样的方法,使用各种各样的数据结构来跟踪对每个文件的分配清空。此外,还需要管理磁盘中的未分配空间,这部分功能主要包括维护一个磁盘分配表,磁盘分配表指明了哪些块是空闲的
第十三章 嵌入式操作系统
13.1 嵌入式系统
与通用计算机(如便携式计算机或桌面系统)不同:“嵌入式系统”的定义涉及产品中电子产品和软件的使用。下面给出一个较好的通用定义
嵌入式系统通常与它们所在的环境紧密关联。与环境交互的需要产生了实时限制。这些限制决定了软件操作的时限。若多个活动必须进行同步管理,就需要更复杂的实时限制
下图显示了一般定义下的嵌入式系统组织结构
处理处机和存储器,还有很多元素不同于传统的台式或笔记本计算机:
- 或许有使得系统能进行测量,计算或外部环境进行交互的许多接口
- 用户界面可以像闪烁灯一样简单,也可以像实时机器人视觉那样复杂
- 诊断端口可以诊断系统是否已处于被控状态
- 能使用专用现场编程(FPGA),特定应用(ASIC)甚至非数字硬件,以增强性能或安全性
- 软件的功能通常是固定的,且特定于某个应用
13.2 嵌入式操作系统的特点
功能简单的嵌入式系统,能由一个或一组特定的程序,在没有其他软件的情况下进行控制。复杂的嵌入式系统通常会包括一个操作系统。尽管原则上能使用一个通用的操作系统,如Linux,但对于嵌入式系统,存储控件的限制,功耗和实时需求都要求为嵌入式系统环境使用专用的操作系统
下面列出嵌入式操作系统的一些独特特性和设计需求:
- 实时操作:在许多嵌入式系统中,计算的准确性部分地取决于递交的时间。通常,实时性受到外围I/O和控制稳定性需求的限制
- 响应操作:嵌入式软件可对外部事件响应进行处理。若这些事件的发生不是周期性的或不可预测,则嵌入式软件应考虑最差情况并为例程的执行设定优先级
- 可配置性:嵌入式操作系统功能性需求方面是多变的,不论是定律的还是定性的。嵌入式操作系统要想应用于不同的嵌入式系统中,其自身必须能灵活配置,以便对特定应用和硬件系统提供所需的功能
- I/O设备的灵活性:事实上,没有设备需要所有版本的操作系统都提供支持,同时I/O设备涵盖的范围也很大。对于慢速设备的处理,建议使用特定任务而不是将这些驱动整合到操作系统的内核中更加合理
- 改进的保护机制:嵌入式系统通常针对某个定义明确的受限功能设计。未经测试的程序很少加入软件。所以除安全措施外,嵌入式系统的保护机制有限
- 直接使用中断:通用操作系统不允许用户进程直接使用中断。这里列出了3个允许不通过操作系统中断服务例程,而让中断直接开始和结束任务的原因:1.嵌入式系统被认为经过了彻底的测试,很少对操作系统或应用程序进行修改。2.不需要保护机制。3.须能高效的控制不同的设备
开发嵌入式操作系统的方法通常有两种。第一种方法是移植现有的操作系统,使之适用于嵌入式应用。另一种方法是为嵌入式设备的使用单独设计和实现所需的操作系统
13.2.1 移植现有商业操作系统
增加实时能力,流水线操作和必要的功能后,现有商业操作系统可用于嵌入式系统。这种方法通常使用Linux,也使用FreeBSD,Windows和其他通用操作系统。这些操作系统一般比专用的嵌入式操作系统满,且可预见性较差。优点是,直接由普通商业操作系统派生的嵌入式操作系统可基于一系列成熟的接口,能方便地移植
缺点是,它并未对实时性和嵌入式应用进行优化。也就是说,要达到适当的性能,需要进行很大的修改。特别的,典型操作系统在调度上是为平均情况而非最差情况进行优化的,通常会按照需求分配资源,且忽略了应用程序的很多语义信息
13.2.2 为特定目的的构建的嵌入式操作系统
特定的嵌入式操作系统包括如下典型特点: - 有快速且轻量级的进程或线程切换
- 调度策略是实时的,分派模块是调度程序的一部分而非单独的模块
- 尺寸很小
- 能快速响应外部中断,典型的需求是响应时间小于10微妙
- 禁止中断的时间间隔尽可能小
- 为存储管理提供固定或可变的分区,并为存储器中的代码和数据提供加速的能力
- 提供特别的顺序文件来快速存储数据
为处理时序约束,内核: - 为大部分原语提供有限制的执行时间
- 维护一个实时的时钟
- 提供特定的警报和暂停
- 支持实时排队策略,比如最早截至时间优先和将消息插入到队列头的原语
- 提供固定时间延迟处理的原语和挂起/恢复执行的原语
上述特性是在实时需求下,嵌入式操作系统的常见特性。实际上,对于复杂的嵌入式系统,需求可能强调预见性操作胜过快速操作,这就迫使人们做出不同的设计决策,特别是在任务调度方面
13.3 嵌入式Linux
嵌入式Linux指的是运行在嵌入式系统中的Linux。本章重点介绍嵌入式Linux系统和运行于笔记本或台式机上的Linux系统的关键区别
13.3.1 内核大小
由于使用时配置的多样化,因此台式机和服务器的Linux系统需要支持非常多的设备。简单来所,这些系统需要为不同的使用目的支持一系列通信和数据交换协议。嵌入式系统一般只需要支持特定的设备,外设和协议,这些都取决于设备提供的硬件和设备的使用目的。所幸的是,Linux内核根据所支持的体现结构,处理器和设备的不同,可以灵活配置
嵌入式Linux是Linux调度一个版本,是基于嵌入式设备的大小和硬件限制而定制的,它同时包括一些软件包,用于支持设备上运行的服务和应用。因此嵌入式Linux的内核比普通Linux的内核要小得多
13.3.2 编译
台式机/服务器Linux和嵌入式Linux的一个关键区别是,台式机/服务器软件通常是在运行平台上编译的,而嵌入式Linux通常在一个平台上编译,但运行于另一个平台,后者称为交叉编译
13.3.3 嵌入式Linux文件系统
有些应用会在运行期间生成一个存储在内存中的较小文件系统。但文件系统通常保存在持久存储器中,如闪存或传统的磁盘存储设备。对于嵌入式系统,内部或外部磁盘并不是较好的选择,因此通常使用闪存作为持久性存储设备
和嵌入式Linux系统的其他方面一样,文件系统也要尽可能小。已有很多为嵌入式系统设计的简洁文件系统。以下是一些常见的系统文件:
- cramfs:压缩的RAM文件系统,是一个简单的只读文件系统,它通过最大化存储的潜在使用效率来减少文件系统的大小。cramfs文件系统中的文件被压缩为适合Linux页面大小的单元,以提高较高的随机访问
- squashfs:和cramfs一样,这也是一个压缩的只读文件系统图,用于低内存或存储容量有限的环境,如嵌入式Linux系统
- jffs2:日志闪存文件系统2是一个基于日志的文件系统,面向NOR和NAND闪存设备,适用于面向闪存的需求,比如可穿戴设备
- ubifs:非排序块镜像文件系统,在较大闪存设备上的性能通常要比jffs2好,同时支持写缓存,能进一步提升性能
- yaffs2:另一个闪存文件系统2,是面向大型闪存设备的快速文件系统。和jffs2相比,yassf2需要更少的RAM来保存文件系统和状态信息,同时能为频繁写的情况提供更好的性能
13.3.5 Android
这是基于Linux内核的一个嵌入式系统,因此我们可以任务Android是嵌入式Linux的一个例子。但是很多嵌入式Linux开发人员不认为Android系统是嵌入式Linux的实例。他们任务,传统的嵌入式系统拥有固定的功能,而且出厂时就已确定。Android能支持各种的应用,因此要比普通平台性操作系统强大的多。而且,Android是垂直一体化的系统,包含针对Linux内核的特定修改。Android的重点是Linux内核的垂直一体化和Android的用户空间构件。总之,由于不存在可以依据的嵌入式Linux的“官方”定义,因此这仅仅是语义层面的定义
第十四章 虚拟机
通常而言,应用程序直接运行在PC或服务器的操作系统上,每台PC或服务器在同一时间只运行一个操作系统。因此,应用程序供应商需要为每个平台重新部分代码,才能使应用能够得到系统支持和运行。如果要支持多个操作系统,应用程序供应商需要耗费昂贵的代价和大量的资源。并且在早期,摩尔定律使得硬件进步的速度远超软件,且大部分服务器都出现了空载现象,每台服务器只用了不到5%的可用资源。大量过剩服务器消耗着大量的电力和冷却资源,这给公司管理造成压力。虚拟机有助于缓解这种压力
虚拟化 能使一台PC或服务器同时运行多个操作系统或一个系统的多个会话。这就解决了服务器在同一时间只能运行一个操作系统给应用程序的使用带来麻烦的问题,关于第二个问题,我们可用把空载的服务器分成好几个虚拟机,每台虚拟机承载不同的任务
启用虚拟化的解决方案是虚拟机监视器,现在通常称为虚拟机管理程序。该软件介于硬件和虚拟机之间,以资源代理的形式存在。简而言之,它使多个虚拟机安全地共存一台物理服务器主机并共享主机的资源。在一台主机上共享的虚拟机数量称为整合率。例如,一台主机支持6台虚拟机,则整合率为6:1。截至2009年,全球的虚拟化服务器的部署数量已超过物理服务器的数量,且仍在持续增长
虚拟机对于商业和个人用户而言,已成为解决遗留应用问题和最大化单台计算机上应用负载量的一种通用方式。着能将一台服务器主机划分为多个独立的服务器,节约硬件资源。还能再需要调整负载平衡或主机发生故障时,迅速将服务器从一台计算机迁移到另一台计算机。在处理“大数据”应用和实施云计算基础设施方面,服务器虚拟化已成为核心
除了用在服务器上,虚拟机技术也用于桌面环境中来运行多个操作系统,如windows和Linux
14.1 虚拟化方法
虚拟化在于抽象。就像一个操作系统通过程序层接口对用户抽象磁盘I/O命令一样,虚拟化对其支持的虚拟机抽象物理硬件。虚拟机监视器就是提供这种抽象的软件,它就像一名经纪人或交警,作为代理为访客请求要使用的物理主机资源
虚拟机是一种模拟物理服务器特点的软件结构,可为它配置一定数量的处理器,一定数量的内存,存储资源及网络连接端口。虚拟机创建后,可以像物理服务器那样使用。虚拟机不能看到主机的所有资源,只能看到为它配置的资源。这种隔离措施允许一台主机运行多个虚拟机,每个虚拟机运行相同或不同的操作系统。共享内存,存储和网络带宽也不会产生问题。虚拟机管理程序实现了虚拟机到物理设备再回到对应虚拟机的I/O命令的转化。这样,一些本应由虚拟机“本地”操作系统执行的特权指令,就由虚拟机管理程序代理执行。但这种方法会影响虚拟化进程的性能,虽然软/硬件的不断发展会减小这种影响
虚拟机由文件构成,典型的虚拟机可以只包括几个文件。配置文件描述虚拟机的各种属性,并且描述虚拟机可以访问的存储空间。通常存储空间在物理文件系统上作为一个附加文件并以虚拟磁盘的方式运行在虚拟机上。虚拟机启动后,会产生一系列附加文件用来记录日志和内存分页等。由文件构成虚拟机,可使某些功能在虚拟环境中比在物理环境更简单,更快。虚拟机本身就是文件,复制这些文件不仅备份了数据,而且备份了整个服务器
要创建物理服务器的副本,需要额外的硬件,并且需要进行一系列操作。具体的准备工作可能会消耗数周甚至数月。由于虚拟机仅由文件组成,通过复制这些文件,虚拟机的完美副本很快就能完成。另一种快速提供虚拟机的方法是使用了虚拟机模板。虚拟机模板是指无法启动但完成了服务器配置,安装了操作系统甚至可能安装了软件的虚拟机,只有涉及该虚拟机的唯一标识的配置工作未完成。由模板创建虚拟机的工作包括:配置虚拟机的唯一标识,使用模板中的软件构建一台虚拟机,并更改配置作为部署的一部分
除整合和快速资源调配外,还有许多其他原语使虚拟环境成为数据中心基础设施的新模型。其中一个原因是增加了可用性。虚拟机主机聚集在一起形成计算机资源池。每台服务器上运行多个虚拟机,当一个物理服务器失效时,失效主机上的虚拟机可在集群中的另一台主机上快速地自动启动。相比在物理服务器上提供类似的可用性,大大降低了成本和复杂性。虚拟环境最引人注目的一个功能是,将一个正在运行的虚拟机从一台物理主机迁移到另一台,无须中断,回退,也不会影响该虚拟机的用户。这就允许管理员在物理主机上工作而不影响虚拟机的运行。维护也不需要停机维护。虚拟机还可根据资源的使用情况自动进行迁移。如果一台虚拟机开始请求更多的资源,那么其他虚拟机可以自动迁移到其他有可用资源的主机上,已确保所有虚拟机的性能和整体性能。这些例子仅体现了虚拟环境所能提供的初级功能