内存管理导学

写在前面的话
       英文原文http://www.osdever.net/(一点点修改)
       这个导学是用来帮助你为你自己的操作系统管理内存。假设你已经完成了boot & loader,打印部分信息。并且内核运行在ring 0。这篇文章不会直接告诉你怎么做。只希望能教你以什么方式实现,为什么要这么做。

 

一、说明
       物理内存管理的任务很简单:把物理内存空间分成合理大小的块,当内核需要的时候分配给内核。这里的块就是“页面”。在x86处理器上,一个页面的大小是4KB。X86处理器是在现代体系结构中是非常独特的,它分段并且分页。使用分段结构有2个优点:
      ●大部分的其他的32位系统结构把4GB的地址空间分成页面,如386。X86使用了分段,使得操作系统可以按照代码段、数据段、堆栈段等等来访问内存。但是在这个方面x86和其他的大多数系统不同。几乎不可能把一个分段的x86操作系统移植到其他的系统上;并且,没有主流的编译器可以在32位模式下处理分段。所以不能在分段的操作系统上写汇编程序。
      ●如果不分页,x86只能寻址16M物理内存。这就回到了286:作为在24位bus上执行的16位处理器,它只能寻址16M。如果没有分段,将被限制在64KB。当386出现的时候它可以把内存分成4KB大小的页面,优点是可以让32位处理器寻址4GB的地址空间。缺点是不能分配比4KB小的内存块。
      这里仅仅讨论页式管理。
      现在我们可以着手开始一些细节部分。事实上我的这里的目标目标只是考虑内存管理,而不去考虑在内核中无处不在的物理选址。这样我们的操作系统就可以从类似于有多少物理内存、内存怎么样被分离中独立出来。例如:Pentium和更高级的处理器可以选择4MB的物理页,通过一个36位地址给你64G物理地址空间(虽然虚拟地址空间仍然被限制在4GB内)。有了一个合适的、独立的物理内存管理,你的操作系统将可以充分利用一个机器,有大量的物理内存,在内存管理外部确看不出任何不同。如果没有,你将也许不得不重新编译你所有的应用程序(不仅仅是内核)。这将是非常枯燥的。

 

二、组织
      内存管理需要留出一部分内存来管理其余的内存。我怎样在分配函数开始工作之前分配内存呢?有两种方法实现:
      ●在每个内存块中留出一部分来保存分配信息,在一个头部中。
      ●在内存中为每个内存块分配一块区域
      如果你在分配小块,第一个选择比较有效。这个经常被malloc()风格的分配函数使用。但是,我们希望以页面的方式管理内存,在每一页的开头处用一些字节保存内存控制信息会很浪费。在这种结构中,内存控制信息也可能被错误的用户程序损坏:通过向保存分配信息的区域写数据。
      因此,对于低级的分配,选择2通常是首选。记住,我们可以访问所有的物理内存就好像内存是全局变量:我们仅仅需要将一个指针指向一块内存区域,我们可以读和写。在这一点上,我喜欢用一个方便的方法来加载内核(在x86及以上的结构上)。你可以跳过下面的内容如果你不懂或者是不关心。
      你的boot & loader也许会将你的内核加载到1MB的地方并且跳过去执行。所以你应该将内核链接到1MB的地方。然后你会开始加载和执行用户程序。你会想将地址空间分离成用户空间和内核空间:用户空间用来存放所有的用户程序的代码和数据,内核空间存放内核和驱动。用户程序不能访问内核空间。没有办法阻止你将内核放在1MB并且把用户空间放在更高的地方(比如,从2GB的地方开始—记住你的虚拟地址空间是和物理地址空间完全独立的)。很多操作系统(Win9x, NT and Linux at least)喜欢让内核从2GB开始,让用户空间在2GB以下。我们能怎么实现,如果我们的内核实际上被加载在1MB的地方?有了分页这个很简单:仅仅让有关的页表项指向正确的物理地址,并且链接内核使他从地址0xC0000000开始。
      但是将内存管理放在boot & loader是不现实的。我们需要让处理器在开启分页前将0xC0000000看做0x100000。然后,我们希望开启分页并且避免重新加载内核。我们可以通过改变内核代码和数据的基地址实现。处理器通过 虚拟地址 + 段基址 得到物理地址并且将它发送给地址总线 (或者,如果分页开启了,发送给MMU)。所以,既然我们的地址空间是连续的(也就是说,没有空洞或跳跃)我们可以让处理器将任何虚拟地址和任何物理地址联系起来。在我们之前的例子中,内核位于0xC0000000但是加载在0x1000000,我们需要一个段基址让0Xc0000000变换到0x1000000。即, 0xC0000000 + base = 0x1000000. 。所以,段基址为0x41000000。当我们涉及到一个在地址0Xc0001000的全局变量,CPU加上 0x41000000并且得到地址0x1001000。这个发生在boot & loader加载了部分内核的情况。如果我们之后开启了分页,并且将地址0xC0000000映射到0x1000000(并且段基址为0),相同的地址可以继续使用。
      现在,我们将留出一部分独立的内核内存来管理其他的内存。
      我们需要知道哪一个页面被分配了哪个页面没有。我们可以想到位图。系统中的每个页面我们都需要一位于之对应。—在一个256MB的系统上,我们需要8192字节的位图来管理所有的65535页。低级的内存管理只需要知道一个页是否被分配。事实上,我们需要知道类似块大小、哪个进程分配它、哪个进程可以访问的信息。基于低级的内存管理写一个高级的内存管理是很容易的。我们所关心的是分割物理地址空间并且向高级内存管理提供接口。
      位图的优势是空间效率,并且简单。每页只需要一位的控制信息:不论页面是否被分配。但是我们每次分配页面的时候我们都要遍历整个位图;在一个大型系统上,遍历时间也许会很大。
      一个可选的方法是使用一个堆栈页。空闲页的物理地址压栈;当分配页面时,从堆栈顶部弹出地址并且使用。当释放页面,它的地址又被压回堆栈。有了这个,一个分配(或者释放)就是一些指针的递增(或递减)。但是虽然大部分分配不需要分配连续的物理地址(MMU可以使不连续的物理地址变成连续),像DMA那样。如果需要物理地址空间是连续的,那么内存管理需要从堆栈的中间取出地址,这是复杂的事情。
      堆栈方法也使得选择物理内存的地址成为困难。

 

三、初始化
      我们需要为控制信息分配一些内存,或者是位图或者是一个或多个堆栈。控制信息的大小将和系统的内存大小相关:最好的解决办法是让boot & loader得到,也许是读CMOS NVRAM得到内存大小。但是那将只会告诉你前64MB。你需要BIOS的15h中断来得到内存大小。
      在我们知道了用户系统拥有多少内存后,留出一部分内存来管理内存。我们在写分配函数之前要怎么做到呢?我们需要分配函数管辖范围之外的一些内存:一些保留的内存。记住我们在系统中有一些保留内存:BIOS,BDA,内存自身。这些都不能被改变。为什么不把我们的位图或者栈附加在内核内存的后面并且像这样对待它:作为一种动态的全局变量。
      我们知道内核的起始和结束地址。我们可以得到内核有多大,来避免分配内核空间中的内存---分配内核中间的内存将会是致命的。所以我们可以扩大内核的区域并且在位图/栈中把那块空间标记为保留。如果是位图的情况,我们需要将对应于那块空间的位置为1.;如果是一个栈,我们将除了保留区域以外的所有地址压栈。

 

四、分配
      我们的第一个内存管理功能是物理页面分配。即,标记一个物理页面为已使用并且返回该页的地址。如果我们用了堆栈就会比较简单:我们记录空闲页面的数目,并且,当需要分配一个页面的时候,减少未使用页面的数目,返回栈顶的页面地址。对于一个位图,我们将需要遍历位图数组并且标记一个页面已使用。
      回到分配指定内存的问题上,如ISA DMA的小于16MB的地址:这对于位图是很简单的(仅仅在在遍历到确定的偏移时停下)。如果是堆栈,你需要两个页面栈,或者是另写一个分配函数,这个函数可以遍历主栈找到一个确定的地址并且标记为已经使用。注意,使用栈很难分配连续的内存。
      如果没有空闲物理页了我们该怎么做?如果我们在硬盘上有一个交换文件,我们需要将页面交换出去直到我们有足够的空闲页面。但是,为了实现那个,我们需要写更多的内核结构(至少硬盘和系统驱动)。所以,到现在,对于低级的内存管理,当没有内存时就panic已足够。
      你的低级别的分配函数返回的是物理地址。这样,你可以读/写。你可以存储数据。一旦你开始将虚拟地址映射到物理地址,你的内存管理将变得更加强大。

 

五、释放
      释放就是分配的反过程所以我不会详细的讲解。清除位图中的一位或者像栈压入一个地址。

 

六、映射
      从这里开始分页结构变得有用了。简而言之(处理器手册解释的更好),分页让你通过软件控制地址空间。你可以将页面映射到任何你喜欢的地方;你可以向页面增加保护功能;并且你可以在缺页中断后分配和映射页面。
      386及以上处理器使用了三级变换结构:CR3保存了页目录的物理地址。页目录是内存中的一个物理页,该物理页分成了1024项,每项是一个32位字,保存了页表的地址。每个页表,也被分成了1024个页表项;每个表项保存了一个物理页的物理地址。
      注意,要覆盖4GB的4096字节大小的页面,对于每个物理地址并不需要32位。CR3、PDEs、PTES仅仅只需要20位,所以低12位用作标志位。这些标志可以为页面和页表提供保护,并且可以把单独的页面和页表标记为存在或者不存在。有一个“accessed”标志,当该位置位时该页面或者页表可以访问。通过转换CR3,可以在不同的环境中使用不同的页目录和页表,这样就可以让不同的应用程序使用不同的地址空间。
      当你开始写用户程序来使用地址空间的时候你会充分意识到分页的好处。现在我们需要写内存管理函数来得到不同的页目录和页表。
      记住页目录和页表仅仅是内存中的普通页面;处理器需要知道他们的物理地址。每个处理器(或每个地址空间)只有一个页目录,每个页目录可以指向1024个页表。我们就可以用我们之前写的分配函数分配这些页面,就像普通页面一样。记住页目录和页表地址需要对齐:即,低12位需要是0(否则他们会和读/写/保护/访问位冲突)。
      在我们开启分页前,我们需要一个有效的CR3和页目录、一个有效的页表指向我们正在执行的地方(一个有效的页表指向现在执行代码的物理页,译者注)。当我们设置CR0中的分页位时,CPU将会从同一个地方开始执行,所以在执行的地方最好有相同的指令。这就是为什么最好将内核放置在在分页开启后他所在的地址。
      所以在开启分页之前,我们需要分配一个页目录和一个页表。你可以用页面分配函数或者是保留大量的全局变量:两个方法是一样的。如果你使用全局变量,确保他们是页面对齐的。
      将所有没有用到的表项设置为0。这将清除现在的所有位,当访问他们的时候会引发一个页面错误。如果你想要你的页面错误看起来比较好,你可以设置一些位让他们处于一种特别的式,。记住每个页目录表项(和页面)覆盖了4MB地址空间,这对于你的内核是足够的。每个页表项覆盖了4KB,所以也许你需要几个页表项来覆盖你的内核。假设你没有动态的重新放置你的内核,你仅仅只需向已经分配的页目录和页表中插入数字,之后你可以让你的内存映射函数更加优雅。

      我之前说的所有是内存管理的初始化路线。所以设置页目录和页表,开启分页,长跳(重置CS)和重载其他选择子(SS,DS,ES,FS和GS).一旦开启了分页需要将段基址设置为0,虽然没有原因为什么要强制这么做.

      幸运的是,我们现在开启了分页。所有的地址都是通过我们之前建立的页目录和页表得到。注意剩余的物理内存没有必要映射到我们的新的地址空间----所需要映射的只是CPU现在执行的代码部分(事实上,你需要映射所有的内核)。我们需要映射显存所以我们可以打印更多信息。

 

七、内存映射
       第一眼看这个非常简单。仅仅向现在的页目录和页表插入正确的页;如果需要分配一个新的页表。但是,CPU的页表和页目录使用物理地址,我们使用虚拟地址。有很多方法实现内存映射:

      ●映射所有的物理内存到地址空间中。这个可以1:1实现(即,物理内存的地址可以被设置为地址空间的底部或者是设置在某个偏移)。这种实现方法的优点是简单;但是,缺点是用户的系统中可能有任意大小的内存,所有的内存都需要可以寻址。设想如果用户有4GB的内存:将没有地址空间剩余。

      ●将每个页映射到地址空间中,并且用和真实页目录同步的虚拟页目录来记录虚拟地址空间。虚拟页目录可以保存每个页表的虚拟地址,同时,真实的页目录保存页表的物理地址。如果需要被直接寻址的物理地址是页目录或者是页表这将是非常好的。但是,它增加了映射所使用的空间—对于一个小系统不够好

      ●让页目录映射自己。这也许看起来是一种很怪异的分层内存映射,但事实上它的效果很好。如果你让每个页目录的第1023项指向页目录自身,处理器将把页目录看做最后一个页表。它将把PDEs当做PTEs,并且将把PTEs看做在最高的4MB地址空间的32位地址。你可以用地址空间的最高4KB作为原页目录的入口。这个的优点是优美并且简单;缺点是你只能在当前的地址空间访问页映射。

      记住为每个阶段设置正确的保护:在页表项,为页设置需要的保护。在页目录表项,为相应的4MB内存设置保护。每个页目录表项应该是可读可写的并且用户可以访问的,除非你有一个好理由让整个的4MB空间让用户不可访问。

 

写在后面的话

       这篇导学分两块讲述了内存管理:管理物理内存(得到总的内存大小、留出一块区域存放内存管理信息、记录物理内存使用情况(位图或栈)、以及分配和释放内存),实现虚拟地址到物理地址的映射。
 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值