操作系统(八) -- 内存的分段与分页

版权声明:若未特别声明,文章可随意转载,但是请注明出处~~ 文章若有侵权请在下方评论,博主看到后马上删除!! https://blog.csdn.net/williamgavin/article/details/83216115

前言

cpu的使用基本上告一段落,接下来是内存部分。

正文

内存如何使用:

内存使用就是放在内存中的程序能够按照正确的逻辑顺序执行

首先让程序进入内存:

问题引入

假设一段c代码

int main (int argc, char * argv[])
{
	
	………………
}

编译之后形成的汇编代码如下:

_entry:
	call _main
	call_exit
_main:
……………………
ret

entry是入口地址,如果_main相对于_entry的偏移地址是40

_entry:		// 入口地址
	call 40
	call _exit
_main:		// 偏移为40
………………

现在如果这段程序要运行,那么只要将PC指向 call 40 这条指令所在的地址就好了,执行完call 40之后,会自动跳到地址为40处执行。但是现在有个问题,_main所在的位置一定是物理地址为40的位置吗?换言之,40表示的是物理地址吗?

肯定不是,在操作系统启动的时候就说过,操作系统加载到内存之后会放在从物理地址为0开始的一段内存里面,因此call 40这条指令的40绝对不能表示物理地址40

初始逻辑地址与物理地址

call 40这个40指的是相对于_entry的偏移量,程序里面的地址是相对地址(逻辑地址),而程序真正运行时的地址是绝对地址(物理地址),即程序运行时这个40肯定是要改的,根据逻辑地址得到物理地址就是地址的重定位。比如_entry这条指令的地址如果存放在物理地址为1000处,那么_main的
地址就应该是1040,所以call 40就要变成call 1040.

运行时进行重定位。

在什么时候进行地址的重定位呢?编译时?载入时?还是运行时?

首先看下如果是编译时就进行地址重定位,程序编译之后的代码如果不运行的话是放在磁盘里面的,如果编译的时候从地址1000开始处有一段空闲内存足够该程序使用,这时候进行地址重定位,将基地址设置为1000。也就是call 40就变成1040。但是程序运行的时候并不能保证这块内存仍然还是空闲的,因此编译时进行重定位不行。

如果是载入时重定位呢?当载入内存的时候再进行地址重定位;看起来好像没问题;但是CPU是多进程执行的,而且内存相对于磁盘来说容量是比较小的,假设进程一存放在内存以1000开始的位置,可能某一时刻进程一阻塞了,而且时间还不短,这时候如果再将其放在内存里面肯定是不合理的,因此会将进程一换出到磁盘,1000开始的这个位置变成了空闲区,可能被其他进程占用了,下次进程一换入的时候可能就是在2000这个位置了,但是如果是载入时重定位就意味着在解释地址的时候还是以1000为基址的,所以也不行。

运行时重定位指的是在运行call 40这条指令的时候才将40这个逻辑地址转化成
实际的物理地址,如果程序存放在1000开始的位置,那么就是call 1040,如果是存放在2000的位置,那么就是call 2040;没毛病。

还有一个问题,从上面的分析也能看出,物理地址=基址+逻辑地址。那么这个基址是放在哪个地方呢?每个进程都需要有自己的基址,每个进程…等等,每个进程都有一个专门用来存放该进程信息的数据结构,即PCB;因此可以将该进程的基址放在PCB中。进程切换根据PCB切换一起切换这个基地址。

内存的分段机制

前面说得都是一次将整个程序放入到某一块空闲内存里面。但是事实上是这样吗?不是的,因为内存是分段,为什么内存会分段?因为程序是分段的。

程序员眼中的程序:
在这里插入图片描述

在程序员眼中的程序是分为很多段的,每一段都有不同的特点。适用于不同的领域。每一段都是从该段的地址0开始的。就是说主程序存放的地方地址应该是从零开始的,变量存放的地方地址也应该是从零开始的,其他区域也是如此。用户程序里面每个区域都有其自己的特点,比如主程序这部分应该是只读的,变量所在的区域是可写的,函数库应该是可以可以链接也可以不链接的,栈应该只能单向增加。如果是将整个程序都放在一块的话这些要求肯定不能保证。因此程序应该是要分段保存的,并且这些段都有自己的特点。

既然是分段的,那么是怎么定义地址的呢?还是基址+偏移。只不过这里的基址不再是这个程序的起始位置了,而是这一段程序的起始地址。这个基址放在段表里面

在这里插入图片描述
CPU每执行一条牵涉到地址的指令都会查一下PCB里面这个进程段表,从而确定物理地址。这个表其实就是LDT表,有一个专门存放该表地址的寄存器LDTR寄存器。到目前为止内存已经可以使用起来了。因为地址已经设定好了。


程序运行应该首先将程序从磁盘上面读到内存的空闲区域,这就引出了一个问题:如何在内存里面找到空闲分区。

如何在内存里面找到空闲分区。

首先考虑的问题应该是如何分割内存,前面说的将内存分段以用户的角度看的,现在来单纯的考虑内存该如何分区。

固定分区

第一种方式固定分区,即操作系统初始化的时候将内存等分为n个分区,大小一样。但是程序运行的时候内存的需求有大有小,如果采用这种方式势必会造成很大的浪费。

可变分区

第二种方式可变分区,可变分区的基本思想是建立已分配分区表和空闲分区表,已分配分区表中记录了已经使用了的内存有哪些,注明了这一段内存是哪个程序使用了,起始地址和长度是多少。空闲分区表记录了内存中的空闲区域,包括起始地址和长度。这时候如果有段内存请求,根据请求的内存大小以及空闲分区表上面空闲分区的大小来给这个请求分配内存,同时更新这两张表;如果有进程运行完了也同样更新这两张表。这样做的好处是:可以给需要大内存的程序分配大块内存,给需要小内存的程序分配小内存,提高内存利用率。

可变分区的三种适配方式

可变分区的方式还有一个需要考虑的问题,比如有一个请求需要40K内存,空闲分区表里面有很多个大于40K的内存区域,应该选择哪一个分配呢?

首先适配,顾名思义,就是将第一个符合该请求的内存分配出去。这样的好处是:快。时间复杂度是O(1)

最佳适配,把所有的空闲内存块都看一遍,将最接近40K并且大于40K的内存分配给它。这样的好处是可以可以提高内存的使用率。

最差适配,把所有的空闲内存块都看一遍,将最大块的内存分配给它,并且该内存块一定大于40K。这样的好处是剩下的内存块都比较均匀。

三种适配方式各有其优劣;选择哪种适配方式要根据实际情况来。

可变分区造成的问题

比如有一个160K的内存请求,但是空闲分区表里面只有一个150K的和一个50K的,都不够,怎么办。这其实就是内存碎片。一种方式是内存紧缩,即将150K的内存大小和50K的想办法移到一起。但是这种方式比较耗费时间,并且这段时间电脑是不能工作的。那有没有方法可以消除掉内存碎片呢?答案是将内存分页

将页作为内存分配的最小单元,假设一页为4K,如果一个内存请求是13K,那么就给它分配4页。这样一个进程最多也只会浪费不到4K的内存区域,这种浪费是很小的,因此也不需要进行内存紧缩了。那这种方式会产生内存碎片吗?其实也有会一些内存没有使用到,但是注意这种思想,页是一个单位,每次分配的内存都是整数个页,也就是将这些分配出去的页都看成是已经使用的了,所以就没有内存碎片。因此从内存角度来说这种方式是比较好的,也就是物理内存想要分页,但是用户程序希望是分段。那这个段和页是如何结合的呢?后面会讲。

如何根据逻辑地址找到物理地址

内存分段的时候有一个段表,分页的时候自然也要有一个页表。有一个专门的寄存器存储页表的地址。注意页在内存里面的排布顺序并不是按照地址的顺序递增的,也就是说页0不一定是放在地址零处;这里引入页框,页框是按照内存顺序排列的,并且页框的大小和页是相同的。页表如下图:
在这里插入图片描述
看一个实际的例子吧。

mov [0x2240],%eax

2240这个地址表示的实际内存地址是多少呢?首先看它是那一页的,每一页的大小为4k,0x2240除以4K得到页号,除以4K也就是右移12位。得到2.即第二页;根据这个页号找到具体的页框号,在上图中为3,那么具体的地址就是3*4K+240=3240,即物理地址为3240.

参考资料

哈工大李志军操作系统

猜你喜欢

转载自blog.csdn.net/williamgavin/article/details/83216115