[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 |
|