rsync源码解析 (3) 进程角色 (Sender/Receiver/Generator)

在上一章节"增量传输算法"中,我们深入剖析了 rsync 的核心算法------它是如何通过巧妙的校验和比对,做到只传输文件的差异部分的。我们理解了"做什么"和"怎么做"的细节。然而,这个算法的执行并不是由一个单一的程序完成的,而是由一组分工明确的进程协同合作的结果。

本章,我们将把镜头从算法本身移开,聚焦于执行这些算法的rsync 的进程角色。你会发现,rsync 的架构并非简单的"客户端-服务器"两方通信,尤其是在文件的接收端,它采用了一个独特的三进程模型。理解这个模型是掌握 rsync 完整工作流程的关键。

一个建筑项目的比喻

想象一下,你要对一栋旧建筑进行现代化改造。这个项目不是简单地把新设计图交给施工队就完事了,而是需要一个更精密的流程:

  1. 架构师 (Generator - 生成器):首先,一位架构师会来到现场,仔细勘测现有的建筑结构(目标端的旧文件),并绘制出一份详细的"物料需求清单"(块校验和列表)。这份清单精确地描述了哪些旧结构可以保留,哪些需要替换。

  2. 物料供应商 (Sender - 发送者):架构师将这份清单发送给远方的物料供应商。供应商拥有所有新建筑的材料(源端的新文件)。他会对照清单,只挑选并运送那些现场没有的、或者需要更新的"新材料"(文件的差异数据)。他不会把整栋新建筑的材料都运过来,那样太浪费了。

  3. 施工队 (Receiver - 接收者):最后,现场的施工队拿到了供应商运来的新材料。他们根据架构师的原始蓝图(这里指重建指令),将新材料安装到正确的位置,并巧妙地利用了所有可复用的旧结构,最终精确地建好了改造后的新建筑(目标端的新文件)。

Rsync 的核心工作流程,就是这三个角色------生成器、发送者、接收者------的协同演出。其中,"生成器"和"接收者"都位于目标机器上,而"发送者"位于源机器上。

进程角色的分工

让我们来正式认识一下这三个角色:

  • 发送者 (Sender)

    • 位置: 源机器(存放新文件的机器)。
    • 职责 :
      1. 接收来自"生成器"的校验和列表(物料需求清单)。
      2. 逐字节扫描自己的新文件,利用增量传输算法与校验和列表进行比对,找出差异。
      3. 将差异部分(新数据块)和匹配指令(重用旧数据块的命令)打包,发送给"接收者"。
    • 对应代码 : 主要逻辑在 sender.c 文件中。
  • 生成器 (Generator)

    • 位置: 目标机器(存放旧文件的机器)。
    • 职责 :
      1. 读取本地的旧文件(如果有的话)。
      2. 将旧文件分割成块,并为每个块计算出弱校验和与强校验和。
      3. 将这个校验和列表发送给"发送者"。
      4. 在整个同步流程中,它还扮演着"总指挥"的角色,决定哪些文件需要同步,并协调其他进程的工作。
    • 对应代码 : 主要逻辑在 generator.c 文件中。
  • 接收者 (Receiver)

    • 位置: 目标机器。
    • 职责 :
      1. 接收来自"发送者"的差异数据和匹配指令。
      2. 像一个"拼装工",根据指令,一部分数据直接使用新接收的,另一部分则从本地的旧文件中复制。
      3. 最终,在本地组装出完整的新文件。
    • 对应代码 : 主要逻辑在 receiver.c 文件中。

协作流程图

这个"建筑项目"的流程可以用一个时序图清晰地展示出来。假设我们要将文件从"源端"同步到"目标端":

sequenceDiagram participant G as 目标端 (生成器) participant S as 源端 (发送者) participant R as 目标端 (接收者) Note over G, R: 目标端启动,分裂为两个进程 G->>G: 读取本地的旧文件 G->>G: 计算旧文件的块校验和 G->>S: 发送校验和列表 ("物料需求清单") S->>S: 接收列表,并用它来扫描新文件 S->>S: 生成差异数据和重建指令 S->>R: 发送差异数据和指令 ("运送新材料") R->>R: 根据指令,使用新材料和旧文件结构 R->>R: 在本地重建出最终的新文件 Note over G, R: 同步完成

这个设计的精妙之处在于,计算密集型的"生成校验和"和 I/O 密集型的"重建文件"被分在了两个进程中,它们可以并行工作,极大地提高了效率。

深入代码:进程是如何诞生的?

你可能会问:这三个进程是在什么时候、又是如何被创建和分配角色的呢?

答案藏在 main.cdo_recv 函数中。当 rsync 确定自己是接收文件的一方时(即它不是 --sender),它会执行这个函数。do_recv 的核心操作之一就是 fork(),这是一个经典的 Unix 系统调用,用于创建一个新的子进程。

c 复制代码
// 文件: main.c (简化逻辑)

static int do_recv(int f_in, int f_out, char *local_name)
{
    int pid;

    // ... 其他准备工作 ...

    if ((pid = do_fork()) == -1) {
        // fork 失败,报错退出
        rsyserr(FERROR, errno, "fork failed in do_recv");
        exit_cleanup(RERR_IPC);
    }

    if (pid == 0) {
        // 这里是子进程的代码
        am_receiver = 1; // 角色分配:我是"接收者" (施工队)
        // ...
        recv_files(f_in, f_out, local_name); // 开始接收并重建文件
        // ...
    }

    // 这里是父进程的代码
    am_generator = 1; // 角色分配:我是"生成器" (架构师)
    // ...
    generate_files(f_out, local_name); // 开始生成校验和并指挥整个流程
    // ...

    // 等待子进程结束
    wait_process_with_flush(pid, &exit_code);
    return exit_code;
}

这段代码清晰地展示了:

  1. 父进程调用 do_fork() 创建一个子进程。
  2. 在子进程(pid == 0 的分支)中,通过设置全局变量 am_receiver = 1,将自己的角色定义为"接收者"。然后它会调用 recv_files (receiver.c 中的核心函数) 来执行重建任务。
  3. 在父进程中,它将自己的角色定义为"生成器"(am_generator = 1),并调用 generate_files (generator.c 中的核心函数) 来总揽全局。

而"发送者"(am_sender)这个角色,则是在程序启动时根据命令行参数(如客户端指定了远程目标)或守护进程配置来确定的,它不需要 fork,因为它自己就是独立的一方。

为了方便调试和日志记录,rsync 提供了一个内部函数 who_am_i(),它会根据这些标志变量返回当前进程的角色名。

c 复制代码
// 文件: rsync.c

const char *who_am_i(void)
{
    // ...
	return am_sender ? "sender"
	     : am_generator ? "generator"
	     : am_receiver ? "receiver"
	     : "Receiver"; /* pre-forked receiver */
}

这个函数在 rsync 的日志输出中被频繁使用,帮助我们理解在复杂的交互中,到底是哪个"角色"在说话。

各角色的核心动作

现在我们知道了角色的来源,再来看看它们各自执行的核心代码片段。

1. 生成器 (Generator) 的工作

generator.cgenerate_and_send_sums 函数中,生成器读取本地文件,计算校验和,并发送出去。

c 复制代码
// 文件: generator.c (简化版)

static int generate_and_send_sums(int fd, OFF_T len, int f_out, int f_copy)
{
    // ...
    // 1. 决定块大小和校验和长度
    sum_sizes_sqroot(&sum, len);
    
    // 2. 将这些头部信息发送给 Sender
    write_sum_head(f_out, &sum);

    // 3. 循环读取文件的每一个块
    for (i = 0; i < sum.count; i++) {
        // ... 读取一个数据块 ...
        
        // 4. 计算两个校验和
        sum1 = get_checksum1(map, n1);
        get_checksum2(map, n1, sum2);

        // 5. 将校验和发送给 Sender
        write_int(f_out, sum1);
        write_buf(f_out, sum2, sum.s2length);
    }
    // ...
    return 0;
}

这个过程就是"绘制物料需求清单"并寄出的过程。

2. 发送者 (Sender) 的工作

sender.csend_files 函数中,发送者接收校验和列表,然后开始匹配和发送差异。

c 复制代码
// 文件: sender.c (简化版)

void send_files(int f_in, int f_out)
{
    // ...
    // 1. 接收来自 Generator 的校验和列表
    s = receive_sums(f_in);

    // 2. 打开本地的新文件
    fd = do_open_checklinks(fname);
    mbuf = map_file(fd, st.st_size, read_size, s->blength);

    // ...
    // 3. 核心步骤:匹配文件,并发送差异数据
    //    这背后就是上一章讲的增量传输算法
    match_sums(f_xfer, s, mbuf, st.st_size); 
    // ...
}

这就是"根据清单发货"的过程。

3. 接收者 (Receiver) 的工作

最后,在 receiver.creceive_data 函数中,接收者根据收到的指令重建文件。

c 复制代码
// 文件: receiver.c (简化版)

static int receive_data(...)
{
    // ...
    // 循环接收来自 Sender 的"令牌"(token)
    while ((i = recv_token(f_in, &data)) != 0) {
        if (i > 0) {
            // 这是一个新数据块 (i 是数据长度)
            // 直接将 data 写入新文件的当前位置
            write_file(fd, 0, offset, data, i);
            offset += i;
        } else {
            // 这是一个匹配指令 (i 是一个负数,代表块的索引)
            i = -(i+1); // 解码出块的索引
            offset2 = i * (OFF_T)sum.blength; // 计算旧文件中该块的位置
            
            // 从旧文件中复制这个块到新文件的当前位置
            map = map_ptr(mapbuf, offset2, len);
            write_file(fd, 0, offset, map, len);
            offset += len;
        }
    }
    // ...
    return 1;
}

这就是"施工队根据新材料和旧结构进行施工"的过程。

总结

在本章中,我们了解了 rsync 独特的三进程协作模型:

  • 动机:为了将计算密集型任务和 I/O 密集型任务分离,实现更高的并行度和效率,rsync 在接收端采用了"生成器+接收者"的双进程模式。
  • 三大角色
    • 生成器 (Generator):目标端的"架构师",负责分析旧文件并创建校验和"蓝图"。
    • 发送者 (Sender):源端的"物料供应商",根据蓝图仅发送必要的差异"材料"。
    • 接收者 (Receiver):目标端的"施工队",利用新材料和旧结构重建文件。
  • 实现 :这一模型通过在接收端执行一次 fork() 来实现,父进程成为"生成器",子进程成为"接收者"。它们通过 am_generatoram_receiver 等全局标志来识别自己的身份。

理解了 rsync 是如何组织其内部"团队"的,我们对它的工作原理有了更宏观的认识。我们已经知道了 rsync 如何解析选项,如何执行算法,以及是由哪些进程角色来执行的。

但是,在这一切发生之前,rsync 首先需要确定到底要同步哪些文件。这个决策过程本身也相当复杂,涉及到递归遍历、过滤规则等。在下一章,我们将探讨 rsync 工作流程的起点------文件列表 (File List),看看 rsync 是如何构建出需要处理的文件清单的。

相关推荐
JulyYu4 小时前
Android系统保存重名文件后引发的异常解决
android·操作系统·源码
灵魂猎手4 小时前
7. MyBatis 的 ResultSetHandler(一)
java·后端·源码
IT毕设梦工厂6 小时前
大数据毕业设计选题推荐-基于大数据的1688商品类目关系分析与可视化系统-Hadoop-Spark-数据可视化-BigData
大数据·毕业设计·源码·数据可视化·bigdata·选题推荐
源码宝2 天前
【智慧工地源码】智慧工地云平台系统,涵盖安全、质量、环境、人员和设备五大管理模块,实现实时监控、智能预警和数据分析。
java·大数据·spring cloud·数据分析·源码·智慧工地·云平台
最初的↘那颗心3 天前
Flink Stream API 源码走读 - window 和 sum
大数据·hadoop·flink·源码·实时计算·窗口函数
灵魂猎手4 天前
3. MyBatis Executor:SQL 执行的核心引擎
java·后端·源码
灵魂猎手5 天前
2. MyBatis 参数处理机制:从 execute 方法到参数流转全解析
java·后端·源码
灵魂猎手5 天前
1. Mybatis Mapper动态代理创建&实现
java·后端·源码
谷哥的小弟9 天前
Spring Framework源码解析——BeanPostProcessor
spring·源码
谷哥的小弟9 天前
Spring Framework源码解析——DisposableBean
spring·源码