Linux2.1.11内存管理(一)——内存管理大致模型

逻辑地址至线性地址的转化

我们知道,自从80386提出了保护模式之后,段页式的三级内存寻址机制就成为了IA32体系架构的标准。而在Linux中,在GDT表中有四个至关重要的段描述符:内核代码段、内核数据段、用户代码段、用户数据段。而这四个段的内存覆盖范围完全相等且覆盖了4GB的内存空间,每个段描述符的段基址起始位置都是0x0,段长都是4G,逻辑地址和线性地址是相等的。Linux中设置了段描述符并使用分段模型仅处出于一个原因:

  • X86架构规定必须在保护模式中使用分段机制

32位Linux中的线性地址布局

内核空间和用户空间

在32位的Linux中,进程的低3G作为用户空间来使用,而高1G作为内核空间来使用。不仅仅是Linux将内核的线性地址放置在高端,32位的Windows也将内核空间划分到线性地址的高2G,为什么不同的操作系统都采用了这个设计呢?其实是因为最早的计算机没有保护模式,计算机为一个程序提供一切的资源,后来保护模式的概念先于虚拟地址出现,相比之下DMA等概念出现的都很晚,因此操作系统为了最大程度兼容用户程序(让程序能够保持原有的地址而不是把所有的汇编程序赶走)就选了用户程序最不可能碰到的最大的地址倒着使用。当然后面有了多级逻辑地址后分配在哪不是特别的重要了,但为了便于CPU管理(登记r0~r3分别能访问哪些地址),索性保留了这个设计。1
每个进程独享一个页表。但是需要注意的一点是,线性地址中的最后一个G所映射的物理地址对于所有进程都是一致的,这显然是正确的,因为所有进程共享一个内核。

内核空间的进一步划分

Linux将1G的内核线性空间划分成两部分:

  1. 第一部分大小为896M,用于一般操作,如内核数据和代码存储;
  2. 第二部分大小为剩下的128M,该部分线性地址用于特殊用途,如进行高端物理地址的寻址;

将宝贵的内核页表保留128M一定是有原因的,原因之一就是保留一定的窗口用于映射高端内存。
总体上来看,Linux的大致线性内存分布情况如下:
在这里插入图片描述

Linux中的物理内存布局

不可使用的物理内存

一般在Linux初始化过程中,会将Linux的内核放置在物理地址1M的起始处,也就是地址0x100000处。那为什么不将内核直接放置在物理内存的开始处呢?原因如下:

  1. 在物理内存的第一个M中,有一部分页框由BIOS使用,如0号页框;
  2. 不同的模型对物理内存的使用规则不一样,部分模型可能要求OS不适用物理内存的初始部分;

实际上,在OS初始化过程中,可以运行BIOS例程来了解哪部分物理内存不能被OS使用,但是不是所有的BIOS都提供这个例程,为了兼容性起见,Linux设置了一个缺省的内核放置位置:1M。

内存管理区

在理想情况下,内存中的每个页框应当是一致的,也就是说对于计算机体系结构来说,每个页框和原子一样,是不能区分彼此的。但是实际上,不同的计算机体系结构自己的独特要求,如MIPS体系结构中内存就有节点之分。而在IA32架构中,OS必须处理两种差异:

  1. ISA总线的DMA芯片只能对RAM的前16M进行寻址;
  2. 在启动了PAE物理扩展的情况下,由于线性地址只有32位,OS必须想出一个办法来在只有32位宽度的情况下来编址36位的地址空间;

出于以上原因,Linux对物理内存划分了三个区域:

  1. ZONE_DMA:最开始16M的物理页框;
  2. ZONE_NORMAL:高于16M而低于896M的物理页框,这部分页框用于和内核线性空间的主要部分相映射;
  3. ZONE_HIGHMEM:高于896M的物理页框,这部分内存空间OS无法通过线性地址直接进行访问;

线性地址到物理地址的映射

在之前提到过,将物理地址划分为DMA、NORMAL、HIGH三块,我们知道划分DMA区域是因为DMA芯片的限制,但是为什么要将剩下的部分划分为NORMAL和HIGH这两部分呢?
我们先不考虑这些问题,让我们自己尝试着来进行一次地址映射,尝试解决上节提出的两个问题。在这次物理映射中仅仅定下两条规矩:

  • 将线性部分的3G映射到物理内存的起始位置,即0字节处
  • 物理内存足够大

那么最简单的方式就是:内核线性空间依次映射到物理内存的起始部分,用户空间则通过算法动态分配到内存的高端区域,也就是说内核物理地址和线性地址的转化规则为:
物 理 地 址 = 线 性 地 址 − 3 G 物理地址=线性地址-3G =线3G
在这样的映射关系下,我们的线性地址到物理地址的映射关系应该为:
在这里插入图片描述
但是这样的映射关系存在什么问题呢?由于OS占据了线性地址的最高端,而且这1G线性空间已经完全映射到了物理地址的0-1G处,因此OS将无法访问地址在1G以上的物理内存。
其中一个解决方法是临时将内核的一个页表项保存下来并将映射更换为想要访问的高端内存,访问完毕之后再将页表项替换回原来的页表项。但是这种做法每访问一次高端内存将产生两次TLB命中缺少,并且更换页表也会产生一定的开销。
为了减少这种开销,Linux采用了另外一种解决方法:将内核空间划分成两部分,其中一部分映射到内存的低端,另一部分就用作这里的动态高端内存映射。而高端内存体现在Linux物理内存规划里面就是ZONE_HIGHMEM。当然,由于IA64架构并不存在这个问题,所以在64位的Linux中,ZONE_HIGHMEM总是为0的。
所以在Linux中,实际上的映射关系如下所示:(内存足够大的情况下)

在这里插入图片描述
当然,当内存不够大的情况下,ZONE_HIGHMEN内存区总是为0的,这时候内核空间仅仅映射到物理内存的尽头。

内存管理

上面几节仅仅讲述了Linux如何处理线性空间的划分、线性空间——物理空间映射。但是还没有讲述到OS是怎么进行物理内存的管理的。接下来我们定义两个概念。

  1. 映射:指将线性空间通过某种关系式映射到物理地址中,即分页式内存管理;
  2. 分配:指OS通过某种算法来进行物理内存的分配,也就是某个物理页必须经过这个算法分配出去之后才能被OS或者用户态进程所使用;

从逻辑上来说,如果OS想使用某个页框,正确的做法应该是先分配在进行映射,等不再使用这个页框时先解映射再解分配。也就是说一个页表对应的页框必须是经过分配算法的页框。而这也是用户态进程获取内存的逻辑
但是这似乎和上面几节所说的东西不一致:在LowMem这块内存区中,内核页表所映射的页框并没有经过分配算法进行分配。原因如下2

  1. 先有鸡还有先有蛋:由于实模式下仅能支持64K的寻址空间,分配算法不可能在实模式下进行是实现。而在保护模式下运行的分配算法,必须进行虚拟内存映射并使用一些页框来存放分配算法初始化前的代码和静态数据,而这些页框是不可能通过分配算法分配出去的。为了解决这个问题,Kernel实现了一个线性映射,但是这就提出了一个新的问题,那就是这个线性映射到底需要多大呢?Linux简单的回答了这个问题,除了用于特殊映射的页表之外,全部用作线性映射的窗口。
  2. 提高TLB命中率:另外,如果在OS运行过程中多次修改页表,会导致TLB命中率缺失。

出于上述两个原因,Linux的系统程序员再未进行分配的情况下就提前进行了映射,但是为了保证物理内存不冲突,Kernel对于内存的使用采取这样的原则:尽管Kernel能够通过线性地址直接访问到物理地址,但是系统程序员保证仅对已经分配过的页框进行读写。当然,这样的保证是有系统程序员来保证的,而不是像页表那样由硬件来保证。


  1. https://www.zhihu.com/question/30067586/answer/49392393 ↩︎

  2. https://www.zhihu.com/question/52711172/answer/273826964 ↩︎

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值