在前一章 Rsync 进程角色 (Sender/Receiver/Generator)中,我们认识了 rsync 内部协同工作的"演员们"------生成器、发送者和接收者。我们知道了"谁"在做事。现在,是时候来了解他们工作的"蓝图"和"清单"了。这份清单,就是本章的主角------文件列表(File List)。
在开始任何实际的文件传输之前,rsync 必须先搞清楚一个根本问题:到底有哪些文件和目录需要被处理?
一场仓库搬迁的库存清单
想象一下,你正在指挥一次大型仓库搬迁。在搬运任何一个箱子之前,你首先需要一份详细的库存清单。这份清单上不仅列出了所有物品的名称(比如"A 型号螺丝刀"),还记录了它们的数量、重量、存放位置、是否易碎等重要属性。
后续的所有工作都依赖于这份清单:
- 规划路线:根据清单决定哪些物品需要优先搬运。
- 筛选物品:决定哪些物品需要搬走,哪些可以留在原地(比如已经过期的物品)。
- 核对签收:新仓库的管理员根据这份清单核对收到的物品是否齐全、完好。
在 rsync 的世界里,文件列表 (File List) 就扮演着这份至关重要的"库存清单"的角色。它是在任何文件传输发生之前,由发送端(Sender)通过扫描源目录结构而创建的一份详细目录。这份列表是整个同步过程的基石和唯一事实来源,后续的所有决策,比如决定哪些文件需要传输(过滤)、传输的顺序(排序),以及如何处理目标端已存在的文件,都完全依赖于这份清单。
文件列表的核心:struct file_struct
这份"库存清单"在 rsync 的代码中,是由一个核心的数据结构 struct file_struct
来表示每一项的。这个结构体就像清单上的一行记录,包含了关于一个文件或目录的所有关键信息。
让我们来看一个 rsync.h
中这个结构体的简化版本:
c
// 文件: rsync.h (简化版)
struct file_struct {
const char *dirname; // 所在目录的名称
time_t modtime; // 文件最后修改时间
uint32 len32; // 文件长度的低32位 (有标志位决定是否使用64位)
uint16 mode; // 文件类型 (普通文件, 目录, 链接等) 和权限
uint16 flags; // 描述文件状态的标志位 (例如, 是否是顶层目录)
const char basename[]; // 文件的基本名称 (不含路径)
};
dirname
: 指向一个字符串,表示这个文件所在的目录路径。modtime
: 记录文件的修改时间戳。这是 rsync 判断文件是否"过时"的关键依据之一。len32
和flags
: 组合起来可以表示文件的完整大小(支持64位大文件)。mode
: 一个非常重要的字段,它通过位操作存储了两种信息:- 文件类型 : 是普通文件 (
S_IFREG
)、目录 (S_ISDIR
) 还是符号链接 (S_ISLNK
)? - 文件权限 : 比如
rwx-r-xr-x
这样的读、写、执行权限。
- 文件类型 : 是普通文件 (
basename
: 文件的名字,不包含它所在的目录路径。比如对于/home/user/doc/report.txt
,basename
就是report.txt
。
通过这个结构体,rsync 为每一个待处理的文件都创建了一个精确的"数字档案"。
文件列表的诞生与传递之旅
文件列表的生命周期可以分为两个主要阶段:生成 (在发送端)和接收(在接收端)。
这个流程确保了在同步开始时,接收端能拿到一份与发送端完全一致的、关于源文件的"事实清单"。
1. 生成列表(发送端)
这个过程由 flist.c
文件中的 send_file_list
函数发起。它会遍历命令行中指定的所有源路径。
当遇到一个目录时,它会调用 send_directory
函数。这个函数就像一个侦察兵,负责打开目录,读取里面的每一个条目。
c
// 文件: flist.c (简化逻辑)
static void send_directory(int f, struct file_list *flist, char *fbuf, ...)
{
DIR *d;
struct dirent *di;
if (!(d = opendir(fbuf))) { // 打开目录
// ... 错误处理 ...
return;
}
// 遍历目录中的每一个条目
while ((di = readdir(d))) {
// ... 跳过 "." 和 ".." ...
// 为找到的文件/目录条目调用 send_file_name
send_file_name(f, flist, fbuf, NULL, ...);
}
closedir(d);
}
对于目录中的每一个条目,它都会调用 send_file_name
,这个函数的核心又是调用 make_file
。make_file
负责执行 lstat()
系统调用,获取文件的详细元数据,并用这些数据填充一个新的 struct file_struct
实例。
c
// 文件: flist.c (简化逻辑)
struct file_struct *make_file(const char *fname, ..., STRUCT_STAT *stp, ...)
{
struct file_struct *file;
STRUCT_STAT st; // 用于存放 lstat 的结果
// 1. 获取文件状态 (lstat 不会跟随符号链接)
if (readlink_stat(fname, &st, ...) != 0) {
// ... 错误处理, 文件可能不存在 ...
return NULL;
}
// ... 检查文件是否被过滤规则排除 ...
// 2. 分配内存给新的 file_struct
file = /* ... 分配内存 ... */;
// 3. 填充结构体
file->basename = /* ... 从 fname 中提取文件名 ... */;
file->dirname = /* ... 从 fname 中提取目录名 ... */;
file->modtime = st.st_mtime;
file->len32 = (uint32)st.st_size;
file->mode = st.st_mode;
// ... 填充其他字段,如 UID, GID 等 ...
return file;
}
一旦 make_file
创建好了 file_struct
,send_file_name
就会调用 send_file_entry
,将这个结构体中的信息序列化(转换成一串字节流),然后通过网络发送给接收端。序列化是一个压缩信息的过程,比如,如果一个文件的权限和上一个文件相同,它就只发送一个标志位,而不是完整的权限数据。
2. 接收列表(接收端)
接收端的 recv_file_list
函数则执行相反的操作。它在一个循环中不断从网络读取数据,直到收到一个特殊的"列表结束"标志。
在循环中,它调用 recv_file_entry
函数。这个函数负责反序列化 从网络收到的字节流,一步步地重建出 struct file_struct
。
c
// 文件: flist.c (简化逻辑)
static struct file_struct *recv_file_entry(int f, struct file_list *flist, int xflags)
{
// 静态变量用于记住上一个文件的属性,以实现压缩
static time_t modtime;
static mode_t mode;
// ...
struct file_struct *file;
// ... 根据 xflags 判断哪些属性是与上一个文件相同的 ...
// 读取文件名
char thisname[MAXPATHLEN];
// ... 从网络流 f 中读取文件名到 thisname ...
// 分配内存
file = /* ... 分配内存 ... */;
// 重建结构体
file->basename = /* ... */
file->dirname = /* ... */
// 如果 xflags 中没有 "XMIT_SAME_TIME" 标志,就从网络读取新的时间
if (!(xflags & XMIT_SAME_TIME)) {
modtime = read_varlong(f, 4);
}
file->modtime = modtime;
// 如果 xflags 中没有 "XMIT_SAME_MODE" 标志,就从网络读取新的模式
if (!(xflags & XMIT_SAME_MODE)) {
mode = from_wire_mode(read_int(f));
}
file->mode = mode;
// ... 读取并设置其他属性 ...
return file;
}
每当一个 file_struct
被成功重建后,它就被添加到一个本地的 file_list
数组中。当整个列表接收完毕后,接收端就拥有了一份关于源文件的完整、精确的"数字蓝图"。
文件列表的威力:驱动后续一切
一旦发送和接收两端的 file_list
准备就绪,rsync 真正的威力才开始显现。这份清单是后续所有高级功能的"数据源":
-
文件过滤 :在同步开始前,rsync 会用过滤规则逐一检查文件列表中的每一个条目。如果一个文件的名字匹配了某个"排除"规则(比如
*.tmp
),rsync 就会在它的file_struct
中做一个标记,后续步骤就会直接跳过这个文件。 -
决策制定:生成器 (Generator) 进程会遍历这份文件列表。对于列表中的每一项,它会去检查目标位置是否存在同名文件。
- 如果目标文件不存在:生成器会指示直接传输整个新文件。
- 如果目标文件存在 :它会比较文件列表中的元数据(记录在
file_struct
中)和目标文件的元数据。如果大小或修改时间不同,它就会启动 增量传输算法,只传输差异部分。如果元数据完全相同,它就什么也不做,从而节省了大量时间和带宽。
-
排序与优化:rsync 会对文件列表进行排序。这种排序保证了目录总是在其内容之前被处理,这对于正确创建目录结构和设置权限至关重要。
总结
在本章中,我们揭开了 rsync 同步流程起点的秘密------文件列表。
- 核心作用:文件列表是 rsync 的"库存清单",它在任何文件操作开始前被创建,详细记录了所有待处理项目的元数据。
- 核心数据结构 :
struct file_struct
是列表中的基本单元,它像一个文件的"身份证",包含了名称、路径、大小、时间、权限等所有关键信息。 - 生成与传递:发送端通过扫描文件系统来构建文件列表,然后将其序列化后高效地传输给接收端。接收端再反序列化,重建一个一模一样的列表。
- 驱动力:这份列表是 rsync 后续所有决策的唯一依据,无论是过滤、比较还是启动增量算法,都离不开它提供的数据。
现在我们知道了 rsync 是如何创建出这份详细的"作战地图"的。但是,如果用户只想同步目录下的 .c
文件,而不想同步 .o
文件,rsync 是如何在这份完整的地图上"划掉"不需要的部分呢?在下一章,我们将深入探讨 rsync 强大而灵活的 过滤规则系统,看看它是如何精确筛选文件列表的。