SSHFS源码剖析③:cache.c元数据缓存内幕——超时、失效策略与线程安全
【免费下载链接】sshfsA network filesystem client to connect to SSH servers项目地址: https://gitcode.com/gh_mirrors/sshfs1/sshfs
SSHFS 是一款基于 SFTP 的网络文件系统客户端,能把任意 SSH 服务器上的目录挂载到本地直接使用。这是 SSHFS 源码剖析系列的第三篇,聚焦元数据缓存模块 cache.c:默认 20 秒的缓存超时如何计时、写操作会触发哪些失效策略、以及一把互斥锁如何保证多线程下的正确性。
为什么 SSHFS 需要元数据缓存?🤔
每次ls、stat都需要通过 SSH 隧道向远程发一次 SFTP 请求,网络延迟会让"看看目录里有什么"都变得缓慢。SSHFS 的解法是:在 FUSE 与 SFTP 之间插入一层元数据缓存代理,把文件属性、目录列表、符号链接目标暂时存到内存里。
这个代理默认就是开启的(-o dir_cache=yes),挂载时在 sshfs.c 中通过cache_wrap把原始操作表整体"套壳":
if(sshfs.dir_cache) sshfs.op = cache_wrap(&sshfs_oper);也就是说:read、write等数据操作原样直通,而getattr、readdir、readlink等元数据操作被拦截,先查缓存、未命中才走网络。这套"代理 + 直通"的分流逻辑集中在 cache_fill 中完成。
缓存架构速览:一张哈希表 + 一把锁 📦
整个缓存模块的核心状态就是一个静态的 struct cache:
GHashTable *table:以文件路径字符串为键的哈希表pthread_mutex_t lock:保护哈希表的互斥锁- 三类超时 + 两个清理参数:下面详述
write_ctr:全局写计数器,是失效策略的关键
哈希表里每条记录是一个 struct node,同时承载三种元数据,各有独立的过期时间戳:
struct node { struct stat stat; /* 文件属性 */ time_t stat_valid; /* 属性过期时刻 */ char **dir; /* 目录项名称列表 */ time_t dir_valid; /* 目录列表过期时刻 */ char *link; /* 符号链接目标 */ time_t link_valid; /* 链接过期时刻 */ time_t valid; /* 三者中最晚的过期时刻 */ };对外只暴露了 5 个接口,见 cache.h:包装操作表、解析选项、写入属性、失效指定路径、获取写计数器——非常克制。
超时机制:默认 20 秒如何计时与过期 ⏱️
cache_parse_options 中设定了四组默认值:
#define DEFAULT_CACHE_TIMEOUT_SECS 20 #define DEFAULT_MAX_CACHE_SIZE 10000 #define DEFAULT_CACHE_CLEAN_INTERVAL_SECS 60 #define DEFAULT_MIN_CACHE_CLEAN_INTERVAL_SECS 5以属性查询为例,读路径是"先查后取":
- cache_get_attr 加锁查哈希表,若
stat_valid - now >= 0(未过期)直接返回缓存的stat; - 未命中或过期则返回
-EAGAIN; - 外层 cache_getattr 收到
-EAGAIN后真正向远程发起 SFTP 请求,成功后调 cache_add_attr 把结果写入缓存,stat_valid = 当前时间 + stat_timeout_secs。
readdir和readlink同理,分别使用dir_timeout与link_timeout。节点的整体有效期valid取三者最大值,供清理逻辑判断"这个节点还有没有价值"。
💡 一个巧妙的细节:cache_readdir命中缓存时只回填文件名(cache.c#L371-L379);而真正从远程读目录时,cache_dirfill 会顺手把每个子项的stat也缓存掉——一次ls -l就能预热一大片属性缓存。
失效策略:写操作后如何避免读到旧数据 ✅
"缓存过期"只保证最终一致,写入场景还需要主动失效,否则改名、删文件后可能继续看到旧数据。模块内有三档失效函数:
| 函数 | 动作 |
|---|---|
| cache_invalidate | 只删除该路径 |
| cache_invalidate_dir | 删除该路径+ 父目录(cache_purge_parent) |
| cache_invalidate_write | 删除该路径,并把全局write_ctr加一 |
各写操作对应哪种失效,规则很直观:
| 操作 | 失效范围 |
|---|---|
mkdir/mknod/create/unlink/rmdir | 该目录 + 父目录 |
chmod/chown/utimens/truncate | 仅该文件 |
write | 该文件 + 写计数器 +1 |
symlink/link | 目标目录(硬链接额外失效源文件) |
rename | 最彻底,见下 |
rename的 cache_do_rename 除了失效新旧路径和双方父目录,还会用前缀匹配把旧路径下的所有子孙条目一并清掉(cache_del_children),防止旧目录的子项在缓存里"幽灵残留"。
📌 关于write_ctr:它解决的是并发竞争——A 线程读到"写前计数器",B 线程写入并 +1,A 再把查到的属性写回缓存时若发现计数器已变,就放弃写入(见 cache_add_attr),避免用"写入发生前"的旧属性覆盖新状态。sshfs.c 的open路径也遵循同一协议:开文件前先取计数器,成功后回填属性、失败则失效该路径。
缓存清理:10000 条容量与 60 秒清理窗口 🧹
缓存条目只增会撑爆内存,因此 cache_clean 内置了"双门槛 + 双触发":
if (now > cache.last_cleaned + cache.min_clean_interval_secs && (g_hash_table_size(cache.table) > cache.max_size || now > cache.last_cleaned + cache.clean_interval_secs)) { g_hash_table_foreach_remove(cache.table, (GHRFunc) cache_clean_entry, &now); ... }翻译成人话:
- 触发条件(满足其一):条目数超过
max_size(默认 10000),或距上次清理超过clean_interval(默认 60 秒); - 限流保护:无论如何,两次清理间隔不能短于
min_clean_interval(默认 5 秒),防止频繁遍历哈希表; - 清理规则:逐个检查节点,
now > node->valid的整条删除。
注意它是惰性触发——没有后台线程,而是在每次写入缓存(add_attr/add_dir/add_link)时顺带调用,实现成本极低。
线程安全:单锁设计如何兼顾性能与正确性 🔒
FUSE 是多线程分发请求的,缓存模块的线程安全策略简单直接:一把互斥锁罩住整个哈希表,但把持锁区间压到最短:
- 读路径(如 cache_get_attr):锁内只做查表 + 拷贝
stat,拷完立即解锁,重网络请求发生在锁外; - cache_readlink 命中时,在锁内完成字符串拷贝后提前解锁返回;
- 所有失效/写入函数都遵循"加锁 → 纯哈希表操作 → 解锁"的短临界区模式。
这种粗粒度单锁看似原始,实则务实:缓存里存的都是轻量元数据(几十字节到几 KB 的字符串),临界区以微秒计,锁竞争远小于其带来的实现复杂度与出错风险——对一个"正确性优先"的挂载客户端来说,这是教科书式的取舍。
实战调参:常用 dcache 选项速查表 ⚙️
以下选项均可通过-o传给 sshfs(帮助文本见 sshfs.c,解析定义见 cache_opts,cache_*旧名仍兼容):
| 选项 | 默认值 | 作用 |
|---|---|---|
dir_cache=yes/no | yes | 整个元数据缓存总开关 |
dcache_timeout=N | 20 | 属性/目录/链接超时统一设置(秒) |
dcache_stat_timeout=N | 20 | 仅文件属性超时 |
dcache_dir_timeout=N | 20 | 仅目录列表超时 |
dcache_link_timeout=N | 20 | 仅符号链接超时 |
dcache_max_size=N | 10000 | 缓存最大条目数 |
dcache_clean_interval=N | 60 | 定时清理间隔(秒) |
dcache_min_clean_interval=N | 5 | 容量超限时的最小清理间隔(秒) |
🎯 调参建议:远端文件频繁变动、又在意一致性时,调小dcache_stat_timeout;目录巨大(上万文件)且基本只读时,可适当调大dcache_dir_timeout与dcache_max_size,让ls更快。
想跟读源码,可以拉取仓库:
git clone https://gitcode.com/gh_mirrors/sshfs1/sshfs重点阅读顺序推荐:cache.h 接口 → cache.c#L23-L48 数据结构 → cache.c#L526-L575 操作包装与初始化 → 各cache_*拦截函数。
小结
cache.c 用约 600 行代码,以"哈希表 + 单锁 + 三类超时 + 分级失效 + 惰性清理"五件套,为 SSHFS 这块网络文件系统补上了本地化体验的关键一环:
- ⏱️超时:属性、目录、链接各自 20 秒过期,节点整体有效期取最大值;
- ✅失效:按操作类型精确失效自身、父目录乃至整棵子树,配合
write_ctr防并发脏写; - 🧹清理:容量或时间双触发,最小间隔限流,无后台线程;
- 🔒线程安全:全局单锁 + 微秒级临界区,简单且可靠。
下一期我们将走进 sshfs.c 的多连接管理,看看-o max_conns如何让多核机器榨干 SSH 带宽。
【免费下载链接】sshfsA network filesystem client to connect to SSH servers项目地址: https://gitcode.com/gh_mirrors/sshfs1/sshfs
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考