本文为<x86汇编语言:从实模式到保护模式> 第16章笔记缓存
由于段的长度不定, 在分配内存时, 可能会发生内存中的空闲区域小于要加载的段, 或者空闲区域远远大于要加载的段. 在前一种状况下, 须要另外寻找合适的空闲区域; 在后一种状况下, 分配会成功, 但太过于浪费. 为了解决这个问题, 从80386处理器开始, 引入了分页机制. 分页功能从整体上来讲, 是用长度固定的页来代替长度不必定的段, 藉此解决因段长度不一样而带来的内存空间管理问题. 尽管操做系统也能够用软件来实施固定长度的内存分配, 但太过于复杂, 由处理器固件来作这件事, 可使速度和效率最大化.操作系统
处理器中有负责分段管理的段部件, 每一个程序或任务都有本身的段, 这些段都用段描述符定义. 随着程序的执行, 当要访问内存, 就用段地址加上偏移量, 段部件就会输出一个线性地址. 在单纯的分段模式下, 线性地址就是物理地址..net
一旦决定采用页式内存管理, 就应当把4GB内存分红大小相同的页. 页的最小单位是4KB, 也就是4096字节, 用十六进制表示就是0x1000. 所以, 第一个页的物理地址就是0x00000000, 第2个页的物理地址是0x00001000, 第3个页的物理地址是0x00002000....最后一个页的物理地址是0xfffff000. 这样, 4GB内存划分为1048576(0x100000)个页. 很显然, 页的物理地址, 其低12位始终为0.设计
段管理机制对于Intel处理器来讲是最基本的, 任什么时候候都没法关闭. 也就是说, 即便启用页管理功能, 分段机制依然是起做用的, 段部件依然工做.code
如上图所示, 内存的分配设计段空间的分配和页分配. 左边是虚幻的, 或者说虚拟的4GB内存空间, 称为虚拟内存; 右边是实实在在的内存, 被分红1048576个4KB的页面(每一个方框4KB, 灰色表明已分配).blog
在分页模式下, 操做系统能够建立一个为全部任务公用的4GB虚拟内存空间, 也能够为每个任务建立独立的4GB虚拟内存空间, 这都是可行的. 当一个程序加载时, 操做系统既要在左边的虚拟内存中分配段空间, 又要在右边的物理内存中分配相应的页面. 所以, 第一步骤是寻找空闲的段空间, 该段空间既没有被其余程序使用, 也没有被同一程序内的其余段使用. 好比上图, 假设已经成功找到并分配了一个段空间, 基地址为0x00200000, 长度为8200字节.索引
页的最小尺寸是4KB, 也就是4096字节, 所以, 8200字节的段, 须要占用3个页面, 其中最后一个页面只用了8个字节, 其他都是浪费着, 但这可有可无, 若是容许页共享, 多个段或多个程序能够用同一个页来存放各自的数据. 在分段以后, 操做系统的任务就是把段拆开, 并分别映射到物理页. 注意, 段必须是连续的, 但不要求所分配的页都是连续的, 挨在一块儿的.ip
就上图中的列子来讲, 该段有8200字节, 须要分配3个页面. 操做系统在物理内存中搜索可用的空闲页, 接下来, 要创建线性地址和也之间的对应关系, 在图中, 0x200000~0x00200FFF对应着物理地址为0x00002000的页, 0x00201000~0x00201FFF对应着0x00004000, 0x00202000~0x00202007对应着0x00007000的页, 固然, 这里只是示例, 线性地址区间和页的对应关系能够随意.内存
4GB虚拟内存空间不可能用来保存任何数据, 由于它是虚拟的, 它只是用来指示内存的使用状况. 当操做系统加载一个程序并建立为任务时, 操做系统在虚拟内存空间寻找空闲的段, 并映射到空闲的页, 而后, 到真正开始加载程序时, 再把本来属于段的数据按页的尺寸拆开, 分开写入对应的页中.get
从段部件输出的是线性地址, 或者叫作虚拟地址. 为了根据线性地址找到页的物理地址, 操做系统必须维护一张表, 把线性地址转换成物理地址, 这是一个反过程.
如上图所示, 由于有1048576个页, 故转换表有1048576个表项. 这是个一维表格, 每一个表项占4字节, 内容为页的物理地址. 这个表格的用法是这样的: 由于页的尺寸是4KB, 故, 线性地址的低12位可用于访问页内偏移, 高20位可用于指定一个物理页. 所以, 把线性地址的高20位当成索引, 乘以4, 做为表内偏移量, 从表中取出一个双字, 那就是该线性地址作对应的页的物理地址. 举个例子: mov edx, [0x0002] 执行这条指令, 段部件用段地址0x00200000加上指令中给出的偏移量0x2002, 获得线性地址0x00200002. 线性地址的高20位是表格索引, 即0x00200, 将索引乘以4, 获得0x00800, 这就是表内偏移, 看图, 从该单元能够取出一个双字0x00007000, 这就是页物理地址. 线性地址的低12位是页内偏移, 用页物理地址加上页内偏移量, 就是最终的物理内存地址. 0x00007000加上0x0002, 获得0x00007002, 这就是实际要访问的物理内存地址. 这里有个问题, 为何表内表内偏移为0x000800的地方, 会刚好是物理地址0x00007000, 而不是其余页地址呢? 当程序加载时, 操做系统会首先在虚拟内存中分配段, 而后, 根据段须要分红多少页, 来搜索空闲页面. 当段较大时, 要按页的尺寸分红好几个地址区段, 操做系统用每一个区段的首地址, 取高20位, 乘以4, 做为偏移量访问表格, 并将分配给区段的页的物理地址写入该表项. 最后, 把本来须要写入每一个区段的程序数据, 写到对应的页中. 注意了, 在页式内存管理中, 页面的管理和分配是独立的, 和分段以及段地址没有关系.操做系统所要作的, 就是寻找空闲页面, 把它分配给须要的段, 并将页的物理地址填写到映射表内. 很显然, 也很重要的结论是, 线性地址, 包括线性地址空间, 和页面分配机制没有关系.
基于以上特色, 同时为了充分挖掘分页内存管理的潜力, 通常来讲, 每一个任务均可以拥有4GB的虚拟内存空间; 同时, 每一个任务都有本身的4GB虚拟内存空间, 可是, 很重要的是, 在整个系统中, 物理页面是统一调配的. 考虑这样一种情景: 任务A有一个段, 基地址为0x00050000, 长度为3000本身, 系统为它分配了物理地址0x08001000的页. 过了一会, 任务B加载了, 它也有一个段, 基地址也是0x00050000, 长度为4096字节, 此时, 操做系统为它分配了另一个不一样的, 物理地址为0x00700000的页. 在这种状况下, 在任务A内访问线性地址0x00050006, 访问的实际上是物理地址0x08001006; 在任务B内访问一样的线性地址时, 访问的实际上是物理地址0x00700006.
另外一个问题是, 每一个任务都有4GB虚拟内存空间, 而物理内存只有一个, 最大也才4GB, 根本不够分的. 事实上, 确实不够分, 可是操做系统能够暂时将不用页退避到磁盘, 调入立刻要使用的页, 经过这种手段来实现分页内存管理.
以上, 就是基本的段页式内存管理机制. 基本的段页式内存管理示意图:
咱们知道, 为了完成从虚拟地址(线性地址)到物理地址的转换, 操做系统应当为每一个任务准备一张页映射表. 由于任务的虚拟地址空间为4GB, 能够分出1048576个页, 因此, 映射表须要1048756个表项, 又由于每一个表项4字节, 故映射表总大小为4MB. 没错, 这张表很大, 要占用至关一部分空间, 考虑到在实践中, 没有哪一个任务会真的用到全部表项, 充其量只是很小一部分, 这就很浪费了. 为了解决这个问题, 处理器设计了层次化的分页结构.
分页结构层次化的主要手段是不采用单一的映射表, 取而代之的是页目录表和页表. 以下图所示:
首先, 由于4GB的虚拟内存空间对应着1048576个4KB页, 能够随机的抽取这些页, 将它们组织在1024个页表内, 每一个页表能够容纳1024个页. 页表内的每一个项目叫作页表项, 占4字节, 存放的是页的物理地址, 故每一个页表的大小是4KB, 正好是一个标准页的长度. 注意, 页在页表内的分布是随机的, 哪一个页位于哪一个页表中, 这是没有规律的.
如图所示, 在将1048576个页归拢到1024个页表以后, 接着, 再用一个表来指向1024个页表, 这就是页目录表(Page Directory Table: PDT), 和页表同样, 页目录项的长度为4字节, 填写的是页表的物理地址, 共指向1024个页表, 因此页目录表的大小是4KB, 正好一个标准页的长度.
这样的层次化分页结构是每一个任务都拥有的, 或者说, 每一个任务都有本身的页目录和页表. 以下图所示, 在处理器中有个控制寄存器CR3, 存放着当前任务页目录的物理地址, 故又叫作页目录基址寄存器(Page Directory Base Register: PDBR). 每一个任务都有本身的TSS, 其中就包括了CR3寄存器域, 存放了任务本身的页目录物理地址. 当任务切换时, 处理器切换到新任务开始执行, 而CR3寄存器的内容也被更新, 以指向新任务的页目录位置. 相应的, 页目录又指向一个个的页表, 这就使得每一个任务都只在本身的地址空间内运行. 从下图能够看出, 页目录和页表也是普通的页, 混迹于所有的物理页中. 它们和普通页的不一样支持仅仅在于功能不同. 当任务撤销以后, 它们和任务所占用的普通页同样会被回收, 并分配给其余任务.
对于Intel处理器来讲, 有关分页, 最简单和最基本的机制就是这些; CR3寄存器给出了页目录的物理地址; 页目录给出了全部页表的物理地址, 而每一个页表给出了它所包含的页的物理地址. 好了, 该清楚的都清楚了, 惟一还不明白的, 应该是如何用这种层次性的分页结构把线性地址转换成物理地址? 这里举个例子, 某任务加载后, 在4GB虚拟地址空间建立了一个段, 起始地址为0x00800000, 段界限为0x5000, 字节粒度. 当前任务执行时, 段寄存器DS指向该段. 又假设执行了下面一条指令
mov edx, [0x1050]
此时, 段部件会输出线性地址0x00801050. 在没有开启分页机制时, 这就是要访问的物理地址. 但如今开启了分页机制, 因此这是一个下虚拟地址, 要通过页部件转换, 才能获得物理地址.
以下图所示, 处理器的页部件专门负责线性地址到物理地址的转换工做. 它首先将段部件送来的32位线性地址分为3段, 分别是高10位, 中间10位, 低12位. 高10位是页目录的索引, 中间10位是页表的索引, 低12位则做为页内偏移量来用.
当前任务页目录的物理地址在处理器的CR3寄存器中, 假设它的内容为0x00005000. 段管理部件输出的线性地址是0x00801050, 其二进制的形式如图中给出. 高10位是十六进制的0x002, 它是页目录表内的索引, 处理器将它乘以4(由于每一个目录项4字节), 做为偏移量访问页目录. 最终处理器从物理地址00005008处取得页表的物理地址0x08001000.
线性地址的中间10位为0x001, 处理器用它做为页表索引取得页的物理地址. 将该值乘以4, 做为偏移量访问页表. 最终, 处理器又从物理地址08001004处取得页的物理地址, 这就是咱们一直努力寻找的那个页.
页的物理地址是0x0000c000, 而线性地址的低12位是数据所在的业内偏移量. 故处理器将它们相加, 获得物理地址0x0000C050, 这就是线性地址0x00801050所对应的物理地址, 要访问的数据就在这里.
注意, 这种变换不是平白无故的, 而是事先安排好的. 当任务加载时, 操做系统先建立虚拟的段, 并根据段地址的高20位决定它要用到哪些页目录项和页表项. 而后, 寻找空闲的也, 将本来应该写入段中的数据写到一个或者多个页中, 并将页的物理地址填写到相对应的页表项中. 只有这样作了, 当程序运行的时候, 才能以相反的顺序进行地址变换, 并找到正确的数据.
页目录和页表中分别存放为页目录项和页表项, 它们的格式以下:
能够看出, 在页目录和页表中, 只保存了页表或者页物理地址的高20位. 缘由很简单, 页表或者页的物理地址, 都要求必须是4KB对齐的, 以便于放在一个页内, 故其低12位全是0. 在这种状况下, 能够只关心其高20位, 低12位安排其余用途.
控制寄存器CR3, 也就是页目录表基地址寄存器PDBR, 该寄存器如上图所示.
因为页目录表必须位于一个天然页内(4KB对齐), 故其物理地址的低12位是全0. 低12位除了PCD和PWT外, 都没有使用. 这两位用于控制页目录的高速缓存特性, 参见上面解释.
控制寄存器CR0的最高位PG位, 用于开启分页或者关闭页功能. 当该位清0时, 页功能关闭, 从段部件来的线性地址就是物理地址. 当它置位时, 页功能开启. 只能在保护模式下才能开启分页功能, 当PE位清0时(实模式), 设置PG位将致使处理器产生一个异常中断.