5.3.6 虚拟地址、线性地址和物理地址之间的关系
内核代码和数据的地址
对于linux 0.11内核代码和数据来说,在head.s程序的初始化操作中已经把内核代码段和数据段都设置成长度为16M的段。在线性地址空间中这两个段的范围重叠,都是从线性地址0开始到地址0XFFFFFF共16M地址范围。在该范围中含有内核所有的代码、内核段表(GDT,IDT,TSS)、页目录表和内核的二级页表、内核局部数据和内核临时堆栈(将被用作第一个任务即任务0的用户堆栈)。其页目录表和二级页表已设置成把0-16M的线性地址空间一一对应到物理地址上,占用了4个目录项即4个二级页表。因此对于内核代码和数据来说,我们可以直接把它看作是物理内存中的地址。如下图:
因此,默认情况下Linux 0.11内核最多可管理16MB的物理内存,共有4096个物理页面,每个页面4KB。通过上述分析可以看出:
1.内核代码段和数据段区域在线性地址空间和物理地址空间中是一样的,这样设置可以大大简化内核的初始化操作
2.GDT和IDT在内核数据段中,因此他们的线性地址也同样等同于他们的物理地址。在实模式下setup.s程序初始化操作中,我们曾经设置过临时的GDT和IDT,这是进入保护模式之前必须设置的。由于这两个表当时处于物理内存大约0x90200处,而进入保护模式之后内核系统模块处于物理内存0开始位置,并且0x90200处的空间也被挪作他用(用于高速缓冲),因此在进入保护模式后,在运行的第一个程序head.s中要重新设置这两个表。即设置GDTR和IDTR指向新的GDT和IDT,描述符也需要重新加载。由于开启分页机制时这两个表的位置没有变动,因此无需再重新建立或移动表位置。
3.除任务0以外,所有其他任务所需要的物理页面与线性地址中不同或部分不同,因此内核需要动态的在主内存区中为他们作映射操作,动态的建立页目录项和页表项。虽然任务的代码和数据也在内核中,但由于它需要另行分配获得内存,因此也需要自己的映射表项。
任务0的地址对应关系
任务0是系统中第一个人工启动的任务。他的代码段和数据段长度被设置为640kb。该任务的代码和数据直接包含在内核代码和数据中,是从地址0开始的640KB的内容,因此它可以直接使用内核代码已经设置好的页目录和页表进行分页地址变换,同样,它的代码和数据在线性地址空间也是重叠的,对应的任务状态段TSS0也是手工预设值好的,并且位于任务0数据结构中,参见sched.h第113行开始的内容。TSS0段位于内核sched.c程序的代码中,长度为104字节,具体位置参加图5-23中"任务0结构信息"所示。三个位置的对应关系如图5-15所示:
由于任务0直接包含在内核中,不再需要为其重新分配内存页,它运行时需要的内核态堆栈和用户态堆栈空间也都在内核代码区中,并且由于在内核初始化时(head.s)这些内核页面在页表项中的属性都已经被设置成了0b111,即对应页面用户可读写且存在,因此用户堆栈user_stack[]空间虽然在内核空间中,但任务0仍能对其进行读写操作。
任务1的地址对应关系
与任务0类似,任务1也是一个特殊任务。它的代码也在内核代码区域中。与任务0不同的是在线性地址空间中系统在使用fork()创建任务1(init进程)时为存放任务1的二级页表而在内存中申请了一页内存区域来存放,并复制了父进程的页目录和二级页表项。因此任务1拥有自己的页目录和页表表项,它把任务1占用的线性空间范围64M-128M(实际上是64M-64M+640KB)也同样映射到物理地址0-640KB处。此时任务1的长度也是640KB,并且其代码段和数据段相重叠,只占用一个页目录项和一个二级页表。另外,系统还会为任务1在主内存区域中申请一页内存来存放它的任务数据结构和用作任务1的内核堆栈空间。任务数据结构中包括任务1的TSS段结构信息,如下图:
任务1的用户态堆栈空间将直接共享使用处于内核代码和数据区域(线性地址0-640KB)中任务0的用户态堆栈空间user_stack[](参见kernek/sched.c,第67-72行),因此这个堆栈需要在任务1实际使用时保持“干净”,以确保被复制用于任务1的堆栈不含有无用数据,在刚开始创建任务1时,任务0的用户态堆栈user_stack[]与任务1共享使用,但当任务1 开始运行时,由于任务1映射到user_stack[]处的页表被设置成只读,使得任务1在执行堆栈操作时将会引起写页面异常,从而由内核另行分配主内存区页面作为堆栈空间使用。
其他任务的地址对应关系
对于被创建的从任务2开始的其他任务,它们的父进程都是init(任务1)进程,我们已经知道,在linux 0.11系统中共可以有64个进程同时存在,下面以任务2为例来说明其他任何任务队地址空间的使用情况。
从任务2开始如果任务号以nr表示,那么任务nr在线性地址空间中的起始位置被设定在nr*64M处,例如任务2的开始位置=nr*64M=2*64M=128M。任务代码段和数据段的最大长度被设置为64M,因此任务2占有的线性地址空间范围是128M-192M,共占用64M/4M=16个页目录项,虚拟空间中任务代码段和数据段都被映射到线性地址空间中相同的范围,因此它们也会完全重叠。图5-17显示出了任务2的代码段和数据段在三种地址空间中的对应关系。
在任务2被创建出来之后,将在其中运行execve()函数来执行shell程序,当内核通过复制任务1刚创建任务2时,除了占用线性地址空间范围不同以外(128M-128M+640K),此时任务2的代码和数据在三种地址空间中的对应关系和任务1的类似。当任务2的代码(init())调用execve()系统调用开始加载并执行shell()程序时,该系统调用会释放掉从任务1复制的页目录和页表表项及相应的内存页面,然后为新的执行程序shell重新设置页目录和页表表项。在执行execve()函数时,系统虽然为任务2分配了64M的空间范围,但是内核并不会立刻为其分配和映射物理页面,只有当任务2开始执行时由于发生缺页而引起异常时才会由内存管理程序为其在主内存区中分配并映射一页物理内存到其线性地址空间中,这种分配和映射物理内存的方法称为需求加载(load on demand).