- 内存的使用仍要从计算机如何工作开始讲起。根据冯 · 诺依曼的理论,在计算机中,程序和数据是以同等的地位存储在存储器中的,计算机工作就是不断地“从内存中取出指令 - CPU分析指令 - CPU执行指令”的过程,由此,内存就被使用了起来。
L20 内存使用与分段
内存的使用
- 内存的使用仍要从计算机如何工作开始讲起。根据冯 · 诺依曼的理论,在计算机中,程序和数据是以同等的地位存储在存储器中的,计算机工作就是不断地“从内存中取出指令 - CPU分析指令 - CPU执行指令”的过程,由此,内存就被使用了起来。
存储程序
所以首先就要让程序进入内存。
如下图所示是一个 C 文件经过编译并将编译后的内容存储到内存中的过程,编译后的文件中的
call 40
指示了在进入程序后应该跳转到哪里去执行 main 函数,于是在将程序存储到内存中时,将 main 函数存储在物理内存地址为 40 的地方,由于在编译后的文件里,main 函数偏移入口地址的偏移量为 40,所以理所当然地将入口地址存储在物理内存地址为 0 的地方,使得在内存中,main 函数偏移入口地址的偏移量也为 40,由此就将程序存储在内存里了。但这样存放程序是有问题的,因为物理内存地址为 0 地址处不一定有空闲的空间来放程序(由之前所学可知,在物理内存地址 0 地址处存放的是 system 模块),而且其他的程序可能也想这样存放在内存中,即从物理内存 0 地址处开始存放,所以要改变存放程序的思路。
在存放程序时,应该是先申请一段空闲的内存,然后将程序存放进去,如上图所示,将该程序的入口地址存放在物理内存地址 1000 地址处,然后将 main 函数存放在物理内存地址 1040 地址处,所以还要进行一定的处理,让
call 40
应该是跳转到 1040 处,而不是跳转到 40 处。
★重定位
我们可以通过重定位来修改程序中的地址,如下图所示,上面的程序在实际存储到物理内存中时,要将所有的地址加上一个基址,比如该程序的入口地址存储在物理内存地址 1000 地址处,那么该程序中的所有逻辑地址都应该加上 1000,才能使程序正常运行。
重定位可以在编译时完成,也可以在载入时完成,这两种方法都各有优缺点。
- 编译时完成重定位是在程序编译时,就明确了要将该段程序存储在内存中的哪个位置,所以在载入到内存中时就不需要再计算了,直接载入即可,但该方法需要提前知道内存中有哪些空闲的地方,而且内存应该为该段程序保留空闲的地方,实际操作起来较为困难,而且不够灵活。
- 载入时完成重定位是在程序编译完成后,在程序载入到内存中时进行的,所以只有在入口地址确定好物理内存地址后,才再将程序中的所有逻辑地址加上一个入口地址基址,再存储到内存中,该方法比较灵活,但在载入时需要计算,所需时间更长一点。
但上述两种完成重定位的时机都不够好,因为程序并不是在载入到内存中后就一直保持不动了,由前面所述,进程是会睡眠的,如果一个进程睡眠的时间比较久,那就相当于该进程一直占着内存的空间却不运行,浪费了资源,所以要将该进程“换出”,为其他程序腾出空间,那当该程序下次再”换入”到内存中时,就不一定是上一次的地址了,所以重定位最合适的时机应该是在★运行时重定位。
运行时重定位时,不再是改变程序中的所有逻辑地址的值,而是在进程运行到具体某条指令时,将此处的逻辑地址(例如40)加上一个进程当前的基址(例如1000、2000),这样就得到了该指令处的“当前物理内存地址”,即★每执行一条指令都要根据逻辑地址算出物理地址,称为地址翻译。而基址base就存储了进程的 PCB 中。
★物理地址 = 基地址 + 逻辑地址。
由此可以再回顾一下进程的运行过程,首先在创建一个进程时,要在内存中找一段空闲的空间,然后创建该进程的进程控制块PCB,再将该空闲内存的基地址存储在PCB中,此时进程就可以存储在这段空闲内存里;在该进程运行时,基址寄存器就会从 PCB 中读出该进程的基地址,再加上进程的逻辑地址,就可以得到进程的物理地址;在该进程切换时,基址寄存器就会读取下一个进程的 PCB 中的基地址,类似于内核栈切换中 esp 的切换。
★★★分段
前面所述的将程序放入空闲内存中是将整个程序放入内存中,但实际上程序是分段放入内存中的。
如下图所示,一个程序可以分为好几个段,每个段都有一个基地址,前面用到的
mov[300]
中,逻辑地址 300 的物理地址是根据变量集段中的基地址加上逻辑地址 300 得到的(**<段号,段内偏移>**)。由于现在将一个程序分成了好几段,所以对应于每个段,都应该有一个基址,即 PCB 中应该存有多个基址(在后面知道,就是 PCB 中的 LDT 表),所以基址寄存器就不止一个了,我们可以用进程段表来存储每个段的基址,对于操作系统来说,这个进程段表就是 GDT 表,而对于一个进程来说,这个进程段表就是 LDT 表。
★最后总结一下:一个程序不是整个地存储在内存中的,而是分段存储,所以一个程序变为进程时,首先在内存中找到空闲的空间,然后将自身分段地存储到这些空间中,由于进程在创建时会建立一个进程控制块 PCB,所以就可以把分段存储的内存空间的基地址都保存在 PCB 的段表中,即,将这些基地址都保存在进程的 LDT 表中,再把这个表存储在 PCB 中,进程在运行时,每次执行一条指令,都要进行地址翻译,通过查 LDT 表的基地址,再加上逻辑地址,就能找到运行时实际的物理地址;在进程切换时,切换了 PCB,也就切换了 LDT表。