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 是如何构建出需要处理的文件清单的。

相关推荐
重启的码农5 小时前
rsync源码解析 (6) 文件属性与元数据处理
源码
重启的码农5 小时前
rsync源码解析 (5) 文件过滤规则系统
源码
重启的码农5 小时前
rsync源码解析 (4) 文件列表 (File List)
源码
重启的码农5 小时前
rsync源码解析 (7) 客户端/服务器通信协议
源码
前端双越老师6 小时前
为何前端圈现在不关注源码了?
面试·前端框架·源码
RPA+AI十二工作室20 小时前
影刀RPA_抖音评价获取_源码解读
运维·机器人·自动化·源码·rpa·影刀
RPA+AI十二工作室1 天前
影刀RPA_Temu关键词取数_源码解读
大数据·自动化·源码·rpa·影刀
重启的码农1 天前
rsync源码解析 (2) 增量传输算法
源码
重启的码农1 天前
rsync源码解析 (1) 选项与配置解析
源码