PHP 8.4.5中filemtime未按预期触发缓存,touch是否会自动更新stat缓存?
PHP 8.4.5中filemtime未按预期触发缓存,touch是否会自动更新stat缓存?
我最近在验证单文件clearstatcache的实现是否正确时,碰到了一个奇怪的问题——filemtime似乎完全没有触发缓存。原本预期在更新文件mtime后,只清除其中一个文件的stat缓存,两次filemtime调用会返回不同的值,但实际结果却是两个文件的返回值完全一致。这让我怀疑是不是touch会自动更新stat缓存?手动修改文件内容也没看到缓存生效,以下是我的测试细节:
环境配置
PHP Version 8.4.5 Core realpath_cache_size 4096K 4096K realpath_cache_ttl 120 120 Zend OPcache: Opcode Caching Up and Running
测试脚本
<?php /** * from phpinfo(): * * PHP Version 8.4.5 * * Core * realpath_cache_size 4096K 4096K * realpath_cache_ttl 120 120 * * Zend OPcache * Opcode Caching Up and Running * */ declare(strict_types=1); error_reporting(E_ALL | E_STRICT); $lines = []; $delimiter = '<br>' . PHP_EOL; $spacer = ''; $isCLI = in_array(php_sapi_name(), ['cli', 'cli-server']) || isset($_GET['cli']); $removeTestFiles = boolval($_GET['remove-test-files'] ?? 1); // round down for easier readability $currentTimestampRoundedDown = intval(time() / 1000) * 1000; // .php can be interchanged with .txt, just in case statcache problems have something todo with that. $pathFileToBeCleared = __DIR__ . '/to-be-cleared.php'; $pathFileUncleared = __DIR__ . '/uncleared.php'; $pathFileToBeClearedContent = '<?php return \'content from cleared file\';'; $pathFileUnclearedContent = '<?php return \'content from UN-cleared file\';'; // show initial settings $lines []= '<b>clearstatcache test</b>'; $lines []= '<span>pathFileToBeCleared: </span>' . $pathFileToBeCleared; $lines []= '<span>pathFileUncleared: </span>' . $pathFileUncleared; $lines []= '<span>current timestamp rounded down: </span>' . $currentTimestampRoundedDown; $lines []= $spacer; // create files and show initial mtime if(!file_exists($pathFileToBeCleared)) { file_put_contents($pathFileToBeCleared, $pathFileToBeClearedContent); } if(!file_exists($pathFileUncleared)) { file_put_contents($pathFileUncleared, $pathFileUnclearedContent); } $lines []= '<b>before touch</b>'; $lines []= '<span>filemtime from cleared: </span>' . filemtime($pathFileToBeCleared); $lines []= '<span>filemtime from UN-cleared: </span>' . filemtime($pathFileUncleared); $lines []= $spacer; // touch files and show update mtime (mtime should be cached and the same as initial mtime) touch($pathFileToBeCleared, $currentTimestampRoundedDown - 1000); touch($pathFileUncleared, $currentTimestampRoundedDown - 1000); // sleep(2); // file_put_contents($pathFileToBeCleared, $pathFileToBeClearedContent . '/**/'); // file_put_contents($pathFileUncleared, $pathFileUnclearedContent . '/**/'); $lines []= '<b>after touch</b>'; $lines []= '<span>filemtime from cleared: </span>' . filemtime($pathFileToBeCleared); $lines []= '<span>filemtime from UN-cleared: </span>' . filemtime($pathFileUncleared); $lines []= $spacer; // clear stat cache for one file only and show mtime again. (should show two different numbers) clearstatcache(true, $pathFileToBeCleared); $lines []= '<b>after clearstatcache on single file</b>'; $lines []= '<span>filemtime from cleared: </span>' . filemtime($pathFileToBeCleared); $lines []= '<span>filemtime from UN-cleared: </span>' . filemtime($pathFileUncleared); $lines []= $spacer; // show contents of files and use include if they have an .php extension $lines []= '<b>contents of files</b>'; $lines []= '<span>content from cleared: </span>' . (str_ends_with($pathFileToBeCleared, '.php') ? include($pathFileToBeCleared) : htmlentities(file_get_contents($pathFileToBeCleared)) ); $lines []= '<span>content from UN-cleared: </span>' . (str_ends_with($pathFileUncleared, '.php') ? include($pathFileUncleared) : htmlentities(file_get_contents($pathFileUncleared)) ); $lines []= $spacer; // remove test files if($removeTestFiles) { unlink($pathFileToBeCleared); unlink($pathFileUncleared); $lines []= 'test files removed'; $lines []= $spacer; } else { $lines []= 'keep test files'; $lines []= $spacer; } // show results if($isCLI) { echo strip_tags(implode($delimiter, $lines)); return; } echo '<style>body{font-family: \'Courier New\', Courier, monospace;background-color:#222;color:#3F0;}span{color:#F80;}b{color:#EEE;}</style>' . PHP_EOL; echo implode($delimiter, $lines); ?>
预期结果 vs 实际结果
- 预期:调用
clearstatcache(true, $pathFileToBeCleared)后,再次获取两个文件的mtime时,被清除缓存的文件返回更新后的mtime,未清除的文件返回缓存的旧值,二者结果不同。 - 实际:两次
filemtime调用返回的都是更新后的mtime,缓存似乎根本没起作用。
问题原因分析
后来我查了PHP底层逻辑才发现:PHP内置的文件修改函数(比如touch、file_put_contents、unlink等)在修改文件元数据或内容后,会自动清除该文件对应的stat缓存条目。
在我的测试流程中,调用touch更新两个文件的mtime时,PHP已经自动把这两个文件的旧stat缓存清理掉了。后续的filemtime调用都会直接从磁盘读取新的mtime,所以即使我只清除其中一个文件的缓存,另一个文件的缓存也早已不存在,最终两次调用返回的结果自然一致。
验证clearstatcache单文件功能的正确方式
要验证单文件clearstatcache的效果,需要绕开PHP的自动缓存清理逻辑——比如使用系统命令修改文件,而不是PHP内置函数。这样PHP无法感知到文件被修改,stat缓存会保留旧值。
修改测试脚本中的touch部分为系统命令调用:
// 替换原有的touch调用为系统命令 $timestamp = $currentTimestampRoundedDown - 1000; exec(sprintf('touch -d @%d %s', $timestamp, escapeshellarg($pathFileToBeCleared))); exec(sprintf('touch -d @%d %s', $timestamp, escapeshellarg($pathFileUncleared)));
调整后再运行脚本,就能看到预期结果:
- 被调用
clearstatcache的文件返回更新后的mtime; - 未被清除缓存的文件返回最初缓存的旧mtime;
- 二者结果明显不同,验证了单文件
clearstatcache的有效性。
额外说明
- realpath缓存 vs stat缓存:
realpath_cache缓存的是文件路径解析结果,和stat缓存(存储文件mtime、大小等元数据)是完全独立的两个机制,本次测试中realpath缓存的配置不影响stat缓存的行为。 - OPcache的影响:OPcache负责 opcode 缓存,和stat缓存没有直接关联,本次测试中OPcache的运行状态不是问题根源。
内容来源于stack exchange
相关产品推荐
相关产品推荐

