操作系统原理之内存(一)
一.内存地址重定位
在汇编指令中,我们有时会看到如下指令:
.text .entry: 代码入口 call 40 - main: .......
那么这里的40指向的是内存中的哪个位置呢?是内存的实际地址吗?
显然,如果是实际地址的话我们的程序必须被装载在内存0地址处,但这样做肯定是存在问题的,一方面,如果这样的话每个程序都要放到0地址处;另一方面,内存0地址
处已经被操作系统占用。
因此,这里的40必然是一个逻辑地址(或相对地址)
那么,程序在内存中需要修改源代码中的逻辑地址,改为实际物理地址(如上图程序被加载到内存1000处时40和300都被修改了),因为程序并不是始终存在于内存的同一位置的,在被换出(swap out)内存并再次换入(swap in)时很可能并不会被加载到同一位置,所以编译时确定实际物理地址是不可取的;在这里,更可行的方式是运行时动态加上一个基地址(base)。
(如图中所示,操作系统进行内存调度,将部分程序换出,将其他程序换入)
那么这个基地址被存放在哪里呢?在Linux中,基地址会被存到PCB中。在操作系统里每个进程都有一个数据结构与之对应,被称为PCB(进程控制块),当我们fork()一个进程的时候实际上是创建了一个PCB并从其父进程的PCB哪里继承一些数据,并将这个数据结构插入到系统的进程树中。
所以,整体大概是如下流程:
编译程序->fork出PCB(进程)->在内存中找到空闲的内存空间->将这段空间的基地址赋值给PCB中的base字段->载入程序->执行每行程序时将源码中的地址与PCB中的基地址相加。
当然为了提高效率当操作系统切换到当前进程时base字段会被放入寄存器中,所以相加是在寄存器中进行,而且CPU本身为这种操作提供了硬件支持--MMU(内存管理单元)。
二.内存的分段机制
我们在平时玩游戏或写代码时,认为我们所使用的进程(比如游戏程序或eclipse)是整个存在于内存中的,而事实上呢?
如果您学过一些汇编或C/C++的话,可能会很自然地认为进程是如下组成的:
进程分为几个段,每个段都有自己的特点,有不同的用途,而事实上就像上图所示,我们前文所讲的0地址并不是整个程序从0开始,而是程序中的每个段都从0开始,
程序的这几个段有的只读(如代码段),有的可写(如数据段),如果同一处理的话明显会造成混乱,比如错误地写了程序段等。
因此程序在加载时并非整个一起加载进内存,通常是分段加载。
所以,在加载段进内存时,地址重定位就与之前加载整个程序进内存有所不同:
这种情况下,定位一个地址就变为段的基地址+段内偏移量,如果使用之前那种加载整个进程的方式,PCB中只需要存放一个程序基地址,而分段加载则需要
存放每个段的基地址,这样仅仅使用一个base字段是满足不了的,在PCB中必须保存一个表用来保存每个段的基地址:
如上图,根据进程段表,汇编指令运行时会进行重定位,过程如下:比如执行图中第一条mov指令,会在段表中找到DS段的基地址360k,再于100相加,这才是
实际地址。
每个Linux中的进程都会使用一个进程段表(LDT表),而操作系统本身也是一个进程,它也同样使用的进程段表(GDT表)。
因此,我们进程地址重定向的一个整体流程变为:
编译程序-->创建PCB-->程序中的某个段(如代码段)找到一个空闲的内存空间-->将内存基地址存入PCB中的LDT表里-->其它段载入过程类似
-->执行程序,将逻辑地址和LDT表中该段的基地址相加。
三.内存分区和分页
1.内存分区
前文提到我们需要将首先我们的程序需要载入内存,在载入时需要在内存中找到空闲区域,因此必须将内存事先分割成不同区域,那么如何分割内存才合理呢?
首先,我们可能会想到将内存等分为n个区域,但这明显是不合理的,因为我们的程序占用空间大小不一,每个段需要的空间亦不相同。
一种很自然地想法是,根据每个段实际需要的大小进行分配,并记录已经占用的空间和剩余空间:
这样,当一个段请求内存时,就到空闲分区中申请一段内存,并在已分配分区表中记录这一过程。
内存适配:
当一个段请求内存时,如果有内存中有很多大小不一的空闲位置,那么选择哪个最合理?
1. 首先适配:空闲分区表中选择第一个位置(优点:查表速度快)
2. 最差适配:选择一个最大的空闲区域
3. 最佳适配:选择一个空闲位置大小和申请内存大小最接近的位置,比如申请一个40k内存,而恰巧内存中有一个50k的空闲位置(这里最佳并不意味着最好,
因为最佳适配可能导致大量内存碎片)
2.内存分页
尽管分区的方式解决了申请内存的问题,但很明显的是其会带来大量的内存碎片,意思是尽管我们内存中仍然存在很大空间,但全部都是一些零散的空间,当申请
大块内存时会出现申请失败。如图中所示,灰色区域为内存碎片。
为了不使这些零散的空间浪费,操作系统会做内存紧缩,即将内存中的段移动到另一位置。但明显移动进程是一个低效的操作。
这就引入了内存分页的机制:
如图中所示,如果操作系统实现将内存分割成多个页框,而段申请内存时按页分配,这样的话,一个段最多浪费一个页框。也就不需要做耗时的内存紧缩操作了。
但如上图所示,一个段会被分割到多个页框中,显然,我们的内存重定位方式需要进一步改变。
待续。。。。
参考资料:
哈工大操作系统公开课:http://mooc.study.163.com/course/HIT-1000003007
Linux原理图和PCB:http://blog.csdn.net/kangear/article/details/41940091