【免费下载链接】fsearch
Whole-disk file search for macOS: fuzzy names, typo tolerance, indexed content grep. ~1 ms over 8M files.
fsearch是一款面向 macOS 的全盘文件搜索工具:首次用约 20 秒建好全磁盘索引,之后按文件名找文件只要约 1 毫秒,还能容忍拼写错误、搜索文件内容。它的核心模块 src/walk.rs 里藏着一套组合拳——macOS 批量系统调用getattrlistbulk+ Rust 并行库rayon——一次调用带回几百个目录项、每个文件零 stat,让 800 万个文件的磁盘枚举成为可能。这篇文章带你拆开看它是怎么做到的。
为什么"逐个 stat"是 800 万文件的天堑
先看看最朴素的思路:拿到一个目录,读出里面的名字,再对每一个文件调用一次stat()获取类型、大小、修改时间……
| 传统做法 | 代价(770 万个条目) |
|---|---|
| 每个文件 1 次 stat | 约 770 万次系统调用 |
| 每次调用 ~2-5 μs(不含安全框架开销) | 光 syscall 就 20~40 秒 |
而且这只是"列目录"这一步,还没开始搜索。fsearch 的解法一句话概括:把"每个文件一次查询"改成"每次调用查一批发"。
getattrlistbulk:一次 syscall 带回几百个目录项 🚀
getattrlistbulk(2)是 macOS/APFS 原生的批量列目录系统调用:给它一个目录 fd,它一次性返回几百个条目,并且每个条目的名字、类型、大小、mtime、flags 都直接附在数据里——所以整个流程里根本不需要为任何文件单独发 stat。
fsearch 在 walk.rs 的list_fd中做了三件关键事:
- 精确点菜:先构造一个
attrlist位图(walk.rs#L165-L170),只索取名字、对象类型、修改时间、文件 flags、挂载状态、文件大小——内核只返回你要的字段; - 循环批量取:往一个线程私有的 256KB 缓冲区(walk.rs#L61-L63)里反复灌数据,直到系统调用返回 ≤ 0,期间逐条解析、直接填进内存结构 RawEnt;
- 零拷贝解析:返回的缓冲区本身就是"条目长度 + 条目数据"的序列,parse_entry 直接在原地按偏移读取,不产生中间字符串。
rayon 并行遍历:目录树如何"炸开"给 8 个工人
一个目录列完后,里面的子目录怎么分给多个线程?finish_dir 的实现非常直白:
- 列完当前目录后,把每个子目录包装成一个任务,丢给 rayon 的任务作用域
s.spawn(...)(walk.rs#L129-L136); - 子目录用
openat()相对父目录的 fd打开(walk.rs#L132),路径永远不用重建,PATH_MAX这类坑也绕开了; - 每个线程的结果写进自己的
Mutex<Vec<Listing>>槽位(walk.rs#L139-L142),避免 8 个线程抢同一把锁。
这实际上是一个"深度优先的工作队列":哪个工人空闲就处理下一个子目录,天然负载均衡,代码却不到 30 行。
8 个线程的甜点位,以及那些隐藏的工程细节
线程数不是拍脑袋定的。作者在 walk.rs#L104-L107 的注释里留下了这台 Mac 上的实测:打开+关闭一个目录约 19 μs(因为 Endpoint Security 客户端会对每次 open 征税),getattrlistbulk 约 14 μs,超过 ~8 线程后内核侧就不再加速了。所以 engine.rs 里写死了SCAN_THREADS: usize = 8。
细节同样讲究:
- fd 上限:raise_fd_limit 把进程 fd 上限抬到 65536,避免深层目录树撞穿
EMFILE; - 不越界:遇到挂载点打上
FLAG_MOUNT标记、不再深入(walk.rs#L224-L229),firmlink 则正常穿过,/Users在数据卷下恰好只出现一次; - 不下载 iCloud:每个线程启动时调用 no_materialize,让"无数据占位文件"快速失败,而不是触发几十 GB 的云端下载。
20 秒是怎么算出来的?📊
把上面的数字乘起来:一个目录 ≈ 一次openat(~19 μs)+ 若干次 getattrlistbulk(~14 μs 起)+ 一次close。8 个线程并行下,单个目录的有效成本约 0.2~0.5 ms,全盘 4~5 万个目录正好落在~20 秒。
建完索引后,/下的每个条目都被排进一个 mmap 的扁平文件 index.bin(按文件夹块深序排列,in:过滤因此只是一个范围边界),随后 full_build 把它落盘并重新映射。日常维护则交给 FSEvents 事件流(src/fsevents.rs):只有变化的目录会被单独重列、做幂等 diff(src/live.rs),重启也只回放增量,从不重扫全盘。
| 指标(M4 Max,7.7M 文件) | 数值 |
|---|---|
| 首次全盘扫描 | ~20 秒(一次性) |
| 按名找文件 | p501.3 ms |
| 文件内容搜索 | p50 9 ms |
| 新文件/改名/删除生效 | ~0.1 s |
| 守护进程内存 | 30–135 MB |
小结:零 stat 的三步组合拳
- 批量取:getattrlistbulk 一次带回几百个"自带元数据"的条目,消灭逐文件 stat;
- 并行分发:rayon 作用域把子目录当任务炸开,openat 相对 fd 避免路径重建,每线程独立输出槽避免锁竞争;
- 守住内核的脾气:8 线程甜点位、抬高 fd 上限、跳过挂载点、屏蔽 iCloud 占位文件。
想看它和 fff 的正面交锋,可以跑一下 demo/vs_fff_demo.py,配套对比数据在 demo/vs_fff_chromium.json 里。想自己动手改,核心就两个文件:src/walk.rs(遍历)和 src/index.rs(索引布局)。
【免费下载链接】fsearch
Whole-disk file search for macOS: fuzzy names, typo tolerance, indexed content grep. ~1 ms over 8M files.
相关推荐
fsearch架构揭秘:mmap、FSEvents与rayon如何协作让全盘搜索只要1毫秒
fsearch架构揭秘:mmap、FSEvents与rayon如何协作让全盘搜索只要1毫秒 fsearch 是一款专为 macOS 打造的全盘文件搜索工具:在
fsearch快速上手:5分钟构建安装,3步让800万文件秒搜(零基础教程)
fsearch快速上手:5分钟构建安装,3步让800万文件秒搜(零基础教程) fsearch 是一款专为 macOS 打造的全盘文件搜索工具,能在约 1 毫秒内
littlefs目录遍历实现:高效枚举嵌入式文件系统中的文件与目录
littlefs目录遍历实现:高效枚举嵌入式文件系统中的文件与目录 在资源受限的嵌入式环境中,高效的文件系统操作往往决定了设备的响应速度和可靠性。littlef
嵌入式存储系统编程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考