Linux内存管理的基本框架(Linux内核源代码情景分析读书笔记连载)

1 Linux内核的映射机制设计成三层,在页面目录和页面表中间增设了一层“中间目录”。页面目录称为PGD,中间目录称为PMD,页面目录称为PT,PT中的表项称为PTE。PGD、PMD、PT三者均为数组。相应的,在逻辑上把线性地址从高位到低位划分成4个位段,各占若干位,分别用作在目录PGD中的下标、中间目录PMD中的下标、页面表PT中的下标以及物理页面内的偏移。

对于CPU发出的线性地址,虚拟的Linux内存管理单元分如下四步完成从线性地址到物理地址的映射:

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

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

(3)用线性地址的第三个位段作为下标在PT中找到相应的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中的一段定义:

#if CONFIG_X86_PAE
# include <asm/pgtable-3level.h>
#else
# include <asm/pgtable-2level.h>
#endif

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

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

/*
 * traditional i386 two-level paging structure:
 */


#define PGDIR_SHIFT 22
#define PTRS_PER_PGD 1024


/*
 * the i386 is two-level, so we don't really have any
 * PMD directory physically.
 */
#define PMD_SHIFT 22
#define PTRS_PER_PMD 1


#define PTRS_PER_PTE 1024

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

#define PGDIR_SIZE (1UL << PGDIR_SHIFT)

也就是说,PGD中的每一个表项所代表的空间大小是1x2exp(22)(2的22次方)。同时,pgtable-2level.h中又定义了PTRS_PER_PGD,也就是每个PGD表中指针的个数为1024。显然,这是与线性地址中PGD位段的长度(10位)相符的。这两个常数值的定义完全是针对i386CPU及其MMU的,因为非PAE模式下的i386 MMU用线性地址中的最高10位作为目录中的下标,而目录的大小为1024.不过,在32位的系统中每个指针大小为4个字节,所以PGD表的大小为4KB.

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

这样,上述的4步映射过程对于内核和i386 MMU就成为:

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

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

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

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

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

具体的映射因空间的性质而异,但是后面将会看到,其段式映射基地址总是0,所以线性地址与虚拟地址总是一致的。

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

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

/*
 * This handles the memory map.. We could make this a config
 * option, but too many people screw it up, and too few need
 * it.
 *
 * A __PAGE_OFFSET of 0xC0000000 means that the kernel has
 * a virtual address space of one gigabyte, which limits the
 * amount of physical memory you can use to about 950MB. 
 *
 * If you want more physical memory than this then see the CONFIG_HIGHMEM4G
 * and CONFIG_HIGHMEM64G options in the kernel configuration.
 */


#define __PAGE_OFFSET (0xC0000000):

..........

#define PAGE_OFFSET((unsigned long)__PAGE_OFFSET)
#define __pa(x) ((unsigned long)(x)-PAGE_OFFSET)
#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))
#define virt_to_page(kaddr) (mem_map + (__pa(kaddr) >> PAGE_SHIFT))
#define VALID_PAGE(page) ((page - mem_map) < max_mapnr)

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

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

/*
 * User space process size: 3GB (default).
 */
#define TASK_SIZE (PAGE_OFFSET)

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

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

/* Re-load page tables */
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个描述项。除一些系统的开销以外,尚有8180个表项可供使用,所以理论上系统中最大的进程数量是4090。








  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值