在上一章 过滤规则系统 中,我们学会了如何像行李打包专家一样,精确地告诉 rsync 哪些文件要同步,哪些要忽略。我们解决了"同步什么"的问题。现在,我们要更进一步,探讨一个更细致的问题:当 rsync 同步一个文件时,它仅仅是复制文件里的内容吗?当然不是。
本章,我们将深入了解文件内容之外的所有信息------它的属性和元数据。
包裹上的标签
想象一下,你邮寄一个包裹。包裹里的物品(比如一本书)是内容。但包裹外面贴着的标签同样至关重要,它记录了:
- 收件人和寄件人(所有者和用户组)
- 寄出日期(时间戳)
- 是否是易碎品(权限,比如只读)
- 这是一个系列包裹的第一件(符号链接或硬链接)
如果快递员只把书送到了,却弄丢了标签,收件人可能会感到困惑。谁送的?什么时候送的?我能把它借给别人看吗?
文件也是如此。一个文件的价值不仅仅在于它的内容,还在于它的元数据 (metadata)------也就是它的"标签"。Rsync 的一个强大之处在于,它在同步时,不仅能高效地复制文件内容,还能精确地读取源文件的这些"标签"信息,并在目标位置一丝不差地重新贴上。这保证了同步后的文件在各个方面都与源文件保持一致,而不仅仅是内容相同。
这些"标签"主要包括:
- 权限 (Permissions): 决定了谁能读、写或执行这个文件。
- 所有者和用户组 (Owner and Group): 文件属于哪个用户和哪个用户组。
- 时间戳 (Timestamps): 文件的最后修改时间、访问时间等。
- 特殊文件类型 (Special File Types) : 文件本身可能是一个指向另一个文件的符号链接 ,或者与另一个文件共享同一份底层数据的硬链接。
- 高级属性 (Advanced Attributes): 更复杂的权限控制(ACLs)和用户自定义的元数据(扩展属性)。
一键搞定:"归档模式"的威力
那么,我们如何告诉 rsync 去处理所有这些元数据呢?幸运的是,我们不需要记住一大堆复杂的选项。对于绝大多数备份和同步场景,一个选项就足够了:-a
(或 --archive
),即归档模式。
让我们回到上一章的例子,假设我们想把 my_project
目录完整地备份到服务器,同时保留所有元数据:
bash
rsync -a my_project/ user@remote:/backup/
这个 -a
选项是一个非常方便的"快捷套餐",它等同于同时开启了多个独立的选项:
-r
:递归同步目录。-l
:保留符号链接。-p
:保留文件权限。-t
:保留文件修改时间。-g
:保留用户组。-o
:保留所有者(通常需要管理员权限)。-D
:保留设备文件和特殊文件。
使用 -a
选项,rsync 就会尽其所能,让目标位置的文件成为源文件的一个完美镜像,包括所有的"标签"。
当然,对于更特殊的需求,比如硬链接、ACLs 或扩展属性,你可能还需要额外添加 -H
、-A
或 -X
选项。但 -a
是所有精确保管的起点。
深入幕后:贴标签的流程
rsync 是如何实现这个精细的"贴标签"过程的呢?这个过程分为两步:在发送端收集信息 ,在接收端应用信息。
-
收集信息 (发送端) :我们在 文件列表 (File List) 章节中已经知道,当 rsync 扫描源目录时,它会为每个文件创建一个
struct file_struct
结构体。这个结构体里就存放了通过lstat()
系统调用获取到的所有元数据。 -
应用信息 (接收端) :这是本章的核心。当一个文件在目标位置被创建或更新后,接收端进程会执行一个关键函数
set_file_attrs
,这个函数负责根据收到的file_struct
中的信息,来设置新文件的各种属性。
整个应用过程可以用一个简单的时序图来描绘:
代码深潜:系统调用的封装
你可能注意到上图中,接收者调用的不是 chmod
、chown
等原生系统函数,而是 do_chmod
、do_lchown
。这是 rsync 设计中的一个重要细节。Rsync 将几乎所有的系统调用都封装在了 syscall.c
文件中。
这么做有几个好处:
- 演习模式 (
--dry-run
) :如果用户只是想看看 rsync 会 做什么,而不是真的执行。这些封装函数会先检查dry_run
标志,如果是演习模式,就直接返回成功,不会对系统做任何实际的改动。 - 集中处理:将特定于操作系统的怪异行为和错误处理集中在一个地方。
让我们看看 do_lchown
的简化实现:
c
// 文件: syscall.c
// 尝试修改文件所有者和用户组
int do_lchown(const char *path, uid_t owner, gid_t group)
{
if (dry_run) return 0; // 如果是演习模式, 什么都不做直接返回成功
RETURN_ERROR_IF_RO_OR_LO; // 如果是只读或只列出模式, 返回错误
#ifndef HAVE_LCHOWN
#define lchown chown
#endif
return lchown(path, owner, group); // 调用真正的系统函数
}
这个简单的封装,为 rsync 提供了巨大的灵活性和健壮性。
代码深潜:属性设置的总指挥 set_file_attrs
所有这些 do_*
封装函数都在 rsync.c
中的 set_file_attrs
函数中被统一调用。这个函数是属性设置的"总指挥"。它会逐一检查各种 preserve_*
选项(比如 preserve_perms
、preserve_mtimes
,这些都是由 -a
选项开启的),然后决定是否需要调用相应的系统函数来更新属性。
c
// 文件: rsync.c (简化逻辑)
int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp, ...)
{
int updated = 0; // 记录是否发生了更新
// ...
// 1. 检查是否需要更新所有者和用户组
// (需要管理员权限, 并且目标文件的所有者与源文件不同)
change_uid = am_root && uid_ndx && sxp->st.st_uid != (uid_t)F_OWNER(file);
change_gid = gid_ndx && sxp->st.st_gid != (gid_t)F_GROUP(file);
if (change_uid || change_gid) {
// 调用封装函数来修改所有权
if (do_lchown(fname, uid, gid) == 0) {
updated |= UPDATED_OWNER;
}
}
// ... 检查并设置扩展属性 (xattrs) 和 ACLs ...
// 2. 检查是否需要更新时间戳
if (preserve_mtimes && !same_mtime(file, &sxp->st, ...)) {
// 调用 set_times 来更新修改和访问时间
if (set_times(fname, &sx2.st) == 0) {
updated |= UPDATED_MTIME;
}
}
// 3. 检查是否需要更新权限
if (preserve_perms && !BITS_EQUAL(sxp->st.st_mode, new_mode, ...)) {
// 调用 do_chmod 来更新权限位
if (do_chmod(fname, new_mode) == 0) {
updated |= UPDATED_MODE;
}
}
return updated;
}
set_file_attrs
函数就像一个细心的工匠,在文件内容就位后,拿出工具箱(syscall.c
),按照图纸(file_struct
)上的要求,对文件的每一个细节进行精雕细琢,直到它与源文件完全一致。
特殊属性的处理
除了基本的权限和时间戳,rsync 对更复杂的元数据也有专门的处理模块。
硬链接 (-H
, --hard-links
)
- 是什么:想象一下,同一个文件在你的文件系统里有两个不同的名字,但它们指向的是完全相同的磁盘数据。删除其中一个名字,文件内容依然存在。这就是硬链接。
- 如何处理 :rsync 在
hlink.c
中处理硬链接。它通过比较文件的设备号(st_dev
)和 inode 号(st_ino
)来识别硬链接组。当它同步第一个文件时,会正常传输内容。当遇到同一个组的后续文件时,它不会再次传输内容,而是在目标位置调用do_link()
创建一个指向已同步文件的硬链接,既快速又节省空间。
ACLs 和扩展属性 (-A
, -X
)
- 是什么 :ACLs (Access Control Lists) 提供了比标准
rwx
更精细的权限控制。扩展属性 (Extended Attributes) 允许用户为文件附加任意的键值对元数据。它们就像是"标签上的附加说明"。 - 如何处理 :rsync 有专门的
acls.c
和xattrs.c
模块。如果开启了-A
或-X
选项,rsync 会:- 在发送端,调用专门的系统函数(如
sys_acl_get_file
,sys_llistxattr
)读取这些高级属性。 - 将它们序列化后,作为文件列表的一部分发送出去。
- 在接收端,由
set_acl()
或set_xattr()
函数负责调用相应的系统函数,将这些高级属性应用到目标文件上。
- 在发送端,调用专门的系统函数(如
c
// 文件: acls.c (简化逻辑)
// 设置文件的 ACL
int set_acl(const char *fname, const struct file_struct *file, stat_x *sxp, ...)
{
int changed = 0;
// ... 比较新旧 ACL 是否相同 ...
if (!eq) { // 如果不相同
changed = 1;
if (!dry_run && fname) {
// 调用一个更深层的函数来应用新的 ACL
set_rsync_acl(fname, duo_item, SMB_ACL_TYPE_ACCESS, ...);
}
}
return changed;
}
这个过程确保了即使是最复杂、最特殊的"标签",也能被 rsync 忠实地复制。
总结
在本章中,我们了解了 rsync 是如何处理文件内容之外的宝贵信息------元数据的。
- 核心理念:一次真正的"同步",意味着目标文件在所有方面------内容、权限、所有者、时间戳等------都应与源文件保持一致。
- 关键选项 :
-a
(归档模式)是实现这一目标最常用、最便捷的开关,它捆绑了多个保留属性的选项。 - 内部流程 :
- 发送端在构建文件列表时收集所有元数据。
- 接收端在文件同步完成后,通过
set_file_attrs
函数作为总指挥,调用syscall.c
中封装好的系统函数,将元数据逐一应用到目标文件上。
- 专业处理 :对于硬链接、ACLs 和扩展属性等特殊元数据,rsync 有
hlink.c
、acls.c
和xattrs.c
等专门模块来处理,确保了最精细级别的同步。
至此,我们已经探索了 rsync 的大部分核心部件:如何解析选项,如何实现增量算法,如何管理进程,如何构建和筛选文件列表,以及如何处理文件元数据。我们几乎了解了 rsync 单机或两台机器间同步的所有秘密。
但是,当 rsync 在客户端/服务器模式下通过网络工作时,这一切是如何被协调的呢?双方是如何对话,如何协商,如何交换数据的?在下一章,也是我们本系列的最后一站,我们将揭示 客户端/服务器通信协议的奥秘。