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升级的两种模式
- A/B(无缝)升级:系统有两个槽位(slot A和slot B)。升级时,新系统写入非活跃槽位,下次启动直接切换。用户几乎无感知。
- 非A/B(传统)升级:升级时,系统进入recovery模式,覆盖写入当前分区。升级期间设备不可用。
我个人更推荐A/B升级。虽然它需要双倍的分区空间,但用户体验好太多。你想想看,用户正在刷抖音,突然弹出一个“系统更新,请等待”的对话框,多扫兴。
11.3.2 OTA升级的核心流程
以A/B升级为例,流程大致如下:
- 下载升级包:系统从服务器下载OTA包(通常是.zip格式),保存在/data/ota_package/目录下。
- 校验签名:用公钥验证OTA包的签名,防止被篡改。
- 解压并写入:将新系统镜像解压,写入非活跃槽位(比如当前是slot A,就写入slot B)。
- 更新bootloader变量:设置下次启动的槽位为slot B。
- 重启:bootloader读取变量,从slot B启动。
- 回滚保护:如果新系统启动失败,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升级就是换血手术。三者缺一不可。
11.5 避坑指南
最后,分享几个我亲身踩过的坑:
- 分区大小预留不足:我曾经给system分区只留了1.5GB,结果Android 10的GMS包塞不进去。后来改成动态分区,再也没愁过空间。
- f2fs的GC导致卡顿:在低端eMMC上,f2fs的垃圾回收会占用大量CPU。解决方案是调低GC触发阈值,或者换用UFS。
- OTA升级后WiFi连不上:有一次升级后,WiFi驱动加载失败。查了半天,是vendor分区里的固件版本不匹配。从那以后,我坚持在OTA脚本里做版本校验。
一句话总结:分区布局要留余量,文件系统要选对场景,OTA升级要防断电。做到这三点,你的嵌入式Android设备就能稳定运行很久。
公众号:蓝海资料掘金营,微信deep3321