分类: LINUX
2009-05-12 20:33:48
当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=
图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是编译内核以后所创建的。 |