17、OTA升级与快照:snapshot、merge、CowWriter、CowReader
各位好,今天我们聊一个在Android OTA升级里非常核心的机制——快照(Snapshot)。
说实话,我第一次接触这个模块的时候,也被那一堆术语搞得有点晕:snapshot、merge、CowWriter、CowReader……它们到底是什么关系?为什么需要它们?
别急,我们一个一个拆开来看。
17.1 为什么需要快照?
先想一个问题:OTA升级过程中,系统正在写入新的分区数据。如果这时候突然断电了,或者系统崩溃了,会发生什么?
嗯,最坏的情况是——分区数据写了一半,系统既不是旧版本,也不是新版本。手机变砖了。
所以,我们需要一种机制来保证升级的原子性:要么全部成功,要么全部回滚。
快照(Snapshot)就是干这个的。它本质上是一个写时复制(Copy-on-Write, CoW)的机制。升级过程中,所有对分区的写入操作,都先记录到快照里,而不是直接覆盖原始数据。这样,如果升级失败,我们可以轻松地丢弃快照,恢复到升级前的状态。
17.2 Snapshot:升级的“保险箱”
Snapshot,说白了就是一个虚拟设备层。它在物理分区之上,提供了一个“看起来像分区,但实际是写时复制”的视图。
我举个例子。假设你的system分区有1GB数据。升级时,你只需要修改其中100MB。如果没有快照,你得把整个1GB都备份一遍,才能保证回滚。有了快照,你只需要记录那100MB的变化。
在代码层面,Snapshot是通过Device Mapper实现的。Android使用了一个叫做dm-snapshot的内核模块。它会创建一个映射设备,所有读操作优先从快照中读取,写操作则写入快照的“例外区”(exception store)。
# 创建一个快照设备的典型命令
dmsetup create system-snapshot \
--table "0 2097152 snapshot /dev/block/system /dev/block/system-cow P 8"
这里的system-cow就是Cow设备,用来存放增量数据。
dmsetup table 查看当前映射状态,确认快照是否处于激活状态。这比看日志快多了。
17.3 CowWriter:谁在写增量数据?
好,现在我们知道快照需要把增量数据写到某个地方。这个“地方”就是Cow设备。而CowWriter,就是负责往Cow设备里写数据的组件。
CowWriter运行在升级过程中。当系统决定要更新某个块时,它会先调用CowWriter,把原始块的数据复制到Cow设备中(如果是第一次修改),然后再写入新数据。
这个过程,我称之为“先备份,再修改”。
CowWriter的写入策略很有意思。它不是每次写一个块就立即刷盘,而是会批量合并。为什么呢?因为OTA升级通常涉及大量连续块的写入,如果每个块都单独写一次,性能会非常差。
// CowWriter 的简化写入逻辑
void CowWriter::WriteBlock(uint64_t block_num, const std::string& data) {
// 1. 检查这个块是否已经被备份过
if (!IsBlockBackedUp(block_num)) {
// 2. 如果没有,先备份原始数据到Cow设备
BackupOriginalBlock(block_num);
}
// 3. 写入新数据到快照设备
WriteToSnapshot(block_num, data);
}
我在项目中遇到过一个问题:CowWriter写入时,如果Cow设备空间不足,升级会直接失败。所以,Cow设备的大小估算非常关键。通常,它需要预留出整个分区大小的10%~20%作为增量空间。
17.4 CowReader:谁在读增量数据?
有写就有读。CowReader负责在合并阶段读取Cow设备中的数据。
你可能会问:合并阶段是什么?别急,我们马上讲到。
CowReader的逻辑相对简单。它需要遍历Cow设备中记录的所有“例外块”,把这些增量数据读出来,然后交给合并流程。
它的工作方式有点像日志回放。Cow设备里存储的,本质上就是一个操作日志:记录了哪些块被修改了,修改后的数据是什么。
// CowReader 的简化读取逻辑
bool CowReader::ReadNextBlock(uint64_t* block_num, std::string* data) {
// 从Cow设备中读取下一条记录
CowRecord record;
if (!ReadRecord(&record)) {
return false; // 没有更多记录了
}
*block_num = record.block_num;
*data = record.data;
return true;
}
17.5 Merge:把快照合并回去
升级完成后,系统需要把快照中的增量数据合并回原始分区。这个过程叫做Merge。
Merge是OTA升级的最后一步,也是最关键的一步。合并完成后,快照设备会被销毁,系统直接使用更新后的分区。
Merge的过程是增量式的。系统不会一次性合并所有数据,而是在后台逐步进行。这样做的好处是:即使合并过程中断电,下次开机时合并可以从中断的地方继续,而不是从头开始。
合并的粒度是块(block)。每次合并一个块,就把Cow设备中的新数据写回到原始分区,然后释放Cow设备中的对应记录。
// Merge 的简化流程
void MergeManager::PerformMerge() {
while (true) {
uint64_t block_num;
std::string data;
// 1. 从CowReader读取下一个待合并的块
if (!cow_reader_->ReadNextBlock(&block_num, &data)) {
break; // 所有块都合并完了
}
// 2. 将数据写回原始分区
WriteToBaseDevice(block_num, data);
// 3. 标记该块已合并
MarkBlockMerged(block_num);
}
}
这里有一个重要的细节:合并的顺序。系统会优先合并那些被频繁访问的块,比如系统启动时需要的文件。这样可以加快合并后的首次启动速度。
17.6 整体流程与SVG图
说了这么多,我们来画一张图,把整个流程串起来。
从图中可以看到,整个流程分为两个阶段:
- 升级阶段:CowWriter负责把增量数据写入Cow设备,快照设备保护原始数据不被覆盖。
- 合并阶段:CowReader从Cow设备中读取增量数据,Merge模块负责把这些数据写回原始分区。
17.7 避坑指南
最后,分享几个我在实际项目中踩过的坑:
- Cow设备空间不足:我曾经遇到过OTA升级到一半,Cow设备满了,导致升级失败。后来我们增加了空间预检机制,在升级前就估算好所需空间。
- 合并中断:合并过程中断电,下次开机后合并继续。但有一次,Cow设备的元数据损坏了,导致合并无法继续。我们后来增加了元数据的冗余备份。
- 性能问题:快照设备在读写时会有额外的开销。如果分区很大,性能下降会比较明显。我建议在性能敏感的场景下,尽量使用增量升级,减少快照的负担。
/sys/block/dm-<id>/dm/name 来确认哪个dm设备对应哪个快照。配合 dmsetup status 可以查看快照的使用情况。
好了,关于OTA升级与快照的内容,我们就讲到这里。快照机制是Android系统保证升级安全性的重要手段,理解它的工作原理,对排查升级问题非常有帮助。
公众号:蓝海资料掘金营,微信deep3321