内核启动流程

内核启动流程

 

       这里不提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
         
下面就开始真正的内核了。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值