posts - 8, comments - 7, trackbacks - 0, articles - 64

Refer to <<linux 内核源代码情景分析 >> and <<Linux kernel Version:2.4.0>>

Having any problems, send mails to viloner@163.com

Linux 内存管理的基本框架

 

I386CPU 中的页式存储管理的基本思路是:通过页面目录和页面表分两个层次实现从线性地址到物理地址的映射。这种采用两层的方式,只要一个目录项所对应的那部分空间一个空洞,就可以把该目录项设置成“空” , 从而省下了与之对应的页面表( 1024 个页面描述项)。当地址的宽度为 32 位时,两层映射机制比较有效也比较合理。但是,当地址宽度大于 32 位时,两层映射就显得不尽合理,有够有效了。
Linux
内核的设计要考虑到在各种不同的微处理器上的实现,还有考虑到在 64 位的微处理器 ( Alpha) 上的实现,所以不能仅仅针对 i386 结构来设计它的映射机制,而要以只要假象的、虚拟的微处理器和 MMU( 内存管理单元 ) 为基础,设计出一种通用的模式,再把它分别落实到具体的微处理器上。因此, Linux 内核的映射机制被设计成三层,在页面目录和页表之间增设了一层 中间目录 。在代码中,页面目录称为 PGD, 中间目录称为 PMD ,而页表称为 PT PT 的表项称为 PTE Page Table Entry )。 PGD PMD PT 均为数组。相应的,在逻辑上也把线性地址从高到低分为 4 各位段,各占若干位,分别用作目录 PGD 的下标、中间目录 PMD 的下标、页表中的下标以及物理页面内的位移。这样,对于 CPU 发出的线性地址,虚拟的 Linux 内存单元分如下四步完成从线性地址到物理地址的映射:

(1)          用线性地址中最高的那一个位段作为下标在 PGD 中找到相应的表项,该表项指向相应的中间目录 PMD

(2)          用线性地址中的第二个位段作为下标在此 PMD 中找到相应的表项,该表项指向相应的页面表。

(3)          用线性地址中的第三个位段作为下标在页面表中找到相应的表项 PTE ,该表项中存放的就是指向物理页面的指针。

(4)          线性地址中的最后位段为物理页面内的相对位移量,将此位移量与目标物理页面的起始地址相加便得到相应的物理地址。

但是,这个虚拟的映射模型必须落实到具体的 CPU MMU 的物理映射机制。就 i386 微处理器来说, CPU 实际上不是按三层而是按两层的模型来进行地址映射而是按两层的模型进行映射的。这就需要将虚拟的三层映射落实到具体的两层的映射,跳过中间的 PMD 层次。另一方面,从 Pentium Pro 开始, Intel 引入了物理地址扩充功能 PAE ,允许将地址宽度从 32 位提高到 36 位,并且在硬件上支持三层映射模型。这样,在 Pentium Pro 及以后的 CPU 上,只要将 CPU 的内存管理设置成 PAE 模式,就能使虚存的映射变成三层模式。

       那么,具体对于 i386 结构的 CPU Linux 内核是怎样实现这种映射机制的呢?首先看 include/asm-i386/pgtable.h 中定义的一段代码:

106  #if CONFIG_X86_PAE

107  #include <asm/pgtable-3level.h>

108  #else

109  include <asm/pgtable-2level.h>

110  #endif

根据在编译 Linux 内核之前的系统配置 (config) 过程中的选择,编译的时候会把目录 include/asm 符号连接到具体的 CPU 专用的文件目录。对于 i386CPU ,该目录被符号连接到 include/asm-i386 。同时,在配置系统时还有一个选择项是关于 PAE 的,如果所用的 CPU Pentium Pro 或以上时,并且决定采用 36 位地址,则在编译时选择项 CONFIG_X86_PAE 1, 否则为 0. 根据此项选择,编译时从 pgtable-3level.h pgtable-2level.h 中二者选一,前者用于 36 位地址的三层映射,而后者用于 32 位地址的二层映射。这里集中讨论 32 位地址的二层映射。

       文件 pgtable-2level.h 中定义了二层映射中 PGD PMD 的基本结构:

04      /*

05      * traditional i386 two-level paging structure:

06      */

07       

08      #define PGDIR_SHIFT       22

09      #define PTRS_PER_PGD  1024

10       

11      /*

12      * the i386 is two-level,so we don’t really have any

13      *PMD derectory physically.

14      */

15      #define PMD_SHIFT          22

16      #define PTRS_PER_PMD        1

17       

18      #define PTRS_PER_PTE          1024

这里 PGDIR_SHIFT 表示线性地址中 PGD 下标位段的起始位置,文件中将其定义为 22, 也即 bit22( 23 ) 。由于 PGD 是线性地址中最高的位段,所以该位段是从第 23 位到第 32 位,一共是 10 位。在文件 pgtable.h 中定义了另一个常数 PGDIR_SIZE 为:

117#define PGDIR_SIZE       (1UL<<PGDIR_SHIFT)

也就是说, PGD 中的每一个表项所代表的空间 ( 并不是 PGD 本身所占的空间 ) 大小是 1x222 。同时, pgtable-2level.h 中又定义了 PTRS_PER_PGD ,也就是每个 PGD 表中指针的个数为 1024 。显然,这是与线性地址中 PGD 位段的长度 (10 ) 相符的,因为 210 1024

PMD 的定义就很有意思了。 PMD_SHIFT 也定义为 22, PGD_SHIFT 相同,表示 PMD 位段的长度为 0, 一个 PMD 表项所代表的空间与 PGD 表项所代表的空间是一样大的。而 PMD 表中指针的个数 PTRS_PER_PMD 则定义为 1, 表示每个 PMD 表中只有一个表项。同样,这也是针对 i386CPU 及其 MMU 项定义的,因为要将 Linux 逻辑上的三层映射模型落实到 i386 结构物理上的二层映射,就要从线性地址逻辑上的 4 个虚拟位段中把 PMD 抽去,使它长度为 0, 所以逻辑上的 PMD 表的大小就成为 1(20=1)

这样,上述的四步映射过程对于内核 ( 软件 ) i386MMU 就成为:

(1)              内核为 MMU 设置好映射目录 PGD MMU 用于线性地址中最高的那一个位段 (10) 作为下标在 PGD 中找到相应的表项。该表项逻辑上指向一个中间目录 PMD ,但是物理上直接指向相应的页面表, MMU 并不知道 PMD 的存在。

(2)              PMD 只是逻辑上存在,即对内核软件在概念上存在,但是表中只有一个表项,而所谓的映射就是保持原值不变,现在一转手却指向页面表了。

(3)              内核为 MMU 设置好了所有的页面表, MMU 用线性地址中的 PT 位段作为下标在相应页面表中找到相应的表项 PTE ,该表项中存放的就是指向物理页面的指针。

(4)              线性地址中的最后位段为物理页面内的相对位移量, MMU 将此位移量与目标物理页面的起始地址相加便得到相应的物理地址。

这样,逻辑上的三层映射对于 i386CPU MMU 就变成了二层映射,把中间目录 PMD 这一层跳过了,但是软件的结构却还保持着三层映射的框架。

32 位地址意味着 4G 字节的虚存空间, Linux 内核将这 4G 字节的空间分成两部分。将最高的 1G 字节 ( 从虚存地址 0xC0000000 0xFFFFFFFF) ,用于内核本身,称为“系统空间”。而将较低的 3G 字节 ( 从虚地址 0x0 0xBFFFFFFF), 用作各个进程的“用户空间”。这样,理论上每个进程可以使用的用户空间都是 3G 字节。当然,实际的空间大小受到物理存储器大小的限制。虽然各个进程拥有其自己的 3G 字节用户空间,系统空间却由所有的进程共享。每当一个进程通过系统调用进入了内核,该进程就在共享的系统空间中运行,不再有其自己的独立空间。从具体进程的角度来看,则每个进程都拥有了 4G 字节的虚存空间,较低的 3G 字节为自己的用户空间,最高的 1G 字节则与所有的进程以及内核共享的系统空间。

虽然系统空间占据了每个虚存空间中的 1G 字节,在物理的内存中却总是从最低的地址 (0) 开始。所以,对于内核来说,其地址的映射是很简单的线性映射, 0xC0000000 就是两者之间的位移量。因此,在代码中将此位移量称为 PAGE_OFFSET 而定义于文件 page.h 中:

81   define __PAGE_OFFSET       (0xC0000000)

    ……..

114  #define PAGE_OFFSET ((unsigned long) __PAGE_OFFSET)

115  #define __pa(x)                  ((unsigned long) (x)-PAGE_OFFSET)

116  #define __va(x)                  ((void *)((unsigned long) (x)+PAGE_OFFSET))

也就是说,对于系统空间而言,给定一个虚地址 x ,其物理地址是从 x 中减去 PAGE_OFFSET; 相应地,给定一个物理地址 x ,其虚地址是 x+PAGE_OFFSET

同时, PAGE_OFFSET 也代表着用户空间的上限,所以常数 TASK_SIZE 就是通过它定义的,在 processor.h 中:

258  /*

259  * User space process size:3GB(default).

260  */

261  #define TASK_SIZE (PAGE_OFFSET)

这是因为在谈论一个用户进程的大小时,并不包括此进程在系统空间中共享的资源。

当然, CPU 并不是通过这里所说的计算方法进行地址映射的, __pa() 只是为内核代码中当需要知道与一个虚地址对应的物理地址提供方便。例如,在切换进程的时候要将寄存器 CR3 设置成指向新进程的页面目录 PGD ,而该目录的起始地址在内核代码中是虚地址,但 CR3 所需要的是物理地址,这时候就要乃至 __pa() 了。这行语句在文件 mmu_context.h 中:

43      /*Re-load page tables */

44      asm volatile(“movl %0,%%cr3”: :”r” (__pa(next->pgd));

这是一行汇编代码,说的是将 next->pgd ,即下一个进程的页面目录起始地址,对过 __pa() 转换成物理地址 ( 存放在某个寄存器 ) ,然后用 mov 指令将其写入寄存器 CR3 。经过这条指令后, CR3 就指向新进程 next 的页面目录表 PGD 了。

每个进程的局部段描述表 LDT 都作为一个独立的段而存在,在全局段描述表 GDT 中要有一个表项指向这个段的起始地址,并说明该段的长度以及其他一些参数。除此之外,每个进程还有一个 TSS 结构 ( 任务状态段 ) 也是一样。所以,每个进程都要在全局段描述表 GDT 中占据两个表项。那么, GDT 的容量有多大呢?段寄存器中用作 GDT 表下标的位段宽度是 13 位,所以 GDT 中可以有 8192 个描述项。除一些系统的开销 ( 例如 GDT 中的第 2 项和第 3 项分别用于内核的代码段和数据段,第 4 项和第 5 项永远用于当前进程的代码段和数据段,第 1 项永远是 0, 等等 ) 以外,尚有 8180 个表项可供使用,所以理论上系统中最大的进程数量是 4090.

只有注册用户登录后才能发表评论。