Linux 0.12 内核管理存储器
其分段,用分段的机制把进程间的虚拟地址分隔开。
每一个进程都有一张段表LDT。整个系统有一张GDT表。且整个系统仅仅有一个总页表。
其地址翻译过程为:
程序中给出的32位地址(实际上被看做段内偏移地址),再依据代码段寄存器CS中的16位段选择子,可在GDT或LDT中查找对应的段描写叙述符。从段描写叙述符中提取段的基地址,与程序给出的32位地址相加。得到结果为线性地址。
依据此线性地址查找系统页文件夹表,再查二级或是多级页表,终于得到物理地址。
此方式系统仅仅有一个4G的线性地址空间由各进程共享(各个进程共享一套页表)。(32位系统一个虚拟段的最大长度,理论上为4G)
Linux 0.12内核人工定义的最大任务数为64个。每一个任务的逻辑地址范围是64MB,故所有任务所使用的线性地址空间范围是64MB*64=4GB。
(即限定一张LDT表全部项的管辖范围是64MB。理论上其一项的管辖范围就是4GB)
(8086为了能寻址1M的空间,设计了称为段(Segment)的寻址技术。因为后来CPU的发展,这样的寻址技术实际上是不必要的,但为了保持兼容。80x86也用了段寻址技术。)
段地址部分使用16位的段选择子指定,当中14位能够选择。即16384个段。
(在保护模式下。段寄存器中的值为段表中的段选择子。在实模式下。段寄存器中的值为段表基址)
段内偏移地址使用32位的值来指定,因此段内地址能够是0~4GB。
即一个段的最大长度可达4G。
程序中由16位的段和32位的偏移构成的48位地址或长指针称为一个逻辑地址(虚拟地址)
80x86为段部分提供了6个存放段选择子的段寄存器:CS、DS、ES、SS、FS和GS。
当中。CS总是用于寻址代码段。而堆栈段则专门使用SS段寄存器。
由CS寻址的段称为当前代码段。此时EIP寄存器中包括了当前代码段内下一条要运行指令的段内偏移地址。因此要运行指令的地址可表示成CS:[EIP]。
由SS寻址的段称为当前堆栈段。栈顶由ESP寄存器内容指定。
因此堆栈顶处地址是SS:[ESP]。
另外4个段寄存器是通用段寄存器。当指令中没有指定所操作数据的段时。那么DS将是默认的数据段寄存器。
CPU的内存管理给程序猿提供了这样一个抽象的内存模型:
即程序猿(不管是汇编的还是高级语言的)能够把内存分布看做是如上图所看到的,能够觉得内存中仅仅有自己的程序,自己独占CPU。
这是硬件和操作系统一起提供给程序猿的简单抽象。
(底层的实现:地址变换、任务切换等对程序猿是透明的)
程序(不管是汇编的还是高级语言的)中的地址是由两部分构成的逻辑地址。
这样的逻辑地址并不能直接用于訪问物理内存,而须要使用地址变换机制将它变换或映射到物理内存地址上。
内存管理机制即用于将这样的逻辑地址转换成物理内存地址。
80x86在从逻辑地址到物理地址变换过程中使用了分段和分页两种机制。
第一阶段使用分段机制,把程序的逻辑地址变换成处理器可寻址的内存空间(称为线性地址空间)
第二阶段使用分页机制。把线性地址转换为物理地址。
第一阶段的分段变换总是使用的。而第二阶段的分页机制则是供选用的。若没有启用分页机制,那么分段机制产生的线性地址空间就直接映射到处理器的物理地址空间上。
物理地址空间定义为处理器在其地址总线上可以产生的地址范围。
(主板提供的统一编址)
分段隔绝了各个代码、数据和堆栈区域的机制。为了定位指定段中的一个字节,程序必须提供一个逻辑地址。逻辑地址包含一个段选择子和一个偏移量。
(程序提供48位的逻辑地址,但指针长度仅仅有32位。在程序载入时,就已经把程序中各段的段选择子载入到了对应的段寄存器中了,程序中所用的指针值是32位的段偏移地址)
段选择子是一个段的唯一标识。
另外,段选择子提供了段表一个段表项的偏移量。一个段表项中含有:段大小、訪问权限、段类型及段的基地址信息。逻辑地址中的偏移量加上段基地址就形成了处理器线性地址空间中的地址。
线性地址空间与物理地址空间具有同样的结构。相对于二维的逻辑地址空间来说,它们都是一维地址空间。
虚拟地址(逻辑地址)空间可包括最多16K个段,而每一个段最长可达4GB,使得虚拟地址空间达到64TB。
线性地址空间和物理地址空间都是4GB。
实际上,假设禁用分页机制,那么线性地址空间就是物理地址空间。
段描写叙述符表(段表)是段描写叙述符的一个数组。段表的长度可变。最多能够包括8192个(2^13)8字节描写叙述符。
有两种段描写叙述符表:
全局描写叙述符表GDT(Global Descriptor Table)和局部描写叙述符表LDT(LocalDescriptor Table)
段表存储在由操作系统维护着的受保护的内存区域中,而且由CPU的内存管理硬件(MMU)来引用。(用于地址翻译)
虚拟地址空间被切割成大小相等的两半。整个虚拟地址空间共含有2^14个段:一半空间(即2^13个段)是由GDT映射的全局虚拟地址空间,还有一半是由LDT映射的局部虚拟地址空间。
LDT段表中的段是一个任务自己的段(代码段、数据段等)
GDT段表中的段是系统中全部任务共同拥有的(操作系统代码段等),还有系统中全部任务的LDT段表段(任务的LDT段表存储在一个段中)
当某任务在执行时,可訪问的段包含自己LDT段表中的段和GDT中操作系统的段。它们组成了此任务的虚拟地址空间。
这样,通过让每一个任务使用不同的LDT,当任务A在执行时,任务B的段不是虚拟地址空间的部分。因此任务A没有办法訪问任务B的内存。
GDT本身并非一个段,而是线性地址空间中的一个数据结构(故其不须要经过段机制地址翻译,GDTR中不须要段选择子)。
GDT的基线性地址和长度值必须载入进GDTR寄存器中。
LDT表存放在LDT类型的系统段中。此时GDT必须含有LDT的段描写叙述符。
【内存管理寄存器】
处理器提供了4个内存管理寄存器(GDTR、LDTR、IDTR、TR)。用于指定内存分段管理所用系统表的基地址。(处理器为这些寄存器的载入和保存提供了特定的指令)
1、全局描写叙述符表寄存器GDTR
为了记录一个段,须要有下面信息:段的大小、段的基地址、段的属性。
CPU用8个字节(64位)的数据来表示这些信息。
故段表最大为8192*8=65536字节(64KB)。
GDT(global (segment) descriptor table)即全局段号记录表。此段表的起始地址和表长度放在被称为GDTR的特殊寄存器中了。
在机器刚加电或处理器复位后,基地址被默认地设置为0。而长度值被设置成0xFFFF。
在保护模式初始化过程中,必须给GDTR载入一个新值。
2、局部描写叙述符表寄存器LDTR
LDT表是当前进程的段的段表。
包括LDT表的段必须在GDT表中有一个段描写叙述符项。
当进行任务切换时,处理器会把新任务LDT的段选择子和段描写叙述符自己主动地载入进LDTR中。
3、中断描写叙述符表寄存器IDTR
与GDTR的作用类似,IDTR寄存器用于存放中断记录表IDT的32位线性基地址和16位表长度值。
相同。在机器刚加电或处理器复位后,基地址被默认地设置为0,而长度值被设置成0xFFFF。在保护模式初始化过程中。必须给IDTR载入一个新值。
4、任务寄存器TR
TR用于寻址一个特殊的任务状态段(Task State Segment,TSS)。TSS中包括着当前运行任务的重要信息。TR寄存器中的段选择子用于索引GDT表中的TSS类型的段。
当运行任务切换时,CPU会把新任务的TSS的段选择子和段描写叙述符自己主动载入进任务寄存器TR中。
(LDT和TSS都存储在段中,且这些段记录在GDT段表中,故LDTR寄存器和TR寄存器仅仅有16位,用于保存段选择子)
段选择子(或称段选择符)是段的一个16位标识符(可理解为段号)。段选择子并不直接指向段。而是指向段描写叙述符表中定义段的段描写叙述符。
请求特权级字段RPL提供了段保护信息。
表索引字段TI用来指出包括指定的是哪个段表。TI=0表示此为GDT表的段选择子,TI=1表示此为LDT表的段选择子。
相应用程序来说段选择子是作为指针变量的一部分而可见的。但选择子的值一般是由链接器或载入器进行设置或改动,而非应用程序。
【段寄存器】
处理器提供6个存放段选择子的寄存器(即段寄存器)。
每一个段寄存器支持特定类型的内存引用(代码、数据或堆栈)。
运行每一个程序都须要至少把有效的段选择子载入到代码段(CS)、数据段(DS)和堆栈段(SS)寄存器中。
处理器还另外提供3个辅助的数据段寄存器(ES、FS、GS),以便当前运行程序可以訪问其它几个数据段。
(各段寄存器有不同的作用。用于訪问不同的段。各司其职。
)
●在32位模式下。段寄存器仍然是16位。
且因为CPU设计上的原因,段寄存器的低3位不能使用。因此可以使用的段号仅仅有13位。即最大段号为8191。
对于訪问某个段的程序,必须已经把段选择子载入到一个段寄存器中。
(汇编程序是分段编写的,在其执行前各段的选择子就已经被默默地载入到了段寄存器中了,进行长跳转指令,从一个段跳转到还有一个段中。这个跳转指令会把新段的选择子载入到寄存器中)
每一个段寄存器都有一个“可见”部分和一个“隐藏”部分。隐藏部分用于段描写叙述符缓存(页表的缓存是TLB)
当一个段选择子被载入到一个段寄存器可见部分中时,CPU也同一时候把段选择子指向的段描写叙述符载入到段寄存器的隐藏部分中。
缓存在段寄存器(可见部分和隐藏部分)中的信息使得CPU能够在进行地址转换时不再须要花费时间从段描写叙述符中读取基地址和限长值。
段描写叙述符是GDT和LDT表中的一个数据结构项,用于向CPU提供有关一个段的位置和大小信息以及訪问控制的状态信息。
每一个段描写叙述符的长度是8字节,含有:段基地址、段限长、段属性(段类型、訪问控制、特权级等)
分段机制把逻辑地址转换成线性地址。而分页则把线性地址转换成物理地址。
与分段机制不同。分页机制对固定大小的内存块(页面)进行操作。分页机制把线性和物理地址空间都划分成页面。
线性地址空间中的不论什么页面能够被映射到物理地址空间的不论什么页面上。
80x86使用4K(2^12)字节固定大小的页面。
因此。线性地址的低12为页内偏移量(段内偏移量为32位)。直接作为物理地址的低12位。
分页机制可看做就是把线性地址的高20位页号 转换到相应物理地址的高20位。
页表(page table)可看做简单的2^20个物理地址数组,线性地址的高20位(可看做虚拟页号)构成这个数组的索引值,用于选择相应页面的物理基址。
线性地址的低12位给出了页面中的偏移量,加上页面的基地址终于形成相应的物理地址。
页表中每一个页表项的大小为32位。因为仅仅须要当中的20位来存放页面的物理基址,因此剩下的12位可用于存放页面属性信息。
(假设页表项中信息表明页不存在,那么当訪问相应物理页面时就会产生一个异常)
(段表项大小为64位,当中仅仅有32位为段基地址。其它为段属性)
1、两级页表结构
页表中含有2^20(1M)个表项。而每项占用4字节。假设作为一个表来存放的话,它们最多将占用4M内存。
为了降低内存占用量,80x86使用了两级页表。
由此。高20位线性地址到物理地址的转换也被分成两步进行。每步转换当中的10bit。
第一级表称为页文件夹(pagedirectory)。具有2^10(1K)个4字节的内存。
这些表项指向相应的二级表。
第二级表称为页表(pagetable)。
最多含有1K个4B的表项。二级页表使用线性地址中间10位作为表项索引值,以获取含有页面的20位物理基地址的表项。
这样,一个文件夹项就“管辖”1024个页。
2、不存在的页表
二级页表结构同意页表被分散在内存各个页面中,而不须要保存在连续的4MB内存块中。(由于每个二级页表都是4K大小,正好放在一个页中)
且。并不须要为不存在的或线性地址空间未使用部分分配二级页表。(一级页表把4G线性空间中切割的全部页都记录了)
文件夹表项中每一个表项有一个存在属性,可用于在虚拟内存中存放二级页表。这意味着仅仅有部分二级页表须要存放在主存中,其余的能够保存在磁盘上。
文件夹项和页表项的格式,例如以下图。当中位31~12含有物理地址的高20位。用于定位物理地址空间中一个页面的物理基地址。表项的低12位含有页属性信息。
P位:存在标志。
P=1表示有效,P=0表示无效。假设P=0。那么其余位可供程序自由使用,比如操作系统能够使用这些位来保存已存储在磁盘上的页面的序号。
R/W位:读/写标志
U/S位:用户/超级用户标志
A位:已訪问(Accessed)标志。
当CPU訪问页表项映射的页面时,页表表项的此标志被置1。
CPU仅仅负责设置该标志,操作系统可通过定期地复位该标志来统计页面的使用情况。
D位:已被改动(Dirty)标志。
当CPU对一个页面运行写操作时,就会置此位。
在保护模式中,80x86同意线性地址空间直接映射到大容量的主存中,或者间接地映射到小容量的物理内存和磁盘中。此方法被称为虚拟存储。
文件夹项和页表项中的存在标志P为使用分页技术的虚拟存储提供了必要的支持。
页面不在物理内存中的表项其标志P=0。
假设程序中訪问主存中不存在的页面。CPU就会产生一个缺页异常。此异常中断处理程序让操作系统把对应页面载入到主存中。并设置P=1。当页面载入到主存中之后,从异常处理过程的返回操作会使得导致异常的指令被又一次运行。