linux 内核启动过程 for arm

ARM 136浏览

这里不提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中执行。

    首先执行的是asmlinkage void __init start_kernel(void)函数。函数执行了一系列的初始化,中断机制初始化,定时器初始化,调度器初始化,软中断初始化,控制台初始化,最重要的是结构体系相关的初始化,在函数setup_arch(&command_line)中。

    start_kernel(void)函数之后调用rest_init()函数。函数目的是创建一个入口点是 kernel_init()函数的内核线程,然后调用 cpu_idle()函数进入空闲状态。新创建的内核线程是系统的1号任务(pid =1),放入了调度队列中,而原先的初始化代码是系统的0号任务不在调度队列中的。

    因此1号任务投入运行,系统转而执行init()函数。这个函数同样定义在 init/main.c 中。

    kernel_init()函数接着完成系统更高层次,比如驱动程序,根文件系统等等的初始化工作。其中的do_basic_setup()函数比较重要,这个函数先调用 driver_init()函数完成驱动程序的初始化,又通过 do_initcalls()函数依次调用了系统中所有的初始化函数。

    在 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,创建终端,等待用户登录等等,系统启动完成。