操作系统学习笔记【P20】——内存使用与分段

1、内存是如何使用的
回忆计算机的工作过程:CPU就是不断地取指执行。
指令是被存放在内存里的。
因此内存的使用就是:将程序放到内存中,PC指向开始地址,然后不断+1(或是执行分支跳转).使得程序不断执行起来。
主要涉及这两方面的问题:
如何将程序放入内存,
如何让程序执行起来。

如何将程序放入内存

从程序员眼里的程序,到机器指令的过程:
高级语言(c\python) —— 汇编语言 —— 机器指令
汇编语言其实就是一串01机器指令的易于人阅读的形式
所以我们可以把机器指令存在内存里,看作汇编语言存在内存里,如下示意图

指令就是描述对操作数执行什么操作,并指出这个操作数存放在哪里。
指令的执行就是根据指令的含义,从指定位置取出操作数,并执行指定操作。
从指令执行的我们也可以感受到,内存的作用:存指令、数据。
所以要思考把程序存在内存的哪个位置。不能乱放、乱放会导致指令执行出问题
这其中有一个矛盾就是,我们写好的程序被编译为可执行文件时,用到的地址都是从0开始的相对地址,也叫逻辑地址。在程序装入内存时,可以使用任意一段空闲物理地址,为了能找到正确的指令和数据,需要对使用逻辑地址的地方进行重定位,将其对应到正确的物理地址上。这就是程序重定位
程序是怎么被放入内存的呢?从磁盘读到内存里很简单,通过读磁盘就能完成(磁盘使用部分讲),关键问题的读到内存具体的哪一个位置。

例如 在执行如下指令:call _main,假设_main的逻辑地址是40(从起始处偏移40),这个指令相当于call 40。那么执行这条指令时,就会取内存地址40处取出指令,那么取出的地址真的是main的第一条mov 1, [300]吗?如果是的话,程序就正确执行了,内存的作用也得到了正确的发挥。
如何保证在内存地址40处取出的指令是mov 1, [300]呢,自然是要将这条指令存在40的位置。这就要求整个程序是从物理地址为0的地方开始存储,这样mov指令才会在40的位置。
内存物理地址为0的地方,是被操作系统代码所占据的,我们必须换一个地方存储程序。我们可以在物理内存中,找另外一个空闲的地方,比如起始地址为1000的一段内存,那么这里的call 40要变成call 1040程序才能正确执行。

这里的核心工作就是将 逻辑地址 转化成 实际存储的物理地址,就是程序重定位的问题。
可以在以下地方进行重定位,(这部分应该属于程序链接的内容,可以参考袁春风《计算机系统基础(第二版)》/CSAPP等相关部分,这里没有细讲)
编译时重定位:在程序编译为指令时,就将逻辑地址转化为物理地址(40->1040),这样编译出的程序只能装在内存的固定位置,只适用于一些卫星系统等,很不灵活。
载入时重定位:当程序装入内存时,才将逻辑地址转化为物理地址(假如装入的起始地址是1000,就是40->1040,起始地址是2000,就是40->2040)。但是这样的程序一旦载入内存就不能再移动了,对于现在多进程情况下,要对一些暂不执行的进程进行换入换出很不友好。
运行时重定位:当指令执行的时候,才进行地址转换。怎么实现呢,很自然的可以想到,我们在将程度装入内存时,需要记录下起始位置,这样后面进行地址转换时,用起始位置+逻辑地址就行。这个起始位置放在哪儿呢?每个进程有不同的起始位置,这是一个跟进程相关的量,自然放在PCB里。

这样的话,每执行一条指令,就要进行一次地址转换,我们可以用硬件来完成这个操作,以提高执行速度。这个硬件就是MMU,MMU进行重定位的 CPU 寄存器只能有一个(称为基址寄存器)。进程切换时,将其 PCB 中存放的基地址取出来赋给这个寄存器。每次执行指令时,MMU会自动将指令里的逻辑地址加上这个基址,形成正确的物理地址然后发射到地址总线上。这个从逻辑地址到物理地址的过程就是地址转换。
进程切换除了之前讲的内核栈、用户栈等的切换,还有就是进程地址空间的切换,就是通过在MMU的基址寄存器里写不同的值实现的。一个基址就代表一片地址空间。

分段

刚才我们已经讨论了 如何把程序放入内存,并使得程序可以正确的执行。(就是通过地址转换机制,使得程序可以自由存放在内存里)
现在我们要思考,如何更好的放入内存
还是一个说过很多次的概念,我们写的程序实际上就是定义一些数据,然后对这些数据进行操作。
代码段是指令组成的,是只可读的;而数据部分是可读可写的。(学过汇编语言的都很熟)
有的数据定义好了大小就不会变,而有些在运行时会变化,如栈。
所以我们最好根据这些不同的特点,对程序分开存储。(内存划分为,这一段是只可读的代码,这一段是可读可写但不会增长的代码,这一段是可读可写可增长的栈,这样也利于内存管理)
一个程序由多个段组成
将程序分段后,程序就不会一整个直接装入内存了,而是分段装入内存。这样的话,每个段就会有不同的基址,一个进程就会有好几个段基址,把这些段基址放在一起,就形成段表。(一般是:段表存在内存里,段表起始地址放在PCB中)
这样,我们进行地址转换的过程就不是 程序的基地址+逻辑地址了,程序里的逻辑地址也变成了相对于段首的偏移,
转换过程是,先查看段表,找到自己所在段的起始地址,然后加上偏移地址。
一个例子:GDT与LDT

内存分区

程序放入内存后,通过重定位可以使得程序得到正确执行。通过分开存放代码、数据等可以使内存空间得到更好划分、利用。
下面我们来解决如何,通过把程序放在内存的具体的那个位置。试想这样的场景,一整段内存被其中各种不同大小的进程划分得稀碎,在哪儿存放当前要存入的程序,使得内存以后可以存入更多程序,空间得到更大的利用呢?这就是内存分区要解决的问题,划分方法也被成为分区适应算法。
可变分区
在这种模式下,内存空间的有增有减,在分配时减少,回收时增加。空闲的段数,每一段的大小都是动态变化的。
假设现在有两段空闲内存,有一个进程想申请内存,这两段都能满足进程需求,分配哪一段更好呢?
最佳适应算法:找空闲分区里 最小的 且能满足要求的一段分配 但会形成很多极小空间,不能再分配给后面的进程
最差适应算法:找最大的一段内存,但会形成很多中等大小的空间,使得一些较大的进程不能被满足
进程空间大中小都有,而且出现得很随机
最先适应算法:从链首开始,找到得第一段能满足的内存分配 这样效率会很高
注意首次与最先的区分
首次适应算法:要求按地址递增顺序排列空闲分区,每次从分区链的链首开始找,分配第一个能满足要求的空闲分区。
这样低地址空间会出现很多小碎片,高地址空间大概率出现较大的空闲分区
循环首次适应算法:在首次适应算法的前提下,不再每次从链首开始查找,而是从上次找到的空闲分区的下一个空闲分区开始查找。直至找到第一个能满足要求的空闲分区。这样碎片均匀,高地址空间小概率出现较大的空闲分区。

分页

如果每个进程都能被划分成一个一个的小页面,就能较好解决碎片问题。
例如,现在有150 KB和50KB两块空闲内存区域,内存请求的尺寸是160 KB。如果要是能将**内存请求打散,**比如以10 KB为单位打散,那么160 KB请求就是16片,150 KB 的空闲内存区域能满足15片请求,然后在50 KB空闲内存区域上分配1片, 160 KB的内存请求就能全部满足。内存碎片解决了!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值