11、文件系统与存储:分区布局、ext4/f2fs特性、OTA升级流程

说到嵌入式Android的存储,我脑子里第一个蹦出来的词就是“分区”。

你想想看,一台手机或者平板,从按下电源键到进入桌面,系统要读多少东西?bootloader、内核、系统镜像、用户数据……如果全塞在一个分区里,那不乱套了?

所以,Android从一开始就设计了一套清晰的分区布局。今天我们就来聊聊这个布局,以及背后的文件系统——ext4和f2fs,最后再讲讲OTA升级到底是怎么把新系统“刷”进去的。

11.1 分区布局:一张“藏宝图”

嵌入式Android的分区布局,说白了就是一张“藏宝图”。每个分区都有固定的位置和用途。我最早接触这块时,是在一块全志A33的开发板上。当时我烧录完系统,发现启动卡在logo界面,查了半天,原来是boot分区写错了位置。

嗯,这里要注意:分区布局不是随便定的,它由bootloader和内核共同约定。常见的分区如下:

分区名称 挂载点 文件系统 用途
boot raw 存放内核+ramdisk
system /system ext4/f2fs 系统镜像(只读)
vendor /vendor ext4/f2fs 厂商专有库和驱动
data /data f2fs(推荐) 用户数据(可读写)
cache /cache ext4 OTA缓存、日志
recovery raw 恢复模式内核+ramdisk
misc raw 启动控制信息

我个人习惯把这张表打印出来贴在工位上。因为每次做分区表调整时,看一眼就能避免踩坑。

关键点:Android 10之后引入了动态分区(super分区),system、vendor、product等被合并到一个super分区里,由逻辑卷管理。这样做的好处是——分区大小可以灵活调整,不用再为“system空间不够”发愁了。

11.2 ext4 vs f2fs:老将与新秀

ext4是Linux的老牌文件系统,稳定、成熟、工具链完善。但它在闪存上有个致命弱点——日志(journal)写放大。你想想看,每次写一个小文件,ext4可能要先写日志,再写数据,最后更新元数据。对于eMMC和UFS这种闪存来说,这就是在“折寿”。

f2fs(Flash-Friendly File System)就是专门为闪存设计的。它由三星开发,2012年合入Linux主线。它的核心思路是:把闪存当作闪存来管理,而不是模拟成机械硬盘

我在项目中遇到过一个问题:某款平板用ext4做data分区,用了半年后,用户反馈“存储空间越用越少”。查了半天,是ext4的预留块(reserved blocks)和日志占用了大量空间。换成f2fs后,问题迎刃而解。

11.2.1 ext4的特性

  • 日志(journal):保证崩溃后数据一致性,但写放大严重
  • 预留块:默认5%空间留给root,普通用户不可用
  • 在线调整大小:支持resize2fs动态扩展
  • 兼容性好:几乎所有Linux工具都支持

11.2.2 f2fs的特性

  • 无日志设计:通过checkpoint保证一致性,写放大极小
  • 多级冷热数据分离:把频繁写入的数据和不常改动的数据分开存储
  • 自适应压缩:支持LZ4/Zstd压缩,节省空间
  • GC(垃圾回收):后台整理碎片,但会消耗CPU

我的建议:system分区用ext4就够了,因为它是只读的。data分区强烈推荐f2fs,尤其是频繁读写的小文件场景。cache分区用ext4,因为OTA升级时可能用到日志功能。

11.3 OTA升级流程:从下载到生效

OTA(Over-The-Air)升级,说白了就是“空中换血”。系统在运行时,把新固件下载下来,然后重启进入recovery模式,把新系统刷进去。

我曾经踩过一个坑:某次OTA升级后,设备变砖了。查了半天,是升级脚本里忘了更新vbmeta分区,导致AVB(Android Verified Boot)校验失败。从那以后,我每次写OTA脚本都会反复检查分区列表。

11.3.1 OTA升级的两种模式

  1. A/B(无缝)升级:系统有两个槽位(slot A和slot B)。升级时,新系统写入非活跃槽位,下次启动直接切换。用户几乎无感知。
  2. 非A/B(传统)升级:升级时,系统进入recovery模式,覆盖写入当前分区。升级期间设备不可用。

我个人更推荐A/B升级。虽然它需要双倍的分区空间,但用户体验好太多。你想想看,用户正在刷抖音,突然弹出一个“系统更新,请等待”的对话框,多扫兴。

11.3.2 OTA升级的核心流程

以A/B升级为例,流程大致如下:

  1. 下载升级包:系统从服务器下载OTA包(通常是.zip格式),保存在/data/ota_package/目录下。
  2. 校验签名:用公钥验证OTA包的签名,防止被篡改。
  3. 解压并写入:将新系统镜像解压,写入非活跃槽位(比如当前是slot A,就写入slot B)。
  4. 更新bootloader变量:设置下次启动的槽位为slot B。
  5. 重启:bootloader读取变量,从slot B启动。
  6. 回滚保护:如果新系统启动失败,bootloader自动切回旧槽位。

注意:OTA升级过程中,绝对不能断电!我见过太多因为升级时拔电池导致变砖的案例。如果条件允许,建议在升级前检查电池电量,低于50%就拒绝升级。

11.3.3 OTA升级脚本示例

下面是一个简化的升级脚本片段,展示了如何更新分区:

# 更新boot分区
write_raw_image(package_extract_file("boot.img"), "boot_b");

# 更新system分区
block_image_update(
    package_extract_file("system.img"),
    map_partition("system_b"),
    "--update");

# 更新vbmeta分区(防回滚)
write_raw_image(package_extract_file("vbmeta.img"), "vbmeta_b");

# 设置下次启动槽位
set_active_slot("B");

嗯,这里要注意:block_image_update函数会做增量更新,只写入变化的数据块,而不是整个镜像。这样能大大减少写入量,延长闪存寿命。

11.4 知识体系总览

为了让你更直观地理解本章的知识结构,我画了一张图:

文件系统与存储知识体系 分区布局 文件系统 OTA升级 boot / system / vendor / data cache / recovery / misc 动态分区(super分区) ext4:日志、预留块、稳定 f2fs:无日志、冷热分离、GC 选型建议:data用f2fs A/B无缝升级 vs 传统升级 下载 → 校验 → 写入 → 切换 回滚保护与防变砖 核心:分区是骨架,文件系统是血肉,OTA是换血手术

这张图把本章的三个核心模块串起来了。分区布局是骨架,文件系统是血肉,OTA升级就是换血手术。三者缺一不可。

11.5 避坑指南

最后,分享几个我亲身踩过的坑:

  • 分区大小预留不足:我曾经给system分区只留了1.5GB,结果Android 10的GMS包塞不进去。后来改成动态分区,再也没愁过空间。
  • f2fs的GC导致卡顿:在低端eMMC上,f2fs的垃圾回收会占用大量CPU。解决方案是调低GC触发阈值,或者换用UFS。
  • OTA升级后WiFi连不上:有一次升级后,WiFi驱动加载失败。查了半天,是vendor分区里的固件版本不匹配。从那以后,我坚持在OTA脚本里做版本校验。

一句话总结:分区布局要留余量,文件系统要选对场景,OTA升级要防断电。做到这三点,你的嵌入式Android设备就能稳定运行很久。


公众号:蓝海资料掘金营,微信deep3321