rsync源码解析 (6) 文件属性与元数据处理

在上一章 过滤规则系统 中,我们学会了如何像行李打包专家一样,精确地告诉 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 是如何实现这个精细的"贴标签"过程的呢?这个过程分为两步:在发送端收集信息 ,在接收端应用信息

  1. 收集信息 (发送端) :我们在 文件列表 (File List) 章节中已经知道,当 rsync 扫描源目录时,它会为每个文件创建一个 struct file_struct 结构体。这个结构体里就存放了通过 lstat() 系统调用获取到的所有元数据。

  2. 应用信息 (接收端) :这是本章的核心。当一个文件在目标位置被创建或更新后,接收端进程会执行一个关键函数 set_file_attrs,这个函数负责根据收到的 file_struct 中的信息,来设置新文件的各种属性。

整个应用过程可以用一个简单的时序图来描绘:

sequenceDiagram participant Receiver as 接收者进程 participant FileStruct as 文件元数据 (file_struct) participant OS as 操作系统 (系统调用) Receiver->>Receiver: 文件 "foo.txt" 同步完成 Receiver->>FileStruct: 读取 "foo.txt" 对应的元数据 FileStruct-->>Receiver: 权限: 0644, 所有者: userA, 时间: ... Note over Receiver,OS: 开始应用元数据... Receiver->>OS: do_chmod("foo.txt", 0644) OS-->>Receiver: 成功 Receiver->>OS: do_lchown("foo.txt", userA_id, groupA_id) OS-->>Receiver: 成功 Receiver->>OS: do_utimensat("foo.txt", ...) OS-->>Receiver: 成功

代码深潜:系统调用的封装

你可能注意到上图中,接收者调用的不是 chmodchown 等原生系统函数,而是 do_chmoddo_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_permspreserve_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.cxattrs.c 模块。如果开启了 -A-X 选项,rsync 会:
    1. 在发送端,调用专门的系统函数(如 sys_acl_get_file, sys_llistxattr)读取这些高级属性。
    2. 将它们序列化后,作为文件列表的一部分发送出去。
    3. 在接收端,由 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(归档模式)是实现这一目标最常用、最便捷的开关,它捆绑了多个保留属性的选项。
  • 内部流程
    1. 发送端在构建文件列表时收集所有元数据。
    2. 接收端在文件同步完成后,通过 set_file_attrs 函数作为总指挥,调用 syscall.c 中封装好的系统函数,将元数据逐一应用到目标文件上。
  • 专业处理 :对于硬链接、ACLs 和扩展属性等特殊元数据,rsync 有 hlink.cacls.cxattrs.c 等专门模块来处理,确保了最精细级别的同步。

至此,我们已经探索了 rsync 的大部分核心部件:如何解析选项,如何实现增量算法,如何管理进程,如何构建和筛选文件列表,以及如何处理文件元数据。我们几乎了解了 rsync 单机或两台机器间同步的所有秘密。

但是,当 rsync 在客户端/服务器模式下通过网络工作时,这一切是如何被协调的呢?双方是如何对话,如何协商,如何交换数据的?在下一章,也是我们本系列的最后一站,我们将揭示 客户端/服务器通信协议的奥秘。

相关推荐
重启的码农5 小时前
rsync源码解析 (3) 进程角色 (Sender/Receiver/Generator)
源码
重启的码农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) 选项与配置解析
源码