SSHFS源码剖析③:cache.c元数据缓存内幕——超时、失效策略与线程安全
2026/8/22 13:55:20 网站建设 项目流程

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 需要元数据缓存?🤔

每次lsstat都需要通过 SSH 隧道向远程发一次 SFTP 请求,网络延迟会让"看看目录里有什么"都变得缓慢。SSHFS 的解法是:在 FUSE 与 SFTP 之间插入一层元数据缓存代理,把文件属性、目录列表、符号链接目标暂时存到内存里。

这个代理默认就是开启的(-o dir_cache=yes),挂载时在 sshfs.c 中通过cache_wrap把原始操作表整体"套壳":

if(sshfs.dir_cache) sshfs.op = cache_wrap(&sshfs_oper);

也就是说:readwrite等数据操作原样直通,而getattrreaddirreadlink元数据操作被拦截,先查缓存、未命中才走网络。这套"代理 + 直通"的分流逻辑集中在 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

以属性查询为例,读路径是"先查后取":

  1. cache_get_attr 加锁查哈希表,若stat_valid - now >= 0(未过期)直接返回缓存的stat
  2. 未命中或过期则返回-EAGAIN
  3. 外层 cache_getattr 收到-EAGAIN后真正向远程发起 SFTP 请求,成功后调 cache_add_attr 把结果写入缓存,stat_valid = 当前时间 + stat_timeout_secs

readdirreadlink同理,分别使用dir_timeoutlink_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/noyes整个元数据缓存总开关
dcache_timeout=N20属性/目录/链接超时统一设置(秒)
dcache_stat_timeout=N20仅文件属性超时
dcache_dir_timeout=N20仅目录列表超时
dcache_link_timeout=N20仅符号链接超时
dcache_max_size=N10000缓存最大条目数
dcache_clean_interval=N60定时清理间隔(秒)
dcache_min_clean_interval=N5容量超限时的最小清理间隔(秒)

🎯 调参建议:远端文件频繁变动、又在意一致性时,调小dcache_stat_timeout;目录巨大(上万文件)且基本只读时,可适当调大dcache_dir_timeoutdcache_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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询