操作系统原理之内存(一),操作系统原理内存


一.内存地址重定位

在汇编指令中,我们有时会看到如下指令:

.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

    

 

 

 

 

 

 

 

 

  

相关内容

    暂无相关文章