Linux 内存管理系统:初始化
作者:Joe Knapka
臭翻:colyli
内存管理系统的初始化处理流程分为三个基本阶段:
激活页内存管理
在swapper_pg_dir中初始化内核的页表
初始化一系列和内存管理相关的内核数据
Turning On Paging (i386)
启动分页机制(i386)
Kernel 代码被加载到物理地址0x100000(1MB),在分页机制打开后被重新映射到
PAGE_OFFSET + 0x100000的位置(PAGE_OFFSET在IA32上为3GB,即进程虚拟地址中用户
空间与内核空间的分界处)。这是通过将物理地址映射到编译进来的页表 (在
arch/i386/kernel/head.S中)的0-8MB以及PAGE_OFFSET-PAGE_OFFSET+8MB实现的。然后
我们跳转到init/main.c中的start_kernel,这个函数被定位到PAGE_OFFSET+某一个地址。
这看起来有些狡猾。要注意到在head.S中启动分页机制的代码是通过让它自己所执行的地
址空间不再有效的方式来实现这一点的;因此0-4MB被映射(不明白:hence the 0-4MB
identity mapping.)。在分页机制没有启动之前,start_kernel是不会被调用的,我们假
定他运行在PAGE_OFFSET+某一个地方的位置。因此head.S中的页表必须同样映射内核代码
所使用的地址,这样后继才能跳转到staert_kernel处;因此PAGE_OFFSET被映射(不明
白:hence the PAGE_OFFSET mapping.)。
下面在head.S中分页机制启动时的一些神奇的代码:
/*
* 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 1f /* flush the prefetch-queue */
1:
movl $1f,%eax
jmp *%eax /* make sure eip is relocated */
1:
在两个1的label之间的代码将第二个label 1的地址加载到EAX中,然后跳转到那里。这
时,指令指针寄存器EIP指向1MB+某个数值的物理地址。而label都在内核的虚拟地址空
间(PAGE_OFFSET+某个位置),所以这段代码将EIP有效的从物理地址空间重新定位到了虚
拟地址空间。
Start_kernel函数初始化了所有的内核数据,然后启动了init内核线程。Start_kernel中
最初的几件事情之一就是调用setup_arch函数,这是一个和具体的体系结构相关的设置函
数, 调用了更底层的初始化细节。对于x86 平台而言, 这些函数在
arch/i386/kernel/setup.c中。
在setup_arch 中和内存相关的第一件事就是计算低端内存(low-memory) 和高端内存
(high-memory)的有效页的数目;每种内存类型(each memory type)最高端的页的数目分别
保存在全局变量highstart_pfn和highend_pfn中。高端内存并不是直接映射到内核的虚拟
内存(VM)中;这是后面要讨论的。
接下来,setup_arch 调用init_bootmem 函数以初始化启动时的内存分配器(boot-time
memory allocator)。Bootmem内存分配器仅仅在系统boot的过程中使用,为永久的内核数
据分配页。因此我们不会对它涉及太多。需要记住的就是bootmem 分配器(bootmem
allocator)在内核初始化时提供页,这些页为内核专门预留,就好像他们是从内核景象文
件中载入的一样,他们在系统启动以后不参与任何的内存管理活动。
初始化内核页表
之后,setup_arch调用在arch/i386/mm/init.c中的paging_init函数。这个函数做了一些
事情。首先它调用pagetable_init函数去映射整个的物理内存,或者在PAGE_OFFSET到4GB
之间的尽可能多的物理内存,这是从PAGE_OFFSET处开始。
在pagetable_init函数中,我们在swapper_pg_dir中精确的建立了内核的页表,映射到
截至PAGE_OFFSET的整个物理内存。
这是一个将正确的数值填充到页目录和页表中去的简单的算术活。映射建立在
swapper_pg_dir中,即kernel页目录;这也是初始化页机制时所使用的页目录。(当使用
4MB的页表的时候,直到下一个4MB边界的虚拟地址才会被映射在这里,但这没有什么,
因为我们不会使用这个内存所以没有什么问题)。如果有这里有剩下物理内存没有被映射,
那就是大于4GB-PAGE_OFFSET范围的内存,这些内存只有CONFIG_HIGHMEM选项被设置后
才能使用(即使用大于4GB的内存)。
在接近pagetable_init函数的尾部,我们调用了fixrange_init为编译时固定的虚拟内存
映射预留页表。这些表将硬编码到Kernel中的虚拟地址进行映射,但是他们并不是已经加
载的内核数据的一部分。Fixmap表在运行时调用set_fixmap函数被映射到物理内存。
在初始化了fixmap之后,如果CONFIG_HIGHMEM被设置了,我们还要分配一些页表给kmap
分配器。Kmap允许kernel将物理地址的任何页映射到kernel的虚拟地址空间,以临时使用。
这很有用,例如对在pagetable_init中不能直接映射的物理内存进行映射。
Fixmap 和kmap 页表们占据了kernel 虚拟空间顶部的一部分——因此这些地址不能在
PAGE_OFFSET映射中被永久的映射到物理页上。由于这个原因,Kernel虚拟内存的顶部的
128MB就被预留了(vmalloc分配器仍然是用这个范围内的地址)。(下面这句实在是不知
道怎么翻译) Any physical pages that would otherwise be mapped into the
PAGE_OFFSET mapping in the 4GB-128MB range are instead (if CONFIG_HIGHMEM is
specified) included in the high memory zone, accessible to the kernel only via
kmap()。如果没有设置CONFIG_HIGMEM,这些页就完全是不可用的。这仅针对配置有大量内
存的机器(900多MB或者更多)。例如,如果PAGE_OFFSET=3GB,并且机器有2GB的RAM,
那么只有开始的1GB-128MB的物理内存可以被映射到PAGE_OFFSET和fixmap/kmap地址范
围之间。剩余的页是不可用的——实际上对于用户进程映射来说,他们是可以直接映射的页
——但是内核不能够直接访问它们。
回到paging_init,我们可以通过调用kmap_init函数来初始化kmap系统,kmap_init简
单的缓存了最先的kmap_pagetable[在TLB?]。然后,我们通过计算zone的大小并调用
free_area_init 去建立mem_map 和初始化freelist,初始化了zone 分配器。所有的
freelist被初始化为空,并且所有的页都被标志为reserved(不可被VM系统访问);这
种情况之后会被纠正。
当paging_init完成后,物理内存的分布如下[注意在2.4的内核中这不全对]:
0x00000000: 0-page
0x00100000: kernel-text
0x????????: kernel_data
0x???????? =_end: whole-mem pagetables
0x????????: fixmap pagetables
0x????????: zone data (mem_map, zone_structs, freelists &c)
0x???????? =start_mem: free pages
这块内存被swapper_pg_dir和whole-mem-pagetables映射以寻址PAGE_OFFSET。
进一步的VM子系统初始化
现在我们回到start_kernel。在paging_init完成后,我们为内核的其他子系统进行一些
额外的配置工作,它们中的一些使用bootmem分配器分配额外的内核内存。从内存管理的观
点来看,这其中最重要的是kmem_cache_init,他初始化了slab分配器的数据。
在kmem_cache_init 调用之后不久,我们调用了mem_init。这个通过清除空闲物理页的
zone数据中的PG_RESERVED位在free_area_init的开始完成了初始化freelist的工作;
为不能被用为DMA的页清除PG_DMA位;然后释放所有可用的页到他们各自的zone中。最后
一步,在bootmem.c 中的free_all_bootmem函数中完成,很有趣。他建立了伙伴位图和
freelist描述了所有存在的没有预留的页,这是通过简单的释放他们并让free_page_ok
做正确的事情。一旦mem_init被调用了,bootmem分配器就不再使用了,所以它的所有的
页也会被释放到zone分配器的世界中。
段
段用来将线性地址空间划分为专用的块。线性空间是被VM子系统管理的。X86体系结构从硬
件上支持段机制;你可以按照段+段内偏移量的方式指定一个地址,这里地址被描述为一定
范围的线性(虚拟地址)并带有特定的属性(如保护属性)。实际上,在x86体系结构中你
必须使用段机制。所以我们要设置4个段:
一个kernel text段:从0 到4GB
一个kernel data段:从0 到4GB
一个user text段:从0 到4GB
一个user data段:从0 到4GB
因此我们可以使用任何一个有效的段选择器(segment selector)访问整个虚拟地址空间。
问题:
段是在哪里被设置的?
答案:
全局描述符表(GDT)定义在head.s的450行。 GDT寄存器在250行被加载。
问题:
为什么将内核段和用户端分离开。是否他们都有权限访问整个4GB的范围?
答案:
这是因为内核和用户段的保护机制有区别:
.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 */
段寄存器(CS,DS等)包含有一个13位的描述符表的索引,索引指向的描述符告诉CPU所选
择的段的属性。段选择器的低3位没有被用来索引描述符表,而是用来保存描述符类型(全
局或局部)以及需要的特权级。因此内核段选择器0x10和0x18使用特权级0(RPL0),用
户选择器0x23和0x2B使用最特权级RPL 3。
要注意到第三个高序字节的高位组对应内核和用户也是不同的:对内核,描述符特权级
(DPL)为0;对用户DPL为3。如果你阅读了Intel的文档,你将看到确切的含义,但是由
于Linux内核的x86段保护没有涉及太多,所以我就不再讨论太多了。
阅读(891) | 评论(0) | 转发(0) |