标签:
本文为<x86汇编语言:从实模式到保护模式> 第16章笔记
因为段的长度不定, 在分配内存时, 可能会发生内存中的空闲区域小于要加载的段, 或者空闲区域远远大于要加载的段. 在前一种情况下, 需要另外寻找合适的空闲区域; 在后一种情况下, 分配会成功, 但太过于浪费. 为了解决这个问题, 从80386处理器开始, 引入了分页机制. 分页功能从总体上来说, 是用长度固定的页来代替长度不一定的段, 藉此解决因段长度不同而带来的内存空间管理问题. 尽管操作系统也可以用软件来实施固定长度的内存分配, 但太过于复杂, 由处理器固件来做这件事, 可以使速度和效率最大化.
处理器中有负责分段管理的段部件, 每个程序或任务都有自己的段, 这些段都用段描述符定义. 随着程序的执行, 当要访问内存, 就用段地址加上偏移量, 段部件就会输出一个线性地址. 在单纯的分段模式下, 线性地址就是物理地址.
一旦决定采用页式内存管理, 就应当把4GB内存分成大小相同的页. 页的最小单位是4KB, 也就是4096字节, 用十六进制表示就是0x1000. 因此, 第一个页的物理地址就是0x00000000, 第2个页的物理地址是0x00001000, 第3个页的物理地址是0x00002000....最后一个页的物理地址是0xfffff000. 这样, 4GB内存划分为1048576(0x100000)个页. 很显然, 页的物理地址, 其低12位始终为0.
段管理机制对于Intel处理器来说是最基本的, 任何时候都无法关闭. 也就是说, 即使启用页管理功能, 分段机制依然是起作用的, 段部件依然工作.
如上图所示, 内存的分配设计段空间的分配和页分配. 左边是虚幻的, 或者说虚拟的4GB内存空间, 称为虚拟内存; 右边是实实在在的内存, 被分成1048576个4KB的页面(每个方框4KB, 灰色代表已分配).
在分页模式下, 操作系统可以创建一个为所有任务公用的4GB虚拟内存空间, 也可以为每一个任务创建独立的4GB虚拟内存空间, 这都是可行的. 当一个程序加载时, 操作系统既要在左边的虚拟内存中分配段空间, 又要在右边的物理内存中分配相应的页面. 因此, 第一步骤是寻找空闲的段空间, 该段空间既没有被其他程序使用, 也没有被同一程序内的其他段使用. 比如上图, 假设已经成功找到并分配了一个段空间, 基地址为0x00200000, 长度为8200字节.
页的最小尺寸是4KB, 也就是4096字节, 因此, 8200字节的段, 需要占用3个页面, 其中最后一个页面只用了8个字节, 其余都是浪费着, 但这无关紧要, 如果允许页共享, 多个段或多个程序可以用同一个页来存放各自的数据. 在分段之后, 操作系统的任务就是把段拆开, 并分别映射到物理页. 注意, 段必须是连续的, 但不要求所分配的页都是连续的, 挨在一起的.
就上图中的列子来说, 该段有8200字节, 需要分配3个页面. 操作系统在物理内存中搜索可用的空闲页, 接下来, 要建立线性地址和也之间的对应关系, 在图中, 0x200000~0x00200FFF对应着物理地址为0x00002000的页, 0x00201000~0x00201FFF对应着0x00004000, 0x00202000~0x00202007对应着0x00007000的页, 当然, 这里只是示例, 线性地址区间和页的对应关系可以随意.
4GB虚拟内存空间不可能用来保存任何数据, 因为它是虚拟的, 它只是用来指示内存的使用情况. 当操作系统加载一个程序并创建为任务时, 操作系统在虚拟内存空间寻找空闲的段, 并映射到空闲的页, 然后, 到真正开始加载程序时, 再把原本属于段的数据按页的尺寸拆开, 分开写入对应的页中.
从段部件输出的是线性地址, 或者叫做虚拟地址. 为了根据线性地址找到页的物理地址, 操作系统必须维护一张表, 把线性地址转换成物理地址, 这是一个反过程.
如上图所示, 因为有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位将导致处理器产生一个异常中断.
标签:
原文地址:http://www.cnblogs.com/Acg-Check/p/4269488.html