为了兼容64位CPU,linux使用三层页式结构管理,分为页面目录PGD,中间目录PMD, 页表PT(其实是3个数组),页表中的项成为PTE
实际在i386架构上,因为硬件只支持2层,所以PMD被忽略了与PGD一样,所以,就是说经过PMD映射后,还是原来PGD的值。
内核为MMU设置好PGD、PT,然后根据线性地址中的相应段作为下标找到最后的PTE。
linux把高1G地址(0xC0000000-0xFFFFFFFF)用于系统空间,低的3G空间为用户空间。
每个进程都有4G虚拟空间,系统空间所有进程共享。对于系统空而言,其物理地址到虚拟地址的映射就是线性映射,
所以把系统的虚拟地址减去0xC0000000就是其物理地址,同理从物理地址可以转换到虚拟地址。
linux内核才用页式存储,但I386硬件是先段式再页式,内核在建立一个进程时都会设置他的段寄存器设置好。
#define start_thread(regs, new_eip, new_esp) do { \
__asm__("movl %0, %%fs; movl %0, %%gs": :"r"(0)); \
set_fs(USER_DS); \
regs->xds = __USER_DS; \
regs->xes = __USER_DS; \
regs->xss = __USER_DS; \
regs->xcs = __USER_CS; \
regs->eip = new_eip; \
regs->esp = new_esp; \
} while (0)
上面的代码说明linux下除CS外,数据段和堆栈段是不分的。
以下是CS和DS段寄存器的值定义:
#define __KERNEL_CS 0x10 // 0000 0000 00010 0 00 ; index=2
#define __KERNEL_DS 0x18 // 0000 0000 00011 0 00 ; index=3
#define __USER_CS 0x23 // 0000 0000 00100 0 11; index=4
#define __USER_DS 0x2b // 0000 0000 00101 0 11; index=5
从上看出,linux只用GDT,而且GDT数组的2、3、4、5位固定存放了一个线性地址。
GDT在arch/i386/kernel/head.S里初始化,其主要内容在运行中不变。
ENTER(gdt_table)
.quad 0x0000000000000000
.quad 0x0000000000000000
.quad 0x00CF9A000000FFFF // 0000 0000 1100 1111 1001 1010 0000 0000 0000000000000000 1111111111111111
.quad 0x00CF92000000FFFF
.quad 0x00CFFA000000FFFF
.quad 0x00CFF2000000FFFF
上面看出 基地址都是0,段上限0xFFFFF,段长单位是4K,段都在内存,有区别的是DPL(0是运行级别0级,3是3级), type 1010是代码段,0010是数据段。(图在38页)
因为基地址是0,所以经过段映射后,地址不变,下面再做页式转换。
页式映射时,每个进程都有各自的页面目录PGD,保存在mm_struct结构体里。
当调度一个进程运行时,内核都先调用switch_mm()函数设置好CR3,MMU会从CR3里取得PGD地址。
switch_mm()函数里有关键一行:
__asm__("movl %0, %%cr3": :"r"(__pa(next->pgd))); // __pa是转换成物理地址
比如call一个地址0x12345678,段式映射后还是这个值,根据10、10、12位的分布,经过页式映射,可以获得物理存储地址,然后去取。