filectime() 函数是PHP中用于获取文件inode修改时间的内置函数。它返回文件inode最后被改变的时间戳,这包括文件权限、所有权、链接数等元数据的改变时间。
重要区别
注意:在Unix/Linux系统中,filectime() 返回的是inode修改时间(change time)。而在Windows系统中,它返回的是文件的创建时间(creation time)。这与 filemtime()(文件内容修改时间)和 fileatime()(文件访问时间)有本质区别。
语法
int filectime ( string $filename )
参数说明
| 参数 |
描述 |
类型 |
是否必需 |
| filename |
要检查的文件路径 |
string |
是 |
返回值
- 成功时返回文件inode修改时间的Unix时间戳
- 失败时返回 FALSE
注意事项
- 在Unix/Linux系统中,
filectime() 返回的是inode状态改变时间,不是文件创建时间
- 在Windows系统中,
filectime() 返回的是文件创建时间
- inode修改时间会在以下情况更新:
- 文件权限改变
- 文件所有者改变
- 文件链接数改变
- 文件内容修改(因为inode会记录修改时间)
- 使用
clearstatcache() 清除文件状态缓存以获得最新信息
- 对于不存在的文件或权限不足的文件,函数会返回FALSE
示例代码
示例1:基本使用 - 获取文件inode修改时间
<?php
$filename = 'test.txt';
// 获取文件的inode修改时间
$ctime = filectime($filename);
if ($ctime !== false) {
echo "文件inode修改时间戳: " . $ctime . "<br>";
echo "格式化时间: " . date('Y-m-d H:i:s', $ctime);
} else {
echo "无法获取文件inode修改时间";
}
?>
示例2:比较三种文件时间
<?php
function compareFileTimes($filename) {
if (!file_exists($filename)) {
return "文件不存在: {$filename}";
}
// 清除文件状态缓存
clearstatcache(true, $filename);
// 获取三种时间
$atime = fileatime($filename); // 最后访问时间
$mtime = filemtime($filename); // 最后修改时间(内容)
$ctime = filectime($filename); // inode修改时间
echo "<h4>文件: {$filename}</h4>";
echo "<table class='table table-bordered'>";
echo "<tr><th>时间类型</th><th>时间戳</th><th>格式化时间</th><th>描述</th></tr>";
echo "<tr><td>访问时间 (atime)</td><td>{$atime}</td><td>" . date('Y-m-d H:i:s', $atime) . "</td><td>文件最后被读取的时间</td></tr>";
echo "<tr><td>修改时间 (mtime)</td><td>{$mtime}</td><td>" . date('Y-m-d H:i:s', $mtime) . "</td><td>文件内容最后被修改的时间</td></tr>";
echo "<tr><td>改变时间 (ctime)</td><td>{$ctime}</td><td>" . date('Y-m-d H:i:s', $ctime) . "</td><td>文件inode最后被改变的时间</td></tr>";
echo "</table>";
// 分析时间关系
if ($ctime >= $mtime) {
echo "<p class='text-info'><i class='fas fa-info-circle'></i> ctime ≥ mtime 是正常的,因为内容修改会导致inode记录变化</p>";
}
}
// 使用示例
compareFileTimes('data.txt');
?>
示例3:检测文件权限或所有权变化
<?php
class FileChangeMonitor {
private $monitor_file = 'file_changes.json';
public function __construct() {
if (!file_exists($this->monitor_file)) {
file_put_contents($this->monitor_file, '{}');
}
}
public function monitorFile($filename) {
if (!file_exists($filename)) {
return ['error' => "文件不存在: {$filename}"];
}
clearstatcache(true, $filename);
// 获取当前状态
$current_state = [
'ctime' => filectime($filename),
'perms' => fileperms($filename),
'owner' => fileowner($filename),
'size' => filesize($filename),
'mtime' => filemtime($filename)
];
// 读取历史状态
$history = json_decode(file_get_contents($this->monitor_file), true);
if (!isset($history[$filename])) {
// 首次监控
$history[$filename] = [
'initial_state' => $current_state,
'last_check' => time(),
'changes' => []
];
$result = ['status' => '首次监控已建立'];
} else {
// 比较状态
$old_state = $history[$filename]['last_state'] ?? $history[$filename]['initial_state'];
$changes = [];
if ($current_state['ctime'] != $old_state['ctime']) {
$changes[] = 'inode修改时间变化 (可能权限、所有者或链接数改变)';
}
if ($current_state['perms'] != $old_state['perms']) {
$changes[] = '文件权限变化';
}
if ($current_state['owner'] != $old_state['owner']) {
$changes[] = '文件所有者变化';
}
if ($current_state['size'] != $old_state['size']) {
$changes[] = '文件大小变化';
}
if ($current_state['mtime'] != $old_state['mtime']) {
$changes[] = '文件内容修改';
}
if (!empty($changes)) {
// 记录变化
$change_record = [
'timestamp' => time(),
'changes' => $changes,
'old_state' => $old_state,
'new_state' => $current_state
];
$history[$filename]['changes'][] = $change_record;
$result = ['status' => '检测到变化', 'changes' => $changes];
} else {
$result = ['status' => '未检测到变化'];
}
$history[$filename]['last_state'] = $current_state;
$history[$filename]['last_check'] = time();
}
// 保存监控数据
file_put_contents($this->monitor_file, json_encode($history, JSON_PRETTY_PRINT));
return $result;
}
public function getChangeHistory($filename = null) {
$history = json_decode(file_get_contents($this->monitor_file), true);
if ($filename) {
return $history[$filename] ?? null;
}
return $history;
}
}
// 使用示例
$monitor = new FileChangeMonitor();
// 监控文件变化
$result = $monitor->monitorFile('important_config.php');
echo "<pre>";
print_r($result);
echo "</pre>";
// 查看历史记录
$history = $monitor->getChangeHistory('important_config.php');
if ($history) {
echo "<h5>监控历史:</h5>";
echo "首次监控时间: " . date('Y-m-d H:i:s', $history['initial_state']['ctime']) . "<br>";
echo "变化次数: " . count($history['changes']);
}
?>
示例4:检测文件是否被非法修改
<?php
class FileIntegrityGuard {
private $signature_file = 'file_signatures.dat';
private $signatures = [];
public function __construct() {
if (file_exists($this->signature_file)) {
$this->signatures = unserialize(file_get_contents($this->signature_file));
}
}
public function createSignature($filename) {
if (!file_exists($filename)) {
return false;
}
clearstatcache(true, $filename);
$signature = [
'filename' => $filename,
'ctime' => filectime($filename),
'mtime' => filemtime($filename),
'size' => filesize($filename),
'perms' => fileperms($filename),
'owner' => fileowner($filename),
'content_hash' => md5_file($filename),
'created_at' => time()
];
$this->signatures[$filename] = $signature;
$this->saveSignatures();
return $signature;
}
public function verifySignature($filename) {
if (!isset($this->signatures[$filename])) {
return ['status' => '未找到签名记录'];
}
if (!file_exists($filename)) {
return ['status' => '文件已删除'];
}
clearstatcache(true, $filename);
$original = $this->signatures[$filename];
$current = [
'ctime' => filectime($filename),
'mtime' => filemtime($filename),
'size' => filesize($filename),
'perms' => fileperms($filename),
'owner' => fileowner($filename),
'content_hash' => md5_file($filename)
];
$issues = [];
if ($current['ctime'] != $original['ctime']) {
$issues[] = [
'type' => 'inode修改',
'detail' => '文件inode已被修改(权限、所有者或链接数改变)',
'original' => date('Y-m-d H:i:s', $original['ctime']),
'current' => date('Y-m-d H:i:s', $current['ctime'])
];
}
if ($current['mtime'] != $original['mtime']) {
$issues[] = [
'type' => '内容修改',
'detail' => '文件内容已被修改',
'original' => date('Y-m-d H:i:s', $original['mtime']),
'current' => date('Y-m-d H:i:s', $current['mtime'])
];
}
if ($current['content_hash'] != $original['content_hash']) {
$issues[] = [
'type' => '内容哈希不匹配',
'detail' => '文件内容已被篡改',
'original_hash' => $original['content_hash'],
'current_hash' => $current['content_hash']
];
}
if ($current['perms'] != $original['perms']) {
$issues[] = [
'type' => '权限修改',
'detail' => '文件权限已被修改',
'original_perms' => substr(sprintf('%o', $original['perms']), -4),
'current_perms' => substr(sprintf('%o', $current['perms']), -4)
];
}
if (empty($issues)) {
return [
'status' => '完整',
'message' => '文件未被修改'
];
} else {
return [
'status' => '篡改检测',
'issues' => $issues,
'message' => '检测到 ' . count($issues) . ' 处修改'
];
}
}
private function saveSignatures() {
file_put_contents($this->signature_file, serialize($this->signatures));
}
}
// 使用示例
$guard = new FileIntegrityGuard();
// 为重要文件创建签名
$signature = $guard->createSignature('config.php');
echo "已为 config.php 创建签名<br>";
// 验证文件完整性
$result = $guard->verifySignature('config.php');
echo "<pre>";
print_r($result);
echo "</pre>";
?>
示例5:文件系统审计工具
<?php
class FileSystemAuditor {
private $audit_log = 'filesystem_audit.log';
public function auditDirectory($directory, $recursive = true) {
if (!is_dir($directory)) {
return ['error' => "目录不存在: {$directory}"];
}
$audit_results = [
'directory' => $directory,
'audit_time' => time(),
'files' => [],
'statistics' => [
'total_files' => 0,
'recently_changed' => 0,
'permission_issues' => 0,
'ownership_issues' => 0
]
];
$iterator = $recursive ?
new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory)) :
new DirectoryIterator($directory);
foreach ($iterator as $file) {
if ($file->isDot()) {
continue;
}
if ($file->isFile()) {
$filepath = $file->getPathname();
clearstatcache(true, $filepath);
$file_info = [
'name' => $file->getFilename(),
'path' => $filepath,
'size' => $file->getSize(),
'ctime' => filectime($filepath),
'mtime' => filemtime($filepath),
'atime' => fileatime($filepath),
'perms' => fileperms($filepath),
'owner' => fileowner($filepath),
'group' => filegroup($filepath)
];
// 格式化权限
$file_info['perms_octal'] = substr(sprintf('%o', $file_info['perms']), -4);
$file_info['perms_human'] = $this->formatPermissions($file_info['perms']);
// 检查安全问题
$file_info['issues'] = $this->checkSecurityIssues($file_info);
// 统计
$audit_results['statistics']['total_files']++;
if (!empty($file_info['issues'])) {
$audit_results['statistics']['permission_issues']++;
}
// 检查最近修改的文件(24小时内)
if ((time() - $file_info['ctime']) < 86400) {
$audit_results['statistics']['recently_changed']++;
}
$audit_results['files'][] = $file_info;
}
}
// 记录审计日志
$this->logAudit($audit_results);
return $audit_results;
}
private function checkSecurityIssues($file_info) {
$issues = [];
// 检查权限是否过宽
$perms = $file_info['perms_octal'];
if (in_array($perms, ['0777', '0775', '0666'])) {
$issues[] = '权限过宽: ' . $perms;
}
// 检查是否有设置用户ID位
if (($file_info['perms'] & 0x800) != 0) {
$issues[] = '设置了setuid位';
}
// 检查是否有设置组ID位
if (($file_info['perms'] & 0x400) != 0) {
$issues[] = '设置了setgid位';
}
// 检查粘滞位
if (($file_info['perms'] & 0x200) != 0) {
$issues[] = '设置了粘滞位';
}
return $issues;
}
private function formatPermissions($perms) {
$symbolic = '';
// 文件类型
$symbolic .= (($perms & 0xC000) == 0xC000) ? 's' : // socket
(($perms & 0xA000) == 0xA000) ? 'l' : // 符号链接
(($perms & 0x8000) == 0x8000) ? '-' : // 普通文件
(($perms & 0x6000) == 0x6000) ? 'b' : // 块设备
(($perms & 0x4000) == 0x4000) ? 'd' : // 目录
(($perms & 0x2000) == 0x2000) ? 'c' : // 字符设备
(($perms & 0x1000) == 0x1000) ? 'p' : // FIFO
'u'; // 未知
// 所有者权限
$symbolic .= (($perms & 0x0100) ? 'r' : '-');
$symbolic .= (($perms & 0x0080) ? 'w' : '-');
$symbolic .= (($perms & 0x0040) ?
(($perms & 0x0800) ? 's' : 'x') :
(($perms & 0x0800) ? 'S' : '-'));
// 组权限
$symbolic .= (($perms & 0x0020) ? 'r' : '-');
$symbolic .= (($perms & 0x0010) ? 'w' : '-');
$symbolic .= (($perms & 0x0008) ?
(($perms & 0x0400) ? 's' : 'x') :
(($perms & 0x0400) ? 'S' : '-'));
// 其他用户权限
$symbolic .= (($perms & 0x0004) ? 'r' : '-');
$symbolic .= (($perms & 0x0002) ? 'w' : '-');
$symbolic .= (($perms & 0x0001) ?
(($perms & 0x0200) ? 't' : 'x') :
(($perms & 0x0200) ? 'T' : '-'));
return $symbolic;
}
private function logAudit($audit_results) {
$log_entry = sprintf(
"[%s] 审计目录: %s, 文件总数: %d, 安全问题: %d\n",
date('Y-m-d H:i:s'),
$audit_results['directory'],
$audit_results['statistics']['total_files'],
$audit_results['statistics']['permission_issues']
);
file_put_contents($this->audit_log, $log_entry, FILE_APPEND);
}
}
// 使用示例
$auditor = new FileSystemAuditor();
$result = $auditor->auditDirectory('/var/www/html', true);
echo "<h4>文件系统审计结果:</h4>";
echo "审计目录: " . $result['directory'] . "<br>";
echo "文件总数: " . $result['statistics']['total_files'] . "<br>";
echo "最近修改的文件: " . $result['statistics']['recently_changed'] . "<br>";
echo "权限问题: " . $result['statistics']['permission_issues'] . "<br>";
// 显示前5个文件的详细信息
echo "<h5>文件示例:</h5>";
for ($i = 0; $i < min(5, count($result['files'])); $i++) {
$file = $result['files'][$i];
echo "文件: {$file['name']}, 权限: {$file['perms_human']}, 大小: {$file['size']} 字节<br>";
echo "ctime: " . date('Y-m-d H:i:s', $file['ctime']) . "<br>";
if (!empty($file['issues'])) {
echo "问题: " . implode(', ', $file['issues']) . "<br>";
}
echo "<hr>";
}
?>
与类似函数的比较
| 函数 |
描述 |
返回内容 |
适用场景 |
filectime() |
获取文件inode修改时间 |
Unix时间戳 |
检测文件权限、所有权等元数据变化 |
filemtime() |
获取文件内容修改时间 |
Unix时间戳 |
检测文件内容变化、版本控制 |
fileatime() |
获取文件最后访问时间 |
Unix时间戳 |
监控文件访问频率、缓存管理 |
stat() |
获取文件所有状态信息 |
包含ctime、mtime、atime的数组 |
需要完整文件信息的场景 |
lstat() |
获取文件或符号链接状态 |
数组(与stat类似) |
处理符号链接的场景 |
不同操作系统差异
跨平台注意事项
| 操作系统 |
filectime() 含义 |
注意事项 |
| Unix/Linux |
inode修改时间(change time) |
权限、所有权、链接数等元数据变化时更新 |
| Windows |
文件创建时间(creation time) |
文件首次创建的时间,通常不会改变 |
| macOS |
inode修改时间 |
与Unix/Linux行为一致 |
性能优化建议
最佳实践
- 对于频繁查询的文件时间,使用缓存机制避免重复调用
- 在处理多个文件时,使用
clearstatcache()确保获取最新信息
- 注意跨平台差异,编写可移植代码时考虑操作系统区别
- 使用绝对路径而不是相对路径,避免路径解析开销
- 结合
filemtime()和fileatime()进行综合文件状态分析
- 对于安全敏感的应用,考虑使用
stat()一次性获取所有文件状态
安全注意事项
安全警告
- 权限检查: 调用
filectime()前确保文件存在且有读取权限
- 时间篡改: 注意系统时间可能被篡改,影响时间戳的可靠性
- 竞态条件: 在多进程/线程环境中,文件状态可能在检查和使用之间改变
- 符号链接: 使用
lstat()检查符号链接本身,而不是其目标
- 敏感文件: 审计系统文件时注意不要泄露敏感信息
完整示例:文件变更监控系统
<?php
class FileChangeDetectionSystem {
private $storage_file = 'file_changes.json';
private $alert_thresholds = [
'ctime_change' => true, // 监控inode变化
'mtime_change' => true, // 监控内容变化
'perms_change' => true, // 监控权限变化
'owner_change' => true, // 监控所有者变化
'size_change' => true // 监控大小变化
];
public function __construct() {
if (!file_exists($this->storage_file)) {
file_put_contents($this->storage_file, json_encode([]));
}
}
public function monitorFiles($file_list) {
$results = [];
$alerts = [];
foreach ($file_list as $filename) {
if (!file_exists($filename)) {
$results[$filename] = ['status' => '不存在'];
continue;
}
clearstatcache(true, $filename);
$current_state = $this->getFileState($filename);
$previous_state = $this->getPreviousState($filename);
if ($previous_state) {
$changes = $this->detectChanges($previous_state, $current_state);
if (!empty($changes)) {
$results[$filename] = [
'status' => '已修改',
'changes' => $changes,
'current_state' => $current_state
];
// 检查是否需要警报
$file_alerts = $this->checkAlerts($changes);
if (!empty($file_alerts)) {
$alerts[$filename] = $file_alerts;
}
} else {
$results[$filename] = ['status' => '未修改'];
}
} else {
$results[$filename] = [
'status' => '首次监控',
'current_state' => $current_state
];
}
// 保存当前状态
$this->saveFileState($filename, $current_state);
}
// 触发警报
if (!empty($alerts)) {
$this->triggerAlerts($alerts);
}
return [
'results' => $results,
'alerts' => $alerts,
'monitored_count' => count($file_list),
'changed_count' => count($alerts)
];
}
private function getFileState($filename) {
return [
'ctime' => filectime($filename),
'mtime' => filemtime($filename),
'atime' => fileatime($filename),
'size' => filesize($filename),
'perms' => fileperms($filename),
'owner' => fileowner($filename),
'group' => filegroup($filename),
'inode' => fileinode($filename),
'hash' => md5_file($filename),
'checked_at' => time()
];
}
private function detectChanges($old, $new) {
$changes = [];
foreach ($this->alert_thresholds as $key => $enabled) {
if (!$enabled) continue;
switch ($key) {
case 'ctime_change':
if ($old['ctime'] != $new['ctime']) {
$changes[] = [
'type' => 'inode修改',
'old' => date('Y-m-d H:i:s', $old['ctime']),
'new' => date('Y-m-d H:i:s', $new['ctime'])
];
}
break;
case 'mtime_change':
if ($old['mtime'] != $new['mtime']) {
$changes[] = [
'type' => '内容修改',
'old' => date('Y-m-d H:i:s', $old['mtime']),
'new' => date('Y-m-d H:i:s', $new['mtime'])
];
}
break;
case 'perms_change':
if ($old['perms'] != $new['perms']) {
$changes[] = [
'type' => '权限修改',
'old' => substr(sprintf('%o', $old['perms']), -4),
'new' => substr(sprintf('%o', $new['perms']), -4)
];
}
break;
case 'owner_change':
if ($old['owner'] != $new['owner']) {
$changes[] = [
'type' => '所有者修改',
'old' => $old['owner'],
'new' => $new['owner']
];
}
break;
case 'size_change':
if ($old['size'] != $new['size']) {
$changes[] = [
'type' => '大小修改',
'old' => $old['size'],
'new' => $new['size']
];
}
break;
}
}
return $changes;
}
private function checkAlerts($changes) {
$alerts = [];
foreach ($changes as $change) {
// 根据变化类型生成警报
$alerts[] = [
'level' => $this->getAlertLevel($change['type']),
'message' => "检测到{$change['type']}",
'details' => $change
];
}
return $alerts;
}
private function getAlertLevel($change_type) {
$critical_changes = ['inode修改', '所有者修改', '权限修改'];
$warning_changes = ['内容修改', '大小修改'];
if (in_array($change_type, $critical_changes)) {
return 'CRITICAL';
} elseif (in_array($change_type, $warning_changes)) {
return 'WARNING';
} else {
return 'INFO';
}
}
private function triggerAlerts($alerts) {
$alert_message = "文件变更警报:\n";
foreach ($alerts as $filename => $file_alerts) {
$alert_message .= "\n文件: {$filename}\n";
foreach ($file_alerts as $alert) {
$alert_message .= sprintf(
" [%s] %s\n",
$alert['level'],
$alert['message']
);
}
}
// 记录到日志文件
file_put_contents(
'security_alerts.log',
date('[Y-m-d H:i:s] ') . $alert_message . "\n",
FILE_APPEND
);
// 可以扩展:发送邮件、短信等通知
// $this->sendEmailAlert($alert_message);
}
private function getPreviousState($filename) {
$data = json_decode(file_get_contents($this->storage_file), true);
return $data[$filename] ?? null;
}
private function saveFileState($filename, $state) {
$data = json_decode(file_get_contents($this->storage_file), true);
$data[$filename] = $state;
file_put_contents($this->storage_file, json_encode($data, JSON_PRETTY_PRINT));
}
}
// 使用示例
$monitor = new FileChangeDetectionSystem();
// 监控重要文件
$important_files = [
'/etc/passwd',
'/etc/shadow',
'/etc/ssh/sshd_config',
'/var/www/html/index.php',
'/var/log/auth.log'
];
$result = $monitor->monitorFiles($important_files);
echo "文件变更监控结果:
";
echo "监控文件数: {$result['monitored_count']}
";
echo "变更文件数: {$result['changed_count']}
";
if (!empty($result['alerts'])) {
echo "安全警报:
";
foreach ($result['alerts'] as $filename => $alerts) {
echo "{$filename}:
";
foreach ($alerts as $alert) {
echo " [{$alert['level']}] {$alert['message']}
";
}
}
}
?>