寄存器: 临时存储数据地址、状态标志,很小、速度很快的数据存储单元,一般几个字节,通常位于中央处理器(CPU)内部。寄存器不是CPU特有的概念,除了CPU,其他一些芯片也有寄存器,比如中断控制芯片等。
寻址:即CPU如何通过一个地址找到内存的数据
寄存器、高速缓存、内存、外部存储(硬盘):存储容量不断增大,而访问速度不断减小。寄存器、高速缓存存在CPU内部访问起来速度很快
内存:需要走总线系统,再加上寻址和读取数据的时间速度会比寄存器和缓存慢的多,是一大片的存储地址,需要统一的编址后通过地址进行读写。
硬盘:大容量寻址、总线传输的限制
x86架构CPU寄存器(16位)
通用寄存器:AX、BX、CX、DX、SI、DI、BP、SP。虽然这些都叫通用寄存器但不是真正意义的通用,例如SP寄存器用来指向堆栈的最顶部;SI、DI:常用来间接寻址的时候用来存放源和目的地址
段寄存器:CS、DS、ES、SS 实模式、保护模式两种方法
指令寄存器:IP 程序下一条将要执行命令的地址,引导CPU的执行流程
标志寄存器:FLAGS 存放程序运行过程中的一些标志和状态信息
实模式下的寻址
内存分段:段基地址+段内偏移的方式,将1MB的地址划分为很多小片段,叫做一个个的段,每个段最大64kB。让段寄存器起始存储的地址叫做段的基地址,其他寄存器比如让SP、IP寄存器存放的相对于段开始地址的偏移,在寻址过程中,需要结合段的基地址加上偏移地址得到完整的地址
8086中一共有四个段寄存器分别是:指向代码段的CS、指向数据段的DS、指向堆栈段的ES、指向附加段的SS
保护模式下的分段式内存管理机制(32位)
不直接将段基地址存放到段寄存器中,而是在内存中建立一个表格将段的基地址放在这个表格里,段寄存器中存放着指向着个段的行号(段索引)即可,CPU再单独设立一个x寄存器指向这个表格。
寻址的时候通过x寄存器找到这个表格,通过段寄存器里面的行号,就可以取出这个段的基地址了。大家是否疑惑权限问题也没有解决?
可以在表格中增加一些字段,用来描述这些段的基本信息,这里面就可以有权限信息、段的大小信息等等。这个表格的信息不在是一个基地址还有很多丰富信息,这每一行叫做一个段描述符。所有段描述符构成的表叫做段描述符表。段描述符用两个比特位来存储CPU特权级别,用来表示这个段是什么样的特权级别(DPL),同时段寄存器也留了两个比特位来存储当前特权级别(CPL)
寻址的时候CPU检查段寄存器中的当前权限CPL,是否够资格访问目标段的DPL,从而完成权限的管理。那些段有那些权限,操作系统在启动时已经安排好了。为了进一步丰富能力,英特尔搞了两个段描述符表,分别时全局描述符表GDT、本地描述符表LDT,再在段寄存器中留了一位用来表示使用哪个表。这样段寄存器被划分为三部分,剩下真正用来表示行号只有13位,英特尔专门设立两个寄存器指向段描述符表分别是GDTR、LDTR
完整的过程是寻址的时候,通过段寄存器的表指示位来确定是使用GDT还是LDT,通过GDTR或LDTR找到段描述符表,根据段描述符表中的行号定位到段描述符,接着比较段寄存器中的CPL和目标段描述符的DPL进行权限校验,如果通过允许本次访问否则抛出异常,上面这些过程都是CPU在硬件电路层自动完成的,对操作系统和应用程序都是透明的。分段式内存管理机制解决了权限问题,并没有解决多进程内存空间隔离问题问题。
x86架构CPU权限级别
Ring0、Ring1、Ring2、Ring3(0环到3环)英特尔设想中让操作系统运行到0环,驱动程序运行在1、2环,应用程序运行在3环。外环的程序代码无法访问内环的内存空间。而Linux、Window都将操作系统内核、驱动程序运行在0环,应用程序运行在3环,所以很少听见1环、2环
保护模式下的分页式内存管理机制
举例:操作系统告诉每个进程都有4GB的独立内存地址空间,彼此之间完全隔离,同一地址在不同进程里对应的是完全不同的内存单元。将内存容量按照一定大小分成一片一片的内存页面,同时将4GB的进程地址空间也按照同样大小划分成一片片的页面。操作系统在创建进程中,只需要分配一些必须的页面给进程,并建立起进程的内存页面与物理内存页面的映射关系,后续用到新的内存页面的时候再向操作系统申请分配,在实际情况中这个页面大约4KB。操作系统掌管所有的地址空间的页面和物理内存页面的映射关系,并负责为所有进程分配和回收页面。进程看到的内存地址空间不再是真实的内存,进程使用的内存地址变成虚拟地址。
随着进程的越来越多真实的物理内存总有用完的时候,那怎么办?操作系统在硬盘中建立一个大文件叫做分页文件,分配的时候发现内存不够用,就选择将很久不怎么访问的内存页面临时保存到硬盘上的分页文件,并修改映射关系,就可以把内存页面腾出空间用来了。之后如果碰到这个内存页面的访问时,触发异常再从硬盘文件中加载会来。这样所用内存的进程加起来实际上比真正的物理地址多了,有一段并不是真正的内存,所以虚拟内存就是这么来的。
寻址:最简单的办法是建立一个表格,表格的第一列是内存页面的虚拟地址(这是页面的起始地址),表格第二列是翻译后真实的页面的起始地址,但是这样存储开销太大。虽然一个进程拥有4GB的内存空间,但是用的内存页面很少,也就是表格第二列字段都是空的,没有实际页面跟它做映射。这样我们可以将大表拆成一个个的小表,如果小表内每项即页面全部没有映射关系,那这个表没有存在的意义,实际是很多都是大片大片的空白,所以有很多小表没有必要存在,这样就可以节省很大的存储开销。
那小表不存在翻译的时候怎么办?单独弄一张表将小表的地址记录起来,暂且将这张表叫根表,根表记录着每个小表,小表中记录着页面映射情况。翻译的时候发现对应小表不存在。那知道这个页面没有被映射。这样只需要存储一张根表以及真正有映射关系的小表。小表叫做页表,根表叫做页目录,然后单独设立一个寄存器GR3来指向当前进程的页目录所在的内存地址。
X86将把32位的虚拟地址划分了三段分别是10位、10位、12位,前十位用来定位页目录的项取出页表的地址,中间的十位用来定位到对应的内存页面地址,最后十二位对应在内存页面的偏移。
CPU完成一次内存寻址需要几次内存访问?抛开最后的内存读写,为了定位虚拟内存对应的真实地址,需要完成查页目录、查页表着两次额外两次内存读取,每次访问都需要这样非常浪费时间和性能,所以CPU内部提供一段缓存(PLB:转换后备缓存区),用来把翻译过的内存地址存储起来,后面翻译的时候先来这查询。
那真实的操作系统是如何管理内存?结合分页式内存管理机制和分段式内存管理机制。以分页式内存管理为主要方式,同时又使用段的权限管理机制。简单来说,就是将应用程序的代码段、数据段、堆栈段几个主要段的基地址都设置为零,这样三个段都重叠在一起,计算地址的时候,不用再考虑段的基地址,直接段内偏移即可,构成一个平坦的地址空间