最近的状态些许迷惑,所以有一段时间没有写东西了,与此同时的,还有几乎停止的OS进度。今天下午也是抽了一片时间来重新学了一下分页存储,然后来写这一篇文章。关于为什么要写,因为真滴觉得分页存储的内容很容易让人发晕,各种相差无几的概念让人经常混淆,所以来写一篇文章梳理一下,也为了接下来更好的学习内存管理的其他内容。
首先来看这个之前提到过的概念。我们在前面的章节在学习内存的分配与回收时,讲过分区存储管理的方式 ,其中不管是固定分区管理还是动态分区管理,都是为程序分配一片连续的内存空间,所以这种方式即为连续分配方式。仔细回想一下连续分配方式会产生的问题:固定分区会产生很多内碎片,而动态分区会产生很多外碎片,想一下为什么会产生碎片呢?因为分给他们的内存是连续的。所以为了解决这个问题,人们就想出了两种解决方法:其一是使用紧凑技术,将分散的多个空闲分区拼接为一个大的空闲分区,看描述也能知道,这需要实现程序在内存中的移动,所以需要配合动态地址重定位的地址重定位方式,但这种方式会增加很多额外开销。其二就是我们今天要介绍的:既然连续分配会产生很多碎片,那为何不离散的分配呢?那么就引出了离散分配方式:
离散分配方式即系统为一个进程分配的未必是一片连续的内存区域,如果离散分配的基本单位是页,就是分页存储管理;如果离散分配的基本单位是段,就是分段存储管理。
既然是离散分配,就是一个程序可能被装入内存的不同区域,这句话的另一层意思是:既要把程序本身分块,也要把内存空间分块,这样才能实现将一个程序装入内存的不同区域。因为是叫分页存储嘛,我们就将名字设置为:在内存空间分块的基础上,对逻辑空间分页(实际上就是把程序分块)。介绍完原理后,我们就可以引出第一组容易混淆的定义了。
对于内存空间:我们将内存空间划分为若干个大小相等的物理块(又称为页框),每个物理块从0开始编号,该编号称为物理块号
对于逻辑空间:按照物理块的大小,系统将装入模块的逻辑地址空间划分为若干大小相等的片,称为页面或页,每个页面也从0开始编号
虽然这样分页存储解决了大多数的碎片问题,但不能忽视的是:进程的最后一页一般装不满一块,会形成一些内碎片。可以发现:内碎片只会出现在最后一页,且这种方式不会出现外碎片。
每个页面不用连续存放,也不用按顺序存放。
明白了其工作原理,接下来一个重要的内容就是,如何实现一个程序的逻辑地址到物理地址的转换。因为其逻辑地址分页以及物理地址分块的特性,想要从逻辑地址到物理地址,我们需要知道以下内容:
知道以上三点后,我们即可以算出物理地址 = 页面在内存中的起始地址 + 页内偏移量。
那么问题来了,如何求出以上三点呢?我们先来看如何求出逻辑地址对应的页号以及逻辑地址相对于页面的偏移量,书上给的名词是页号和页内地址。
要想求两个东西,我们要先明白,我们有什么?我们有一个现成的逻辑地址,以及一个页面的大小,我们称之为页面长度。ok,那问题就很简单了。
页号 = int(\frac{逻辑地址}{页面大小})
页内地址 = 逻辑地址 mod 页面大小
那么现在,我们就有了三者中的两个,接下来就是剩下怎么知道某个页面在内存的起始地址。
按照我们的已知:逻辑地址与页面大小,是无法推出这个页面在内容的起始地址的,既然没有条件,那就创造条件!所以页表这个数据结构就诞生了。
页表:记录页面与其分配物理块的对应关系,基本单位是页表项,一个页表项包括一个页号以及对应的物理块号。
这里有两点需要注意:
那么到目前为止,求出这三点后,我们就可以根据物理地址 = 页面在内存中的起始地址 + 页内偏移量。将逻辑地址转化为物理地址。
其实上面说了这么多,主要还是基于手算的方法,给出一个逻辑地址求出物理地址。对于计算机,其实方法要简单的多,这里我们举个栗子🌰帮助理解:
假设用32位二进制位表示逻辑地址,页面大小为2^12^B,即4K。此时如果给出一个逻辑地址,计算机如何求出物理地址?
即如果每个页面的大小为2^k^B,用二进制表示逻辑地址,则末尾K位即为页内偏移量,其余部分就是页号。