本文约3000字,今天完成了基于AX615平台长电型号(SPI NOR Flash + U‑Boot)的一次根文件系统运行方式改造,本文来详细介绍如何将根文件系统从Flash分区迁移到内存中,通过内核initrd机制完成rootfs挂载到内存,并重点整理调试过程中遇到的两个典型“坑”及其解决方案。
关注公众号, 即可获得与Linux相关的电子书籍以及常用开发工具。
01PART
— — —
挂载“放弃物理分区,用内存”
背后原因
做这次改动的根本原因是因为没有A/B分区,挂载squashfs格式的rootfs到物理分区,因squashfs日志型文件系统有个特点,不是一次性全部加载到内存,而是按需提取,这会导致ota升级时一边擦除一边使用会引起冲突,最终ota升级系统成砖的情况。
我们决定将根文件系统制作成 Squashfs 只读镜像,由U‑Boot 在启动时从Flash加载到内存,然后通过内核的initrd机制将其作为根设备。这样,根文件系统完全在内存中运行,既提高了启动速度,又杜绝了对Flash的写操作(所有临时数据通过挂载到tmpfs 解决)。
02PART
— — —
方案架构梳理
原理分析
整个启动过程如下:
U‑Boot 启动 1.从Flash中读取rootfs镜像数据到DRAM的指定地址 2.root=/dev/ram0 3.跳转内核内核启动 1.解析initrd参数,将内存区域模拟为 /dev/ram0 设备 2.挂载 /dev/ram0(文件系统类型为 squashfs)作为根 3.执行 /linuxrc或/sbin/init完成系统初始化initrd(Initial RAM Disk):内核将内存中的一段数据作为块设备(/dev/ramX)处理,然后挂载其中的文件系统。它支持任意文件系统类型(如 ext2、squashfs)。
initramfs:内核将cpio归档解压到tmpfs中,形成可读写根,然后执行 /init。它只接受 cpio格式。
我们选择initrd+squashfs组合,因为:
1.Squashfs压缩率高,节省内存空间;
2.无需制作cpio包,减少构建步骤;
3.内核原生支持挂载squashfs,性能优良。
03PART
— — —
我的修改
uboot/kernel/project.mak
确保以下选项被选中(.config 中为 y):
.config CONFIG_BLK_DEV=yCONFIG_BLK_DEV_LOOP=yCONFIG_BLK_DEV_INITRD=yCONFIG_BLK_DEV_RAM=yCONFIG_BLK_DEV_RAM_COUNT=1CONFIG_BLK_DEV_RAM_SIZE=8192CONFIG_SQUASHFS=y CONFIG_DEVTMPFS=yCONFIG_DEVTMPFS_MOUNT=y
注意:CONFIG_BLK_DEV_RAM_SIZE 必须大于等于 rootfs 镜像解压后的大小(squashfs挂载后占用空间即为镜像大小,因为它是按块读取,并非展开)。若设置过小,挂载会失败。
如果内核未开启 DEVTMPFS,则 /dev/ram0 节点不会自动创建,需手动 mknod,故强烈建议开启。
我们项目中的内存布局规划关键计算如下:
project.mak # DRAM 基址SYSD_RAM_BASE = 0x40000000 # 内核占用大小(包括设备树等)OS_MEM_SIZE = 73 # MB# 内核结束地址即文件系统加载起始CMP_START_ADDR = $(call AddAddressMB, $(SYSD_RAM_BASE), $(OS_MEM_SIZE))# rootfs 镜像大小(来自 Flash 分区)ROOTFS_PARTITION_SIZE_MB = 32# rootfs 内存起始地址(紧挨内核)ROOTFS_START_ADDR = $(shell printf "0x%x" $$(($(CMP_START_ADDR) - $(ROOTFS_PARTITION_SIZE_MB) * 1024 * 1024)))# Flash 中 rootfs 分区的偏移和大小ROOTFS_DATA_FLASH_BASE = $(call calculate_flash_base,$(FLASH_PARTITIONS),rootfs)ROOTFS_SIZE_BYTES = $(shell expr $(ROOTFS_PARTITION_SIZE_MB) \* 1024 \* 1024)# 构建 U‑Boot 加载命令ROOTFS_LOAD_CMD_PARAM = "sf probe; sf read $(ROOTFS_START_ADDR) $(ROOTFS_DATA_FLASH_BASE) $(ROOTFS_SIZE_BYTES)"
特别注意:rootfs 最好被放置在内核结束地址的下方(即低地址),确保不与内核重叠。如果内核占用 73MB 从0x40000000 开始,则结束地址为 0x40000000 + 73*1024*1024 = 0x4400000,rootfs 起始地址即为0x4400000 - 3*1024*1024 = 0x4100000。这样两者之间无空隙,但互不重叠。
此处极易因疏忽导致踩踏内核数据,从而uboot启动内核时报错找不到内核。
修改 uboot/Makefile.common:将读取命令作为宏传递给C代码
makefile EXT_FLAG := ... KCPPFLAGS="-I... -DROOTFS_LOAD_CMD='$(ROOTFS_LOAD_CMD_PARAM)'"
修改 cmd/axera/boot/axera_boot.c:在启动内核前执行该命令
axera_boot.c #define STRINGIFY(x) #x#define TOSTRING(x) STRINGIFY(x)// 在 do_axera_boot() 函数中,加载内核之后、启动之前添加:#ifdef ROOTFS_LOAD_CMD printf("Loading rootfs: %s\n", TOSTRING(ROOTFS_LOAD_CMD)); ret = run_command(TOSTRING(ROOTFS_LOAD_CMD), 0); if (ret) { printf("rootfs load failed\n"); goto failed; }#endif
这样uboot在引导内核前会将Flash中的squashfs镜像完整读入内存指定位置。
bootargs的宏定义是编译到uboot中,然后由uboot传递给内核,在project.mak中重新构造bootargs:
project.mak KERNEL_BOOTARGS = $(OS_MEM) console=ttyS0,115200 root=/dev/ram0 ro rootfstype=squashfs \ initrd=$(ROOTFS_START_ADDR),$(ROOTFS_PARTITION_SIZE_BYTES) \ mtdparts=spi3.0:$(FLASH_PARTITIONS) ...
root=/dev/ram0:指定根设备为 ramdisk。
rootfstype=squashfs:明确文件系统类型,防止内核自动探测失败。
initrd=addr,size:告知内核 initrd 镜像的位置和大小(字节为单位)。
05PART
— — —
我踩过的两大坑与避坑方法
笔记重点
initrd 地址与内核重叠导致数据损坏现象:uboot下启动内核,提示找不到内核。
根源:计算地址时没有完全避开内核所在内存区域,发生了踩踏。
对策:
明确各段内存布局:内核、DTB、initrd 三者必须互不重叠。
在 project.mak 中仔细校验计算公式,使用十六进制打印输出,确保对齐。
现象:在U‑Boot中 printenv bootargs 显示已更新,但内核启动时打印的 Kernel command line 仍是旧值,导致initrd参数未传递。
根源:U‑Boot的环境变量通常保存在Flash中,并设有缓存。若只修改了内存中的环境变量(如通过 setenv),但未执行 saveenv,则下次重启或重载时会从存储区恢复旧值。此外,若代码中通过 CONFIG_BOOTARGS 硬编码了默认值,并且环境变量未覆盖,也会导致忽略用户设置。
对策:
在U‑Boot 命令行修改后,务必执行 saveenv。
若通过构建系统生成 bootargs(如本文),bootargs 中正确赋值,并在启动前通过 printenv bootargs 确认。
可以在U‑Boot脚本中直接执行 setenv bootargs ... 再 saveenv,或直接先擦除env分区。
注意: 如果内核配置为 CONFIG_INITRAMFS_SOURCE(内嵌 initramfs),则内核会优先使用内嵌的cpio而忽略外部initrd。因此应确保该选项为空,以允许外部 initrd 生效。
05PART
— — —
关于Squashfs与CPIO格式的选择分析
场景选择
很多传统方案将 rootfs 制作成 cpio.gz 或 cpio.lzma,作为 initramfs 使用。而我们直接使用Squashfs镜像,两者适用场景对比如下:
| 镜像格式 | ||
| 挂载方式 | ||
| 内存占用 | ||
| 启动速度 | ||
| 可写性 | ||
| 构建复杂度 | ||
| 适用场景 |
在我们的场景中,内存资源有限(DRAM 总大小128MB),故 Squashfs + initrd 是更优选择。它不仅节省内存,还减少了内核解压时间,整体启动耗时更短。
06PART
— — —
文末小结
牢记于心
通过本次改造,成功将根文件系统从Flash分区迁移到内存中挂载,整个过程仅需调整内核配置、内存地址计算和U‑Boot加载逻辑,无需修改根文件系统本身。
内核配置三件套:BLK_DEV_INITRD、BLK_DEV_RAM、SQUASHFS,缺一不可;
内存地址计算务必严谨,建议画出内存布局图,避免重叠;
U‑Boot 环境变量修改后立即保存,避免缓存干扰;
根据实际需求选择文件系统格式,Squashfs 适合只读内存运行,CPIO 更适合灵活的 initramfs。
💡 提示: 总结也是让自己将原理,坑点牢记于心,同时也希望本文能为同类项目的开发者提供参考,让后来者少走弯路。
这里是女程序员的笔记本
15年+嵌入式软件工程师兼二胎宝妈
分享读书心得、工作经验,自我成长和生活方式。
希望我的文字能对你有所帮助