1. 项目概述为什么选择Cramfs在嵌入式开发尤其是资源受限的MCU或早期ARM9、Cortex-A系列平台的项目中文件系统的选型往往是一个需要权衡的决策。你手头可能有一个已经构建好的根文件系统rootfs里面包含了BusyBox、库文件、配置文件等现在需要将它安全、高效地烧录到目标板的NAND Flash上。这时像JFFS2、YAFFS2这类可读写的日志文件系统固然强大但对于系统分区这种“只读”的场景它们带来的擦写损耗和空间开销有时就显得不那么必要了。CramfsCompressed 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镜像前请务必进行以下检查权限与所有权使用ls -l检查关键目录如/bin,/sbin,/etc和文件如/linuxrc,/init的权限是否正确。通常二进制文件应为755配置文件为644设备节点需要特殊权限。一个常见的检查命令是sudo chown -R root:root rootfs/*来统一所有权但更推荐在构建系统阶段就处理好。设备节点检查/dev目录下是否包含了必要的设备节点如console,null,ttySAC0对应你的串口等。在制作只读镜像前这些节点必须存在。可以使用mknod命令手动创建或者确保你的构建系统包含了devtmpfs或mdev的初始化逻辑。空间预估进入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位的组IDGID即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下。通过TFTP加载镜像到内存tftp 32000000 rootfs.cramfstftp: U-Boot的TFTP下载命令。32000000: 这是目标板内存SDRAM中的一个地址。选择这个地址通常是因为它位于内核解压和运行区域之上足够高以避免冲突。你需要根据你板子的具体内存映射来选择一个安全的、未被使用的地址区域。0x30000000或0x31000000也是常见选择。rootfs.cramfs: 存放在TFTP服务器上的文件名。执行后观察U-Boot会显示加载的字节数例如 “Done: 3,870,208 bytes”。请务必确认这个字节数与你在主机上看到的rootfs.cramfs文件大小一致。如果不一致说明网络传输可能出错绝对不要进行后续的擦写操作。擦除目标Flash分区nand erase 400000 400000nand 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版本接受的参数格式。错误的擦除地址和长度是变砖的主要原因之一。将内存中的镜像写入Flashnand write.jffs2 32000000 400000 200000nand write.jffs2: 这是写入命令的关键。使用.jffs2后缀是因为JFFS2文件系统在设计中考虑到了NAND Flash的坏块管理。这个命令会在写入时跳过已标记的坏块将数据写入后续的好块中从而避免因坏块导致写入失败。对于Cramfs镜像我们同样使用这个命令来获得坏块跳过能力。32000000: 源数据在内存中的起始地址。400000: 写入Flash的目标起始地址。200000:这里是最容易出错的地方。这个参数是数据的长度而不是分区的长度它应该等于你通过TFTP加载的字节数。例如镜像大小是0x003B0000约3.7MB那么这里应该填写0x3b0000。原文中的0x2000002MB显然小于常见的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 3b00004.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 rootfstypecramfs consolettySAC0,115200 init/linuxrc noinitrd mem64M我们来逐一拆解每个参数的意义和配置要点root/dev/mtdblock3这是最核心的参数指定根文件系统所在的设备。/dev/mtdblock3对应MTD分区表中的第4个分区从0开始计数即我们烧录cramfs镜像的分区。如何确定是mtdblock3在Linux内核中MTD块设备接口会为每个MTD分区创建/dev/mtdblockX节点。分区的编号通常与U-Boot或内核中定义的顺序一致。你可以在系统启动后通过cat /proc/mtd来查看MTD分区信息确认cramfs分区对应的mtd编号。rootfstypecramfs指定根文件系统的类型为cramfs。内核在尝试挂载根文件系统时会根据这个类型来调用对应的文件系统驱动fs/cramfs/下的代码。如果内核编译时没有包含Cramfs支持即使指定了这个参数启动也会失败并报错 “VFS: Unable to mount root fs”。consolettySAC0,115200指定内核和控制台的串口设备及波特率。ttySAC0是三星S3C系列SoC常见的串口设备名对于其他平台可能是ttyAMA0ARM PrimeCell PL011、ttyS0、ttyO0TI OMAP等。这个参数必须与你的硬件原理图和内核串口驱动配置完全匹配否则你将看不到任何内核启动输出。init/linuxrc指定内核初始化完成后运行的第一个用户空间进程。通常是/sbin/init但在使用BusyBox构建的简单根文件系统中/linuxrc是指向/bin/busybox的一个符号链接由BusyBox来扮演init的角色。你需要确认你的rootfs中确实存在/linuxrc这个文件。noinitrd告诉内核不使用初始RAM磁盘initrd或initramfs。因为我们直接从Flash的MTD分区挂载根文件系统所以不需要ramdisk。mem64M指定内核可用的物理内存大小。这对于内存检测不准确的旧款芯片或自定义内存配置的板子非常重要。请根据你板载的SDRAM实际大小进行设置例如mem128M。5.2 在U-Boot中设置并保存参数在U-Boot命令行中使用setenv命令来设置bootargssetenv bootargs root/dev/mtdblock3 rootfstypecramfs consolettySAC0,115200 init/linuxrc noinitrd mem64M注意参数列表作为一个字符串通常用单引号括起来避免特殊字符被解释。设置完成后使用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进行配置。进入内核源码目录。执行make menuconfig或你喜欢的配置界面。导航到以下路径File systems --- * Miscellaneous filesystems --- * Compressed ROM file system support (cramfs)确保cramfs选项被编译进内核*而不是作为模块M。因为根文件系统需要在初始化早期就被挂载此时模块可能还无法加载。保存配置重新编译内核。将生成的zImage或uImage烧写到Flash的kernel分区。重要提示不同内核版本对Cramfs的支持可能有差异。较新的内核如5.x系列可能还提供了“Cramfs write support (EXPERIMENTAL)”选项这是一个实验性的写支持功能强烈不建议在生产环境中启用因为它破坏了Cramfs只读的安全特性且不稳定。7. 常见问题与排查技巧实录在实际操作中你几乎一定会遇到一些问题。下面是我在多年嵌入式开发中总结的关于Cramfs的常见“坑”和解决方法。7.1 镜像制作与烧录问题问题1mkcramfs制作镜像时失败提示 “File system larger than 272MB” 或 “file length too long”。原因与解决这是Cramfs文件系统格式的固有限制。单个Cramfs镜像最大支持约272MB受限于其元数据结构。如果你的rootfs确实很大考虑精简文件系统删除调试文件、文档、未使用的库或者评估是否必须使用Cramfs。对于更大的只读文件系统可以考虑SquashFS它支持更大的容量和更高的压缩率但需要内核支持。问题2TFTP下载速度极慢或总是超时失败。排查步骤确认网络连通性在U-Boot中ping你的服务器IPping 192.168.1.100。确认防火墙临时关闭开发主机上的防火墙sudo ufw disable或相应命令。确认TFTP目录与权限确保文件在TFTP服务器目录如/tftpboot并且该目录有读权限chmod r /tftpboot/rootfs.cramfs。使用更简单的网络避免使用复杂的公司网络或VPN直接使用交换机或路由器连接开发板和主机。问题3nand 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)” 或类似。排查思路按照可能性从高到低检查bootargs这是最常见的原因。使用U-Boot的printenv确认bootargs字符串完全正确特别是root后面的设备节点。确保没有拼写错误或多余的空格。检查内核配置确认内核已编译进Cramfs支持。可以检查生成的内核配置文件.config中是否有CONFIG_CRAMFSy。检查镜像烧录是否正确按照4.3节的方法在U-Boot中读回并校验镜像数据。确认烧写的地址和长度无误。检查MTD分区号确认内核看到的MTD分区布局与U-Boot一致。有时U-Boot通过mtdparts命令定义的分区表需要传递给内核通过bootargs中的mtdparts参数否则内核可能使用默认的或设备树DTS中的分区导致编号对不上。在bootargs中添加mtdparts参数或确保设备树中的分区定义与U-Boot一致。检查文件系统内容极少数情况下rootfs目录本身有问题例如缺少/linuxrc或/init。可以尝试在主机上使用sudo chroot rootfs /bin/sh来模拟启动看能否获得一个shell。问题5内核启动后串口有输出但最后提示 “Kernel panic - not syncing: No working init found.”原因与解决内核成功挂载了根文件系统但找不到或无法执行init参数指定的程序如/linuxrc。检查bootargs中的init参数路径是否正确。检查rootfs中该文件是否存在且具有可执行权限ls -l /linuxrc。检查该文件是否因架构不匹配而无法运行。例如为ARM编译的BusyBox不能在x86主机上运行。使用file rootfs/bin/busybox命令检查其架构。7.3 性能与特性限制问题6系统运行时对Cramfs中的文件进行读操作感觉速度不如预期的快。原因这是Cramfs的工作机制决定的。每次读取文件内核都需要实时解压对应的数据块。对于小文件或随机读取性能开销是明显的。Cramfs的设计目标是节省空间和保证只读安全而非高性能。如果对读性能有要求可以考虑使用未压缩的ROMFS更简单但体积大或者将频繁访问的只读数据放到内存文件系统如tmpfs中。问题7如何在Cramfs分区更新文件根本方法Cramfs是只读的。无法在目标板运行时更新单个文件。唯一的更新方式是在开发主机上修改rootfs目录中的文件。重新运行mkcramfs生成全新的rootfs.cramfs镜像。重新通过U-Boot的TFTP和烧写流程将整个新镜像覆盖写入Flash的Cramfs分区。重启设备。 因此对于需要频繁更新的配置文件或数据应该将它们放在另一个可读写的文件系统分区如原文中的jffs2或data分区并在系统启动后通过脚本符号链接或绑定挂载到Cramfs根文件系统的相应位置。问题8Cramfs支持软链接Symbolic Link吗答案是的Cramfs支持软链接。mkcramfs工具会将软链接信息打包进镜像。在挂载的Cramfs文件系统中软链接可以正常使用。