嵌入式Linux(二) - Arm架构嵌入式Linux启动流程

Arm架构凭借低功耗、高性能的优势,成为嵌入式设备的主流选择。对于嵌入式开发者来说,理解Linux系统从“上电”到“运行用户应用”的完整启动流程,是定位启动故障、优化系统性能的基础。本文将分4个章节,用通俗的语言+直观图表,详细拆解Arm架构下嵌入式Linux的启动全过程。

一、概述:嵌入式Linux启动全流程总览

很多新手会误以为“上电后直接启动Linux内核”,但实际情况更复杂——嵌入式Linux启动是一个“分层接力”的过程,从硬件复位到最终运行用户应用,需要经过4个核心阶段,每个阶段都有明确的核心任务,上一阶段完成后再“交接”给下一阶段。

核心结论先明确:上电复位 → 引导程序(如U-boot)初始化 → 内核初始化 → 用户空间初始化 → 运行用户应用。下面用流程图直观展示各阶段的衔接关系、核心任务及输出结果:

graph TD A[上电复位] -->|硬件自动执行| B[引导程序阶段(如U-boot)] B -->|核心任务:初始化关键硬件(DDR、串口)、加载内核| C[内核初始化阶段] C -->|核心任务:初始化系统资源、创建Init线程| D[用户空间初始化阶段] D -->|核心任务:挂载根文件系统、启动init进程| E[运行用户应用] %% 补充各阶段细节说明 B --> B1[输出:内核镜像+设备树加载到内存] C --> C1[输出:可用的系统内核环境] D --> D1[输出:完整的用户空间环境] E --> E1[最终:执行用户编写的应用程序]

简单解释各阶段的“接力逻辑”:

  • 上电复位:硬件层面的初始动作,CPU会自动从固定地址(通常是ROM)读取引导程序代码,相当于“启动的发令枪”;

  • 引导程序:相当于“启动助手”,负责完成最基础的硬件初始化(比如让内存可用),然后把Linux内核“找出来”并加载到内存;

  • 内核初始化:内核接管系统后,会初始化更复杂的系统资源(比如进程管理、文件系统、网络),并创建第一个用户空间进程;

  • 用户空间初始化:搭建好用户程序运行的环境,最终启动用户应用,完成整个启动流程。

二、引导程序:U-boot与设备树的“启动铺垫”

引导程序是嵌入式Linux启动的“第一步”,位于内核之前,核心作用是“为内核启动做铺垫”。目前Arm架构嵌入式设备中,最主流的引导程序是U-boot(Universal Bootloader),而设备树对象(DTO)则是引导程序和内核之间“传递硬件信息”的关键载体。

2.1 U-boot:嵌入式领域的“万能引导程序”

在U-boot运行期间,CPU的MMU(内存管理单元)通常是没有被初始化的,所有地址访问都直接使用物理地址。U-boot是一个开源的引导程序,支持几乎所有主流的Arm芯片,其核心目标是“初始化关键硬件,加载并启动内核”。U-boot的启动过程可以分为两个阶段:

2.1.1 第一阶段(汇编阶段):最基础的硬件初始化

U-boot的第一阶段代码用汇编语言编写,运行在芯片的内部RAM(IRAM)中(此时外部DDR内存还未初始化,无法使用),核心任务:

  • 初始化芯片核心硬件(如CPU寄存器、时钟控制器、电源管理模块);

  • 初始化外部DDR内存(让内核能后续加载到DDR中);

  • 把U-boot的第二阶段代码(C语言编写)从Flash加载到DDR内存中,然后跳转到第二阶段代码执行。

2.1.2 第二阶段(C语言阶段):功能扩展与内核加载

第二阶段代码用C语言编写,功能更丰富,核心任务:

  1. 初始化更多硬件设备(如串口、SD卡、Flash、网卡);

  2. 提供命令行接口(通过串口或网络),用户可以手动输入命令(如查看内存、烧写镜像、启动内核);

  3. 读取内核镜像和设备树镜像(从Flash、SD卡或网络),加载到DDR内存的指定地址;

  4. 向内核传递启动参数(如bootargs,包含根文件系统位置、串口配置等信息),然后跳转到内核入口地址,把控制权交给内核。

举个实际的U-boot启动内核命令:bootm 0x80008000 - 0x80800000,其中0x80008000是内核镜像在DDR中的地址,0x80800000是设备树镜像的地址。

2.2 设备树对象(DTO):内核与硬件的“沟通桥梁”

在早期的Linux内核中,硬件信息(如GPIO引脚、串口地址、中断号)是硬编码在内核中的——如果更换硬件,就需要修改内核代码并重新编译,非常不方便。而设备树的出现,解决了这个问题。

设备树对象(DTO)是一个二进制文件,由设备树源文件(DTS)编译生成。它的核心作用是“描述硬件信息”,把硬件信息从内核中分离出来,由引导程序传递给内核。

2.2.1 设备树的核心结构

设备树的结构类似“树形目录”,每个节点代表一个硬件设备(如CPU、串口、GPIO),节点下的属性代表设备的具体参数(如地址、中断号、引脚配置)。简化的设备树示例(DTS代码片段):



/{
    #address-cells = <1>;  // 地址长度为1个32位整数
    #size-cells = <1>;     // 大小长度为1个32位整数
    
    cpu: cpu@0 {          // CPU节点,地址为0
        compatible = "arm,cortex-a53";  // 兼容的CPU型号
        reg = <0x0 0x1000>;            // 寄存器地址和大小
    };
    
    uart0: uart@10000000 {  // 串口0节点,地址为0x10000000
        compatible = "arm,pl011";       // 兼容的串口驱动
        reg = <0x10000000 0x1000>;     // 串口寄存器地址和大小
        interrupts = <0 10 4>;         // 中断号和触发方式
    };
};

2.2.2 设备树的工作流程与存储位置

设备树的工作流程:

  1. 开发者根据硬件设计,编写设备树源文件(DTS);

  2. 用设备树编译器(DTC)把DTS编译成二进制的设备树镜像(DTB);

  3. U-boot把DTB和内核镜像一起加载到DDR内存中;

  4. U-boot启动内核时,把DTB的地址传递给内核;

  5. 内核启动后,解析DTB中的硬件信息,自动匹配对应的设备驱动,完成硬件初始化。

关于设备树的存储与查看位置:1. 存储位置:编译生成的DTB文件通常存储在嵌入式设备的Flash或SD卡的特定分区(如boot分区)中,与U-boot镜像、内核镜像放在一起;2. 启动时位置:U-boot启动过程中会将DTB从存储介质加载到DDR内存的指定地址;3. 系统运行时查看:系统启动后,内核会将设备树信息导出到sysfs文件系统中,可通过ls /sys/firmware/devicetree/base命令查看,该目录下以树形结构展示了所有硬件设备的节点和属性信息。

核心优势:设备树实现了“内核与硬件解耦”——更换硬件时,只需修改DTS文件重新编译DTB,无需修改内核代码,大大提高了内核的可移植性。

三、内核初始化:从“镜像解压”到“Init线程创建”

当引导程序(如U-boot)把Linux内核镜像加载到内存后,就会把系统控制权交给内核。内核初始化阶段的核心目标是:把“裸机硬件”变成“能运行用户程序的系统环境”,这个过程可以拆解为4个关键步骤:piggy镜像解压、控制流流转、子系统初始化、Init线程创建。

3.1 piggy:内核镜像的“压缩包载体”

我们编译出来的Linux内核镜像(如zImage),其实是一个“压缩包”——为了减少镜像体积、节省存储和加载时间,内核会被压缩后存储,而piggy就是这个压缩包的“载体”。

具体流程:引导程序加载的zImage中,包含了两部分内容:① 解压程序(头部代码);② 压缩的内核镜像(存储在piggy中)。当内核获得控制权后,首先执行的就是头部的解压程序,把piggy中的压缩内核解压到内存的指定位置,然后跳转到解压后的内核代码继续执行。

小知识:为什么需要piggy?如果内核不压缩,镜像体积会大很多,对于嵌入式设备有限的存储(如Flash)和内存来说,会增加成本和启动时间。piggy的作用就是安全存储压缩内核,配合解压程序完成内核的“解包启动”。

3.2 控制流:内核初始化的“执行路线”

内核解压完成后,会进入启动入口函数(Arm架构下通常是start_kernel()),这是内核初始化的“总入口”,后续的所有初始化工作都从这里开始。控制流的流转过程可以简单理解为:

  1. start_kernel():初始化内核最基础的资源,比如中断控制器、页表、系统时钟等,相当于“搭建内核运行的基础框架”;

  2. rest_init():start_kernel()执行完成后,会调用rest_init(),这个函数的核心作用是“拆分内核线程”——把内核初始化的后续工作交给内核线程执行,同时准备创建用户空间的Init线程;

  3. kernel_init():rest_init()创建的内核线程,负责完成剩余的内核初始化工作(如子系统初始化),最终启动Init线程。

用简化的调用链表示:解压程序 → start_kernel() → rest_init() → kernel_init() → 启动Init线程

3.3 子系统初始化:搭建系统的“核心功能模块”

kernel_init()执行的核心工作就是“子系统初始化”——Linux内核是一个模块化的系统,包含多个核心子系统,这些子系统需要依次初始化才能正常工作。核心子系统包括:

  • 内存管理子系统:初始化内存分配器(如slab、buddy系统),让内核和用户程序能正常申请、释放内存;

  • 进程管理子系统:初始化进程调度器,制定进程的调度规则(如CFS调度器),为后续进程创建和运行做准备;

  • 文件系统子系统:初始化虚拟文件系统(VFS),VFS是Linux的“文件系统抽象层”,能让不同类型的文件系统(如ext4、ramfs)统一对外提供接口;

  • 设备驱动子系统:根据设备树信息,初始化硬件设备的驱动程序(如串口、GPIO、网卡),让内核能操控硬件。

子系统初始化的顺序很关键,比如必须先初始化内存管理子系统,才能后续分配内存给其他子系统。

3.4 Init线程:用户空间的“第一个进程”

内核子系统初始化完成后,kernel_init()会创建一个特殊的线程——Init线程,这个线程是用户空间的“第一个进程”(进程号为1),也是所有用户进程的“祖先”。

Init线程的核心作用:把系统控制权从内核空间转移到用户空间,后续的用户空间初始化工作(如挂载根文件系统、启动应用程序)都由Init线程主导。这里要注意区分“内核线程”和“用户进程”:内核线程运行在内核空间,只能操作内核资源;而用户进程运行在用户空间,通过系统调用访问内核资源,Init线程是第一个进入用户空间的进程。

当引导程序(如U-boot)把Linux内核镜像加载到内存后,就会把系统控制权交给内核。内核初始化阶段的核心目标是:把“裸机硬件”变成“能运行用户程序的系统环境”,这个过程可以拆解为4个关键步骤:piggy镜像解压、控制流流转、子系统初始化、Init线程创建。

四、用户空间初始化:从“根文件系统”到“应用启动”

内核初始化完成后,系统已经具备了基本的运行能力,但用户程序还无法运行——因为缺少用户空间的“运行环境”。用户空间初始化的核心目标是:搭建完整的用户空间环境,最终启动用户应用,这个过程的关键步骤包括:挂载根文件系统、启动init进程、初始化RAM磁盘、处理关机流程。

4.1 根文件系统:用户空间的“基础载体”

很多新手会把“内核”和“根文件系统”混淆,其实两者是独立的:内核负责管理硬件和系统资源,而根文件系统是用户空间的“文件和程序载体”,包含了用户程序、库文件、配置文件等。简单说,根文件系统就是用户空间的“硬盘”,没有它,用户程序就没有存储和运行的地方。

Init线程的第一个核心任务就是“挂载根文件系统”,具体流程:

  1. 内核通过引导程序传递的参数(如U-boot传递的bootargs),知道根文件系统的位置(如存储在Flash的某个分区、SD卡,或网络中);

  2. Init线程根据参数,调用文件系统子系统的接口,挂载根文件系统(比如挂载ext4格式的Flash分区作为根文件系统);

  3. 根文件系统挂载成功后,Init线程会切换到根目录(/),后续的操作都基于根文件系统展开。

常见的根文件系统类型:ext4(适用于Flash)、ramfs(内存文件系统,速度快但断电丢失)、nfs(网络文件系统,适用于开发调试)。

如何查看Linux系统的文件系统类型?有三种常用方法:1. df -T:查看已挂载分区的文件系统类型,输出中“Type”列即为文件系统类型;2. mount:直接查看所有已挂载设备的详细信息,包含文件系统类型;3. blkid:查看块设备的文件系统类型及UUID信息,适用于未挂载或已挂载的分区。

4.2 init进程:用户空间的“总管家”

根文件系统挂载完成后,Init线程会查找并启动根文件系统中的init进程(注意:这里的init进程是用户空间的程序,和前面的Init线程是“衔接关系”)。init进程是用户空间的“总管家”,负责启动和管理其他用户进程,常见的init实现有sysvinit、upstart、systemd(目前主流)。

init进程的核心工作:

  • 读取配置文件(不同init实现的配置文件不同);

  • 根据配置启动必要的服务进程(如串口登录服务、网络服务、日志服务);

  • 启动用户交互终端(如串口终端、SSH终端),让用户能登录系统;

  • 监控子进程状态,若子进程异常退出,根据配置决定是否重启。

不同init实现的具体配置文件:

  1. sysvinit:核心配置文件为/etc/inittab,用于定义默认运行级别、系统启动脚本路径等;启动脚本存储在/etc/init.d/目录下;
  2. upstart:配置文件为/etc/init/*.conf(.conf后缀的文件),采用事件驱动机制,配置更灵活;
  3. systemd:核心配置文件为/etc/systemd/system/(系统级服务)和~/.config/systemd/user/(用户级服务)下的.service文件,每个服务对应一个独立的.service文件,可通过systemctl命令管理。

小细节:进程号为1的进程就是init进程,它是所有用户进程的父进程或祖先进程。如果init进程异常退出,整个用户空间会崩溃,系统会重启或挂起。

4.3 初始化RAM:临时文件系统的“舞台”

在根文件系统挂载完成前,内核需要一个临时的文件系统来存储临时文件和运行部分程序,这个临时文件系统就是“初始化RAM磁盘”(initramfs)。initramfs是基于内存的文件系统,速度快,断电后数据丢失,其核心作用是:

  1. 在根文件系统挂载前,提供临时的文件系统环境,让init进程能先运行起来;

  2. 如果根文件系统存储在特殊设备(如加密分区、网络存储),initramfs中会包含对应的驱动和工具,协助内核挂载根文件系统;

  3. 根文件系统挂载完成后,initramfs会被卸载,释放占用的内存。

initramfs在Linux系统中的具体体现与查询方法:1. 体现形式:initramfs本质是一个内存中的临时文件系统,启动初期会作为根文件系统挂载(此时真实根文件系统未挂载),真实根挂载后会被卸载;2. 查询方法:① lsinitramfs /boot/initrd.img-$(uname -r):查看当前内核对应的initramfs镜像中的文件列表(initramfs通常打包为initrd.img文件存储在/boot目录);② cat /proc/cmdline:查看内核启动参数,若包含“initrd=/boot/initrd.img-xxx”则表示启用了initramfs;③ 启动初期(未卸载initramfs时)可通过mount | grep ramfs查看其挂载状态。

4.4 关机流程:用户空间的“收尾工作”

启动流程的反向就是关机流程,init进程同样主导关机过程:当用户触发关机(如执行shutdown命令),init进程会先向所有用户进程发送终止信号,让进程有时间保存数据和清理资源;所有用户进程终止后,init进程会通知内核卸载所有文件系统,最后内核关闭系统时钟、电源管理等硬件资源,完成关机。

总结:嵌入式Linux启动的“核心逻辑”

回顾整个Arm架构嵌入式Linux的启动流程,核心逻辑就是“分层接力、逐步搭建环境”:从硬件复位开始,引导程序(U-boot)完成基础硬件初始化,把内核和设备树加载到内存;内核初始化系统资源,创建Init线程进入用户空间;用户空间通过init进程挂载根文件系统、启动服务,最终运行用户应用。调整章节顺序后,更贴合“引导程序→内核→用户空间”的实际启动时序,便于理解各阶段的衔接关系。

每个阶段都有明确的“任务边界”,上一阶段为下一阶段铺垫必要的环境,任何一个阶段出现问题,都会导致启动失败。理解这个流程,不仅能帮助我们快速定位启动故障(比如U-boot阶段失败可能是硬件初始化问题,用户空间启动失败可能是根文件系统问题),也能为后续的系统优化(如缩短启动时间、定制启动流程)打下基础。同时,掌握文件系统类型查看、init配置文件定位、initramfs查询、设备树位置查找等实操方法,能更深入地掌控嵌入式Linux系统。

回顾整个Arm架构嵌入式Linux的启动流程,核心逻辑就是“分层接力、逐步搭建环境”:从硬件复位开始,引导程序(U-boot)完成基础硬件初始化,把内核和设备树加载到内存;内核初始化系统资源,创建Init线程进入用户空间;用户空间通过init进程挂载根文件系统、启动服务,最终运行用户应用。

每个阶段都有明确的“任务边界”,上一阶段为下一阶段铺垫必要的环境,任何一个阶段出现问题,都会导致启动失败。理解这个流程,不仅能帮助我们快速定位启动故障(比如U-boot阶段失败可能是硬件初始化问题,用户空间启动失败可能是根文件系统问题),也能为后续的系统优化(如缩短启动时间、定制启动流程)打下基础。

posted @ 2025-12-22 17:49  Asp1rant  阅读(7)  评论(0)    收藏  举报