在Linux0.11内核中,为了有效地使用机器中的物理内存,内存被划分成几个功能区域,如下图
其中,Linux内核程序占据在物理内存的开始部分,接下来是用于供硬盘或软盘等块设备使用的高速缓冲区部分。当一个进程需要读取块设备中的数据时,系统会首先将数据读到高速缓冲区中;当有数据需要写到块设备上去时,系统也是先将数据放到高速缓冲区中,然后由块设备驱动程序写到块设备上。最后部分是供所有程序可以随时申请使用的主内存区部分。内核程序在使用主内存区时,也同样要首先向内核的内存管理模块提出申请,在申请成功后方能使用。对于含有RAM虚拟盘的系统,主内存区头部还要划去一部分,供虚拟盘存放数据。
在IntelCPU中,提供了两种内存管理(变换)系统: 内存分段系统(segmentation System)和分页系统(Paging System)。而分页管理系统是可选择的,由系统程序员通过编程来确定是否采用。为了能有效地使用这些物理内存,Linux系统同时采用了Intel CPU的内存分段和分页管理机制。
在Linux0.11内核中,在进行地址映射时,我们需要首先分清3种地址以及它们之间的变换概念:
a.程序(进程)的逻辑地址;b.CPU的线性地址;c.实际物理内存地址
逻辑地址(Logical Address)是指有程序产生的与段相关的偏移地址部分。在Intel保护模式下即是指程序执行代码段限长内的偏移地址(假定代码段,数据段完全一样)。应用程序员仅需与逻辑地址打交道,而分段和分页机制对他来说是完全透明的,仅由系统编程人员涉及。
线性地址(Linear Address)是逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。如果启用了分页机制,那么线性地址可以再经变换以产生一个物理地址。若没有启用分页机制,那么线性地址直接就是物理地址。Intel 80386的线性地址空间容量为4G。
物理地址(Physical Address)是指出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。如果启用了分页机制,那么线性地址会使用页目录和页表中的项变化成物理地址。如果没有启用分页机制,那么线性地址就直接成为物理地址了。
虚拟内存(Virtual Memory)是指计算机呈现出要比实际拥有的内存大得多的内存量。因此它允许程序员编制并运行比实际系统拥有的内存大得多的程序。这是的许多大型项目也能够在具有有限内存资源的系统上实现。一个很恰当的比喻是:你不需要很长的轨道就可以让一列火车从上海开到北京。你只需要足够长的铁轨(比如说3公里)就可以完成这个任务。采取的方法是把后面的忒贵立刻铺到火车的卡尼曼,只要你的操作足够快并能满足需求,列车就能象在一条完整的轨道上运行。这也就是虚拟内存管理需要完成的任务。在Linux0.11内核中,给每个程序(进程)都划分了总容量为64MB的虚拟内存空间。因此程序的逻辑地址是0x000000到0x4000000。
有时我们把逻辑地址成为虚拟地址。因为与虚拟内存空间的概念类似,逻辑地址也是与实际物理内存容量无关的。
在内存分段系统中。一个程序的逻辑地址是通过分段机制自动地映射(变换)到中间层的现行地址上。每次对内存的引用都是对内存段中内存的引用。当一个程序引用一个内存地址时,通过把相应的段基址加到程序看得见的逻辑地址上就形成了一个对应的线性地址。此时若没有启用分页机制,则该线性地址就被送到CPU的外部地址总线上,用于直接寻址对应的物理内存。
若采用了分页机制,则此时线性地址只是一个中间结果,把需要使用分页机制进行变换,再最终映射到实际物理内存地址上。与分段机制类似,分页机制允许我们重新定向(变换)每次内存引用,以适应我们的特殊要求。使用分页机制最普遍的场合是当系统内存实际上被分成很多凌乱的块时,它可以建立一个大而连续的内存空间影响,好让程序不用操心和管理这些分散的内存块。分页机制增强了分段机制的性能。页地址变换是建立在段变换基础之上的。任何分页机制的保护措施并不会取代段变换的保护措施而不只是进行更进一步检查操作。
因此,CPU进行地址变换(映射)的主要目的是为了解决虚拟内存空间到物理内存空间的映射问题。虚拟内存空间的含义是指一种利用二级或外部存储空间,使程序能不受实际物理内存量限制而使用内存的一种方法。通常虚拟内存空间要比实际物理内存量大得多。
那么虚拟内存空间管理是怎样实现的呢?原理与上述列车运行的比喻类似。首先,当一个程序需要使用一块不存在的内存时(也即在内存页表项中已标出相应内存页面不在内存中),CPU就需要一种方式来得知这个情况。这是通过80386的页错误异常中断来实现的。当一个进程引用一个不存在页面中的内存地址时,就会触发CPU产生页出错异常中断,并把引起中断的线性地址放到CR2控制寄存器中。因此处理该中断的过程就可以知道发生页异常的确切地址,从而可以把进程要求的页面从二级存储空间(比如硬盘)加载到物理内存中。如果此时物理内存已经被全部占满,那么可以借助二级存储空间的一部分作为交换缓冲区(Swapper)把内存中暂时不使用的页面交换到二级缓冲区中,然后把要求的页面调入内存中。这也就是内存管理的缺页加载机制,在Linux0.11内核中是在程序mm/memory.c中实现。
Intel CPU使用段(Segment)的概念来对程序进行寻址。每个段定义了内存中的某个区域以及访问的优先级等信息。而每个进程都可有若干个内存段组成。程序的逻辑地址(或成为虚拟地址)即是用于寻址这些段和段中具体地址位置。在Linux0.11中,程序逻辑地址到线性地址的变换过程使用了CPU的全局段描述符表GDT和局部段描述符表LDT。由GDT映射的地址空间称为全局地址空间,由LDT映射的地址空间则称为局部地址空间,而这两者构成了虚拟地址的空间。具体的使用方式如下图
图中画出了具有两个任务时的情况。对于中断描述符表idt,它是保存在内核代码段中的。由于在Linux0.11内核中,内核和各任务的代码段和数据段都分别被映射到线性地址空间中相同的基地址处,且段限长也一样,因此内核和任务的代码段和数据段都分别是重叠的。另外,Linux0.11内核中没有使用系统段描述符。
内存分页管理的基本原理是将整个主内存区域划分成4086字节为一页的内存页面。程序申请使用内存时,就以内存页尾单位进行分配。
在使用这种内存分页管理方式时,每个执行中的进程(任务)可以使用比实际内存容量大得多的连续地址空间。对于Intel80386系统,其CPU可以提供多达4G的线性地址空间。对于Linux0.11内核,系统设置全局描述符表GDT中的分段描述符项数最大为256,其中2项空闲,2项系统使用,每个进程使用两项。因此,此时系统可以最多容纳(256-4)/2 +1 =127个任务,并且虚拟地址范围是((256-4)/2)*64MB =4G,见下图。4G正好与CPU的线性地址空间范围或物理地址空间范围相同,因此在0.11内核中比较容易混淆三种地址概念。
进程的虚拟地址需要首先通过其局部段描述符变换为CPU整个线性地址空间的地址,然后再使用页目录表PDT(一级页表)和页表PT(二级页表)映射到实际物理地址页上。因此两种变换不能混淆。
为了使用实际物理内存,每个进程的线性地址通过二级内存页表动态地映射到主内存区域的不同内存页上。因此每个进程最大可用的虚拟内存空间是64MB。每个进程的逻辑地址通过加上任务号*64M,即可转换为线性地址。不过在注释中,我们通常将进程中的地址简单地称为线性地址。
从Linux内核0.99版以后,对内存空间的使用方式发生了变化。每个进程可以单独享用整个4G的地址空间范围。
分页管理机制在第10章中有进一步说明