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

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

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

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

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

答案1

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

find /filesystem/mount/point -xdev -printf '%T@\t%p\0' > timestamps

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

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

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

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

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

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

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

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

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

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

LC_ALL=C locate -0 '/filesystem/mount/point/*' |
  find -files0-from - -prune -printf '%T@\t%p\0' > timestamps

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

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

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

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

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

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

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

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

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

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

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

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

zmodload zsh/stat || exit
for filepath (${(0)"$(<list)"})
  stat -LF %s.%9. -A timestamp +mtime -- $filepath &&
    something with $filepath and $timestamp

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

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

builtin ls || exit
while IFS= read -ru3 filepath; do
  timestamp=${ ls -dZ '%(mtime:%s.%N)s' -- "$filepath"; } &&
    something with "$filepath" and "$timestamp"
done 3< list

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


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

答案2

第一个想到的就是:

for file in `head -10000 files.txt`; do stat -c "%n %z $file; done

1m3.546s我第一次运行它时获取了10,000 个文件。随后的运行需要0m33.597s 0m22.127s0m25.038s0m19.810s0m25.246s

只是为了确保我不会在等等上浪费太多时间headforstat大约echo的饰面替换0.270s

find在单个文件上使用感觉很奇怪,但令人惊讶的是它运行得更快一些:

for file in `head -10000 files.txt`; do find $file -printf '%p %T+\n'; done;

在各种运行中以0m29.357s0m20.185s0m30.540s0m31.000s和完成。0m44.836s

然后是第三个选项:

for file in `head -10000 files.txt`; do echo "$file $(date -In -r $file)"; done;

各种运行的完成时间为0m25.828s, 0m12.649s, 0m23.695s, 0m12.789s, 0m43.782s, 0m28.396s, , 0m15.800s0m15.510s

显然,为了获得更明确的结果,我应该进行更多的运行,尝试不同的系统,但我感觉到,可能在这三个操作上花费的大部分时间都是相同的操作,并且它们会收敛到相当相似的数字,但是也就是说,date -r根据这个有限的样本,似乎确实要快一些。

相关内容