You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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的有效性。

额外说明

  1. realpath缓存 vs stat缓存:realpath_cache缓存的是文件路径解析结果,和stat缓存(存储文件mtime、大小等元数据)是完全独立的两个机制,本次测试中realpath缓存的配置不影响stat缓存的行为。
  2. OPcache的影响:OPcache负责 opcode 缓存,和stat缓存没有直接关联,本次测试中OPcache的运行状态不是问题根源。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.07 12:07:59