1.1.1 操作系统的概念、功能和目标
1.1.2 操作系统的特征
1.1.3 操作系统的发展与分类
手工操作阶段
输入和输出都是一张打了孔的纸袋(有孔是1,无孔是0),速度很慢,而CPU处理速度很快,这就是矛盾的。
单道批处理系统
计算机处理打孔纸带的速度很慢,所以这里引入了中间介质磁带,计算机读写磁带的速度比读写纸带的速度快很多,这就提高了CPU的利用率。
多道批处理系统
多道批处理系统及之前的系统都没有人机交互,计算机在执行程序的过程中用户不能进行操作。
分时操作系统
计算机以时间片为单位轮流为各个用户/作业服务,任务的调度缺乏优先级。
实时操作系统
小结
1.1.4 操作系统的运行机制与体系结构
两种指令
- 特权指令:如内存清零指令,不允许用户程序使用。
- 非特权指令:如普通的运算指令,用户程序可以使用。
两种处理器状态
CPU如何判断当前是否可以执行特权指令?通过CPU的状态,这个状态使用程序状态字寄存器(PSW)中的某标志位来标识当前处理器处于什么状态,如0为用户态,1为核心态。如下:
- 用户态(目态):此时CPU只能执行非特权指令。
- 核心态(管态):特权指令、非特权指令都可以执行。
两种程序
- 内核程序:是操作系统的管理者,既可以执行特权指令,也可以执行非特权指令,运行在核心态。
- 应用程序:只能执行非特权指令,运行在用户态。
操作系统的内核
内核包含了操作系统中一些最基本、最重要的功能,尤其是与硬件直接交互的功能。
操作系统的体系结构
不同的操作系统对内核的定义不一样,有的OS仅将与硬件交互的功能包括在内核中,有的OS会增加进程管理、存储器管理等功能进内核。前者称为“微内核”,后者称为“大内核”。
1.1.5 中断和异常
中断的概念和作用
用户态——>内核态只能通过中断来实现,请求中断的方法是设置PSW中相应标志位为1,然后CPU在当前指令周期执行完之后去查看PSW是否有中断产生,有就从用户态切换到内核态(将PSW中的状态标志位设为1)响应中断。
PSW
程序状态字用来指示处理器状态、控制指令的执行顺序并且保留和指示与运行程序有关的各种信息,其主要作用是方便地实现程序状态的保护和恢复。每个正在执行的程序都有一个与其执行相关的PSW,而每个处理器都设置一个程序状态字寄存器。一个程序占有处理器执行,它的PSW将占有程序状态字寄存器。一般来说,程序状态字寄存器包括以下几类内容:
程序基本状态。包括:(1)程序计数器:指明下一条执行的指令地址;(2)条件码:表示指令执行的结果状态:(3)处理器状态位:指明当前的处理器状态,如目态或管态、运行或等待。
-
中断码。保存程序执行时当前发生的中断事件。
-
中断屏蔽位。指明程序执行中发生中断事件时,是否响应出现的中断事件。
由于不同处理器中的控制寄存器组织方式不同,所以在大多数计算机的处理器现场中可能找不到一个称为程序状态字寄存器的具体寄存器,但总是有一组控制与状态寄存器实际上起到这一作用。
中断的分类
内中断的信号来源是CPU内部,外中断的信号来源是CPU外部。
外中断的处理过程
注意下面step3,中断处理程序是内核程序,所以CPU在转入执行它的时候要从用户态切换到内核态;step4中,退出中断处理程序,转回执行用户程序,CPU要从内核态切换到用户态。
中断请求标志寄存器
CPU在指令执行阶段结束前就在标记寄存器中查询是否有中断产生,有的话,CPU响应中断的时间是在指令执行结束之后。
关于响应中断的第3个条件,“没有更紧迫的任务”表示没有优先级更高的中断请求。
1.1.6 系统调用
概念
系统调用是操作系统提供给应用程序(程序员)使用的接口,系统调用的相关处理要在核心态下进行。
系统调用并不是直接请求硬件资源,而是向操作系统发出访问硬件资源的请求,OS然后对这些请求进行响应的管理,OS批准后才能访问,这样就能保证系统的稳定性和安全性,防止用户进行非法操作。
系统调用与库函数的区别
库函数就是将OS的系统调用进行了封装,做成了功能更强大的命令集。
系统调用的过程
trap指令其实就是一个中断指令,执行之后CPU切换成核心态,执行系统调用的相关代码。
2.1.1 进程的定义、组成、组织方式、特征
定义
进程由程序段、数据段、PCB三部分表示。
组成
- 程序段:程序代码表示的二进制指令。
- 数据段:程序运行时使用、产生的运算数据的二进制。
- PCB:包含操作系统对进程进行管理所需的各种信息。
组织
OS中可能会有成百上千个PCB(进程),为了能有效地进行管理,要将这些PCB组织起来。
- 链接方式(图1):执行指针(运行),就绪队列指针(就绪),阻塞队列指针(阻塞)。
- 索引方式(图2):索引表。
特征
进程的状态与转换
进程由运行态——>阻塞态是进程自身做出的主动行为(如请求资源失败时),而从阻塞态——>就绪态是被动行为,是被CPU唤醒的。
2.1.3 进程控制
如何实现进程控制
图1中显示了进程状态的转换图,其中进程从一个状态转换到另一个状态需要两步,1是将PCB从相应队列中移出,2是改变PCB的状态标志位。这两步是不能被打断的,否则会出现PCB状态与所在队列不一致的情况,导致程序出错,所以要通过原语来保证这两部操作的原子性。
原语采用“开中断”和“关中断”指令实现。
进程控制相关原语
学习技巧:进程控制会导致进程状态的转换。无论哪个原语,要做的无非三类事情:
1. 更新PCB中的信息(如修改进程状态标志、将运行环境保存到PCB、从PCB恢复运行环境)
- 所有的进程控制原语一定都会修改进程状态标志
- 剥夺当前运行进程的Cp∪使用权必然需要保存其运行环境
- 某进程开始运行前必然要恢复期运行环境
2. 将PCB插入合适的队列
3. 分配/回收资源
进程的阻塞原语和唤醒原语必须成对使用,因为进程因为什么事件阻塞,就要被响应的事件唤醒。
2.1.4 进程通信
共享存储
两个进程对共享空间的访问必须是互斥的。
管道通信
“管道”是一个共享文件,是内存中开辟的一个大小固定的缓冲区。
消息传递
2.1.5 线程概念和多线程模型
线程的实现方式
各个系统实现线程的方式不完全相同,有的系统,特别是一些数据库管理系统,实现的是用户级线程;而另一些系统如OS/2实现的 是内核支持线程;还有一些系统如Solaris,则同时实现了这两种类型的线程。
用户级线程
用户及线程是在用户空间中实现的(创建、撤销、阻塞、切换等),用户可以看得到用户及线程,操作系统只能看得到进程,所以调度是以进程为单位的。在采用轮转调度算法时,各个进程轮流执行一个时间片,这对诸进程貌似是公平的,但假如进程A中包含一个用户及线程,进程B中包含100个用户及线程,那进程A中线程的运行时间将是进程B中各线程运行时间的100倍。
下图中的线程库是一种轻型线程(Light Weight Process)的线程池(如Java中的线程池),每一个进程都可以拥有多个LWP,LWP和普通的用户级线程一样也拥有自己的TCB等信息,可以共享进程的所有资源,区别是LWP可通过系统调用来获得内核提供的服务,这样,如果一个用户级线程想要访问内核,只需要将它连接到一个LWP上即可。
内核级线程
内核级线程是在内核空间中实现的(创建、撤销、阻塞、切换等),可以理解为用户线程只是一个表面线程,实际实现是内核空间中的内核线程,用户线程如果要切换,需要从用户态切换到核心态。
下图中,用户能看到三个线程,操作系统能看到一个进程和三个线程。
组合方式
注意,内核级线程才是处理机分配的单位,如图1,3个用户线程对应到2个内核线程,这个进程最多只能被分配2个CPU。
- 多对一模型(图2):用户线程的切换不需要切换到核心态,但是一个用户线程被阻塞会导致整个内核线程阻塞(整个进程阻塞)。
- 一对一模型(图3):并发能力强,但是进程内用户线程的切换会导致两态的切换。
- 多对多模型(图4):将许多用户线程映射到同样数量或更少数量的内核线程上。是对上面两个模型的强化,既有并发能力,也能减少线程切换导致的两态阻塞。
2.2.1 处理机调度、层次
概念
从就绪队列中按照一定的算法选择一个进程并将处理机分配给它运行,以实现进程的并发执行。调度有三个层次:高级调度、中级调度、低级调度。
高级调度
作业相关的进程还没创建,换句话说就是一开始就发现内存不足,作业调入内存后创建进程。
- 前提:由于内存空间有限,有时无法将用户提交的作业全部放入内存,因此就需要确定某种规则来决定将作业调入内存的顺序。
- 原理:高级调度(作业调度),是按一定的原则从外存上处于后备队列的作业中挑选一个(或多个)作业,给他们分配内存等必要资源,并建立相应的进程(建立PCB),以使它(们)获得竞争处理机的权利。
高级调度是辅存(外存)与内存之间的调度。每个作业只调入一次,调出一次。作业调入时会建立相应的PCB,作业调出时才撤销PCB。高级调度主要是指调入的问题,因为只有调入的时机需要操作系统来确定,但调岀的时机必然是作业运行结束才调出。
中级调度
作业之前已经被调入过内存,并且进程已经创建,只是后面发现内存不够,就将进程的相关信息(不包括PCB,指程序段和数据段)调入到外存中。中级调度就是指将这部分相关
- 背景:引入了虚拟存储技术之后,可将暂时不能运行的进程调至外存等待。等它重新具备了运行条件且内存又稍有空闲时,再重新调入内存,这么做的目的是为了提高内存利用率和系统吞吐量。
暂时调到外存等待的进程状态为挂起状态。值得注意的是,PCB并不会一起调到外存,而是会常驻内存。PCB中会记录进程数据在外存中的存放位置,进程状态等信息,操作系统通过内存中的PCB来保持对各个进程的监控、管理。被挂起的进程PCB会被放到的挂起队列中。
中级调度(内存调度),就是要决定将哪个处于挂起状态的进程重新调入内存。一个进程可能会被多次调出、调入内存,因此中级调度发生的频率要比高级调度更高。
注意,线程在创建、就绪、执行、阻塞状态都可能因为内存不足的原因而被挂起。注意挂起和阻塞的区别。
低级调度
低级调度(进程调度),其主要任务是按照某种方法和策略从就绪队列中选取一个进程,将处理机分配给它。
进程调度是操作系统中最基本的一种调度,在一般的操作系统中都必须配置进程调度。进程调度的频率很高,一般几十毫秒一次。
三层调度对比
2.2.2 进程调度的时机和方式
什么情况需要调度、不能调度的情况
图2中,临界区访问分为内核临界区访问和普通临界区访问,内核临界区一般访问内核数据结构,如就绪队列,如果一个进程在锁住就绪队列的情况下被切换,就绪队列没有解锁,那切换就会失败,影响操作系统内核的工作;而普通临界区(如访问打印机这种非内核资源)被切换不会影响操作系统内核的工作。因此图2中前者对,后者错。
进程调度方式
- 非抢占式:只允许进程主动放弃处理机,知道该进程终止或主动要求进入阻塞态。实现简单,但无法处理紧急任务。
- 抢占式:一个进程在CPU上执行时,如果有更重要或更紧迫的进程需要使用CPU,可以立即暂停正在执行的进程,将CPU分配给更紧急或重要的任务。
进程切换的过程
进程切换是指一个进程让出处理机,由另一个进程占用处理机的过程。
广义的进程调度包含了选择一个进程和进程切换两个步骤。进程切换的过程主要完成了:
- 对原来运行进程各种数据的保存
- 对新的进程各种数据的恢复。如:程序计数器、程序状态字、各种数据寄存器等处理杋现场信息,这些信息一般保存在进程控制块)
注意:进程切换是有代价的,因此如果过于频繁的进行进程调度、切换,必然会使整个系统的效率降低,使系统大部分时间都花在了进程切换上,而真正用于执行进程的时间减少。
2.2.3 调度方法的评价指标
- CPU利用率:CPU忙碌时间/(忙碌时间+空闲时间)。
- 系统吞吐量:单位时间完成的作业道数。完成了多少道作业/完成时间。
- 周转时间:作业从提交到完成花了多少时间。
- 等待时间:指进程/作业处于等待处理机状态时间之和。对进程来说,指进程在建立后等待被服务的时间之和;对作业来说,包括作业在外存中的等待时间和作业被调入内存创建进程后的等待时间。
- 响应时间:值从用户提交请求到被响应所花的时间。
2.2.4 FCFS、SJF、HRRN调度算法
FCFS算法
First Come First Serve,先来先服务。
SJF算法
Shortest Job First,短作业优先。
HRRN算法
Highest Response Ratio Next,高响应比优先。
响应比 = (等待时间+要求服务时间)/要求服务时间。注意图2中响应比的计算。
2.2.5 时间片轮转、优先级、多级反馈队列 调度算法
时间片轮转调度算法
Round-Robin。 如果时间片太大,时间片轮转算法就退化为FCFS算法;太小就会导致频繁的进程切换。
优先级调度算法
图2中每个进程到来时,就会有一个优先级。优先级分为:
- 静态优先级:创建进程时确定,之后一直不变。
- 动态优先级:创建进程时有一个初始值,只后会根据情况动态调整优先级。比如一个进程频繁进行IO操作,或者在就绪队列中等待了较长时间,可以提升优先级;而进程占用了CPU很长时间,可以降低优先级。
进程可以分为IO繁忙型进程和CPU繁忙型进程,操作系统更偏好IO型进程,因为这能让IO设备尽早投入工作,提升资源利用率和系统吞吐量。
多级反馈队列调度算法
图2建议看原视频动画演示过程。
https://www.bilibili.com/video/BV1YE411D7nH?p=17 29:00
三种算法对比
2.3.1 进程同步和互斥
进程同步
进程具有异步性,即各个进程的执行过程是独立的,以不可预知的速度向前推进。但是我们有些时候(比如进程之间进行通信的时候)有需要保证进程间的执行顺序,如管道通信,读进程必须要在写进程写完据后才能读取。
- 进程同步:指为完成某个任务而建立的两个或多个进程,这些进程间因为需要在某些位置上协调它们的工作次序而产生的制约关系。
进程互斥
系统资源分为共享资源(多个进程可共享)和临界资源(同一时间只能有一个进程访问资源),一个进程访问临界资源时其他需要访问的进程必须等待,这就是进程互斥。
2.3.2 进程互斥的软件实现方法
单标志法
使用turn这个变量来表示当前能访问临界区的进程。只有一个进程访问了临界区,turn才会改变,不访问就一直不会改变,违背了“空闲让进”的原则。
双标志先检查法
会出现并发的问题,如下图中,while (flag); flag = true;这两条语句是典型的读写操作,如果没有进行同步(即将这两条语句变成原子操作),那会发生并发问题,导致p0和p1两个进程同时访问临界区,违背了“忙则等待”的原则。
双标志后检查法
如下图,可能会导致死锁,p0和p1都阻塞在while语句上。
Peterson算法
可以在不同步的情况下完成进程的互斥。但是有个问题,即使p0谦让p1先执行,但是也要等到自己的时间片执行完,这期间一直执行while循环,浪费了时间,没有遵循“让权等待”的原则。
2.3.3 进程互斥的硬件实现方法
中断屏蔽方法
利用开/关中断来保证进程互斥。优点是简单高效,缺点是只适用于单CPU的计算机,对于多核CPU无效。
TestAndSet指令
TSL指令是用硬件实现的,相当于加锁(见下面代码)。
Swap指令
用硬件实现,执行过程不允许被中断。
2.3.4 信号量机制
信号量其实就是一个变量(整型或记录型变量),可以用来表示系统中某种资源的数量。
OS中有一对特殊的原语:wait(S)和signal(S),S是信号量参数。wait、signal原语简称为P、V操作。
整型信号量
wait(S)将判断和加锁放到了一个原语(原子操作)中,所以不会出现“双标志检查法”中的并发问题。但是会导致没有获得资源的进程一直循环等待。
记录型信号量
记录型的信号量是一个数据结构:剩余资源数+进程等待队列。下面wait()中,当没有资源可以访问时,将该进程加入到信号量的等待队列中,并阻塞该进程;signal()中会唤醒等待队列中的进程。这样一来,就不会出现循环等待的情况。
2.3.5 用信号量机制实现进程互斥、同步、前驱关系
实现进程互斥
进程互斥发生在多个进程访问同一个临界资源时,仅有一个进程能获得资源。所以将信号量中的资源数设置为1即可,然后每个进程访问都要调用P、V操作。
实现进程同步
进程同步,即让各个进程的执行有序推进。下图中代码就保证了代码1、2在代码4、5、6之前执行,注意设置的信号量初始值为0。
实现进程前驱关系
跟进程同步一个道理,不过是要协调多个进程的代码执行顺序,创建多个信号量即可。
2.3.6 使用信号量实现生产者-消费者模式
3.1.1 内存基础知识
逻辑地址和物理地址
逻辑地址只是规定了程序内部各个指令地址的顺序关系,物理地址才是程序在主存中的实际地址。
从写程序到程序运行
下面的装入阶段就是逻辑地址——>物理地址的转换阶段。
程序装入的三种方式
绝对装入
在编译时,如果知道程序将放到内存中的哪个位置,编译程序将产生绝对地址的目标代码。装入程序按照装入模块中的地址,将程序和数据装入内存。
静态重定位
程序装入之后,逻辑地址就变成了物理地址(从80变成180),之后地址就不能再改变了。这样在运行期间不能移动程序在内存中的位置,因为物理地址已经确定了。
动态重定位
程序装入依旧是逻辑地址,直到程序被执行时确定最终的物理地址。这意味着可以在程序运行期间移动程序,因为每次程序执行都会重新计算物理地址。
- 并且可将程序分配到不连续的存储区中。
- 在程序运行前只需装入它的部分代码即可投入运行,然后在程序运行期间,根据需要动态申请分配内存。
- 便于程序段的共享,可以向用户提供一个比存储空间大得多的地址空间。
链接的三种方式
链接是指在电子计算机程序的各模块之间传递参数和控制命令,并把它们组成一个可执行的整体的过程。
- 静态链接:在程序运行之前先将各目标模块及它们所需的库函数连接成一个完整的可执行文件(装入模块)之后不再拆开。
- 装入时动态链接:将各目标模块装入内存时,边装入边链接的链接方式。
- 运行时动态链接:在程序执行中需要该目标模块时,才对它进行链接。其优点是便于修改和更新,便于实现对目标模块的共享。
3.1.2 内存管理
- 操作系统需要提供某种技术从逻辑上对內存空间进行扩充。即一个8G ARM的计算机怎么运行80G的游戏,虚拟存储技术。
- 操作系统负责内存空间的分配与回收。
- 操作系统需要提供地址转换功能,负责程序的逻辑地址与物理地址的转换。方法就是上面提到的程序装入的3种方式。
- 操作系统需要提供内存保护功能。保证各进程在各自存储空间内运行,互不干扰。1、上下限寄存器,存储进程的物理地址的上下界,判断访问的地址是否超出了进程所在的地址范围。2、基地址寄存器(存储进程的起始物理地址)和界地址寄存器(进程最大的逻辑地址),见下图。
3.1.3 覆盖与交换
操作系统需要提供某种技术从逻辑上对內存空间进行扩充。即一个8G ARM的计算机怎么运行80G的游戏,下面介绍覆盖和交换技术。
覆盖技术
下图中程序时一次调用的,即同一时间只有一个模块在内存中执行,这样就可以采用覆盖技术,即所有模块都共享一个覆盖区,一个模块执行后就掉入下一个模块覆盖上一个模块,这样程序使用的内存大小就是最大模块的大小(如下图是12K)。
交换技术(swap)
交换(对换)技术的设计思想:内存空间紧张时,系统将内存中某些进程暂时换出外存,把外存中某些已具备运行条件的进程换入内存(进程在内存与磁盘间动态调度)。
交换技术的3个问题及解决方法。注意磁盘分为对换区和文件区,对换区IO速度高于文件区,采用连续存储方式,目的是提高IO速度;文件区使用离散存储方式,目的是尽可能提高空间利用率。
3.1.4 连续分配管理方式
内存被分为系统区(运行操作系统程序)和用户区(运行用户进程)。
单一连续分配
整个内存中只能有一道用户程序,用户程序独占整个用户区空间。
- 优点:实现简单;无外部碎片;可以采用覆盖技术扩充内存;不一定需要采取内存保护(eg:早期的PC操作系统 MS-DOS)。
- 缺点:只能用于单用户、单任务的操作系统中;有内部碎片;存储器利用率极低。
- 内部碎片:分配给某进程的内存区域中,如果有些部分没有用上。
- 外部碎片:是指内存中的某些空闲分区由于太小而难以利用。
固定分区分配
将用户空间预先分为多个分区,每个分区运行一个用户进程。见图1。
同时,操作系统需要建立一个数据结构(分区说明表)来记录每个分区的大小、起始地址、分配状态等信息。见图2.
- 优点:实现简单,无外部碎片。
- 缺点:a.当用户程序太大时,可能所有的分区都不能满足需求,此时不得不采用覆盖技术来解决,但这又会降低性能;b.会产生内部碎片,内存利用率低。
动态分区分配
在进程装入内存时,根据进程的大小动态地建立分区,并使分区的大小正好适合进程的需要,因此系统分区的大小和数目是可变的。
动态分区分配要考虑一下三个问题:
系统要用什么样的数据结构记录内存的使用情况?
当很多个空闲分区都能满足需求时,应该选择哪个分区进行分配?
应该用最大的分区进行分配?还是用最小的分区进行分配?又或是用地址最低的部分进行分配?见后面“动态分区分配算法”的章节。
如何进行分区的分配与回收?
假设使用空闲分区表来记录内存使用情况,当一个进程的空间被回收后,可以:
- 新增一个空闲分区。
- 与上下空闲分区合并成一个空闲分区。
- 优点:没有内部碎片;使用灵活。
- 缺点:有外部碎片。可以通过“拼凑”技术来解决外部碎片,即通过将内存中的进程移动到连续地址空间,腾出一块连续的大的内存空间。
3.1.5 动态分区分配算法
首次适应算法
- 算法思想:每次都从低地址(分区表首行或分区链头结点)开始査找,找到第一个能满足大小的空闲分区。
- 如何实现:空闲分区以地址递增的次序排列。每次分配内存时顺序查找空闲分区链(或空闲分区表),找到大小能满足要求的第一个空闲分区。
最佳适应算法
找到能满足进程空间的最小空闲分区进行分配。
- 算法思想:由于动态分区分配是一种连续分配方式,为各进程分配的空间必须是连续的一整片区域。因此为了保证当“大进程”到来时能有连续的大片空间,可以尽可能多地留下大片的空闲区,即,优先使用更小的空闲区。
- 如何实现:空闲分区按容量递增次序链接。每次分配内存时顺序査找空闲分区链(或空闲分区表),找到大小能满足要求的第一个空闲分区
- 缺点:每次都选最小的分区进行分配,会留下越来越多的、很小的、难以利用的内存块。因此这种方法会产生很多的外部碎片。
最坏适应算法
与最佳适应算法相反,先分配最大的空闲分区。
- 算法思想:为了解决最佳适应算法的问题一一即留下太多难以利用的小碎片,可以在每次分配时优先使用最大的连续空闲区,这样分配后剩余的空闲区就不会太小,更方便使用。
- 如何实现:空闲分区按容量递减次序链接。每次分配内存时顺序査找空闲分区链(或空闲分区表),找到大小能满足要求的第一个空闲分区。
- 缺点:每次都选最大的分区进行分配,虽然可以让分配后留下的空闲区更大,更可用,但是这种方式会导致较大的连续空闲区被迅速用完。如果之后有“大进程”到达,就没有内存分区可用了。
邻近适应算法
- 算法思想:首次适应算法每次都从链头开始査找的。这可能会导致低地址部分出现很多小的空闲分区,而每次分配査找时,都要经过这些分区,因此也增加了查找的开销。如果每次都从上次査找结東的位置开始检索,就能解决上述问题。
- 如何实现:空闲分区以地址递增的顺序排列(可排成一个循环链表),每次分配内存时从上次查找结束的位置开始査找空闲分区链(或空闲分区表),找到大小能满足要求的第一个空闲分区。
总结
3.1.5.5 非连续分配管理方式
将进程分为多个模块,每个模块连续存储在内存中的一块空间,不同模块在内存中可以是错序分配空间的。
基本分页存储管理
基本分段存管理
基本段页式存储管理
3.1.6 基本分页存储管理(非连续式)的基本概念
连续分配方式会产生大量的内存碎片(固定分区分配产生内部碎片,动态分区分配产生外部碎片)。非连续分配方式会为用户分配一些离散的内存空间。
分页存储的基本概念
- 分区(页框):将内存空间分为一个个大小箱相等的分区(页框),每个分区有一个编号,从0开始。
- 页(页面):用户进程的地址空间也被分为与页框大小相等的一个个区域,称为“页”,每个页也有一个编号,从0开始
操作系统以页框为单位为各个进程分配内存空间。进程的每个页面分别放入一个页框中。也就是说,进程的页面与内存的框有一一对应的关系。各个页面不必连续存放,也不必按先后顺序来,可以放到不相邻的各个页框中。
如何实现地址转换(计算页号和页内偏移量)
- 算出逻辑地址对应的页号。如下图中指令1往逻辑地址80写数据,就要计算出80对应的程序模块所在的页号,因为一个分区是50,所以实在第1个分区(从0开始)。
- 知道程序页在内存中的起始地址(这一步就涵盖了通过页号在页表中查找对应块号,根据块号计算出内存起始地址,详情见下面页表小节)。假设页1起始地址是450.
- 算出逻辑地址在页面内的偏移量。页0已经占了50个字节,所以80在页1中的偏移量是30.
- 物理地址 = 页面起始地址 + 页内偏移量。即450+30 = 480.
实际计算时根据给出的二进制地址能直接计算出页号和偏移量。如图2中,用32位表示逻辑地址,以4KB为一个页,然后给出4097的逻辑地址,就可以根据前20位得出页号,根据后12位得出在该页中的偏移量。
结论:如果每个页面大小为2KB,用二进制数表示逻辑地址,则末尾K位即为页内偏移量,其余部分就是页号。因此,如果让每个页面的大小为2的整数幂,计算机就可以很方便地得出一个逻辑地址对应的页号和页内偏移量。
如果有K位表示“页内偏移量”,则说明该系统中一个页面的大小是2^K个内存单元;如果有M位表示“页号”,则说明在该系统中,一个进程最多允许有2^M个页面。
页表
根据页号如何知道这个页在内存中的起始地址呢?
图2解释了为什么页表中的页号是隐含的?因为页号就是一个递增序列,没必要去存储它,根据块在页表数组中的位置就能推出它的页号。
逻辑地址包括页号和页内偏移量(页号和页内偏移量可以直接根据逻辑地址得出,见上一小节),如何计算进程的某个页(如页号为2的页)在内存中的实际地址?
- 首先要知道这个页在页表中对应的页表项的起始地址(页表项起始地址(块号所在地址)= 页表在内存中的起始地址+页表项长度*页号)。
- 然后拿到页表项中的块号。
- 再根据块号计算出这个页在内存中的物理地址(块号*页面大小+页内偏移量)。
3.1.7 基地址变换机构
图1是基地址变换的流程图。
页表项长度、页表长度、页面大小
这三个值CPU在一开始就是知道的。
- 页表项长度:每个页表项占多大的存储空间。
- 页表长度:页表中页表项([页号]+块号)的个数。
- 页面大小:每个页面占多大的存储空间。
逻辑地址转换成内存中的实际地址的过程
逻辑地址包括页号和页内偏移量(页号和页内偏移量可以直接根据逻辑地址得出,见上一小节),如何计算进程的某个页(如页号为2的页)在内存中的实际地址?
- 首先要知道这个页在页表中对应的页表项的起始地址(页表项起始地址(块号所在地址)= 页表在内存中的起始地址+页表项长度*页号)。
- 然后拿到页表项中的块号。
- 再根据块号计算出这个页在内存中的物理地址(块号*页面大小+页内偏移量)。
3.1.8 具有快表的地址变换机构
局部性原理
- 时间局部性:如果执行了程序中的某条指令,那么不久后这条指令很有可能再次执行;如果某个数据被访问过,不久之后该数据很可能再次被访问。(因为程序中存在大量的循环)。
- 空间局部性:一旦程序访问了某个存储单元,在不久之后,其附近的存储单元也很有可能被访问。(因为很多数据在内存中都是连续存放的)。
上小节介绍的基本地址变换机构中,每次要访问一个逻辑地址,都需要査询内存中的页表。由于局部性原理,可能连续很多次查到的都是同一个页表项。既然如此,能否利用这个特性减少访问页表的次数呢?
快表
快表,又称联想寄存器(πLB),是一种访问速度比内存快很多的高速缓冲存储器,用来存放当前访问的若干页表项,以加速地址变换的过程。与此对应,内存中的页表常称为慢表。
地址变换过程
- ①CPU给出逻辑地址,由某个硬件算得页号、页内偏移量,将页号与快表中的所有页号进行比较。
- ②如果找到匹配的页号,说明要访问的页表项在快表中有副本,则直接从中取出该页对应的内存块号,再将内存块号与页内偏移量拼接形成物理地址,最后,访问该物理地址对应的内存单元。因此,若快表命中,则访问某个逻辑地址仅需一次访存即可。
- ③如果没有找到匹配的页号,则需要访问内存中的页表,找到对应页表项,得到页面存放的内存块号,再将内存块号与页内偏移量拼接形成物理地址,最后,访问该物理地址对应的内存单元。因此,若快表未命中,则访问某个逻辑地址需要两次访存(注意:在找到页表项后,应同时将其存入快表,以便后面可能的再次访问。但若快表已满,则必须按照一定的算法对旧的页表项进行替换,见下面“页面置换算法”的小节)。
由于查询快表的速度比查询页表的速度快很多,因此只要快表命中,就可以节省很多时间因为局部性原理,一般来说快表的命中率可以达到90%以上。总的来说,当快表命中时,就节省了通过页号计算块号的过程(页表项起始地址(块号所在地址) = 页表在内存中的起始地址+页表项长度*页号)。
3.1.9 两级页表
单级页表存在的问题
- 问题一:页表必须连续存放,因此当页表很大时,需要占用很多个连续的页框。
- 问题二:没有必要让整个页表常驻内存,因为进程在一段时间内可能只需要访问某几个特定的页面。
解决办法
可将长长的页表进行分组,使每个内存块刚好可以放入一个分组(比如上个例子中,页面大小4KB,每个页表项4B,每个页面可存放个页表项,因此每1K个连续的页表项为一组,每组刚好占一个内存块,再讲各组离散地放到各个内存块中)。
另外,要为离散分配的页表再建立一张页表,称为页目录表,或称外层页表,或称顶层页表。
两级页表
图1中进程由2^20个页表项,太大了,所以拆分成1024个页组,每个组有1024个页表项(每个组的大小是4B * 1024 = 4KB),以组为单位存放在内存块中,正好存的下。
图2中建立了顶级页表,来索引到二级页表,同时逻辑地址的结构也发生了变化。
两级页表的访存次数分析(假设没有快表机构)
第一次访存:访问内存中的页目录表
第二次访存:访问内存中的二级页表
第三次访存:访问目标内存单元