Linux ext4 在坏的备份/还原后恢复文件和目录访问权限

我在使用rsync备份我的个人目录时出了点问题(可能是因为我正在一个NTFS文件系统上进行备份):所有文件都在这里,但是所有文件和目录的访问权限都是777。我想知道是否有一个“神奇”的实用程序可以递归地更改:

  • 将目录从777更改为755。
  • 将普通文件从777更改为644。我家里没有很多可执行文件,所以稍后我可以手动处理。
  • 保持其他文件(链接、其他任何东西?)不变。

在shell中做这个很容易,但是需要花费几个小时...

附属问题:如何正确备份Linux目录层次结构到NTFS(使用rsync或其他方法)的建议?


那个目录有多大?为什么从shell中需要花费几个小时?你所要求的神奇工具正好就在shell中。 - Fanatique
Linux层次结构是什么?你是在说WSL吗?如果是的话,请注意除了文件权限之外,还有许多其他元数据可能需要关注,特别是如果目录位于LXFS而不是DrvFS上时。这些元数据包括每个目录的大小写敏感性等内容。 - user541686
附属问题:有没有关于如何在NTFS上正确备份Linux目录结构的建议(使用rsync或其他工具)。你应该将你的问题表述为一个问题,但是关于如何在NTFS上正确备份Linux目录结构的简短答案是不要在NTFS上备份Linux目录结构。NTFS是为Windows设计的,虽然它在技术上支持(大部分,如果不是全部)备份Linux文件所需的元数据,但是简单的复制或使用rsync不会使用维护这些元数据所需的特殊API。请使用pax,那就是它的用途所在。 - Jörg W Mittag
为了归档目的,使用"tar"来打包一个目录。这样做不会提供rsync的更新速度,但会保留所有的Unix属性。 - Thorbjørn Ravn Andersen
4个回答

标准推荐的解决方案很简单明了:
find . -type d -exec chmod 0755 "{}" \+
find . -type f -exec chmod 0644 "{}" \+

这将尽可能将尽可能多的文件名作为参数附加到单个命令中,直到达到系统的最大命令行长度。如果行的长度超过此限制,则会多次调用该命令。
如果您希望每个文件调用一次命令,您可以使用以下方法:
find . -type d -exec chmod 0755 "{}" \;
find . -type f -exec chmod 0644 "{}" \;

1实际上,我发现@Fanatique的方法更快,因为它涉及较少的chmod调用,这要归功于参数追加。通过将\;改为\+,您的答案可以得到改进,这也将启用参数追加。 - Deltik
@Deltik:我看到你的编辑了,但是我正在进行我的编辑的过程中。 - harrymc
5\;这种方式太慢了,与\+相比,我的基准测试已经运行了超过5分钟(参考此链接)。我建议在您的回答中删除\; - Deltik
@Deltik:在这种情况下,添加引用并没有改变任何东西。在两种情况下,find接收到的参数[..., "-exec", "chmod", "0755", "{}", ";"]都是一样的。在两种情况下,当它找到一个完全匹配"{}"的参数时,它会将文件名作为一个参数进行替换。唯一会有区别的情况是如果你使用了-exec sh -c "chmod 0755 {}" \;,也就是说它是一个子字符串替换。 - user1686
3@Deltik:我仍希望答案能够全面,但我已明确指出这是许多文件的首选方法。 - harrymc
@grawity:你说得对,这是我的错。 - Deltik
5"+"不是一个特殊的shell字符,所以不需要转义。 - R.. GitHub STOP HELPING ICE

chmod -R a=,u+rwX,go+rX $DIR 看起来运行得很好,而且无论怎么看,都很可能是最快的。

(我用 strace 进行了检查,它每个文件/目录只进行了一次 fchmodat() 系统调用 -- 对于文件是 644,对于目录是 755)。

关键在于 X 权限,在 man chmod 中有记录,它只对目录起作用,就是你想要的区别。

没有记录的是我的猜测,它们会按照指定的顺序应用,而不仅仅是随机顺序,但是通过几个变体的重复测试已经让我确信它们确实按照给定的顺序运行,所以我非常确定这种方式总是有效的。

我应该提到这是在 Linux 上,尽管对于 BSD 的 chmod 手册的粗略阅读表明它也应该在那里工作。


这种方式确实是最快的,正如我在我的回答中更新的基准测试所示。 - Deltik
更新:我找到了一种约40%更快的方法,它利用了任务并行性! - Deltik
关于并行性:这是在固态硬盘上还是一些非常快的磁盘上?基准工具运行后会执行sync操作以确保所有数据都写入磁盘吗? - user1003916
我在NVMe SSD上运行了这些测试,但是我在两台机器上(12)使用HDD也得到了类似的结果。sync似乎没有产生任何差异。不过,我并没有只在一台普通硬盘上运行这些测试,因为目前我没有只有硬盘的机器。更多信息 - Deltik

我对sitaram的回答, Peter Cordes的评论, Fanatique的回答harrymc的回答进行了基准测试,但这个答案是最快的方法
平均值:
Deltik的答案* - 7.480秒
* 感谢Peter Cordes提出并行性建议 sitaram的答案 - 12.962秒(比最佳答案慢73.275%) Peter Cordes的评论 - 14.414秒(比最佳答案慢92.685%) Fanatique的答案 - 14.570秒(比最佳答案慢94.772%) harrymc的更新答案 - 14.791秒(比最佳答案慢97.730%) harrymc的原始答案 - 1061.926秒(比最佳答案慢14096.113%)
完整的统计摘要:
Author              N      min     q1      median  q3      max     mean    stddev
------------------  --     ------- ------- ------- ------- ------- ------- --------
Deltik              10     7.121   7.3585  7.4615  7.558   8.005   7.4804  0.248965
sitaram             10     12.651  12.803  12.943  13.0685 13.586  12.9617 0.276589
Peter Cordes        10     14.096  14.2875 14.375  14.4495 15.101  14.4136 0.269732
Fanatique           10     14.219  14.512  14.5615 14.6525 14.892  14.5697 0.211788
harrymc (updated)   10     14.38   14.677  14.8595 14.9025 15.119  14.791  0.21817
harrymc (original)  1      1061.93 1061.93 1061.93 1061.93 1061.93 1061.93 N/A

Deltik的命令,以基准格式:

find "$(pwd)" -type d -print0 | xargs -0 -P4 chmod 755 & \
find "$(pwd)" -type f -print0 | xargs -0 -P4 chmod 644 & wait

sitaram的命令,以基准格式:

chmod -R a=,u+rwX,go+rX "$(pwd)"

Peter Cordes的命令,以基准格式:

find "$(pwd)" \( -type d -exec chmod 755 {} + \) \
           -o \( -type f -exec chmod 644 {} + \)

Fanatique的命令,以基准格式:

find "$(pwd)" -type d -print0 | xargs -0 chmod 755 ; \
find "$(pwd)" -type f -print0 | xargs -0 chmod 644

harrymc的更新后的命令,以基准格式:

find "$(pwd)" -type d -exec chmod 755 {} + ; \
find "$(pwd)" -type f -exec chmod 644 {} +

harrymc的原始命令,以基准格式:

找到"$(pwd)" -type d -exec chmod 755 {} \; ; \
找到"$(pwd)" -type f -exec chmod 644 {} \;

我的命令是最快的,感谢每个文件类型中四个并行的chmod进程。这样可以让多个CPU核心同时运行chmod,将瓶颈转移到了内核I/O线程或磁盘上。

sitaram的命令是第二名,因为所有操作都在chmod命令中完成。与其他答案相比,这大大减少了开销,因为:

  • 文件只需要被扫描一次(类似于执行一次find而不是两次),并且
  • 不需要创建子进程。

然而,这个命令的灵活性最差,因为它依赖于有关常规文件和目录之间可执行位含义的巧妙处理。

Peter Cordes的评论使用了一个find命令,避免了目录条目的双重查找。文件越多,这种改进就越显著。它仍然需要创建子chmod进程的开销,这就是为什么它比仅使用chmod的解决方案慢得多的原因。

在Fanatique的命令和harrymc更新的命令之间,find管道传递到xargsfind | xargs)的速度更快,因为结果流是异步处理的。而不是find暂停其查找行为进行-exec,找到的结果被发送给xargs以进行并发处理。
(空字节分隔符(find -print0 | xargs -0)似乎不会影响运行时间。)
由于每个文件和文件夹都需要一个新的chmod命令的开销,并且按顺序执行,所以harrymc的原始命令速度太慢。
在测试设置中,有1001个目录中包含了1000002个常规文件:
root@demo:~# echo {0..999} | xargs mkdir -p root@demo:~# find -type d -exec bash -c "cd {}; echo {0..999} | xargs touch" \; root@demo:~# find | wc -l 1001003 root@demo:~# find -type d | wc -l 1001 root@demo:~# find -type f | wc -l 1000002
我将所有的文件和文件夹权限设置为777,就像问题的初始条件一样。
然后,我对命令进行了十次基准测试,每次在运行测试之前都使用chmod -R 0777 "$(pwd)"恢复权限为777。
使用OUTPUT表示包含每个基准测试命令输出的文件,我使用以下方法计算平均时间:
bc <<< "scale=3; ($(grep real OUTPUT | grep -Po '(?<=m).*(?=s)' | xargs | sed 's/ /+/g'))/10"

Deltik答案的基准测试结果

root@demo:~# for i in {0..9} ; do chmod -R 0777 "$(pwd)" ; time { find "$(pwd)" -type d -print0 | xargs -0 -P4 chmod 755 & find "$(pwd)" -type f -print0 | xargs -0 -P4 chmod 644 & wait ; } ; done [1] 9791 [2] 9793 [1]- Done find "$(pwd)" -type d | xargs -P4 chmod 755 [2]+ Done find "$(pwd)" -type f | xargs -P4 chmod 644
real 0m7.634s user 0m2.536s sys 0m23.384s [1] 9906 [2] 9908 [1]- Done find "$(pwd)" -type d | xargs -P4 chmod 755 [2]+ Done find "$(pwd)" -type f | xargs -P4 chmod 644
real 0m7.443s user 0m2.636s sys 0m23.106s [1] 10021 [2] 10023 [1]- Done find "$(pwd)" -type d | xargs -P4 chmod 755 [2]+ Done find "$(pwd)" -type f | xargs -P4 chmod 644
real 0m8.005s user 0m2.672s sys 0m24.557s [1] 10136 [2] 10138 [1]- Done find "$(pwd)" -type d | xargs -P4 chmod 755 [2]+ Done find "$(pwd)" -type f | xargs -P4 chmod 644
real 0m7.480s user 0m2.541s sys 0m23.699s [1] 10251 [2] 10253 [1]- Done find "$(pwd)" -type d | xargs -P4 chmod 755 [2]+ Done find "$(pwd)" -type f | xargs -P4 chmod 644
real 0m7.397s user 0m2.558s sys 0m23.583s [1] 10366 [2] 10368 [1]- Done find "$(pwd)" -type d | xargs -P4 chmod 755 [2]+ Done find "$(pwd)" -type f | xargs -P4 chmod 644
real 0m7.482s user 0m2.601s sys 0m23.728s [1] 10481 [2] 10483 [1]- Done find "$(pwd)" -type d | xargs -P4 chmod 755 [2]+ Done find "$(pwd)" -type f | xargs -P4 chmod 644
real 0m7.679s user 0m2.749s sys 0m23.395s [1] 10596 [2] 10598 [1]- Done find "$(pwd)" -type d | xargs -P4 chmod 755 [2]+ Done find "$(pwd)" -type f | xargs -P4 chmod 644
real 0m7.243s user 0m2.583s sys 0m23.400s [1] 10729 [2] 10731 [1]- Done find "$(pwd)" -type d | xargs -P4 chmod 755 [2]+ Done find "$(pwd)" -type f | xargs -P4 chmod 644
real 0m7.320s user 0m2.640s sys 0m23.403s [1] 10844 [2] 10847 [1]- Done find "$(pwd)" -type d | xargs -P4 chmod 755 [2]+ Done find "$(pwd)" -type f | xargs -P4 chmod 644
real 0m7.121s user 0m2.490s sys 0m22.943s
平均时间:7.480秒
sitaram答案的基准测试结果
root@demo:~# for i in {0..9} ; do chmod -R 0777 "$(pwd)" ; time chmod -R a=,u+rwX,go+rX "$(pwd)" ; done 真实时间 0分12.860秒 用户时间 0分0.940秒 系统时间 0分11.725秒
真实时间 0分13.059秒 用户时间 0分0.896秒 系统时间 0分11.937秒
真实时间 0分12.819秒 用户时间 0分0.945秒 系统时间 0分11.706秒
真实时间 0分13.078秒 用户时间 0分0.855秒 系统时间 0分12.000秒
真实时间 0分12.653秒 用户时间 0分0.856秒 系统时间 0分11.667秒
真实时间 0分12.787秒 用户时间 0分0.820秒 系统时间 0分11.834秒
真实时间 0分12.651秒 用户时间 0分0.916秒 系统时间 0分11.578秒
真实时间 0分13.098秒 用户时间 0分0.939秒 系统时间 0分12.004秒
真实时间 0分13.586秒 用户时间 0分1.024秒 系统时间 0分12.372秒
真实时间 0分13.026秒 用户时间 0分0.976秒 系统时间 0分11.910秒
平均时间:12.962秒
Peter Cordes评论的基准结果
root@demo:~# for i in {0..9} ; do chmod -R 0777 "$(pwd)" ; time find "$(pwd)" \( -type d -exec chmod 755 {} + \) -o \( -type f -exec chmod 644 {} + \) ; done real 0m14.096s user 0m1.455s sys 0m12.456s
real 0m14.492s user 0m1.398s sys 0m12.897s
real 0m14.309s user 0m1.518s sys 0m12.576s
real 0m14.451s user 0m1.477s sys 0m12.776s
real 0m15.101s user 0m1.554s sys 0m13.378s
real 0m14.223s user 0m1.470s sys 0m12.560s
real 0m14.266s user 0m1.459s sys 0m12.609s
real 0m14.357s user 0m1.415s sys 0m12.733s
real 0m14.393s user 0m1.404s sys 0m12.830s
real 0m14.448s user 0m1.492s sys 0m12.717s
平均时间:14.414秒
Fanatique回答的基准测试结果
root@demo:~# for i in {0..9} ; do chmod -R 0777 "$(pwd)" ; time { find "$(pwd)" -type d -print0 | xargs -0 chmod 755 ; find "$(pwd)" -type f -print0 | xargs -0 chmod 644 ; } ; done real 0m14.561s user 0m1.991s sys 0m13.343s
real 0m14.521s user 0m1.958s sys 0m13.352s
real 0m14.696s user 0m1.967s sys 0m13.463s
real 0m14.562s user 0m1.875s sys 0m13.400s
real 0m14.609s user 0m1.841s sys 0m13.533s
real 0m14.892s user 0m2.050s sys 0m13.630s
real 0m14.291s user 0m1.885s sys 0m13.182s
real 0m14.843s user 0m2.066s sys 0m13.578s
real 0m14.219s user 0m1.837s sys 0m13.145s
real 0m14.503s user 0m1.803s sys 0m13.419s

平均时间:14.570秒

harrymc更新答案的基准测试结果

root@demo:~# for i in {0..9} ; do chmod -R 0777 "$(pwd)" ; time { find "$(pwd)" -type d -exec chmod 755 {} + ; find "$(pwd)" -type f -exec chmod 644 {} + ; } ; done 实际 0分14.975秒 用户 0分1.728秒 系统 0分13.050秒
实际 0分14.710秒 用户 0分1.586秒 系统 0分12.979秒
实际 0分14.644秒 用户 0分1.641秒 系统 0分12.872秒
实际 0分14.927秒 用户 0分1.706秒 系统 0分13.036秒
实际 0分14.867秒 用户 0分1.597秒 系统 0分13.086秒
实际 0分15.119秒 用户 0分1.666秒 系统 0分13.259秒
实际 0分14.878秒 用户 0分1.590秒 系统 0分13.098秒
实际 0分14.852秒 用户 0分1.681秒 系统 0分13.045秒
实际 0分14.380秒 用户 0分1.603秒 系统 0分12.663秒
实际 0分14.558秒 用户 0分1.514秒 系统 0分12.899秒

平均时间:14.791秒

harrymc原答案的基准测试结果

由于此命令非常缓慢,我只运行了一次基准测试。

root@demo:~# for i in {0..0} ; do chmod -R 0777 "$(pwd)" ; time { find "$(pwd)" -type d -exec chmod 755 {} \; ; find "$(pwd)" -type f -exec chmod 644 {} \; ; } ; done
 
real    17m41.926s
user    12m26.896s
sys     4m58.332s

耗时:1061.926秒


2+1:分析得很好,而且全面。 - harrymc
为什么是 "$(pwd)"?与仅使用 . 有什么区别吗? - JoL
1@JoL:我使用"$(pwd)"来模拟较长的路径在参数列表中。使用.会使性能稍微好一些,因为在超过最大参数长度之前可以容纳更多路径。 - Deltik
"$PWD" 是一个更简单的写法;你不需要调用命令并捕捉其输出,因为shell已经维护了一个具有该值的变量。 - Peter Cordes
find | xargs 就像 find -exec {} +(GNU 扩展)一样,而不是 find -exec {} \;(POSIX 标准,但每个 fork+exec 只传递一个文件名。为每个 chmod 系统调用启动一个完整的动态链接进程显然效率低下)。你所说的“这种方式更快”的表述并不需要那么模糊;这些工作原理已经被充分记录和广为人知(我认为)。xargs(1) 页 对于如何构建长命令的参数非常清楚;这也是它存在的主要原因之一。请参考第二段。 - Peter Cordes
所有的find答案都会遍历树两次,因为它们不费心使用find的能力。find \( -type d -exec chmod 755 {} + \) -o \( -type f -exec chmod 644 + \)应该可以工作。虽然我还没有尝试过,以确保当find遇到交替的文件和目录时,两个-exec操作会分别累积参数。顺便说一下,如果每个目录有很多文件,使用-execdir {} +可能是一个好主意,这样可以在一个命令行上允许更多的名称,并避免内核在每个chmod系统调用中遍历太多的目录组件。 - Peter Cordes
@PeterCordes:总体来说是很好的观点。(参考:1)我之前在处理一些会改变$PWD的脚本时,养成了使用"$(pwd)"的习惯。当然,对于测试来说,"$PWD"也完全可以。 (参考:2)@harrymc改为使用find -exec {} +,这也是我进行基准测试的方式,但是find | xargs仍然更快,并且我已经在我的回答的第6个修订版本中解释了原因。 (参考:3)我已经将您建议的命令添加到了基准测试中。 - Deltik
不错的更新,现在更清楚了在比较什么。现在我明白你是说xargs可能比find更好地进行批处理,或者find通过/bin/sh执行?也许有趣的是,在find -execfind | strace ... xargs上使用strace -f -eexecve来查看我们得到了多少个fork+execve调用。如果xargs正在构建更长的命令行,也许它知道在实践中现代Linux允许非常长的命令行(如兆字节),而find可能更保守。还有感谢你测试我的一个find想法 :) - Peter Cordes
1@PeterCordes:无论是find -exec还是find | xargs都会分割相同的次数。我认为find | xargs稍微快一些,因为这两个命令可以并行工作。使用find -exec时,文件搜索会在-exec命令运行时暂停。(这是我从strace输出中理解到的,但输出内容太大无法粘贴。) - Deltik
1哦,是的,使用并行处理可以解决这个问题。我曾经考虑过这一点,但却忘了提及 >.<。你也可以同时运行两个 find|xargs 命令,只需用 & 替代 ; 来分隔它们。它们可能不会长时间占用其他进程需要遍历的目录项锁,因此应该可以很好地重叠进行。 - Peter Cordes
1@PeterCordes: 太棒了的点子!通过使用Bash作业(&)和xargs -P4,速度提高了大约42.667%。我回答的第9个修订版添加了并行chmod命令的基准测试。 - Deltik
我对你们基准测试中使用的磁盘类型很感兴趣。在固态硬盘上,将磁盘访问并行化效果非常好,但是在传统旋转硬盘上,并不能有效地实现写入的并行化。 - Christopher Schultz
@ChristopherSchultz:答案中的基准测试是在一台使用了大约155GB的512GB容量的Samsung MZVPV512HDGL NVMe SSD上运行的,无论是已使用还是未修剪。并行命令在旋转硬盘上仍然是最快的。目前我没有只有一个HDD的机器。我拼凑了一个基准测试脚本,你可以用它来重现这些测试。脚本:基准测试脚本输出文件解析器。结果:HDD ZFS RAID-1 结果HDD 硬件 RAID-5 结果 - Deltik
仅靠并行处理是不够的。你还需要利用磁盘缓存(这样第一次读取就不必重新读取整个文件),并且输出周期要与两者重叠。此外,延迟写入可能是一个可调参数。 - Thorbjørn Ravn Andersen
我怀疑你的解决方案比harrymc的第二个更快的原因是,在你的解决方案中,这两个find命令会并行运行。这允许第二个find读取第一个find已经拉入文件系统缓存的目录信息。 - CSM
你应该考虑在你的解决方案中添加-print0,因为它可以处理文件/目录名称中带有空格的情况。 - CSM
你知道吗,你应该将并行性与我的解决方案结合起来,看看是否会有所不同。基本上就是 find ${pwd} | xargs -P4 chmod a=,u+rwX,go+rX - user1003916
@sitaram:这个表现几乎完全一样 - Deltik
@Deltik -- 很好了解。这也是我猜测的。就系统调用而言,你的方法将从find命令中产生2D+F个stat调用和D+F个chmod调用,不考虑forks。我的方法将产生D+F个stat调用和D+F个chmod调用。我猜想只有在目录数量(D)接近文件数量(F)或更大的情况下(比如说如果有成千上万个空目录),你才会看到差异。 - user1003916

如果目录太大并且包含太多文件,@harrymc所展示的原始方法将会失败。
如果你有太多的文件,你需要使用管道将`find`和`xargs`与`chmod`结合使用。
find /base/dir -type d -print0 | xargs -0 chmod 755 
find /base/dir -type f -print0 | xargs -0 chmod 644

2你的方法实际上比@harrymc的方法更快,但原因并不正确。原因是chmod的调用次数减少了,因为xargs会自动堆叠参数而不超过shell的限制。@harrymc的方法会对每个文件和目录都运行chmod命令。 - Deltik
我在考虑使用递归的方式来执行test -dtest -f,然后根据情况进行chmod操作。只是我没有考虑到使用find命令。感谢@Fanatique和@harrymc的提醒。 - mszmurlo
2为什么你认为harrymc的答案在处理过多文件时会失败?我认为这是一个不正确的说法。 - R.. GitHub STOP HELPING ICE

  • 相关问题