0%

virtual ab方案调研

virtual ab方案调研

从recovery开始说起

Recovery挂载system

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
- CreateSnapshotPartitions
| - SnapshotManager::NewForFirstStageMount "first stage mount"
\ - New(info) "SnapshotManager构造函数,初始化两个目录 gsid_dir_=ota metadata_dir_=/metadata/ota"
| - ForceLocalImageManager "构造ImageManager"
\ - ImageManager::Open(gsid_dir_) "初始化两个目录 metadata_dir = /metadata/gsi/ota data_dir=/data/gsi/ota"
| - sm->NeedSnapshotsInFirstStageMount() "调用上面SnapshotManager的实例的该函数,根据merge state确定是否进行后续步骤"
\ - GetCurrentSlot() != Slot::Target " 根据这个判断是不是ota后的切换slot过程,Target=2,Source=1 "
" =1说明未进行ota,未进行ota就不需要挂载snapshot,return false"
| - ReadUpdateState
\ - ReadSnapshotUpdateStatus " /metadata/ota/state文件中获取ota后的merge状态"
" Unverified Merging MergeFailed 返回true"
| - true - InitRequiredDevices({"userdata"}) "NeedSnapshotsInFirstStageMount 返回true时"
\ - InitDeviceMapper "初始化dm框架,创建uevent监听 /sys/devices/virtual/misc/device-mapper"
| - return UeventCallback(uevent, &devices); "如果下面有userdata设备创建了,才说明required Device初始化成功了,才返回true。 而lp_metadata_partition_是FirstStageMount早期阶段初始化的/devices/virtual/block/dm-<>/dm/name文件中读出的super partition名字,赋值给/dev/block/mapper/<partition>"
| - true - sm->CreateLogicalAndSnapshotPartitions(lp_metadata_partition_) "创建logical device和snapshot device"
\ - ReadMetadata(opener, super_device, slot) "读取super device对应slot的metadata描述"
| - CreateLogicalPartitionParams (for params.partition:metadata->partitions) "挨个挂接metadata描述的partitions及其对应的snapshot设备"
\ - MapPartitionWithSnapshot(std::move(params))
\ - GetSnapshotStatusFilePath(params.GetPartitionName() "/metadata/ota/snapshots/<partition_name>"
| - ReadSnapshotStatus(lock, params.GetPartitionName(), &*live_snapshot_status) # /metadata/ota/snapshots/<partition_name>,
\ - status->ParseFromFileDescriptor(fd.get()) "解析该文件,获得live_snapshot_status状态"
| - # MERGE_COMPLETED | NONE | "cow_partition_size + cow_file_size = 0 代表no live snapshot"
| - live_snapshot_status.has_value- "如果有live snapshot"
\ - params.device_name = GetBaseDeviceName(params.GetPartitionName()); "devicename变成了<partionname>—base"
| - created_devices.EmplaceBack<AutoUnmapDevice>(&dm, params.GetDeviceName())
| - CreateLogicalPartition(params, &base_path) "映射logical device 动态分区的映射,返回的path为/dev/block/dm-<num>"
| - live_snapshot_status.has_value- "如果有live snapshot"
\ - dm.GetDeviceString(params.GetDeviceName(), &base_device) "返回base_device major:minor设备号"
| - CreateLogicalPartitionParams cow_params = params
| - MapCowDevices(lock, cow_params, *live_snapshot_status, &created_devices, &cow_name) "映射cow_device写时复制"
\ - cow_image_name = GetCowImageDeviceName(partition_name) #<partion_name>-cow-img
| - *cow_name = GetCowName(partition_name) #<partition_name>-cow
| - EnsureImageManager()
| - "因为前面已经执行过ForceLocalImageManager,所以image_不为空,后面的步骤不走了"
| - MapCowImage(partition_name, remaining_time)
| - has_local_image_manager_- true "image_为之前ForceLocalImageManager时的实例,指向ImageManager"
\ - images_->MapImageWithDeviceMapper(opener, cow_image_name, &cow_dev) "cow_image_name为<partion_name>-cow-img"
| - MapWithDmLinear(opener, cow_image_name, {}, &ignore_path)
\ - metadata = OpenMetadata(metadata_dir_) "metadata_dir_=/metadata/gsi/ota"
\ - metadata_file = GetMetadataFile(metadata_dir_) "/metadata/gsi/ota/lp_metadata"
| | | - metadata = ReadFromImageFile(metadata_file) "从上述文件中解析出metadata描述"
| - super = GetMetadataSuperBlockDevice() "获得super封装"
| - block_device = GetBlockDevicePartitionName(super)
| - CreateLogicalPartition(params, path) "dm-linear 跟之前的动态分区的逻辑一样,映射设备,获得path /dev/block/dm-<num>"
| - WriteStringToFile(status_string, status_file) "/metadata/gsi/ota/<parition_name-cow-img>.status中写入dm:<partition_name-cow-img>"
| - dm.GetDeviceString(name, dev) "返回major:minor号给dev"
| - created_devices->EmplaceBack<AutoUnmapImage>(images_.get(), cow_image_name) "created_devices再push新的cow_img"
| - CreateLogicalPartitionParams cow_partition_params = params (.partition_name=*cow_name;);
"cow_name = <partition>_cow"
| - CreateDmTable(cow_partition_params, &table) "创建映射表"
| - cow_partition_sectors = snapshot_status.cow_partition_size() / kSectorSize "扇区"
| - cow_image_sectors = snapshot_status.cow_file_size() / kSectorSize;
| - table.Emplace<DmTargetLinear>(cow_partition_sectors, cow_image_sectors, cow_image_device, 0);
" 从cow-img的尾部开始算,长度为cow_image_sectors 个扇区,block_device还是用前面挂接的cow-img的device"
| - dm.CreateDevice(*cow_name, table, &cow_path, remaining_time) "为<partition_name>-cow创建dm设备"
| - created_devices->EmplaceBack<AutoUnmapDevice>(&dm, *cow_name); "push到created_devices中"
| - dm.GetDeviceString(cow_name, &cow_device) "<partion_name>-cow获取设备号major /minor"
| - MapSnapshot(lock, params.GetPartitionName(), base_device, cow_device, remaining_time, path)
"base_device 代表动态分区的base_device, cow_device代表映射partion_name-cow的设备,为这两个设备之间建立映射快照"
\ - ReadSnapshotStatus(name, &status) "/metadata/ota/snapshots/<partition_name>读取状态,如果是MERGE_COMPLETED|NONE就退出,不需要快照了"
| - snapshot_sectors = status.snapshot_size() / kSectorSize "snapshot快照的扇区数"
| - linear_sectors = (status.device_size() - status.snapshot_size()) / kSectorSize "logical本身的扇区数"
| - ReadUpdateState() "/metadata/ota/state中读取ota的状态 MergeCompleted| MergeNeedsReboot return false,退出
Merging|MergeFailed mode=merge或者Persistent"
| - snap_name = GetSnapshotExtraDeviceName(partition_name) "<parition_name>-inner"
| - table.Emplace<DmTargetSnapshot>(0, snapshot_sectors, base_device, cow_device, mode, kSnapshotChunkSize)
"创建映射表,单位是4k,即8*sector_size DmTargetSnapshot start=0. length = snapshot_sectors,mode=merge|Persistent"
"DmTargetSnapshot::name() mode=merge时返回"snapshot-merge",其他状态下返回"snapshot""
| - dm.CreateDevice(snap_name, table, dev_path, timeout_ms) "创建dm设备"
| - linear_sectors>0 - "动态分区里的img不为空 "
\ - dm.GetDeviceString(snap_name, &snap_dev) "获取snap_dev的major/minor"
| - table.Emplace<DmTargetLinear>(0, snapshot_sectors, snap_dev, 0); "先是snap_device"
| - table.Emplace<DmTargetLinear>(snapshot_sectors, linear_sectors, base_device, snapshot_sectors);
"再是linear_device追加"
| - dm.CreateDevice(name, table, dev_path, timeout_ms) "最终创建<partition_name>的dm设备映射"

小结

对于动态分区的挂载流程,最终是按创建设备时的第一个参数device-name进行匹配查找对应的dm设备,从上述映射过程中可以发现,

  • 没有live.snapshot时,只进行了CreateLogicalPartition过程,传入的device_name就是partition_name

  • 而有live snapshot时,第一步的CreateLogicalPartition过程传入的device_name<partition_name>-base,再走完了MapSnapshot过程后,最终创建的dm设备device_name才是partition_name

    映射表,前面是base_device到cow_device的snapshot,后面是其本身的动态分区的数据。

  • 借助动态分区的实现逻辑,既然可以有metadata描述分区信息的描述块这种东西,复用见userdata上cow_device的数据描述放在了metadata分区,然后就可以通过dm-linear 将数据映射成dm设备。
    当前的分析还只是在userspace,跟核心的实现应该是device-mapper的内核实现,dm-snapshot 和dm-snapshot-merge的内部是怎样实现的,才是virtual ab方案的真正的核心。

    snapshot理解

    android的virtual ab方案中snapshot的方案来自于lvm(Logical Volume Manager),要理解snapshot先从lvm说起
    首先可以想通过lvm大致还原snapshot创建的过程

lvm snapshot过程理解

在pc机上创建snapshot过程模拟

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 创建device的模拟设备
dd if=/dev/zero of=sd10 bs=4M count=1000
# 关联loop设备
losetup /dev/loop0 sd10
# 为loop设备创建lvm的物理分区
sudo pvcreate /dev/loop0
sudo pvscan
# 创建卷组
sudo vgcreate -s 32M nickgroup /dev/loop0
sudo vscan
sudo vgdisplay
# 创建卷
lvcreate -L 2G --name nickgroup/nicklv00
sudo lvscan
# 为卷建立文件系统
sudo mkfs.ext4 /dev/nickgroup/nicklv00
sudo mount -t ext4 /dev/nickgroup/nicklv00 nicklv
echo "hello world" > test_change; echo "i'm not change">test_no_change
# 为卷建立snapshot
sudo lvcreate -L 1G --snapshot --name nicklv00_snap nickgroup/nicklv00
sudo mount -t ext4 /dev/nickgroup/nicklv00_snap nicklv_snap
# 改变原始卷某文件的内容
echo "i'm change" >> nicklv/test_change

为原始卷建关联snapshot设备后,改变原始卷的某文件的内容,做一下对比
首先,先看下未修改的文件test_no_change的内容,在原始卷和snapshot上内容都是一样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
[~/program/testlvm] - [五 4月 10, 15:33]
[$] -> sudo istat /dev/nickgroup/nicklv00_snap 13
inode: 13
Allocated
Group: 0
Generation Id: 2264406482
uid / gid: 1000 / 1000
mode: rrw-rw-r--
Flags: Extents,
size: 16
num of links: 1
Inode Times:
Accessed: 2020-04-10 11:22:07.917092707 (CST)
File Modified: 2020-04-10 11:22:03.053131212 (CST)
Inode Modified: 2020-04-10 11:22:03.053131212 (CST)
File Created: 2020-04-10 11:22:03.053131212 (CST)
Direct Blocks:
32898
[~/program/testlvm] - [五 4月 10, 15:33]
[$] -> sudo istat /dev/nickgroup/nicklv00 13
inode: 13
Allocated
Group: 0
Generation Id: 2264406482
uid / gid: 1000 / 1000
mode: rrw-rw-r--
Flags: Extents,
size: 16
num of links: 1
Inode Times:
Accessed: 2020-04-10 11:22:07.917092707 (CST)
File Modified: 2020-04-10 11:22:03.053131212 (CST)
Inode Modified: 2020-04-10 11:22:03.053131212 (CST)
File Created: 2020-04-10 11:22:03.053131212 (CST)
Direct Blocks:
32898

而对于修改的文件 test_change

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[$] -> sudo istat /dev/nickgroup/nicklv00 12
inode: 12
Allocated
Group: 0
Generation Id: 3262808564
uid / gid: 1000 / 1000
mode: rrw-rw-r--
Flags: Extents,
size: 37
num of links: 1
...
Direct Blocks:
33280
[~/program/testlvm] - [五 4月 10, 15:34]
[$] -> sudo istat /dev/nickgroup/nicklv00_snap 12
inode: 12
Allocated
Group: 0
Generation Id: 3777049701
uid / gid: 1000 / 1000
mode: rrw-rw-r--
Flags: Extents,
size: 24
num of links: 1
...

Direct Blocks:
32897

发现修改文件的inode指向的direct block内容发生了变化,且snapshot的direct block对应的块号比较小,说明其是原始的,也可以在改之前验证下,确实是这样的。
说明修改原始卷后,共享的data block的内容并没有改变,而是修改了原始卷的inode的data block索引,并将修改后的内容写入到了新的data block上。
但是又不能简单这样理解,因为lvm是逻辑卷管理,并不代表实际的物理数据也是这样的,lvm也是借助device_mapper来实现的,其extents指向关系并不能简单的从文件系统的描述data block的变化得到。不能得到原始卷中改变了的内容只是将data block块换了位置存放新内容,而snapshot的inode继续指向原始datablock的结论,因为这样是违反常识的,一旦data block被耗尽,那原始的data block的内容岂不是要被删掉,那快照还有什么意义呢。
这里先提前将snapshot的原理说下
创建快照,实际上就是将原始卷文件系统的元数据信息复制了一份(包括super block inode imap bmap group gdt)等,这里可以先验证下

1
sudo dumpe2fs /dev/nickgroup/nicklv00_snap|  sudo dumpe2fs /dev/nickgroup/nicklv00

文件系统结构
lvm snapshot

实际上snapshot的原理可以理解为下面的过程:
左图为最初创建的快照数据卷状况,LVM 会预留一个区域 (比如左图的左侧三个 PE 区块) 作为数据存放处。 此时快照数据卷内并没有任何数据,而快照数据卷与源数据卷共享所有的 PE 数据, 因此你会看到快照数据卷的内容与源数据卷中的内容是一模一样的。 等到系统运行一阵子后,假设 A 区域的数据被更新了(上面右图所示),则更新前系统会将该区域的数据移动到快照数据卷中, 所以在右图的快照数据卷中被占用了一块 PE 成为 A,而其他 B 到 I 的区块则还是与源数据卷共享!
由於快照区与原本的 LV 共享很多 PE 区块,因此快照区与被快照的 LV 必须要在同一个 VG 上头,下面两点非常重要:
VG中需要预留存放快照本身的空间,不能全部被占满。
快照所在的 VG 必须与被备份的 LV 相同,否则创建快照会失败。
重要的一点是,snapshot会存在一个cow img,但原始数据改变后,会将原来的内容拷贝一份,放到这个cow img中,而且data block号会复用,建立快照的过程,就是数据映射的过程,快照一旦建立,数据的映射关系就确定下来了。快照从文件系统层面上讲也固定下来了

可以验证上述结论的实例是:
创建总大小为400M的loop设备,在这上面创建200M的原始卷,往其中写入198M大小的文件,对原始卷可以创建最小32M的snapshot,发现可以创建成功,也可以挂载snapshot,挂载后可以198M的文件是正常的,说明快照不是简单的备份关系。
而在上述过程走完后,可以修改原始卷中198M大小的文件,发现无论修改了什么内容,只要该文件被改了,那备份卷就被损坏了,同时原始卷中文件写入的操作会报空间不足的错误。
从上面snapshot的实现过程来推测,写时复制过程应该是借助device-mapper修改了物理块-逻辑块的映射关系,不然data block的复用就有问题了。

下面再看下merge过程。
合并快照的操作也需要卸载源数据卷:
确认源数据卷和快照数据卷都没有被挂载后就可以执行合并快照的操作了

1
sudo lvconvert --merge nickgroup/nicklv00_snap

merge过程可以看作是将snapshot的cow-img拷贝,覆盖掉改动文件的inode和data block的过程。merge之后,snapshot就变成无效的了
这个过程中牵涉到的核心点其实还是data_block的处理,因为从snapshot快照的建立到写时复制过程,如果要保证改写前后snapshot文件系统内data block块号不变,需要重定向物理扇区的过程,这里面需要device-mapper的参与。

lvm-android上snapshot

从lvm的snapshot到android上的变体,可以发现的是android上是改动了一些东西,是不是可以认为<partition_name>-cow是snapshot复制的原始卷的元数据区,而<partition_name>-cow-img是写时复制的数据区呢。这个有待拿到真机后再进一步验证。

理解v-ab的升级过程

下面还需要研究virtual ab打包过程的变化,以便更好理解virtual ab的升级过程。

对于升级过程, virtual ab方案使用的升级包和ab的升级包是共用的, 从build下的打包change来看,并没有直接适配virtual ab的升级包。另一方面,从update-engine的分析来看,在virtual ab的属性打开后,额外进行了一些操作:

  1. 根据手机中版本的信息和升级包的信息作比对, 通过文件系统级别判断中差异量的大小,根据该大小,新建一个文件,用来存放cow 区。创建欲升级分区的dm-snap镜像,放在super的尾部,存放了欲升级分区的文件系统的元数据的复制信息,形成cow-device。
  2. 存放cow(写时复制)区的文件,通过ioctl获取其占用的block map,和lp的extent数据结构结合, 最终形成dm-linear的索引,放到metadata分区中。
  3. 升级过程中(整包升级及差分升级过程),将变动的文件存放到cow区域
  4. 升级结束后,下次重启时,将cow-device和cow-file(写时复制区)拼接,形成snapshot-img。 对系统分区进行snapshot的映射,映射后的dm设备即为新版本。如果能正常开机,挂载dm-snapshot-merge在内核空间进行merge,merge的过程即将cow-file的内容覆盖到系统img中被修改的block块的过程。merge开始前,应该会根据需要resize动态分区。merge完成后,系统分区被更新为新版本。

kernel的实现:

kernel/msm-4.19/Documentation/device-mapper/snapshot.txt

  • *) snapshot <persistent?>

    A snapshot of the block device is created. Changed chunks of <chunksize> sectors will be stored on the <COW device>. Writes will
    only go to the
    . Reads will come from the or
    from for unchanged data. will often be
    smaller than the origin and if it fills up the snapshot will become
    useless and be disabled, returning errors. So it is important to monitor
    the amount of free space and expand the before it fills up.

    <persistent?> is P (Persistent) or N (Not persistent - will not survive
    after reboot). O (Overflow) can be added as a persistent store option
    to allow userspace to advertise its support for seeing “Overflow” in the
    snapshot status. So supported store types are “P”, “PO” and “N”.

    The difference between persistent and transient is with transient
    snapshots less metadata must be saved on disk - they can be kept in
    memory by the kernel.

    When loading or unloading the snapshot target, the corresponding
    snapshot-origin or snapshot-merge target must be suspended. A failure to
    suspend the origin target could result in data corruption.

  • snapshot-merge

    takes the same table arguments as the snapshot target except it only
    works with persistent snapshots. This target assumes the role of the "snapshot-origin" target and must not be loaded if the "snapshot-origin" is still present for <origin>.

    Creates a merging snapshot that takes control of the changed chunks
    stored in the of an existing snapshot, through a handover
    procedure, and merges these chunks back into the . Once merging
    has started (in the background) the may be opened and the merge
    will continue while I/O is flowing to it. Changes to the are
    deferred until the merging snapshot’s corresponding chunk(s) have been
    merged. Once merging has started the snapshot device, associated with
    the “snapshot” target, will return -EIO when accessed.

从上述文档上看, 首先snapshot-origin的角色相当与传统的lvm的snapshot机制,即文件改动时,改动的内容写到了origin区域,而原始内容备份到了cow-device上。snapshot-merge的角色相当于snapshot-origin。 snapshot的角色机制与snapshot-origin机制相反: 改动的内容写到cow-device,原始内容不变。

通过这两个机制能保证在ota升级过程中,挂载target为snapshot,改动的内容写到cow-device的cow-file中。 ota升级结束后,merge过程中,挂载类型为snapshot-merge改动的内容写到原始区域,即系统分区空间。

从update_engine的升级过程看,以及对snapshot的实例分析,virtual ab的升级包应该和ab的升级包是同一个。差异化处理是在update_engine具体的升级过程。对snapshot的实例验证表明,snapshot的cow写时复制并不是以文件系统为基本单位的,而是以block为基本单位,block内容变更了,才会将原来的block的内容复制到cow-file中,并重用之前的block块号,再更新block写入新的修改,在lvm上是这样处理的。对于android而言,这个过程需要反过来,所以推测android上改写了snapshot的逻辑,再检测到block块号的内容变更后,由于系统分区为只读设备,新的内容是写进了cow-file中。而源分区的数据并没有进行改动。

virtual ab recovery下的挂载流程

源码位于

1
kernel/msm-4.19/drivers/md/dm-snap*.c

android针对cow 镜像采用了移花接木的手段,正常recovery子系统是不能访问userdata分区的,但是cow-file又是存在于userdata分区上,而对于ab/virtual ab升级来说,虽然是在主系统升级,但是还是需要recovery子系统作为一个备胎支持ab升级的能力。所以问题是recovery子系统上如何完整的挂载snapshot呢?

这里从前面的流程分析可见,

对于某个系统分区的cow镜像来说,存在一个_cow 为分区名的分区在super分区的metadata上,而对应该分区的分区组名为cow,元数据区域也在这个super上,对于cow-file,即写时复制的区域,是存在了userdata分区上的,而其metadata的描述是存在了metadata分区身上。对于metadata描述是指的metadata数据结构,其中包含了分区extents数据段,cow-file是先初始化,根据当前版本和目标版本的metadata的描述的差异,先计算cow-file需要多大的空间,直接在userdata上创建了这样一个文件,然后就可以通过ioctl命令获得其block map,将block map转化成extents,再通过dm-linear 根据extents制成dm table,最终可以创建dm设备,还原cow-file,再同cow-partition的元数据信息拼接形成完成cow partition的镜像。再通过map snapshot和源分区关联起来。