inux 内存管理系统:初始化 作者: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段保护没有涉及太多,所以我就不再讨论太多了。 下面的命令是我自己增加的命令,不使用uImage,直接引导zImage文件。 具体方法是使用tftp命令从网络下载zImage文件到内存中或者直接读取flash数据,拷贝到内存中,假设拷贝到了A地址处,接下来就可以调用: Uboot在设置启动命令
的时候使用的是Tag方式,也就是内核现在期望使用的参数传递方式。还有一种引导设置方式,就是采用2.2以及以前版本使用的参数设置方式,2.4和
2.6内核为了兼容之前版本参数设置,对老版本参数数据进行了解析,转换成了内部tag方式。这样我们完全可以使用老版本的参数传递方式。 /* This is the old deprecated way to pass parameters to the kernel */ int do_bootzimage(cmd_tbl_t *cmdtp, int flag, int argc, char *argv[]) // 接下来将结构的数据清零, 不用的全部清零, 防止出错 // 拷贝命令行参数到命令位置 这里我们在引导的时候使用了老版本的参数传递方式,下面时Linux内核在进行参数解析时的代码,可以看到,内核将这些参数自动转换成能够识别的类型: convert_to_tag_list()函数实现在arch/arm/kernel/compat.c文件中,该函数随后调用build_tag_list()函数进行参数重组,具体可以参考内核的源代码。 vivi与Linux kernel的参数传递情景分析(下) - Vivi - CalmArrow
下面进入Linux kernel部分,分析与bootloader参数传递对应的部分。
移植Linux需要很大的工作量,其中之一就是HAL层的编写。在具体实现上,HAL层以arch目录的形式存在。显然,该层需要与bootloader
有一定的约定,否则就不能很好的支持。其实,这个地方应该思考一个问题,就是说,boot loader可以做到Linux
kernel里面,但是这样带来的问题就是可移植性和灵活性都大为降低。而且,bootloader的功能并非操作系统的核心范畴,Linux的核心应该
始终关注操作系统的核心功能上,将其性能达到最优。所以,bootloader分离出来单独设计,是有一定的道理的。bootloader现在除了完成基
本功能外,慢慢地变得“肥胖”了。在高性能bootloader设计中,可能会把调试内核等的一些功能集成进来,这样在内核移植尚未完成阶
段,bootloader可以充当调试器的作用。功能趋于完善,也慢慢趋于复杂。废话不说,进入正题。
三、Linux kernel接受参数分析
这部分主要分析如下问题:
·Linux kernel支持压缩映象和非压缩映象两种方式启动,那么这两种流程和函数入口有何不同?
·如何使用非压缩映象?做一下测试。
·zImage是如何生成的?其格式如何?
·启动之后,Linux kernel如何接收参数?
这里不具体区分每个问题,按照理解和开发的思路来进行。
1、思考:前面做的基本实验中,并没有采用压缩映象。因为程序规模太小,压缩带来的时间开销反而降低了性能。但是对Linux
kernel来说,映象还是比较大的,往往采用了压缩。但是,同样有需求希望Linux
kernel小一些,不采用压缩方式来提高内核启动的速度,对时间要求比较苛刻。那么,这样就出现了两种情况:压缩映象和非压缩映象。由此带来的问题就在
于:如果是压缩映象,那么必须首先解压缩,然后跳转到解压缩之后的代码处执行;如果是非压缩映象,那么直接执行。Linux必须对这两种机制提供支持,这
里就需要从整体上来看一下生成的映象类型了。
因为vivi的Makefile都是直接来源于Linux,前面对vivi的Makefile已经分析清楚了,这里看Linux的Makefile就容易多了,大同小异,而且还有丰富的文档支持。
(1)非压缩映象
$make vmlinux
这里生成的是vmlinux,是ELF文件格式。这个文件是不能烧写存储介质的,如果想了解ELF文件格式,需要参考专门的文章。当然,这里,如果想要使
用非压缩映象,可以使用arm-linux-objcopy把上述ELF格式的vmlinux转化为二进制格式的vmlinux.bin,这样就可以直接
烧写了。
于是我做了如下的修改,在Makefile中增加了:
同时在clean file的列表中增加vmlinux.bin。这样就可以生成vmlinux.bin了,前面的基础实验都讲过了。然后烧写vmlinux.bin到nand flash的kernel分区,引导启动,正常,而且不会出现解压缩提示:
可见,可以通过非压缩映象格式启动。
(2)压缩映象
下面看看压缩映象是如何得到的。顶层的Makefile没有压缩映象的生成,显然就在包含的子Makefile中。容易查知在arch/arm/下的Makefile,可见:
也就是说,有bzImage、zImage几种。其中arch/boot下有:
这里发现如果采用make Image,则生成的非压缩映象的二进制格式,可以直接烧写,可见前面第一步的工作是浪费了,Linux内核还是很完善的,提供了这种方式,所以,如果想要生成非压缩二进制映象,那么就要使用make Image。
另外,这里提供了两种压缩的映象,其实就是一种,这里能够看到的就是如果采用make zImage或者make
bzImage,就要把compressed/vmlinux处理为二进制格式,可以下载使用。下面就看compressed/vmlinux是什么。进
入compressed文件夹,看看Makefile:
很明显了,这里的vmlinux是由四个部分组成:head.o、head-s3c2410.o、misc.o、piggy.o。关于这几个文件是干什么用的,看看各自的编译规则就非常清晰了:
可见,vmlinux是把顶层生成的非压缩的ELF映象vmlinux进行压缩,同时加入了加压缩代码部分。真正的压缩代码就是lib/inflate.c。可以看看,主要是gunzip,具体的压缩算法就不分析了。
至此,就可以用下图作出总结了:
bootloader把存储介质中的kernel映象下载到mem_base+0x8000的位置,执行完毕后,跳转到这一位置,执行此处的代码。这一位置的入口可能有两种情况,第
一种是kernel映象为非压缩格式,通过make
Image获得,那么真正的入口就是arch/arm/kernel/head_armv.S(ENTRY(stext));第二种是kernel映象为
压缩格式,通过make
zImage获得,那么真正的入口就是arch/arm/boot/compressed/head.S(ENTRY(_start))。这个地方并不是kernel判断,也不需要判断。道理很简单,cpu只会按照读入的代码执行,两种情况下执行的代码不同,自然也就有两种不同的过程了。
(3)探讨zImage的magic number的位置
可以看出,如果是zImage,那么程序的入口是arch/arm/boot/compressed/head.S。分析程序头部:
可见前面8条指令均为mov r0, r0,从前面的zImage的16进制格式中可以看出,前面8个字都是相同的,均为00 00
A0 E1,第9条指令就是b 1f,然后就应该是0x016f2818.这样就与前面程序的判断对应上了,也就是说,此处的magic
number是固定位置,固定数值的,注释中也写的很清晰,那就是magic numbers to help the
loader,也就是说帮助bootloader确定映象的文件格式。但是应该说明的是,在vivi的bootloader设计中,虽然检测zImage
的magic
number,但是并没有进行未识别处理。也就是说,假定用ultra-edit32把此位置的0x016f2818破坏掉,其他不变,那么虽然vivi
提示无法识别zImage映象,但是并不影响实际的执行。当然,你也可以有其他的设计思路。不过设计的哲学思想是,要完成一件事情,并不只有一种方式。所
以,bootloader不能限死只是使用zImage格式,需要有一定的灵活性,为了引导内核启动,可以采用不同的方式。
(4)完成了前面的理解,下面就要重点看解析参数一部分了。这里不将zImage方式的启动作为重点分析内容,静下心来跟踪代码并不是难事。从
整体的角度理解,如果采用zImage,那么在执行完成解压缩之后,自然会调转到解压之后的kernel的第一条指令处。这时就是真正的启动内核了。所以
我们可以看arch/arm/kernel/head-armv.S,此处做的工作可以参考taoyuetao的分析,完成的功能比较简单。这里就感兴趣
的参数问题分析,需要注意的是,
可见R0是0,R1是mach
type,这些都是必须要设定的。在这里,并没有限定R2必须为参数的起始地址。kernel本身并没有使用R0-R2,如果设定了R2,在这里也不会修
改其值。后面的工作也没有设计接收参数,最后直接跳到start_kernel(【init/main.c】)
从开头分析,首先是lock_kernel,这里是SMP相关,我的是单CPU,所以实际上该函数为空。然后打印版本信息,在vivi中已经分析过这个机
制了,两者相同。下面的setup_arch就是分析的重点了,它要获取命令行启动参数,然后打印获得的命令行参数,然后进行语法解析选项。我们关注的重
点就在setup_arch上了。参数设置都在【arch/arm/kernel/setup.c】,这个函数也不例外,进入setup.c。
这里面涉及到3个比较复杂的结构体,包括param_struct、tag、machine_desc。第一步的操作是关于根设备号,暂时不探讨;第二步
工作setup_processor,是设置处理器,这是多处理器相关部分,暂时不探讨;第三步工作是setup_machine,这里就需要了解了。
首先,machine_arch_type没有定义,仅仅在头部有定义,这是全局变量,两者之间一定存在联系:
看看头文件,应该有#include
<asm/mach-types.h>,但是未编译时并没有,可以确定是编译前完成的。这里只有看Makefile了。因为setup.c在
这里,首先看同层的Makefile。这一层没有关于mach-types.h的信息,然后到上一层Makefile,发现了:
说现在使用MRPROPER_FILES,但是下面没有出现,故而应该看几个宏的定义:
由此知道,对应的子文件夹包括boot和tools,boot是与启动相关,不太可能;而前面也看到,tools下有mach-types,所以判断在tools下面,看看tools/Makefile:
由此判断出,mach-types.h是如何生成的,主要是利用awk脚本处理生成。生成之后与s3c2410有关的部分为:
由此就知道了,这里的machine_arch_type为193,所以此函数实际上执行:mdesc = setup_machine(193);它要填充结构体machine_desc,如下:
另外,还提供了一系统的宏,用于填充该结构体:
EDUKIT填充了一个结构体,用如下的方式:
看到有特殊的设置部分,那就是开始为之分配了一个段,段的名字是.arch.info,也就是说把这部分信息单独作为一个段来进行处理。下面把这个宏展开如下:
可见,基本的信息已经具备了,而且从这里,我们也可以看出,启动参数地址由这个段就可以完成,不需要传递了。当然,必须保证bootloader的值,与此处的相同。这样,也就说明如果不使用R2传递参数的起始地址,那么这个地方就需要把这个结构体设置好。
下面看看这个函数完成什么功能:
这个地方就是要把上面这一系列的信息连贯起来,那么就不难理解了。上述的宏已经完成了.arch.info段,这个段实际上在内存中就是一个
machine_desc形式组织的信息(对Linux内核来说,并不一定仅仅有一个结构块),上述函数的两个变量__arch_info_begin和
__arch_info_end很明显是有链接脚本传递进来。于是查看近层的链接脚本(【arch/arm/vmlinux-armv.lds.in】,
可以发现:
所以上述的功能就很简单了,就是查看是否有mach-type为193的结构存在,如果存在就打印出name,这也就是开机启动后,出现Machine: Embest EduKit III (S3C2410)的原因了。
接下来关注:
很明显,这里的mdesc->param_offset并不为0,而是0x30000100,所以要做一步变换,就是物理地址映射成虚拟地址。把这
个地址附给tags指针。然后就是判断是param_struct类型还是tags类型,如果是param_struct类型,那么首先转换成tags类
型,然后对tags类型进行解析。
要注意parse_tags函数是非常重要的,它有隐含的功能,不太容易分析。跟踪上去,主要看这个函数:
这里又用到链接器传递参数,现在就是来解析每个部分。先看一下tagtable是如何来的。首先看【include/asm-arm/setup.h】,看看宏的定义,也就是带有__tag,就归属为.taglist段。
利用__tag有构造了一个复杂的宏__tagtable,实际上就是定义了tagtable列表。现在看setup.c中的宏形式示例:
展开之后为:
于是,段.taglist就是这样一系列的结构体。那么上述的函数实际上就是把传递进来的tag与此表比较,如果tag标记相同,证明设置了此部分功能,就执行相应的解析函数。以ATAG_CMDLINE为例,就要执行:
这样也就是实现了把tag中的命令行参数复制到了default_command_line中。
在返回来到函数【arch/arm/kernel/setup.c】,看函数setup_arch,定义中有:
说明from指向数组default_command_line。于是知道,当你完成tag解析的时候,所有传递过来的参数实际上已经复制到了相应的部
分,比如命令行设置复制到了default_command_line。其他类似,看相应的解析行为函数就可以了。因为现在vivi只是传递了命令行,所
以只是分析清楚这个。后面执行:
这就比较容易理解了,就是将传递进来的命令行参数复制到saved_command_line,后面还可以打印出此信息。再往后的工作已经与此情景关系不大,所以不再进行详细分析。
至此,vivi与Linux kernel的参数传递情景分析就完成了。
linux2.6内核中的MACHINE_START宏 现在正在阅读linux2.6.18内核,在mainstone.c文件中,有如下的宏定义:
MACHINE_START(MAINSTONE, "Intel HCDDBBVA0 Development Platform (aka Mainstone)") /* Maintainer: MontaVista Software Inc. */ .phys_io = 0x40000000, .boot_params = 0xa0000100, /* BLOB boot parameter setting */ .io_pg_offst = (io_p2v(0x40000000) >> 1 & 0xfffc, .map_io = mainstone_map_io, .init_irq = mainstone_init_irq, .timer = &pxa_timer, .init_machine = mainstone_init, MACHINE_END 请问各位大侠,这个宏定义甚么时候调用的,是谁调用的它,象里面的mainstone_init是哪个函数调用的它?是不是在main函数中的初始化的时候? 自己看宏的定义,主要是定义了"struct machine_desc"的类型,放在 section(".arch.info.init"),是初始化数据,Kernel 起来之后将被丢弃。
kernel boot 起来的时候期望 bootloader 传参数进来,其中包括 Machine Type,参考 arch/arm/tools/mach-types 并和 MACHINE_START() 第一个参数对上号。因此,哪个 MACHINE 是 run-time 的时候决定的,this way, you can pack as many machine as you want, and dynamically initialize the specific platforms. 各个成员函数在不同时期被调用: 1. .init_machine 在 arch/arm/kernel/setup.c 中被 customize_machine 调用,放在 arch_initcall() 段里面,会自动按顺序被调用 start_kernel,参考 init/main.c 2. init_irq在start_kernel() --> init_IRQ() --> init_arch_irq() 被调用 3. map_io 在 setup_arch() --> paging_init() --> devicemaps_init() 其他主要都在 setup_arch() 中用到 |
|