当Linux启动时,首先运行在实模式下,随后就要转到保护模式下运行。因为在第二章段机制中,我们已经介绍了Linux对段的设置,在此我们主要讨论与分页机制相关的问题。Linux内核代码的入口点就是/arch/i386/kernel/head.S中的startup_32。
1.页表的初步初始化:
/*
* The page tables are initialized to only 8MB here - the final page
* tables are set up later depending on memory size.
*/
.org 0x2000
ENTRY(pg0)
.org 0x3000
ENTRY(pg1)
/*
* empty_zero_page must immediately follow the page tables ! (The
* initialization loop counts until empty_zero_page)
*/
.org 0x4000
ENTRY(empty_zero_page)
/*
* Initialize page tables
*/
movl $pg0-__PAGE_OFFSET,%edi /* initialize page tables */
movl $007,%eax /* "007" doesn't mean with right to kill, but
PRESENT+RW+USER */
2: stosl
add $0x1000,%eax
cmp $empty_zero_page-__PAGE_OFFSET,%edi
jne 2b
内核的这段代码执行时,因为页机制还没有启用,还没有进入保护模式,因此指令寄存器EIP中的地址还是物理地址,但因为pg0中存放的是虚拟地址(想想gcc编译内核以后形成的符号地址都是虚拟地址),因此,“$pg0-__PAGE_OFFSET ”获得pg0的物理地址,可见pg0存放在相对于内核代码起点为0x2000的地方,即物理地址为0x00102000,而pg1的物理地址则为0x00103000。Pg0和pg1这个两个页表中的表项则依次被设置为0x007、0x1007、0x2007等。其中最低的三位均为1,表示这两个页为用户页,可写,且页的内容在内存中(参见图2.24)。所映射的物理页的基地址则为0x0、0x1000、0x2000等,也就是物理内存中的页面0、1、2、3等等,共映射2K个页面,即8MB的存储空间。由此可以看出,Linux内核对物理内存的最低要求为8MB。紧接着存放的是empty_zero_page页(即零页),零页存放的是系统启动参数和命令行参数,具体内容参见第十三章。
2.启用分页机制:
/*
* This is initialized to create an identity-mapping at 0
* purposes) and another mapping of the 0
* PAGE_OFFSET.
*/
.org 0x1000
ENTRY(swapper_pg_dir)
.long 0x00102007
.long 0x00103007
.fill BOOT_USER_PGD_PTRS-2,4,0
/* default: 766 entries */
.long 0x00102007
.long 0x00103007
/* default: 254 entries */
.fill BOOT_KERNEL_PGD_PTRS-2,4,0
/*
* Enable paging
*/
3:
movl $swapper_pg_dir-__PAGE_OFFSET,%eax
movl %eax,%cr3 /* set the page table pointer.. */
movl %cr0,%eax
orl $0x80000000,%eax
movl %eax,%cr0 /* ..and set paging (PG) bit */
jmp
1:
movl $
jmp *%eax /* make sure eip is relocated */
1:
我们先来看这段代码的功能。这段代码就是把页目录swapper_pg_dir的物理地址装入控制寄存器cr3,并把cr0中的最高位置成1,这就开启了分页机制。
但是,启用了分页机制,并不说明Linux内核真正进入了保护模式,因为此时,指令寄存器EIP中的地址还是物理地址,而不是虚地址。“jmp
然后再看页目录swapper_pg_dir中的内容。从前面的讨论我们知道pg0和pg1这两个页表的起始物理地址分别为0x00102000和0x00103000。从图2.22可知,页目录项的最低12位用来描述页表的属性。因此,在swapper_pg_dir中的第0和第1个目录项0x00102007、0x00103007,就表示pg0和pg1这两个页表是用户页表、可写且页表的内容在内存。
接着,把swapper_pg_dir中的第2~767共766个目录项全部置为0。因为一个页表的大小为4KB,每个表项占4个字节,即每个页表含有1024个表项,每个页的大小也为4KB,因此这768个目录项所映射的虚拟空间为768´1024´4K=
最后,在第768和769个目录项中又存放pg0和pg1这两个页表的地址和属性,而把第770~1023共254个目录项置0。这256个目录项所映射的虚拟地址空间为256´1024´4K=
由此可以看出,在初始的页目录swapper_pg_dir中,用户空间和内核空间都只映射了开头的两个目录项,即8MB的空间,而且有着相同的映射,如图6.6所示。
图6.6 初始页目录swapper_pg_dir的映射图
读者会问,内核开始运行后运行在内核空间,那么,为什么把用户空间的低区(
但是,在CPU转入内核空间以后,应该把用户空间低区的映射清除掉。后面读者将会看到,页目录swapper_pg_dir经扩充后就成为所有内核线程的页目录。在内核线程的正常运行中,处于内核态的CPU是不应该通过用户空间的虚拟地址访问内存的。清除了低区的映射以后,如果发生CPU在内核中通过用户空间的虚拟地址访问内存,就可以因为产生页面异常而捕获这个错误。
3.物理内存的初始分布
经过这个阶段的初始化,初始化阶段页目录及几个页表在物理空间中的位置如图6.7所示:
图6.7 初始化阶段页目录及几个页表在物理空间中的位置
其中empty_zero_page中存放的是在操作系统的引导过程中所收集的一些数据,叫做引导参数。因为这个页面开始的内容全为0,所以叫做“零页”,代码中常常通过宏定义ZERO_PAGE来引用这个页面。不过,这个页面要到初始化完成,系统转入正常运行时才会用到。为了后面内容介绍的方便,我们看一下复制到这个页面中的命令行参数和引导参数。这里假定这些参数已被复制到“零页”,在setup.c中定义了引用这些参数的宏:
/*
* This is set up by the setup-routine at boot-time
*/
#define PARAM ((unsigned char *)empty_zero_page)
#define SCREEN_INFO (*(struct screen_info *) (PARAM+0))
#define EXT_MEM_K (*(unsigned short *) (PARAM+2))
#define ALT_MEM_K (*(unsigned long *) (PARAM+0x1e0))
#define E820_MAP_NR (*(char*) (PARAM+E820NR))
#define E820_MAP ((struct e820entry *) (PARAM+E820MAP))
#define APM_BIOS_INFO (*(struct apm_bios_info *) (PARAM+0x40))
#define DRIVE_INFO (*(struct drive_info_struct *) (PARAM+0x80))
#define SYS_DESC_TABLE (*(struct sys_desc_table_struct*)(PARAM+0xa0))
#define
MOUNT_ROOT_RDONLY (*(unsigned short *) (PARAM+0x
#define
RAMDISK_FLAGS (*(unsigned short *) (PARAM+0x
#define ORIG_ROOT_DEV (*(unsigned short *) (PARAM+0x1FC))
#define AUX_DEVICE_INFO (*(unsigned char *) (PARAM+0x1FF))
#define LOADER_TYPE (*(unsigned char *) (PARAM+0x210))
#define KERNEL_START (*(unsigned long *) (PARAM+0x214))
#define INITRD_START (*(unsigned long *) (PARAM+0x218))
#define
INITRD_SIZE (*(unsigned long *) (PARAM+0x
#define COMMAND_LINE ((char *) (PARAM+2048))
#define COMMAND_LINE_SIZE 256
其中宏PARAM就是empty_zero_page的起始位置,随着代码的阅读,读者会逐渐理解这些参数的用途。这里要特别对宏E820_MAP进行说明。E820_MAP是个struct e820entry数据结构的指针,存放在参数块中位移为0x2d0的地方。这个数据结构定义在include/i386/e820.h中:
struct e820map {
int nr_map;
struct e820entry {
unsigned long long addr; /* start of memory segment */
unsigned long long size; /* size of memory segment */
unsigned long type; /* type of memory segment */
} map[E820MAX];
};
extern struct e820map e820;
其中,E820MAX被定义为32。从这个数据结构的定义可以看出,每个e820entry都是对一个物理区间的描述,并且一个物理区间必须是同一类型。如果有一片地址连续的物理内存空间,其一部分是RAM,而另一部分是ROM,那就要分成两个区间。即使同属RAM,如果其中一部分要保留用于特殊目的,那也属于不同的一个分区。在e820.h文件中定义了4种不同的类型:
#define
E820_RAM 1
#define
E820_RESERVED 2
#define E820_ACPI 3 /* usable as RAM once ACPI tables have been read */
#define E820_NVS 4
#define HIGH_MEMORY (1024*1024)
其中E820_NVS表示“Non-Volatile Storage”,即“不挥发”存储器,包括ROM、EPROM、Flash存储器等。
在PC中,对于最初1MB存储空间的使用是特殊的。开头640KB(0x0~0x9FFFF为RAM,从0xA0000开始的空间则用于CGA、EGA、VGA等图形卡。现在已经很少使用这些图形卡,但是不管是什么图形卡,开机时总是工作于EGA或VGA模式。从0xF0000开始到0xFFFFF,即最高的4KB,就是在EPROM或Flash存储器中的BIOS。所以,只要有BIOS存在,就至少有两个区间,如果nr_map小于2,那就一定出错了。由于BIOS的存在,本来连续的RAM空间就不连续了。当然,现在已经不存在这样的存储结构了。1MB的边界早已被突破,但因为历史的原因,把1MB以上的空间定义为“HIGH_MEMORY”,这个称呼一直沿用到现在,于是代码中的常数HIGH_MEMORY就定义为“1024´1024”。现在,配备了128MB的内存已经是很普遍了。但是,为了保持兼容,就得留出最初1MB的空间。
这个阶段初始化后,物理内存中内核映像的分布如图6.8所示:
图6.8 内核映象在物理内存中的分布
符号_text对应物理地址0x00100000,表示内核代码的第一个字节的地址。内核代码的结束位置用另一个类似的符号_etext表示。内核数据被分为两组:初始化过的数据和未初始化过的数据。初始化过的数据在_etext后开始,在_edata处结束,紧接着是未初始化过的数据,其结束符号为_end,这也是整个内核映像的结束符号。
图中出现的符号是由编译程序在编译内核时产生的。你可以在System.map文件中找到这些符号的线性地址(或叫虚拟地址),System.map是编译内核以后所创建的。