当前位置: 首页 > news >正文

[hpatch]差分算法学习笔记 -- lite解压

文章目录

  • [hpatch]差分算法学习笔记 -- lite解压
  • HPatchLite\HDiffPatch\libHDiffPatch\HPatchLite\hpatch_lite.c
    • Hptach 算法
      • **1. Hptach 的核心:加法差分 (Additive Diff)**
      • **2. 其他类型的差分编码**
        • **a. 复制/指令式差分 (Copy/Instruction-based Diff)**
        • **b. 异或差分 (XOR Diff)**
      • **总结对比**
    • hpatch_lite_open **Hpatch Lite 头部数据解析**
      • **原理与设计思路解析**
      • **代码解析**
    • _cache_unpackUInt **可变长度整数解码**
      • **原理与设计思路解析**
      • **代码解析**
    • `_patch_copy_diff` 和 `_patch_add_old_withClip` **将上一层函数解析出的指令转化为对文件的具体读写操作**
      • **原理与设计思路解析**
      • **代码解析**
    • _hpi_cache_success_finish **Hpatch Lite 源码解析:流结束状态校验**
      • **原理与设计思路解析**
      • **代码解析**
    • hpatch_lite_patch **Hpatch Lite 主补丁应用逻辑**
      • **原理与设计思路解析**
      • **代码解析**
    • hpatchi_inplace_open **Hpatch In-place 头部数据解析**
      • **原理与设计思路解析**
      • **代码解析**
    • **Hpatch In-place I/O Interception and Buffering**
      • **原理与设计思路解析**
      • **代码解析**
    • hpatchi_inplaceB **Hpatch In-place 补丁协调器**
      • **原理与设计思路解析**
      • **代码解析**

[hpatch]差分算法学习笔记 – lite解压

在这里插入图片描述

https://github.com/wdfk-prog/hptach_study

HPatchLite\HDiffPatch\libHDiffPatch\HPatchLite\hpatch_lite.c

Hptach 算法

1. Hptach 的核心:加法差分 (Additive Diff)

这种方法可以被称为加法差分增量差分

  • 核心公式: NewData = OldData + DiffData
  • 反向公式: DiffData = NewData - OldData (在生成补丁时计算)

工作流程图:

                  +----------------------+
生成补丁时 (hpatch_diff) |                      ||   新文件 (New File)  |+----------------------+| (减法)v+----------------------+|   旧文件 (Old File)  |+----------------------+|v
+-------------------------------------------------------------------------+
|                         差分数据 (DiffData)                             |
| (大部分是0或接近0的小数值,代表“无变化”或“微小变化”,非常适合压缩) |
+-------------------------------------------------------------------------++----------------------+
应用补丁时 (hpatch_patch) |                      ||   旧文件 (Old File)  |+----------------------+| (加法)v+----------------------+|  差分数据 (DiffData) |+----------------------+|v
+-------------------------------------------------------------------------+
|                           新文件 (New File)                             |
|                           (被成功还原)                                  |
+-------------------------------------------------------------------------+

为什么这种方法很有效?

因为在多数文件更新中(无论是程序、文档还是图片),大部分内容是不变的,或者只有微小的改动

  • NewData[i] == OldData[i] 时,DiffData[i] 就是 0
  • NewData[i]OldData[i] 非常接近时,DiffData[i] 就是一个很小的正数或负数

这导致 DiffData 中充满了大量的 0 和小数值,这种数据的非常低,因此可以被像 zlib、LZMA 这样的通用压缩算法极大地压缩。Hptach 正是利用了“加法差分”与“通用压缩”的组合拳来获得极高的压缩率。


2. 其他类型的差分编码

“差分编码”是一个广义的概念,其核心思想是“不记录数据本身,而是记录数据与某个参考(基准)之间的差异”。加法只是实现这种思想的一种方式。

以下是其他几种常见的差分编码方式:

a. 复制/指令式差分 (Copy/Instruction-based Diff)

这是最著名的一种,以 rsyncbsdiff 算法为代表。它的核心不是字节运算,而是生成一系列指令

  • 核心指令:

    1. COPY(old_pos, length): 从旧文件的 old_pos 位置复制 length 长度的数据到新文件。
    2. 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字节的头部中编码了多种信息,其结构如下:
字节偏移字段名称长度 (字节)描述
0Magic Byte 11固定为 'h',用于文件类型识别。
1Magic Byte 21固定为 'I',与'h'共同组成 “hI” 魔法标识。
2compress_type1压缩类型。标识补丁数据使用了哪种压缩算法(如 zlib, bzip2, lzma 等)。
3Packed Info1一个被打包的字节,通过位运算存储了3种不同的信息。
  • Packed Info 字节 (第3字节) 的详细解析:
    这个字节被巧妙地划分成三个部分:

    • 高2位 (Bits 6-7): (byte >> 6)
      • 版本号 (Version Code): 用于标识 Hpatch Lite 格式的版本,确保兼容性。代码会检查它是否等于预定义的 kHPatchLite_versionCode
    • 中3位 (Bits 3-5): ((byte >> 3) & 7)
      • uncompressSize 的字节数: 表示解压后(或原始)数据的大小值本身占用了多少个字节(0-7字节)。
    • 低3位 (Bits 0-2): (byte & 7)
      • newSize 的字节数: 表示应用补丁后新文件的大小值本身占用了多少个字节(0-7字节)。
  • 数据读取流程

    1. 读取固定头部: 首先,函数读取固定长度(hpi_kHeadSize)的头部数据。
    2. 校验与解析:
      • 校验文件魔法标识 'hI' 和版本号,如果不对,则说明不是一个有效的或兼容的 Hpatch Lite 文件,解析失败。
      • 直接读取压缩类型。
      • 通过位运算从 Packed Info 字节中解包newSizeuncompressSize 这两个字段的长度
    3. 读取可变长数据: 根据上一步解析出的长度,函数接着从数据流中分别读取 newSizeuncompressSize 的实际数据。例如,如果解析出 newSize 的长度是4字节,它就会再读取4个字节。
    4. 字节到整数的转换 (_hpi_readSize):
      • 由于 newSizeuncompressSize 是以多个字节的形式存储的,需要一个辅助函数 _hpi_readSize 将这些字节转换成一个整数。
      • 这个函数的实现方式 v=(v<<8)|(hpi_pos_t)buf[len] 并且 len 从高到低递减,表明它将字节数组视为 小端序 (Little-Endian) 来解析。也就是说,缓冲区中的第一个字节 buf[0] 是整数的最低位字节 (LSB)。
  • 设计优点

    • 紧凑: 将多个元数据打包进一个字节,节省了头部空间。
    • 灵活: newSizeuncompressSize 的长度是可变的,对于较小的文件,可以用1或2个字节来表示其大小,从而节省空间;对于非常大的文件,最多可以使用8个字节(64位)来表示,扩展性强。

代码解析

/*** @brief 从字节缓冲区中读取一个小端序存储的整数。* @param buf 存放整数数据的缓冲区。* @param len 整数占用的字节数。* @return hpi_pos_t 返回转换后的整数值。*/
static hpi_pos_t _hpi_readSize(const hpi_byte* buf,hpi_size_t len){hpi_pos_t v=0; // 初始化结果为0。// 从 len-1 到 0 循环,即从缓冲区的末尾向前读取。while(len--){// 核心逻辑:// 1. v << 8: 将当前结果左移8位,为下一个字节腾出空间。// 2. | (hpi_pos_t)buf[len]: 将当前字节(buf[len])放到 v 的低8位上。// 因为 len 是从高到低变化的,所以 buf[len-1]...buf[0] 依次成为整数的 MSB...LSB。// 举例:buf=[0x12, 0x34], len=2.// 第一次循环 (len=1): v = (0<<8) | buf[1] = 0x34// 第二次循环 (len=0): v = (0x34<<8) | buf[0] = 0x3400 | 0x12 = 0x3412v=(v<<8)|(hpi_pos_t)buf[len];}return v;
}/*** @brief 打开并解析一个 Hpatch Lite 格式的补丁流,读取其头部信息。* @param diff_data         补丁数据流的句柄。* @param read_diff         用于从流中读取数据的函数指针。* @param out_compress_type 输出参数,用于返回解析出的压缩类型。* @param out_newSize       输出参数,用于返回补丁应用后的新文件大小。* @param out_uncompressSize 输出参数,用于返回解压缩后数据的大小(如果适用)。* @return hpi_BOOL         成功返回 hpi_TRUE,失败返回 hpi_FALSE。*/
hpi_BOOL hpatch_lite_open(hpi_TInputStreamHandle diff_data,hpi_TInputStream_read read_diff,hpi_compressType* out_compress_type,hpi_pos_t* out_newSize,hpi_pos_t* out_uncompressSize){// 定义版本号常量。#define kHPatchLite_versionCode     1// lenn 和 lenu 用于临时存储长度信息。hpi_size_t lenn=hpi_kHeadSize; // 期望读取的头部长度。hpi_size_t lenu;// 定义一个缓冲区。其大小取 hpi_kHeadSize 和 sizeof(hpi_pos_t) 中的较大者,// 以确保该缓冲区既能装下固定头部,也能装下后续读取的变长整数。hpi_byte   buf[(hpi_kHeadSize>sizeof(hpi_pos_t))?hpi_kHeadSize:sizeof(hpi_pos_t)];// 通过函数指针 read_diff 读取固定长度的头部到 buf 中。_CHECK 是一个错误检查宏。_CHECK(read_diff(diff_data,buf,&lenn));// --- 开始校验和解析头部 ---// buf[3] 是包含多种信息的 "Packed Info" 字节。lenu=buf[3];// _SAFE_CHECK 是一个安全校验宏,用于验证头部数据的合法性。// 1. lenn==hpi_kHeadSize: 确认是否成功读取了预期长度的头部。// 2. (buf[0]=='h')&(buf[1]=='I'): 校验文件魔法标识是否为 "hI"。// 3. ((lenu>>6)==kHPatchLite_versionCode): 将 buf[3] 右移6位取出高2位,校验版本号。_SAFE_CHECK((lenn==hpi_kHeadSize)&(buf[0]=='h')&(buf[1]=='I')&((lenu>>6)==kHPatchLite_versionCode));// buf[2] 直接存储了压缩类型。*out_compress_type=buf[2];// 从 "Packed Info" 字节中解包长度信息。// (lenu & 7): 取 buf[3] 的低3位,得到 newSize 字段的字节数。lenn=lenu&7; // ((lenu >> 3) & 7): 将 buf[3] 右移3位再取低3位,得到 uncompressSize 字段的字节数。lenu=(lenu>>3)&7;// 校验解析出的长度是否合法(不能超过 hpi_pos_t 类型的最大字节数)。_SAFE_CHECK((lenn<=sizeof(hpi_pos_t))&(lenu<=sizeof(hpi_pos_t)));// --- 读取可变长度的元数据 ---// 根据刚刚解析出的长度 lenn,读取 newSize 的数据。_CHECK(read_diff(diff_data,buf,&lenn));// 调用 _hpi_readSize 将读取到的字节数据转换为整数,并存入输出参数。*out_newSize=_hpi_readSize(buf,lenn);// 根据刚刚解析出的长度 lenu,读取 uncompressSize 的数据。_CHECK(read_diff(diff_data,buf,&lenu));// 同样,转换为整数并存入输出参数。*out_uncompressSize=_hpi_readSize(buf,lenu);// 所有操作成功,返回真。return hpi_TRUE;
}

_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_begincache_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 (初始调用时)。

      +---------------------------------+
      |           开始解码              |
      |      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)            |
      +---------------------------------+
      

代码解析

// 定义一个名为 _hpi_TInputCache 的结构体,用于管理带缓冲的输入流。
// 这个结构体的核心思想是:一次从底层输入流(如文件、网络)读取一大块数据到内存缓冲区(cache_buf),
// 然后上层调用者每次只从这个缓冲区中取数据,从而减少昂贵的I/O操作次数。
typedef struct _hpi_TInputCache{hpi_size_t      cache_begin; // 缓冲区域中,当前未读数据的起始位置(索引)。hpi_size_t      cache_end;   // 缓冲区域中,已加载数据的结束位置(索引),即有效数据末尾+1。hpi_byte*       cache_buf;   // 指向实际存储数据的内存缓冲区的指针。hpi_TInputStreamHandle  inputStream; // 底层输入流的句柄(例如文件指针或socket描述符)。hpi_TInputStream_read   read_code;   // 一个函数指针,指向真正从底层输入流(inputStream)读取数据的函数。
} _hpi_TInputCache;// --- 类型和函数宏定义,用于简化代码 ---
#define _TInputCache            _hpi_TInputCache          // 为结构体类型创建别名。
#define _cache_read_1byte       _hpi_cache_read_1byte     // 为函数创建别名。
#define _cache_update           _hpi_cache_update         // 为函数创建别名。
#define _cache_success_finish   _hpi_cache_success_finish // 为函数创建别名 (此片段中未使用)。/*** @brief 更新(填充)缓冲区。* @param self 指向 _TInputCache 实例的指针。* @return hpi_BOOL 类型,如果成功读取到新数据则返回真 (非0),否则返回假 (0)。** 当缓冲区中的数据被读完时 (cache_begin == cache_end),此函数被调用。* 它会调用 read_code 函数指针,从底层输入流读取新数据,并填充到 cache_buf 的开头。*/
hpi_BOOL _cache_update(struct _TInputCache* self){hpi_size_t len=self->cache_end;// 断言:确保调用此函数时,缓冲区确实是空的。这是一个防御性编程措施。assert(len==self->cache_begin); // 调用函数指针 read_code 从底层流读取数据。// 参数: self->inputStream 是流句柄,self->cache_buf 是目标缓冲区,&len 是期望读取的长度(同时也是返回实际读取的长度)。// 如果读取失败,read_code 返回假,此时将 len 设为0。if (!self->read_code(self->inputStream,self->cache_buf,&len))len=0;// 重置缓冲区的起止位置。self->cache_begin=0;     // 新数据从缓冲区的开头开始。self->cache_end=len;     // 数据的结束位置是实际读取到的长度。// 如果 len 不为0,说明成功读取到了数据,返回真。return len!=0;
}/*** @brief 从缓冲区中读取一个字节。* @param self 指向 _TInputCache 实例的指针。* @return hpi_fast_uint8 类型,返回读取到的字节。如果已到流末尾且无法再读取,则返回 0。** 这是上层代码获取数据的基本接口。*/
hpi_fast_uint8 _cache_read_1byte(struct _TInputCache* self){// 首先检查缓冲区中是否还有未读数据。if (self->cache_begin != self->cache_end){
__cache_read_1byte: // goto 标签// 缓冲区非空,直接返回当前字节,并将起始位置后移一位。return self->cache_buf[self->cache_begin++];}// 如果缓冲区已空,尝试调用 _cache_update 填充数据。if(_cache_update(self))// 如果填充成功,使用 goto 跳转到__cache_read_1byte标签处,重新执行读取操作。// 这里使用 goto 是为了避免函数递归调用或重复写读取逻辑,是一种在C语言底层代码中常见的性能优化技巧。goto __cache_read_1byte;else// 如果填充失败(意味着流已结束),返回 0。return 0;
}/*** @brief 从流中解包一个可变长度编码的无符号整数 (VLQ/LEB128 类似格式)。* @param self 指向 _TInputCache 实例的指针。* @param v 初始值,通常是第一个字节解码后的值。* @param isNext 一个标志,表示是否还有后续字节需要读取。* @return hpi_pos_t 类型,返回完整解码后的整数。** 这种编码方式可以用更少的字节表示小的数字,从而节省空间。*/
static hpi_pos_t _cache_unpackUInt(_TInputCache* self,hpi_pos_t v,hpi_fast_uint8 isNext){// 循环条件 isNext 由每个字节的最高位决定。while (isNext){// 读取下一个字节。hpi_fast_uint8 b=_cache_read_1byte(self);// 核心解码逻辑:// 1. (b & 127): 取出当前字节的低7位作为数据。// 2. (v << 7): 将之前已经解码出的值向左移动7位,为新的7位数据腾出空间。// 3. |: 将新的7位数据合并到结果中。v=(v<<7)|(b&127);// 更新循环条件:// b >> 7: 取出当前字节的最高位 (MSB)。如果MSB是1,表示后面还有后续字节,isNext为1,循环继续;// 如果MSB是0,表示这是最后一个字节,isNext为0,循环结束。isNext=b>>7;}return v;
}

_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 算法的一个关键实现:
      1. 差分合成模式 (isNotNeedSubDifffalse): 这是标准的差分补丁逻辑。它执行 NewData = OldData + DiffData 的操作。数据流是:从旧文件读一块数据到 temp_cache -> 从 diff 文件读一块差分数据 -> 调用 addData 将差分数据temp_cache 中 -> 将 temp_cache 的结果写入新文件。
      2. 旧数据复制模式 (isNotNeedSubDifftrue): 在某些情况下,新文件的一部分可能与旧文件的某部分完全相同。此时,就不需要在补丁文件中存储差分数据(因为差分都是0),只需记录一个“从旧文件某处复制一段数据过来”的指令即可。此时,这个函数的数据流简化为:从旧文件读一块数据到 temp_cache -> 直接将 temp_cache 的内容写入新文件。
    • temp_cache 的作用: 在这里,temp_cache 扮演了一个至关重要的“工作台”角色。所有来自旧文件的数据和来自补丁文件的差分数据,都会在这个临时的内存区域中进行合并运算,然后再被写入最终目标。
    • addData 函数: 这是一个被强制内联 (hpi_force_inline) 的辅助函数,因为它执行的是最频繁、最底层的字节加法操作。*dst++ += *src++ 是一个非常高效的C语言惯用法,编译器能将其优化为非常快速的机器指令。

总而言之,hpatch_lite_patch 负责“决策”(解析指令),而这两个函数负责“执行”(搬运和加工数据)。它们的设计共同保证了补丁过程的高效性和灵活性。

代码解析

/*** @brief 从 diff 缓存中直接复制一段数据到新文件流。* @param out_newData 目标新文件流的监听器。* @param diff        源 diff 数据流的输入缓存。* @param copyLength  需要复制的总字节数。* @return hpi_BOOL   成功返回 hpi_TRUE。*/
static hpi_BOOL _patch_copy_diff(hpatchi_listener_t* out_newData,_TInputCache* diff,hpi_pos_t copyLength){// 只要还有数据需要复制,就继续循环。while (copyLength>0){// 计算当前 diff 缓存中还有多少可用的数据。hpi_size_t decodeStep=diff->cache_end-diff->cache_begin;// 如果缓存为空...if (decodeStep==0){_CHECK(_cache_update(diff)); // 填充缓存。decodeStep=diff->cache_end-diff->cache_begin; // 重新计算可用数据量。}// 本次循环处理的数据量不能超过剩余需要复制的总量。if (decodeStep>copyLength)decodeStep=(hpi_size_t)copyLength;// 调用回调函数,将缓存中的数据块写入新文件。_CHECK(out_newData->write_new(out_newData,diff->cache_buf+diff->cache_begin,decodeStep));// 更新缓存的读取位置和剩余需要复制的长度。diff->cache_begin+=decodeStep;copyLength-=(hpi_pos_t)decodeStep;}return hpi_TRUE;
}/*** @brief 一个强制内联的辅助函数,用于将两个字节数组按位相加。* @param dst 目标数组,也是被修改的数组。* @param src 源数组。* @param length 数组长度。*/
static hpi_force_inline void addData(hpi_byte* dst,const hpi_byte* src,hpi_size_t length){while (length--) { *dst++ += *src++; } 
}/*** @brief 从旧文件中读取数据,根据需要与 diff 数据相加,然后写入新文件。* @param old_and_new      包含读旧文件和写新文件回调的监听器。* @param diff             diff 数据流的输入缓存。* @param isNotNeedSubDiff 标志位,如果为真,则直接复制旧文件数据;否则,执行加法。* @param oldPos           在旧文件中读取的起始位置。* @param addLength        需要处理的总数据长度。* @param temp_cache       用于暂存和运算的临时缓冲区。* @return hpi_BOOL        成功返回 hpi_TRUE。*/
static hpi_BOOL _patch_add_old_withClip(hpatchi_listener_t* old_and_new,_TInputCache* diff,hpi_BOOL isNotNeedSubDiff,hpi_size_t oldPos,hpi_size_t addLength,hpi_byte* temp_cache){// 只要还有数据需要处理,就继续循环。while (addLength>0){hpi_size_t decodeStep;// 根据是否需要差分数据,来确定本次处理块的大小。if (!isNotNeedSubDiff){// 需要差分:块大小由 diff 缓存的可用数据量决定。decodeStep=diff->cache_end-diff->cache_begin;if (decodeStep==0){ // 如果 diff 缓存为空,则填充。_cache_update(diff);decodeStep=diff->cache_end-diff->cache_begin;}}else{// 无需差分:块大小可以任意定,这里巧妙地复用了 diff 缓存的总大小作为处理块的大小。decodeStep=diff->cache_end;}_CHECK(decodeStep>0); // 确保处理块大小有效。// 本次处理的块大小不能超过剩余需要的总长度。if (decodeStep>addLength)decodeStep=(hpi_size_t)addLength;// 核心操作步骤:// 1. 从旧文件读取数据块到 "工作台" temp_cache。_CHECK(old_and_new->read_old(old_and_new,oldPos,temp_cache,(hpi_pos_t)decodeStep));// 2. 如果需要差分运算...if (!isNotNeedSubDiff){// 将 diff 缓存中的数据加到 temp_cache 中。addData(temp_cache,diff->cache_buf+diff->cache_begin,decodeStep);// 消耗掉已使用的 diff 缓存数据。diff->cache_begin+=decodeStep;}// 3. 将 "工作台" temp_cache 中的结果数据写入新文件。_CHECK(old_and_new->write_new(old_and_new,temp_cache,decodeStep));// 更新旧文件的读取位置和剩余处理长度。oldPos+=decodeStep;addLength-=decodeStep;}return hpi_TRUE;
}

_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); 这行代码嵌入到调用它的地方。
    • 对于这种在核心流程末尾的、极度简单的函数,内联可以消除微小的性能开销。

代码解析

// 为内部函数 _hpi_cache_success_finish 创建一个简短的别名。
// 这是一种常见的编码风格,用于隐藏实现细节并提供一个更清晰的API。
#define _cache_success_finish   _hpi_cache_success_finish/*** @brief 对输入缓存的状态进行最终的健全性检查。* @param self 指向 _TInputCache 实例的常量指针。* @return hpi_BOOL 如果缓存状态被认为是成功的,则返回 hpi_TRUE。* * `static` 关键字意味着此函数仅在当前文件内可见。* `hpi_force_inline` 是一个性能优化指令,建议编译器将此函数内联。*/
static hpi_force_inline 
hpi_BOOL _hpi_cache_success_finish(const _hpi_TInputCache* self){ // 返回一个布尔值,判断 cache_end 是否不为0。// cache_end 只有在尝试从一个已结束或出错的流中读取数据时才会被置为0。// 因此,这个检查确认了流在整个补丁过程中没有意外枯竭。return (self->cache_end!=0); 
}

hpatch_lite_patch Hpatch Lite 主补丁应用逻辑

原理与设计思路解析

hpatch_lite_patch 函数实现了基于一系列“覆盖(cover)”操作的补丁算法。它从补丁流中顺序读取指令,每一条指令都描述了如何生成新文件的一部分数据。

  • 核心概念:“覆盖(Cover)”与“新数据(New Data)”

    • Hpatch 算法将新文件看作是由两种数据组成的:

      1. 覆盖数据 (Cover Data): 这部分数据是通过将旧文件中的某一段数据,与补丁文件中的一小段“差分数据”相加(或直接复制)得到的。
      2. 新数据 (New Data): 这部分数据在旧文件中完全不存在,因此直接从补丁文件中复制而来。
    • 整个补丁过程就是交替进行“复制新数据”和“生成覆盖数据”的过程,直到新文件被完整创建。

  • 指令流与状态机

    • 函数首先从补丁流中读取一个 coverCount,它代表了总共有多少个“覆盖”操作。
    • 随后,函数进入一个大循环,每次循环处理一个“覆盖”操作。
    • 为了跟踪进度,函数维护了两个关键的状态变量(可以看作是“光标”):
      • newPosBack: 当前在新文件上已经写入到的位置。
      • oldPosBack: 上一个覆盖操作在旧文件上读取结束的位置。
  • 关键优化:差量编码 (Delta Encoding)

    • 为了让补丁文件尽可能小,cover_newPoscover_oldPos不是以绝对位置存储的,而是使用了差量编码
      • cover_newPos: 存储的是相对于上一个写入点 newPosBack 的增量。因为新文件总是向前生成的,所以 cover_newPos 总是大于等于 newPosBack
      • cover_oldPos: 存储的是相对于上一个读取点 oldPosBack 的偏移量,并且一个标志位决定了这个偏移是向前(+)还是向后(-)。这使得算法可以灵活地引用旧文件中的任何数据。
    • 这些经过差量编码后的小数值,可以被 _cache_unpackUInt 函数(即 VLQ/LEB128 编码)用非常少的字节来表示,这是 Hpatch 格式高效的关键。
  • 内存使用 (temp_cache)

    • 调用者传入一块内存 temp_cache,函数将其一分为二。
    • 后半部分用作 _TInputCache 的缓冲区,用于高效地从补丁数据流中读取指令和数据。
    • 前半部分则在 _patch_add_old_withClip 函数中被用作临时工作区,例如,用于存放从旧文件中读取的数据块,以便和差分数据进行运算。

代码解析

/*** @brief 执行 Hpatch Lite 的核心补丁逻辑。* @param listener        一个包含所有I/O流句柄和回调函数的监听器结构体。* @param newSize         期望生成的新文件的总大小(用于最后校验)。* @param temp_cache      用于内部操作的临时内存缓冲区。* @param temp_cache_size 临时缓冲区的大小。* @return hpi_BOOL       成功返回 hpi_TRUE,失败返回 hpi_FALSE。*/
hpi_BOOL hpatch_lite_patch(hpatchi_listener_t* listener,hpi_pos_t newSize,hpi_byte* temp_cache,hpi_size_t temp_cache_size){_TInputCache  diff;       // 创建一个输入缓存来读取 diff 文件。hpi_pos_t  coverCount;    // 补丁中包含的“覆盖”操作的总数。hpi_pos_t  newPosBack=0;  // “光标”,记录当前在新文件中的写入位置。hpi_pos_t  oldPosBack=0;  // “光标”,记录上一次在旧文件中的读取位置。{// --- 初始化输入缓存 ---assert(temp_cache); // 确保临时缓存有效。_SAFE_CHECK(temp_cache_size>=hpi_kMinCacheSize); // 确保缓存大小满足最低要求。temp_cache_size>>=1; // 将缓存大小除以2。diff.cache_begin=temp_cache_size; // 缓存的起始和结束都指向中间,表示缓存为空。diff.cache_end=temp_cache_size;diff.cache_buf=temp_cache+temp_cache_size; // 使用传入缓存的后半部分作为 diff 读取缓冲区。diff.inputStream=listener->diff_data;    // 设置底层输入流为 diff 文件。diff.read_code=listener->read_diff;      // 设置读取函数。}// 从 diff 流中解包出“覆盖”操作的总数。coverCount=_cache_unpackUInt(&diff,0,hpi_TRUE);// 循环处理每一个“覆盖”操作。while (coverCount--){hpi_pos_t cover_oldPos;   // 本次覆盖在旧文件中的起始位置。hpi_pos_t cover_newPos;   // 本次覆盖在新文件中的起始位置。hpi_pos_t cover_length;   // 本次覆盖的数据长度。hpi_BOOL  isNotNeedSubDiff; // 一个标志,表示是直接复制旧数据,还是需要和差分数据相加。{// 从 diff 流中读取并解码一个覆盖操作的元数据cover_length=_cache_unpackUInt(&diff,0,hpi_TRUE);{// 读取一个8位的标签(tag)字节,它包含了多种打包信息。hpi_fast_uint8 tag=_cache_read_1byte(&diff);// 解码 cover_oldPos 的差量值。cover_oldPos=_cache_unpackUInt(&diff,tag&31,tag&(1<<5)); // 低5位是数值,第6位是连续标志。isNotNeedSubDiff=(tag>>7); // 最高位是 isNotNeedSubDiff 标志。// 根据第7位标志,决定 oldPos 是向前还是向后偏移。if (tag&(1<<6))cover_oldPos=oldPosBack-cover_oldPos; // 向后偏移elsecover_oldPos+=oldPosBack; // 向前偏移}// 解码 cover_newPos 的差量值。cover_newPos=_cache_unpackUInt(&diff,0,hpi_TRUE);cover_newPos+=newPosBack; // newPos 总是向前累加。}// 校验数据合法性,新位置不能在已写位置之前。_SAFE_CHECK(cover_newPos>=newPosBack);// 如果新位置和上次写入位置之间有“沟”,说明这段是新数据。if (newPosBack<cover_newPos)// 直接从 diff 流中复制这段新数据到输出。_CHECK(_patch_copy_diff(listener,&diff,cover_newPos-newPosBack));// 执行核心的“覆盖”操作:从旧文件读取、与差分数据运算、写入新文件。_CHECK(_patch_add_old_withClip(listener,&diff,isNotNeedSubDiff,cover_oldPos,cover_length,temp_cache));// --- 更新状态光标,为下一次循环做准备 ---newPosBack=cover_newPos+cover_length;oldPosBack=cover_oldPos+cover_length;// 校验:覆盖长度必须大于0,除非这是最后一个覆盖操作。_SAFE_CHECK((cover_length>0)|(coverCount==0));}// 最终校验:// 1. 生成的新文件大小是否和头部声明的一致。// 2. diff 流是否被完整、正确地读取完毕。/* #define _cache_success_finish   _hpi_cache_success_finishstatic hpi_force_inline hpi_BOOL _hpi_cache_success_finish(const _hpi_TInputCache* self){ return (self->cache_end!=0); }*/return (newSize==newPosBack)&_cache_success_finish(&diff);
}

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 字节
字节偏移字段名称长度 (字节)描述
0Magic Byte 11固定为 'h',用于文件类型识别。
1Magic Byte 21固定为 'I',与'h'共同组成 “hI” 魔法标识。
2compress_type1压缩类型。标识补丁数据使用了哪种压缩算法(如 zlib, bzip2, lzma 等)。
3Packed Info1一个被打包的字节,通过位运算存储了3种不同的信息(同 lite 模式)。
4extraSafeSize len1新增字节:表示 extraSafeSize 字段本身占用了多少个字节。
  • Packed Info 字节 (第3字节) 的详细解析 (与 lite 模式相同):

    • 高2位 (Bits 6-7): (byte >> 6)
      • 版本号 (Version Code): 值为 kHPatchLite_inplaceCode (2),用于与 lite 模式区分。
    • 中3位 (Bits 3-5): ((byte >> 3) & 7)
      • uncompressSize 的字节数: 存储该字段需要读取的字节数。
    • 低3位 (Bits 0-2): (byte & 7)
      • newSize 的字节数: 存储该字段需要读取的字节数。
  • 新增的核心字段:extraSafeSize

    • 这是 In-place 模式与 lite 模式最根本的区别。
    • extraSafeSize 是由补丁生成工具 (hpatch_diff) 在分析完整个差分过程后计算出的一个安全值。它代表了在整个原地更新过程中,为了避免“读写冲突”(即写入新数据的位置覆盖了稍后需要读取的旧数据),必须设置的最小内存缓冲区大小
    • 解析流程与 newSize 类似,也是一个两步过程:
      1. 先从第4个字节读取 extraSafeSize 这个数值本身占用了多少字节(长度)。
      2. 再根据这个长度,从流中读取相应字节数的数据,并用 _hpi_readSize 将其转换为一个整数。
  • 数据读取流程

    1. 读取固定头部: 首先,函数读取固定长度(hpi_kInplaceHeadSize,即5字节)的头部数据。
    2. 校验与解析:
      • 校验文件魔法标识 'hI' 和 In-place 版本号,确保文件类型正确。
      • 直接读取压缩类型。
      • 通过位运算从第3字节中解包newSizeuncompressSize 这两个字段的长度
      • 直接从第4字节读取 extraSafeSize 字段的长度
    3. 读取可变长数据: 根据上一步解析出的三个长度值,依次从数据流中分别读取 newSizeuncompressSizeextraSafeSize 的实际数据。
    4. 字节到整数的转换: 调用 _hpi_readSize 将读取到的字节数据转换为整数,并填充到对应的输出参数中。

代码解析

/*** @brief 打开并解析一个 Hpatch In-place 格式的补丁流,读取其头部信息。* @param diff_data           补丁数据流的句柄。* @param read_diff           用于从流中读取数据的函数指针。* @param out_compress_type   输出参数,用于返回解析出的压缩类型。* @param out_newSize         输出参数,用于返回补丁应用后的新文件大小。* @param out_uncompressSize  输出参数,用于返回解压缩后数据的大小。* @param out_extraSafeSize   输出参数,用于返回原地更新所需的安全缓冲区大小。* @return hpi_BOOL           成功返回 hpi_TRUE,失败返回 hpi_FALSE。*/
hpi_BOOL hpatchi_inplace_open(hpi_TInputStreamHandle diff_data,hpi_TInputStream_read read_diff,hpi_compressType* out_compress_type,hpi_pos_t* out_newSize,hpi_pos_t* out_uncompressSize,hpi_size_t* out_extraSafeSize){// 定义 In-place 模式的版本号常量。#define kHPatchLite_inplaceCode     2// lenn, lenu, lene 用于临时存储各个字段的长度信息。hpi_size_t lenn=hpi_kInplaceHeadSize; // 期望读取的头部长度(通常为5字节)。hpi_size_t lenu,lene;// 定义一个缓冲区,确保其大小足以容纳固定头部或任何一个可变长度的整数。hpi_byte   buf[(hpi_kInplaceHeadSize>sizeof(hpi_pos_t))?hpi_kInplaceHeadSize:sizeof(hpi_pos_t)];// 通过函数指针 read_diff 读取固定长度的头部到 buf 中。_CHECK(read_diff(diff_data,buf,&lenn));// --- 开始校验和解析头部 ---// buf[3] 是包含版本号和两个长度信息的 "Packed Info" 字节。lenu=buf[3];// _SAFE_CHECK 宏用于验证头部数据的合法性。// 1. lenn==hpi_kInplaceHeadSize: 确认是否成功读取了预期长度的头部。// 2. (buf[0]=='h')&(buf[1]=='I'): 校验文件魔法标识是否为 "hI"。// 3. ((lenu>>6)==kHPatchLite_inplaceCode): 将 buf[3] 右移6位取出高2位,校验版本号是否为 In-place 的版本号(2)。_SAFE_CHECK((lenn==hpi_kInplaceHeadSize)&(buf[0]=='h')&(buf[1]=='I')&((lenu>>6)==kHPatchLite_inplaceCode));// buf[2] 直接存储了压缩类型。*out_compress_type=buf[2];// 从 "Packed Info" 字节 (buf[3]) 中解包长度信息。lenn=lenu&7;       // 取低3位,得到 newSize 字段的字节数。lenu=(lenu>>3)&7; // 取中3位,得到 uncompressSize 字段的字节数。// buf[4] 直接存储了 extraSafeSize 字段的字节数。lene=buf[4];// 校验解析出的三个长度是否合法(不能超过其对应类型的最大字节数)。_SAFE_CHECK((lenn<=sizeof(hpi_pos_t))&(lenu<=sizeof(hpi_pos_t))&(lene<=sizeof(hpi_size_t)));// --- 读取可变长度的元数据 ---// 根据长度 lenn,读取 newSize 的数据。_CHECK(read_diff(diff_data,buf,&lenn));// 调用 _hpi_readSize 将字节数据转换为整数。*out_newSize=_hpi_readSize(buf,lenn);// 根据长度 lenu,读取 uncompressSize 的数据。_CHECK(read_diff(diff_data,buf,&lenu));*out_uncompressSize=_hpi_readSize(buf,lenu);// 根据长度 lene,读取 extraSafeSize 的数据。_CHECK(read_diff(diff_data,buf,&lene));*out_extraSafeSize=(hpi_size_t)_hpi_readSize(buf,lene);// 所有操作成功,返回真。return hpi_TRUE;
}

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 将其真正写入文件。

    环形缓冲区工作示意图:

    write_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 的职责就是在最后时刻被调用,确保这两个缓冲区中的所有剩余数据都被强制刷写到物理文件中,保证新文件的完整性。

代码解析

#if (_IS_WTITE_NEW_BY_PAGE!=0)/*** @brief (页模式) 将一个满页的数据刷写到底层文件流。*/static hpi_BOOL _hpatchi_listener_page_out(hpatchi_listener_extra_t* self){// 调用被包装的 listener,将页缓冲区的数据真正写入文件。// 页缓冲区位于 extraSafeSize 缓冲区的正后方。hpi_BOOL result=self->_wrap_listener->write_new(self->_wrap_listener,self->write_extra+self->kWriteExtraSize,self->page_data_len);self->page_data_len=0; // 写出后,清空页缓冲区。return result;}/*** @brief (页模式) 将数据写入页缓冲区,并在页满时自动刷写。* 这是一个二级缓冲,用于凑齐整页数据再进行I/O。*/static hpi_force_inlinehpi_BOOL _hpatchi_listener_write_page(hpatchi_listener_extra_t* self,const hpi_byte* data,hpi_size_t data_size){hpi_byte* page_buf=self->write_extra+self->kWriteExtraSize;while (data_size){// 计算当前页还剩多少空间。hpi_size_t cur_len=self->kPageSize-self->page_data_len;if (cur_len==0){ // 如果页满了...if (!_hpatchi_listener_page_out(self)) return _hpi_FALSE; // 刷写当前页。cur_len=self->kPageSize; // 重置可用空间。}// 确定本次要复制的长度(不能超过剩余空间或输入数据长度)。cur_len=(cur_len<=data_size)?cur_len:data_size;data_size-=cur_len;// 将数据逐字节复制到页缓冲区。while (cur_len--)page_buf[self->page_data_len++]=*data++;}return hpi_TRUE;}
#endif //_IS_WTITE_NEW_BY_PAGE/*** @brief 从 extraSafeSize 环形缓冲区的头部,消费(写出)指定长度的数据。*/
static hpi_BOOL _hpatchi_listener_extra_out(hpatchi_listener_extra_t* self,hpi_size_t out_size){assert(out_size<=self->cached_data_len); // 确保请求写出的数据量不超过已缓存的数据量。while (out_size){// 计算从当前读指针(cached_data_pos)到缓冲区物理末尾的连续数据长度。hpi_size_t cur_len=self->kWriteExtraSize-self->cached_data_pos;cur_len=(cur_len<=out_size)?cur_len:out_size;#if (_IS_WTITE_NEW_BY_PAGE!=0)// 页模式:将数据发送到二级页缓冲。if (!_hpatchi_listener_write_page(self,self->write_extra+self->cached_data_pos,cur_len))return _hpi_FALSE;#else// 标准模式:直接将数据写入底层文件流。if (!self->_wrap_listener->write_new(self->_wrap_listener,self->write_extra+self->cached_data_pos,cur_len))return _hpi_FALSE;#endif //_IS_WTITE_NEW_BY_PAGE// 更新环形缓冲区的读指针和剩余数据长度。self->cached_data_pos+=cur_len;// 实现环形回绕:如果读指针到达末尾,则归零。self->cached_data_pos-=(self->cached_data_pos<self->kWriteExtraSize)?0:self->kWriteExtraSize;self->cached_data_len-=cur_len;out_size-=cur_len;}return hpi_TRUE;
}/*** @brief 在补丁过程结束后,刷写所有缓冲区中剩余的数据。*/
static hpi_force_inline
hpi_BOOL _hpatchi_listener_extra_flush(hpatchi_listener_extra_t* self){// 首先,刷写 extraSafeSize 环形缓冲区中所有剩余的数据。hpi_BOOL result=_hpatchi_listener_extra_out(self,self->cached_data_len);
#if (_IS_WTITE_NEW_BY_PAGE!=0)// 页模式下,如果环形缓冲刷写成功,且页缓冲中还有数据,则强制刷写最后一页(即使不满)。if (result&&(self->page_data_len))result=_hpatchi_listener_page_out(self);
#endif //_IS_WTITE_NEW_BY_PAGEreturn result;
}/*** @brief 拦截对新文件的写操作,将数据存入 extraSafeSize 环形缓冲区。* 这是被 hpatch_lite_patch 调用的核心“写”函数。*/
static hpi_BOOL _hpatchi_listener_extra_write_new(hpatchi_listener_t* listener,const hpi_byte* data,hpi_size_t data_size){hpatchi_listener_extra_t* self=(hpatchi_listener_extra_t*)listener;while (data_size){hpi_size_t posi;// 计算如果把新数据加进来,总共需要多少缓存。hpi_size_t cur_len=data_size+self->cached_data_len;// 如果需要的缓存大于总容量...if (cur_len>self->kWriteExtraSize){// ...就需要从缓冲区头部腾出空间。cur_len-=self->kWriteExtraSize; // 计算需要腾出的最小空间。// 确保腾出的空间不超过当前已缓存的数据量。cur_len=(cur_len<=self->cached_data_len)?cur_len:self->cached_data_len;// 调用 out 函数将最旧的数据写出。if (!_hpatchi_listener_extra_out(self,cur_len)) return _hpi_FALSE;}assert(self->cached_data_len<self->kWriteExtraSize);// 计算当前可写入的连续空间大小。cur_len=self->kWriteExtraSize-self->cached_data_len;cur_len=(cur_len<=data_size)?cur_len:data_size;data_size-=cur_len;// 计算写入的起始物理位置(写指针)。posi=self->cached_data_pos+self->cached_data_len;self->cached_data_len+=cur_len;// 将新数据逐字节复制到环形缓冲区。while (cur_len--){// 实现环形回绕。posi-=(posi<self->kWriteExtraSize)?0:self->kWriteExtraSize;self->write_extra[posi++]=*data++;}}return hpi_TRUE;
}

hpatchi_inplaceB Hpatch In-place 补丁协调器

原理与设计思路解析

hpatchi_inplaceB 的核心职责不是执行补丁算法本身,而是创建一个特殊的环境,然后在这个环境中调用通用的 hpatch_lite_patch 函数来完成实际工作。这个“特殊环境”的关键就是我们之前讨论过的 hpatchi_listener_extra_t 包装器。

  • 核心策略:内存分区与I/O重定向
    这个函数最精妙的设计在于它对调用者提供的单一内存块 temp_cache 的巧妙划分和使用。它将这块内存“一分为二”:

    1. 前端部分 (Extra Buffers): 专门用于 In-place 模式的特殊需求,即 extraSafeSize 安全缓冲和可选的 pageSize 页缓冲。
    2. 后端部分 (Standard Cache): 剩下的内存则被原封不动地传递给 hpatch_lite_patch,供其内部用于标准的差分流读取缓存 (_TInputCache) 和数据运算。

    内存布局示意图:

    <------------------------------- 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_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 的读缓存很大时,可能引发的性能问题或逻辑冲突,保证了系统的稳定运行。

代码解析

#if (_IS_WTITE_NEW_BY_PAGE!=0)
// 当需要按页写入时,编译此函数签名。
hpi_BOOL hpatchi_inplaceB_by_page(hpatchi_listener_t* listener,hpi_pos_t newSize,hpi_byte* temp_cache,hpi_size_t extraSafeSize,hpi_size_t pageSize,hpi_size_t temp_cache_size){
#else //_IS_WTITE_NEW_BY_PAGE==0
// 否则,编译标准版函数签名。
hpi_BOOL hpatchi_inplaceB(hpatchi_listener_t* listener,hpi_pos_t newSize,hpi_byte* temp_cache,hpi_size_t extraSafeSize,hpi_size_t temp_cache_size){// 在标准版中,pageSize 逻辑上为0,使其在后续计算中无效。const hpi_size_t pageSize=0;
#endif //_IS_WTITE_NEW_BY_PAGEhpi_BOOL result;hpatchi_listener_extra_t extra_listener; // 栈上分配装饰器实例。// 计算前端“特殊缓冲”需要的总内存。const hpi_size_t needExtra=extraSafeSize+pageSize;// 断言检查:确保调用者提供的总内存足够大。assert(temp_cache_size>=hpi_kMinInplaceCacheSize+extraSafeSize+pageSize);// _hpi_Do4Page 宏包裹的代码只在“按页写入”版本中生效。_hpi_Do4Page(assert(pageSize>0));// 一个健壮性检查和优化。if (needExtra){// 计算扣除特殊缓冲后,留给 hpatch_lite_patch 的内存,并将其一半用作其内部的读缓存大小。hpi_size_t cache_size=(temp_cache_size-needExtra)>>1;// 确保 extraSafeSize 至少不小于 hpatch_lite_patch 的读缓存大小。extraSafeSize=(extraSafeSize>=cache_size)?extraSafeSize:cache_size;}// 初始化装饰器。它将拦截 listener 的写操作,并使用 temp_cache 的起始部分作为环形缓冲区。_hpatchi_listener_extra_init(&extra_listener,listener,temp_cache,extraSafeSize);// _hpi_Do4Page 宏确保以下代码只在“按页写入”版本中执行。_hpi_Do4Page(extra_listener.kPageSize=pageSize);    // 设置页大小_hpi_Do4Page(extra_listener.page_data_len=0);       // 初始化页数据长度_hpi_Do4Page(extraSafeSize+=pageSize);              // 更新偏移量,为页缓冲区腾出空间// 调用通用的补丁函数,这是整个设计的核心!result=hpatch_lite_patch(needExtra?&extra_listener.base:listener, // 如果需要特殊缓冲,就传入装饰器;否则直接传入原始listener。newSize,temp_cache+extraSafeSize, // 巧妙地将 temp_cache 的“后端部分”内存传递给 hpatch_lite_patch。temp_cache_size-extraSafeSize // 传递后端内存的相应大小。);// 最终结果:必须补丁过程成功,并且最终的缓存刷写操作也成功。return result&&_hpatchi_listener_extra_flush(&extra_listener);
}
http://www.dtcms.com/a/430762.html

相关文章:

  • 【langgraph】conda创建3.13环境并运行langgraph dev
  • 免费企业建站模板wordpress媒体库一直加载
  • 自己建设公司网站免费建站网站 seo
  • web开发,在线%校园,论坛,社交管理%系统,基于html,css,python,django,mysql
  • 基于开源AI智能名片链动2+1模式S2B2C商城小程序的引流爆款设计策略研究
  • Stable Diffusion里面Cross-Attention设计:为啥Q来自图像/噪声,K和V来自文本
  • 镇江网站关键字优化建立网站站点的过程中正确的是
  • 深度学习第九章 卷积神经网络
  • 【数据结构】堆、计数、桶、基数排序的实现
  • 【数据结构】数据结构秘籍:如何衡量“查找”的快慢?ASL是关键!
  • 1688网站入口学编程的正规学校培训机构
  • Python 2025:嵌入式系统与物联网(IoT)开发新趋势
  • 怎么看网站备案网店运营都要做什么
  • 【数据结构与算法学习笔记】栈
  • Java-Spring入门指南(十八)JSON字符串的解析与对象转换
  • JavaScript 严格模式
  • 数据时代的基石 —— 数据库的核心价值:MySQL 三大范式精讲
  • **跨平台开发:发散创新,探索无界限**随着技术的飞速发展,跨平台开发已经成为软件开发的必然趋势
  • 2025年中小工程企业成本管理新突破:如何选对管理软件?
  • JVM初始堆大小和最大堆大小多少合适?
  • 网站汉英结合的怎么做织梦欧美网站模板
  • 构建AI智能体:五十、ModelScope MCP广场 · MCP协议 · Cherry Studio:AI应用生产线
  • 从零开始:MCP数据库助手(二)- 核心功能实现
  • Django Admin 完全指南:内置功能、高级使用与第三方扩展
  • Django 路由详解
  • Django Admin 配置完全指南:从基础到高级实战
  • MySQL 全量 + 增量备份脚本(RPM 安装)实践与问题解析
  • 嘉兴网站建设方案外包运营推广公司
  • 第6章串数组:稀疏矩阵三元组顺序表的行逻辑连接顺序表
  • MATLAB仿真:编程基础实验全解析——从入门到实战