311_内存的基础知识
# 3.1_1_内存的基础知识
各位同学大家好。从小节开始,我们会开始学习内存管理新的章节
为了照顾跨考的同学,还有不用考计算机组成原理这门课的同学,我们会在小节当中介绍一些和内存相关的基础知识,我们首先会介绍什么是内存,内存有什么作用,会介绍两个很重要的概念,什么叫内存的存储单元,什么叫做内存的地址,另外内存在进程运行的过程当中充当了什么样的角色,有什么作用。在使用内存的过程当中又会遇到一些什么问题?这个是我们之后会聊的问题,那么我们会按照从上至下的顺序依次讲解。
# 什么是内存
首先来看一下什么是内存,其实这个概念所有的同学肯定都接触过,大家在买手机的时候,一般会告诉我们手机的内存是多少
买电脑的时候也会告诉我们一个电脑的内存是多少。另外如果组装过电脑,或者说自己改装过电脑的同学,可能也买过内存条这种硬件,比如说像内存条 4GB 大小的
那么为什么无论是手机还是电脑上都会有内存这种部件呢?是因为我们的程序或者说软件在运行之前,其实都需要先把相关的数据放到内存里才可以被 CPU 处理。平时我们各种各样的软件其实都是存放在外存或者说储存里的,像电脑的话一般来说就是存放在硬盘里,但是硬盘是一种慢速的设备,但是 CPU 又是一种超快速的设备,所以如果 CPU 要处理的那些程序数据,直接从外存当中拿存取的话,那么很显然 CPU 会有大量的时间需要等待外存的存取操作的完成,所以显然 CPU 直接和慢速的外存进行数据的交互的话,那么是会有速度的矛盾的
所以人们就引入了内存这种部件,它是一种更快速的存储存放数据的硬件,我们可以把需要运行的程序软件,各种各样的数据先放到内存里,内存是一种高速的存储设备里,然后 CPU 直接和从内存当中存取这些数据,这样的话就可以缓和 CPU 和外存之间的速度矛盾了。
那么我们来思考一个问题,在多道程序环境下,系统当中会有多个程序并发执行,既然这些程序在运行之前都需要先放到呃,内存当中也就是说有这么多的程序并发执行,就有这么多的程序的数据需要同时的放到内存里,那么怎么区分各个程序的数据是放在了内存的什么地方呢?
在提出正式的解决方案之前,我们先来看一下我们平时住酒店的例子,每个酒店会有很多房间,这些酒店是怎么区分每个客人到底是住在哪个房间当中的呢?其实很简单,无非就是给房间编上房间号。
和这些酒店一样,其实内存当中也会有一个一个的小房间,每一个小房间其实就是一个所谓的存储单元,我们可以学习酒店给房间编号的这种思想,也给内存的这些各个存储单元编上序号,这就形成了所谓的内存的地址,每一个内存地址会对应一个存储单元。
一般来说内存的地址都是从 0 开始编的,依次递增,在不同的内存中存储单元的大小可能会不一样,那么怎么区分一个存储单元的大小是多大呢?一般来说需要看计算机是不是按字节编制还是按字编制,如果说计算机是按字节编制的话,那就意味着每个存储单元的大小是一个字节,按字节编制的意思就是说一个字节对应一个地址,所以那么既然每个地址又对应一个存储单元,那就意味着一个存储单元的大小就是一个字节。
类似的如果一个字长为 16 位的计算机按字编指,那么就意味着每个存储单元的大小是一个字,而每个字的大小得看计算机的字长是多少。16 位字长的计算机那一个字大小就是 16 个二进制位,也就是 2 个字节,所以在这种情况下,每 1 个存储单元的大小就是 16 个二进制位,也就是 2 个字节。但需要注意的是不同的计算机可能字长还不一样,有的计算机它的字长还有可能是 32 位或者 64 位,具体得看题目当中给出的条件到底是什么,所以一个内存中的存储单元到底有多大。我们得看计算机到底是按字节编制的还是按字编指的,如果是按字编制,我们还得去关注每个字长到底是多少倍。
通过这些讲解,大家对存储单元和内存地址这两个概念应该已经有了比较清晰的认识了。
# 常用的数量单位
接下来我们来补充几个在操作系统这门课当中经常会用到的一些数量单位,比如说像我们买电脑,买手机的时候经常会看到说有 4GB 内存,有 8GB 内存。4GB 到底是什么意思呢?在操作系统这门课当中,1k 指的是 2 的 10 次方这么多,一兆或者说,1m 这是 2 的 20 次方这么多,而 1g 指的是 2 的 30 次方这么多
所以其实4GB 内存指的就是内存当中有 4×2 的 30 次方个字节,大写的 B 指的是一个字节,如果说这个电脑是按字节编制的话,那就意味着内存当中应该有 2 的 32 次方个存储单元。
在实际做题的过程当中经常会有题目告诉我们内存的大小,让我们确定地址的长度应该是多少。所谓地址长度其实就是指要多少个,要用多少个二进制位才可以表示相应数目的存储单元。像这个例子当中,有 2 的 32 次方个存储单元,所以我们就需要用 32 个二进制位来表示这么多的地址,也就是说我们的地址长度应该是 32 位。
# 进程的运行原理:指令
那么在了解了内存相关的最基本的这些概念之后,我们再来看一下内存在进程的运行过程当中到底起到了什么样的作用。我们从指令的角度进行分析。
其实指令这个概念,我们在第一章的时候有很简单的提到过,我们写的用高级语言写的这些代码,经过编译之后会形成与它对等的一系列的机器指令。这些指令是用机器语言二进制来写的,可以被 CPU 识别。那么 CPU 会根据我们的这些指令做出一系列的事情来完成我们指定的操作。
假设我们的变量 x 是存放在这个地址对应的内存单元里的,当然这个地址是用二进制数表示的,那么一般来说像变量这些数据都是存放在所谓的数据段里,而指令是存放在程序段里,还记得程序段和数据段的概念吗?咱们在进程章节学过每一个,进程在逻辑上有三个部分组成,程序段、数据段,还有 PCB,也就是进程控制块。
那么 CPU 会根据进程的程序段里的这些指令来执行一系列的操作,它会依次执行这些指令,比如说此时执行到了指令一,也就是一个数据传送指令,CPU 首先会根据指令前面的这几位来判断,此时这个指令到底是让他干一件什么事情,这几位叫做操作码,那么这个操作码就是指明让 CPU 进行数据的传送,后面的这两组数据其实就是两个参数,就是让 CPU把内存单元为这个地方的数据取出来,放到地址为这个地方的寄存器当中,所以 CPU 根据这条指令,会把 x 的值传送到寄存器这个地方。
接下来会执行第二条指令,第二条指令是让 CPU 进行一个加法操作,就是把这个地址的寄存器当中的数据加 1,于是寄存器当中的内容变成了 11。
接下来执行完第二条指令之后,又会执行第三条指令,同样是一个数据传送指令。不过和刚才的传送方向相反,这次是让 CPU把寄地址的寄存器当中的数据传送到内存的地址当中,所以 CPU 会把数据传送回变量 x 存放的地方,这样的话变量 x 就实现了 x 等于 x 加一这样的操作。
需要强调的是这个地方给出的这些指令并不严谨,这只是为了让大家能够比较直观的体会到这些指令运行大概是一个什么样的流程。总之每一个指令会有一个操作码,告诉 CPU 此时他要做的是什么事情,并且不同的指令可能会对应一系列的参数,那么 CPU 会根据这些参数还有指令的操作码来执行最后具体的操作。 CPU 会根据地址参数来决定到底要去内存的哪个地方去存数据,或者去哪个地方取数据。
在这个例子当中,指令中给出的参数是直接指向了变量 x 存放的实际的内存地址,但是在实际的应用当中,我们在编译的时候,其实并不能确定我们的变量最后到底会被存放在什么地方,比如说我们把进程的数据整体往前移或者整体往后移,这样的话变量 x 存放地址其实就会发生改变了。所以其实我们在编译时是很难确保指令当中的这些地址参数是能够直接指向各个变量最终存放的实际地址的。所以为了解决这个问题,在编译时指令中给出的这些地址参数,一般来说使用的是逻辑地址或者叫相对地址。
# 逻辑地址 vs 物理地址
接下来我们会具体解释什么是逻辑地址或者说相对地址,我们直接来看一个例子,假设有 1 个宿舍,4 个同学要一起出去旅行,然后 4 个人的学号的尾号分别是 0123,住酒店的时候,酒店给他们安排了 4 个房间号相连的房间,然后这几个同学会按照学号递增的次序依次入住房间,比如说 0123这几个同学分别入住了 5678 这几个房间
其实这个地方 4 个同学的编号,0123反映的其实是 4 个同学的一种相对的位置,而各自入住的房间号是指他们最后实际入住的一个绝对位置。由于这几个同学他们是按学号递增的次序依次入住房间的,所以其实我们只要知道 0 号同学住的房间号是多少,我们就可以算出 m 号同学的房间号到底是多少,比如 0 号同学住的是 8 号房间,那么 2 号同学入住的就肯定是 8+2,也就是 10 号房间。所以说我们知道了各个同学的相对位置,还有 0 号同学入住的房间号,也就是他们的起始房号,我们就一定可以算出所有同学入住的房间号,也就是绝对位置。
所以其实这个思想我们也可以把它应用到指令的地址当中。我们在编译的时候只需要关心各个数据存放的相对位置,等实际放入内存中的时候,再想办法根据进程存放的起始位置来得到各个数据的绝对地址。
比如说在之前的那一页提到的例子当中,如果我们能够确定变量 x 存放的相对地址是 100,也就是说相对于进程在内存当中的起始地址而言,再往后数 100 个存储单元,就是变量 x 的存放地址, CPU 想要找到 x 的话,只需要用进程的起始地址再加 100,其实就是 x 的绝对地址,也就是物理地址,相对地址又可以称作逻辑地址,绝对地址又可以称作物理地址,相信通过这个例子,大家相对和绝对到底是什么意义,应该已经有了比较直观的体会了。
# 从写程序到程序运行
那么接下来我们再结合从写程序到程序运行的过程再进行进一步的分析。程序员可以编辑一系列的源代码文件,比如说像 c 语言里的 .c 文件,之后在经过编译器编译之后,会形成与他们对应的若干个目标模块,像 c 语言里的 .o 文件,而这些目标模块其实就是用机器语言表示的一系列等价的指令集合,就像咱们之前提到的 x 等于 x 加一的例子一样。
那么编译的过程其实就可以把它理解为是把高级语言翻译成机器语言的一个过程。需要注意的是这些目标模块当中包含的那一系列的指令当中使用的地址其实是逻辑地址,每个目标模块逻辑地址都是从 0 开始的,那么之后经过链接,会把这些目标模块组装成一个完整的装入模块,并且形成一个完整的逻辑地址空间。像 windows 操作系统里的.exe 文件,其实它就是一个完整的装入模块,又称作为可执行文件,最后会由装入程序,把装入模块放到内存相应的位置当中,形成最终的物理地址,接下来 CPU 就可以正式的开始执行这个程序了。
所以从整个过程来看,从逻辑地址到物理地址的转换,应该是装入这一步需要关心解决的问题。接下来我们需要具体看一下怎么实现逻辑地址到物理地址的转换。
假如我们用 c 语言写了一个程序,定义了一个变量 A,初始值为一,定义了一个变量 x,x 等于 a+1,那么这个程序可能形成了一系列与它对应的指令,比如说第一条指令是往地址为 80 的存储单元当中写入 1 数据,也就是在地址为 80 的那个地方存放 a 变量。
指令二是取出变量 a 的值,并且在加 1,再把它写回地址为 81 的存储单元。那么 81 存储单元存储的就是 x 变量,但是这个地方使用的这些地址指的其实是一种相对地址,也就是逻辑地址。如果说装入模块能从内存地址为 0 的地方开始存放的话,那么其实这些指令里使用的这些地址参数是不需要改变的,因为在这种情况下,指令当中使用的这些逻辑地址其实指向的就是最终的物理地址,所以在 80 这个地方会存放变量 a 的值,然后 81 这个地方会存放变量 x 的值
但是如果我们把装入模块放到了另外一个地方,比如说是从内存地址为 100 的这个地方开始存放的,那么这些指令当中包含的这些地址参数,其实就会指向一个错误的地方。因为本来我们期待变量 a 是存放在相对位置为 80,也就是这个位置,然后变量 x 是存放在这个位置的,但是如果我们不修改这些指令的话,那么最后这个指令有可能会把一这个数写到 80 那个地方去
所以我们在装入的时候可以采取一些方式来解决这些问题,总共有三种处理方式,也就是用三种不同的方法来完成逻辑地址到物理地址的转换,分别是绝对装入,静态重定位和动态重定位。
# 绝对装入
首先来看绝对装入方式,绝对装入是指如果在编译的时候就能够知道程序最后会放在内存中的什么位置,那么编译程序在编译的时候就会直接产生一个包含绝对地址的目标代码或者说指令,那么装入程序也会把这个程序的数据放到相应的位置上。
比如说如果我们刚开始就知道装入模块会从地址为 100 的地方开始存放的话,那么我们经过编译链接之后所形成的装入模块当中使用的地址就不应该是相对地址,也就是逻辑地址,而应该是直接写成一个绝对地址,于是像指令 1 往地址 180,存储单元里放写入 1 数据,实际上也就是相当于往逻辑地址为 80 的地方写入了数据 1,也就是这个位置。
所以如果采用绝对装入的方式的话,那么我们需要保证在编译的时候就能知道这个装入模块最后会被放在什么位置,并且装入程序也需要遵照之前的约定,把装入模块放到之前约定好的位置去,
但是绝对装入这种方式灵活性很低,只适用于单道程序环境。因为在单道程序环境下,内存当中同一时刻只会有一个程序正在运行,所以每一个程序要从什么位置开始存放,那么我们其实可以刚开始就约定好在程序当中使用的这些绝对地址,可以在编译或汇编的时候给出,也可以由程序员直接赋予,但一般来说都是在编译的时候才会产生最终的绝对地址
# 静态重定位
第二种装入方式叫做静态重定位,又叫可重定位装入,在编译链接之后形成的装入模块当中,所使用的那些各种各样的地址,其实也都是逻辑地址,只不过可以根据内存的情况,把装入模块装到内存的适当位置当中,并且在装入的过程当中对各种地址进行重定位,也就是说会由装入程序负责把逻辑地址变换成物理地址。
比如说我们经过编译链接之后形成的装入模块当中使用的这些所有的地址指的都是逻辑地址,也就是相对地址,那么再把它装入到起始地址为 0 的那一系列的内存单元当中的时候,装入程序会负责把所有的指令当中涉及到的那些地址都进行加 100 的操作,这样的话就完成了从逻辑地址到物理地址的转变。所以采用静态重定位方式,这种逻辑地址到物理地址的转变是由装入程序负责进行的
那么静态重定位的一个特点就是作业在装入内存的时候,必须分配它所需要的全部的内存空间,如果内存空间不够的话,就暂时不能装入作业,并且这个作业一旦装入了之后,在运行期间就不能再移动。因为这些指令当中使用的地址在装入的时候就已经确定了,如果说之后这个程序的位置发生了改变,它所存储的这些数据的相应位置也会发生改变,这些指令当中所写的这些地址就会变成一种错误的地址,所以这是静态重定位方式。
# 动态重定位方式
第三种方式叫做动态重定位方式,现代的计算机系统一般都是采用这种方式,那动态重定位又称为动态运行时装入,如果采用这种方式的话,编译链接后形成的装入模块使用的其实也是逻辑地址,也就是从 0 开始的地址。
并且装入程序,把这个装入模块放到内存里的时候,也不会修改这些指令当中的地址,这些地址的转变实际要留到正式运行的时候才会进行。系统会设置一个重定位寄存器,用于存放进程或者说程序的起始地址。比如说从 100 这个位置开始存放,那么重定位寄存器里存放的就是 100, CPU 在执行指令的时候,如果涉及到访问某一个地址的内存单元,那么它会把逻辑地址的值和重定位寄存器里的值进行一个相加的操作。
比如说要访问逻辑地址为 80 的内存单元,那么这个内存单元的实际物理地址应该是 80+100,也就是 180 这个地方。所以动态重定位方式比起之前的那两种方式来说是要灵活的多,采用这种方式的话是允许程序在内存当中发生于移动的,比如说我们把整个程序移动到从 200 那个地方开始,那么其实我们只需要把重定位寄存器的值改成 200 就可以了,这依然不会影响到之后的访存操作。
所以如果采用动态重定位方式的话,逻辑地址到物理地址的转换是到指令运行的时候才进行的,除了允许程序在运行的过程当中发生移动之外,这种方式还有很多优点,比如说可以把程序分配到不连续的存储区,或者在程序运行前只需要装入部分代码就可以投入运行,然后在运行期间再根据需要动态的申请分配内存,并且这种方式还便于程序段的共享,可以向用户提供一个比实际存储空间大得多的地址空间。这些特性现在大家可能还暂时没法理解,咱们在学习分页存储和分段存储之后,再回来看这个部分的内容就很容易了,所以这就是装入的三种方式。
# 链接的三种方式:静态链接
其实在装入这一步之前,还需要经过链接这样一个步骤,链接的方式其实也有三种,第一种方式叫静态链接,就是指在程序运行之前把各个目标模块还有使用到的那些库函数,把它们链接成一个完整的可执行文件,也就是一个完整的装入模块,之后就不再拆开。需要注意的是在链接的过程中,需要把各个目标模块独立的逻辑地址,把它们合并为一个完整的逻辑地址,
通过静态链接形成了完整的装入模块之后,再就可以把这个装入模块放入内存,然后开始运行了。
# 动态链接
第二种方式叫做装入时动态链接,就是把各个目标模块一边装入内存,一边进行链接。
# 运行时动态连接
而第三种连接方式叫做运行时动态连接,就是指在程序执行的过程当中,只有需要目标模块的时候,才会把这个目标模块放到内存并且进行链接。比如说刚开始目标模块一当中写的是 main 函数,那么我们的程序刚开始就需要从目标模块一这儿开始运行,所以目标模块一需要先放入内存,
当运行了一段时间之后,如果说调用到了目标模块二当中的某一个函数,那么我们就需要把目标模块二也放入到内存,并且进行链接的工作。
如果说整个运行过程都用不到目标模块三当中的那些功能,那么目标模块三就不需要再放入内存,所以这种方式比起前两种方式来说要灵活的多,这就是链接的三种方式
# 小结
那么在小节中我们介绍了内存的一系列基础知识,大家一定需要理解存储单元,内存地址,这两个极其重要的概念。那么一个内存地址会对应一个存储单元,而存储单元的大小需要根据计算机是按字节编制,还是按字编制来进行判断
除此之外,我们还介绍了进程运行的一系列基本原理,那么大家需要注意体会什么是逻辑地址,也就是相对地址,什么是物理地址,也就是绝对地址,他们俩的联系和区别是什么
另外从写一个程序到程序运行的过程,也就是编译、链接、装入这几个过程,也曾经作为真题的考点进行考察,需要注意的是链接这一步完了之后会形成完整的逻辑地址,装入这一步结束之后会形成最后的物理地址。
另外任何一个程序的运行都离不开逻辑地址到物理地址的转换,那么这个转换过程一般来说会有三种方式来解决,绝对装入,可重定位装入,和动态运行时装入,绝对装入是在编译时产生了绝对地址,可重定位装入是在装入时将逻辑地址转换成了物理地址,而动态运行时装入是在运行的时候才进行这个地址转换,并且还需要设置一个专门的重定位寄存器,这样的硬件部件才可以实现。
我们之后会花大量篇幅来学习的,段式存储,页式存储都是采用的动态运行时装入。同学们还需要注意这两种装入方式的别名,这种装入又可以称为静态重定位,而这种装入又可以称为动态重定位。这个小节的内容一般都是作为选择题进行考察,大家只需要能够理解,然后对各个概念也能有个印象就可以了。