[TOC]
学习笔记
HPatchLite\HDiffPatch\libHDiffPatch\HPatchLite\hpatch_lite.c
Hptach 算法
1. Hptach 的核心:加法差分 (Additive Diff)
这种方法可以被称为加法差分或增量差分。
- 核心公式:
NewData = OldData + DiffData - 反向公式:
DiffData = NewData - OldData(在生成补丁时计算)
工作流程图:
1 | +----------------------+ |
为什么这种方法很有效?
因为在多数文件更新中(无论是程序、文档还是图片),大部分内容是不变的,或者只有微小的改动。
- 当
NewData[i] == OldData[i]时,DiffData[i]就是 0。 - 当
NewData[i]与OldData[i]非常接近时,DiffData[i]就是一个很小的正数或负数。
这导致 DiffData 中充满了大量的 0 和小数值,这种数据的熵非常低,因此可以被像 zlib、LZMA 这样的通用压缩算法极大地压缩。Hptach 正是利用了“加法差分”与“通用压缩”的组合拳来获得极高的压缩率。
2. 其他类型的差分编码
“差分编码”是一个广义的概念,其核心思想是“不记录数据本身,而是记录数据与某个参考(基准)之间的差异”。加法只是实现这种思想的一种方式。
以下是其他几种常见的差分编码方式:
a. 复制/指令式差分 (Copy/Instruction-based Diff)
这是最著名的一种,以 rsync 和 bsdiff 算法为代表。它的核心不是字节运算,而是生成一系列指令。
核心指令:
COPY(old_pos, length): 从旧文件的old_pos位置复制length长度的数据到新文件。ADD(diff_pos, length): 从补丁文件的diff_pos位置复制length长度的新数据到新文件。
优点: 对于数据块发生移动(例如,在代码中移动一个函数)、大段重复或大段删除的情况,这种方法极为高效。它只需要记录一条“复制”指令,而不需要记录大量字节的加法差分。因此,
bsdiff通常能生成比 Hptach 更小的补丁包。缺点: 算法更复杂,通常需要更多的内存,并且在应用补丁时速度可能比纯粹的加法差分要慢。
b. 异或差分 (XOR Diff)
这种方法在某些场景下也很有用,尤其是在处理二进制数据或需要可逆操作时。
- 核心公式:
NewData = OldData ^ DiffData - 优点: 异或操作有一个独特的对称性:
A ^ B = C,那么A ^ C = B。这意味着打补丁和制作补丁可以用完全相同的操作,非常优雅。 - 缺点: 对于很多数据类型,异或后的差分数据不一定比加法差分的压缩率更高。
总结对比
| 差分方法 | 核心操作 | 优点 | 缺点 | 典型应用 |
|---|---|---|---|---|
| 加法差分 (Hptach) | New = Old + Diff |
算法简单,速度极快,对微小改动友好,差分数据压缩率高 | 对数据块移动不敏感,可能不是最小的补丁 | Hptach, Google Courgette |
| 指令式差分 (bsdiff) | COPY(old) 和 ADD(diff) 指令集 |
对数据块移动、复制、删除极为高效,通常能生成最小的补丁 | 算法复杂,内存消耗大,速度相对较慢 | bsdiff, rsync, Git 的 pack 文件 |
| 异或差分 (XOR) | New = Old ^ Diff |
操作对称可逆,实现简单 | 差分数据的可压缩性不一定最优 | 二进制文件版本控制,一些数据库增量备份 |
结论:
您看到的“按位相加”是 Hptach 所选择的差分编码核心。Hptach 的设计哲学是在速度、内存占用和补丁大小之间寻求一个出色的平衡点。它通过极其快速和简单的加法运算,生成高度可压缩的差分数据,从而在大多数场景下都能获得非常好的性能和效果。
hpatch_lite_open Hpatch Lite 头部数据解析
- 负责打开一个补丁文件(diff_data),并解析其文件头,以获取补丁应用所必需的元数据
原理与设计思路解析
hpatch_lite_open 函数的核心职责是验证并解析 Hpatch Lite 格式的补丁文件头部。这个头部是一个紧凑的、固定大小(hpi_kHeadSize,通常为4字节)的数据块,包含了应用补丁所需的所有初始信息。
- 文件头结构 (Header Format)
Hpatch Lite 的文件头设计得非常紧凑,在一个4字节的头部中编码了多种信息,其结构如下:
| 字节偏移 | 字段名称 | 长度 (字节) | 描述 |
|---|---|---|---|
| 0 | Magic Byte 1 | 1 | 固定为 'h',用于文件类型识别。 |
| 1 | Magic Byte 2 | 1 | 固定为 'I',与'h'共同组成 “hI” 魔法标识。 |
| 2 | compress_type |
1 | 压缩类型。标识补丁数据使用了哪种压缩算法(如 zlib, bzip2, lzma 等)。 |
| 3 | Packed Info | 1 | 一个被打包的字节,通过位运算存储了3种不同的信息。 |
Packed Info 字节 (第3字节) 的详细解析:
这个字节被巧妙地划分成三个部分:- 高2位 (Bits 6-7):
(byte >> 6)- 版本号 (Version Code): 用于标识 Hpatch Lite 格式的版本,确保兼容性。代码会检查它是否等于预定义的
kHPatchLite_versionCode。
- 版本号 (Version Code): 用于标识 Hpatch Lite 格式的版本,确保兼容性。代码会检查它是否等于预定义的
- 中3位 (Bits 3-5):
((byte >> 3) & 7)uncompressSize的字节数: 表示解压后(或原始)数据的大小值本身占用了多少个字节(0-7字节)。
- 低3位 (Bits 0-2):
(byte & 7)newSize的字节数: 表示应用补丁后新文件的大小值本身占用了多少个字节(0-7字节)。
- 高2位 (Bits 6-7):
数据读取流程
- 读取固定头部: 首先,函数读取固定长度(
hpi_kHeadSize)的头部数据。 - 校验与解析:
- 校验文件魔法标识
'hI'和版本号,如果不对,则说明不是一个有效的或兼容的 Hpatch Lite 文件,解析失败。 - 直接读取压缩类型。
- 通过位运算从 Packed Info 字节中解包出
newSize和uncompressSize这两个字段的长度。
- 校验文件魔法标识
- 读取可变长数据: 根据上一步解析出的长度,函数接着从数据流中分别读取
newSize和uncompressSize的实际数据。例如,如果解析出newSize的长度是4字节,它就会再读取4个字节。 - 字节到整数的转换 (
_hpi_readSize):- 由于
newSize和uncompressSize是以多个字节的形式存储的,需要一个辅助函数_hpi_readSize将这些字节转换成一个整数。 - 这个函数的实现方式
v=(v<<8)|(hpi_pos_t)buf[len]并且len从高到低递减,表明它将字节数组视为 小端序 (Little-Endian) 来解析。也就是说,缓冲区中的第一个字节buf[0]是整数的最低位字节 (LSB)。
- 由于
- 读取固定头部: 首先,函数读取固定长度(
设计优点
- 紧凑: 将多个元数据打包进一个字节,节省了头部空间。
- 灵活:
newSize和uncompressSize的长度是可变的,对于较小的文件,可以用1或2个字节来表示其大小,从而节省空间;对于非常大的文件,最多可以使用8个字节(64位)来表示,扩展性强。
代码解析
1 | /** |
_cache_unpackUInt 可变长度整数解码
原理与设计思路解析
这段代码的核心是实现了一个高效的输入缓存(Input Cache)机制,并在此基础上提供了一个用于解码可变长度整数的工具函数。这是一个设计精良、高效且可扩展的底层数据读取模块。
设计目的:性能优化
- 操作系统和硬件的I/O操作(如读文件、收网络包)通常是昂贵的、有延迟的。频繁地进行单字节的I/O请求会导致性能低下。
_hpi_TInputCache结构体通过引入一个内存缓冲区cache_buf,完美地解决了这个问题。它通过_cache_update函数一次性从底层输入流inputStream读取一大块数据到内存中。- 之后,
_cache_read_1byte等函数就可以直接从高速的内存中逐字节地提供数据,只有当内存缓冲区耗尽时,才需要再次进行真正的I/O操作。这大大减少了系统调用的次数,提升了数据读取效率。
数据结构 (
_hpi_TInputCache)- 这是一个典型的”生产者-消费者”模型的简化版。
_cache_update是生产者,负责填充数据。_cache_read_1byte是消费者,负责消耗数据。 cache_begin和cache_end两个索引(或指针)是管理缓冲区的关键。它们定义了一个“窗口”[cache_begin, cache_end),表示缓冲区中当前有效的、尚未被读取的数据范围。当cache_begin == cache_end时,意味着缓冲区已被读空。
- 这是一个典型的”生产者-消费者”模型的简化版。
核心函数与技巧
_cache_update(填充器): 这是缓存机制的“后勤”。它被设计为在缓存为空时被动调用,其职责单一且明确:填充缓存并重置状态。它通过函数指针read_code的设计,将缓存管理逻辑与具体的I/O实现(读文件、读网络等)解耦,使得这套缓存机制可以适用于任何类型的输入流。_cache_read_1byte(读取器): 这是提供给上层逻辑的主要接口。它对上层屏蔽了复杂的缓存管理细节。goto语句在此处的使用是一种性能优化技巧,用于避免在成功更新缓存后进行函数递归调用或重复编写逻辑,这在底层的C代码中很常见。_cache_unpackUInt(解码器) 与 VLQ/LEB128 详解:
这个函数揭示了 Hptach 所处理数据的一种底层格式。它实现了一种名为可变长度数量 (Variable-length quantity, VLQ) 或 LEB128 (Little-Endian Base 128) 的编码方案。1. 核心思想
用可变数量的字节来表示一个整数。对于数值较小的整数,使用较少的字节(通常是1个字节);对于数值较大的整数,使用较多的字节。因为在大多数数据中,小数值的出现频率远高于大数值,所以这种方法能有效地压缩数据。2. 编码规则
- 每个字节提供 7位 (bit) 用来存储数据。
- 每个字节的最高位 (Most Significant Bit, MSB) 作为“连续标志位”。
- 如果 MSB 是
1,表示后面还有后续的字节属于同一个整数。 - 如果 MSB 是
0,表示这是这个整数的最后一个字节。
3. 编码示例:数字
300
我们来看如何将整数300编码成这种格式:- 二进制表示:
300的二进制是0000 0001 0010 1100。 - 分组: 将二进制从右到左按7位一组进行切分。
- 第一组 (低位):
010 1100(十进制为 44) - 第二组 (高位):
000 0010(十进制为 2)
- 第一组 (低位):
- 添加标志位: 我们需要两个字节来存储。第一个字节的标志位设为
1,第二个(最后一个)字节的标志位设为0。- 第一个字节:
1(标志位) +000 0010(高位数据) ->10000010(十六进制0x82) - 第二个字节:
0(标志位) +010 1100(低位数据) ->00101100(十六进制0x2C)
- 第一个字节:
- 所以,整数
300最终被编码为字节流:0x82 0x2C。
4. 解码流程 (即
_cache_unpackUInt函数的逻辑)
现在,我们模拟函数如何从字节流0x82 0x2C中解码出300。
假设v=0,isNext=1(初始调用时)。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+---------------------------------+
| 开始解码 |
| v = 0, isNext = 1 |
+---------------------------------+
|
+---------------------------------+
| 循环: while(isNext) |
| (isNext is 1) |
+---------------------------------+
|
v
+---------------------------------+
| b = _cache_read_1byte() |
| (读取第一个字节 0x82) |
+---------------------------------+
|
v
+---------------------------------+
| v = (v << 7) | (b & 127) |
| v = (0 << 7) | (0x82 & 127) |
| v = 0 | 2 => v = 2 |
+---------------------------------+
|
v
+---------------------------------+
| isNext = b >> 7 |
| isNext = 0x82 >> 7 => 1 |
+---------------------------------+
|
+---------------------------------+ <---+
| 循环: while(isNext) | | (回到循环顶部)
| (isNext is 1) | |
+---------------------------------+ |
| |
v |
+---------------------------------+ |
| b = _cache_read_1byte() | |
| (读取第二个字节 0x2C) | |
+---------------------------------+ |
| |
v |
+---------------------------------+ |
| v = (v << 7) | (b & 127) | |
| v = (2 << 7) | (0x2C & 127) | |
| v = 256 | 44 => v = 300 | |
+---------------------------------+ |
| |
v |
+---------------------------------+ |
| isNext = b >> 7 | |
| isNext = 0x2C >> 7 => 0 | |
+---------------------------------+ |
| |
+---------------------------------+ ----+
| 循环: while(isNext) |
| (isNext is 0) |
+---------------------------------+
|
v
+---------------------------------+
| 返回 v |
| (返回 300) |
+---------------------------------+
代码解析
1 | // 定义一个名为 _hpi_TInputCache 的结构体,用于管理带缓冲的输入流。 |
_patch_copy_diff 和 _patch_add_old_withClip 将上一层函数解析出的指令转化为对文件的具体读写操作
原理与设计思路解析
这两个函数是补丁应用过程中的两个基本操作原子,分别对应了我们之前提到的“新数据”和“覆盖数据”的处理。
_patch_copy_diff:处理“新数据”- 职责单一: 这个函数的唯一目标就是将补丁文件 (
diff) 中的一段连续数据,原封不动地复制到新文件 (out_newData) 中。 - 缓存感知 (Cache-Aware): 它并非逐字节复制,而是以“块”(chunk)为单位进行。它会首先检查
diff的输入缓存区里有多少可用数据 (decodeStep),然后一次性处理整个可用数据块。这种块处理方式远比单字节I/O高效。 - 流程: 不断地从
diff缓存中取出数据块,写入到新文件,直到满足要求的copyLength为止。如果中途缓存耗尽,它会自动调用_cache_update来填充缓存。
- 职责单一: 这个函数的唯一目标就是将补丁文件 (
_patch_add_old_withClip:处理“覆盖数据”- 双重功能: 这个函数通过
isNotNeedSubDiff标志实现了两种操作模式,是 Hpatch 算法的一个关键实现:- 差分合成模式 (
isNotNeedSubDiff为false): 这是标准的差分补丁逻辑。它执行NewData = OldData + DiffData的操作。数据流是:从旧文件读一块数据到temp_cache-> 从diff文件读一块差分数据 -> 调用addData将差分数据加到temp_cache中 -> 将temp_cache的结果写入新文件。 - 旧数据复制模式 (
isNotNeedSubDiff为true): 在某些情况下,新文件的一部分可能与旧文件的某部分完全相同。此时,就不需要在补丁文件中存储差分数据(因为差分都是0),只需记录一个“从旧文件某处复制一段数据过来”的指令即可。此时,这个函数的数据流简化为:从旧文件读一块数据到temp_cache-> 直接将temp_cache的内容写入新文件。
- 差分合成模式 (
temp_cache的作用: 在这里,temp_cache扮演了一个至关重要的“工作台”角色。所有来自旧文件的数据和来自补丁文件的差分数据,都会在这个临时的内存区域中进行合并运算,然后再被写入最终目标。addData函数: 这是一个被强制内联 (hpi_force_inline) 的辅助函数,因为它执行的是最频繁、最底层的字节加法操作。*dst++ += *src++是一个非常高效的C语言惯用法,编译器能将其优化为非常快速的机器指令。
- 双重功能: 这个函数通过
总而言之,hpatch_lite_patch 负责“决策”(解析指令),而这两个函数负责“执行”(搬运和加工数据)。它们的设计共同保证了补丁过程的高效性和灵活性。
代码解析
1 | /** |
_hpi_cache_success_finish Hpatch Lite 源码解析:流结束状态校验
原理与设计思路解析
这个函数 _hpi_cache_success_finish 是在整个补丁应用过程 hpatch_lite_patch 的最后一步被调用的,作为最终的校验条件之一。
函数目的:最终健全性检查 (Final Sanity Check)
- 它的作用不是检查补丁文件末尾是否还有多余的数据,而是对输入流缓存 (
_TInputCache) 的最终状态进行一次简单的健全性检查。 - 它被用于
return (newSize==newPosBack) & _cache_success_finish(&diff);这一行,与“生成的新文件大小是否正确”这个条件共同决定了整个补丁操作是否成功。
- 它的作用不是检查补丁文件末尾是否还有多余的数据,而是对输入流缓存 (
self->cache_end != 0的含义- 要理解这个判断,我们需要回顾
_TInputCache的工作机制。cache_end成员变量记录的是当前缓冲区中有效数据的末尾位置(或者说,有效数据的长度)。 - 这个值只会在一个地方被设置为
0:在_cache_update函数中,当底层的read_code函数尝试从输入流读取数据,但失败了或者读到了文件末尾 (EOF),返回的读取长度为0时。 - 因此,
self->cache_end变成0标志着输入流已经枯竭。 - 那么,
return (self->cache_end != 0)的逻辑就是:“只要输入流没有进入枯竭状态,就认为是成功的。”
- 要理解这个判断,我们需要回顾
为什么这是有效的?
- 在一个设计良好的补丁流程中,
hpatch_lite_patch函数的主循环会精确地读取所有它需要的数据。如果补丁文件是完整的,那么当主循环结束时,所有必需的数据都已经被消费掉了。 - 此时,
_TInputCache的状态可能是:其内部缓冲区中还有一些从最后一次I/O读取中剩下的、但未使用的数据。在这种情况下,cache_end显然不为0。 - 如果补丁文件被意外截断,那么在主循环的某个时刻,
_cache_read_1byte会发现缓存为空并调用_cache_update,而_cache_update会因为读不到数据而将cache_end置为0,并返回失败。这个失败会通过_CHECK宏被捕获,导致hpatch_lite_patch提前失败退出。 - 所以,这个最终的检查可以看作是一个“兜底”的校验,确保补丁流程正常结束后,输入流的状态依然是健康的(即没有在不该结束的时候结束)。
- 在一个设计良好的补丁流程中,
性能优化 (
hpi_force_inline)- 这个函数非常短小,就是一个简单的比较操作。
hpi_force_inline是一个给编译器的强烈建议(甚至在某些编译器上是强制指令),告诉它不要为这个函数生成常规的函数调用(这会涉及压栈、跳转等开销),而是直接将return (self->cache_end!=0);这行代码嵌入到调用它的地方。 - 对于这种在核心流程末尾的、极度简单的函数,内联可以消除微小的性能开销。
- 这个函数非常短小,就是一个简单的比较操作。
代码解析
1 | // 为内部函数 _hpi_cache_success_finish 创建一个简短的别名。 |
hpatch_lite_patch Hpatch Lite 主补丁应用逻辑
原理与设计思路解析
hpatch_lite_patch 函数实现了基于一系列“覆盖(cover)”操作的补丁算法。它从补丁流中顺序读取指令,每一条指令都描述了如何生成新文件的一部分数据。
核心概念:“覆盖(Cover)”与“新数据(New Data)”
Hpatch 算法将新文件看作是由两种数据组成的:
- 覆盖数据 (Cover Data): 这部分数据是通过将旧文件中的某一段数据,与补丁文件中的一小段“差分数据”相加(或直接复制)得到的。
- 新数据 (New Data): 这部分数据在旧文件中完全不存在,因此直接从补丁文件中复制而来。
整个补丁过程就是交替进行“复制新数据”和“生成覆盖数据”的过程,直到新文件被完整创建。
指令流与状态机
- 函数首先从补丁流中读取一个
coverCount,它代表了总共有多少个“覆盖”操作。 - 随后,函数进入一个大循环,每次循环处理一个“覆盖”操作。
- 为了跟踪进度,函数维护了两个关键的状态变量(可以看作是“光标”):
newPosBack: 当前在新文件上已经写入到的位置。oldPosBack: 上一个覆盖操作在旧文件上读取结束的位置。
- 函数首先从补丁流中读取一个
关键优化:差量编码 (Delta Encoding)
- 为了让补丁文件尽可能小,
cover_newPos和cover_oldPos并不是以绝对位置存储的,而是使用了差量编码。cover_newPos: 存储的是相对于上一个写入点newPosBack的增量。因为新文件总是向前生成的,所以cover_newPos总是大于等于newPosBack。cover_oldPos: 存储的是相对于上一个读取点oldPosBack的偏移量,并且一个标志位决定了这个偏移是向前(+)还是向后(-)。这使得算法可以灵活地引用旧文件中的任何数据。
- 这些经过差量编码后的小数值,可以被
_cache_unpackUInt函数(即 VLQ/LEB128 编码)用非常少的字节来表示,这是 Hpatch 格式高效的关键。
- 为了让补丁文件尽可能小,
内存使用 (
temp_cache)- 调用者传入一块内存
temp_cache,函数将其一分为二。 - 后半部分用作
_TInputCache的缓冲区,用于高效地从补丁数据流中读取指令和数据。 - 前半部分则在
_patch_add_old_withClip函数中被用作临时工作区,例如,用于存放从旧文件中读取的数据块,以便和差分数据进行运算。
- 调用者传入一块内存
代码解析
1 | /** |
hpatchi_inplace_open Hpatch In-place 头部数据解析
原理与设计思路解析
hpatchi_inplace_open 函数是 In-place 更新模式的入口,其核心职责与 hpatch_lite_open 类似:验证并解析补丁文件的头部。但它专门用于解析为 In-place 模式设计的、包含额外信息的头部。
- 文件头结构 (In-place Header Format)
In-place 模式的头部是lite模式头部的扩展,增加了一个关键字段。其头部大小hpi_kInplaceHeadSize通常为 5 字节。
| 字节偏移 | 字段名称 | 长度 (字节) | 描述 |
|---|---|---|---|
| 0 | Magic Byte 1 | 1 | 固定为 'h',用于文件类型识别。 |
| 1 | Magic Byte 2 | 1 | 固定为 'I',与'h'共同组成 “hI” 魔法标识。 |
| 2 | compress_type |
1 | 压缩类型。标识补丁数据使用了哪种压缩算法(如 zlib, bzip2, lzma 等)。 |
| 3 | Packed Info | 1 | 一个被打包的字节,通过位运算存储了3种不同的信息(同 lite 模式)。 |
| 4 | extraSafeSize len |
1 | 新增字节:表示 extraSafeSize 字段本身占用了多少个字节。 |
Packed Info 字节 (第3字节) 的详细解析 (与
lite模式相同):- 高2位 (Bits 6-7):
(byte >> 6)- 版本号 (Version Code): 值为
kHPatchLite_inplaceCode(2),用于与lite模式区分。
- 版本号 (Version Code): 值为
- 中3位 (Bits 3-5):
((byte >> 3) & 7)uncompressSize的字节数: 存储该字段需要读取的字节数。
- 低3位 (Bits 0-2):
(byte & 7)newSize的字节数: 存储该字段需要读取的字节数。
- 高2位 (Bits 6-7):
新增的核心字段:
extraSafeSize- 这是 In-place 模式与
lite模式最根本的区别。 extraSafeSize是由补丁生成工具 (hpatch_diff) 在分析完整个差分过程后计算出的一个安全值。它代表了在整个原地更新过程中,为了避免“读写冲突”(即写入新数据的位置覆盖了稍后需要读取的旧数据),必须设置的最小内存缓冲区大小。- 解析流程与
newSize类似,也是一个两步过程:- 先从第4个字节读取
extraSafeSize这个数值本身占用了多少字节(长度)。 - 再根据这个长度,从流中读取相应字节数的数据,并用
_hpi_readSize将其转换为一个整数。
- 先从第4个字节读取
- 这是 In-place 模式与
数据读取流程
- 读取固定头部: 首先,函数读取固定长度(
hpi_kInplaceHeadSize,即5字节)的头部数据。 - 校验与解析:
- 校验文件魔法标识
'hI'和 In-place 版本号,确保文件类型正确。 - 直接读取压缩类型。
- 通过位运算从第3字节中解包出
newSize和uncompressSize这两个字段的长度。 - 直接从第4字节读取
extraSafeSize字段的长度。
- 校验文件魔法标识
- 读取可变长数据: 根据上一步解析出的三个长度值,依次从数据流中分别读取
newSize、uncompressSize和extraSafeSize的实际数据。 - 字节到整数的转换: 调用
_hpi_readSize将读取到的字节数据转换为整数,并填充到对应的输出参数中。
- 读取固定头部: 首先,函数读取固定长度(
代码解析
1 | /** |
Hpatch In-place I/O Interception and Buffering
- 这部分代码是实现“写延迟”以规避读写冲突的关键,它通过一个精巧的环形缓冲区(Ring Buffer)和可选的页缓冲机制来完成。
原理与设计思路解析
这组函数共同构成了一个I/O装饰器或中间件 (hpatchi_listener_extra_t)。它的核心任务是拦截所有对新文件的写操作,将它们暂存到一个安全的内存缓冲区中,直到可以安全地将这些数据写入物理文件时,再执行实际的I/O。
核心机制:环形缓冲区 (Ring Buffer)
_hpatchi_listener_extra_write_new和_hpatchi_listener_extra_out共同实现了一个经典的先进先出 (FIFO) 环形缓冲区。这块缓冲区就是write_extra内存,大小为kWriteExtraSize。cached_data_pos是缓冲区的读指针(下一个要被写出到文件的字节位置)。cached_data_pos + cached_data_len(需要考虑回绕)是缓冲区的写指针(下一个新数据要写入的位置)。_hpatchi_listener_extra_write_new(生产者): 当hpatch_lite_patch引擎需要写入新数据时,此函数被调用。它将数据添加到环形缓冲区的末尾(写指针处)。如果添加新数据会导致缓冲区溢出,它会先调用_hpatchi_listener_extra_out来清空缓冲区最前端的旧数据,以腾出空间。_hpatchi_listener_extra_out(消费者): 当需要从缓冲区刷写数据到文件时,此函数被调用。它从环形缓冲区的头部(读指针处)读取数据,并通过底层的_wrap_listener将其真正写入文件。
环形缓冲区工作示意图:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16write_extra buffer:
+-------------------------------------------------------------+
| |##########| (cached_data_len) | |
+-------------------------------------------------------------+
^ ^
| |
cached_data_pos 缓存数据结束
(从这里读取) (在这里写入)
// 当pos+len超过末尾时,会回绕到开头
+-------------------------------------------------------------+
|#####| (len) | |##########| (len) |
+-------------------------------------------------------------+
^ ^
| |
End of cached data cached_data_pos可选的二级缓冲:页缓冲 (
_IS_WTITE_NEW_BY_PAGE)- 目的: 许多存储介质(特别是 NAND Flash)以“页 (Page)”为单位进行写入时效率最高。频繁的小数据量写入会导致性能下降和“写放大”问题。这个可选特性就是为了优化这种情况。
- 机制: 当这个特性被启用时,
_hpatchi_listener_extra_out的行为会改变。它不再直接将数据写入文件,而是将数据传递给第二个缓冲层——_hpatchi_listener_write_page。 _hpatchi_listener_write_page会将接收到的数据积累在一个专门的页缓冲区里 (write_extra + kWriteExtraSize)。只有当这个页缓冲区被写满时,_hpatchi_listener_page_out才会被调用,将一整个满页的数据一次性写入物理文件。- 这形成了一个两级缓冲流水线:
hpatch引擎 ->extraSafeSize环形缓冲 ->pageSize页缓冲 -> 物理文件。
收尾工作 (
_hpatchi_listener_extra_flush)- 在整个补丁过程结束后,
extraSafeSize环形缓冲和pageSize页缓冲中几乎肯定还留有未被写出的数据。 _hpatchi_listener_extra_flush的职责就是在最后时刻被调用,确保这两个缓冲区中的所有剩余数据都被强制刷写到物理文件中,保证新文件的完整性。
- 在整个补丁过程结束后,
代码解析
1 |
|
hpatchi_inplaceB Hpatch In-place 补丁协调器
原理与设计思路解析
hpatchi_inplaceB 的核心职责不是执行补丁算法本身,而是创建一个特殊的环境,然后在这个环境中调用通用的 hpatch_lite_patch 函数来完成实际工作。这个“特殊环境”的关键就是我们之前讨论过的 hpatchi_listener_extra_t 包装器。
核心策略:内存分区与I/O重定向
这个函数最精妙的设计在于它对调用者提供的单一内存块temp_cache的巧妙划分和使用。它将这块内存“一分为二”:- 前端部分 (Extra Buffers): 专门用于 In-place 模式的特殊需求,即
extraSafeSize安全缓冲和可选的pageSize页缓冲。 - 后端部分 (Standard Cache): 剩下的内存则被原封不动地传递给
hpatch_lite_patch,供其内部用于标准的差分流读取缓存 (_TInputCache) 和数据运算。
内存布局示意图:
1
2
3
4
5
6
7
8
9
10
11<------------------------------- temp_cache_size --------------------------------->
+--------------------------------+-----------------------------------------------+
| 前端 (Extra) | 后端 (Standard) |
+================================+===============================================+
| extraSafeSize | (opt) pageSize | 传递给 hpatch_lite_patch 供其自身使用 |
| (write_extra | (page_buf for | (例如,不同的流读取缓存,临时工作区)|
| ring buffer) | page writes) | |
+--------------------------------+-----------------------------------------------+
^ ^
| |
temp_cache temp_cache + needExtra- 前端部分 (Extra Buffers): 专门用于 In-place 模式的特殊需求,即
设计模式:装饰器与逻辑复用
该函数完美体现了“装饰器模式”和代码复用思想。- 它创建并初始化
extra_listener,这个extra_listener装饰 (或包装) 了原始的listener。 - 然后,它复用了完全通用的
hpatch_lite_patch算法。补丁引擎本身无需关心是否在进行 In-place 更新,它只管通过listener接口读写数据。 - 由于传递进去的是被装饰过的
extra_listener,所有write_new操作都被透明地拦截并路由到我们的“写延迟”环形缓冲逻辑中,从而实现了原地更新的安全性。
- 它创建并初始化
编译时配置 (
_IS_WTITE_NEW_BY_PAGE)
代码使用了C预处理器#if来生成两个版本的函数:hpatchi_inplaceB_by_page: 当需要按页对齐写入时编译,接收一个pageSize参数。hpatchi_inplaceB: 标准版本,内部将pageSize视为0。
这种方式使得核心逻辑可以共享,同时为特定需求(如操作Flash存储)提供专门的接口。
一个精巧的健壮性设计
代码中有一行extraSafeSize=(extraSafeSize>=cache_size)?extraSafeSize:cache_size;。这是一个非常巧妙的健壮性保证。它确保了extraSafeSize安全缓冲区的大小至少不小于后面hpatch_lite_patch将要使用的差分流读取缓存 (cache_size) 的大小。这可以防止因extraSafeSize过小而hpatch_lite_patch的读缓存很大时,可能引发的性能问题或逻辑冲突,保证了系统的稳定运行。
代码解析
1 |
|









