Linux内核启动流程

目录

Uboot启动流程

BL0阶段 [运行在ROM]

疑问

BL1 [运行在soc内部SRAM] spl阶段

其他

BL2 [运行在外部DDR] 完整uboot阶段

NandFlash设备的分区方案

uboot整体编译流程

BL1与BL2阶段详述

BL1阶段

 代码入口

疑问

BL2板级初始化

函数调用

疑问

为什么要将uboot进行重定向?

流程简单总结

uboot启动kernel相关的指令

vmlinuz、zimage和uimage的区别

bootcmd环境变量

 bootargs环境变量

u-boot启动内核的过程

Linux内核启动流程

启动参数

Linux内核启动分为两个阶段

第一阶段

汇编函数中主要操作

第二阶段

setup_arch(&command_line)

mm_init()

fork_init(void)

rest_init()

1号进程init

kernel_init_freeable();

设备驱动初始化do_basic_setup

挂在根文件系统prepare_namespace();

疑问

2号进程kthreadd

0号进程idle

参考文献


Uboot启动流程

Linux内核启动流程_第1张图片

BL0阶段 [运行在ROM]

 1、上电后CPU会直接从IROM上开始执行引导程序(通常是汇编代码),对cpu的寄存器初始化,启动核0,等待核0在内核启动正常后通过中断或者事件将其他核唤醒

2、初始化时钟、关看门狗、根据 Bootstrap Pin 来确定启动设备,比如启动设备是flash

2、初始化flash和SRAM,为后面从flash加载bin文件,在DDR中运行程序提供基本环境

3、将flash中的uboot-spl镜像文件加载到SRAM

疑问

为什么不是直接将完整的uboot装载到外部内存DDR运行?

        soc厂商一般支持多种类型DDR,SoC 厂家怎么可能知道用户 PCB 上到底用了哪种内存?直接将uboot装在到SRAM中又太大了装不下,比如SRAM大小8K,uboot大小200K。

        1、将uboot截断,前面 8K 被加载进入 SRAM 执行,需要保证在 u-boot 的前 8KB 代码,把板载的 DDR 初始化好,把整个 u-boot 拷贝到 DDR

        2、先做一个小的 u-boot ,这个 u-boot 就叫做 spl,它先被 rom中的引导程序加载到 SRAM 运行,其最主要的就是要初始化 DDR Controller,然后将真正的大 u-boot 从外部存储器读取到 DDR 中,然后跳转到大 u-boot

BL1 [运行在soc内部SRAM] spl阶段

BL1阶段代码通常放在start.s文件中

        1、SPL 初始化外部 DDR

        2、SPL 使用驱动从外部存储器读取 u-boot 并放到 DDR

        3、跳转到 DDR 中的 u-boot 执行

其他

CONFIG_SPL
用于指定是否需要编译SPL,也就是是否需要编译出uboot-spl.bin文件

连接脚本的位置在
u-boot/arch/arm/cpu/u-boot-spl.lds

BL2 [运行在外部DDR] 完整uboot阶段

1、初始化大部分硬件

2、读取环境变量,执行用户命令

3、引导加载内核

NandFlash设备的分区方案

uboot整体编译流程

Linux内核启动流程_第2张图片

Linux内核启动流程_第3张图片

BL1与BL2阶段详述

BL1阶段

uboot的BL1阶段代码通常放在start.s文件中,用汇编语言实现
arch级初始化是和spl完全一致

 代码入口

u-boot/arch/arm/cpu/u-boot.lds(u-boot-spl.lds好像和这差不多,猜测uboot和uboot-spl arch级初始化是一样的)

ENTRY(_start)

reset

SoC上电复位后运行的第一段代码就是reset

A、关闭IRQ、FIQ,并将处理器模式设置为SVC模式

B、    CPU关键寄存器的初始化cpu_init_crit:

    关闭L2 cache

    初始化L2 cache

    开启L2 cache

    关闭L1 cache

    关闭MMU

    读取OM启动引脚信息

    确定从启动设备flash

    设置SRAM中的栈为调用lowlevel_init做准备(lowlevel_init内部有嵌套调用)

    调用lowlevel_init(主要初始化系统时钟、SDRAM初始化、串口初始化等)

    设置SDRAM中的栈

    从flash拷贝uboot到SDRAM


C、设置MMU,开启MMU

D、通过对SDRAM整体使用规划,在SDRAM中合适的地方设置栈

E、清除bss段,远跳转到start_armboot执行,BL1阶段执行完

疑问

在spl的阶段中已经对arch级进行了初始化了,为什么uboot里面还要对arch再初始化一遍?
        spl对于启动uboot来说并不是必须的,在某些情况下,上电之后uboot可能在ROM上或者nor flash上开始执行而并没有使用spl。这些都是取决于平台的启动机制。因此uboot并不会考虑spl是否已经对arch进行了初始化操作,uboot-spl是uboot的子集,uboot会完整的做一遍初始化动作,以保证cpu处于所要求的状态下。

uboot和uboot-spl在启动过程的差异在哪里?
        前期arch的初始化流程基本上是一致的,出现本质区别的是在board_init_f开始的。spl的board_init_f是由board自己实现相应的功能,其主要实现了复制uboot到ddr中,并且跳转到uboot的对应位置上。一般spl在这里就可以完成自己的工作了。uboot的board_init_f是在common下实现的,其主要实现uboot relocate前的板级初始化以及relocate的区域规划,其还需要往下走其他初始化流程
Linux内核启动流程_第4张图片
BL2板级初始化

感觉BL1是BL2的子集,其实这部分应该是囊括BL1阶段的,下面应该是BL1没有的部分

函数调用

Linux内核启动流程_第5张图片

疑问
为什么要将uboot进行重定向?

考虑以下问题
* 在某些情况下,uboot是在某些只读存储器上运行,比如ROM、nor flash或者BL1拷贝的仅仅是uboot的一部分。还需要将自身(uboot镜像)完整的拷贝到内存中
* 给kernel腾位置,一般会把kernel放在ddr的低端地址上,防止 Linux kernel 覆盖掉 uboot,将 DRAM 前面的区域完整的空出来

一般情况BL1和BL2的地址并不相同,uboot代码搬到dram之后,代码的运行地址发生了变化,如何保证程序跳转不会出错?
这就有了「位置无关代码」的概念,指代码不在连接时指定的运行地址空间,也可以执行,它一段加载到任意地址空间都能执行的特殊代码,uboot搬移到DRAM中,然后跳转到DRAM继续运行uboot剩下的代码,那么在搬移之前的这段代码必须是位置无关,而且不能使用绝对寻址指令,否则寻址就会出错,详见一口Linux_从0学ARM-什么是位置无关码?

Linux内核启动流程_第6张图片

 

流程简单总结

运行在ROM上的BL0阶段:上电后CPU会直接从IROM上开始执行引导程序,初始化flash和SRAM,然后将flash中的uboot-spl镜像文件加载到SRAM。

运行在SRAM上的BL1阶段:初始化外部 DDR,从flash中将完整的uboot装载到外部DDR中,sp跳转到 DDR 中的 u-boot 其实地址执行

运行在外部DDR上的BL2阶段:根据链接文件u-boot.lds,找到入口_start,先进行arch级初始化,会和spl阶段有重复的部分,然后到_main函数进行板级初始化,重定向前的区域规划,uboot将自己拷贝到 DRAM 最后面的内存区域中,为内核腾出空间,然后继续板级初始化各种还没初始化完的设备,然后命令行状态

uboot启动kernel相关的指令

uboot支持一堆指令有操作环境变量,操作内存、网络、nand flash,文件等,无非是读写IO与内存

通过网络启动Linux内核,用tftp将内核镜像、initrd(如果有)、设备树,传到内存指定位置,再使用bootz命令指定刚刚那内存地址启动linux内核

tftp 80800000 zImage
tftp 83000000 imx6ull-14x14-emmc-7-1024x600-c.dtb
bootz 80800000 - 83000000

bootz启动zImage镜像,bootu启动uimage镜像,其他都一样,问题是zimage和uimage有啥区别?

vmlinuz、zimage和uimage的区别

Linux内核启动流程_第7张图片

编译kernel时, 直接make 生成zImage,make uImage, 生成uImage, 但是会用到mkimage工具

Linux内核经过编译后也会生成一个elf格式的可执行程序,叫vmlinux或vmlinuz,这个是原始的未经任何处理加工的原版内核elf文件。嵌入式系统部署时烧录的一般不是这个vmlinux/vmlinuz,而是要用objcopy工具去制作成烧录镜像格式(就是u-boot.bin这种,但是内核没有.bin后缀),制作出来的镜像文件就叫Image(这个镜像就比elf格式的文件要小很多)。

原则上Image就可以直接烧录到启动介质中,但是实际上linux的开发者认为Image的大小还是太大,所以对Image进行了压缩,并且在Image压缩后的文件的前端附加了一部分解压缩代码。构成了压缩格式的镜像就叫zImage,uImage其实就是在zImage的前面加上64字节的uImage的头信息

有些uboot支持zImage启动,有些则不支持。但是所有的uboot肯定都支持uImage启动

98D板子,烧固件时用run upt,其实upt是一环境变量

也可以使用boot指令启动内核,其实是去指向bootcmd环境变量中的指令集,当然也可以只使用run mycmd,执行mycmd环境变量中的指令

Linux内核启动流程_第8张图片


 

bootcmd环境变量

实践情况中,如果 EMMC 或者 NAND 中没有保存 bootcmd 的值板子,第一次运行 uboot 的时候都会文件 include/env_default.h,中定义字符串,而这字符串使用到的宏在include/configs/中板级相关的头文件中

Linux内核启动流程_第9张图片

 bootargs环境变量

bootargs 保存着 uboot 传递给 Linux 内核的参数,其由 mmcargs 设置的, mmcargs 环境变量如下:mmcargs=setenv bootargs console= ttymxc0, 115200 root= /dev/mmcblk1p2 rootwait rw
而mmcargs环境变量在run bootcmd时会被调用
Linux内核启动流程_第10张图片       

u-boot启动内核的过程

Linux内核启动流程_第11张图片

        

Linux内核启动流程

启动参数

        Linux 内核一开始是汇编代码,因此函数 kernel_entry 就是个汇编函 数。向汇编函数传递参数要使用 r0 r1 r2( 参数数量不超过 3 个的时候 ) ,所以 r2 寄存器就是函数 kernel_entry 的第三个数。
        如果使用设备树的话,r2 应该是设备树的起始地址,而设备树地址保存在 images 的 ftd_addr 成员变量中。
        如果不使用设备树的话,r2 应该是 uboot 传递给 Linux 的参数起始地址,也就 是环境变量 bootargs 的值,

Linux内核启动分为两个阶段

在 linux内核启动前, bootloader会将存储介质中的 initrd 文件加载到内存,内核启动时会在访问真正的根文件系统前先访问该内存中的 initrd 文件系统。在 bootloader 配置了 initrd 的情况下,内核启动被分成了两个阶段,第一阶段先执行 initrd 文件系统中的"某个文件",完成加载驱动模块等任务,第二阶段才会执行真正的根文件系统中的 /sbin/init 进程
 

第一阶段

head.S

arm和mips架构的Linux内核 的入口函数不一样

ARM:ENTRY(stext) arch/arm/kernel/head.S

mips:ENTRY(kernel_entry)

Linux内核启动流程_第12张图片

汇编函数中主要操作
  • 使能SVG模式
  • 关闭所有中断
  • 校验 atags 或设备树(dtb)的合法性
  • 使能MMU

Linux内核启动流程_第13张图片

第二阶段

init/main.c

当前了解过的函数有以下:

setup_arch(&command_line)
页表、CPU相关的初始化,获取启动参数存储在command_line中。arm架构的代码此函数会解析设备树,从设备树的chosen节点获取bootargs

mm_init()

内存相关的初始化,创建并初始化 slab allocator 体系,vmalloc初始化(还没研究),其中slab allocator 体系通过临时静态变量的方式创建第一个slab cache,然后创建创建kmalloc slab缓存(kmalloc 内存池的本质其实还是 slab 内存池,底层依赖于 slab alloactor 体系,在 kmalloc 体系的内部,管理了多个不同尺寸的 slab cache,kmalloc 只不过负责根据内核申请的内存块尺寸大小来选取一个最佳合适尺寸的 slab cache)

fork_init(void)

创建task_struct的slab缓存

rest_init()

reset_init函数共创建了3个进程,分别是idle、kernel_init和kthreadd,依次叫做0号进程,1号进程和2号进程

Linux内核启动流程_第14张图片

1号进程init

kernel_init主要任务是完成设备驱动初始化和挂载根文件系统,并读取根文件系统中init程序,将从内核态转变到用户态。

kernel_init_freeable();
设备驱动初始化do_basic_setup
挂在根文件系统prepare_namespace();
疑问

init程序来源于哪里呢?

根据uboot传来的参数rdinit=xxxx,init=xxxxx,在根文件系统找到要执行的init程序,如果 bootargs 设置 init=/linuxrc,那么 linuxrc 就是可以 作为用户空间的 init 程序,所以用户态空间的 init 程序是 busybox 来生成的,若参数 都为空,那么就依次查找:

/sbin/init、

/etc/init、

/bin/init和

/bin/sh,

这四个相当于备用 init 程序,如果这四个也不存在,那么 Linux 启动失败!

Linux内核启动流程_第15张图片

2号进程kthreadd

内核线程的守护进程,始终运行在内核态,负责所有内核线程的创建。原理是不断循环kthread_create_list全局链表,如果为空,则调度出去;否则则调用create_kthread接口来创建内核线程。

kthread_create_list全局链表的添加是调用kthread_create->kthread_create_on_node->__kthread_create_on_node()实现的,所以说,所有的内核线程都是间接以kthreadd进程为父进程

0号进程idle
最后调用函数 cpu_startup_entry 来进入 idle 进程, cpu_startup_entry 会调用
cpu_idle_loop, cpu_idle_loop 是个 while 循环,也就是 idle 进程代码。 idle 进程的 PID 0 ,idle 进程叫做空闲进程

参考文献

一口Linux_Cortex-A9 uboot启动代码详解

天山老妖S的博客_linux系统移植_51CTO博客

[uboot] (第五章)uboot流程——uboot启动流程_spi_boot_ooonebook的博客-CSDN博客

你可能感兴趣的:(linux,c语言,驱动开发,arm开发)