1. 嵌入式系统分区设计的核心逻辑与价值在嵌入式开发这个行当里摸爬滚打了十几年我越来越觉得一个项目的成败往往在硬件选型和软件架构定稿之前就已经被一个看似不起眼的环节决定了——那就是存储分区方案。这玩意儿就像盖房子的地基图纸画得对不对直接关系到后面砌墙、装修乃至住进去舒不舒服。很多新手工程师甚至是有些经验的老手都容易在这个环节上“偷懒”直接沿用上一个项目的分区表或者在网上找个“通用”方案就往上套。结果呢项目做到中后期要么发现OTA升级做不了要么系统跑着跑着就崩了要么存储空间莫名其妙就写满了回头一看全是分区设计时埋下的雷。嵌入式系统的分区绝不仅仅是把一块存储芯片比如eMMC、NAND Flash、NOR Flash切成几块那么简单。它是一套关乎系统稳定性、长期可靠性、安全性和未来可维护性的综合设计。一个好的分区方案能让你的系统在生命周期内“稳如老狗”升级维护“丝般顺滑”而一个糟糕的方案则会让你在深夜被报警电话吵醒去现场“救火”的次数多到怀疑人生。今天我就结合自己踩过的无数个坑以及最终总结出来的一套“省时省力”的方法论来和大家深入聊聊嵌入式系统到底该怎么分区。我们会从最基础的方案讲起一直深入到针对不同存储介质的复杂场景目标就是让你看完之后能直接上手设计出适合自己项目的、健壮的分区布局。2. 从基础到进阶分区方案的演进之路2.1 经典四段式简单直接但隐患重重绝大多数嵌入式Linux系统的入门教程或者芯片原厂提供的参考设计都会给出一个最基础的分区方案。这个方案通常按照系统启动和加载的顺序将存储空间划分为四个连续的区域Bootloader区存放U-Boot、Barebox等引导程序。这个分区通常很小几百KB到几MB但至关重要因为它负责最底层的硬件初始化并加载内核。环境变量区一个独立的小分区专门存放U-Boot的环境变量。像bootargs内核启动参数、bootcmd自动启动命令、IP地址、串口波特率等都存在这里。它与Bootloader分离方便单独管理和恢复。内核区存放压缩后的Linux内核镜像如zImage或uImage。大小一般在几MB到几十MB。根文件系统区存放整个系统的根文件系统rootfs包含了操作系统所有的库、配置文件、用户程序和数据。这是最大的一个分区。这种布局清晰直观符合系统启动的线性流程在开发调试初期非常方便。我早期很多项目图省事用的就是这种方案。但它的缺点在项目需要量产和维护时就会暴露无遗无法安全地进行系统在线升级这是最致命的缺陷。如果你想升级内核或根文件系统就必须直接覆盖正在运行的分区。一旦升级过程断电或数据校验出错整个系统就会变砖因为唯一的可启动镜像被破坏了。系统回滚能力为零新版本有问题对不起没有旧版本可以切回去只能通过烧录器重新烧录整个存储芯片这在现场是灾难性的。灵活性差所有东西都塞在一个rootfs里系统应用、用户数据、日志都混在一起。如果想对系统部分进行只读保护以提升安全性或者想单独扩展数据存储空间都会非常困难。实操心得这个方案仅适用于产品生命周期内绝对不需要OTA升级、功能极其简单稳定、且存储介质寿命不是问题的场景比如一些工业控制器的只读系统。但凡你对产品的可维护性有一点要求就请果断放弃它。2.2 A/B分区为在线升级而生为了解决上述方案的升级痛点A/B分区方案也称为双副本或冗余分区方案成为了工业界的标准实践。其核心思想是为关键组件内核和根文件系统准备两套完整的副本A组和B组。分区布局存储空间会被划分为Bootloader、环境变量、Kernel_A、Rootfs_A、Kernel_B、Rootfs_B可能还有数据区等。工作流程系统默认从A组Kernel_A Rootfs_A启动。当需要进行OTA升级时升级包会被下载并完整地写入到B组分区Kernel_B Rootfs_B。这个过程完全不影响正在运行的A组系统。B组分区写入完毕并校验通过后更新引导程序如U-Boot中的环境变量将下一次的启动标志位设置为B组。系统重启引导程序根据标志位加载B组的内核和根文件系统完成升级切换。如果B组启动失败例如连续重启N次都失败引导程序的“看门狗”逻辑可以自动将标志位切回A组实现自动回滚。这种方案的优点显而易见升级过程安全升级操作针对的是非活动分区即使中途断电原系统依然完好无损。支持回滚提供了天然的版本回退机制极大提升了系统可靠性。升级原子性从用户角度看升级是“瞬间”完成的重启切换体验更好。当然代价就是存储空间的需求几乎翻倍。但在存储芯片成本日益降低的今天用空间换取极高的可靠性和可维护性是绝对值得的买卖。注意事项实现A/B升级时环境变量分区的管理是关键。必须确保更新启动标志位的操作是原子性的例如通过备份环境变量区并校验的机制否则标志位错乱会导致系统无法引导。同时Bootloader本身通常不采用A/B方案因为其非常稳定且更新频率极低升级Bootloader需要格外谨慎一般通过独立的、带有强校验的恢复模式来完成。2.3 精细化分区兼顾安全、灵活与生命周期管理在A/B分区的基础上我们可以进一步对根文件系统进行“瘦身”和“拆分”这就是精细化分区。它的目标是将不同属性、不同用途的数据分离从而提升整体的安全性、灵活性和管理效率。一个典型的精细化分区布局可能如下分区文件系统类型挂载点读写属性内容设计考量Bootloader原始数据-只读U-Boot固定位置无需文件系统。Env原始数据-读写U-Boot环境变量独立小分区可擦写。Kernel_A原始数据-只读内核镜像AA/B升级的一部分。Rootfs_ASquashFS/ROMFS/(只读)只读系统基础库、核心服务只读保证系统核心不可篡改提升安全性。App_ASquashFS/ROMFS/opt/app只读应用程序与系统分离可独立升级。Kernel_B原始数据-只读内核镜像BA/B升级的一部分。Rootfs_BSquashFS/ROMFS/(只读)只读系统基础库、核心服务同上。App_BSquashFS/ROMFS/opt/app只读应用程序同上。DataExt4/F2FS/data读写用户数据、配置、日志独立可读写分区生命周期内频繁擦写。LogExt4/F2FS/var/log读写系统日志防止日志写满根分区可单独设置大小和轮转策略。Temptmpfs/tmp读写临时文件内存文件系统重启即清空减少对存储的写入。这样设计的好处安全性提升将操作系统核心和应用程序设置为只读使用SquashFS等压缩只读文件系统可以有效防止恶意软件或意外操作对系统文件的篡改。即使应用程序被攻破系统底层依然是安全的。升级粒度更细你可以单独升级App分区而无需触动庞大的Rootfs。这减少了升级包的大小和升级风险。生命周期管理优化Data和Log分区是读写最频繁的区域。将它们独立出来你可以为Data分区选择更耐写的文件系统如F2FS专为Flash优化。单独监控Data分区的剩余空间和健康度。即使日志疯狂增长写满Log分区也不会影响系统核心Rootfs的正常运行Rootfs是只读的不会被写满。恢复与调试方便出现问题时可以尝试清空Data分区以恢复出厂设置保留只读系统或者打包Log分区数据用于分析。踩坑实录我曾在一个视频监控设备项目中将所有东西都放在一个可读写的rootfs里。设备运行一段时间后客户反映频繁死机。排查发现是某个调试日志服务配置错误导致日志疯狂输出短短几天就把整个存储芯片写满了系统无法创建新进程而崩溃。后来改为独立的Log分区并设置日志轮转和大小限制问题彻底解决。把可变的东西和不变的东西分开是嵌入式存储设计的一条黄金法则。3. 存储介质差异下的分区实战嵌入式设备使用的存储介质主要分为两大类块设备和原始Flash设备。它们底层的工作原理不同导致上层的分区和文件系统选择策略有巨大差异。3.1 块设备eMMC/UFS/SD卡这类设备如eMMC、UFS、SD卡、SSD内部集成了Flash转换层对操作系统呈现为标准的块设备就像PC上的硬盘一样。你看到的是/dev/mmcblk0、/dev/sda这样的设备节点。分区与文件系统操作分区工具直接使用fdisk、parted、gdisk等标准工具进行分区。分区表格式可以是MBR或GPT。文件系统可以选择任何Linux支持的、面向块设备的文件系统。Ext4最经典、最稳定日志功能完善适合大多数场景。但针对Flash的磨损均衡依赖硬件eMMC控制器或手动fstrim。F2FS专为Flash存储设计具有更好的磨损均衡和性能尤其在小文件随机写入场景下优势明显。是Data等读写频繁分区的优秀选择。SquashFS一种高度压缩的只读文件系统。将你的Rootfs或App目录树打包成.squashfs镜像直接放在分区里。系统启动时将其挂载为环回设备可以极大节省空间并天然具备只读属性。针对eMMC等块设备的分区实战步骤假设我们为一个基于eMMC的设备设计一个包含A/B升级和精细化分区的方案。# 1. 使用 parted 工具对 eMMC (/dev/mmcblk0) 进行GPT分区 sudo parted /dev/mmcblk0 mklabel gpt # 2. 创建分区 (单位MB) # bootloader分区 4MB sudo parted /dev/mmcblk0 mkpart primary 1MiB 5MiB # env分区 1MB sudo parted /dev/mmcblk0 mkpart primary 5MiB 6MiB # kernel_a分区 32MB sudo parted /dev/mmcblk0 mkpart primary 6MiB 38MiB # rootfs_a分区 256MB (只读SquashFS) sudo parted /dev/mmcblk0 mkpart primary 38MiB 294MiB # app_a分区 128MB (只读SquashFS) sudo parted /dev/mmcblk0 mkpart primary 294MiB 422MiB # kernel_b分区 32MB sudo parted /dev/mmcblk0 mkpart primary 422MiB 454MiB # rootfs_b分区 256MB sudo parted /dev/mmcblk0 mkpart primary 454MiB 710MiB # app_b分区 128MB sudo parted /dev/mmcblk0 mkpart primary 710MiB 838MiB # data分区 剩余所有空间 (Ext4或F2FS) sudo parted /dev/mmcblk0 mkpart primary 838MiB 100% # 3. 格式化分区 # 格式化data分区为F2FS (需内核支持) sudo mkfs.f2fs -l DATA /dev/mmcblk0p9 # 将rootfs和app分区格式化为普通ext4用于存放squashfs镜像或者直接留空由烧录工具写入镜像 sudo mkfs.ext4 /dev/mmcblk0p4 sudo mkfs.ext4 /dev/mmcblk0p5 # ... 其他分区类似 # 4. 制作只读根文件系统镜像 # 假设你的根文件系统目录是 ./rootfs sudo mksquashfs ./rootfs rootfs.squashfs -comp xz -b 256K -no-xattrs # 然后将 rootfs.squashfs 镜像烧录到 /dev/mmcblk0p4 和 /dev/mmcblk0p7 # 5. 配置启动参数 # 在U-Boot环境变量中设置 setenv bootargs root/dev/mmcblk0p4 ro rootfstypesquashfs rootwait consolettyS0,115200 # 设置升级标志位变量例如 boot_partition0 表示A区1表示B区 setenv upgrade_available 0 setenv boot_partition 0 saveenv3.2 原始Flash设备SPI NAND/NOR这类设备如SPI NAND Flash、并行NOR Flash对操作系统呈现为MTD设备。你看到的是/dev/mtd0、/dev/mtdblock0。操作系统需要直接管理Flash的擦除、读写和坏块因此复杂度更高。核心概念与分层架构MTD内存技术设备是Linux内核中用于访问原始Flash的抽象层。一个MTD分区对应Flash上一段连续的物理空间。UBIUnsorted Block Images运行在MTD之上。它有两个核心功能卷管理可以在一个物理MTD分区上创建多个逻辑卷UBI Volume类似于LVM的逻辑卷。可以动态调整卷大小。全分区磨损均衡这是UBI最强大的特性。它对整个底层MTD分区进行磨损均衡而不是单个卷。这意味着所有逻辑卷的写入操作会被均匀分布到整个物理Flash空间极大延长了Flash寿命。UBIFS专为UBI设计的日志文件系统运行在UBI卷之上。它提供了类似Ext4的完整文件系统功能但针对Flash特性进行了优化。分区布局策略对于原始Flash我们通常在物理上划分几个大的MTD分区然后在关键的MTD分区上创建UBI再在UBI上创建多个UBIFS逻辑卷。MTD分区0bootloader。U-Boot直接存放在这里。MTD分区1env。U-Boot环境变量。MTD分区2kernel。内核镜像。通常不放在UBI里因为UBI启动需要驱动加载而内核需要在内核启动前就被U-Boot加载。MTD分区3rootfs_ubi。这是一个大的MTD分区我们将在此建立UBI并创建多个UBI卷。UBI Volume 0:rootfs_a(UBIFS只读或读写)UBI Volume 1:rootfs_b(UBIFS只读或读写)UBI Volume 2:app(UBIFS读写)UBI Volume 3:data(UBIFS读写)针对SPI NAND Flash的UBI/UBIFS分区实战# 1. 在Linux系统中假设Flash被识别为 /dev/mtd3 # 2. 擦除整个MTD分区并附着UBI (创建UBI设备) sudo ubiformat /dev/mtd3 -y sudo ubiattach -p /dev/mtd3 -d 0 # 此时会生成 /dev/ubi0 设备节点 # 3. 在UBI设备上创建逻辑卷 # 创建 rootfs_a 卷大小 100MiB 动态卷 sudo ubimkvol /dev/ubi0 -N rootfs_a -s 100MiB # 创建 rootfs_b 卷大小 100MiB sudo ubimkvol /dev/ubi0 -N rootfs_b -s 100MiB # 创建 data 卷使用剩余所有空间 sudo ubimkvol /dev/ubi0 -N data -m # 4. 在UBI卷上创建UBIFS文件系统 sudo mkfs.ubifs -r ./rootfs_a_content -m 4096 -e 253952 -c 400 -o rootfs_a.ubifs.img # 将镜像写入卷 sudo ubiupdatevol /dev/ubi0_0 rootfs_a.ubifs.img # 对于 data 这种读写卷可以直接格式化为空UBIFS并挂载使用 sudo mkfs.ubifs /dev/ubi0_2 -r ./empty_dir sudo mount -t ubifs ubi0:data /mnt/data # 5. 内核启动参数需要相应调整 # 告诉内核 rootfs 在 UBI 卷上 setenv bootargs ubi.mtd3 rootubi0:rootfs_a rootfstypeubifs ro rootwait consolettyS0,115200重要提示Bootloader和Env绝对不能放在UBI卷里必须直接放在MTD分区上。因为UBI的驱动需要由内核加载而U-Boot需要在内核启动前就读取这些内容。同理内核本身通常也建议直接放在MTD分区而不是UBI卷中以确保U-Boot能直接、快速地加载它。4. 分区方案设计方法论与避坑指南4.1 一套“省时省力”的设计流程经过多年实践我总结了一套四步走的分区设计流程能帮你系统性地思考避免遗漏需求梳理与约束分析功能需求是否需要OTA升级是否需要系统回滚应用程序是否需要独立升级用户数据量预计多大日志策略是什么非功能需求安全性要求哪些部分需要只读可靠性要求MTBF启动速度要求硬件约束存储芯片总容量是多少是eMMC还是Raw Flash读写速度如何擦除块大小是多少对Flash很重要选择存储架构与文件系统根据硬件约束决定使用块设备架构eMMC/Ext4/F2FS还是MTDUBIUBIFS架构Raw Flash。根据安全性和内容属性为每个分区选择文件系统只读系统/应用用SquashFS读写数据分区用Ext4/F2FS块设备或UBIFS原始Flash。绘制分区布局图并计算大小用表格或图表画出所有分区包括名称、类型、大小、文件系统、属性。精确计算分区大小Bootloader和Kernel要留足余量未来可能增大Rootfs大小可以通过du -sh编译后的目录来估算并增加30%-50%的冗余Data分区要根据产品生命周期内可能产生的数据量来预估。对齐至关重要特别是对于Flash设备分区起始和结束地址必须对齐到擦除块大小的整数倍否则性能会急剧下降甚至出错。使用cat /proc/mtd可以查看擦除块大小。实现与验证编写分区表描述文件如Linux内核的DTS中的partition节点或U-Boot的mtdparts命令。制作镜像烧录脚本。进行破坏性测试模拟升级过程中断电疯狂写入数据直到填满分区测试回滚功能是否正常。这是检验分区方案健壮性的唯一标准。4.2 常见问题与排查技巧实录问题1系统启动失败提示“Wrong filesystem type”或“Cannot open block device”。排查思路检查内核命令行首先确认U-Boot传递给内核的root参数是否正确。例如root/dev/mmcblk0p4指定的是eMMC的第4个分区而rootubi0:rootfs_a指定的是UBI设备0上的rootfs_a卷。检查文件系统类型rootfstype参数是否与分区实际格式化的文件系统匹配ext4、squashfs、ubifs不能混用。检查分区是否存在在U-Boot中使用mmc part对于eMMC或mtdparts对于Flash命令查看分区表是否被正确识别和创建。检查镜像是否损坏确认烧录到存储设备中的内核镜像和根文件系统镜像的完整性例如通过CRC32或SHA256校验。问题2OTA升级后系统无法启动也无法自动回滚。排查思路检查升级标志位进入U-Boot打印环境变量printenv查看决定启动A区还是B区的标志位如boot_partition是否被正确设置。升级脚本在更新完镜像后必须在重启前更新这个标志位。检查回滚机制U-Boot中的回滚逻辑是否生效通常是通过一个启动计数器bootcount来实现如果从新分区启动失败比如连续重启3次则自动将标志位切回旧分区。检查U-Boot的源码或配置中相关的看门狗或bootcmd逻辑。检查分区内容在U-Boot中尝试手动读取新分区B区的内核镜像头信息例如使用iminfo命令确认镜像是否被完整、正确地写入。问题3设备运行一段时间后系统变慢或出现“只读文件系统”错误。排查思路检查存储空间使用df -h命令查看各个挂载点的使用率。很可能是/data或/var/log等读写分区被写满了。对于只读的rootfs写满不会导致错误但读写分区写满会导致任何写入操作失败。检查Flash健康度针对UBIFS/原始Flash使用ubiattach时的内核信息或ubinfo /dev/ubi0命令查看UBI设备报告的坏块计数和平均擦除次数。使用ubirsvol可以调整卷大小如果某个卷如data快满了可以尝试从其他不常用的卷分配一些空间过来前提是UBI设备有预留空间。检查文件系统错误对于Ext4/F2FS可以尝试在恢复模式下执行fsck进行修复。对于UBIFS由于其日志结构一般不需要fsck但严重的坏块可能导致数据丢失。问题4针对Flash设备如何确定UBI的预留空间大小经验法则UBI需要在MTD分区上预留一部分物理空间PEB物理擦除块来处理磨损均衡和坏块替换。这个预留空间的大小是关键配置。计算公式经验预留PEB数量 (总PEB数 * 磨损均衡预留百分比) 坏块预留数磨损均衡预留通常为总空间的1%-2%。例如一个有1000个PEB的分区可以预留20个。坏块预留根据Flash芯片的标称坏块率通常在Datasheet里如每1024块初始有最多20个坏块和生命周期末期可能增加的坏块来估算。一个保守的做法是再额外预留2%-5%。最终设置在通过ubiattach附着MTD时可以通过-b参数指定预留块数或者在内核的DTS中为MTD分区设置ubi.reserved-pebs属性。宁多勿少预留空间不足会导致UBI运行一段时间后因无可用块而崩溃。