最近一直在接着操作系统的课程,着手跟着os.dev上的大神的文档写一个小型的内核,然后前期的东西自己一直在看也没时间写Blog,最近做到了内存管理这里,看了Jamesmolloy的文档还有一些os.dev.org上的关于memorymanage的东西,总觉的还是写点东西吧,虽是赘述,但对自己而言定是有用的。
我不想说太多关于早期8086分段的原因,只是简单说一下这种概念。我们知道x86架构下的分段+分页机制共同实现了我们OS的内存管理逻辑,当然现在很多除x86架构的CPU已经完全不需要分段(段地址+偏移地址)这种机制,因为分页完全可以满足我们对于内存的映射管理与寻址。Intel只是因为早期8086—80286时期的问题,他需要历史保证兼容,所以一直保留着分段的机制。关于分段这块,我们更多关心的是段寄存器地址,Intel在32位windows程序在内存中的布局采用了所谓的平坦内存模式,这种模式虽然只有一个段,却同时包了含代码和数据。早期的16位程序,由不同的段组合而成,且每个段的地址重定位有64K的限制。386后的平坦内存模式下,程序无需进行地址重定位,内存访问范围达到4G宽度。其优点是,汇编程序更容易编写,且代码执行速度更快。在32位程序中,所有的段寄存器依然存在,但是都被设置成了同一个值,所以段寄存器和地址重定位已经无须使用了。这简化了我们很多的代码逻辑。
虽然80386有的通用寄存器(EAX,EDI等等)被扩充倒了32位,但是其中的段寄存器(DS,ES等)仍然只有16位,显然不可能再用16位的段寄存器直接存放4G空间需要的32位地址了,所以必须引入了一种间接办法——将段寄存器中存放的地址换成一个索引指针,寻址时不再是从段寄存器中去寻址,而是先取指针,再通过该指针搜索一个系统维护的“查找表”读出所需段的具体信息。剩下的动作和传统行为没什么区别,将刚刚取得的段的基地址加上偏移量便构成了一个32位地址(即,线性地址)。
我们知道在多用户多任务环境下,内存寻地工作不再是简单地取得32位的内存地址就可以直接不假思索地放到地址总线上去读写内存了,此刻必须先要对需访问的地址进行合法性检查,看看访问者是不是有权利去访问它要求的地址。如果发现有非法访问企图,则立刻阻止(CPU会产生一个内部异常的中断)。系统设计师们便把属性信息、段的基地址和界限都糅合在一起,形成了一个新的信息单元——段描述符号,它整整占用了8个字节。显然,寄存器太小,不够存放段描述符,所以段描述符都被统一存在专门的系统段描述符号表中(GTD或LDT)保存。其中包含了段基地址、段的大小信息、段的属性信息,而且在属性信息里还包含了和访问权限有关的信息。最主要的描述符表是全局描述符表(GlobalDescriptor Table,GDT),所谓全局,意味着该表是为整个软硬件系统服务的。在进入保护模式前,必须要定义全局描述符表。盗用一张图(GDT和GDTR)如下:
GDT中23位G是粒度(Granularity)位,用于解释段界限的含义。当G位是“0”时,段界限以字节为单位。此时,段的扩展范围是从1字节到1兆字节(1B~1MB),因为描述符中的界限值是20位的。相反,如果该位是“1”,那么,段界限是以4KB为单位的。这样,段的扩展范围是从4KB到4GB。S位用于指定描述符的类型(DescriptorType)。当该位是“0”时,表示是一个系统段;为“1”时,表示是一个代码段或者数据段(堆栈段也是特殊的数据段)。DPL表示描述符的特权级(DescriptorPrivilege Level,DPL)。这两位用于指定段的特权级。共有4种处理器支持的特权级别,分别是0、1、2、3,其中0是最高特权级别,3是最低特权级别。刚进入保护模式时执行的代码具有最高特权级0(可以看成是从处理器那里继承来的),这些代码通常都是操作系统代码,因此它的特权级别最高。每当操作系统加载一个用户程序时,它通常都会指定一个稍低的特权级,比如3特权级。不同特权级别的程序是互相隔离的,其互访是严格限制的,而且有些处理器指令(特权指令)只能由0特权级的程序来执行,为的就是安全。
为了跟踪全局描述符表,处理器内部有一个48位的寄存器,称为全局描述符表寄存器(GDTR,下图所示),该寄存器分为两部分,分别是32位的线性地址和16位的边界。32位的处理器具有32根地址线,可以访问的地址范围是0x00000000到0xFFFFFFFF,即4GB内存。所以,GDTR的32位线性基地址部分保存的是全局描述符表在内存中的起始线性地址,16位边界部分保存的是全局描述符表的边界(界限),其在数值上等于表的大小(总字节数)减一。
描述符中指定了32位的段起始地址,以及20位的段边界。在实模式下,段地址并非真实的物理地址,在计算物理地址时,还要左移4位(乘以16)。和实模式不同,在32位保护模式下,段地址是32位的线性地址,如果未开启分页功能,该线性地址就是物理地址。在进入保护模式之后,处理器立即要按新的内存访问模式工作,所以,必须在进入保护模式之前定义GDT。但是,由于在实模式下只能访问1MB的内存,故GDT通常都定义在1MB以下的内存范围中。
ok,GDT就说到这,下面我们开始启动内存分页的东西。其实也是我们所谓的实模式和保护模式的分界。在此之前,我们必须知道CPU的一些控制寄存器。CR0,CR1,CR2,CR3,CR4。。。。。,CR0是32位的寄存器,包含了一系列用于控制处理器操作模式和运行状态的标志位。所示,它的第1位(位0)是保护模式允许位(ProtectionEnable,PE,是开启保护模式大门的门把手,如果把该位置“1”,则处理器进入保护模式,按保护模式的规则开始运行。32位处理器下的6个段寄存器分为两部分,前16位和8086相同,在实模式下,它们用于按传统的方式寻址1MB内存,使用方法也没有变化。在保护模式下访问一个段时,传送到段选择器的是段选择子(上图)。它由三部分组成,第一部分是描述符的索引号,用来在描述符表中选择一个段描述符。TI是描述符表指示器 (TableIndicator),TI=0时,表示描述符在GDT中;TI=1时,描述符在LDT中。LDT的知识将在后面进行介绍,它也是一个描述符表,和GDT类似。上图中的RPL是请求特权级,表示给出当前选择子的那个程序的特权级别,正是该程序要求访问这个内存段。对于GDT的一些说明就到这里,其实也都是文档里的东西加上search资料,(都是固定标准的东西)。然后我们开始说Paging机制。
说分页机制,必须了解什么是虚拟内存,我们知道程序代码和数据必须驻留在内存中才能得以运行,然而系统内存数量很有限,往往不能容纳一个完整程序的所有代码和数据,更何况在多任务系统中,可能需要同时打开子处理程序,画图程序,浏览器等很多任务,想让内存驻留所有这些程序显然不太可能。因此我们首先能想到的就是将程序分割成小份,只让当前系统运行它所有需要的那部分留在内存,其它部分都留在硬盘。当系统处理完当前任务片段后,再从外存中调入下一个待运行的任务片段。的确,老式系统就是这样处理大任务的,而且这个工作是由程序员自行完成。但是随着程序语言越来越高级,程序员对系统体系的依赖程度降低了,很少有程序员能非常清楚的驾驭系统体系,因此放手让程序员负责将程序片段化和按需调入轻则降低效率,重则使得机器崩溃;再一个原因是随着程序越来越丰富,程序的行为几乎无法准确预测,程序员自己都很难判断下一步需要载入哪段程序。因此很难再靠预见性来静态分配固定大小的内存,然后再机械地轮换程序片进入内存执行。so,start虚拟内存。
然后问题来了:虚拟地址映射到物理地址上的呢?内存又如何能不断换入换出虚拟地址呢?用段机制?我们知道逻辑地址通过段机制后变为一个32位的地址,足以覆盖4G的内存空间,而系统内存一般也就几百M吧,所以当程序需要的虚拟地址不在内存时,只依靠段机制很难进行虚拟空间地换入换出,因为不大方便把整段大小的虚拟空间在内存和硬盘之间调来调去(直接跪了吧)。
通过段机制转换得到的地址仅仅是作为一个中间地址——线性地址了,该地址不代表实际物理地址,而是代表整个进程的虚拟空间地址。在线性地址的基础上,页机制接着会处理线性地址映射:当需要的线性地址(虚拟空间地址)不在内存时,便以页为单位从磁盘中调入需要的虚拟内存;当内存不够时,又会以页为单位把内存中虚拟空间的换出到磁盘上。使用页机制,4G空间被分成2的20次方个4K大小的页面(页面大小其实有多种选择,只是4KB是工程师的经验得来),因此定位页面需要的索引表(页表)中每个索引项至少需要20位,但是在页表项中往往还需要附加一些页属性,所以页表项实际为32位,其中12位用来存放诸如“页是否存在于内存”或“页的权限”等信息。
上面所说的2的20次方个4K大小的页面,就是1MB个也,4GB的空间需要映射的页存储空间也就是4K*1MB,但相对而言这只能算是一个虚拟地址空间的映射,我们知道每个进程都需要自己的映射,如果有100个进程,那就是400MB的映射存储空间,Areyou Kidding me ?当然不现实了,所以产生了所谓的分级页表,即页目录和页表的概念,32位的地址来说,分为3段来寻址,分别是地址的低12位,中间10位和高10位。高10位表示当前地址项在页目录中的偏移,最终偏移处指向对应的页表,中间10位是当前地址在该页表中的偏移,我们按照这个偏移就能查出来最终指向的物理页了,最低的12位表示当前地址在该物理页中的偏移。就这样,我们就实现了分级页表。
给张图:
32位下一个进程实际使用到的内存不会有4GB这么大,所以通过两级页表的映射,我们就可以完成映射。
上面说过CPU的控制寄存器对吧,我们这里的页目录生成后就是需要送到CR3寄存器中,因为页目录为我们提供了所有进程页表的映射关系(虚拟地址->物理地址),电路中的MMU逻辑单元的寻址就是从CR3寄存器的页目录数据开始管理的。之后我们具体作映射、内存分配时需要页异常(page_fault),如果产生处理器会把引起异常的线性地址存放在CR2中。操作系统中的页异常处理程序可以通过检查CR2的内容来确定线性地址空间中哪一个页面引发了异常。,所以OS的设计远远离不开硬件的支持。
就先说道这里。唉,东西太多了,IT不归路啊!!!