《操作系统原理、实现与实践》笔记
第一、二章
冯诺依曼“存储程序”思想
“存储程序”的基本含义就是将程序存储在内存中,其中程序是一段规定CPU如何运算的指令序列;计算机执行时,CPU会将这一段指令逐条载入到寄存器中,依据其描述完成规定的运算(计算机将这个指令序列中的指令逐条取出并解释执行);“存储程序”——载入程序——执行程序,是计算机的基本工作模式,即取指-执行
内核态、用户态、系统调用
内核态是操作系统代码执行时的状态,用户态是应用程序代码执行时的状态
操作系统中,无论是内核代码还是应用程序代码,都是装入内存后执行的,因此内核态代码和用户态代码在内存中放置的区域不同
所以也可以这么理解,放置内核代码的那一段内存区域是“内核态区域”,放置用户代码的那段内存区域是“用户态区域”
系统调用(fork、exec、open、read、write)就像一扇门,意义就在于让执行在用户态区域的代码不能进入内核态区域,比如用户态代码不能通过jmp指令跳转到内核态内存区域中,也不能通过mov访问内核态内存中的数据
当前特权级(CPL),描述符特权级(DPL),用来检查访问权限,操作系统也提供了0X80号中断让上层应用进入到操作系统
第三章 多进程——操作系统最核心的视图
3.1 什么是进程
在讲述进程之前,首先需要了解一下CPU的工作原理:不断地取指执行。但是遇上有IO的情况,效率会非常低下,书上讲到写一个整数的I/O所花费的时间约等于10^6条CPU计算指令的执行时间
因此引入了一种程序执行结构:并发,即多个程序同时出发,交替执行。当CPU执行到程序A的I/O操作时,CPU不是等待A的I/O完成,而是切换到程序B去执行,当程序A的I/O操作完成以后,CPU可以再切换到程序A继续执行,这样CPU就可以一直处于忙碌状态
根据并发的思想,CPU要在多段程序之间来回切换,但是仅仅修改PC指针(即修改寄存器CS:EIP)是不够的,还需要保存每个程序当前的一些信息。这就引出了进程控制块(process control block,PCB)的概念,PCB中保存了所属程序当前执行位置,执行现场等重要信息
进程的概念:进程用来描述一个程序及其执行过程中的信息,即描述一个执行中的程序;或者说:进程描述的是程序以及反映程序执行信息的数据结构的总和
程序是静态的指令,数据等,而进程是执行起来的程序
3.2 多进程引起的基本问题
3.2.1 多个进程的组织与进程状态
操作系统管理进程的关键就是管理进程对应的PCB数据结构,因此组织多个进程就是用合适的数据结构管理这些PCB;简单而高效的一种方式就是将这些PCB组织成队列
但管理进程时需要区分进程位于哪个队列,由此引入了进程的状态
进程的状态:
- 运行态,当前占有CPU、正在执行的进程状态
- 就绪态,一个进程具备了所有可以执行的条件,只要获得CPU就可以开始执行
- 阻塞态,也称睡眠态或等待态,指一个进程因为缺少某些条件,即使分配了CPU也无法执行的状态
有了这三个状态,就相应产生了三个PCB队列:运行队列、就绪队列、阻塞队列(阻塞等待的事件可能有多个,相应的也有多个阻塞队列)
根据进程的状态还可以描述一个进程在执行过程中的演化过程,这个过程也被称为进程的生存周期
3.2.2 多个进程的切换和调度
并发需要回答两个问题:1.什么时候切换 2.如何切换
什么时候切换
当CPU出现空闲时,进行切换;有很多原因导致CPU空闲,例如当前进程执行了需要CPU等待的指令(如启动磁盘读写)、当前进程执行了exit()退出,等等;这些空闲点也被称为调度点
如何切换
操作系统调用函数schedule()实现切换
schedule()函数的实现原理很简单,即从就绪队列中选出下一个进程的PCB,即pNew,然后用PCB结构pNew中存放的执行现场,去替换CPU中的寄存器,当然为了将来能切换回当前进程,切换之前还应该将CPU里面的“当前进程执行现场”保存在当前进程的PCB结构,即pCur
如何选择新的pNew很复杂,并不是单纯的队列FIFO,这设计到调度算法
3.2.3 进程间的影响分离
进程的代码都属于用户态内存区域,所有进程的CPL=DPL=3,因此可能产生问题:进程A执行一条修改内存地址100的指令,而内存地址100处存放的是进程2的数据,显然这会导致进程2发生错误
解决这个问题的办法是地址隔离:每个进程操作的地址不直接是真实的物理内存地址,而是通过一个映射表对应到一个真实物理地址;这就是需要用GDT表和页来翻译CS:EIP的根本原因
操作系统给每个进程分配一段只属于该进程的、互相不重叠的内存区域、因此各个进程的地址空间完全被分离开来,每个进程都可以随意独写任何地址,不用担心因误操作影响其他进程,也不用担心其他进程会影响自己
地址空间隔离措施是操作系统内存管理的核心概念,操作系统内存管理部分论述的主要工作都基于这一基本概念
3.2.4 进程间的通信与合作
进程间的通信方法很多,比如读写同一个数据库、读写同一个文件、读写一段共享内存,读写一段内核态内存等等
进程间的合作机制采用的是生产者-消费者模型,向共享缓存区中写的进程为生产者进程,从共享缓存区中读的进程为消费者进程,两个进程通过共享缓存区进行通信与合作
模型代码如下:
typedef struct {```} item;
item buffer[BUFFER_SIZE];//缓存区定义
int in = out = counter = 0;
//生产者进程producer
while (true) {
while (counter == BUFFER_SIZE);//缓存区满了,自旋等待
buffer[in] = item;
in = (in + 1) % BUFFER_SIZE;
counter++;
}
//消费者进程consumer
while (true) {
while (counter == 0);//缓存区没有数据,自旋等待
item = buffer[out];
out = (out + 1) % BUFFER_SIZE;
counter--;
}
问题:整个合作机制完全依靠counter,counter的语义必须时刻正确,两个进程都要修改counter,counter能保证总是修改正确吗?
解决办法:counter要么全部修改完成,要么一点也不修改,这就是临界区的概念,在进程同步一章中会讲到
第四章 线程切换与调度——操作系统的发动机
线程切换是进程切换的核心内容:进程切换由资源切换和指令流切换两部分组成,资源切换是将分配给进程的非CPU以外的资源进行切换,如对当前地址空间的切换;指令流切换是CPU切换,也就是线程切换
资源切换在内存管理,文件系统章节学习
4.1 线程与进程
4.1.1 线程概念的引入
并发是CPU高效工作的基础,并发的基本含义就是多段程序交替执行;交替执行不一定总存在于两段“很远”的代码之间,例如浏览器程序和编译器程序之间的交替,即使是在同一个可执行文件内,两个函数之间也可以交替,如在同一个浏览器内的函数也可以交替执行,这样的交替执行就产生了线程的概念
4.1.2 一个多线程实例
一个多线程浏览器,内部有GetData、ShowText、ProcessImage、ShowImage
四个函数,打开一个网页时,获取数据的线程(GetData)被调度开始工作,将网页的总体布局以及页面中的文本信息下载下来;然后切换到显示文本的线程(ShowText)将网页的基本结构和其中的文本信息显示在浏览器中;接下来再切换到解压图片的线程(ProcessImage)执行,解码完成后再切换到渲染图片的线程(ShowImage)执行
如果不用线程,只用一个进程来完成上述工作,执行的代码首先将页面布局、文本信息、图片对象等内容全部下载下来,再逐个解码渲染,最后在将所有信息全部整理好输出到屏幕上,用户会等待一段时间,然后看到完整的页面,体验不好
为什么说上述四个函数是四个线程:因为此时不需要地址隔离策略将两个内存缓存区分离在两个不同的进程中,因此这四个函数是四个并发的指令执行序列,并使用共同地址空间等进程资源
4.1.3 线程与进程
线程 | 进程 | 描述 | |
---|---|---|---|
能否并发 | 能 | 能 | |
切换内容 | 指令流 | 指令流+其他资源 | |
切换代价 | 小 | 大 | 线程切换时一些资源不需要切换,例如内存映射表 |
创建速度(资源消耗) | 快(小) | 慢(大) | 创建线程时直接使用进程共有的资源部分 |
相互影响 | 操作同一内存 | 完全分离 | 多个线程(同进程)使用相同的的地址空间,即同一个映射表 |
安全性 | 小 | 大 | 一个线程中的代码可以访问同一进程下其他线程的任意内存位置 |
隶属关系 | 隶属于一个进程 | 隶属于操作系统 | 线程不能脱离进程存在,没有进程就不能创建线程 |
4.2 用户级线程的切换与创建
由用户程序自己管理的线程对操作系统透明,操作系统完全不知道这些线程的存在,称之为用户级线程;由操作系统管理的线程是内核级线程
4.2.1 用户级线程的切换
用户级线程通过Yield()函数切换,Yield()函数也是一个普通的用户态函数,由用户自己编写
- 用户级线程的切换就是在切换位置上调用Yield()函数
- Yield()函数的工作:找到下一个线程的TCB,然后根据当前线程的TCB和下一个线程的TCB完成用户栈的切换
- 切换到新栈以后用Yield()函数中的"}"将PC指针切换到下一个线程要执行的指令处
- 线程切换时保存和恢复一些执行现场,无非就是保存一些通用寄存器,这些寄存器的值也要放在线程各自的栈中来保存,在栈切换完成后弹栈恢复下一个线程的执行现场
总的来说,切换就是:TCB切换,根据TCB中存储的栈指针完成用户栈切换、根据用户栈中压入函数返回地址完成PC指针切换
4.3 内核级线程
4.3.1内核级线程的引出
用户级线程是完全在用户态内存中创建的一个指令执行序列,即用户级线程的TCB、栈等内容都是创建在用户态中的,操作系统完全不知道;内核级线程就是要让内核态内存和用户态内存合作创建一个指令执行序列,内核级线程的TCB等信息是创建在操作系统内核中的,操作系统通过这些数据结构可以感知和操纵内核线程
内核级线程优点:提高并发性、有效支持多核处理器
进程优点:以进程为单位分配计算机资源,方便管理,进程之间互相分离,安全性高,可靠性好
用户级线程优点:用户在应用程序中随意创建、创建代价小、灵活性大、具有一定的并发性
用户级线程、内核级线程、进程三者的关系:
- 引出进程是为了管理CPU,即通过执行程序来使用CPU;进程、内核级线程、用户级线程都是执行一个指令序列,没有本质区别,三者都属于CPU管理范畴
- 要执行一个指令序列,除了分配栈、创建数据结构记录执行位置等以外,还要分配内存等资源,这就是进程的概念
- 将进程中的资源和执行序列分离以后引出线程概念,进程必须在操作系统内核创建,这是因为进程创建要涉及计算机硬件(内存)资源的分配;因此进程中的那个执行序列实际上就是一个内核级线程
- 内核级线程是操作系统在一套进程资源下创建的,可以并发执行的多个执行序列,操作系统为每个这样的执行序列创建了相应的数据结构来实现对这些内核级线程控制,如切换、调度等
- 上层应用程序也可以创建并交替执行多个指令执行序列,因为执行程序所需要的资源已经在创建进程时分配好了;此时启动多个执行序列所需要的TCB和用户栈等信息完全可以由应用程序自己编程实现,由应用程序负责操控多个执行序列,对操作系统完全透明
4.3.2 内核级线程之间的切换
总的来说,内核级切换仍然完成三个工作:切换TCB,切换栈与切换PC指针,它与用户级切换的区别:
- 内核级线程的TCB存储在操作系统内核中,完成TCB切换的程序应该执行在操作系统内核中,通过中断进入内核
- PC指针存放在栈中,利用栈完成切换,这个栈应该是内核栈,和用户级线程相比,内核级线程切换栈要同时切换用户栈和内核栈
大概过程:
- 中断进入,核心工作是记录当前程序在用户态执行时的信息,如当前使用的用户栈、当前程序执行位置、当前执行的现场信息等
- 调用schedule,切换TCB
- 切换内核栈
- 中断返回,将存放在下一个线程的内核栈中的用户态程序执行现场恢复出来,这个现场是这个线程在切换出去时由中断入口程序保存的
- 用户栈切换,即切换用户态程序PC指针以及相应的用户栈
4.5 CPU调度
如果操作系统支持线程,则线程是CPU调度的基本单位,否则是进程,下述调度方法线程进程都适用,统一称之为“任务”
PC机上主要考虑三个基本准则:
- 任务的周转时间,即任务从新建进入操作系统到该任务完成离开操作系统所经历的全部事件
- 任务的响应时间,即用户向某程序发起一个交互操作到该任务响应这个操作之间经历的时间,例如单击菜单到菜单弹出的这段时间
- 系统吞吐量,即一段时间区域内计算机系统能完成的任务总数
交互式任务更关心响应时间,非交互任务更关心周转时间
先来先服务调度
选择就绪队列头部的那个任务调度执行,是公平的
最短作业优先调度
按照任务的执行时间从小到大排序,任务按照这个顺序依次调度执行
实际环境中,任务不可能是一下子都出现在零时刻,无法进行先排一次序,因此这个调度只具有理论意义
最短剩余时间优先调度
这是一种可抢占式的调度,每次新任务到达时,选择当前剩余执行时间最短的那个任务调度执行
对于非交互式任务比较好
时间片轮转
将一段时间等分地分割给每个任务,即给每个任务分配一个执行时间片,当前任务地时间片用完时就切换到下一个任务,下一个任务时间片用完再切换
比较适合于交互式任务
多级队列调度
系统中同时存在交互式任务和非交互式任务,可以引入两个队列,一个为前台任务队列(交互式任务),一个为后台任务队列(非交互式任务);两个队列分别采用时间片轮转和最短剩余时间优先调度
还需要定义各个队列地关系,常用的是定义一个优先关系,通常让前台队列具有更高的优先级
多级反馈队列调度
多级队列调度存在两个问题:
-
如果采用非抢占式调用,后台任务得到CPU后只能等到任务执行完成释放CPU,这对于前台任务很不友好;如果采用抢占式任务,则如果一直有前台任务,那么后台任务一直不能执行
解决办法:后台任务也应该按照时间片来调度,即使有前台任务,后台任务也能得到一个执行时间片
-
怎么判定哪些任务是前台或者后台任务,并且前后台任务可能会相互转化
解决办法:应该根据任务在执行过程中的具体表现动态调整,如果一个任务最近发生了I/O,根据局部性原理,这个任务表现出前台任务特点,因此将该任务放到高优先级队列中,用记录阻塞态的方法时别I/O;如果任务一个时间片用完之后,还需要继续执行,说明没有发生I/O,也没有完成,根据局部性原理,可以近似地认为这是一个后台任务,因此可以给它分配更长的时间片,同时降低优先级,并且解决了最短剩余时间优先调度的任务时长预测问题
第五章 进程同步——让多个进程的退进合理有序
等待和唤醒实现了进程之间的相互依赖
可以给进程同步一个描述性定义:进程同步就是通过对进程的走走停停(等待和唤醒)的控制来让多个进程步调一致,合理有序地向前推进,完成相互依赖、相互合作
5.1 从信号到信号量
重点看书上关于生产者消费者的应用
信号,其实就是Java中的锁
信号量:
- 一个整型变量,用来记录和进程同步有关的重要信息
- 能让进程阻塞睡眠在这个信号量上
- 需要同步的进程通过操作加减信号量实现进程的阻塞和唤醒,即进程间的同步
Java高并发程序设计中,信号量(Semaphore)是指定最多有N个线程可以访问某一个资源,注意区别
5.2 临界区
信号量的作用就是根据信号量数值表达出来的语义来决定进程的停与走,因此多个进程共同修改信号量时,要保护信号量,不能随意修改
对信号量的保护就是保证每个进程对信号量的修改操作是原子操作,保护信号量的机制就是在修改信号量的代码基础上包裹其他代码来让信号量的操作称为原子操作
临界区就是对信号量操作的代码,也可以理解为是信号量这个资源;之所以称之为临界区,因为一旦进入这段代码,操作系统的状态发生改变,现在不能在进程之间随意切换
有了临界区的概念,信号量的保护实质就是让进程中修改信号量的代码变成临界区代码
5.2.1 临界区的软件实现
Peterson算法:用用标记法判断进程是否请求进入临界区;如果进程想进入临界区,用轮换法给就进程一个明确的优先排序;
这个算法只能处理两个进程的临界区
Lamport面包店算法:用面包店举例:N个谷歌要进入面包店采购,首先按照次序给每个顾客安排一个号码,顾客按照其号码大小从小到大依次购买,完成购买的顾客号码重置为0,这样完成购买的故可如果要再次购买,就必须重新排队
5.2.2 临界区的硬件实现
禁止中断
硬件原子指令法
5.3 死锁
死锁产生的条件:
- 互斥:资源不能共享,一个资源每次只能被一个进程使用
- 不可剥夺:进程已获得的资源,未使用完之前,不能强行剥夺
- 请求与保持:一个进程因请求资源阻塞时,对已获得的资源保持不放
- 循环等待:若干进程之间形成一种头尾相接的循环性资源等待关系
5.3.1 死锁预防
一次申请所有资源或者按序申请资源
缺点:需要预先计算程序请求的资源,很久之后才使用到的资源要很早预留,资源浪费
5.3.2 死锁避免
每次资源申请时都判断是否有死锁出现的危险,如果有危险就拒绝申请
银行家算法
对所有进程的资源请求都存在一种调度方案令其满足,从而都能顺利执行完成,这种进程序列称为安全序列
核心:遇到一个资源请求时,假设允许之后,能找到安全序列,说明此次请求安全,可以分配资源,否贼拒绝资源请求
银行家算法是保守的算法,本来可能不会导致死锁的资源请求可能会被拒绝,因为安全序列是不构成死锁的充分条件,而不是必要条件,因为进程中的资源并不是等待全部资源以后才释放,是使用完就释放的,而该算法的解是充分必要的
5.3.3 死锁检测/恢复与死锁忽略
让资源随意使用,在出现问题的时候检测死锁,并恢复
通过改进银行家算法可以检测死锁
鸵鸟算法:针对死锁不做任何处理,Window、linux都采用死锁忽略处理算法
第六章 内存管理—给程序执行提供一个舞台
6.1 程序重定位
指令执行时,通过程序重定位将逻辑地址转换为物理内存地址,实现的硬件叫做MMU(存储管理部件)
MMU进行重定位的CPU寄存器只有一个,因此进程的重定位基址都放在其PCB中,进程切换时将其PCB中存放的基址取出来赋给这个CPU寄存器
6.2 分段
6.2.1 段的概念
采用分治的思想,将程序分成多个段,比如代码段(程序指令形成的段)、数据段(程序使用的数据)、栈段(实现函数调用)等
程序分段后,就不应该将程序作为一个整体载入内存,应该将多个段分别载入内存,需要记录各个段的基址
6.2.2 分段机制下的地址转换
采用分段机制后,程序中的逻辑地址会变成“段号:偏移”,段号对应的基址要从LDT(局部描述符表)中查询
显然,每个进程都有一个LDT表,用来描述该进程的代码段、数据段等
操作系统有一个GDT(全局描述符表),描述了操作系统的代码段、数据段、还有指向各个进程LDT表的表项
6.3 内存分区
6.3.1 可变分区与适配算法
需要先在内存中分割出一段空闲内存,然后才能将程序载入到内存中
分区适配算法有:最佳适配算法(每次只选最小的,能满足段大小的内存)、最差适配(选最大能满足的内存)、最先适配(从空闲分区表的最前面开始找)
6.3.2 内存碎片
虽然总空闲内存大小能够满足内存请求,但是不是连续的,也无法载入程序,这就是内存碎片
内存碎片:虽然总的空闲内存很大,但是由一堆分散在物理内存多个位置的小区域组成,这些小区域不能满足进程的段尺寸要求而无法使用,从而造成空间浪费
解决方法:
- 内存紧缩,通过移动整理将很多零散的空闲内存碎片合并成一整块空闲区域;但是缺点很明显,耗时,并且整个过程所有进程不能执行任何动作
- 内存离散化,将内存分割成固定大小的小片,内存请求到达时,根据请求尺寸计算出总共需要的小片个数,然后在内存中(任意位置)找出同样数量的小片分配给内存请求;这是分页机制的基本思想
6.4 分页
6.4.1 分页机制
分页机制首先将物理内存分割成大小相等的页框,然后将请求放入物理内存的数据(比如代码段)也分割成同样大小的页,最后将所有的页都映射到页框上,完成物理内存页框的使用
通常将页设置为4kb(Mysql中的B+树索引)
要让内存中的程序可被取值-执行,内存中的数据可被寻址读写,还需要解决重定位问题
页表:由页表项组成,记录逻辑页放入到哪个物理页框
映射关系:逻辑地址——>页号——>页框号——>物理地址
6.4.2 多级页表与快表
如果不加处理,页表占用的内存会非常大,比如32位系统,一个进程需要4MB的页表,100个进程就要占用400MB,并且实际上整个逻辑页很少全部被用到,一些程序可能只用到几个逻辑页而已
给定一个逻辑页号,要得到对应的物理页框号,需要和页表中存储的逻辑页号诸葛比较,即使采用二分查找,时间效率也是不能接受的
多级页表:在页表项基础上建立一个高层结构,称为页目录,每个页目录中包含多个页表项,通常4M区域是一章,其中的每个4K是章中的节(就像书的目录一样,就章,节)
如果某些逻辑内存区域,比如8MB-12MB区域没有映射到物理内存中,就不需要将这个页目录下的1024个页表项存放在内存中,减少了存放页表的空间浪费
两级页表的基本结构:引入页目录项,一个页目录项下面包括多个页号连续的页表项,页表项映射了一页内存,而页目录项映射了一块内存;此时有两个表,由页目录项组成的页目录表和页表项组成的页表,查找一个逻辑页号时,先查找页目录表找到页目录项,然后根据页目录项中存放的指针找到页表,再查找页表找到逻辑号对应的页表项
多级页表虽然可以解决空间效率问题,但是也会引出地址转换的时间效率问题
快表(变换旁查缓冲器):缓存那些常用的逻辑页映射关系(存放在CPU的高速缓存)
总结:
- 将物理内存分成页并以页为单位进行内存分配,解决内存碎片问题造成的空间浪费
- 一旦分页以后,存放页表完成地址转换过程
- 采用多级页表降低页表存储的空间开销
- 采用快表降低多级页表的时间开销
- 最终形成综合多级页表和快表的分页机制
6.5 段页式内存管理与虚拟内存
虚拟内存将分段机制和分页机制结合在了一起
程序如何放到内存?
- 在虚拟内存中分割出一些分区,将程序的各个段“放入”,并不是真正的放入,只是建立一个映射关系
- 建立段表记录这个映射关系
- 将虚拟内存分割成页,选择物理内存中的空闲页框,将虚拟内存中的“页内容”放到物理页框中;因为虚拟内存的中间作用,从用户出发看到的视图是程序段被放到一个连续“内存”区域上,即分段效果,操作系统将这个“内存”区域按照分页方式真正放到物理内存中,实现了分页机制
- 建立页表记录虚拟内存页与物理页框之间的映射关系
内存中的指令如何执行,以call 40为例
逻辑地址CS:40,假定代码段是第0段
- 先查段表取出基址,算处CS:40在虚拟内存中的位置,1000+40=1040,即虚拟地址
- 根据虚拟地址查找页表找到物理地址,1040对应的虚拟页号是1040/100=10~40,虚拟页号是10,页号偏移40,查找页表得到物理页框号5,所以最终的物理地址是540
- 在地址总线上放入地址540后进行取指,取出的指令是“mov,[300]”,指令call 40被正确执行
第七章 换入\换出—用磁盘和时间来换取一个规整的虚拟内存
假如一个店面只能放10台车,但是仓库里还有很多车。顾客来看车,发现没有马自达3,于是销售需要去仓库调一辆马自达3,同时把店面的一辆车运回仓库,以便于给马自达3腾出位置,这时,顾客就能在店面欣赏马自达3了
店面就是内存,仓库是磁盘,而仓库里所有的车就对应虚拟内存
用户可以访问任何地方的虚拟内存,如果虚拟内存没有和物理内存关联起来,赶紧从磁盘上读进来并建立关联,这称为“换入”,由于物理内存是有限的,很多时候也需要将物理内存中的部分内容移除到磁盘上,并和虚拟内存区域解除关联,这称为“换出”
换入\换出是形成规整虚拟内存的关键所在
换入
内存换入的核心,当缺少虚拟页的时候请求调页,操作系统实现换入就是实现请求调页
请求调页的整个过程从MMU发现虚拟页面在页表项中的有效位为0开始,这个时候MMU会向CPU发现缺页中断;操作系统的内存换入就是从这个缺页中断开始,在这个中断处理中操作系统会去磁盘(仓库)上找到这个虚拟页(商品),并将这个虚拟页从磁盘上读进来,当然读进来之前需要先找到一个空闲的物理内存页框(店面中的一个柜台),再将那个虚拟页读到空闲页框之后(将商品放到柜台上),虚拟页面就和物理页框建立关联了,再更新页表来记录这个映射关系
换出
换出的一个核心工作是找到空闲的物理内存页框,如果没有空闲的,则通过换出已映射的页,腾出空闲页框
页面换出算法
评价指标:缺页次数,较少的缺页次数会提高请求调页的性能
最优置换OPT(optimal replacement)
选择未来最远使用的页面进行淘汰,但这是不可行的,因为很难知道未来的使用情况
最近最少使用LRU(least recently used)
选择历史上最近很长时间没被访问的页面淘汰
LRU算法实现
-
给每个页维护一个时间戳
不可行,因为有的机器永不关闭,时间戳会溢出,并且页表长度很大,还有需要通过比较来确定时间戳最小的页,代价很大
-
维护一个页面栈,可以采用指针链表数据结构来实现,这样页面修改时时间复杂度为O(1);也不可行,因为每次维护页面栈而的时候,都会造成几次指针读写,每次页面访问都需要维护页面栈,所以每次地址访问都需要额外的内存访问,内存的读写效率降低很多
-
clock算法
clock算法是对LRU算法的一种近似
- 最好能用硬件实现来维护访问时间信息
- 在页表项中存放一个简单的数来实现对“最近最少使用”的近似表示,并且用硬件在访问页面时自动更新这个数
对时间戳的近似:
-
用一位二进制数0、1来近似表示时间戳,如果页面访问了,置为1,所以这一位也称为访问位(R位)
基于R位可以构造出基础的clock算法,也成为第二次机会置换(second chance replacement)算法:首先将分配给进程的所有页框组织成环形线性表, 产生缺页时,就从当前的线性表指针(一直停留在上一次缺页处理完后的位置)处进行环形扫描,如果扫描到的虚拟页面R位位1,则修改为0,指针向后移动;如果发现扫描位置R位位0,就将该页淘汰换出;由于访问过的页要从1修改为0以后才被换出,所以给访问过过的页多给了1次机会,所以也成为SCR算法
SCR算法的问题:如果“最近过长”,所有的R都是1,该算法会退化成FIFO算法(转一圈置0,回到第一个置0的页框,换出)
-
更形象的clock算法
对“最近”有一个更合适的估计,再引入一个扫描指针,该指针定期扫描所有页面,并将所有的R置0;发生缺页时,用换出指针扫描也秒,如果该页面的R位仍然是0,则淘汰,换出指针只负责换出,不负责更改R;
该算法对LRU的近似是:如果自从上一次定期扫描以来,页面一直没有被访问过,就认定该页最近没有被访问过,将其淘汰
页框个数分配与全局置换
分配给进程的所有物理页框都被用完之后,发生缺页才会调用clock算法进行页面淘汰,所以操作系统需要确定给进程分配多少个物理页框;分配物理页框方法加上页面置换算法、页面换入、换出机制才算完整
系统颠簸:进程数量增加时,CPU利用率下降;原因:分配给进程的物理页框数量太少,少到无法覆盖当前进程执行时的“局部”,导致换出去页面又要访问,需要换入,又将某个页面换出,不断往返
解决系统颠簸的方法:计算出进程当前执行需要覆盖的局部是多大,操作系统保证分配该进程的物理页框数量大于其局部,如果系统的空闲内存不足,就将某些进程挂起,将其切换到磁盘上,腾出地方保证分配给每个进程的物理页框个数足够覆盖其局部
工作集模型
是估计当前局部需要多大的数学方法
核心是统计进程在最近一个历史窗口中访问了哪些页,这些页形成的集合被称为工作集WS,这个集合的大小被称为工作集大小,记为WWSi,给进程Pi分配页框的时候,就分配WWSi个页框
实际系统中,历史窗口是自适应调整的:如果最近一段时间系统整体缺页率较高,则将历史窗口增大,反之减小,提高并发度
缺点:准确求出局部很困难,在实际操作系统中很难完美工作
全局置换策略
某个进程需要物理页框时,操作系统会去一个全局空闲物理页框链表中取出一个空闲物理页框进行分配,同时操作系统定期对分配给所有进程的所有页进行clock算法扫描,发现最近一段时间内没有被访问的页面,就将其换出到磁盘上,并将对应的物理页框释放到空闲页框链表中
缺点:如果一个进程的局部大,将比其他进程获取更多的内存资源,并且进程可以估计设计编码抢占内存
第九章 文件系统
9.1 磁盘工作过程
构成
磁盘由多个圆形盘面组成,每个盘面上有多个同心圆环,每个同心圆环称为磁道,多个磁盘的同一磁道组合新成柱面;每个磁道被分割为多个扇区,扇区是磁盘读写的基本单位;每个磁盘面上都有一个磁头,进行磁盘读写时,只有一个磁头上电,读取对应扇区的数据
工作过程
- 磁头移动,找到对应的柱面(C)
- 从柱面中选择磁道,给对应磁头上电
- 旋转磁盘,寻找扇区
- 开始读写
9.2 生磁盘的使用
9.2.1 第一层抽象-从扇区到磁盘块
要读磁盘上的数据,理论上需要知道C(柱面),H(磁头),S(扇区)等信息,但是这对于用户来说不友好,因此需要建立一个在C,H,S基础之上的编制方案
对扇区编号:选取一个0号扇区,那么1号扇区应该对应哪块位置呢?寻道(移动磁臂)是时间的主要消耗,因此1号扇区应该挨着0号扇区,即同一个盘面同一个磁道,此时读写1号扇区不需要寻道和旋转圆盘(读取0号扇区完划过整个扇区,有相对运动才能将磁信号变为电信号,所以此时磁头在1号扇区上)
为了提高速度,引入了磁盘块的概念
磁盘块:多个连续的扇区;寻道、旋转一次读写K个扇区的策略,比只读一个扇区速度提高接近K倍
缺点:会有空间浪费,即使该文件最后一个盘块没有用完,也不能给其他文件使用
现在只需要告诉告诉操作系统,需要读取的磁盘块号
9.2.2 第二层抽象-多个进程产生的磁盘读写请求
多个进程会有多个读写磁盘的请求,需要用队列来组织这些请求
此时读写过程:想进行磁盘读写的进程首先建立一个磁盘请求数据结构,其中封装了盘块号,然后这个数据结构被放入磁盘请求队列,剩下的交给操作系统处理,对用户完全透明
步骤:
- 从队列中选择一个磁盘请求
- 取出磁盘块号
- 计算C,H,S
- 用out语句向磁盘控制器发出具体指令
这层抽象的核心是磁盘调度算法
影响时间的主要因素是寻道,因此应该优先考虑柱面号这个因素,寻道总距离是评价的一个基本准则
最短寻道时间优先(SSTF)
每次选择离当前磁头最近的柱面,这是一个贪心算法,并不是最短寻道距离的离线算法,并且两端柱面很可能出现饥饿问题
离线算法:算法输入在算法执行之前已经全部出现
在线算法:输入在执行过程中仍不断进入
磁盘调度扫描算法(SCAN)
首先向一个方向进行扫描,处理经过的所有请求,直到这个方向不再有磁盘请求时,向另一个方向扫描,并处理经过的所有磁盘请求
不公平性:位于中间的柱面请求比两端的请求能够更快处理
循环扫描算法(CSCAN)
可以解决SCAN中公平性问题
首先向某个方向进行扫描,比如沿着柱面号小的方向扫描,处理经过的所有磁盘请求,直到这个方向不再有磁盘请求时,磁头迅速复位到另一个方向最大的请求位置,然后沿着同样的方向(柱面号最小)进行扫描,如此反复
又称为电梯调度算法
9.2.3 第三层抽象-从磁盘请求到高速缓存
目前为止磁盘处理过程:提取各个进程的磁盘请求,根据磁盘块号找到相应的扇区位置,将这些扇区位置放入内核态内存中,再由系统调用将存放于内核态内存中的磁盘数据复制到用户态内存中,用户态程序操作用户态内存中的数据
引入磁盘高速缓存:用一个散列表来组织那些已经载入的磁盘块数据——缓存快;用一个空闲链表组织空闲缓存块
9.3 基于文件的磁盘使用
9.3.1 第四层抽象-引出文件
通过磁盘块号读取数据,并不方便
为了更符合人们的习惯,引入了第四层抽象——文件,文件是一个连续的字符流
字符流如何存放在磁盘块上呢?1.顺序存储结构
引入字符流后,操作系统需要能根据字符流位置找到对应的盘块号,即需要建立字符流和盘块号之间的映射关系,这就是FCB(文件控制块),用于记录对应的文件名,及其起始块号,文件长度
这种方式对于查找方便,但是对于文件的修改、追加、删除很耗费时间,类似于 ArrayList
链式存储结构,类似于LinkedList
文件字符流存放的磁盘块不要求连续,只需要每个磁盘块中存放下一个字符流片段所在的磁盘块号即可
显然,链式存储结构的文件读效率很低
索引存储结构
文件字符被分割成多个逻辑块,在物理磁盘上寻找一些空闲物理盘块(无须连续),将这些逻辑块的内容存放进去,再找一个磁盘块作为索引块,按序存放对应的物理磁盘块号
问题:一个很小的文件,只有一个磁盘块大小,还需要引入索引块吗?当一个文件很大时,存放字符流的磁盘块号数量太多,多到一个磁盘块容纳不下怎么办?
索引存储结构:索引节点
就是文件的FCB,其中存放了直接数据块号,索引块号,间接索引块号;直接数据块直接指向文件内容,字符流中的前6个逻辑块对应的磁盘块号用节点中的直接块信息获得
利用索引节点中的索引块号可以读出索引块、根据索引块中存放的物理块号可以找到文件的内容,由于读入一次索引块,被称为一阶索引;如果是间接索引,首先读入间接索引块,其中存放的是下一阶索引的索引块号,下一阶索引块中存放的才是对应逻辑块的物理块号,这是二阶索引
UNIX,Linux都采用这种文件存储结构
9.3.2第五层抽象-将整个磁盘抽象成一个文件系统
本节的核心是组织和管理一堆文件
磁盘的抽象就是目录树,目录树由文件和目录组成,文件上一节已经讲过,因此实现目录树的关键就是实现目录
目录中存放文件名、文件的FCB地址;FCB地址:将磁盘上所有的FCB数据结构连续地放再一个磁盘块序列上,这个地址就代表FCB数组中地索引
这样设计后,目录的内容是[文件名:索引],称之为目录项
因此:目录的内容就是一个目录项数组
使用位图来描述磁盘上物理盘块和FCB数组的使用情况,1表示占用,0表示空闲
举例:如果要新建一个文件,首先用FCB位图找到一个空闲的FCB,将这个FCB分配给新建文件,并修改FCB位图;有了FCB后,修改文件所在目录,添加目录项;新建文件存放内容时,用空闲数据盘快位图找到空闲物理盘块并分配给新建文件,并修改物理盘快位图和文件FCB