1.内核代码和数据的地址
对于Linux 0.12内核代码和数据来说,在head.s程序的初始化操作中已经把内核代码段和数据段都设置成为长度为16MB的段。在线性地址空间中这两个段的范围重叠,都是从线性地址0开始到地址0xFFFFFF共16MB地址范围。在该范围中含有内核所有的代码、内核段表(GDT、IDT、TSS)、页目录表和内核的二级页表、内核局部数据以及内核临时堆栈(将被用作第1个任务,即任务0的用户堆栈)。其页目录表和二级页表已设置成把0~16MB的线性地址空间一一对应到物理地址上,占用了4个目录项,即4个二级页表。因此对于内核代码或数据的地址来说,我们可以直接把它们看做物理内存中的地址。此时内核的虚拟地址空间、线性地址空间和物理地址空间三者之间的关系可用图5-14来表
![]() |
图5-14 内核代码和数据段在三种地址空间中的关系 |
因此,默认情况下Linux 0.12内核最多可管理16MB的物理内存,共有4096个物理页面(页帧),每个页面4KB。通过上述分析可以看出:①内核代码段和数据段区域在线性地址空间和物理地址空间中是一样的。这样设置可以大大简化内核的初始化操作。②GDT和IDT在内核数据段中,因此它们的线性地址也同样等于它们的物理地址。在实模式下的setup.s程序初始化操作中,我们曾经设置过临时的GDT和IDT,这是进入保护模式之前必须设置的。由于这两个表当时处于物理内存大约0x90200处,而进入保护模式后内核系统模块处于物理内存0开始位置,并且0x90200处的空间将被挪作他用(用于高速缓冲),因此在进入保护模式后,在运行的第1个程序head.s中我们需要重新设置这两个表。即设置GDTR和IDTR指向新的GDT和IDT,描述符也需要重新加载。但由于开启分页机制时这两个表的位置没有变动,因此无须重新建立或移动表位置。③除任务0以外,所有其他任务使用的物理内存页面与线性地址中的页面至少有部分不同,因此内核需要动态地在主内存区中为它们作映射操作,动态地建立页目录项和页表项。虽然任务1的代码和数据也在内核中,但由于其需要另行分配获得内存,因此也需要自己的映射表项。
虽然Linux 0.12默认可管理16MB物理内存,但是系统中并不是一定要有这些物理内存。机器中只要有4MB(甚至2MB)物理内存就完全可以运行Linux 0.12系统了。若机器只有4MB物理内存,那么此时内核4MB~16MB地址范围就会映射到不存在的物理内存地址上。但这并不妨碍系统的运行。因为在初始化时内核内存管理程序会知道机器中所含物理内存量的确切大小,因而不会让CPU分页机制把线性地址页面映射到不存在的4MB~16MB中去。内核中这样的默认设置主要是为了便于系统物理内存的扩展,实际并不会用到不存在的物理内存区域。如果系统有多于16MB的物理内存,由于在init/main.c程序中初始化时限制了对16MB以上内存的使用,并且这里内核也仅映射了0~16MB的内存范围,因此在16MB之上的物理内存将不会用到。
通过在这里为内核增加一些页表,并且对init/main.c程序稍作修改,我们可以对此限制进行扩展。例如在系统中有32MB物理内存的情况下,我们就需要为内核代码和数据段建立8个二级页表项来把32MB的线性地址范围映射到物理内存上。
2.任务0的地址对应关系
任务0是系统中人工启动的第1个任务。它的代码段和数据段长度被设置为640KB。该任务的代码和数据直接包含在内核代码和数据中,是从线性地址0开始的640KB内容,因此它可以直接使用内核代码已经设置好的页目录和页表进行分页地址变换。同样,它的代码和数据段在线性地址空间中也是重叠的。对应的任务状态段TSS0也是手工预设置好的,并且位于任务0数据结构信息中,参见include/linux/sched.h第156行开始的数据。TSS0段位于内核sched.c程序的代码中,长度为104字节,具体位置可参见图5-24中"任务0结构信息"一项所示。3个地址空间中的映射对应关系如图5-15所示。
![]() |
(点击查看大图)图5-15 任务0在3个地址空间中的相互关系 |
由于任务0直接被包含在内核代码中,因此不需要为其再另外分配内存页。它运行时所需要的内核态堆栈和用户态堆栈空间也都在内核代码区中,并且由于在内核初始化时(head.s)这些内核页面在页表项中的属性都已经被设置成了0b111,即对应页面用户可读写并且存在,因此用户堆栈user_stack[]空间虽然在内核空间中,但任务0仍然能对其进行读写操作。
3.任务1的地址对应关系
与任务0类似,任务1也是一个特殊的任务。它的代码也在内核代码区域中。与任务0不同的是在线性地址空间中,系统在使用fork()创建任务1(init进程)时为存放任务1的二级页表而在主内存区申请了一页内存来存放,并复制了父进程(任务0)的页目录和二级页表项。因此任务1有自己的页目录和页表表项,它把任务1占用的线性空间范围64~128MB(实际上是64MB~64MB+640KB)也同样映射到了物理地址0~640KB处。此时任务1的长度也是640KB,并且其代码段和数据段相重叠,只占用一个页目录项和一个二级页表。另外,系统还会为任务1在主内存区域中申请一页内存用来存放它的任务数据结构和用作任务1的内核堆栈空间。任务数据结构(也称进程控制块PCB)信息中包括任务1的TSS段结构信息,如图5-16所示。
![]() |
(点击查看大图)图5-16 任务1在3种地址空间中的关系 |
任务1的用户态堆栈空间将直接共享使用处于内核代码和数据区域(线性地址0~640KB)中任务0的用户态堆栈空间user_stack[](参见kernel/sched.c,第82~87行),因此这个堆栈需要在任务1实际使用之前保持"干净",以确保被复制用于任务1的堆栈不含有无用数据。在刚开始创建任务1时,任务0的用户态堆栈user_stack[]与任务1共享使用,但当任务1开始运行时,由于任务1映射到user_stack[]处的页表项被设置成只读,使得任务1在执行堆栈操作时将会引起写页面异常,从而由内核另行分配主内存区页面作为堆栈空间使用。
4.其他任务的地址对应关系
对于被创建的从任务2开始的其他任务,它们的父进程都是init(任务1)进程。我们已经知道,在Linux 0.12系统中共可以有64个进程同时存在。下面我们以任务2为例来说明其他任何任务对地址空间的使用情况。
从任务2开始,如果任务号以nr来表示,那么任务nr在线性地址空间中的起始位置将被设定在nr×64MB处。例如任务2的开始位置= nr×64MB = 2×64MB = 128MB。任务代码段和数据段的最大长度被设置为64MB,因此任务2占有的线性地址空间范围是128MB~192MB,共占用64MB/4MB = 16个页目录项。虚拟空间中任务代码段和数据段都被映射到线性地址空间相同的范围,因此它们也完全重叠。图显示出了任务2的代码段和数据段在3种地址空间中的对应关系。
在任务2被创建出来之后,将在其中运行execve()函数来执行shell程序。当内核通过复制任务1刚创建任务2时,除了占用线性地址空间范围不同外(128MB~128MB+640KB),此时任务2的代码和数据在3种地址空间中的关系与任务1的类似。当任务2的代码(init())调用execve()系统调用开始加载并执行shell程序时,该系统调用会释放掉从任务1复制的页目录和页表表项及相应内存页面,然后为新的执行程序shell重新设置相关页目录和页表表项。图给出的是任务2中开始执行shell程序时的情况,即任务2原先复制任务1的代码和数据被shell程序的代码段和数据段替换后的情况。图中显示出已经映射了一页物理内存页面的情况。这里请注意,在执行execve()函数时,系统虽然在线性地址空间为任务2分配了64MB的空间范围,但是内核并不会立刻为其分配和映射物理内存页面。只有当任务2开始执行时由于发生缺页而引起异常时才会由内存管理程序为其在主内存区中分配并映射一页物理内存到其线性地址空间中。这种分配和映射物理内存页面的方法称为需求加载(load on demand),请参见第13章中的相关描述。
![]() |
(点击查看大图)图5-17 其他任务地址空间中的对应关系 |
从Linux内核0.99版以后,对内存空间的使用方式发生了变化。每个进程可以单独享用整个4GB的地址空间范围。如果我们能理解本节说描述的内存管理概念,那么对于现在所使用的Linux 2.x内核中所使用的内存管理原理也能立刻明白。由于篇幅所限,这里对此不再