satgo1546’s ocean

Any sufficiently primitive magic is indistinguishable from technology.

Linux程序兼容性疑难解答(Everything与SpaceSniffer篇)

迁移到Linux后,我非常想念Everything

只要稍加一些想象力,Everything的用途可远远不止按文件名搜索。我用它做这些事:

这些是我现在能想起的用途。Everything既有速度又有实时性,让许多原本运行代价高昂的操作方式变为了可以日常使用的高效流程。只要问题能化归为按某些条件查找文件元数据,就总是能立即给出结果;只要搜索条件足够精确,就无需指定在何处扫描。

Everything的原理是读取NTFS MFT和USN日志,自行构建和更新文件信息数据库。

骗你的,真正的原理是外星科技

因为没有主流文件系统原生支持全盘搜索,所以,要做到瞬间完成搜索,自有数据库必不可少。Linux上,思路类似的工具我只找到locate系列和FSearch。然而,它们都为了搜索速度牺牲了实时性,要搜索新文件必须先等待建立索引。在我的电脑上,FSearch索引约200万个文件需要30秒。而且它们目前都较少有功能更新与维护。

要做到实时搜索,可以读取文件系统日志或利用操作系统提供的监听修改接口。Windows是这样的,Everything只要支持NTFS就行了,而Linux要考虑的事情就很多了。由于Linux支持的文件系统多样,读取文件系统日志的开发成本较高,我没有找到哪个软件走这条路线。而Linux提供的用户级监视功能inotify需要为每个目录分别建立监听,效率很低,且有上限,无法用于监视全盘。KDE的搜索功能为此还会特地提示用户增加上限。

我没有想到的是,时至今日,就算不建数据库,暴力扫盘也可以很快!

在我的电脑上,find搜索全盘需要十几秒,而fd-u(不要忽略隐藏文件等)硬扫全盘也只需要不到1秒

根本原因在于,新时代的硬盘已经不像以前那么慢了。在机械硬盘上如此之快,体现了Everything的实力;在固态硬盘上要达到同样的效率,只需要回归暴力。NVMe固态硬盘的顺序读取速度量级可达GB/s,随机读取性能也不差。更重要的是,如今的固态硬盘支持并行处理。

fd使用的递归遍历核心代码来自ripgrep作者BurntSushi开发的ignore crate中的WalkParallel对象。我写了个用它统计文件数量的小程序测试了一下,结果发现,只要当调包侠,就可以把遍历全盘的耗时压到2秒以内。

代码
use ignore::WalkBuilder;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};

fn main() {
    let count = Arc::new(AtomicUsize::new(0));
    WalkBuilder::new(".")
        .hidden(false)
        .build_parallel()
        .run(|| {
            let count = Arc::clone(&count);
            Box::new(move |entry| {
                if entry.is_ok() {
                    count.fetch_add(1, Ordering::Relaxed);
                }
                ignore::WalkState::Continue
            })
        });
    println!("{}", count.load(Ordering::Relaxed));
}

这当然不是Rust的专利,各种为并行提供良好支持的语言都能实现这种加速。Go语言写的fzf自带的文件名搜索依靠并行获得了基础速率提升,至少能打过find。gdu比功能相似的ncdu快十倍,同样是并行所赐。在我的电脑上,Windows上用SpaceSniffer扫盘的耗时是分钟级;ncdu扫全盘需要40秒,而gdu只需要4秒。我的硬盘在Windows上的时候塞得更满,与Linux比较不完全科学,但至少说明SpaceSniffer还有提速的空间。

虽然不停地暴力扫盘并不是解决实时性问题的真正方法,但已经足以应对大部分先查询后使用的场景了。

在GitHub上查看和发表评论