内核启动流程
这里不提bootloader是怎么加载内核,只谈arm体系结构下linux内核如何启动的。
linux内核编译完成后生成vmlinux ELF格式文件,并经过压缩成bin格式的zImage内核映像。当bootloader经过初始化硬件把zImage影响调入内存中时,内核代码该怎么工作,才能将系统软件带入一个合适的环境。
首先zImage虽然为压缩过的文件,但并不是完全压缩了的,起始位置还有未压缩的代码,这部分代码就是解压缩,通过将后面的内核代码解压后,执行内核部分的代码,就是vmlinux。
在内核源码根目录下的Makefile中,关于vmlinux的生成规则:
vmlinux: $(vmlinux-lds) $(vmlinux-init) $(vmlinux-main) vmlinux.o $(kallsyms.o) FORCE
可以看出vmlinux的依赖有$(vmlinux-lds) $(vmlinux-init) $(vmlinux-main)。其中$(vmlinux-lds)是编译连接脚本,对于ARM平台,就是 arch/arm/kernel/vmlinux-lds文件。
$(vmlinux-init)也定义在顶层Makefile中,vmlinux-init := $(head-y) $(init-y)
head-y 在 arch/arm/Makefile 中定义:
head-y:= arch/arm/kernel/head$(MMUEXT)。o arch/arm/kernel/init_task.o
对于有MMU的处理器,MMUEXT 为空白字符串,所以arch/arm/kernel/head.o是第一个连
接的文件,而这个文件是由 arch/arm/kernel/head.S编译产生成的。
实际上,head.s程序在被编译成目标文件后与内核其他程序一起链接成system模块,head.s位于系统内核代码的最前端,处于内存绝对地址0处开始的地方。也是从这里开始,内核完全处于保护模式下运行。
1.head.S
程序的功能也比较单一。
1)设置处理器为 SVC 模式,关中断
2)读取 CPUID,调用__lookup_processor_type 查找处理器信息结构 proc_info
3)调用__lookup_machine_type 查找机器类型信息结构 machine_desc
4)调用__create_page_tables 函数为内核创建页面映射表
5)调用处理器底层初始化函数,初始化 MMU,Cache,TLB
6)跳转到__enalbe_mmu 函数,打开 MMU
7)跳转到__mmap_switched 函数,建立 C 语言运行环境(搬移数据段,清理 BSS 段),保
存 CPUID 和机器类型代码,最后跳转到 C 语言入口函数 start_kernel()
head.s最终利用返回指令将预先放置在堆栈中的init/main.c程序的入口地址弹出,即start_kernel(),进而去执行main.c程序。
2.main.c
在main.c函数中,内核进行一系列的初始化工作,包括陷阱门、块设备、字符设备和tty,包括人工设置的第一个任务task0,等所有的初始化工作完成之后就设置中断允许标志和开启中断,main()也转入到任务0中执行。
1.首先执行的是asmlinkage void __initstart_kernel(void)函数。
函数执行了一系列的初始化,中断机制初始化,定时器初始化,调度器初始化,软中断初始化,控制台初始化,最重要的是结构体系相关的初始化,在函数setup_arch(&command_line)中。
2.start_kernel(void)函数之后调用rest_init()函数。
函数目的是创建一个入口点是 kernel_init()函数的内核线程,然后调用 cpu_idle()函数进入空闲状态。新创建的内核线程是系统的1号任务(pid =1),放入了调度队列中,而原先的初始化代码是系统的0号任务不在调度队列中的。
3. 因此1号任务投入运行,系统转而执行init()函数。这个函数同样定义在 init/main.c 中。
kernel_init()函数接着完成系统更高层次,比如驱动程序,根文件系统等等的初始化工作。其中的do_basic_setup()函数比较重要,这个函数先调用 driver_init()函数完成驱动程序的初始化,又通过 do_initcalls()函数依次调用了系统中所有的初始化函数。
4. 在 init()函数的最后,调用了init_post()函数。该函数打开了控制台设备,然后,函数依次尝试执行以下几个外部程序:
1)由 ramdisk_execute_command 指定的外部程序,即内核启动参数“rdinit=XXX”指定的
程序
2)由 execute_command 指定的外部程序,即内核启动参数“init=XXX”指定的程序
3)/sbin/init 4)/etc/init
5)/bin/init 6)/bin/sh
这几个程序中任何一个加载执行成功,就进入了用户态,内核启动就宣告结束。
1号任务原先是个内核线程,加载外部程序后就有了自己的用户态空间,成为一个进程,这就是系统中所有进程的祖先 1 号进程。然后 1 号进程再执行用户态的初始化程序,例如处理/etc/inittab,创建终端,等待用户登录等等,系统启动完成。
内核压缩和解压缩代码都在目录kernel/arch/arm/boot/compressed,
编译完成后将产生vmlinux、head.o、misc.o、head-xscale.o、piggy.o这几个文件,
head.o是内核的头部文件,负责初始设置;
misc.o将主要负责内核的解压工作,它在head.o之后;
head-xscale.o文件主要针对Xscale的初始化,将在链接时与head.o合并;
piggy.o是一个中间文件,其实是一个压缩的内核(kernel/vmlinux),只不过没有和初始化文件及解压文件链接而已;
vmlinux是(没有--lw:zImage是压缩过的内核)压缩过的内核,就是由piggy.o、head.o、misc.o、head-xscale.o组成的。
在BootLoader完成系统的引导以后并将Linux内核调入内存之后,调用bootLinux(),
这个函数将跳转到kernel的起始位置。如果kernel没有压缩,就可以启动了。
如果kernel压缩过,则要进行解压,在压缩过的kernel头部有解压程序。
压缩过得kernel入口第一个文件源码位置在arch/arm/boot/compressed/head.S。
它将调用函数decompress_kernel(),这个函数在文件arch/arm/boot/compressed/misc.c中,
decompress_kernel()又调用proc_decomp_setup(),arch_decomp_setup()进行设置,
然后使用在打印出信息“UncompressingLinux...”后,调用gunzip()。将内核放于指定的位置。
以下分析head.S文件:
(1)对于各种Arm CPU的DEBUG输出设定,通过定义宏来统一操作。
(2)设置kernel开始和结束地址,保存architectureID。
(3)如果在ARM2以上的CPU中,用的是普通用户模式,则升到超级用户模式,然后关中断。
(4)分析LC0结构deltaoffset,判断是否需要重载内核地址(r0存入偏移量,判断r0是否为零)。
这里是否需要重载内核地址,我以为主要分析arch/arm/boot/Makefile、arch/arm/boot/compressed/Makefile
和arch/arm/boot/compressed/vmlinux.lds.in三个文件,主要看vmlinux.lds.in链接文件的主要段的位置,
LOAD_ADDR(_load_addr)=0xA0008000,而对于TEXT_START(_text、_start)的位置只设为0,BSS_START(__bss_start)=ALIGN(4)。
对于这样的结果依赖于,对内核解压的运行方式,也就是说,内核解压前是在内存(RAM)中还是在FLASH上,
因为这里,我们的BOOTLOADER将压缩内核(zImage)移到了RAM的0xA0008000位置,我们的压缩内核是在内存(RAM)从0xA0008000地址开始顺序排列,
因此我们的r0获得的偏移量是载入地址(0xA0008000)。接下来的工作是要把内核镜像的相对地址转化为内存的物理地址,即重载内核地址。
(5)需要重载内核地址,将r0的偏移量加到BSSregion和GOTtable中。
(6)清空bss堆栈空间r2-r3。
(7)建立C程序运行需要的缓存,并赋于64K的栈空间。
(8)这时r2是缓存的结束地址,r4是kernel的最后执行地址,r5是kernel境象文件的开始地址。检查是否地址有冲突。
将r5等于r2,使decompress后的kernel地址就在64K的栈之后。
(9)调用文件misc.c的函数decompress_kernel(),解压内核于缓存结束的地方(r2地址之后)。此时各寄存器值有如下变化:
r0为解压后kernel的大小
r4为kernel执行时的地址
r5为解压后kernel的起始地址
r6为CPU类型值(processorID)
r7为系统类型值(architectureID)
(10)将reloc_start代码拷贝之kernel之后(r5+r0之后),首先清除缓存,而后执行reloc_start。
(11)reloc_start将r5开始的kernel重载于r4地址处。
(12)清除cache内容,关闭cache,将r7中architectureID赋于r1,执行r4开始的kernel代码。
下面简单介绍一下解压缩过程,也就是函数decompress_kernel实现的功能:
解压缩代码位于kernel/lib/inflate.c,inflate.c是从gzip源程序中分离出来的。包含了一些对全局数据的直接引用。
在使用时需要直接嵌入到代码中。gzip压缩文件时总是在前32K字节的范围内寻找重复的字符串进行编码,
在解压时需要一个至少为32K字节的解压缓冲区,它定义为window[WSIZE]。inflate.c使用get_byte()读取输入文件,
它被定义成宏来提高效率。输入缓冲区指针必须定义为inptr,inflate.c中对之有减量操作。inflate.c调用flush_window()
来输出window缓冲区中的解压出的字节串,每次输出长度用outcnt变量表示。在flush_window()中,还必
须对输出字节串计算CRC并且刷新crc变量。在调用gunzip()开始解压之前,调用makecrc()初始化CRC计算表。
最后gunzip()返回0表示解压成功。
我们在内核启动的开始都会看到这样的输出:
Uncompressing Linux...done, booting the kernel.
这也是由decompress_kernel函数内部输出的,它调用了puts()输出字符串,
puts是在kernel/include/asm-arm/arch-pxa/uncompress.h中实现的。
执行完解压过程,再返回到head.S中,启动内核:
call_kernel: bl cache_clean_flush
bl cache_off
mov r0, #0
mov r1,r7 @ restore architecturenumber
mov pc,r4 @ call kernel
下面就开始真正的内核了。