嵌入式Linux系统组成:Bootloader、内核、rootfs、设备树的关系,以及rootfs的启动流程
在RK3568平台的实际开发中,很多工程师在完成内核编译后,面对烧录镜像时常常会困惑:为什么需要烧录四个不同的镜像文件?它们各自承担什么职责?为什么设备树(DTB)既不属于内核镜像,也不属于rootfs,却必须单独分区存放?要回答这些问题,必须从嵌入式Linux系统的整体组成出发,理清Bootloader、内核、rootfs与设备树之间的依赖关系和启动时序。本章将围绕这四个核心组件的职责边界、协作机制以及rootfs从挂载到用户态初始化的完整流程展开,并结合RK3568的硬件特性给出工程实践中的关键要点。
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失败"的问题。以下是一套快速定位组件匹配性的检查流程:
- 查看Bootloader启动日志中的版本号,确认U-Boot版本是否支持当前内核的启动方式(如FIT镜像或普通zImage)。
- 检查内核启动日志中"Machine model"字段,确认DTB是否正确描述了当前板卡型号。如果显示"Unknown"或错误型号,说明DTB与板卡不匹配。
- 确认内核配置中CONFIG_CMDLINE与Bootloader传递的bootargs是否冲突。RK3568平台通常以Bootloader传入的bootargs为准。
- 验证rootfs类型是否被内核支持。例如,如果rootfs是ext4格式,内核必须开启CONFIG_EXT4_FS;如果是initramfs,需要确认CONFIG_BLK_DEV_INITRD=y。
mkimage.sh)默认支持这种分离方式。
2.1.3 案例拆解:RK3568默认分区表与镜像对应关系
以Rockchip官方SDK为例,eMMC的默认分区表如下:
| 分区名 | 起始地址 | 大小 | 镜像文件 | 作用 |
|---|---|---|---|---|
| uboot | 0x4000 | 4MB | uboot.img | U-Boot主镜像 |
| trust | 0x6000 | 4MB | trust.img | ATF(ARM可信固件) |
| misc | 0x8000 | 4MB | misc.img | 启动模式标志 |
| dtb | 0x9000 | 8MB | dtb.img | 设备树镜像 |
| boot | 0xA000 | 64MB | boot.img | 内核镜像 |
| rootfs | 0x14000 | 512MB | rootfs.img | 根文件系统 |
| userdata | 0x54000 | 剩余 | — | 用户数据分区 |
这个分区设计体现了组件分离的原则:Bootloader、内核、DTB、rootfs各自独立,便于单独升级和调试。例如,当需要修改GPIO复用功能时,只需重新编译DTB并烧录到dtb分区,无需重新烧录内核和rootfs。
2.2 rootfs的启动流程:从内核挂载到init进程
rootfs的启动流程是嵌入式Linux系统从内核态切换到用户态的关键转折点。理解这一过程,对于排查启动失败、优化启动时间、定制系统服务都至关重要。
2.2.1 核心原理:内核如何找到并挂载rootfs
内核在完成设备初始化后,会调用prepare_namespace()函数来准备根文件系统。具体流程如下:
- 内核解析bootargs中的
root=参数,该参数指定根文件系统的位置。常见形式包括:root=/dev/mmcblk0p6(eMMC分区)、root=UUID=xxxx(按UUID)、root=/dev/nfs(网络文件系统)。 - 如果bootargs中包含
initrd=或initramfs=参数,内核会优先将initramfs镜像解压到内存中作为临时根文件系统。initramfs是一个cpio格式的归档文件,包含最基本的目录结构和init程序。 - 内核根据
root=指定的设备,调用对应文件系统驱动(如ext4、squashfs、nfs)来挂载根文件系统。挂载成功后,内核会检查根目录下是否存在init文件(或bootargs中init=指定的程序)。 - 内核通过
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:
| 对比维度 | 直接挂载rootfs | initramfs过渡 |
|---|---|---|
| 启动速度 | 快(无需解压额外镜像) | 稍慢(需解压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。
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大小、并行启动服务)打下基础。
init=/bin/sh,这样内核会直接进入shell而不是启动完整系统,便于手动检查rootfs挂载状态和文件完整性。排查完毕后,再恢复为init=/sbin/init。