我们刚才对内存管理的功能
进行了一个基本的讨论
接下来在讨论具体的内存管理算法之前
我们有必要来讨论一下
内存管理当中地址的生成
我们说 从你写的程序里用到的符号
到最后在总线上出现的物理地址
这中间有一个转换的过程
在具体说转换算法之前
我们需要知道地址的生成过程
和在生成过程当中并不是任何一个
你想要访问的地址都是会允许你访问的
这里还有一些安全的检查
或者说合法性的检查在里头
接下来我们看地址空间的定义
我们在机器里总线上看到的地址
是这里我们所说的 物理地址
所有的物理地址空间所构成的空间
叫做物理地址空间
它是由硬件支持的
通常情况下 比如说
我们机器里说有多少位地址总线
指的就是这里的物理地址总线的条数
比如说32位的
通常情况下就是32条地址线
在我们这里它的编号是从0
比如说32位 是0到4G减一
那么这是从0开始一直到它最大编号
这个编号在存储单元角度来讲是唯一的
但是这种唯一
实际上对于我们写程序来讲
是不太容易来使用的
因为我到底用哪个地址
在程序写成之前 或者运行之前
我可能是不知道的
那么这样一来
我们在这里用到第二个地址
是逻辑地址
逻辑地址是CPU运行的时候
里边的进程看到的地址空间
那通常情况下
对应我们可执行文件里的那一段区域
加载程序的时候
你的程序加载到内存当中 它变成进程
这个时候在你的可执行文件里的
0到它最大值
这个地方在相应地址空间里有一段区域
这段区域就是我们进程的逻辑地址空间
逻辑地址转换成物理地址
就是我们后面会说到的方法
那么这时我们这里访问到一条指令
这条指令在执行的过程中
它会访问相应的内存单元
这些内存单元的地址从哪来
就是从这 逻辑地址
根据我们后面会说到方法
转换成物理地址
最后在总线上访问相应的存储单元
我们在后边 一直说需要大家理解
我到底用到逻辑地址
物理地址分别是什么
它们之间的转换过程是什么样子
接下来我们看逻辑地址的生成
大家在写程序的时候 通常情况下
现在我们写程序都是用高级语言
高级语言里头 这就是一个小例子
一个程序 它有一个保留字
prog到end
这是它的开始和结束标志
然后中间我调用了一个函数
这个函数就是一个地址
我们通常在写函数的时候
里头你不会写 比如说0X多少多少
作为你的函数的名字
这个很不容易记
我们会用一个符号来表示
用符号之后 不同的函数之间
这些符号之间
它们就没有一个先后顺序的关系
那你放到内存里头
我可以把它放到任何一个位置
这是我们在写程序源代码的时候
你所希望见到状态
然后这个源代码我们就会进行一次编译
源代码里的这些语句
我们的CPU是没办法直接认识
我们为了让CPU能认识
必须转化成CPU能认识的指令
指令我们转换出来的第一步
是把它转变成机器能认识的指令的汇编码
这是汇编指令
这样转换过来之后
我们看到它会转变成函数调用
jmp或者是call
都是会有的
然后后面仍然用的是符号名字
这还只是汇编的源代码
那我们通过编译之后得到汇编码
然后我们会再对它进行一次汇编
汇编之后实际上我们就变成了二进制代码了
这个时候就是实实在在是机器能认识的指令
这个时候里头的符号就不能再是
我们前面讲的这些字符串了
而必须是地址空间里的某一个位置
比如这个地方就是75
那75 这是从0
假定这个长度是蹦到这个位置
那么就是你在当前的位置蹦到75
这个地方用到的就是编号
这是一个编号实际上是
在这里头我可能会用到别的符号
别的地址
比如我有一个函数调用
从模块A调用模块B里的一个函数
在这个调用过程中
在你做汇编的时候
另一个模块的位置你并不知道
这时我需要再有一个链接的过程
把多个模块和你用到的函数库
搁在一起 把它排成一个线性的序列
排了之后这个时候我就知道
你跳转的另一个符号的位置在哪
比如说我们在这里头你自己会移动
模块之间也会有
那这个时候我会告诉你
这个地方蹦完之后
我放的位置往后挪了
前面是放的函数库
所以我起头的位置往后挪了100
那我这变成了175
这个175实际上就是从0开始
我只是在这一个文件内的
如果说我的程序这个时候去运行
那么它运行的时候放到什么地方
不一定能正好放到0的位置
那这个时候我在加载的时候
还会再有一个重定位
这个重定位是说我原先的0
这是175
现在我加载进来之后
我把它放到1000的位置
那这1000呢 从1000到1175
这个时候我的跳转
我原来蹦到175
你蹦的位置就错了
我要统一把这个要平移一遍
这我就变成1175
这是你在加载时候的
由操作系统提供的
重定位的功能要干的事情
有了这个之后
我们的程序在跑的时候
它就变成是实实在在的地址了
这是逻辑地址
然后 我们在获取这个地址的时候
可以在什么时候来做到
刚才做的是直接加载的时候
但实际上地址生成机会有这样几个情况
第一个是编译
假定我知道我最后要放的位置
我在编译的时候就可以把这个地址写死
如果是这种情况 你的起始地址发生变化
你就必须重新编译了
这种情况现在在什么地方出现
像我们用到手机
如果说你的手机是功能手机的话
不是智能手机
这个时候里面的程序通常情况是写死的
不允许你买了手机之后
自己再往里装地址本了
或者说装软件之类的
那么这个时候通常情况下
在前面这些都是写死的
还有一种情况是允许你加载到不同地方
比如说像我们现在的智能手机
那你就可以在买到这个手机之后
我再往里加我的程序
这个时候写程序的人就没办法知道
你这个程序最后会加载到
你的系统里的什么地方去
如果是这种情况 我在加载的时候
就必须做重定位
也就是说我根据我装到内存位置里的不同
我要把里头那些符号的名字或者跳转的地址
把它重新捋一遍
通常情况下在我们的可执行文件里头
它前面有一个重定位表
那这个重定项目表
里头包含内容就是
你在这个程序里头
到底哪些地方需要去改的
加载的时候把这个都改成绝对地址
那你的程序就可以跑了
这是我们刚才见的情况
还有一种情况 是执行时生成
这个相当于我们在前面用的
一直就是相对地址
那么到执行的时候
执行到这条指令的时候
我才可以去知道它确切访问的是什么地方
这种情况出现在
我们使用虚拟存储的系统里头
也就是说我执行一条指令
这条指令访问的位置 访问到那之后
有可能你当时把这一块区域
放到内存的某一个位置
这个时候它有一个映射
映射过去之后你找到相应的位置
那这个时候只是在执行这条指令的时候
才会做这种映射
这样做就有一个什么样的好处
我这个程序在执行的过程当中
我就可以把它所在的位置
在物理存储的位置 我可以挪
而如果说是前面两种情况的话
你不但要求你在地址空间是连续的
同时在这里头 它运行起来之后
你是不能再动它的
比如说在这里头我加载的时候做了重定位
我已经写了绝对地址了
由于你存储空间不够
或者说别的程序的存储空间不够
你把它位置往后挪了一段
你这么一挪完之后 你那些位置就不对了
所以从灵活性的角度来讲
我们在执行时候生成这个地址是最好的
而前面几种它有一个好处是简单
所以在这里 不同系统里这几种做法
我们现在都是有采用的
下面我们通过一个例子 图示
来看一下地址的生成过程
这是我们在前面已说到过的系统结构
CPU 内存 I/O设备
我们一条指令的执行
比如说在这里头
CPU当前正在执行一条指令
这条指令是movl指令
这条指令在执行的时候里头有地址
这个地址在CPU里先看到了
然后这个时候我的MMU
它依据这边的页表
来把你这边见到地址翻译成物理地址
翻译成物理地址之后
然后CPU里头有一个控制器
这个控制器负责把你得到的物理地址
和相关总线控制信号送到总线上去
这个时候存储单元 存储芯片
这个时候会识别总线上地址和控制信号
依据你控制信号的 到底是读还是写
总线上有一组相应的持续逻辑的交互
如果是写 就会把这边CPU送过来的数据
写到内存当中 指定的存储单元上
如果读那就从指定的内存单元当中
读出数据 放到数据总线上
然后CPU拿回去
这是它的一个交互过程
在这个交互过程当中 CPU能干什么呢
CPU能干的是
地址转换过程它的影响
实际上在每一次访问的时候
它是不依赖于软件的
是由硬件来完成这个转换的
但这个转换的表
我们是可以通过操作系统
来建立这两者之间的关系
这是我们后边会说到的页表的功劳
接下来我们讨论
在地址生成过程当中的地址检查
这里是一个图示
说明我们CPU在执行指令的时候
它的地址处理过程 生成过程
这是一条movl指令
movl指令在CPU执行过程当中
它会产生逻辑地址
这个逻辑地址
比如我访问的是数据段的数据
那好 这时候数据段
它有一个段基址和段的长度
如果说你从数据段去访问的
偏移量超过这个长度
这个时候的这个访问应该是非法的
对于这种情况 在每次访问的时候
每一条指令在访问的时候
它都会去检查你的段的长度和偏移量
是不是有效的范围
如果不是
那么这个时候就走上面这一条
告诉你内存访问异常
这条指令执行失败
进行相应的错误处理
这个由操作系统来做
另一种情况是说
这里检查完的结果
你访问的偏移量
是在0和你的最大长度之间
我认为这是合理的 是合法的
这个时候它会和段基址加在一起
得到你的物理地址
比如说在这500 1000
那就是1500
从这访问到你对应进程的
物理地址空间里去
那在这个过程中我们说
操作系统可以通过用指令来
设置相应的长度和段基址
这是会我们可以通过软件方法来
影响到我这里做相应检查
有了这个检查之后我们就有了
从符号一直到你的逻辑地址
逻辑地址在执行过程当中转变成物理地址
并且在这个过程中有相应的检查机制
这是我们在这里说到地址的生成和检查