本文翻译自http://unixetc.co.uk/2012/05/20/large-directory-causes-ls-to-hang/

当一个文件夹里面包含上百万的文件时,直接ls经常会卡死。

ls -1 -f则可以立即显示所有文件。如果我们要删除这个大文件夹里面的所有文件,我们应该用下面的命令来删除

ls -1 -f | xargs rm

当你删除了非常多不想要的文件时,你很有可能会导致生成一个非常大并且稀疏的文件夹对象。一个拥有300百万文件的文件夹,除了这些文件自身占用的空间外,这个文件夹对象可能会占用到100Mb的空间。

你可能会想到重建这个文件夹来释放这些空间。但是如果这个文件夹是/tmp目录,在单用户模式下我们处理的时候要非常小心。

为什么ls命令会卡住

如果直接使用ls,它默认会对输出结果进行排序。为了实现排序,它首先将每一个文件的文件名导入内存中。当在一个大文件夹里面使用ls命令时,你就要乖乖的等着它读取所有文件名最近按字典顺序输出,但是同时占用的内存也会越来越多。

举个栗子

下面是我们对一个包含300万文件的文件夹做的测试。这些文件的文件名是这样的:test_file_a_1, test_file_a_2…直到test_file_a_3000000。使用Perl脚本按照数字的顺序来创建这些文件。

ls -f -1用来列举开始的一部分文件,几乎是瞬间返回

bash-4.2$ time ls -1 -f | head
.
..
test_file_a_2531963
test_file_a_467778
test_file_a_2677947
test_file_a_329896
test_file_a_835701
test_file_a_1266060
test_file_a_261887
test_file_a_311007

real	0m0.006s
user	0m0.000s
sys	0m0.008s

接下来我们去除-1 -f 参数,则ls命令大概用了57s,比之前慢了约10000倍。

bash-4.2$ time /bin/ls | head
test_file_a_1
test_file_a_10
test_file_a_100
test_file_a_1000
test_file_a_10000
test_file_a_100000
test_file_a_1000000
test_file_a_1000001
test_file_a_1000002
test_file_a_1000003

real	0m57.880s
user	0m55.644s
sys	0m2.121s

不仅时间上非常慢,第二种方式的命令还会占用更多的内存。在打印文件名的阶段,ls命令会存储300万个文件名,最终将使用507Mb内存. 而ls -f -1命令使用的内存都没有超过4.5Mb,几乎少了100倍。

一种 ext3/ext4 bug?

遍历ext3/ext4文件系统占用太多的时间与内存通常会认为是文件系统的bug.但是从个人角度来说,我更认为是遍历软件的bug,而非是文件系统本身的bug.

排序,排序

不过上面的测试过程中也有一些比较奇特的事。ls命令总能正确的安排字典顺序输出所有文件名。但是ls -f -1命令输出的结果总是随机的。前三个文件分别是test_file_a_2531963, test_file_a_467778, test_file_a_2677947. 这些文件并没有按文件的创建时间,更新时间或者inode数目或者其它什么的排序。到底发生了什么呢?

这种情况主要是ext4文件系统本身导致的。ext4(和ext3)底层使用“Htree”的hash树的算法来存储所有文件名的,当使用这个算法时,他返回的文件名是随机的。所以,ls -f -1将会显示很原始的文件夹顺序,使的输出结果看来越来比较混乱和无序。 Ext2文件系统有时会和ext3和ext4一样,乱序返回文件名(比如在Fedora 16上),有时则不会(比如在Red Hat 4)上面,比如按当时创建的顺序返回所有文件名。

程序HANGING住了? Spy on it.

作为ls -1 -f命令的一种替代,或者有时这个命令不能用时,有时我们可以用strace(Linux)或者truss(Solaris)来监测一个程序并从中探听到一些有用的信息。我第一次看这种巧妙的方法的时候,是我看到我同事Neil Dixon使用来他来监测一个在非常大的/tmp目录下hang住的ls进程.在一个终端下 :

cd verybigdir
ls -l >/dev/null

让上面的命令一直跑着,然后我们获取ls进程id,然后从另外一个终端窗口来监视他,并显示出所有文件。

[[email protected] ~]# strace -p 3963 2>&1 | grep lstat
lstat64(“test_file_a_2433028”, {st_mode=S_IFREG|0664, st_size=0, …}) = 0
lstat64(“test_file_a_2047256”, {st_mode=S_IFREG|0664, st_size=0, …}) = 0
lstat64(“test_file_a_1201573”, {st_mode=S_IFREG|0664, st_size=0, …}) = 0

你就可以立马看到所有文件名了。如果你想删除他们,则可以用awk命令过滤出文件名并删除他们。

我认为这个方法可以用到探测其它程序。已经有人开始使用这种方法了,比如,比如检测一个备份程序,一个非常大的find/cpio job或者就是一个非常大的cp的具体的进度。