2016-09-01 19:46:08 gatieme 阅读数 6964
日期 内核版本 架构 作者 GitHub CSDN
2016-09-01 Linux-4.7 X86 & arm gatieme LinuxDeviceDrivers Linux内存管理

1 分页机制


在虚拟内存中,页表是个映射表的概念, 即从进程能理解的线性地址(linear address)映射到存储器上的物理地址(phisical address).

很显然,这个页表是需要常驻内存的东西, 以应对频繁的查询映射需要(实际上,现代支持VM的处理器都有一个叫TLB的硬件级页表缓存部件,本文不讨论)。

1.1 为什么使用多级页表来完成映射



但是为什么要使用多级页表来完成映射呢?

用来将虚拟地址映射到物理地址的数据结构称为页表, 实现两个地址空间的关联最容易的方式是使用数组, 对虚拟地址空间中的每一页, 都分配一个数组项. 该数组指向与之关联的页帧, 但这会引发一个问题, 例如, IA-32体系结构使用4KB大小的页, 在虚拟地址空间为4GB的前提下, 则需要包含100万项的页表. 这个问题在64位体系结构下, 情况会更加糟糕. 而每个进程都需要自身的页表, 这回导致系统中大量的所有内存都用来保存页表.

设想一个典型的32位的X86系统,它的虚拟内存用户空间(user space)大小为3G, 并且典型的一个页表项(page table entry, pte)大小为4 bytes,每一个页(page)大小为4k bytes。那么这3G空间一共有(3G/4k=)786432个页面,每个页面需要一个pte来保存映射信息,这样一共需要786432个pte!

如何存储这些信息呢?一个直观的做法是用数组来存储,这样每个页能存储(4k/4=)1K个,这样一共需要(786432/1k=)768个连续的物理页面(phsical page)。而且,这只是一个进程,如果要存放所有N个进程,这个数目还要乘上N! 这是个巨大的数目,哪怕内存能提供这样数量的空间,要找到连续768个连续的物理页面在系统运行一段时间后碎片化的情况下,也是不现实的。

为减少页表的大小并容许忽略不需要的区域, 计算机体系结构的涉及会将虚拟地址分成多个部分. 同时虚拟地址空间的大部分们区域都没有使用, 因而页没有关联到页帧, 那么就可以使用功能相同但内存用量少的多的模型: 多级页表


但是新的问题来了, 到底采用几级页表合适呢?

1.2 32位系统中2级页表


从80386开始, intel处理器的分页单元是4KB的页, 32位的地址空间被分为3部分

单元 描述
页目录表Directory 最高10位
页中间表Table 中间10位
页内偏移 最低12位

即页表被划分为页目录表Directory和页中间表Tabl两个部分

此种情况下, 线性地址的转换分为两步完成.

  • 第一步, 基于两级转换表(页目录表和页中间表), 最终查找到地址所在的页帧

  • 第二步, 基于偏移, 在所在的页帧中查找到对应偏移的物理地址

使用这种二级页表可以有效的减少每个进程页表所需的RAM的数量. 如果使用简单的一级页表, 那将需要高达220个页表, 假设每项4B, 则共需要占用2204B=4MB的RAM来表示每个进程的页表. 当然我们并不需要映射所有的线性地址空间(32位机器上线性地址空间为4GB), 内核通常只为进程实际使用的那些虚拟内存区请求页表来减少内存使用量.

1.3 64位系统中的分页


正常来说, 对于32位的系统两级页表已经足够了, 但是对于64位系统的计算机, 这远远不够.

首先假设一个大小为4KB的标准页. 因为1KB覆盖210个地址的范围, 4KB覆盖212个地址, 所以offset字段需要12位.

这样线性地址空间就剩下64-12=52位分配给页中间表Table和页目录表Directory. 如果我们现在决定仅仅使用64位中的48位来寻址(这个限制其实已经足够了, 2^48=256TB, 即可达到256TB的寻址空间). 剩下的48-12=36位被分配给Table和Directory字段. 即使我们现在决定位两个字段各预留18位, 那么每个进程的页目录和页表都包含218个项, 即超过256000个项.

基于这个原因, 所有64位处理器的硬件分页系统都使用了额外的分页级别. 使用的级别取决于处理器的类型

平台名称 页大小 寻址所使用的位数 分页级别数 线性地址分级
alpha 8KB 43 3 10 + 10 + 10 + 13
ia64 4KB 39 3 9 + 9 + 9 + 12
ppc64 4KB 41 3 10 + 10 + 9 + 12
sh64 4KB 41 3 10 + 10 + 9 + 12
x86_64 4KB 48 4 9 + 9 + 9 + 9 + 12

1.4 Linux中的分页


层次话的页表用于支持对大地址空间快速, 高效的管理. 因此linux内核堆页表进行了分级.

前面我们提到过, 对于32位系统中, 两级页表已经足够了. 但是64位修奥更多数量的分页级别.

为了同时支持适用于32位和64位的系统, Linux采用了通用的分页模型. 在Linux-2.6.10版本中, Linux采用了三级分页模型. 而从2.6.11开始普遍采用了四级分页模型.

目前的内核的内存管理总是嘉定使用四级页表, 而不管底层处理器是否如此.

单元 描述
页全局目录 Page GlobalDirectory
页上级目录 Page Upper Directory
页中间目录 Page Middle Directory
页表 Page Table
页内偏移 Page Offset


Linux不同于其他的操作系统, 它把计算机分成独立层(体系结构无关)/依赖层(体系结构相关)两个层次. 对于页面的映射和管理也是如此. 页表管理分为两个部分, 第一个部分依赖于体系结构, 第二个部分是体系结构无关的. 所有数据结构几乎都定义在特定体系结构的文件中. 这些数据结构的定义可以在头文件arch/对应体系/include/asm/page.harch/对应体系/include/asm/pgtable.h中找到. 但是对于AMD64和IA-32已经统一为一个体系结构. 但是在处理页表方面仍然有很多的区别, 因为相关的定义分为两个不同的文件arch/x86/include/asm/page_32.harch/x86/include/asm/page_64.h, 类似的也有pgtable_xx.h .

2 页表


Linux内核通过四级页表将虚拟内存空间分为5个部分(4个页表项用于选择页, 1个索引用来表示页内的偏移). 各个体系结构不仅地址长度不同, 而且地址字拆分的方式也不一定相同. 因此内核使用了宏用于将地址分解为各个分量.

linux四级页表

其他内容请参照博主的另外两篇博客, 我就不罗嗦了

深入理解计算机系统-之-内存寻址(五)–页式存储管理, 详细讲解了传统的页式存储管理机制

深入理解计算机系统-之-内存寻址(六)–linux中的分页机制, 详细的讲解了Linux内核分页机制的实现机制

3 Linux分页机制的演变


3.1 Linux的页表实现


由于程序存在局部化特征, 这意味着在特定的时间内只有部分内存会被频繁访问,具体点,进程空间中的text段(即程序代码), 堆, 共享库,栈都是固定在进程空间的某个特定部分,这样导致进程空间其实是非常稀疏的, 于是,从硬件层面开始,页表的实现就是采用分级页表的方式,Linux内核当然也这么做。所谓分级简单说就是,把整个进程空间分成区块,区块下面可以再细分,这样在内存中只要常驻某个区块的页表即可,这样可以大量节省内存。

3.2 Linux最初的二级页表


Linux最初是在一台i386机器上开发的,这种机器是典型的32位X86架构,支持两级页表

一个32位虚拟地址如上图划分。当在进行地址转换时,

结合在CR3寄存器中存放的页目录(page directory, PGD)的这一页的物理地址,再加上从虚拟地址中抽出高10位叫做页目录表项(内核也称这为pgd)的部分作为偏移, 即定位到可以描述该地址的pgd;

从该pgd中可以获取可以描述该地址的页表的物理地址,再加上从虚拟地址中抽取中间10位作为偏移, 即定位到可以描述该地址的pte;

在这个pte中即可获取该地址对应的页的物理地址, 加上从虚拟地址中抽取的最后12位,即形成该页的页内偏移, 即可最终完成从虚拟地址到物理地址的转换。
从上述过程中,可以看出,对虚拟地址的分级解析过程,实际上就是不断深入页表层次,逐渐定位到最终地址的过程,所以这一过程被叫做page talbe walk。

至于这种做法为什么能节省内存,举个更简单的例子更容易明白。比如要记录16个球场的使用情况,每张纸能记录4个场地的情况。采用4+4+4+4,共4张纸即可记录,但问题是球场使用得很少,有时候一整张纸记录的4个球场都没人使用。于是,采用4 x 4方案,即把16个球场分为4组,同样每张纸刚好能记录4组情况。这样,使用一张纸A来记录4个分组球场情况,当某个球场在使用时,只要额外使用多一张纸B来记录该球场,同时,在A上记录”某球场由纸B在记录”即可。这样在大部分球场使用很少的情况下,只要很少的纸即困记录,当有球场被使用,有需要再用额外的纸来记录,当不用就擦除。这里一个很重要的前提就是:局部性。

3.3 Linux的三级页表


当X86引入物理地址扩展(Pisycal Addrress Extension, PAE)后,可以支持大于4G的物理内存(36位),但虚拟地址依然是32位,原先的页表项不适用,它实际多4 bytes被扩充到8 bytes,这意味着,每一页现在能存放的pte数目从1024变成512了(4k/8)。相应地,页表层级发生了变化,Linus新增加了一个层级,叫做页中间目录(page middle directory, PMD), 变成:

字段 描述 位数
cr3 指向一个PDPT crs寄存器存储
PGD 指向PDPT中4个项中的一个 位31~30
PMD 指向页目录中512项中的一个 位29~21
PTE 指向页表中512项中的一个 位20~12
page offset 4KB页中的偏移 位11~0

实际的page table walk依然类似,只不过多了一级

现在就同时存在2级页表和3级页表,在代码管理上肯定不方便。巧妙的是,Linux采取了一种抽象方法:所有架构全部使用3级页表: 即PGD -> PMD -> PTE。那只使用2级页表(如非PAE的X86)怎么办?

办法是针对使用2级页表的架构,把PMD抽象掉,即虚设一个PMD表项。这样在page table walk过程中,PGD本直接指向PTE的,现在不了,指向一个虚拟的PMD,然后再由PMD指向PTE。这种抽象保持了代码结构的统一。

3.4 Linux的四级页表


硬件在发展,3级页表很快又捉襟见肘了,原因是64位CPU出现了, 比如X86_64, 它的硬件是实实在在支持4级页表的。它支持48位的虚拟地址空间1。如下:

字段 描述 位数
PML4 指向一个PDPT 位47~39
PGD 指向PDPT中4个项中的一个 位38~30
PMD 指向页目录中512项中的一个 位29~21
PTE 指向页表中512项中的一个 位20~12
page offset 4KB页中的偏移 位11~0

Linux内核针为使用原来的3级列表(PGD->PMD->PTE),做了折衷。即采用一个唯一的,共享的顶级层次,叫PML4[2]。这个PML4没有编码在地址中,这样就能套用原来的3级列表方案了。不过代价就是,由于只有唯一的PML4, 寻址空间被局限在(239=)512G, 而本来PML4段有9位, 可以支持512个PML4表项的。现在为了使用3级列表方案,只能限制使用一个, 512G的空间很快就又不够用了,解决方案呼之欲出。

在2004年10月,当时的X86_64架构代码的维护者Andi Kleen提交了一个叫做4level page tables for Linux的PATCH系列,为Linux内核带来了4级页表的支持。在他的解决方案中,不出意料地,按照X86_64规范,新增了一个PML4的层级, 在这种解决方案中,X86_64拥一个有512条目的PML4, 512条目的PGD, 512条目的PMD, 512条目的PTE。对于仍使用3级目录的架构来说,它们依然拥有一个虚拟的PML4,相关的代码会在编译时被优化掉。 这样,就把Linux内核的3级列表扩充为4级列表。这系列PATCH工作得不错,不久被纳入Andrew Morton的-mm树接受测试。

不出意外的话,它将在v2.6.11版本中释出。但是,另一个知名开发者Nick Piggin提出了一些看法,他认为Andi的Patch很不错,不过他认为最好还是把PGD作为第一级目录,把新增加的层次放在中间,并给出了他自己的Patch:alternate 4-level page tables patches。Andi更想保持自己的PATCH, 他认为Nick不过是玩了改名的游戏,而且他的PATCH经过测试很稳定,快被合并到主线了,不宜再折腾。

不过Linus却表达了对Nick Piggin的支持,理由是Nick的做法conceptually least intrusive。毕竟作为Linux的扛把子,稳定对于Linus来说意义重大。

最终,不意外地,最后Nick Piggin的PATCH在v2.6.11版本中被合并入主线。在这种方案中,4级页表分别是:PGD -> PUD -> PMD -> PTE。

4 链接


我对linux内核四级分页理解

Linux内核4级页表的演进

Linux内存 之 页表

内存管理(四) 页表数据结构

Linux内存管理之我见(二)-页表、页式内存管理机制

2013-05-11 12:35:19 bcs_01 阅读数 1427

         先前我们介绍段机制的时候说到,x86的段机制把程序的逻辑地址转换成线性地址,这里要讲的分页机制是把线性地址映射成物理地址,也就说说,x86其实是用了两套机制把逻辑地址转换成物理地址的。我们也提到linux内核是怎样绕过段机制从而让x86的分段机制看起来不起作用的,我们还说到这样的处理造成了段的数据保护的问题。下来我们就来讨论x86的分页机制。我们的思路依然是先介绍x86的分页机制,然后再讨论linux在上边的实现。

      我们还是从实现linux分页机制的软硬件构造上入手。实现分页机制的硬件结构有:四个专用于分页机制的32位寄存器:CR0、CR1、CR2、CR3,分页部件,页面高速缓存;软件结构有:页,页表,页目录,页表项。

1、四个寄存器

        我们说CR0这个寄存器决定了我们是否启用分页机制,CR0=0时表示不启用分页机制,此时经过段机制转换来的地址直接映射到物理地址(大家可以想到,如果这样的话,像上节linux内核的处理方法,势必会带来段的数据保护问题的),反之,映射到4G的线性地址。

        CR1暂时没用。

        CR2是缺页线性地址寄存器,当发生缺页异常的时候,该寄存器保存最后一次出现缺页的全32位线性地址,这在异常处理时会介绍到,这里我们不深究它的实现过程。

        CR3就厉害了,它被叫做页目录基址寄存器,顾名思义,它保存的是页目录的物理地址。我们就是通过该寄存器里边的值,找到对应的页目录中的页表项,从而知道我们的一个页是存储里内存中的哪个位置的。

2、页与页表

        1)、页

       分页机制是这么一个思路:我们将线性地址空间划分成若干相等的片,这一片就称为一个页,并给各页编号,从0开始,如第1页,第2页等。相应的,我们也把物理地址空间划分成与页大小相等的若干存储块,也同样为他们编号,仍然可以从0开始。程序的映射过程是这样的,现在我们的程序的容量是以页为单位(即要么占一个页大小,要么占几个页大小),那么我们要控制线性地址空间中的每个页怎么往物理地址里边存就比较容易了。

        这样就有一个问题了,因为页是固定大小的,那么它过大或者过小都会影响内存使用率。页过大,有时候会存在冗余空间;页过小,若有分支指令,则会存在在页之间跳转的情况,同样影响效率。x86支持的标准页大小为4KB(也支持4MB)。

        2)、页表

        页表是把线性地址转换成物理地址的一种数据结构。它的作用相当于段描述符表。它包括两个成员:

        物理页面基地址:线性地址空间中的一个页装入内存后所对应的物理页面的起始地址。

        页的属性:描述一些页的属性信息。

        我们说过页面大小为4KB,即一个页面会占4KB的空间,那么每个页面的物理页面基地址必然是4KB的整数倍,这样其地址的最低12位总是0,那么我们就可以用这12位存放页的属性,这样用32位的地址就完全可以描述页的映射关系,也就是页表中一个表项占四个字节就够了。

        不过,4GB的线性地址空间可以被划分为1M个4KB大小的页,每个页表项占4个字节,则1M个页表项就需要占4MB的空间,而且还要求是连续的,这显然是不现实的,于是把这4MB的页表再以4KB为大小分页,分为1K个页,同样对每个项描述需要四个字节,这就是两级页表的管理方式。

        3)、两级页表

                             两级页表结构

        两级页表的第一级把它叫做页目录,用它来管理1M个页的页表。上边说我们把1M个页表项以4KB为页分了1K个页,于是我们用10位就可以检索到这1K个页目录的每个目录项了。现在我们把这1K个页目录(占4KB)放进内存的某个位置,这个位置就叫做页目录起始地址。我们把这个起始地址放进CR3中,现在来一个线性地址,我们根据线性地址的前10位,再根据CR3中的页目录起始地址,就可以得到目录项在内存中的地址。那我们读取这个目录项,其高20位就是页表在内存中的起始地址。于是我们用线性地址的中间10位和这个页表起始地址就可以计算出页表项的位置,这个页表项和线性地址的的低12位就可以把这个线性地址定位到内存中具体的位置了。下图是32位线性地址往物理地址的转换过程。

3、页面高速缓存

        知道cache原理的童鞋们对这个理解起来就比较容易了。它无非就是为了加快访问速度。

        由于在分页情况下,页表是放在内存中的,这使CPU每次至少两次去访问内存。页面高速缓冲器保存最近处理过的32项页表项。当访问线性地址空间的某个地址时,先检查对应的页表项是否在缓存中,如果在就没必要两次访问内存了。高速缓存的命中率哈市相当高的。

       

       

       

2013-10-15 21:52:02 TihsYloH 阅读数 607

涉及的硬件平台是X86,如果是其它平台,嘻嘻,不保证能一一对号入座,但是举一反三,我想是完全可行的。

一、概念 物理地址(physical address) 用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。 ——这个概念应该是这几个概念中最好理解的一个,但是值得一提的是,虽然可以直接把物理地址理解成插在机器上那根内存本身,把内存看成一个从0字节一直到最大空量逐字节的编号的大数组,然后把这个数组叫做物理地址,但是事实上,这只是一个硬件提供给软件的抽像,内存的寻址方式并不是这样。所以,说它是“与地址总线相对应”,是更贴切一些,不过抛开对物理内存寻址方式的考虑,直接把物理地址与物理的内存一一对应,也是可以接受的。也许错误的理解更利于形而上的抽像。 虚拟内存(virtual memory) 这是对整个内存(不要与机器上插那条对上号)的抽像描述。它是相对于物理内存来讲的,可以直接理解成“不直实的”,“假的”内存,例如,一个0x08000000内存地址,它并不对就物理地址上那个大数组中0x08000000 - 1那个地址元素;之所以是这样,是因为现代操作系统都提供了一种内存管理的抽像,即虚拟内存(virtual memory)。进程使用虚拟内存中的地址,由操作系统协助相关硬件,把它“转换”成真正的物理地址。这个“转换”,是所有问题讨论的关键。有了这样的抽像,一个程序,就可以使用比真实物理地址大得多的地址空间。(拆东墙,补西墙,银行也是这样子做的),甚至多个进程可以使用相同的地址。不奇怪,因为转换后的物理地址并非相同的。 ——可以把连接后的程序反编译看一下,发现连接器已经为程序分配了一个地址,例如,要调用某个函数A,代码不是call A,而是call 0x0811111111 ,也就是说,函数A的地址已经被定下来了。没有这样的“转换”,没有虚拟地址的概念,这样做是根本行不通的。打住了,这个问题再说下去,就收不住了。

逻辑地址(logical address) Intel为了兼容,将远古时代的段式内存管理方式保留了下来。逻辑地址指的是机器语言指令中,用来指定一个操作数或者是一条指令的地址。以上例,我们说的连接器为A分配的0x08111111这个地址就是逻辑地址。 ——不过不好意思,这样说,好像又违背了Intel中段式管理中,对逻辑地址要求,“一个逻辑地址,是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 [段标识符:段内偏移量],也就是说,上例中那个0x08111111,应该表示为[A的代码段标识符: 0x08111111],这样,才完整一些”

线性地址(linear address)或也叫虚拟地址(virtual address) 跟逻辑地址类似,它也是一个不真实的地址,如果逻辑地址是对应的硬件平台段式管理转换前地址的话,那么线性地址则对应了硬件页式内存的转换前地址。 ------------------------------------------------------------- CPU将一个虚拟内存空间中的地址转换为物理地址,需要进行两步:首先将给定一个逻辑地址(其实是段内偏移量,这个一定要理解!!!),CPU要利用其段式内存管理单元,先将为个逻辑地址转换成一个线程地址,再利用其页式内存管理单元,转换为最终物理地址。 这样做两次转换,的确是非常麻烦而且没有必要的,因为直接可以把线性地址抽像给进程。之所以这样冗余,Intel完全是为了兼容而已。

2、CPU段式内存管理,逻辑地址如何转换为线性地址 一个逻辑地址由两部份组成,段标识符: 段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节,如图: 最后两位涉及权限检查,本贴中不包含。 索引号,或者直接理解成数组下标——那它总要对应一个数组吧,它又是什么东东的索引呢?这个东东就是“段描述符(segment descriptor)”,呵呵,段描述符具体地址描述了一个段(对于“段”这个字眼的理解,我是把它想像成,拿了一把刀,把虚拟内存,砍成若干的截——段)。这样,很多个段描述符,就组了一个数组,叫“段描述符表”,这样,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段,我刚才对段的抽像不太准确,因为看看描述符里面究竟有什么东东——也就是它究竟是如何描述的,就理解段究竟有什么东东了,每一个段描述符由8个字节组成,如下图: 这些东东很复杂,虽然可以利用一个数据结构来定义它,不过,我这里只关心一样,就是Base字段,它描述了一个段的开始位置的线性地址。 Intel设计的本意是,一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中。那究竟什么时候该用GDT,什么时候该用LDT呢?这是由段选择符中的T1字段表示的,=0,表示用GDT,=1表示用LDT。 GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。 好多概念,像绕口令一样。这张图看起来要直观些: 首先,给定一个完整的逻辑地址[段选择符:段内偏移地址], 1、看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。 2、拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。 3、把Base + offset,就是要转换的线性地址了。 还是挺简单的,对于软件来讲,原则上就需要把硬件转换所需的信息准备好,就可以让硬件来完成这个转换了。OK,来看看Linux怎么做的。

3、Linux的段式管理 Intel要求两次转换,这样虽说是兼容了,但是却是很冗余,呵呵,没办法,硬件要求这样做了,软件就只能照办,怎么着也得形式主义一样。另一方面,其它某些硬件平台,没有二次转换的概念,Linux也需要提供一个高层抽像,来提供一个统一的界面。所以,Linux的段式管理,事实上只是“哄骗”了一下硬件而已。 按照Intel的本意,全局的用GDT,每个进程自己的用LDT——不过Linux则对所有的进程都使用了相同的段来对指令和数据寻址。即用户数据段,用户代码段,对应的,内核中的是内核数据段和内核代码段。这样做没有什么奇怪的,本来就是走形式嘛,像我们写年终总结一样。 include/asm-i386/segment.h

CODE:
#define GDT_ENTRY_DEFAULT_USER_CS 14 #define __USER_CS (GDT_ENTRY_DEFAULT_USER_CS * 8 + 3) #define GDT_ENTRY_DEFAULT_USER_DS 15 #define __USER_DS (GDT_ENTRY_DEFAULT_USER_DS * 8 + 3) #define GDT_ENTRY_KERNEL_BASE 12 #define GDT_ENTRY_KERNEL_CS (GDT_ENTRY_KERNEL_BASE + 0) #define __KERNEL_CS (GDT_ENTRY_KERNEL_CS * 8) #define GDT_ENTRY_KERNEL_DS (GDT_ENTRY_KERNEL_BASE + 1) #define __KERNEL_DS (GDT_ENTRY_KERNEL_DS * 8)

把其中的宏替换成数值,则为:

CODE:
#define __USER_CS 115 [00000000 1110 0 11] #define __USER_DS 123 [00000000 1111 0 11] #define __KERNEL_CS 96 [00000000 1100 0 00] #define __KERNEL_DS 104 [00000000 1101 0 00]

方括号后是这四个段选择符的16位二制表示,它们的索引号和T1字段值也可以算出来了

CODE:
__USER_CS index= 14 T1=0 __USER_DS index= 15 T1=0 __KERNEL_CS index= 12 T1=0 __KERNEL_DS index= 13 T1=0

T1均为0,则表示都使用了GDT,再来看初始化GDT的内容中相应的12-15项(arch/i386/head.S):

CODE:
.quad 0x00cf9a000000ffff /* 0x60 kernel 4GB code at 0x00000000 */ .quad 0x00cf92000000ffff /* 0x68 kernel 4GB data at 0x00000000 */ .quad 0x00cffa000000ffff /* 0x73 user 4GB code at 0x00000000 */ .quad 0x00cff2000000ffff /* 0x7b user 4GB data at 0x00000000 */

按照前面段描述符表中的描述,可以把它们展开,发现其16-31位全为0,即四个段的基地址全为0。 这样,给定一个段内偏移地址,按照前面转换公式,0 + 段内偏移,转换为线性地址,可以得出重要的结论,“在Linux下,逻辑地址与线性地址总是一致(是一致,不是有些人说的相同)的,即逻辑地址的偏移量字段的值与线性地址的值总是相同的。!!!” 忽略了太多的细节,例如段的权限检查。呵呵。 Linux中,绝大部份进程并不例用LDT,除非使用Wine ,仿真Windows程序的时候。

4.CPU的页式内存管理 CPU的页式内存管理单元,负责把一个线性地址,最终翻译为一个物理地址。从管理和效率的角度出发,线性地址被分为以固定长度为单位的组,称为页(page),例如一个32位的机器,线性地址最大可为4G,可以用4KB为一个页来划分,这页,整个线性地址就被划分为一个tatol_page[2^20]的大数组,共有2的20个次方个页。这个大数组我们称之为页目录。目录中的每一个目录项,就是一个地址——对应的页的地址。 另一类“页”,我们称之为物理页,或者是页框、页桢的。是分页单元把所有的物理内存也划分为固定长度的管理单位,它的长度一般与内存页是一一对应的。 这里注意到,这个total_page数组有2^20个成员,每个成员是一个地址(32位机,一个地址也就是4字节),那么要单单要表示这么一个数组,就要占去4MB的内存空间。为了节省空间,引入了一个二级管理模式的机器来组织分页单元。文字描述太累,看图直观一些: 如上图, 1、分页单元中,页目录是唯一的,它的地址放在CPU的cr3寄存器中,是进行地址转换的开始点。万里长征就从此长始了。 2、每一个活动的进程,因为都有其独立的对应的虚似内存(页目录也是唯一的),那么它也对应了一个独立的页目录地址。——运行一个进程,需要将它的页目录地址放到cr3寄存器中,将别个的保存下来。 3、每一个32位的线性地址被划分为三部份,面目录索引(10位):页表索引(10位):偏移(12位) 依据以下步骤进行转换: 1、从cr3中取出进程的页目录地址(操作系统负责在调度进程的时候,把这个地址装入对应寄存器); 2、根据线性地址前十位,在数组中,找到对应的索引项,因为引入了二级管理模式,页目录中的项,不再是页的地址,而是一个页表的地址。(又引入了一个数组),页的地址被放到页表中去了。 3、根据线性地址的中间十位,在页表(也是数组)中找到页的起始地址; 4、将页的起始地址与线性地址中最后12位相加,得到最终我们想要的葫芦; 这个转换过程,应该说还是非常简单地。全部由硬件完成,虽然多了一道手续,但是节约了大量的内存,还是值得的。那么再简单地验证一下: 1、这样的二级模式是否仍能够表示4G的地址;页目录共有:2^10项,也就是说有这么多个页表每个目表对应了:2^10页;每个页中可寻址:2^12个字节。还是2^32 = 4GB 2、这样的二级模式是否真的节约了空间;也就是算一下页目录项和页表项共占空间 (2^10 * 4 + 2 ^10 *4) = 8KB。哎,……怎么说呢!!! 红色错误,标注一下,后文贴中有此讨论。。。。。。 值得一提的是,虽然页目录和页表中的项,都是4个字节,32位,但是它们都只用高20位,低12位屏蔽为0——把页表的低12屏蔽为0,是很好理解的,因为这样,它刚好和一个页面大小对应起来,大家都成整数增加。计算起来就方便多了。但是,为什么同时也要把页目录低12位屏蔽掉呢?因为按同样的道理,只要屏蔽其低10位就可以了,不过我想,因为12>10,这样,可以让页目录和页表使用相同的数据结构,方便。 本贴只介绍一般性转换的原理,扩展分页、页的保护机制、PAE模式的分页这些麻烦点的东东就不啰嗦了……可以参考其它专业书籍。

5.Linux的页式内存管理 原理上来讲,Linux只需要为每个进程分配好所需数据结构,放到内存中,然后在调度进程的时候,切换寄存器cr3,剩下的就交给硬件来完成了(呵呵,事实上要复杂得多,不过偶只分析最基本的流程)。 前面说了i386的二级页管理架构,不过有些CPU,还有三级,甚至四级架构,Linux为了在更高层次提供抽像,为每个CPU提供统一的界面。提供了一个四层页管理架构,来兼容这些二级、三级、四级管理架构的CPU。这四级分别为: 页全局目录PGD(对应刚才的页目录)页上级目录PUD(新引进的)页中间目录PMD(也就新引进的)页表PT(对应刚才的页表)。 整个转换依据硬件转换原理,只是多了二次数组的索引罢了,如下图: 那么,对于使用二级管理架构32位的硬件,现在又是四级转换了,它们怎么能够协调地工作起来呢?嗯,来看这种情况下,怎么来划分线性地址吧!从硬件的角度,32位地址被分成了三部份——也就是说,不管理软件怎么做,最终落实到硬件,也只认识这三位老大。从软件的角度,由于多引入了两部份,,也就是说,共有五部份。——要让二层架构的硬件认识五部份也很容易,在地址划分的时候,将页上级目录和页中间目录的长度设置为0就可以了。这样,操作系统见到的是五部份,硬件还是按它死板的三部份划分,也不会出错,也就是说大家共建了和谐计算机系统。 这样,虽说是多此一举,但是考虑到64位地址,使用四层转换架构的CPU,我们就不再把中间两个设为0了,这样,软件与硬件再次和谐——抽像就是强大呀!!! 例如,一个逻辑地址已经被转换成了线性地址,0x08147258,换成二制进,也就是: 0000100000 0101000111 001001011000 内核对这个地址进行划分 PGD = 0000100000 PUD = 0 PMD = 0 PT = 0101000111 offset = 001001011000 现在来理解Linux针对硬件的花招,因为硬件根本看不到所谓PUD,PMD,所以,本质上要求PGD索引,直接就对应了PT的地址。而不是再到PUD和PMD中去查数组(虽然它们两个在线性地址中,长度为0,2^0 =1,也就是说,它们都是有一个数组元素的数组),那么,内核如何合理安排地址呢?从软件的角度上来讲,因为它的项只有一个,32位,刚好可以存放与PGD中长度一样的地址指针。那么所谓先到PUD,到到PMD中做映射转换,就变成了保持原值不变,一一转手就可以了。这样,就实现了“逻辑上指向一个PUD,再指向一个PDM,但在物理上是直接指向相应的PT的这个抽像,因为硬件根本不知道有PUD、PMD这个东西”。 然后交给硬件,硬件对这个地址进行划分,看到的是:页目录 = 0000100000 PT = 0101000111 offset = 001001011000 嗯,先根据0000100000(32),在页目录数组中索引,找到其元素中的地址,取其高20位,找到页表的地址,页表的地址是由内核动态分配的,接着,再加一个offset,就是最终的物理地址了

2017-08-01 16:21:54 Davincdada 阅读数 786
在深入学习Linux内核源代码之前,需要先对Linux运行的硬件基础有个大概的认识,主要包括CPU中的寄存器和磁盘。
1.i386寄存器和系统指令

在Linux系统中使用的主要包括i386寄存器中的16位标志寄存器,4个内存管理寄存器和4个控制寄存器及调试寄存器。
(1) 标志寄存器
8086CPU中一种特殊的寄存器,用来存储CPU的状态或者指令执行后的结果,控制CPU的工作方式。
(2) 内存管理寄存器
处理器提供了4个内存管理寄存器(GDTR, IDTR, LDTR and TR),用于指定内存分段管理所用系统表的基地址,处理器为致谢寄存器的加载和保护提供了特定指令。
(3) 控制寄存器 
控制寄存器(CR0~CR3)用于控制和确定处理器的操作模式以及党庆执行任务的特性。
(4) 调试寄存器
Intel 80386以上的CPU提供了调试寄存器,用来调试软件。386和486包括留个32位寄存器:Dr0~Dr3, Dr6, Dr7 .

2.总线 Bus

(1) 数据总线:是计算机中各个组成部件间进行数据传输时的公共通道;“内数据总线宽度”是指CPU芯片内部数据传送的宽度;“外数据总线宽度”是指CPU与外部交换数据时的数据宽度。显然,数据总线位数越多,数据交换的速度就越快。
(2) 地址总线:是載对存储器或I/O端口进行访问时,传送由CPU提供的要访问的存储单元或I/O端口的地址信息的总线,其宽度决定了处理器能直接访问的主存容量的大小。

3.三级存储器组织结构

现在的微型计算机系统采用下图的三级存储器组织结构,即缓冲存储器Cache、主存、和外存。高速缓冲存储器Cache的使用,大大减少了CPU读取指令和操作数所需的时间,使CPU的执行速度显著提高。

在80X86CPU的发展过程中,存储器的管理机制发生了较大的变化。8086/8088CPU对存储器的管理采用分段的实方式;80286CPU除了可在实方式下工作,还可以在保护模式下工作;而80386CPU之后的处理器则具有三种工作方式:实方式、保护方式和虚拟8086方式。

三种工作方式的转换如图:
三级存储器组织结构

三级存储器结构系统

OK,上面简单的介绍了Linux运行的硬件基础,这些不是重点,重点是后面要说的分段、分页机制。我也是边学边记,不足之处请不吝赐教~

1.问题来了,分段到时是怎么回事?有何用?

实模式的诞生(16位处理器及寻址

在8086处理器诞生之前,内存寻址方式就是直接访问物理地址。8086处理器为了寻址1M的内存空间,把地址总线扩展到了20位。但是,一个尴尬的问题出现了,ALU(算术逻辑单元)的宽度只有16位,也就是说,ALU不能计算20位的地址。为了解决这个问题,就引入了分段机制。

为了支持分段,8086处理器设置了四个段寄存器:CS, DS, SS, ES. 每个段寄存器都是16位的,同时访问内存的指令中的地址也是16位的。但是,在送入地址总线之前,CPU先把它与某个段寄存器内的值相加。这里要注意:段寄存器的值对应于20位地址总线的中的高16位,所以相加时实际上是内存总线中的高12位与段寄存器中的16位相加,而低4位保留不变,这样就形成一个20位的实际地址,也就实现了从16位内存地址到20位实际地址的转换,或者叫“映射”。

保护模式的诞生(32位处理器及寻址)

* 80286处理器的地址总线为24位,寻址空间达16M,同时引入了保护模式(内存段的访问受到限制)
* 80386处理器是一个32位处理器,ALU和地址总线都是32位的,寻址空间达 4G。也就是说它可以不通过分段机制,直接访问4G的内存空间。虽然它是新时代的小王子,超越它的无数前辈,然而,它需要背负家族的使命–兼容前代的处理器。也就是说,它必须支持实模式和保护模式。所以,80386在段寄存器的基础上构筑保护模式,并且保留16位的段寄存器。
* 从80386之后的处理器,架构基本相似,统称为IA32(32 Bit Intel Architecture)。

                                    从80386之后的处理器统称为IA32

2.IA32的内存寻址机制

寻址硬件

在 8086 的实模式下,把某一段寄存器左移4位,然后与地址ADDR相加后被直接送到内存总线上,这个相加后的地址就是内存单元的物理地址,而程序中的这个地址就叫逻辑地址(或叫虚地址)。在IA32的保护模式下,这个逻辑地址不是被直接送到内存总线而是被送到内存管理单元(MMU)。MMU由一个或一组芯片组成,其功能是把逻辑地址映射为物理地址,即进行地址转换,如图所示

MMU

IA32的三种地址

* 逻辑地址:
机器语言指令仍用这种地址指定一个操作数的地址或一条指令的地址。 这种寻址方式在Intel的分段结构中表现得尤为具体,它使得MS-DOS或Windows程序员把程序分为若干段。每个逻辑地址都由一个段和偏移量组成。
* 线性地址:
线性地址是一个32位的无符号整数,可以表达高达232(4GB)的地址。通常用16进制表示线性地址,其取值范围为0x00000000~0xFFFFFFFF.
* 物理地址:
也就是内存单元的实际地址,用于芯片级内存单元寻址。 物理地址也由32位无符号整数表示。

MMU地址转化过程

MMU是一种硬件电路,它包含两个部件,一个是分段部件,一个是分页部件,在此,我们把它们分别叫做分段机制和分页机制,以利于从逻辑的角度来理解硬件的实现机制。分段机制把一个逻辑地址转换为线性地址;接着,分页机制把一个线性地址转换为物理地址。

MMU_translater

IA32的段寄存器

IA32中有六个16位段寄存器:CS, DS, SS, ES,FS, GS.跟8086的段寄存器不同的是,这些寄存器存放的不再是某个段的基地址,而是某个段的选择符(Selector)

分段机制的实现

段是虚拟地址空间的基本单位,分段机制必须把虚拟地址空间的一个地址转换为线性地址空间的一个线性地址。

为了实现这种映射,仅仅用段寄存器来确定一个基地址是不够的,还得描述段的长度,段的属性等。这就是段描述符:

段的基地址(Base Address):在线性地址空间中段的起始地址。
段的界限(Limit):在虚拟地址空间中,段内可以使用的最大偏移量。
段的保护属性(Attribute):表示段的特性。例如,该段是否可被读出或写入,或者该段是否作为一个程序来执行,以及段的特权级等等。

多个段描述符组成的表称为段描述符表。

段描述符表

各种各样的用户描述符和系统描述符,都放在对应的全局描述符表、局部描述符表和中断描述符表中。描述符表(即段表)定义了IA32系统的所有段的情况。所有的描述符表本身都占据一个字节为8的倍数的存储器空间,空间大小在8个字节(至少含一个描述符)到64K字节(至多含8K)个描述符之间。

1.全局描述符表(GDT)
全局描述符表GDT(Global Descriptor Table),除了任务门,中断门和陷阱门描述符外,包含着系统中所有任务都共用的那些段的描述符。 它的第一个8字节位置没有使用。

2.中断描述符表IDT(Interrupt Descriptor Table)
中断描述符表IDT(Interrupt Descriptor Table),包含256个门描述符。IDT中只能包含任务门、中断门和陷阱门描述符,虽然IDT表最长也可以为64K字节,但只能存取2K字节以内的描述符,即256个描述符,这个数字是为了和8086保持兼容。

3.局部描述符表(LDT)
局部描述符表LDT(local Descriptor Table),包含了与一个给定任务有关的描述符,每个任务各自有一个的LDT。 有了LDT,就可以使给定任务的代码、 数据与别的任务相隔离。每一个任务的局部描述符表LDT本身也用一个描述符来表示,称为LDT描述符,它包含了有关局部描述符表的信息,被放在全局描述符表GDT中。

总结

IA32的内存寻址机制完成从逻辑地址–线性地址–物理地址的转换。其中,逻辑地址的段寄存器中的值提供段描述符,然后从段描述符中得到段基址和段界限,然后加上逻辑地址的偏移量,就得到了线性地址,线性地址通过分页机制得到物理地址。
首先,我们要明确,分段机制是IA32提供的寻址方式,这是硬件层面的。就是说,不管你是windows还是linux,只要使用IA32的CPU访问内存,都要经过MMU的转换流程才能得到物理地址,也就是说必须经过逻辑地址–线性地址–物理地址的转换。

Linux中分段的实现

前面说了那么多关于分段机制的实现,其实,对于Linux来说并没有什么卵用。因为Linux基本不使用分段机制,或者说,Linux中的分段机制只是为了兼容IA32的硬件而设计的。

Intel微处理器的段机制是从8086开始提出的, 那时引入的段机制解决了从CPU内部16位地址到20位实地址的转换。为了保持这种兼容性,386仍然使用段机制,但比以前复杂得多。因此,Linux内核的设计并没有全部采用Intel所提供的段方案,仅仅有限度地使用了一下分段机制。这不仅简化了Linux内核的设计,而且为把Linux移植到其他平台创造了条件,因为很多RISC处理器并不支持段机制。但是,对段机制相关知识的了解是进入Linux内核的必经之路。

从Linux2.2开始,Linux让所有的进程(或叫任务)都使用相同的逻辑地址空间,因此就没有必要使用局部描述符表LDT。但内核中也用到LDT,那只是在VM86模式中运行Wine,因为就是说在Linux上模拟运行Winodws软件或DOS软件的程序时才使用。

在 IA32 上任意给出的地址都是一个虚拟地址,即任意一个地址都是通过“选择符:偏移量”的方式给出的,这是段机制存访问模式的基本特点。所以在IA32上设计操作系统时无法回避使用段机制。一个虚拟地址最终会通过“段基地址+偏移量”的方式转化为一个线性地址。 但是,由于绝大多数硬件平台都不支持段机制,只支持分页机制,所以为了让 Linux 具有更好的可移性,我们需要去掉段机制而只使用分页机制。但不幸的是,IA32规定段机制是不可禁止的,因此不可能绕过它直接给出线性地址空间的地址。

怎么办呢?Linux的设计人员干脆让段的基地址为0,而段的界限为4GB,这时任意给出一个偏移量,则等式为“0+偏移量=线性地址”,也就是说“偏移量=线性地址”。另外由于段机制规定“偏移量<4GB”,所以偏移量的范围为0H~FFFFFFFFH,这恰好是线性地址空间范围,也就是说虚拟地址直接映射到了线性地址,我们以后所提到的虚拟地址和线性地址指的也就是同一地址。看来,Linux在没有回避段机制的情况下巧妙地把段机制给绕过去了。

另外,由于IA32段机制还规定,必须为代码段和数据段创建不同的段,所以Linux必须为代码段和数据段分别创建一个基地址为0,段界限为4GB的段描述符。不仅如此,由于Linux内核运行在特权级0,而用户程序运行在特权级别3,根据IA32段保护机制规定,特权级3的程序是无法访问特权级为0的段的,所以Linux必须为内核用户程序分别创建其代码段和数据段。这就意味着Linux必须创建4个段描述符——特权级0的代码段和数据段,特权级3的代码段和数据段。

总结

分段机制是IA32架构CPU的特色,并不是操作系统寻址方式的必然选择。Linux为了跨平台,巧妙的绕开段机制,主要使用分页机制来寻址。
参考资料

<<Linux内核源码注释>>
<<深入分析Linux内核源码>>
以及"benpaobagzb"的博客
2015-11-02 10:43:43 jiurl 阅读数 201
作者:JIURL
日期:2015年10月30日

分页机制

    Linux(x64CPU)使用基于分页机制的虚拟内存。每个进程有256TB(48位)的虚拟地址空间。基于分页机制,这256TB地址空间的一些部分被映射了物理内存,一些部分什么也没有映射。程序中使用的都是256TB地址空间中的虚拟地址。而访问物理内存,需要使用物理地址。

   物理地址(physical address):放在寻址总线上的地址。放在寻址总线上,如果是读,电路根据这个地址每位的值就将相应地址的物理内存中的数据放到数据总线中传输。如果是写,电路根据这个地址每位的值就将相应地址的物理内存中放入数据总线上的内容。物理内存是以字节(8位)为单位编址与寻址的。

    虚拟地址(virtual address): 256TB虚拟地址空间中的地址,程序中使用的都是虚拟地址。

   如果CPU相关寄存器中的分页标志位被设置,那么执行内存操作的机器指令时,CPU会自动根据相应转换结构(PML4,PDPT,PD,PT)中的信息,把虚拟地址转换成物理地址,完成该指令。比如"mov eax,dword ptr[13FF3904Ch]",就是把地址13FF3904Ch处的值赋给寄存器的汇编代码,13FF3904Ch这个地址就是虚拟地址。CPU在执行这行代码时,发现寄存器中的分页标志位已经被设定,就自动完成虚拟地址到物理地址的转换,使用物理地址取出值,完成指令。对于x86-64CPU 来说,寄存器CR0的第31位是分页标志位,为1表示使用分页,为0表示不使用分页。对于初始化之后的 Linux 我们通过qemu monitor 观察 CR0,发现第31位的值为1。表明Linux是使用分页的。有多个寄存器中的控制位与分页有关,具体细节可以参考Intel软件开发手册。

    x86-64 CPU的实际虚拟地址大小为48位(64位中的低48位,高16位不用于地址转换),可寻址256TB地址空间。x86-64 CPU的实际物理地址大小,不同的CPU不相同,但最大不超过52位。可以通过机器指令cpuid来查询该CPU的实际虚拟地址长度和实际物理地址长度。例如,我查询了两台计算机,虚拟地址的大小两台计算机都为0x30(16进制30就是十进制48),物理地址的大小一台计算机为0x24(16进制24就是十进制36,36位可寻址64GB),另一台计算机为0x27(16进制27就是十进制39,39位可寻址512GB)。为了后面的叙述方便,我们把CPU实际物理地址的位数叫做M,比如,对于物理地址大小为36位的CPU,M就是36。

   使用了分页机制之后,256TB的地址空间被分成了固定大小的页(有三种大小,4KB,2MB,1GB),每一页或者被映射到物理内存,或者没有映射任何东西。对于一般程序来说,256TB的地址空间,只有一小部分(沧海一粟)映射了物理内存,大片大片的部分是没有映射任何东西。物理内存也被分页,来映射地址空间。对于x86-64,页的大小有三种,分别是4KB,2MB,1GB。CPU用来把虚拟地址转换成物理地址的信息存放在叫做"page map level 4"," page directory pointer","pagedirectory"(页目录),"pagetable"(页表)的结构里。每个进程都有自己的一套PML4,PDPT,PD,PT结构。一个进程中的虚拟地址,最多需要四级转换,来得到对应的物理地址。

   物理内存分页,一个物理页的大小为4KB,第0个物理页从物理地址 0x0000000000000000处开始,大小为4KB(0x1000B),第1个物理页从物理地址 0x0000000000001000 处开始。第2页从物理地址0x0000000000002000处开始。由于页的大小是4KB,所以只需要64位的地址中的M-12bit(低12bit之后)来寻址物理页。

    "pagetable"(页表),一个页表的大小为4KB,放在一个物理页中。由512个8字节的PTE(页表项)组成。页表项的大小为8个字节(64位),所以一个页表中有512个页表项。页表中的每一项的内容(每项8个字节,64位)低12位之后的M-12位用来放一个物理页的物理地址,低12bit放着一些标志。

    "pagedirectory"(页目录),一个页目录大小为4KB,放在一个物理页中。由512个8字节的PDE(页目录项)组成。页目录项的大小为8个字节(64位),所以一个页目录中有512个页目录项。页目录中的每一项的内容(每项8个字节,64位)低12位之后的M-12位用来放一个页表(页表放在一个物理页中)的物理地址,低12bit放着一些标志。

    "pagedirectory pointer"表,一个page directorypointer表大小为4KB,放在一个物理页中。由512个8字节的PDPTE项组成。PDPTE项的大小为8个字节(64位),所以一个pagedirectory pointer表中有512个PDPTE。page directorypointer表中的每一项的内容(每项8个字节,64位)低12位之后的M-12位用来放一个页目录(页目录放在一个物理页中)的物理地址,低12bit放着一些标志。

    "page maplevel 4"表,一个page map level4表大小为4KB,放在一个物理页中。由512个8字节的PML4E项组成。PML4E项的大小为8个字节(64位),所以一个pagemap level 4表中有512个PML4E。page map level4表中的每一项的内容(每项8个字节,64位)低12位之后的M-12位用来放一个page directory pointer表(pagedirectory pointer表放在一个物理页中)的物理地址,低12bit放着一些标志。

   对于x86-64系统,"page map level 4"表的物理地址放在CPU的CR3寄存器中。

   CPU把虚拟地址转换成物理地址:
   一个虚拟地址,大小8个字节(64位,实际只使用低48位),包含着找到物理地址的信息,分为5个部分:第39位到第47位这9位(最高9位)是"pagemap level 4"表中的索引,第30位到第38位这9位是"page directorypointer"表中的索引,第21位到第29位这9位是页目录中的索引,第12位到第20位这9位是页表中的索引,第0位到第11位这12位(低12位)是页内偏移。对于一个要转换成物理地址的虚拟地址,CPU首先根据CR3中的值,找到"pagemap level4"表所在的物理页,然后根据虚拟地址的第39位到第47位这9位(最高9位)的值作为索引,找到相应的PML4E项,PML4E项中有这个虚拟地址所对应的"pagedirectory pointer"表的物理地址。有了"page directorypointer"表的物理地址,根据虚拟地址的第30位到第38位这9位的值作为索引,找到该"page directorypointer"表中相应的PDPTE项,PDPTE项中有这个虚拟地址所对应的页目录的物理地址。有了页目录的物理地址,根据虚拟地址的第21位到第29位这9位的值作为索引,找到该页目录中相应的页目录项,页目录项中有这个虚拟地址所对应的页表的物理地址。有了页表的物理地址,根据虚拟地址的第12位到第20位这9位的值作为索引,找到该页表中相应的页表项,页表项中有这个虚拟地址所对应的物理页的物理地址。最后用虚拟地址的最低12位,也就是页内偏移,加上这个物理页的物理地址,就得到了该虚拟地址所对应的物理地址。

    一个"page maplevel 4"表有512项,虚拟地址从48位向低走的9位刚好可以索引512项(2的9次方等于512),一个"pagedirectorypointer"表有512项,虚拟地址接下来的9位刚好索引512项。一个页目录有512项,虚拟地址接下来的9位刚好索引512项。一个页表有512项,虚拟地址接下来的9位刚好索引512项。虚拟地址最低的12位(2的12次方等于4096),作为页内偏移,刚好可以索引4KB,也就是一个物理页中的每个字节。

   一个虚拟地址转换成物理地址的计算过程就是,处理器通过CR3找到当前"page map level4"表所在物理页,取虚拟地址从48位向低走的9位,然后把这9位右移3位(因为每个PML4E项8个字节长,右移3位相当于乘8)得到在该页中的地址,取出该地址处的PML4E(8个字节),就找到了该虚拟地址对应"pagedirectorypointer"表所在物理页,然后同样方法依次找出该虚拟地址对应的页目录所在物理页,该虚拟地址对应的页表所在物理页,该虚拟地址对应的物理页的物理地址,最后将虚拟地址对应的物理页的物理地址加上12位的页内偏移得到了物理地址。

   48位的一个指针,可以寻址范围0x000000000000-0xFFFFFFFFFFFF,256TB大小。也就是说一个48位的指针可以寻址整个256TB地址空间的每一个字节。
一个页表项负责4KB的地址空间和物理内存的映射,一个页表512项,也就是负责512*4KB=2MB的地址空间的映射。
一个页目录项,对应一个页表。一个页目录有512项,也就对应着512个页表,每个页表负责2MB地址空间的映射,512个页表负责512*2MB=1GB的地址空间映射。
一个PDPTE项,对应一个页目录。一个"page directorypointer"表有512项,也就对应着512个页目录,每个页目录负责1GB地址空间的映射。512个页目录负责512*1GB=512GB的地址空间映射。
一个"page map level 4"表项,对应一个"page directory pointer"表。一个"page maplevel 4"表有512项,也就对应着512个"page directory pointer"表,每个"page directorypointer"表负责512GB地址空间的映射。512个"page directorypointer"表负责512*512GB=256TB的地址空间映射。
一个进程有一个"page map level4"表。所以以页为单位,一套PML4,PDPT,PD,PT结构可以保证256TB的地址空间中的每页和物理内存的映射。

   一个虚拟地址在转换过程中,如果发现对应的PDPTE项的PS位为0则继续之后的转换步骤,如果发现对应的PDPTE项的PS位为1,则本PDPTE项中的物理地址,就是该虚拟地址对应的一个大小为1GB物理页的地址,该虚拟地址之后的30位为这个1GB物理页的页内偏移,就可以得到该虚拟地址对应的物理地址。

   一个虚拟地址在转换过程中,如果发现对应的页目录项的PS位为0则继续之后的转换步骤,如果发现对应的页目录项的PS位为1,则本页目录项中的物理地址,就是该虚拟地址对应的一个大小为2MB物理页的地址,该虚拟地址之后的21位为这个2MB物理页的页内偏移,就可以得到该虚拟地址对应的物理地址。

   虚拟地址48位,寻址256TB。48位地址:9bit+9bit+9bit+9bit+12bit。
每一级表的大小都为4KB。表项大小8B(64位),所以每个表512项。
4级转换:CR3-> "page map level 4"表-> "page directorypointer"表-> 页目录-> 页表-> 得到物理地址。
一个PTE项对应4KB地址范围。一张PT表对应512*4KB=2MB地址范围。
一个PDE项对应2MB地址范围。一张PD表对应512*2MB=1GB地址范围。
一个PDPTE项对应1GB地址范围。一张PDPT表对应512*1GB=512GB地址范围。
一个PML4E项对应512GB地址范围。一张PML4表对应512*512GB=256TB地址范围。
页大小有3种,4KB,2MB,1GB。

   每个进程都有自己的256TB地址空间,"0000000000000000 - 00007fffffffffff" 和"ffff800000000000 - ffffffffffffffff" 。因为CPU会忽略高16位,所以相当于"000000000000 - 7fffffffffff" 和 "800000000000 -ffffffffffff"。
因为CPU有特殊规定,被忽略的高16位的每一位必须等于第47位的值(并会做检查,不符合规定将引发异常)。"000000000000 -7fffffffffff"的第47位是0,所以高16位也必须都为0。"800000000000 -ffffffffffff"的第47位是1,所以高16位也必须都为1。
每个进程都有自己的256TB地址空间,通过每个进程自己的一套PML4,PDPT,PD,PT结构来实现。由于每个进程有自己的一套PML4,PDPT,PD,PT结构,所以每个进程的地址空间映射的物理内存是不一样的。两个进程的同一个虚拟地址处(如果都有物理内存映射)的值一般是不同的,因为他们往往对应不同的物理页。

   256TB地址空间中低128TB,0x000000000000-0x7fffffffffff是用户地址空间,256TB地址空间中高128TB,0x800000000000-0xffffffffffff是系统地址空间。访问系统地址空间需要程序有ring0的权限。

未完待续...

微博:http://weibo.com/ddqqppb
邮箱:thejiurl@163.com
QQ:6291898

欢迎交流。



没有更多推荐了,返回首页