找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

5939

积分

0

好友

752

主题
发表于 昨天 15:49 | 查看: 7| 回复: 0

这次改动的直接原因是设备没有 A/B 分区。如果继续把 Squashfs 格式的 rootfs 挂载到物理 Flash 分区,由于 Squashfs 只读文件系统按需读取、不会一次性全部载入内存,OTA 升级时一边擦除 Flash 一边使用就会产生竞争,最终可能导致系统成砖。

所以我们把根文件系统制作成 Squashfs 只读镜像,由 U‑Boot 在启动时从 Flash 加载到内存,再通过内核的 initrd 机制将其作为根设备。这样根文件系统完全在内存中运行,既加快了启动速度,也避免了对 Flash 的写操作,所有临时数据通过挂载到 tmpfs 解决。

方案架构梳理与原理分析

整个启动过程如下:

U‑Boot 启动
  1.从Flash中读取rootfs镜像数据到DRAM的指定地址
  2.root=/dev/ram0
  3.跳转内核

内核启动
  1.解析initrd参数,将内存区域模拟为 /dev/ram0 设备
  2.挂载 /dev/ram0(文件系统类型为 squashfs)作为根
  3.执行 /linuxrc或/sbin/init完成系统初始化

initrd 与 initramfs 的区别

initrd(Initial RAM Disk):内核将内存中的一段数据作为块设备(/dev/ramX)处理,然后挂载其中的文件系统。它支持任意文件系统类型,例如 ext2、squashfs。

initramfs:内核将 cpio 归档解压到 tmpfs 中,形成可读写根,然后执行 /init。它只接受 cpio 格式。

我们选择 initrd+squashfs 组合,主要原因如下:

  1. Squashfs 压缩率高,能节省内存空间;
  2. 无需制作 cpio 包,减少构建步骤;
  3. 内核原生支持挂载 squashfs,性能优良。

修改过程:U‑Boot / 内核 / project.mak

内核配置(必须项)

确保以下选项被选中(.config 中为 y):

CONFIG_BLK_DEV=y
CONFIG_BLK_DEV_LOOP=y
CONFIG_BLK_DEV_INITRD=y
CONFIG_BLK_DEV_RAM=y
CONFIG_BLK_DEV_RAM_COUNT=1
CONFIG_BLK_DEV_RAM_SIZE=8192
CONFIG_SQUASHFS=y 
CONFIG_DEVTMPFS=y
CONFIG_DEVTMPFS_MOUNT=y

注意CONFIG_BLK_DEV_RAM_SIZE 必须大于等于 rootfs 镜像解压后的大小。Squashfs 挂载后占用空间即为镜像大小,因为它是按块读取,并非展开。如果设置过小,挂载会失败。

如果内核未开启 DEVTMPFS,则 /dev/ram0 节点不会自动创建,需要手动执行 mknod,因此强烈建议开启。

内存地址规划与参数传递

我们项目中的内存布局规划关键计算如下:

# 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 = 0x44900000。按 32MB 的 rootfs 分区大小计算,rootfs 起始地址应为 0x44900000 - 32*1024*1024 = 0x42900000。这样内核与 rootfs 之间没有空隙,也不会互相重叠。

此处一旦疏忽,很容易发生数据踩踏,导致 U‑Boot 启动内核时直接报找不到内核。

U‑Boot 修改:自动读取 rootfs 到内存

修改 uboot/Makefile.common:将读取命令作为宏传递给 C 代码。

EXT_FLAG := ... KCPPFLAGS="-I... -DROOTFS_LOAD_CMD='$(ROOTFS_LOAD_CMD_PARAM)'"

修改 cmd/axera/boot/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

这样 U‑Boot 在引导内核前,就会将 Flash 中的 Squashfs 镜像完整读入内存指定位置。

内核启动参数(bootargs)

bootargs 的宏定义会被编译到 U‑Boot 中,再由 U‑Boot 传递给内核。我们在 project.mak 中重新构造 bootargs:

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 镜像的位置和大小,单位为字节。

我踩过的两大坑与避坑方法

坑一:initrd 地址与内核重叠导致数据损坏

现象:U‑Boot 下启动内核,提示找不到内核。

根源:计算地址时没有完全避开内核所在内存区域,发生了数据踩踏。

对策

  • 明确各段内存布局:内核、DTB、initrd 三者必须互不重叠;
  • project.mak 中仔细校验计算公式,使用十六进制打印输出,确保对齐。

坑二:bootargs 修改不生效——环境变量缓存作祟

现象:在 U‑Boot 中执行 printenv bootargs 显示已经更新,但内核启动时打印的 Kernel command line 仍是旧值,导致 initrd 参数没有传递给内核。

根源:U‑Boot 的环境变量通常保存在 Flash 中,并且带有缓存。如果只修改了内存中的环境变量,比如使用 setenv 却没有执行 saveenv,下次重启或重载时会从存储区恢复旧值。此外,如果代码中通过 CONFIG_BOOTARGS 硬编码了默认值,而环境变量没有覆盖它,也可能导致用户设置被忽略。

对策

  • 在 U‑Boot 命令行修改后,务必执行 saveenv
  • 如果通过构建系统生成 bootargs,要在 project.mak 中正确赋值,并在启动前通过 printenv bootargs 确认;
  • 也可以在 U‑Boot 脚本中直接执行 setenv bootargs ...saveenv,或者直接先擦除 env 分区。

注意:如果内核配置了 CONFIG_INITRAMFS_SOURCE,也就是内嵌 initramfs,内核会优先使用内嵌的 cpio,而忽略外部 initrd。因此务必确保该选项为空,让外部 initrd 生效。

Squashfs 与 CPIO 格式的选择分析

很多传统方案会把 rootfs 制作成 cpio.gzcpio.lzma,作为 initramfs 使用。我们则直接使用 Squashfs 镜像。两者适用场景对比如下:

特性 Squashfs + initrd CPIO + initramfs
镜像格式 原生压缩文件系统,按块压缩 归档文件,整体压缩(gz/lzma)
挂载方式 内核挂载为块设备,只读 内核解压到 tmpfs,可读写
内存占用 镜像本身大小,访问时缓存部分数据 解压后完整大小,占用全部内存
启动速度 无需完全解压,按需读取,较快 需完整解压到内存,稍慢
可写性 只读,需额外挂载 tmpfs 用于临时数据 初始即可读写,便于临时修改
构建复杂度 直接生成 Squashfs 镜像,简单 需要制作 cpio 并压缩,多一步
适用场景 内存紧张、根文件系统稳定的产品 需要启动阶段灵活写入、调试方便的场合

在我们的场景中,DRAM 总大小只有 128MB,内存资源比较紧张,因此 Squashfs + initrd 是更优的选择。它不仅节省内存,还减少了内核解压时间,整体启动耗时更短。

文末小结

通过本次改造,成功将根文件系统从 Flash 分区迁移到内存中挂载。整个过程只需要调整内核配置、内存地址计算和 U‑Boot 加载逻辑,不用修改根文件系统本身。

核心经验:

  • 内核配置三件套BLK_DEV_INITRDBLK_DEV_RAMSQUASHFS,缺一不可;
  • 内存地址计算务必严谨,建议画出内存布局图,避免重叠;
  • U‑Boot 环境变量修改后立即保存,避免缓存干扰;
  • 根据实际需求选择文件系统格式,Squashfs 适合只读内存运行,CPIO 更适合灵活的 initramfs。

💡 提示:总结也是让自己将原理、坑点牢记于心,同时也希望本文能为同类项目的开发者提供参考,让后来者少走弯路。

如果你也在做嵌入式 Linux、U‑Boot 相关的根文件系统改造,欢迎到 云栈社区 一起交流。




上一篇:改了bootargs编译却不生效?U-Boot环境变量缓存的坑
下一篇:Linux内核启动流程详解:AX615 armv7 4.19.125 逐阶段源码分析
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-9-10 16:00 , Processed in 1.446256 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表