jdupes 四种去重方式详解:CoW、软链接、硬链接与删除怎么选

从文件系统语义、安全边界和验证方法出发,详解 jdupes -B、-l、-L、-d 四种重复文件处理方式,并说明如何避免误删和链接陷阱。

jdupes 找到重复文件以后,可以用四种完全不同的方式处理它们:

  • -B --dedupe:让重复文件共享底层数据块;
  • -l --link-soft:把重复副本替换为相对符号链接;
  • -L --link-hard:把重复副本替换为硬链接;
  • -d --delete:交互选择要保留的文件,并删除其余副本。

这四个选项都能减少重复数据,但不能简单理解为四种写法不同的“删除重复文件”。

它们改变的是不同层次的对象:数据块、inode、路径引用或目录项。

选择错误时,最麻烦的往往不是当场报错,而是几个月后某个程序修改、移动或删除文件,才发现其他路径也受到了影响。

本文从文件系统规格和行为出发,说明四种模式的真实区别、适用场景、限制条件和验证方法。

特别提醒:jdupes -d 是会删除文件的操作。不要把同一个目录重复写入命令行,也不要在没有充分理解符号链接遍历行为时把 -d-s--symlinks 一起使用。

先看结论:四种方式的核心区别

选项 处理结果 路径是否保留 inode 是否独立 后续修改是否相互影响 主要限制
-B --dedupe 共享相同物理数据块 全部保留 通常不影响,写入时 CoW 分离 文件系统必须支持去重或克隆接口
-l --link-soft 副本变为相对符号链接 路径名保留,但类型变为 symlink symlink 自有 inode,内容来自目标 修改链接指向的内容会修改目标文件 目标移动或删除后可能成为断链
-L --link-hard 多个路径指向同一 inode 全部保留 会影响,因为本质是同一个文件 不能跨文件系统,应用语义可能变化
-d --delete 删除未被选择保留的副本 只保留选中的路径 不适用 不会联动,但被删路径彻底消失 选择错误会直接丢失路径和元数据

如果只记住一句话,可以这样判断:

  • 希望各个文件仍然独立,且文件系统支持 CoW:优先考虑 -B
  • 明确需要多个文件名共同代表同一个文件:考虑 -L
  • 明确希望某些路径只是另一个文件的引用:考虑 -l
  • 确认多余路径没有保留价值:才使用 -d

jdupes 如何确认两个文件相同

处理方式虽然不同,但前面的重复文件识别流程相同。

按照 jdupes 官方手册,默认匹配过程依次包括:

  1. 比较文件大小;
  2. 比较部分文件哈希;
  3. 比较完整文件哈希;
  4. 最后进行逐字节比较。

只有通过这些阶段的文件,才会进入同一个重复文件集合。

因此,默认模式并不是“文件名相同就算重复”,也不是“哈希相同就立即执行操作”。

不带任何动作选项时,jdupes 默认只打印重复文件集合:

1
jdupes -r /srv/data

不同集合之间以空行分隔。

在真正使用 -B-l-L-d 以前,应当先运行一次只读扫描,确认路径范围和匹配结果符合预期。

不要为追求速度绕过最终校验

-Q --quick 会跳过逐字节确认,只依赖哈希结果。

官方手册明确把它标记为存在数据丢失风险。

-T --partial-only 的风险更高,因为它只依据文件开头的部分哈希进行匹配。

在执行删除、链接或 CoW 去重时,不应为了缩短扫描时间而随意加入这两个选项。

本文后续示例均使用默认的完整确认流程。

-B --dedupe:保留独立文件,共享数据块

基本命令如下:

1
jdupes -r -B /srv/data

-B 会请求文件系统对重复文件执行底层去重。

官方手册把这一过程称为 copy-on-write、CoW、cloning 或 reflink 去重。

从目录层看,原来的文件名都还在。

从 inode 层看,它们仍然是彼此独立的文件。

从数据块层看,相同内容可以引用同一组物理块,从而减少实际占用空间。

CoW 去重后的结构

假设有两个完全相同的文件:

1
2
/srv/data/a.iso -> inode 1001 -> 数据块 A、B、C
/srv/data/b.iso -> inode 2002 -> 数据块 A、B、C

两个路径对应不同 inode,但相同区段共享底层数据块。

如果以后修改 b.iso 的一部分,支持 CoW 的文件系统会为变化的区段分配新块:

1
2
/srv/data/a.iso -> inode 1001 -> 数据块 A、B、C
/srv/data/b.iso -> inode 2002 -> 数据块 A、X、C

未修改的区段仍可共享,发生写入的区段则分离。

这正是 -B 与硬链接最关键的区别。

为什么它适合仍可能被修改的文件

使用硬链接时,多个路径实际上是同一个 inode。

程序通过任一路径原地写入内容,其他路径看到的内容也会改变。

使用 CoW 去重时,文件仍然独立。

只要文件系统正确实现 CoW,修改一个文件不会把另一个文件同步改掉。

因此,以下场景通常更适合 -B

  • 多份虚拟机镜像;
  • 多个版本的备份目录;
  • 照片或视频素材副本;
  • 软件仓库中的重复安装包;
  • 内容相同但生命周期不同的文件。

-B 的文件系统要求

-B 不是 jdupes 自己重新压缩或搬运文件内容。

它依赖操作系统和文件系统提供的同区段去重或克隆接口。

官方手册列出的支持对象包括:

  • Btrfs;
  • 启用 reflink 特性的 XFS;
  • Apple APFS。

具体能否工作还取决于内核、挂载环境、jdupes 编译特性和文件系统格式化参数。

对于 XFS,仅仅看到文件系统类型是 XFS 还不够;文件系统创建时必须启用 reflink 能力。

可以先查看文件系统类型:

1
findmnt -T /srv/data

XFS 可进一步查看文件系统信息:

1
xfs_info /srv/data

不要仅凭命令没有明显报错,就认定所有重复数据都已经释放。

如何验证 CoW 去重结果

先确认文件仍然拥有不同 inode:

1
2
3
stat -c '%n inode=%i size=%s blocks=%b' \
  /srv/data/a.iso \
  /srv/data/b.iso

如果两个 inode 不同,这是预期现象。

stat 显示的块数不一定能直接、准确地表达共享区段。

不同文件系统对共享块统计方式可能不同。

还可以比较去重前后的文件系统可用空间:

1
df -h /srv/data

对于 Btrfs,可结合文件系统专用工具观察空间分配:

1
btrfs filesystem usage /srv/data

测试时应使用可丢弃的样本,分别修改其中一个文件,然后校验另一个文件的哈希没有变化。

-B 的局限

CoW 去重并不等于“零成本”。

共享区段需要文件系统维护引用关系。

后续写入会触发新块分配,并可能产生额外碎片。

对高频随机写入的数据、数据库文件或持续变化的虚拟磁盘,应先评估写入放大和碎片影响。

快照也可能让空间统计变得更复杂。

删除一个目录中的文件后,共享块若仍被另一个文件或快照引用,就不会立即释放。

如果你的重点是 Btrfs 和 NAS 场景,可继续参考站内的 Btrfs + jdupes CoW 去重实践

基本命令如下:

1
jdupes -r -L /srv/data

-L 会把每组重复文件中的其他副本替换为指向第一个文件 inode 的硬链接。

处理完成后,多个路径名称仍然存在,但它们不再是独立文件。

硬链接后的结构

处理前:

1
2
/srv/data/a.bin -> inode 1001 -> 数据块 A、B、C
/srv/data/b.bin -> inode 2002 -> 数据块 A、B、C

处理后:

1
2
3
/srv/data/a.bin --+
                 +-> inode 1001 -> 数据块 A、B、C
/srv/data/b.bin --+

此时 a.binb.bin 是同一个 inode 的两个目录项。

它们不只是“共享内容”,而是同一个文件拥有两个名字。

修改任一硬链接会发生什么

如果程序直接打开 b.bin 并覆盖其中的字节,a.bin 读取到的内容也会改变。

因为两条路径最终访问的是同一个 inode。

文件权限、所有者和时间戳等 inode 元数据同样不再彼此独立。

但有一个容易混淆的例外:

有些编辑器并不是原地修改文件,而是创建临时文件,再用重命名替换原路径。

这种“写新文件再替换”的保存方式会让被替换的路径获得新 inode,从而解除硬链接关系。

所以不能仅凭一次编辑测试,就推断所有应用程序都具有相同行为。

硬链接不能跨文件系统

硬链接只能在同一个文件系统内创建。

即使两个挂载点都使用 ext4,设备或文件系统实例不同,也不能跨越边界建立硬链接。

可以用下面的命令查看设备号和 inode:

1
2
3
stat -c '%n device=%d inode=%i links=%h' \
  /srv/data/a.bin \
  /srv/data/b.bin

处理成功后,两条路径的设备号和 inode 应当相同,链接计数通常会增加。

也可以使用:

1
ls -li /srv/data/a.bin /srv/data/b.bin

删除一个硬链接路径不会立即删除内容

删除 b.bin 只是移除其中一个目录项。

只要 a.bin 仍然指向该 inode,文件数据就仍然存在。

只有最后一个硬链接被删除,并且没有进程继续打开该文件时,文件系统才会回收相应对象和数据块。

这与符号链接的“目标路径消失后留下断链”完全不同。

什么时候适合使用 -L

硬链接适合这些前提明确的场景:

  • 文件位于同一个文件系统;
  • 多个路径在业务上确实可以视为同一个对象;
  • 文件预计只读或内容不会再被原地修改;
  • 备份、同步、索引和权限管理软件能够正确处理硬链接;
  • 不需要每个路径拥有独立的 inode 元数据。

典型例子包括只读软件包缓存、不可变归档副本和受控的媒体资源库。

什么时候不要使用 -L

以下情况应谨慎或避免使用:

  • 某个副本可能被应用原地修改;
  • 文件分别属于不同用户或权限策略;
  • 备份软件可能把硬链接展开成多份数据;
  • 同步软件可能不保留硬链接关系;
  • 应用依赖 inode 唯一性识别文件;
  • 文件跨越多个文件系统或网络挂载点。

如果两个路径现在内容相同,但未来职责不同,内容相同并不代表它们应该成为同一个 inode。

基本命令如下:

1
jdupes -r -l /srv/data

-l 会保留每组中的第一个文件,并把其他重复文件替换为指向它的相对符号链接。

假设处理前有:

1
2
/srv/data/original/report.pdf
/srv/data/archive/report-copy.pdf

处理后第二个路径可能表现为类似关系:

1
/srv/data/archive/report-copy.pdf -> ../original/report.pdf

实际相对路径由目录位置决定。

相对符号链接意味着什么

符号链接保存的是目标路径文本,而不是目标 inode 的额外名称。

相对链接的解析起点是符号链接所在目录,不是运行命令时的当前工作目录。

因此,整体移动一棵内部结构不变的目录树时,相对符号链接通常仍能工作。

但是,只移动链接或只移动目标,就可能让关系失效。

软链接与硬链接的行为差异

软链接可以跨文件系统。

硬链接通常不能跨文件系统。

软链接拥有自己的文件类型和 inode。

硬链接只是同一普通文件 inode 的另一个名称。

软链接目标被删除或改名后,链接可能变成断链。

删除一个硬链接名称时,只要还有其他硬链接,内容仍然可访问。

软链接可以指向目录;本文讨论的 jdupes 动作针对重复文件集合。

软链接会改变文件类型

执行 -l 以前,两个路径都是普通文件。

执行以后,其中一个仍是普通文件,其他路径变成符号链接。

这会影响某些应用:

  • 安全策略可能拒绝跟随符号链接;
  • 容器或沙箱可能看不到链接目标;
  • Web 服务可能禁止访问链接指向的路径;
  • 备份软件可能只备份链接本身;
  • 文件监控程序可能对链接和目标产生不同事件;
  • 权限检查最终会落在目标路径及其父目录上。

所以,“文件还能打开”并不等于应用兼容性已经验证。

如何验证软链接

查看链接保存的目标文本:

1
readlink /srv/data/archive/report-copy.pdf

解析最终目标:

1
realpath /srv/data/archive/report-copy.pdf

检查目录树中的断链:

1
find /srv/data -xtype l -print

find -xtype l 会找出最终目标无法解析的符号链接。

在正式使用前,还应测试移动目标文件、移动上级目录、备份恢复和应用读取等操作。

什么时候适合使用 -l

软链接适合“引用关系本来就合理”的场景:

  • 希望显式指定一个权威副本;
  • 其他路径本来就只是入口或兼容路径;
  • 需要跨文件系统引用;
  • 应用明确支持符号链接;
  • 能够保证目标路径的生命周期。

如果目录树经常被拆分、单独同步或分别打包,软链接通常不是理想选择。

-d --delete:选择保留项,删除其余副本

基本命令如下:

1
jdupes -r -d /srv/data

-d 会针对每组重复文件提示用户选择要保留的路径,然后删除其余路径。

这是四种方式里最直观的一种,也是最不可逆的一种。

-B-L-l 都会在一定形式上保留原来的路径入口。

-d 则真正移除未保留文件的目录项。

删除重复内容不等于没有信息损失

两个文件的内容完全相同,并不代表两条路径没有各自价值。

不同路径可能表达不同的:

  • 目录分类;
  • 文件名语义;
  • 所属项目;
  • 访问控制上下文;
  • 备份保留策略;
  • 应用索引关系;
  • 用户工作流程。

jdupes 判断的是文件内容是否重复,不会替你判断哪个目录项具有业务意义。

因此,删除前需要同时审查内容和路径。

最重要的风险一:不要重复指定同一个目录

不要这样运行:

1
jdupes -d /srv/data /srv/data

也要避免通过不同写法把同一棵目录树传入两次,例如:

1
2
cd /srv
jdupes -d data /srv/data

官方文档警告:当同一个目录被指定多次时,同一文件可能以“自己的重复项”形式出现在集合中。

如果用户在这种混乱的集合里保留了一个显示项,却删除了代表同一真实文件的另一个显示项,就可能造成数据丢失。

执行前应把所有输入路径规范化,并确认它们没有重复、别名或意外重叠。

可以先检查真实路径:

1
2
realpath /srv/data
realpath ./data

还要留意 bind mount、符号链接目录和容器映射是否让同一内容从多个入口被遍历。

最重要的风险二:谨慎组合 -d-s

-s--symlinks 会跟随符号链接目录。

当它与 -d 同时使用时,交互列表可能同时出现符号链接相关路径和被指向的真实文件。

官方手册指出,用户可能误保留符号链接,却删除它所指向的文件。

结果就是看似保留了一个路径,实际上只留下无法访问目标的链接。

除非已经完整绘制并核对符号链接关系,否则不要使用:

1
jdupes -r -s -d /srv/data

更稳妥的做法是先不跟随符号链接,分开检查真实目录和链接结构。

-N --no-prompt 会把删除变成自动操作

-N--delete 一起使用时,会自动保留每组的第一个文件并删除其余文件。

例如:

1
jdupes -r -d -N /srv/data

这不是普通的“跳过确认”,而是把“第一项是谁”直接变成保留策略。

若确实需要自动化,应先理解:

  • -o name 默认按文件名排序;
  • -o time 可按修改时间排序;
  • -i 会反转排序;
  • -O --param-order 会优先保留命令行参数顺序对结果集合的影响。

自动删除前,应在同样的路径、排序和递归选项下先执行只读扫描,并保存结果供审计。

对没有可靠备份的数据,不建议直接使用 -d -N

四种模式对权限和元数据的影响

默认情况下,jdupes 的核心目标是判断内容重复。

但内容相同的文件可能有不同的所有者、组或权限位。

-p --permissions 可以要求所有者、组和权限位不同的文件不要被视为重复项:

1
jdupes -r -p /srv/data

这对 -L 尤其重要。

硬链接后多个路径共享 inode 元数据,不可能继续保持各自不同的属主和权限位。

-d 来说,选择保留哪一个路径也会决定最终留下哪份文件元数据。

-l 来说,访问内容时最终受目标文件和路径遍历权限控制。

-B 来说,文件 inode 仍然独立,因此各自的权限和大部分元数据可以继续独立存在。

如果还依赖 ACL、扩展属性、SELinux 标签或文件能力,应使用对应工具额外核查。

仅比较传统权限位,并不代表所有扩展元数据都已纳入业务判断。

跨文件系统时怎么选

扫描多个挂载点时,可以加入:

1
jdupes -r -1 /srv/data /mnt/archive

-1 --one-file-system 会阻止跨文件系统或设备匹配。

这能减少后续动作遇到能力边界的概率。

四种模式在跨文件系统方面的差异如下:

  • -B 依赖文件系统接口和共享区段能力,通常需要目标位于兼容范围内;
  • -L 无法创建跨文件系统硬链接;
  • -l 可以保存跨文件系统的路径引用,但目标挂载缺失时链接不可用;
  • -d 可以删除不同文件系统上的副本,但必须确认保留项所在存储长期可用。

例如,把本地副本删掉,只保留可拔移动硬盘或临时网络挂载上的副本,技术上可能成功,业务上却非常危险。

一套稳妥的执行流程

不要直接把动作选项加到生产目录上。

建议按以下顺序执行。

第一步:确认备份或快照可恢复

快照存在不代表一定可恢复。

至少确认:

  • 快照覆盖本次扫描的所有目录;
  • 快照创建时间早于去重动作;
  • 快照不会与生产目录共享同一故障域;
  • 已知道恢复单个文件和整个目录的方法;
  • 恢复操作经过抽样测试。

硬链接和符号链接还需要确认备份工具是否保留链接语义。

第二步:确认输入路径没有重复

列出准备扫描的每一个目录:

1
2
realpath /srv/data/project-a
realpath /srv/data/project-b

检查它们是否:

  • 指向同一真实目录;
  • 一个目录完整包含另一个目录;
  • 通过符号链接再次进入同一目录树;
  • 通过 bind mount 或网络挂载出现别名;
  • 因 shell 通配符展开而重复。

尤其在使用 -d 时,任何不清楚的重叠都应先消除。

第三步:先做只读扫描

1
jdupes -r /srv/data/project-a /srv/data/project-b

把输出保存到审核文件:

1
2
jdupes -r /srv/data/project-a /srv/data/project-b \
  > /tmp/jdupes-review.txt

这里没有使用动作选项,因此只输出匹配集合。

逐组检查哪些路径应该保留独立语义,哪些只是冗余副本。

第四步:在小型测试目录验证行为

创建一个可丢弃的测试目录:

1
2
3
4
test_dir="$(mktemp -d)"
mkdir -p "$test_dir/a" "$test_dir/b"
printf 'jdupes test data\n' > "$test_dir/a/sample.txt"
cp "$test_dir/a/sample.txt" "$test_dir/b/sample.txt"

先查看 inode 和哈希:

1
2
stat -c '%n inode=%i links=%h' "$test_dir"/*/sample.txt
sha256sum "$test_dir"/*/sample.txt

然后每次只测试一种动作。

测试硬链接:

1
2
jdupes -L "$test_dir/a" "$test_dir/b"
stat -c '%n inode=%i links=%h' "$test_dir"/*/sample.txt

要测试其他动作,应重新创建干净样本,不要在已经硬链接的文件上继续叠加测试。

第五步:一次只执行一种动作

不要把四种处理模式堆在同一条命令里。

根据目录职责拆分批次,例如:

1
jdupes -r -B /srv/data/mutable-archive
1
jdupes -r -L /srv/data/immutable-cache
1
jdupes -r -l /srv/data/compat-links
1
jdupes -r -d /srv/data/manual-review

这样更容易解释结果、验证变化和执行恢复。

第六步:操作后验证

无论选择哪一种方式,都至少执行这些检查:

1
jdupes -r /srv/data
1
find /srv/data -xtype l -print
1
df -h /srv/data

再根据模式补充验证:

  • -B:确认 inode 独立,抽样修改一个副本后另一个不变;
  • -L:确认 inode 相同,链接计数正确;
  • -l:用 readlinkrealpath 确认目标,检查断链;
  • -d:根据审核清单确认该保留的路径仍存在。

最后让实际使用这些文件的应用执行一次读取、索引、备份或同步测试。

如何根据场景选择

NAS 媒体库和照片归档

如果底层是支持去重的 Btrfs,且文件可能被照片管理或媒体管理程序修改,优先评估 -B

它能保留独立文件语义,降低硬链接联动修改的风险。

如果目录完全只读,而且应用能正确处理硬链接,-L 也可能有效。

软件包缓存和构建产物

不可变构建产物位于同一文件系统时,-L 通常简单高效。

但构建工具若会原地改写缓存文件,就应避免硬链接。

能随时重新生成的缓存,也可以直接使用 -d,但仍要确认删除不会破坏索引。

多版本备份目录

多版本目录需要每个版本保持独立视图。

支持 CoW 时,-B 通常比 -L 更符合这种语义。

硬链接也常用于增量备份,但必须由备份程序系统性管理,不能只凭“当前内容相同”临时替换。

兼容旧路径

如果一个权威文件需要从多个旧路径访问,而且软件支持符号链接,可以选择 -l

这相当于明确告诉系统:其他路径只是引用。

目标路径必须稳定,并且备份、容器和权限策略都要覆盖它。

一次性清理下载目录

确认重复副本没有目录语义,且备份可用时,可以用交互式 -d

先只读扫描,再逐组选择保留项。

不要为了省几次按键就立刻加入 -N

常见误区

误区一:四种模式节省的空间完全一样

不一定。

-d-L-l 最终只保留一份普通文件内容,但仍有不同数量和类型的目录项或链接对象。

-B 的实际节省量取决于文件系统成功共享的区段、块大小、对齐、快照和后续写入。

误区二:硬链接是更可靠的软链接

两者语义不同,不能按“可靠程度”简单排序。

硬链接把多个名称绑定到同一 inode。

软链接保存一个可解析的目标路径。

前者没有“目标路径被改名就断链”的问题,后者则能跨文件系统并明确表达引用关系。

误区三:CoW 去重后两个文件就是硬链接

不是。

CoW 去重后的文件仍有独立 inode,只是共享部分或全部数据块。

硬链接则从 inode 层就是同一个文件。

ls -listat 可以观察这一差别。

误区四:内容相同就可以安全删除任意一个

内容相同只证明字节相同。

路径、文件名、权限、标签、业务归属和备份策略仍可能不同。

-d 的选择必须基于路径语义,而不只是文件内容。

误区五:相对软链接一定更便携

相对链接只有在链接和目标保持相对位置时才便携。

如果只复制其中一个子目录,链接可能立即失效。

打包、同步和容器挂载都可能改变原有目录关系。

误区六:删除后 df 一定立即增加可用空间

不一定。

文件可能仍被进程打开,也可能被快照、其他硬链接或 CoW 共享引用。

文件系统延迟回收和空间统计方式也会影响观察结果。

参考资料

最终选择建议

如果文件系统支持 CoW,而且希望文件以后还能独立修改,-B --dedupe 通常是四种方案中语义最温和的选择。

如果明确需要多个路径表示同一个不可变文件,并且都在同一文件系统,使用 -L --link-hard

如果需要建立清晰的路径引用关系,并能接受目标移动后断链的风险,使用 -l --link-soft

如果多余路径本身没有任何保留价值,而且已经有可靠恢复手段,再使用 -d --delete

无论选择哪一种,都应遵循同一原则:

先只读扫描,核对路径,测试应用行为,再执行动作,最后验证文件系统和业务结果。

-d 再强调一次:不要重复指定同一目录,也不要在没有完整核对符号链接关系时与 -s--symlinks 组合使用。

省下来的存储空间可以重新购买,丢失的目录语义和唯一副本却未必能够重建。