如果您在一个巨大的文件系统中拥有数十万个文件的列表,那么在 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
// mlocate
,plocate
则使用其缓存的文件列表将使您无需爬行文件系统,并可能有助于加快该过程(冒着给您提供过时信息的风险)。
从版本 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 是tcsh
、zsh
和ksh93
busybox sh
。
tcsh
并不能真正用于脚本编写。
date
对于 ksh93,您需要使用或内置函数来构建它,ls
但这种情况很少见。对于busybox来说,虽然它的sh
小程序可以调用它的stat
小程序而无需重新执行自身,但它仍然在子进程中执行,并且分叉进程是相当昂贵的。 Busybox stat
(具有与 GNU 类似的 API stat
)也不支持亚秒级精度。此外,busybox sh
也ksh93
不能处理 NUL 分隔的记录。
对于包含 NUL 分隔文件路径的文件zsh
:list
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
其内置函数ls
和list
每行一个文件路径:
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()
(就像传递-L
到ls
),而不是lstat()
。
¹ 它的date
小程序可以在构建时配置以支持纳秒精度,尽管它在默认构建中未启用
答案2
第一个想到的就是:
for file in `head -10000 files.txt`; do stat -c "%n %z $file; done
1m3.546s
我第一次运行它时获取了10,000 个文件。随后的运行需要0m33.597s
0m22.127s
、0m25.038s
、0m19.810s
和0m25.246s
只是为了确保我不会在等等上浪费太多时间head
,for
用stat
大约echo
的饰面替换0.270s
。
find
在单个文件上使用感觉很奇怪,但令人惊讶的是它运行得更快一些:
for file in `head -10000 files.txt`; do find $file -printf '%p %T+\n'; done;
在各种运行中以0m29.357s
、0m20.185s
、0m30.540s
、0m31.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.800s
。0m15.510s
显然,为了获得更明确的结果,我应该进行更多的运行,尝试不同的系统,但我感觉到,可能在这三个操作上花费的大部分时间都是相同的操作,并且它们会收敛到相当相似的数字,但是也就是说,date -r
根据这个有限的样本,似乎确实要快一些。