在 bash 中获取文件上最后修改的 TS 的最快方法是什么?

Jun*_*awa 0 linux bash stat

如果您在一个巨大的文件系统中拥有数十万个文件的列表,那么在 bash 中获取所有文件的最后修改时间的最快方法是什么?

假设没有办法对它们进行排序来提高速度,那么这个问题的核心实际上是:在 bash 中获取文件的上次修改时间的最快方法是什么?stat似乎是最常见的方法,但也有find(with -printf "%T+") 和date -r. 还有其他人吗?

(这取决于文件系统吗?)

Sté*_*las 5

如果您有 GNU find(支持 的 GNU -printf)。

\n
find /filesystem/mount/point -xdev -printf \'%T@\\t%p\\0\' > timestamps\n
Run Code Online (Sandbox Code Playgroud)\n

将是最快的。find经过高度优化,可以遍历目录树,然后lstat()系统调用自身来检索时间戳。它还将调用相对于它找到它们的目录的路径,这意味着与在完整路径上调用lstat()相比,内核要做的工作更少。lstat()

\n

使用它将时间戳打印为十进制纪元时间,它所要做的就是将数字(秒和纳秒)从二进制转换为十进制,这比计算用户时区中的日历时间要%T@少得多。%T+

\n

命令有许多不同且不兼容的实现stat,但它们都没有查找文件,它们只是执行一些//stat()或等效操作从路径作为参数给出的文件中检索元数据信息,因此您需要其他东西来查找文件并将它们的完整路径传递给.lstat()statx()statfs()stat

\n

因为在大多数系统上,命令只能采用有限数量的参数,这意味着您可能需要stat多次调用该实用程序,每次都在其自己的进程中,每次都必须加载、初始化、处理其参数等。

\n

一个例外是它stat的内置函数zsh确实早于 GNU 或 BSD stat(尽管不是 GNU find\'s -printf)。

\n

zsh可以通过递归 glob 找到文件,因此可以完成整个过程而无需运行另一个命令,但永远不会像find.

\n

请注意date -r(也是 GNU 非标准扩展)执行 astat()或等效操作,而不是lstat(). 因此,对于符号链接,它报告目标的时间戳(或者如果无法解析链接则失败),而不是符号链接的时间戳。在各种stat实现中,有些使用stat(),有些lstat()默认使用,但都可以告诉在两者之间切换。

\n

为了进一步优化它,您可以用 C 语言实现它,手动进行目录遍历,而不需要实现一些额外的保护措施find。在最新版本的 Linux 上,使用statx()it 可以被告知检索较少的信息可能会有所帮助。

\n

如果您有locate// mlocateplocate则使用其缓存的文件列表将使您无需爬行文件系统,并可能有助于加快该过程(冒着给您提供过时信息的风险)。

\n

从版本 4.9 开始,GNUfind可以使用 stdin 传递要处理的文件列表-files0-from -,因此您可以执行以下操作:

\n
LC_ALL=C locate -0 \'/filesystem/mount/point/*\' |\n  find -files0-from - -prune -printf \'%T@\\t%p\\0\' > timestamps\n
Run Code Online (Sandbox Code Playgroud)\n

这比使用类似的东西| xargs -r0 stat --printf \'%.9Y\\t%n\\0\' --(这里假设 GNUstat并且没有输入文件路径是-)更有效,它仍然会运行多次调用stat.

\n

如果您有一个文件路径列表作为 NUL 分隔记录存储在文件中,则可以使用相同的方法。如果采用其他格式,您需要先将其转换。例如,对于每行包含一个路径的文本文件(这意味着您不能存储包含换行符的文件路径),您可以这样做tr \'\\n\' \'\\0\' < list.txt | find...

\n

在我在这里的测试中,它仍然比让find自己查找文件效率低,可能是因为find最终调用lstat()完整路径,这意味着内核必须对每个文件进行完整查找。

\n

另请注意,它无法处理长于PATH_MAX(在 Linux 上通常约为 4KiB,请参阅 的输出getconf PATH_MAX /mount/point)的文件路径。

\n

无论如何,为了提高性能,您最不想做的就是为每个文件运行外部实用程序(例如 GNUdate或 GNU)stat,就像在 shell 循环中一样。如果由于某种原因,您需要在 shell 中循环处理文件及其时间戳(例如bash没有stat内置函数),您可以执行以下操作:

\n
while IFS=/ read -u3 -rd \'\' timestamp filepath; do\n  something with "$timestamp" and "$filepath"\ndone 3< <(find /filesystem/mount/point -xdev -printf \'%T@/%p\\0\')\n
Run Code Online (Sandbox Code Playgroud)\n

我们使用/分隔符,因为这是保证不会出现在文件路径末尾的唯一字符。一个例外是您传递到的目录find。例如,在 的输出中find / -xdev -printf \'%T@/%p\\0\',第一条记录(并且仅第一条记录)将以 结尾/。它将包含<timestamp>//, 并将read存储空字符串而不是/in $filepath。您可以通过使用zsh代替bash(其中$IFS真正被视为内部字段分隔符而不是分隔符)或${filepath:-/}在引用文件路径时使用来解决此问题。

\n

请注意,它read本身效率很低,因为它需要一次读取一个字节。请参阅为什么使用 shell 循环处理文本被认为是不好的做法?了解更多详情。如果性能是一个问题,那么您可能最好使用适当的编程语言。

\n

我知道,具有内置支持检索文件修改时间(并避免为每个文件运行单独实用程序的高昂成本)的 shell 是tcshzshksh93busybox sh

\n

tcsh并不能真正用于脚本编写。

\n

date对于 ksh93,您需要使用或内置函数来构建它,ls但这种情况很少见。对于busybox来说,虽然它的sh小程序可以调用它的stat小程序而无需重新执行自身,但它仍然在子进程中执行,并且分叉进程是相当昂贵的。Busybox stat(具有与 GNU 类似的 API stat)也不支持亚秒精度\xc2\xb9。此外,busybox shksh93不能处理 NUL 分隔的记录。

\n

对于包含 NUL 分隔文件路径的文件zshlist

\n
zmodload zsh/stat || exit\nfor filepath (${(0)"$(<list)"})\n  stat -LF %s.%9. -A timestamp +mtime -- $filepath &&\n    something with $filepath and $timestamp\n
Run Code Online (Sandbox Code Playgroud)\n

对于每行list包含一个(无换行符)文件路径的文件路径,请替换(0)(f).

\n

使用ksh93其内置函数lslist每行一个文件路径:

\n
builtin ls || exit\nwhile IFS= read -ru3 filepath; do\n  timestamp=${ ls -dZ \'%(mtime:%s.%N)s\' -- "$filepath"; } &&\n    something with "$filepath" and "$timestamp"\ndone 3< list\n
Run Code Online (Sandbox Code Playgroud)\n

您也可以builtin date; date -f %s.%N -m -- "$filepath"在那里使用,但要注意它执行的是 a stat()(就像传递-Lls),而不是lstat()

\n
\n

\xc2\xb9 它的date小程序可以在构建时配置为支持纳秒精度,尽管它在默认构建中未启用

\n