分类:
2005-05-20 16:34:31
1. Intel的CPU进行段页式管理的原始思想
Intel
的8086是“实地址模式”,就象改革开放后赤裸裸的爱情一样没有没有对内核实现任何的保护机制。针对8086的这种缺陷,Intel从80286开始实
现其“保护模式”(Protected
Mode,但是早期的80286只能从实地址模式转入保护模式,却不能从保护模式转回实地址)。不久32位的80386CPU也开发成功,IT业的辛亥革
命就这样发生了。这样从8086/8088到80386就完成了一次比较原始的16位CPU到现代32位CPU的飞跃,而80286则变成了这次革命的一
个中间步骤。从80386以后,Intel的CPU经历80486、Pentium、PentiumⅡ等等型号,虽然速度上提高了好几倍,功能上也有不少
改进,但无质的改变,所以统称为i386结构。
因为80386是从8086、80286过度而来的,所以必须保证兼容,这样就不得不继续利用以前
的16位段寄存器,而且还不得不在采取段式地址映射来实现其“保护模式”,虽然只用页式管理就能实现更加有效和高效的“保护模式”下的内存地址映射。所以
大家要清楚,现在计算机里的段式管理仅仅是Intel产品兼容的结果,而不是最佳的虚拟内存管理方式。如果有可能,Intel应该进行一次彻底的革新,只
用页式管理就足够了。
1) 段式管理的地址映射
首先自己给出地址偏移量,然后根据以下步骤进行映射
a) 根据指令的性质确定应该使用那一个段寄存器,例如转移指令中的地址在代码段,而取数指令中的地址在数据段。
b) 根据寄存器的内容,以GDTR(LDTR)作为基地址,以DS(ES,CS,SS)作为偏移量,找到相应的“地址段描述结构”,方式为GDTR+DS(CS、ES..)或LDTR+DS(CS、ES…)。(注:地址段描述结构见下面)
c) 从地址段描述结构中得到基地址。
d) 将指令中发出的地址作位位移,与段描述结构中规定的段长度相比,看是否越界。
e) 根据指令的性质和段描述符中的访问权限来确定是否越权
f) 将指令中发出的地址作位位移,与基地址相加而的出实际的“物理地址”
图1
地址段描述结构:
typedef struct
{
unsigned int base24_31 : 8 //基地址的高8位
unsigned int g :1 //表示段长度单位,0表示字节,1表示4K
unsigned int d_b :1
unsigned int unused :1
unsigned int avl :1
unsigned int seg_limit_16_19:4 //段长度高4位
unsigned int p :1
unsigned int dpl :1
unsigned int s :1
unsigned int type :4
unsigned int base_0_23 :24 //基地址的低24位
unsigned int seg_limit_0_15:16 //段长度的低16位
}
图2
2) 页式管理的地址映射
首先要说明的是页式管理完全可以在没有段式管理的基础上实现,但是为了实现保护模式和以前的内存管理模式相兼容,才不得不在段式管理的基础上实现页式管理。
这样由段式管理而映射出的线性地址(没有页式管理前即为“物理地址”)就和以前由段式管理映射出的物理地址含义不同了。线性地址的具体含义如下:
typeded struct
{
unsigned int dir:10; // 表示页面表目录的下标,该目录指向一个页面表
unsigned int page: 10; // 表示页面表的下表,该表项指向一个物理页面
unsigned int offset:12; // 在4K字节物理页面内的偏移量
}线性地址
下面详细说明页式管理的内存映射过程:
a) 在CR3寄存器中取得页面目录的基地址。
b) 以线性地址中的dir项为下标,在目录中取得相应页面表的基地址
c) 以线性地址中的page项为下标,在所得的页面表中取得相应的页面描述项。
d) 将页面描述项中给出的页面基地址与线性地址中的offset项相加得到物理地址。
至此,就可以映射出物理地址了,图示如下:
31 22 21 12 11 0
线性地址格式
CR3
图3
可以看出目录项和页表项中指针的个数都有1024个,而每个都占4byte,所以目录项和页表项的大小都是4K。
如
前所述,目录项中含有一个页面表的指针,而页面表项中则含有指向一个页面其实地址的指针。由于页面表和页面的起始地址都总是4K的边界上,这些指针的低
12位永远是0。这样目录项和页表项中都只要有20位用于表似乎指针就够了,而余下的12位则可以用于控制或其他的目的。
3) 段描述表
整个系统有一个全局段描述表GDT, 每个进程有一个局部段描述表LDT,GDT中要有一个表项指向这个LDT段的起始地址,并说明该段的长度以及其他一些参数。
因
为段寄存器的长度是16,而可用于表示GDT表下表的只有13位,所以GDT长度最长为8192,其中第0、1项永远是0,第2、3项分别表示内核的代码
段和数据段,第4、5项分别表示当前运行进程的代码段和数据段等等,所以还有8180个表项可用,而每个进程有要占GDT表中的两项,所以理论上系统的最
大进程数应该是4090。
但是,实际上,LINUX并没有使用LDT,而只用了GDT,这与INTEL的设计意图不一致了。
注:LDTR(local descriptor table register):局部性的段描述表寄存器
GDTR(global descriptor table register):全局性的段描述表寄存器
CT3:指向当前页面目录的指针。
2. Linux的具体实现
由
于段式管理只是Intel为了兼容才设置的,已经成了内存管理映射的负担,而基于IntelCPU的MMU又必须要进行段式映射,所以在linux下,段
式映射只是一个形式,只是例行公事,其实并没有对映射前虚拟地址进行改变。也就是说,在linux下,MMU对虚拟地址进行段式映射前后,其虚拟地址==
逻辑地址。而真正的映射和“地址保护”是由页式映射来完成的。
下面举例说明。
假定我们写了这么一个程序:
#include
greeting()
{
printf(“hello,world
”);
}
main()
{
greeting();
}
1)段式映射。
假定该程序已经在运行,整个映射机制都已经建立好,并且CPU正在执行的过程中是有CPU的“指令计数器”EIP所指向的,所以在代码段中。
首先我们看内核建立一个进程时,是怎么设置其寄存器的(include/asm-i386/processor.h)
#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)
这里regs->xds是段寄存器DS的影象,其次类推。
再看看USER_CS和USER_DS到底是什么。他们在include/asm-i386/segment.h中定义:
#define __KERNEL_CS 0x10
#define __KERNEL_DS 0x18
#define __USER_CS 0x23
#define __USER_DS 0x2B
下面我们将他们展开成二进制,看起含义:
Index TI RPL
__KERNEL_CS 0x10 0000 0000 0001 0 (2) 0 00(0)
__KERNEL_DS 0x18 0000 0000 0001 1 (3) 0 00(0)
__USER_CS 0x23 0000 0000 0010 0 (4) 0 11(3)
__USER_DS 0x2B 0000 0000 0010 1 (5) 0 11(3)
可以清楚看到他们在GDT中的索引分别是2,3,4,5。
我们可以看看GDT的定义,在arch/i386/kernel/head.S中
/*
* This contains typically 140 quadwords, depending on NR_CPUS.
*
* NOTE! Make sure the gdt descriptor in head.S matches this if you
* change anything.
*/
ENTRY(gdt_table)
.quad 0x0000000000000000 /* NULL descriptor */
.quad 0x0000000000000000 /* not used */
.quad 0x00cf9a000000ffff /* 0x10 kernel 4GB code at 0x00000000 */
.quad 0x00cf92000000ffff /* 0x18 kernel 4GB data at 0x00000000 */
.quad 0x00cffa000000ffff /* 0x23 user 4GB code at 0x00000000 */
.quad 0x00cff2000000ffff /* 0x2b user 4GB data at 0x00000000 */
.quad 0x0000000000000000 /* not used */
.quad 0x0000000000000000 /* not used */
所以其2,3,4,5项的数据分别是:
.quad 0x00cf9a000000ffff /* 0x10 kernel 4GB code at 0x00000000 */
.quad 0x00cf92000000ffff /* 0x18 kernel 4GB data at 0x00000000 */
.quad 0x00cffa000000ffff /* 0x23 user 4GB code at 0x00000000 */
.quad 0x00cff2000000ffff /* 0x2b user 4GB data at 0x00000000 */
展开为:
K_CS:0000 0000 1100 1111 1001 1010 0000 0000 0000 0000 0000 0000 1111 1111 1111 1111
K_DS:0000 0000 1100 1111 1001 0010 0000 0000 0000 0000 0000 0000 1111 1111 1111 1111
U_CS:0000 0000 1100 1111 1111 1010 0000 0000 0000 0000 0000 0000 1111 1111 1111 1111
U_DS:0000 0000 1100 1111 1111 0010 0000 0000 0000 0000 0000 0000 1111 1111 1111 1111
对照图2,我们可以看到四个段描述项的共同处:
a) B0-B15,B16-B33都是0 --------基地址全为0;
b) L0-L15,L16-L19都是1 ---------段的上限全是0xfffff;
c) G位都是1 ----------段长单位都是4KB
d) P位都是1 ----------四个段都在内存
结论:每个段是从9地址开始的4GB的虚拟空间,虚地址到线性地址的映射保持原值不变。因此,讨论和理解LINUX内核的内核的页市映射时,可以直接将线性地址当做虚拟地址,二者完全一致。
2)页式映射。
现在我们来讨论有线性地址到物理地址的页式映射机制。
为此,我们首先来说明系统占用的1G虚拟空间到物理地址的映射
虽然系统空间占据了每个虚拟空间中最高的1G字节,在物理的内存中却总是从最低的地址(0)开始。所以,对于内核来说,其笛子后的映射是很简单的线性映
射,0xC0000000就是两者之间的位移量。因此,在代码中将此位移称为PAGE_OFFSET,而定义于文件page.h中:
#define __PAGE_OFFSET (0xC0000000)
…
#define PAGE_OFFSET ((unsigned long) __PAGE_OFFSET)
#define __pa(x) ((unsigned long)(x)-PAG_OFFSET)
#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))
与段式映射中所有进程全都共用一个GDT不一样,每个进程都有自身的页面目录PGD,指向这个目录的指针保持在每个进程的mm_struct
数据结构中。每当调度一个进程进入运行的时候,内核都要为即将运行的进程设置好控制寄存器CR3,而MMU的硬件则总是从CR3中取得指向当前页面目录的
指针。不过CPU在执行程序时,使用的虚拟地址,而MMU硬件在进行映射时所用的是物理地址。这是在inline函数switch)mm()中完成的可,
其代码见
include/asm-i386/mmu_contest.h。但是我们在此关心的只是其中最关键的一行:
static inline void switch_mm(struct mm_struct *prev, struct mm_struct *next, struct task_struct
*tsk, unsigned cpu)
{
…
asm volatile(“movl %0, %%cr3”: :”r”(__pa(next->pgd)));
…
}
当我们在程序中要转移到地址0X08048568去的时候,进程正在运行中,CR3早已设置好,指向我们这个进程的页面目录了。先将线性地址0X08048568按二进制展开:
0000 1000 0000 0100 1000 0101 0110 1000
可
见,高10位是0000 1000 00, 也就是32,
所以i386CPU就以32位下标去页面目录中找目录项。这个目录项中的高20位指向一个页面表。找到页面表后,CPU再来看线性地址的中间10位,为
0001001000,即十进制的72,于是CPU就以此为下标在已经找到的页表中找到相应的表项。这时这个线性地址的最低12位为0X568,所以如果
目标页面的起始地址为0X740000的话(具体取决于内核中的动态分配),那么greeting ()入口物理地址就是0X740568。