嵌入式Linux系统组成:Bootloader、内核、rootfs、设备树的关系,以及rootfs的启动流程

在RK3568平台的实际开发中,很多工程师在完成内核编译后,面对烧录镜像时常常会困惑:为什么需要烧录四个不同的镜像文件?它们各自承担什么职责?为什么设备树(DTB)既不属于内核镜像,也不属于rootfs,却必须单独分区存放?要回答这些问题,必须从嵌入式Linux系统的整体组成出发,理清Bootloader、内核、rootfs与设备树之间的依赖关系和启动时序。本章将围绕这四个核心组件的职责边界、协作机制以及rootfs从挂载到用户态初始化的完整流程展开,并结合RK3568的硬件特性给出工程实践中的关键要点。

RK3568 嵌入式Linux系统组成与启动链路 硬件层:RK3568 SoC(CPU / DDR / eMMC / SD / 串口 / 网口 / USB) 上电后从 Boot ROM 启动,加载 Bootloader 到 SRAM Bootloader(U-Boot / Rockchip MiniLoader) 初始化DDR、时钟、存储控制器 → 加载设备树到内存 → 加载内核镜像到内存 传递启动参数(bootargs)→ 跳转到内核入口 Linux Kernel(boot.img / kernel.img) 解压自身 → 解析设备树(DTB)→ 注册平台设备与驱动 → 挂载 initramfs 或直接挂载 rootfs 执行 /init 或 /sbin/init 完成用户态初始化 设备树(DTB / dtb.img) 描述硬件拓扑:CPU、内存基址、外设寄存器地址 中断号、时钟频率、引脚复用、I2C/SPI/UART 配置 rootfs(rootfs.img / system.img) 包含 /bin /sbin /etc /lib /usr /var 等目录 提供 init 进程、动态链接库、配置文件、应用程序 启动流程编号: ① Boot ROM → ② Bootloader(DDR初始化) → ③ 加载DTB到内存 → ④ 加载Kernel到内存 ⑤ 内核解析DTB → ⑥ 挂载rootfs(initramfs或块设备) → ⑦ 执行/sbin/init → ⑧ 启动系统服务 eMMC分区:| uboot | trust | misc | dtb | boot | rootfs | userdata |

2.1 四大组件的职责边界与依赖关系

嵌入式Linux系统与桌面Linux最大的区别在于,它没有BIOS或UEFI固件来统一管理硬件初始化,而是由Bootloader、内核、设备树和rootfs四个独立镜像协同完成从上电到用户态可用的完整过程。在RK3568平台上,这四个组件分别对应不同的分区和镜像文件,任何一个缺失或版本不匹配都会导致系统无法正常启动。

2.1.1 核心原理:各组件在启动链路中的角色

Bootloader(RK3568上通常为U-Boot或Rockchip官方MiniLoader)是上电后第一个执行的软件。它运行在SRAM或DDR中,负责最基础的硬件初始化:设置CPU工作模式、初始化DDR控制器、配置时钟树、使能存储接口(eMMC/SD/NAND)、初始化串口用于调试输出。Bootloader的核心任务是将内核镜像和设备树从存储介质加载到DDR的指定地址,并准备好启动参数(bootargs),最终跳转到内核入口。

Linux内核是操作系统的核心,负责进程管理、内存管理、文件系统、网络协议栈以及设备驱动框架。但内核本身并不包含具体硬件型号的寄存器级配置信息,它通过设备树来获取这些信息。内核启动早期会解压自身,然后解析设备树二进制(DTB),根据DTB中的节点描述来注册平台设备、匹配驱动、初始化外设。

设备树(DTB)本质上是一个描述硬件拓扑的数据结构,它把"哪个芯片挂在哪个I2C总线上"、"GPIO复用为UART还是PWM"、"中断号是多少"这类硬件细节从内核源码中剥离出来。RK3568的每个板级设计(如EVB、核心板、自定义底板)都有对应的.dts源文件,编译后生成.dtb镜像。设备树与内核版本有兼容性要求,但允许一定的灵活性——同一内核可以配合不同DTB运行在不同硬件上。

rootfs是根文件系统,它提供了Linux运行所需的目录结构、动态链接库、系统工具、配置文件和应用程序。内核启动完成后,必须挂载一个根文件系统才能执行第一个用户态进程(init)。rootfs可以是initramfs(内存文件系统)、eMMC/SD上的ext4分区、NFS网络文件系统或只读的squashfs。

2.1.2 判断方法:如何确认各组件版本匹配

在实际开发中,经常遇到"内核启动到一半卡死"或"挂载rootfs失败"的问题。以下是一套快速定位组件匹配性的检查流程:

  1. 查看Bootloader启动日志中的版本号,确认U-Boot版本是否支持当前内核的启动方式(如FIT镜像或普通zImage)。
  2. 检查内核启动日志中"Machine model"字段,确认DTB是否正确描述了当前板卡型号。如果显示"Unknown"或错误型号,说明DTB与板卡不匹配。
  3. 确认内核配置中CONFIG_CMDLINE与Bootloader传递的bootargs是否冲突。RK3568平台通常以Bootloader传入的bootargs为准。
  4. 验证rootfs类型是否被内核支持。例如,如果rootfs是ext4格式,内核必须开启CONFIG_EXT4_FS;如果是initramfs,需要确认CONFIG_BLK_DEV_INITRD=y。
经验提示:在RK3568开发中,建议将设备树与内核分开烧录,而不是打包进内核镜像。这样在调试不同底板时,只需更换dtb分区,无需重新编译内核。Rockchip的打包脚本(如mkimage.sh)默认支持这种分离方式。

2.1.3 案例拆解:RK3568默认分区表与镜像对应关系

以Rockchip官方SDK为例,eMMC的默认分区表如下:

分区名起始地址大小镜像文件作用
uboot0x40004MBuboot.imgU-Boot主镜像
trust0x60004MBtrust.imgATF(ARM可信固件)
misc0x80004MBmisc.img启动模式标志
dtb0x90008MBdtb.img设备树镜像
boot0xA00064MBboot.img内核镜像
rootfs0x14000512MBrootfs.img根文件系统
userdata0x54000剩余—用户数据分区

这个分区设计体现了组件分离的原则:Bootloader、内核、DTB、rootfs各自独立,便于单独升级和调试。例如,当需要修改GPIO复用功能时,只需重新编译DTB并烧录到dtb分区,无需重新烧录内核和rootfs。

2.2 rootfs的启动流程:从内核挂载到init进程

rootfs的启动流程是嵌入式Linux系统从内核态切换到用户态的关键转折点。理解这一过程,对于排查启动失败、优化启动时间、定制系统服务都至关重要。

2.2.1 核心原理:内核如何找到并挂载rootfs

内核在完成设备初始化后,会调用prepare_namespace()函数来准备根文件系统。具体流程如下:

  1. 内核解析bootargs中的root=参数,该参数指定根文件系统的位置。常见形式包括:root=/dev/mmcblk0p6(eMMC分区)、root=UUID=xxxx(按UUID)、root=/dev/nfs(网络文件系统)。
  2. 如果bootargs中包含initrd=或initramfs=参数,内核会优先将initramfs镜像解压到内存中作为临时根文件系统。initramfs是一个cpio格式的归档文件,包含最基本的目录结构和init程序。
  3. 内核根据root=指定的设备,调用对应文件系统驱动(如ext4、squashfs、nfs)来挂载根文件系统。挂载成功后,内核会检查根目录下是否存在init文件(或bootargs中init=指定的程序)。
  4. 内核通过run_init_process()执行init程序,从此系统从内核态切换到用户态。init进程的PID为1,是所有用户态进程的祖先。

在RK3568平台上,常见的rootfs挂载方式有两种:一种是直接将eMMC的rootfs分区挂载为根文件系统;另一种是使用initramfs作为过渡,initramfs中包含一个小的init程序,负责加载必要的驱动(如eMMC驱动)后再切换到真正的rootfs。第二种方式在需要早期加载驱动模块时非常有用。

2.2.2 操作流程:从Bootloader到init的完整启动时序

以下是在RK3568 EVB板上,使用eMMC启动时完整的rootfs启动流程:

// Bootloader阶段(U-Boot串口输出)
U-Boot 2017.09-g5a2b3c4 (Mar 12 2024 - 10:30:00)
DRAM:  2 GiB
MMC:   dwmmc@fe310000: 1, dwmmc@fe320000: 0
Loading DTB from mmc 0:0x9000 ... OK
Loading Kernel from mmc 0:0xA000 ... OK
Starting kernel ...

// 内核阶段
[    0.000000] Booting Linux on physical CPU 0x0
[    0.000000] Machine model: Rockchip RK3568 EVB1 DDR4 V2.0
[    0.000000] Kernel command line: root=/dev/mmcblk0p6 rootwait rw console=ttyS2,1500000
[    1.234567] mmc0: new HS400 MMC card at address 0001
[    1.240000] mmcblk0: mmc0:0001 14.6 GiB
[    1.245000] mmcblk0p6: 512 MiB ext4 filesystem
[    1.250000] EXT4-fs (mmcblk0p6): mounted filesystem with ordered data mode
[    1.260000] VFS: Mounted root (ext4 filesystem) on device 179:6.
[    1.270000] Freeing unused kernel memory: 2048K
[    1.280000] Run /sbin/init as init process

// 用户态阶段
Starting syslogd: OK
Starting klogd: OK
Starting network: OK
Starting sshd: OK

Welcome to Buildroot for RK3568
rk3568 login:

从日志中可以清晰看到三个阶段:Bootloader加载DTB和内核、内核挂载ext4根文件系统、执行/sbin/init进入用户态。其中rootwait参数告诉内核等待eMMC设备就绪后再尝试挂载,避免因设备未准备好而挂载失败。

2.2.3 案例拆解:initramfs与直接挂载rootfs的对比

在RK3568项目中,我们曾遇到一个场景:客户需要从eMMC启动,但eMMC驱动需要以模块方式加载(而非编译进内核)。此时如果直接挂载eMMC上的rootfs,内核会因为无法识别eMMC设备而挂载失败。解决方案是使用initramfs:

对比维度直接挂载rootfsinitramfs过渡
启动速度快(无需解压额外镜像)稍慢(需解压initramfs)
驱动加载必须编译进内核可从initramfs加载模块
灵活性低(rootfs必须包含完整系统)高(initramfs可定制最小环境)
调试便利性启动日志直接输出initramfs中可加入调试工具
镜像大小rootfs通常100MB+initramfs可控制在10MB以内

具体实现时,需要在内核配置中开启CONFIG_BLK_DEV_INITRD=y,并在bootargs中指定initrd=0x06000000,0x100000(内存地址和大小)。initramfs中的init脚本负责加载eMMC驱动模块,然后执行mount -t ext4 /dev/mmcblk0p6 /sysroot,最后通过switch_root切换到真正的rootfs。

风险提示:使用initramfs时,必须确保initramfs中包含了切换到真实rootfs所需的完整工具链(如mount、switch_root、必要的动态库)。否则会出现"Failed to execute /init"错误。建议使用BusyBox构建最小initramfs,并静态编译关键工具。

2.2.4 常见误区与总结复盘

在rootfs启动流程中,工程师常犯以下错误:

  • 误区一:认为root=参数可以随意指定。实际上,如果使用root=/dev/mmcblk0p6,内核必须等待该设备节点出现,否则会报"VFS: Unable to mount root fs"。此时应添加rootwait参数。
  • 误区二:忽略init程序的可执行权限。内核执行init时,如果文件没有执行权限,会报"Permission denied"。建议在构建rootfs时统一设置chmod 755 /sbin/init。
  • 误区三:混淆initramfs和initrd。initramfs是cpio格式,由内核直接解压到rootfs;initrd是块设备镜像,需要内核模拟RAM磁盘来挂载。RK3568 SDK默认使用initramfs方式。
  • 误区四:在rootfs中缺少必要的设备节点。如果使用devtmpfs,内核会自动生成设备节点;但如果使用静态/dev目录,必须手动创建/dev/console和/dev/null,否则init无法启动。

总结来说,rootfs的启动流程可以概括为:内核根据bootargs定位rootfs设备 → 挂载文件系统 → 执行init程序 → init根据/etc/inittab或systemd配置启动系统服务。在RK3568平台上,理解这一流程有助于快速定位启动失败问题,并为后续的启动优化(如减少initramfs大小、并行启动服务)打下基础。

实战要点:在调试RK3568启动问题时,建议在bootargs中临时添加init=/bin/sh,这样内核会直接进入shell而不是启动完整系统,便于手动检查rootfs挂载状态和文件完整性。排查完毕后,再恢复为init=/sbin/init。