1. 项目概述:为什么选择Cramfs?

在嵌入式开发,尤其是资源受限的MCU或早期ARM9、Cortex-A系列平台的项目中,文件系统的选型往往是一个需要权衡的决策。你手头可能有一个已经构建好的根文件系统(rootfs),里面包含了BusyBox、库文件、配置文件等,现在需要将它安全、高效地烧录到目标板的NAND Flash上。这时,像JFFS2、YAFFS2这类可读写的日志文件系统固然强大,但对于系统分区这种“只读”的场景,它们带来的擦写损耗和空间开销有时就显得不那么必要了。

Cramfs(Compressed ROM File System)就是为了解决这个痛点而生的。它是一个经过压缩的、只读的Linux文件系统。其最大的优势在于,它直接将整个文件系统镜像进行压缩,在系统运行时,内核的Cramfs驱动会按需、透明地解压单个文件到内存中供访问。这意味着两点核心价值:第一,它能显著节省宝贵的Flash存储空间,对于容量以MB计甚至更小的存储介质至关重要;第二,由于其只读属性,它从根本上杜绝了运行时意外修改系统文件导致设备变砖的风险,提升了系统的鲁棒性。因此,将制作好的rootfs打包成Cramfs镜像,再通过U-Boot烧录到Flash的固定分区,是许多嵌入式产品量产的标准步骤之一。

本文将以一个真实的开发场景为例,详细拆解从准备环境、制作镜像、到通过U-Boot烧录、最后配置内核启动的完整闭环流程。我会基于一个经典的开发环境组合(Ubuntu 8.04 + U-Boot 1.3.3)进行说明,但其中的原理、命令和避坑经验,完全适用于更新的开发环境和主流的嵌入式平台。无论你是正在从事相关开发的工程师,还是希望了解嵌入式系统部署细节的爱好者,这篇手把手的指南都能让你彻底掌握Cramfs镜像的制作与使用。

2. 环境准备与工具链解析

2.1 开发主机环境搭建

原文提到了Ubuntu 8.04,这是一个非常经典的嵌入式开发发行版版本,其软件仓库中的工具链版本稳定,与当时主流的U-Boot和内核版本匹配良好。当然,在现今的主流Ubuntu LTS版本(如20.04, 22.04)上,我们同样可以完成所有工作,但需要注意工具版本可能带来的细微差异。

核心工具是 cramfsprogs 软件包,它提供了 mkcramfs 命令。在任何现代Ubuntu或Debian系系统上,安装命令都是通用的:

sudo apt-get update
sudo apt-get install cramfsprogs

安装完成后,可以通过 mkcramfs --help 来验证安装并查看其简要帮助信息。

注意 :在一些极简的Docker构建环境或特定的交叉编译工具链环境中, cramfsprogs 可能默认未安装。如果你的CI/CD流水线需要在干净环境中制作镜像,务必在Dockerfile或构建脚本中显式加入此安装步骤。

2.2 目标板引导程序(U-Boot)确认

U-Boot 1.3.3是一个比较早期的版本,但它已经具备了我们需要的关键功能:支持网络(如TFTP)下载、完整的NAND Flash操作命令( nand erase , nand write 等)、以及灵活的环境变量设置。你的目标板U-Boot版本可能不同,但只要支持这些基本命令集,后续操作就完全可行。

在进入实操前,务必通过串口连接目标板,在U-Boot启动倒计时时打断,进入命令行,并执行 printenv 命令。你需要重点关注以下几个环境变量:

  • ipaddr : 目标板的IP地址。
  • serverip : 你的开发主机(TFTP服务器)的IP地址。
  • bootargs : 内核启动参数,我们后续需要修改它。
  • bootcmd : 默认启动命令。

确保 ipaddr serverip 设置正确,并且与你的主机在同一网段。这是后续通过TFTP传输镜像的前提。

2.3 文件系统目录准备

这是整个流程的基石。你的 rootfs 目录应该是一个完整的、可运行的Linux根文件系统。通常,它可以通过Buildroot、Yocto项目编译生成,或者手动基于BusyBox构建。在制作Cramfs镜像前,请务必进行以下检查:

  1. 权限与所有权 :使用 ls -l 检查关键目录(如 /bin , /sbin , /etc )和文件(如 /linuxrc , /init )的权限是否正确。通常,二进制文件应为 755 ,配置文件为 644 ,设备节点需要特殊权限。一个常见的检查命令是: sudo chown -R root:root rootfs/* 来统一所有权,但更推荐在构建系统阶段就处理好。
  2. 设备节点 :检查 /dev 目录下是否包含了必要的设备节点,如 console , null , ttySAC0 (对应你的串口)等。在制作只读镜像前,这些节点必须存在。可以使用 mknod 命令手动创建,或者确保你的构建系统包含了 devtmpfs mdev 的初始化逻辑。
  3. 空间预估 :进入 rootfs 目录,使用 du -sh 命令查看其总大小。这有助于你判断生成的Cramfs镜像大小,并确认目标Flash分区是否有足够空间容纳。

3. Cramfs镜像制作详解

3.1 mkcramfs命令实战

制作镜像的命令语法非常简单,但其背后有一些值得深究的选项和原理。

cd /path/to/parent-directory
mkcramfs rootfs/ rootfs.cramfs
  • 第一个参数 ( rootfs/ ) : 是你要打包的根文件系统目录的路径。 务必使用目录路径,并以斜杠结尾 ,这是一个好的习惯,能明确指定的是目录。
  • 第二个参数 ( rootfs.cramfs ) : 是输出的镜像文件名。

执行后,你会看到类似如下的输出,其中包含了镜像的尺寸信息:

Directory data: 7296 bytes
Everything: 3752 kilobytes
Super block: 76 bytes
CRC: bd4f8831
warning: gids truncated to 8 bits. (This may be a security concern.)

关键点解析

  • Everything 行显示的是压缩前的总数据量,而最终生成的 rootfs.cramfs 文件大小会远小于此值,这体现了Cramfs的压缩优势。
  • warning: gids truncated to 8 bits 这个警告非常重要。Cramfs文件系统本身设计上的一个限制是它只支持8位的组ID(GID),即GID范围是0-255。如果你的 rootfs 中有文件的GID超过了255,在制作镜像时会被截断(通常取低8位)。这在大多数嵌入式系统中不会造成问题,因为用户和组数量很少。但如果你有复杂的权限系统,需要特别注意。

3.2 高级参数与优化

mkcramfs 还有一些有用的参数:

  • -N [hostid] : 设置镜像中的 hostid 字段,用于网络文件系统(NFS)的缓存一致性,在纯Flash启动场景下很少使用。
  • -D [devtable] : 使用一个设备表文件。这是一个非常强大的功能。你可以在一个文本文件中预先定义镜像中需要创建的设备节点、目录、以及修改文件权限,而无需在 rootfs 目录中真实存在。例如,一个 devtable.txt 文件内容如下:
    # 设备节点 类型 权限 uid gid 主设备号 次设备号
    /dev/console c 600 0 0 5 1
    /dev/null c 666 0 0 1 3
    /dev/ttySAC0 c 600 0 0 204 64
    # 创建目录
    /tmp d 755 0 0
    # 修改已有文件权限
    /etc/shadow - 600 0 0
    
    使用命令 mkcramfs -D devtable.txt rootfs/ rootfs.cramfs 制作镜像时,工具会自动根据表格创建或修改条目,这能让你保持 rootfs 目录的干净,并精确控制镜像内容。
  • 压缩率考量 :Cramfs使用zlib进行压缩,压缩率是固定的。如果你对镜像大小有极致要求,可以尝试在制作前,使用 find rootfs/ -type f -exec gzip -9 {} \; 之类的命令预先压缩 rootfs 中的文本文件(如手册页、脚本),但要注意内核的Cramfs驱动可能无法直接读取这种“双重压缩”的文件,通常不推荐。更务实的做法是清理 rootfs 中无用的文档、本地化文件、调试符号等。

制作完成后,使用 ls -lh rootfs.cramfs 查看生成的镜像文件大小,并记录下这个值。例如,你可能会得到一个大约 3.7M 的镜像。

4. U-Boot烧录全流程

这是将镜像部署到硬件设备的关键步骤,需要谨慎操作,因为错误的擦写地址可能损坏Bootloader或内核,导致设备无法启动。

4.1 理解Flash分区布局

原文中给出的MTD分区表是一个典型示例:

0x00000000-0x00030000 : "bootloader"  // 192KB
0x00030000-0x00200000 : "kernel"      // 1856KB (~1.8MB)
0x00200000-0x00400000 : "ramdisk"     // 2MB
0x00400000-0x00800000 : "cramfs"      // 4MB  <-- 我们的目标分区
0x00800000-0x01000000 : "jffs2"       // 8MB
0x01000000-0x04000000 : "data"        // 48MB
  • 分区3 ( cramfs ) : 起始地址是 0x00400000 ,大小是 0x00800000 - 0x00400000 = 0x00400000 ,即4MB。对应的MTD设备节点是 /dev/mtdblock3 (块设备接口)或 /dev/mtd3 (字符设备接口,用于擦写)。在U-Boot中,我们直接使用物理地址进行操作。
  • 地址计算 : 在U-Boot命令中,我们需要指定操作的起始地址和长度(十六进制)。例如,擦除整个cramfs分区:起始地址= 0x400000 ,长度= 0x400000

4.2 TFTP传输与烧写命令逐行解析

假设你的开发主机IP是 192.168.1.100 ,目标板IP是 192.168.1.50 ,并且 rootfs.cramfs 文件已放在主机的TFTP服务目录(如 /tftpboot )下。

  1. 通过TFTP加载镜像到内存

    tftp 32000000 rootfs.cramfs
    
    • tftp : U-Boot的TFTP下载命令。
    • 32000000 : 这是目标板内存(SDRAM)中的一个地址。选择这个地址通常是因为它位于内核解压和运行区域之上,足够高以避免冲突。你需要根据你板子的具体内存映射来选择一个安全的、未被使用的地址区域。 0x30000000 0x31000000 也是常见选择。
    • rootfs.cramfs : 存放在TFTP服务器上的文件名。
    • 执行后观察 :U-Boot会显示加载的字节数,例如 “Done: 3,870,208 bytes”。请务必确认这个字节数与你在主机上看到的 rootfs.cramfs 文件大小一致。如果不一致,说明网络传输可能出错, 绝对不要 进行后续的擦写操作。
  2. 擦除目标Flash分区

    nand erase 400000 400000
    
    • nand erase : 擦除NAND Flash命令。
    • 400000 : 擦除的起始地址(对应分区起始 0x00400000 )。
    • 400000 : 擦除的长度(4MB,即 0x00400000 )。 这里原文的 nand erase 400000 800000 命令有误 800000 是结束地址,但 nand erase 命令的第二个参数通常是长度(Length),而不是结束地址。根据U-Boot版本和具体驱动实现,命令格式可能是 nand erase[.spread] [addr] [size] nand erase [addr] [end-addr] 。最安全的方式是使用 nand erase.part <partition-name> ,例如 nand erase.part cramfs ,这会自动计算分区大小。如果必须使用地址,请先通过 nand info mtdparts 命令确认你U-Boot版本接受的参数格式。 错误的擦除地址和长度是变砖的主要原因之一。
  3. 将内存中的镜像写入Flash

    nand write.jffs2 32000000 400000 200000
    
    • nand write.jffs2 : 这是写入命令的关键。使用 .jffs2 后缀是因为JFFS2文件系统在设计中考虑到了NAND Flash的坏块管理。这个命令会在写入时跳过已标记的坏块,将数据写入后续的好块中,从而避免因坏块导致写入失败。对于Cramfs镜像,我们同样使用这个命令来获得坏块跳过能力。
    • 32000000 : 源数据在内存中的起始地址。
    • 400000 : 写入Flash的目标起始地址。
    • 200000 : 这里是最容易出错的地方 。这个参数是 数据的长度 ,而不是分区的长度!它应该等于你通过TFTP加载的字节数。例如,镜像大小是 0x003B0000 (约3.7MB),那么这里应该填写 0x3b0000 。原文中的 0x200000 (2MB)显然小于常见的rootfs镜像大小,这会导致只写入了一部分镜像,系统必然无法启动。 正确的做法是 :在执行 tftp 命令后,U-Boot通常会显示 “Bytes transferred = XXXX (YYYY hex)”。记下这个十六进制数(YYYY hex),它就是 nand write.jffs2 的最后一个参数。
    • 一个更可靠的完整命令序列示例
      # 1. 设置服务器IP(如果环境变量未设置)
      setenv serverip 192.168.1.100
      # 2. TFTP加载镜像
      tftp 32000000 rootfs.cramfs
      # 假设输出:Bytes transferred = 3870208 (3b0000 hex)
      # 3. 擦除分区(使用分区名更安全)
      nand erase.part cramfs
      # 4. 写入镜像,使用上一步得到的实际长度 0x3b0000
      nand write.jffs2 32000000 400000 3b0000
      

4.3 验证烧写结果

烧写完成后,强烈建议进行一次读回校验,以确保数据完整写入。

nand read.jffs2 33000000 400000 3b0000
cmp.b 32000000 33000000 3b0000
  • 第一条命令将刚写入Flash的数据读回到内存的另一位置( 0x33000000 )。
  • 第二条命令逐字节比较原始内存数据( 0x32000000 )和读回的数据( 0x33000000 ),比较长度为 0x3b0000
  • 如果输出 “Total of xxx bytes were the same”,则校验通过。如果显示不同,说明烧写过程有问题,需要检查Flash硬件、电源或驱动。

5. 配置内核从Cramfs启动

镜像烧录成功后,需要告诉内核去哪里找到并如何挂载这个根文件系统。这通过U-Boot传递给内核的启动参数( bootargs )来实现。

5.1 启动参数(bootargs)深度解析

原文给出的参数是一个很好的起点:

root="/dev/mtdblock3" rootfstype="cramfs" console="ttySAC0",115200 init="/linuxrc" noinitrd mem="64M"

我们来逐一拆解每个参数的意义和配置要点:

  1. root="/dev/mtdblock3"

    • 这是最核心的参数,指定根文件系统所在的设备。 /dev/mtdblock3 对应MTD分区表中的第4个分区(从0开始计数),即我们烧录 cramfs 镜像的分区。
    • 如何确定是mtdblock3? 在Linux内核中,MTD块设备接口会为每个MTD分区创建 /dev/mtdblockX 节点。分区的编号通常与U-Boot或内核中定义的顺序一致。你可以在系统启动后,通过 cat /proc/mtd 来查看MTD分区信息,确认 cramfs 分区对应的 mtd 编号。
  2. rootfstype="cramfs"

    • 指定根文件系统的类型为 cramfs 。内核在尝试挂载根文件系统时,会根据这个类型来调用对应的文件系统驱动( fs/cramfs/ 下的代码)。 如果内核编译时没有包含Cramfs支持,即使指定了这个参数,启动也会失败,并报错 “VFS: Unable to mount root fs”
  3. console="ttySAC0",115200

    • 指定内核和控制台的串口设备及波特率。 ttySAC0 是三星S3C系列SoC常见的串口设备名,对于其他平台,可能是 ttyAMA0 (ARM PrimeCell PL011)、 ttyS0 ttyO0 (TI OMAP)等。 这个参数必须与你的硬件原理图和内核串口驱动配置完全匹配 ,否则你将看不到任何内核启动输出。
  4. init="/linuxrc"

    • 指定内核初始化完成后,运行的第一个用户空间进程。通常是 /sbin/init ,但在使用BusyBox构建的简单根文件系统中, /linuxrc 是指向 /bin/busybox 的一个符号链接,由BusyBox来扮演init的角色。你需要确认你的 rootfs 中确实存在 /linuxrc 这个文件。
  5. noinitrd

    • 告诉内核不使用初始RAM磁盘(initrd或initramfs)。因为我们直接从Flash的MTD分区挂载根文件系统,所以不需要ramdisk。
  6. mem="64M"

    • 指定内核可用的物理内存大小。这对于内存检测不准确的旧款芯片或自定义内存配置的板子非常重要。请根据你板载的SDRAM实际大小进行设置,例如 mem=128M

5.2 在U-Boot中设置并保存参数

在U-Boot命令行中,使用 setenv 命令来设置 bootargs

setenv bootargs 'root=/dev/mtdblock3 rootfstype=cramfs console=ttySAC0,115200 init=/linuxrc noinitrd mem=64M'

注意 :参数列表作为一个字符串,通常用单引号 ' 括起来,避免特殊字符被解释。

设置完成后,使用 saveenv 命令将环境变量保存到Flash(通常是NOR Flash或NAND Flash上的一个受保护区域),这样下次启动时配置依然有效。

saveenv

最后,使用 boot bootm (如果是从内存启动内核)命令来启动系统。如果一切配置正确,你将看到内核解压、启动,并最终由BusyBox的init进程给出一个shell提示符。

5.3 在运行时挂载Cramfs分区

有时,你的根文件系统可能是其他类型(如JFFS2),但你想以只读方式访问另一个分区上的Cramfs镜像(例如一个包含静态资源的分区)。这可以在系统启动后,通过 mount 命令实现:

mkdir -p /mnt/cramfs  # 创建一个挂载点目录
mount -t cramfs /dev/mtdblock3 /mnt/cramfs

挂载成功后,你就可以在 /mnt/cramfs 目录下访问该分区的内容了。由于是只读挂载,任何写入操作都会失败。

6. 内核配置与驱动编译

要让内核支持从Cramfs启动,必须在编译内核时启用相关选项。这通常通过 make menuconfig 进行配置。

  1. 进入内核源码目录。
  2. 执行 make menuconfig (或你喜欢的配置界面)。
  3. 导航到以下路径:
    File systems  --->
        <*> Miscellaneous filesystems  --->
            <*> Compressed ROM file system support (cramfs)
    
    确保 cramfs 选项被编译进内核( * ),而不是作为模块( M )。因为根文件系统需要在初始化早期就被挂载,此时模块可能还无法加载。
  4. 保存配置,重新编译内核。将生成的 zImage uImage 烧写到Flash的 kernel 分区。

重要提示 :不同内核版本对Cramfs的支持可能有差异。较新的内核(如5.x系列)可能还提供了“Cramfs write support (EXPERIMENTAL)”选项,这是一个实验性的写支持功能, 强烈不建议在生产环境中启用 ,因为它破坏了Cramfs只读的安全特性,且不稳定。

7. 常见问题与排查技巧实录

在实际操作中,你几乎一定会遇到一些问题。下面是我在多年嵌入式开发中总结的关于Cramfs的常见“坑”和解决方法。

7.1 镜像制作与烧录问题

问题1: mkcramfs 制作镜像时失败,提示 “File system larger than 272MB” 或 “file length too long”。

  • 原因与解决 :这是Cramfs文件系统格式的固有限制。单个Cramfs镜像最大支持约272MB(受限于其元数据结构)。如果你的 rootfs 确实很大,考虑精简文件系统(删除调试文件、文档、未使用的库),或者评估是否必须使用Cramfs。对于更大的只读文件系统,可以考虑SquashFS,它支持更大的容量和更高的压缩率,但需要内核支持。

问题2:TFTP下载速度极慢或总是超时失败。

  • 排查步骤
    1. 确认网络连通性 :在U-Boot中 ping 你的服务器IP: ping 192.168.1.100
    2. 确认防火墙 :临时关闭开发主机上的防火墙( sudo ufw disable 或相应命令)。
    3. 确认TFTP目录与权限 :确保文件在TFTP服务器目录(如 /tftpboot ),并且该目录有读权限( chmod +r /tftpboot/rootfs.cramfs )。
    4. 使用更简单的网络 :避免使用复杂的公司网络或VPN,直接使用交换机或路由器连接开发板和主机。

问题3: nand write 命令失败,提示 “Bad block”。

  • 原因与解决 :NAND Flash天生存在坏块。 永远不要使用 nand write 命令 ,而应该使用 nand write.jffs2 nand write.yaffs2 (如果镜像格式是YAFFS2),这些命令能自动跳过坏块。如果坏块数量超过了Flash厂商的规格,可能是Flash芯片物理损坏。

7.2 内核启动挂载失败问题

问题4:内核启动时卡住,最后报错 “VFS: Unable to mount root fs on unknown-block(31,3)” 或类似。

  • 排查思路(按照可能性从高到低)
    1. 检查 bootargs :这是最常见的原因。使用U-Boot的 printenv 确认 bootargs 字符串完全正确,特别是 root= 后面的设备节点。确保没有拼写错误或多余的空格。
    2. 检查内核配置 :确认内核已编译进Cramfs支持。可以检查生成的内核配置文件 .config 中是否有 CONFIG_CRAMFS=y
    3. 检查镜像烧录是否正确 :按照4.3节的方法,在U-Boot中读回并校验镜像数据。确认烧写的地址和长度无误。
    4. 检查MTD分区号 :确认内核看到的MTD分区布局与U-Boot一致。有时U-Boot通过 mtdparts 命令定义的分区表需要传递给内核(通过 bootargs 中的 mtdparts= 参数),否则内核可能使用默认的或设备树(DTS)中的分区,导致编号对不上。在 bootargs 中添加 mtdparts= 参数,或确保设备树中的分区定义与U-Boot一致。
    5. 检查文件系统内容 :极少数情况下, rootfs 目录本身有问题(例如缺少 /linuxrc /init )。可以尝试在主机上使用 sudo chroot rootfs /bin/sh 来模拟启动,看能否获得一个shell。

问题5:内核启动后,串口有输出但最后提示 “Kernel panic - not syncing: No working init found.”

  • 原因与解决 :内核成功挂载了根文件系统,但找不到或无法执行 init 参数指定的程序(如 /linuxrc )。
    1. 检查 bootargs 中的 init= 参数路径是否正确。
    2. 检查 rootfs 中该文件是否存在且具有可执行权限( ls -l /linuxrc )。
    3. 检查该文件是否因架构不匹配而无法运行。例如,为ARM编译的BusyBox不能在x86主机上运行。使用 file rootfs/bin/busybox 命令检查其架构。

7.3 性能与特性限制

问题6:系统运行时,对Cramfs中的文件进行读操作,感觉速度不如预期的快。

  • 原因 :这是Cramfs的工作机制决定的。每次读取文件,内核都需要实时解压对应的数据块。对于小文件或随机读取,性能开销是明显的。Cramfs的设计目标是节省空间和保证只读安全,而非高性能。如果对读性能有要求,可以考虑使用未压缩的ROMFS(更简单但体积大),或者将频繁访问的只读数据放到内存文件系统(如 tmpfs )中。

问题7:如何在Cramfs分区更新文件?

  • 根本方法 :Cramfs是只读的。 无法在目标板运行时更新单个文件 。唯一的更新方式是:
    1. 在开发主机上,修改 rootfs 目录中的文件。
    2. 重新运行 mkcramfs 生成全新的 rootfs.cramfs 镜像。
    3. 重新通过U-Boot的TFTP和烧写流程,将整个新镜像覆盖写入Flash的Cramfs分区。
    4. 重启设备。 因此,对于需要频繁更新的配置文件或数据,应该将它们放在另一个可读写的文件系统分区(如原文中的 jffs2 data 分区),并在系统启动后通过脚本符号链接或绑定挂载到Cramfs根文件系统的相应位置。

问题8:Cramfs支持软链接(Symbolic Link)吗?

  • 答案 :是的,Cramfs支持软链接。 mkcramfs 工具会将软链接信息打包进镜像。在挂载的Cramfs文件系统中,软链接可以正常使用。
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐