Linux的内存寻址——浅谈分段和分页机制

时间:2022-12-29 08:40:09

本文会以80x86架构,linux2.6为例,简单介绍内存的分段和分页机制。

1. 三种内存地址

关于内存地址,首先要了解它有三种,分别是

逻辑地址、线性地址和物理地址。

把逻辑地址转换为线性地址是由一个叫做分段单元的硬件电路完成的。

同样地,还有一个叫做分页单元的硬件电路负责把线性地址转换为物理地址。

那现在很明确地,当我们讨论分段的时候,就是讨论逻辑地址是如何转换成线性地址的。当我们讨论分页的时候,就是在讨论线性地址如何转换成了物理地址

下面分开讨论分段和分页功能是如何实现。

2. 逻辑地址

介绍分段之前,先明确逻辑地址的构成分两部分:一个16位的段选择符,一个32位的偏移量。

其中段选择符由高到低是:13位index,1位TI,2位RPL。它们的作用在下文介绍。

3. 段选择符、段寄存器、段描述符

现在我们知道,段选择符是逻辑地址的一部分,为了方便找到段选择符,处理器提供了6个段寄存器,段寄存器的唯一目的就是存放段选择符。

段描述符有8个字节,负责描述段的特征,包括段的首字节的线性地址、段的最大长度、段的特权级、区分段是数据段还是代码段的标志位……一共9个字段。

段选择符和段描述符具有对应关系,那应该如何通过段选择符得到段描述符呢?答案是查找全局描述符表(GDT)或者局部描述符表(LDT)。

4. GDT\LDT

在linux中,每个CPU都有一个自己的GDT,用于存放本处理器寻址要用到的段描述符。linux有一个全局的cpu_gdt_table数组,放着每个cpu当前的GDT。每个GDT有32个表项,其中有14个空置,剩下的都是段描述符,包括了用户态代码段、用户态数据段、内核态数据段、内核态代码段、任务状态段TSS、3个局部线程存储段TLS……共18个段描述符。。

所谓LDT就是某些进程要有一些自己用的段,需要创建一个自己专用的描述符表,这就是LDT。

从硬件上看,处理器有两个寄存器专门存放GDT和LDT的基地址(是一个线性地址),分别叫做gdtr和ldtr。

5.分段单元的工作过程

Input:逻辑地址(段选择符和偏移量)

段选择符的TI字段决定了这个段的段描述符是在GDT还是LDT中,通过对TI字段的检查,分段单元可以知道它应该去gdtr还是ldtr去读表的基地址。

接下来看段选择符的index字段,这个字段说明了段描述符在表的第几项。因为每个段描述符是8字节,那么表的基地址+index*8就是段描述符的地址。

得到段描述符后,也就知道了段的基地址了。此时用段的基地址+段的偏移量,就得到最终的线性地址了。

Output:该逻辑地址对应的线性地址。

分段就介绍完了。而事实上,从2.6内核开始,Linux就只在80x80结构下才需要使用分段了。为什么linux偏爱分页呢?首先是因为忽略掉分段后,内存的管理简单了,相当于所有的段的基地址都相同,大家都是同一个段,linux只负责处理线性地址就好了;另外,不同的计算机体系结构对分段的支持不一,使用分段不利于linux的跨处理器平台。

所以在linux里,用户代码段、数据段,内核代码段数据段,它们的段描述符里,段的起始线性地址都是0x00000000。这意味着一个逻辑地址,它的偏移量就是它的线性地址。

好,接下来讨论分页。

6. 页,页框,页表

页是为了提高效率,把线性地址分成了固定长度的组,所以页就是一组连续的线性地址。在转换成物理地址的时候一次转换一页。

既然这样,那物理地址也要分组。分页单元把所有的RAM也分成了固定长度的组,叫做页框(所以页框就是一组连续的物理地址),页框和页长度一致,一个页框可以放一页。

知道了页和页框以后,那么我们怎么知道该把哪一页放进哪个页框里呢?这就要查页表了。页表放在主存里,页表可能分多级。

7.常规分页,扩展分页,64位系统的分页

从这里开始,我们默认分页单元处理的一页是4096字节。

一个32位的线性地址被分成三部分,从高位到地位分别是:10位的Directory,10位的table,12位的offset。

首先为什么要分多级呢?因为如果只有一级目录的话,那么表项会非常多。因为一页是4096 = 2^12字节,那么offset是一定要占12位的,剩下的20位将产生2^20个表项,一个表项4字节,载入一个页表就要占4MB的RAM,这太浪费了内存了。而当使用这种二级模式时,可以通过目录表先找到要读的页,这样一次只读2^10个表项,小了一半。

现在我们可以基本想象到分页单元是怎么把线性地址变成物理地址的了:假设有一个线性地址0x20180601,低12位offset是0x601,中间10位的table是0x180,高十位directory是0x080,directory表的物理地址在寄存器cr3中,读到directory后从第0x80项找到table,从table的第0x180项找到本页对应页框的起始物理地址,最后这个地址+0x601得到最终的物理地址。

但是实际操作中还有些细节,比如说,1024项directory,1024项table,和12位的偏移,一共可以寻址2^32个地址,涵盖了整个内存,但事实上一个进程只会从有限的线性地址空间内分配空间,假设内核给了一个线程64页,这已经是可以分配给进程的上限了,假设它的空间是0x2000 0000 到0x2003 ffff,目录项只有0x080是有用的,其他都要设置成0,同理,table里只有0到0x3f号的64个表项有用,其他也都设置成0。

如果这个进程想要访问超过这64页之外的空间,比如0x3000 0000,那必然是非法的,这个页根本不会在内存中,这就要返回一个缺页异常。我们需要一个标志位来表明这个页到底在不在主存里。可见,directory表和table表,每个表项除了要有对应的页表或者页框地址之外,还要有一些标志位。实际上,有一个present标志负责记录该页是否在主存中。如果试图访问一个present=0的表项,就会产生缺页异常。

除了这种常规分页,从Pentium开始,还引入了扩展分页PAE,允许一个页框达到4MB项,引入扩展分页后,offset有22位,高10位做目录,不再有二级模式。

分页到了64位系统中又有不同,因为在64位地址,4kb页框的情况下,除去12位的offset,剩下了48位做directory或者table。那这样显然会造成每张表的表项非常高。

那应该怎么解决呢?一个是增加分页级别,从原来的二级升高到3级或4级;另外可以让64位并不全用来寻址。至于寻址用多少位,分页分几级,会因处理器类型而异。像x64_64,用48位寻址,按9+9+9+9+12划分,一共有4级页表。

8. Linux分页

既然32位系统和64位系统分页方法不同,不同硬件系统分页的策略也不同,Linux是怎么适应这诸多硬件模型的呢?

linux采用了4级分页模型:分别是页全局目录PGD,页上级目录PUD,页中间目录PMD,页表PTE。如果是32位系统,而且没有PAE,那么PUD和PMD就不启用,全部置0。

Linux内用pte_t、pmd_t、pud_t和pgd_t描述页表、页中间目录、页上级目录和页全局目录,提供了丰富的api,可以操作每一级页表,进行读、写、清空等一系列操作。

OK,到此为止,本文简单的介绍了几个和分段分页相关的概念,包括分段相关的逻辑地址、段描述符、段选择符、段寄存器、GDT和LDT,以及分页相关的页、页框、页表、PAE、PTE、PMD、PUD和PGD等,并简单描述了分段单元和分页单元的工作过程。