一、文件传输
文件传输一直是互联互通以来的一个重要问题,为了这个问题,有各种的ftp实现,sz/rz模式,diff工具、以及scp等各种方法,它们各有各自的应用场景,也不能说一个就直接取代另一个。对于rsync来说,它应该算是一个比较特殊的方法,或者说在diff和scp之间有一个比较好的中和。通常来说,通过diff文件生成补丁,然后把补丁传递到另一个文件,再执行patch,这个步骤相对比较复杂,而且会在两个主机上各自需要一份额外的patch文件,所以diff一般在大量发布的时候使用,或者说是一种server/client的模式,一个diff可以被任意多个客户端公用的情况下使用,并且版本已经比较稳定的情况下使用。
rsync的目的是为了避免scp之类的全量赋值,取而代之使用一种增量复制。作为一个程序员来说,我们见到的最多的情况就是在一个版本的基础上增加或者修改一个内容。回想一下典型的vim操作,就是使用i插入一个新的内容,或者使用r来修改一个部分的内容,其它的大部分时间中我们并没有对文件进行翻天覆地的变化,所以一般文件的修改比较少,此时如果能够识别出目标文件和源文件之间相同的内容,对于传输来说无疑是非常有优势的。
rsync的实现算法在google搜索的第一项中就有,该文章写成于1996年,那时候我想大部分人还没有上大学吧,文章的名字就叫做《The rsync algorithm》,实现的代码也不多,只是和大部分的开源软件一样,代码写的比较随意,所以不太好理解。好在这篇文章不长,而且描述详细,大致看一下应该能够理解它的精髓。
二、传输的问题
现在假设希望将α机器中的A文件同步到β机器上的B文件,此时 大致执行的操作为一下步骤。首先是β机器将B文件切换成指定大小的block(或者说是chunk),对每个chunk计算一个rolling checksum和一个MD5 摘要信息,之后将这些信息发送到alpha机器,然后由α机器决定是有哪些字段需要发送,或者经过哪些操作子可以使B文件和A文件类似。
再次回想之前说的例子,假设说A和B文件原本相同,A然后A中加了一行注释,然后其实A和B的差别并不大,它的大部分内容都死相同的,如果对A也进行相同的block划分,那么几乎可以肯定的说A和B的所有block都是不同的,所以此时对于B的每一个block,A要对自己文件的每一个可能的偏移量位置开始的block都要计算一个rolling checksum和一个MD5摘要信息进行匹配。
这里进而带来的问题是如果计算文件的每个偏移位置开始的block,那么此时对于系统CPU的消耗势必难以承受,我们可以想象一下一个二进制文件加上调试信息可能达到30M左右,而即使是一个比较大的源文件,它通常也有将近50KB左右,如果对于每个block计算一下checksum,那么对于所有字节的遍历次数应该为50KB×blocksize。
所以rsync使用的checksum使用的是adler32 的rolling checksum ,也就是两个连续的block的计算非常简单,可以通过前一个block的checksum方便的计算出紧邻的block的checksum,这样遍历整个文件的checksum其实就是一个一次遍历的过程,从而降低了系统CPU的消耗。
三、hash查找
在《The rsync algorithm》这篇文章中,大家可能没有注意到一个细节,在第4节“Checksum searching”一节中,其中建立的hash表是以B文件中的所有block为输入而建立的hash队列,而不是为A建立hash队列。这一点结合之前的说明也容易理解。B文件分成不同的block之后,它的真正的block块的数目是B文件的大小除以block大小;如果让A文件建立hash链表,那么所有的rolling checksum将会是整个A文件的大小,每个checksum至少需要20字节,所以相当于A文件大小乘以20的额外内存需要量。考虑到这个原因,其中以B文件的各个block为hash还是比较合适的选择。
四、基本数据结构
sender.c
read_sum_head(f, s);
其中主要的两个结构包含了下面数据结构
struct sum_buf {
OFF_T offset; /**< offset in file of this chunk */
unsigned int len; /**< length of chunk of file */
uint32 sum1; /**< simple checksum */
short flags; /**< flag bits */
char sum2[SUM_LENGTH]; /**< checksum */
};
struct sum_struct { 这个是一个统领性文件,包含了B文件的chunk数量(每个chunk对应一个sum_buf结构),每个chunk的长度,文件中剩余的不够一个chunk的部分长度,sum2部分长度(使用MD4或者MD5长度)等,这个结构的构造在receive_sums函数中完成。
OFF_T flength; /**< total file length */
size_t count; /**< how many chunks */
unsigned int blength; /**< block_length */
unsigned int remainder; /**< flength % block_length */
int s2length; /**< sum2_length */
struct sum_buf *sums; /**< points to info for each chunk */
};
另一方面,本地文件也需要建立自己的映射结构,这个结构在源代码中通过
struct map_struct {
char *p; /* Window pointer */
int fd; /* File Descriptor */
int p_size; /* Largest window size we allocated */
int p_len; /* Latest (rounded) window size */
int def_window_size; /* Default window size */
int status; /* first errno from read errors */
OFF_T file_size; /* File size (from stat) */
OFF_T p_offset; /* Window start */
OFF_T p_fd_offset; /* offset of cursor in fd ala lseek */
};
结构来表示,由函数
struct map_struct *map_file(int fd, OFF_T len, OFF_T map_size, size_t block_size)
完成该结构的映射和返回。
五、hash表的构建
match_sums--->>build_hash_table
static void build_hash_table(struct sum_struct *s)
{
size_t i;
if (!tag_table)
tag_table = new_array(size_t, TABLESIZE);
targets = new_array(struct target, s->count);
if (!tag_table || !targets)
out_of_memory("build_hash_table");
for (i = 0; i < s->count; i++) {
targets[i].i = i;//i应该为文件中chunk的编号,而t则是该chunk的tag值,这个值是将rolling checksum按照简单运算得到的一个16bit数值,否则使用32bits太多,不可能建立一个内存hash的,因为地址空间之后2^32.。
targets[i].t = gettag(s->sums[i].sum1);
}
//targets数组没有浪费,分配的时候是按照文件中实际block数量分配
// 并且按照各个block中的tag值大小排序
qsort(targets,s->count,sizeof(targets[0]),(int (*)())compare_targets);
for (i = 0; i < TABLESIZE; i++)
tag_table[i] = NULL_TAG;
//由于target是按照tag来排序的,所以在targets中,所有具有相同tag的
//文件会被连续存储,这样找到最开始的一个tag,顺序遍历即可得到
//具有相同tag的链表内容
// 后序遍历,其中tag_table中以tag为索引,序号最小的block编号存储在其中
//也即是说,对于一个特定的tag,tag_table[tag]中存储的是tagets数组中具有相同tag的所有chunk的最小一个的编号。
for (i = s->count; i-- > 0; )
tag_table[targets[i].t] = i;
}
六、源文件遍历中查找相同chunk
static void hash_search(int f,struct sum_struct *s, struct map_struct *buf, OFF_T len)
函数中完成对本地A文件的遍历,它的做法就是在循环中不断步进一个偏移,然后看这个偏移是否和B中的hash表中的一个chunk相同,相同的判断经过了两部(根据作者的说明是三步),其中第一步就是tag值相同,这个是rolling sum经过简单计算而得到的hash值,是16bits,由于即是是冲突可能性较大的rolling sum都有冲突,所以这个tag的冲突更大;进一步检测32bit的rolling sum,如果相同,计算16字节的MD5和,如果再次相同,确认为两个相同的chunk。有些同学可能会觉得这样不科学、不严谨(例如我就这么认为),但是作者说了,这种概率太小,在实际应用中可以忽略。
七、发送修改内容
当一个chunk完成匹配之后,发送方调用matched函数完成和对方的通讯。看一下这个协议可以知道,它由两部分组成,开始是B中没有因此需要A中传输过来的内容,这部分通过一个 “长度 ++++ 指定长度内容 ”的方式发送,接下来是是匹配的内容,这个内容在代码中成为token,也就是B中的第若干个chunk的编号。发送的时候是一个负值。
/* non-compressing send token */
static void simple_send_token(int f,int token,
struct map_struct *buf,OFF_T offset,int n)
{
if (n > 0) {
int l = 0;
while (l < n) {
int n1 = MIN(CHUNK_SIZE,n-l);//每次发送不超过一个chunk长度。
write_int(f,n1); 首先发送将要发送的字节流长度,
write_buf(f,map_ptr(buf,offset+l,n1),n1); 发送字节流内容。
l += n1;
}
}
/* a -2 token means to send data only and no token */
if (token != -2) {
write_int(f,-(token+1));//匹配的token编号,转换为负值,以区别之前的长度。
}
}
八、匹配之后
在一个chunk匹配完成之后,发送方的文件指针将以直接跳过这一个chunk,而不是像之前那样单个字节的步进,这不仅是为了效率,同样也是符合真正的现实模型。
整篇文章写的很乱,可能大部分看的人都会觉得看不懂。well,本来就没有准备让所有的人都懂。