Linux 0.12 内核对内存的管理

着重点在于分段,用分段的机制把进程间的虚拟地址分隔开。

每个进程都有一张段表LDT,整个系统有一张GDT表,且整个系统只有一个总页表。

Linux 0.12 内核对内存的管理_第1张图片

 

地址翻译过程为:

程序中给出的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的内存管理给程序员提供了这样一个抽象的内存模型:

Linux 0.12 内核对内存的管理_第2张图片

即程序员(无论是汇编的还是高级语言的)可以把内存分布看做是如上图所示,可以认为内存中只有自己的程序,自己独占CPU。

这是硬件和操作系统一起提供给程序员的简单抽象。

(底层的实现:地址变换、任务切换等对程序员是透明的)

 

程序(无论是汇编的还是高级语言的)中的地址是由两部分构成的逻辑地址。这种逻辑地址并不能直接用于访问物理内存,而需要使用地址变换机制将它变换或映射到物理内存地址上。内存管理机制即用于将这种逻辑地址转换成物理内存地址。

 

80x86在从逻辑地址到物理地址变换过程中使用了分段和分页两种机制。

第一阶段使用分段机制,把程序的逻辑地址变换成处理器可寻址的内存空间(称为线性地址空间)

第二阶段使用分页机制,把线性地址转换为物理地址

第一阶段的分段变换总是使用的,而第二阶段的分页机制则是供选用的。若没有启用分页机制,那么分段机制产生的线性地址空间就直接映射到处理器的物理地址空间上。


Linux 0.12 内核对内存的管理_第3张图片

物理地址空间定义为处理器在其地址总线上能够产生的地址范围。(主板提供的统一编址)


分段机制

分段隔绝了各个代码、数据和堆栈区域的机制。为了定位指定段中的一个字节,程序必须提供一个逻辑地址。逻辑地址包括一个段选择子和一个偏移量。

(程序提供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个内存管理寄存器(GDTRLDTRIDTRTR),用于指定内存分段管理所用系统表的基地址。(处理器为这些寄存器的加载和保存提供了特定的指令)

Linux 0.12 内核对内存的管理_第4张图片


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。

Linux 0.12 内核对内存的管理_第5张图片

对于访问某个段的程序,必须已经把段选择子加载到一个段寄存器中。

(汇编程序是分段编写的,在其运行前各段的选择子就已经被默默地加载到了段寄存器中了,进行长跳转指令,从一个段跳转到另一个段中,这个跳转指令会把新段的选择子加载到寄存器中)

每个段寄存器都有一个“可见”部分和一个“隐藏”部分。隐藏部分用于段描述符缓存(页表的缓存是TLB)

当一个段选择子被加载到一个段寄存器可见部分中时,CPU也同时把段选择子指向的段描述符加载到段寄存器的隐藏部分中。

缓存在段寄存器(可见部分和隐藏部分)中的信息使得CPU可以在进行地址转换时不再需要花费时间从段描述符中读取基地址和限长值


段描述符

段描述符是GDT和LDT表中的一个数据结构项,用于向CPU提供有关一个段的位置和大小信息以及访问控制的状态信息。

每个段描述符的长度是8字节,含有:段基地址、段限长、段属性(段类型、访问控制、特权级等)

 


分页机制

分段机制把逻辑地址转换成线性地址,而分页则把线性地址转换成物理地址

与分段机制不同,分页机制对固定大小的内存块(页面)进行操作。分页机制把线性和物理地址空间都划分成页面。线性地址空间中的任何页面可以被映射到物理地址空间的任何页面上。


Linux 0.12 内核对内存的管理_第6张图片

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个页。

Linux 0.12 内核对内存的管理_第7张图片

 

2、不存在的页表

二级页表结构允许页表被分散在内存各个页面中,而不需要保存在连续的4MB内存块中。(因为每一个二级页表都是4K大小,正好放在一个页中)

且,并不需要为不存在的或线性地址空间未使用部分分配二级页表。(一级页表把4G线性空间中分割的所有页都记录了)

目录表项中每个表项有一个存在属性,可用于在虚拟内存中存放二级页表。这意味着只有部分二级页表需要存放在主存中,其余的可以保存在磁盘上。

 

页表项格式

目录项和页表项的格式,如下图。其中位31~12含有物理地址的高20位,用于定位物理地址空间中一个页面的物理基地址。表项的低12位含有页属性信息。

Linux 0.12 内核对内存的管理_第8张图片

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。当页面加载到主存中之后,从异常处理过程的返回操作会使得导致异常的指令被重新执行。

你可能感兴趣的:(内存管理)