处理器上电以后,首先执行引导程序,引导程序把内核加载到内存,然后执行内核,内核初始化完成以后,启动用户空间的第一个进程。
引导程序在哪读取?
处理器在上电时自动把程序计数器设置为处理器厂商设计的某个固定值,对于 ARM64 处理器,这个固定值是0。处理器的内存管理单元 (Memory Management Unit, MMU) 负责把虚拟地址转换为物理地址, ARM64 处理器刚上电的时候是没有开启内存管理单元的,物理地址和虚拟地址相同,所以 ARM64 处理器到物理地址 0 取第一条指令。嵌入式设备通常使用 NOR 闪存作为只读存储器来存放引导程序。从物理地址 0 开始的一段物理地址空间被分配给 NOR 闪存。
ARM64 处理器到虚拟地址 0 取指令,就是到物理地址 0 取指令,也就是到 NOR 闪存的起始位置取指令。
引导程序
ARM64 处理器的 U-Boot(Universal Boot Loader) 程序的执行入口是文件 “arch/arm/cp armv8/start.S” 定义的标号_start 。
为U-Boot分配临时栈,并将引导程序复制到内存中。当引导程序初始化完成后,开始执行其中的命令,其中重要的操作是函数 bootm_find_os 把内核镜像从存储设备读到内存,函数 bootm_load_os 把内核加载到正确的位置,如果内核镜像是被压缩过的,需要解压缩。
内核初始化
内核初始化分为汇编语言部分和 C 语言部分。
汇编部分
汇编部分的主要处理如下:
arch/arm64/kernel/head.S
1 | ENTRY(stext) |
第 7 行代码,调用函数 __ __create_page_tables __, 创建页表映射。
第 14 行代码,调用函数 __ cpu_setup, 为开启处理器的内存管理单元做准备,初始化处理器。
第 15 行代码,调用函数 __ primary_switch, 为主处理器开启内存管理单元,搭建 C 语言执行环境,进入 C 语言部分的入口函数 start_kernel 。
__create_page_tables函数
函数 _create_page_tables 的主要工作如下
(1) 创建恒等映射(identity mapping)
(2) 为内核镜像创建映射。
恒等映射的特点是虚拟地址和物理地址相同,是为了在开启处理器的内存管理单元的一瞬间能够平滑过渡。函数 __enable_mnu 负责开启内存管理单元,内核把函数 enable_mmu 附近的代码放在恒等映射代码节 (.idmap.text) 里面,恒等映射代码节的起始地址存放在全局变量 _idmap_text_start 中,结束地址存放在全局变 **idmap_text_end **中。
恒等映射是为恒等映射代码节创建的映射, idmap_pg_dir 是恒等映射的页全局目录(即第一级页表)的起始地址。在内核的页表中为内核镜像创建映射,内核镜像的起始地址是 _text, 结束地址是 _end, swapper_pg_dir是内核的页全局目录的起始地址。
__primary_switch函数
函数**__primary_switch** 的主要执行流程如下:
- 调用函数__enable_mmu 以开启内存管理单元
- 调用函数**__primary_switched** 函数(不是 switch 函数)
enable_mmu 的主要执行流程如下:
- 把转换表基准寄存器 0 (TTBR0_EL1) 设置为恒等映射的页全局目录的起始物理地址。
- 把转换表基准寄存器 1 (TTBR1_EL1) 设置为内核的页全局目录的起始物理地址。
- 设置系统控制寄存器 (SCTLR_EL1), 开启内存管理单元,以后执行程序时内存管理单元将会把虚拟地址转换成物理地址。
函数**__primary_switched** 的执行流程如下:
(1) 把当前异常级别的栈指针寄存器设置为 0 号线程内核栈的顶部 (init_thread_union + THREAD_ SIZE)
(2) 把异常级别 0 的栈指针寄存器( SP_EL0) 设置为 0 号线程的结构体 thread_info 地址 (init_task.thread_info)
(3) 把向量基准地址寄存器 (VBAR_EL1) 设置为异常向量表的起始地址 (vectors)
(4) 计算内核镜像的起始虚拟地址 (kimage_vaddr) 和物理地址的差值,保存在全局变量 kimage_voffset 中
(5) 用 0 初始化内核的未初始化数据段
(6) 调用 C 语言函数 start_kernel
C语言部分
内核初始化的 C 语言部分入口是函数 start_kernel, 函数 start_kernel 首先初始化基础设施,即初始化内核的各个子系统,然后调用函数 rest_init 函数。rest_init 的执行流程如下。
(1) 创建 1 号线程,即 init 线程,线程函数是 kernel_init
(2) 创建 2 号线程,即 kthreadd 线程,负责创建内核线程
(3) 0 号线程最终变成空闲线程
init 线程继续初始化,执行的主要操作如下:
(1) smp_prepare_cpus(): 在启动从处理器以前执行准备工作
(2) do_pre_smp_initcalls(): 执行必须在初始化 SMP 系统以前执行的早期初始化,即使用宏 early_initcall 注册的初始化函数。
(3) smp_init(): 初始化 SMP 系统,启动所有从处理器。
(4) do_initcalls(): 执行级别 0~7 的初始化。
(5) 打开控制台的字符设备文件 “/dev/console”, 文件描述符0、1 和 2 分别是标准输入、标准输出和标准错误,都是控制台的字符设备文件。
(6) prepare_namespace(): 挂载根文件系统,后面装载 init 程序时需要从存储设备上的文件系统中读文件。
(7) free_initmem(): 释放初始化代码和数据占用的内存。
(8) 装载 init 程序 (U-Boot 程序可以传递内核参数 “init= “ 以指定 init 程序),从内核线程转换成用户空间的 init 进程。
do_initcalls()函数
级别 0~7 的初始化,是指使用以下宏注册的初始化函数:
**include/linux/init.h **
1 |
SMP系统
对称多处理器 (Symmetric Multi-Processor, SMP) 系统包含多个处理器,并且每个处理器的地位平等。在启动过程中 ,处理器的地位不是平等的, 0 号处理器称为引导处理器, 负责执行引导程序和初始化内核;其他处理器称为从处理器,等待引导处理器完成初始化。 引导处理器初始化内核以后,启动从处理器。