sccache 架构全解:哈希决策、Direct Mode 预处理器缓存与双执行模式剖析
2026/9/16 17:14:47 网站建设 项目流程

sccache 架构全解:哈希决策、Direct Mode 预处理器缓存与双执行模式剖析

【免费下载链接】sccacheSccache is a ccache-like tool. It is used as a compiler wrapper and avoids compilation when possible. Sccache has the capability to utilize caching in remote storage environments, including various cloud storage options, or alternatively, in local storage.项目地址: https://gitcode.com/GitHub_Trending/sc/sccache

sccache 是一个 ccache 风格的编译器缓存工具,通过包装编译器调用来复用缓存结果。本文以 docs/Architecture.md 为主线,结合仓库源码(src/commands.rs、src/protocol.rs、src/cache/ipc_storage.rs、src/compiler/preprocessor_cache.rs 等)展开,系统讲解 sccache 高层架构:一次编译调用如何通过哈希决定“直接编译”还是“复用缓存”、C/C++ 的 Direct Mode(预处理器缓存)如何跳过预处理,以及服务端模式与客户端模式(SCCACHE_CLIENT_SIDE)两种执行模式的分工与演进方向。读完本文,你将能理解 sccache 的缓存命中链路、两种模式的异同与配置方式,并能读懂其核心源码。

一、整体架构:一次编译调用的决策流程

sccache 的核心决策模型非常简单:把一次编译的所有关键输入折叠成一个哈希键,用该键去存储后端(本地磁盘、S3、Redis、Memcached、GCS 等)查询缓存;命中则直接下载并复用结果,未命中则真正执行编译并把产物上传回存储。

sccache 高层决策流程图

下面的流程图(源自 docs/Architecture.md 原文)直观地展示了这一决策过程:

1.1 哈希输入:什么会被折叠进缓存键

上图中的蓝色输入节点——环境变量、编译器二进制、编译器参数、文件内容——共同决定了缓存键的取值。任何一项发生变化都会导致哈希变化,从而产生缓存未命中。具体到语言实现,docs/Caching.md 给出了详细清单:

  • Rust 编译:对每个编译文件生成 blake3 摘要,同时把 rustc 可执行文件路径、Host triple、rustc sysroot 路径、$sysroot/lib下所有共享库的摘要、rlib 依赖(dist-client 场景)以及解析后的 rustc 参数一并纳入哈希。
  • C/C++ 编译:哈希基于预处理后文件(-E产物)的 blake3 摘要,并额外纳入编译器二进制哈希、汇编器二进制哈希与版本、编程语言、语言编译标志、依赖生成参数、预处理参数、架构参数、需要哈希内容的额外文件、覆盖率/剖析数据标志、颜色模式以及环境变量等。
  • C/C++ 预处理器(即下文 Direct Mode):在 C/C++ 编译器键的基础上,额外加入输入文件路径与输入文件内容哈希。

1.2 命中与未命中

  • 命中(Storage 返回 yes):从存储下载缓存的目标产物(object 文件),跳过编译,直接返回。
  • 未命中(Storage 返回 no):真正执行编译(Compile),随后把产物上传(Upload)回存储,供下次复用。

需要说明的是,上述缓存逻辑无论运行在哪种执行模式下都完全一致,区别只在于“由哪个进程实际运行编译器、谁与存储后端对话”——这正是本文第三部分要展开的两大执行模式。

二、Direct Mode:预处理器缓存(C/C++ 专属优化)

对于 C/C++,sccache 还提供一种额外的缓存:缓存预处理结果本身,从而让一次缓存查找可以完全跳过预处理步骤。这一设计灵感来自 ccache 的 direct mode,其细节记录在 docs/Local.md 中。

2.1 工作方式

在计算 object 缓存键之前,sccache 会先以一个“预处理器缓存条目”作为键去查询存储。该条目的键由源文件(路径 + 内容)预处理参数共同决定,条目中记录着源文件所包含(include)的每一个文件。逻辑如下:

  1. 若条目存在,且其中记录的所有被包含文件都未发生变化,则复用该条目,完全跳过预处理;
  2. 否则正常执行预处理,并写回一条新的条目。

该步骤默认开启(对应流程图中蓝色区域),但在以下场景会被跳过:

  • 非 C/C++ 语言;
  • 显式禁用(见下文的SCCACHE_DIRECT/use_preprocessor_cache_mode);
  • 命令行中存在-Wp,*-Xpreprocessor等标志(在 src/compiler/c.rs 中体现为too_hard_for_preprocessor_cache_mode判定)。

2.2 源码实现:PreprocessorCacheEntry

src/compiler/preprocessor_cache.rs 中定义了核心数据结构PreprocessorCacheEntry(src/compiler/preprocessor_cache.rs#L49-L59):

  • results: BTreeMap<String, Vec<IncludeEntry>>:按 object 缓存键(result key)组织的一组被包含文件清单。之所以允许一个源文件对应多个 result,是因为“头文件变了但源文件没变”时,旧条目不能立刻作废。
  • IncludeEntry(src/compiler/preprocessor_cache.rs#L446-L459)记录每个被包含文件的绝对路径、内容摘要(digest)、文件大小、mtime 与 ctime。

命中判定lookup_result_digest(src/compiler/preprocessor_cache.rs#L176-L191)会按“最新优先”顺序遍历 results,对每个条目调用result_matches:先比较文件大小,再根据配置比较 mtime/ctime,最后回退到内容摘要比对。源码还做了健壮性兜底:条目总数上限MAX_PREPROCESSOR_CACHE_ENTRIES = 100、被包含文件信息上限MAX_PREPROCESSOR_CACHE_FILE_INFO_ENTRIES = 10000(src/compiler/preprocessor_cache.rs#L45-L47),超限时直接清空重建,防止条目无限膨胀拖慢查找。

2.3 时间宏的特殊处理

预处理结果可能受到__DATE____TIME____TIMESTAMP__等时间宏的影响,因此 src/compiler/preprocessor_cache.rs 对它们做了专门处理:扫描文件时若发现__TIME__,直接禁用预处理器缓存模式(因为同一秒内的命中概率极低且结果会过期);若发现__DATE__/__TIMESTAMP__,则把日期/时间信息折叠进摘要,并考虑SOURCE_DATE_EPOCH环境变量的影响。相关测试见该文件末尾的test_find_time_macros_*系列用例。

2.4 相关配置项

docs/Local.md 中给出了预处理器缓存模式的可配置项:

  • use_preprocessor_cache_mode(默认true):是否启用预处理器缓存模式;单次调用可用环境变量SCCACHE_DIRECT(取true/on/1false/off/0)覆盖。
  • ignore_time_macros(默认false):为true时忽略源码中的__DATE____TIME____TIMESTAMP__,可加速缓存命中但可能产生过期结果。
  • skip_system_headers(默认false):为true时预处理器缓存只把系统头文件的路径计入缓存键而忽略其内容。

三、CLI 与守护进程:两种执行模式的分工

sccache 被拆分成两个进程(docs/Architecture.md):

  • 短生命周期的 CLI 进程:每次编译器调用(例如make -jN为每个编译任务)都会启动一个,负责解析命令行、与守护进程通信;
  • 长生命周期的守护进程(daemon):常驻后台,持有存储后端与累计统计信息。

两者通过本地 IPC 连接通信。连接地址默认端口为4226(常量DEFAULT_PORT,见 src/commands.rs#L51),可通过SCCACHE_SERVER_PORT环境变量覆盖;在 Unix 上还可通过SCCACHE_SERVER_UDS指定 Unix 域套接字(src/commands.rs#L57-L69)。当 CLI 尝试连接而端口无服务时,会通过connect_or_start_server(src/commands.rs#L315-L352)自动以SCCACHE_START_SERVER=1重新拉起守护进程,并等待其启动就绪(默认超时 10 秒,SERVER_STARTUP_TIMEOUT,src/commands.rs#L54)。

“谁跑编译、谁访问存储”有两种划分方式,即下文的服务端模式与客户端模式。两种模式共享同一套缓存决策逻辑,差异仅在进程职责。

四、服务端模式(默认)

服务端模式是当前默认的执行方式。CLI 把整个编译任务转发给守护进程:守护进程执行 Direct Mode 的预处理器缓存查找、计算哈希、查询 object 缓存,未命中时运行编译器并把结果写入存储。编译产物文件(如 object 文件)由守护进程直接写盘;回传给 CLI 的只有 stdout / stderr 输出流与退出码。

4.1 源码佐证:Compile 请求链路

在源码层面,CLI 侧通过do_compile(src/commands.rs#L633-L654)把解析后的命令包装成Request::Compile发给守护进程;守护进程端由handle_compile(src/server.rs#L1167-L1178)接收并执行完整的缓存决策流水线。协议层的数据结构定义在 src/protocol.rs:

  • Compile结构体(src/protocol.rs#L101-L110)承载编译器可执行文件路径、当前工作目录、命令行参数与环境变量;
  • CompileFinished(src/protocol.rs#L85-L97)携带编译进程的返回码(retcode)、终止信号(signal)、stdout、stderr 以及颜色模式(color_mode),这正是上图中“只有输出流与退出码回传 CLI”的实现载体。

值得注意的是,服务端模式下若守护进程在发送CompileStarted后、发送CompileFinished前意外断开,CLI 会回退到本地直接执行编译(见 src/commands.rs#L517-L569 及同文件末尾的test_handle_compile_response_disconnect_falls_back_to_local测试),保证构建不因守护进程崩溃而失败。

五、客户端模式(SCCACHE_CLIENT_SIDE

客户端模式下,编译流水线跑在 CLI 进程内部,守护进程只作为访问存储后端的“共享网关”(同时也是聚合统计信息的地方)。

5.1 执行流程

启动时 CLI 先做一次性的StorageHandshake,从守护进程拉取缓存元数据(缓存模式、最大尺寸、basedirs、预处理器缓存配置);随后在本地执行与守护进程完全相同的编译流水线(含 Direct Mode),并把每一次独立的缓存操作通过 IPC 逐条转发给守护进程:

5.2 关键机制:StorageGetPath 与原始字节回退

上图中StorageGetPath让 CLI 在存储后端暴露本地路径时直接读取磁盘上的缓存条目,省去一次网络/序列化开销;对于不暴露本地路径的后端(如 S3、Redis 等),CLI 回退为通过StorageGetRaw拉取原始字节。Direct Mode 的条目则通过StorageGetPreprocessorEntry/StoragePutPreprocessorEntry交换。

由于客户端模式下每个 CLI 进程各自累积统计信息,进程退出前会通过RecordStats把增量统计冲刷给守护进程合并。

5.3 源码佐证:IpcStorage 与协议

客户端模式的核心实现是IpcStorage(src/cache/ipc_storage.rs#L36-L68)。它实现了与所有存储后端相同的Storagetrait(src/cache/cache.rs#L75),因此编译流水线的其余部分无需任何改动——这正是该设计的关键巧妙之处:后端无关性通过 trait 抽象天然获得。

IpcStorage::get(src/cache/ipc_storage.rs#L72-L105)体现了完整的回退链:先尝试StorageGetPathGetPathResult::Found直接打开文件、Miss返回未命中、Unsupported回退),再回退StorageGetRaw拉取字节。而协议层的请求/响应定义在 src/protocol.rs#L10-L38 与 src/protocol.rs#L42-L71,StorageHandshakeInfo(src/protocol.rs#L113-L121)则完整携带了握手所需的缓存元数据。

守护进程端对这些存储 RPC 的分发逻辑在 src/server.rs#L904-L981:StorageHandshake从当前存储实例收集 location、cache_type_name、basedirs、预处理器缓存配置、缓存模式与最大尺寸;StorageGetPath/StorageGetRaw/StoragePutRaw/StorageGetPreprocessorEntry/StoragePutPreprocessorEntry逐一映射到对应存储方法;RecordStats将增量合并进守护进程的统计。

CLI 侧的入口分支位于 src/commands.rs#L916-L939:当config.client_side_mode为真时,创建仅含 2 个工作线程的 runtime(一个用于预处理/编译,一个用于 IPC),调用do_compile_client_side(src/commands.rs#L662-L716)在进程内构造IpcStorageSccacheService并直接执行compile_direct(src/server.rs#L1184-L1199),最后把take_stats得到的统计增量经RecordStats回传守护进程。

5.4 开启方式与互斥条件

客户端模式通过环境变量SCCACHE_CLIENT_SIDE(或配置文件中的client_side_mode键)启用,docs/Configuration.md#L195 明确其为推荐模式,并预期未来成为唯一受支持的配置,服务端模式最终会被移除。配置解析在 src/config.rs#L1279(环境变量)与 src/config.rs#L802(文件配置FileConfig字段)完成,环境变量优先级高于文件配置(src/config.rs#L1412-L1419)。

该设置目前与以下两项互斥,任一存在时设置会被忽略、sccache 回退到服务端模式:

互斥项原因
错误日志(SCCACHE_ERROR_LOG客户端模式下日志总是写到 stderr,多个并发的 CLI 进程会争用同一个日志文件产生写竞争(见 src/config.rs#L1413-L1417 注释);触发条件是设置了SCCACHE_LOG等日志环境变量
分布式编译(配置了 scheduler URL)客户端模式与分布式编译不兼容;触发条件是dist.scheduler_url已配置(src/config.rs#L1419)

5.5 两种执行模式对比

维度服务端模式(默认)客户端模式(SCCACHE_CLIENT_SIDE
编译执行位置守护进程CLI 进程
存储访问方式守护进程直接访问CLI 经IpcStorage逐条 RPC 转发
预处理器缓存查找守护进程执行CLI 本地执行,经StorageGet/PutPreprocessorEntry转发
统计信息守护进程统一维护CLI 各自累积,退出前RecordStats合并
SCCACHE_ERROR_LOG/ 分布式编译兼容互斥(存在则回退服务端模式)
演进方向计划移除未来唯一支持的配置

六、总结:一次调用的完整心智模型

把全文串起来,sccache 对sccache cc -c foo.c的处理可以归纳为:

  1. 进程拆分:短命 CLI + 常驻 daemon,本地 IPC 通信(默认端口 4226),daemon 未启动时自动拉起;
  2. Direct Mode 优先(C/C++ 且未禁用):以“源文件路径 + 内容 + 预处理参数”查预处理器缓存条目,命中且被包含文件未变则跳过预处理,直接得到 object 缓存键;
  3. object 缓存决策:以环境变量、编译器二进制、参数、文件内容(预处理产物)等折叠出的哈希查询存储,命中即下载复用,未命中则编译并上传;
  4. 执行模式:默认由 daemon 承担全部工作(服务端模式);启用SCCACHE_CLIENT_SIDE后编译流水线移入 CLI,daemon 退化为存储网关,IpcStorage通过统一的Storagetrait 保证流水线代码零改动。

如果希望进一步深入哈希生成细节与 Direct Mode 的本地实现,可继续阅读 docs/Caching.md 与 docs/Local.md;各存储后端(本地、S3、Redis、Memcached、GCS 等)的实现与配置则分别记录在 docs/S3.md、docs/Redis.md、docs/Memcached.md、docs/Gcs.md 等文档中,其后端模块均位于 src/cache 目录之下。

【免费下载链接】sccacheSccache is a ccache-like tool. It is used as a compiler wrapper and avoids compilation when possible. Sccache has the capability to utilize caching in remote storage environments, including various cloud storage options, or alternatively, in local storage.项目地址: https://gitcode.com/GitHub_Trending/sc/sccache

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

立即咨询