Rsync为了只同步文件变化的部分,使用了两种 hash 算法:弱校验算法和强校验算法。
弱校验用于快速分辨出不同的块,使用的是 Adler-32 算法。
强校验用于确保数据块是真的相同,因为弱校验很有可能hash冲突,加一层强校验,双保险!
强校验使用的是 MD5 算法。
这篇文章记录自己学习弱检验 Adler-32 算法的体会。
Adler-32算法简介
Adler-32 算法是由 Mark Adler 在1995年发明的一种滚动校验算法,算法因此也以作者名字命名。
除了Rsync,还有大名鼎鼎的 zlib 也在使用这一算法。
算法原理
这个算法的原理说起来也简单。
假设我们要为数组:A1, A2, A3, … An 计算出一个32 位的hash值。
sum = adler32(A1, A2, A3, … An)
记 sum 的低16位为:s1
记 sum 的高16位为:s2
sum = s2 × 216 + s1
其中s1 是指从A1到An每个数字依次相加得到的和。
s2 指每处理一位数字,s2的值就加上s1 (难以用文字描述,还是看公式吧)
s1 = A1 + A2 + A3 + … + An
s2 = A1 + (A1 + A2) + (A1 + A2 + A3) + … + (A1 + A2 + … + An)
= A1 × n + A2 × (n - 1) + A3 × (n - 2) + … +An
我们来看下Rsync-1.0 版本中实现的 Adler32 算法
uint32 get_checksum1(char *buf,int len)
{
int i;
uint32 s1, s2;
s1 = s2 = 0;
for (i = 0; i < len; i++) {
s1 += buf[i];
s2 += s1;
}
return (s1 & 0xffff) + (s2 << 16);
}
怎么样?比起上面眼花缭乱的数学公式,代码看起来是不是更简单,更容易懂?
Rsync之后的版本对Adler32算法的实现进行了优化,最新的Rsync-3.2.3中Adler32算法实现如下:
/* a non-zero CHAR_OFFSET makes the rolling sum stronger, but is
incompatible with older versions :-( */
#define CHAR_OFFSET 0
uint32 get_checksum1(char *buf1, int32 len)
{
int32 i;
uint32 s1, s2;
schar *buf = (schar *)buf1;
s1 = s2 = 0;
for (i = 0; i < (len-4); i+=4) {
s2 += 4*(s1 + buf[i]) + 3*buf[i+1] + 2*buf[i+2] + buf[i+3] + 10*CHAR_OFFSET;
s1 += (buf[i+0] + buf[i+1] + buf[i+2] + buf[i+3] + 4*CHAR_OFFSET);
}
for (; i < len; i++) {
s1 += (buf[i]+CHAR_OFFSET); s2 += s1;
}
return (s1 & 0xffff) + (s2 << 16);
}
不同于原始算法中一个字节一个字节的处理,优化后的算法一次处理4个字节,可以显著降低运算量,提高性能。
另外一次处理4个字节,是不是32位CPU刚好一次能容纳4个字节宽度的原因?我不确定。
算法中还引入了一个宏变量:CHAR_OFFSET
通过注释得知如果CHAR_OFFSET设置为非0值,可以提高算法健壮性,这一点也不明白为什么。
滚动校验
能计算出一组数据的校验和不算本事,市面上有很多种算法都能计算出校验和,甚至自己也可以设计一种算法。
Rsync对算法的要求是:
在已知 [A1, A2, … An] 校验和的情况下,如果再来个数据:An+1,能够在之前校验和的基础上迅速推导出 [A2, A3, … An, An+1] 的校验和,也就是数据块向后滑动1字节后,迅速计算出下一个块的校验和, 而不是逐字节的计算[A2, A3, … An, An+1]的校验和,那样就太慢了。
我们记 [A1, A2, … An] 的校验和为: sum[1, n]
记 [A2, A3, … An, An+1] 的校验和为: sum[2, n+1]
sum[2, n+1]校验和求值的基本思路是:
在 sum[1, n]的基础上,去除 A1 相关部分,再加上 An+1 相关部分。
前面说过,sum 由低16位的s1和高16位的s2组成,我们做如下标记:
记滑动前的块 sum[1, n] s1 部分为: s1[1, n]
记滑动前的块 sum[1, n] s2 部分为: s2[1, n]
记滑动后的块 sum[2, n+1] s1 部分为: s1[2, n+1]
记滑动后的块 sum[2, n+1] s2 部分为: s2[2, n + 1]
那么我们可以做出如下数学推导:
sum[2, n+1] = s1[2, n+1] + s2[2, n + 1] × 216
= (s1[1, n] - A1 + An+1 ) + (s2[1, n] - n × A1 + (s1[1, n] - A1 + An+1 ) ) × 216
再来看看 Rsync-1.0 代码中关于块向后滑动一字节后,如何根据前一个块的校验和计算下一个块的校验和 (代码来自 match.c: hash_search() 函数,为了便于描述,做了抽取和变量名修改)
uint32 s1, s2, sum;
/* 1. 计算出第一块校验和, n代表数据块长度 */
sum = get_checksum1(buf, n);
s1 = sum & 0xFFFF;
s2 = sum >> 16;
/* 2. 数据块向后滑动1字节 */
/* 2.1 去除被滑动掉的第一个字节相关部分 */
/* Trim off the first byte from the checksum */
s1 -= buf[offset];
s2 -= n * buf[offset];
/* 2.2 添加新被滑入字节的相关部分 */
s1 += buf[offset+n];
s2 += s1;
/* 2.3 计算出滑动1字节后新块的校验和 */
sum = (s1 & 0xffff) + (s2 << 16);
代码和数学式对的上了么?我觉得直接看代码,反而更容易理解。
参考文章:
[1] Rsync官网对于adler-32算法的描述