计算机系统中的时间不只是一个数字。文件系统用多个时间戳记录读取、内容修改和元数据变更;应用还必须处理 UTC、时区规则与历史时间。本文先说明 Linux 文件时间戳、stat、touch 和 atime 挂载选项,再介绍 Unix 时间戳与 IANA 时区。
1. 文件时间戳、系统命令与挂载选项
1.1 使用 stat 识别四种时间戳
stat 可以显示文件时间戳和 inode 元数据:
$ stat /var/log/syslog
File: /var/log/syslog
Size: 1048576 Blocks: 2048 IO Block: 4096 regular file
Device: 253,0 Inode: 131074 Links: 1
Access: (0640/-rw-r-----) Uid: ( 0/ root) Gid: ( 4/ adm)
Access: 2026-07-31 21:30:00.123456789 +0800
Modify: 2026-07-31 20:15:33.987654321 +0800
Change: 2026-07-31 20:15:33.987654321 +0800
Birth: 2026-07-01 08:00:00.000000000 +0800
| 字段 | 时间戳 | 全称 | 含义 | 常见更新操作 |
|---|---|---|---|---|
Access: | atime | Access Time | 最近一次访问时间 | cat、less、grep、read() |
Modify: | mtime | Modification Time | 最近一次内容修改时间 | write()、truncate()、编辑器保存 |
Change: | ctime | Change Time | 最近一次 inode 状态变更时间 | chmod、chown、链接数变化、内容修改 |
Birth: | birthtime | Birth/Creation Time | 文件创建时间 | 文件系统支持时在创建文件时记录 |
ctime 的 c 表示 change,不是 creation。修改内容会同时改变 mtime 和 ctime;修改权限、所有者或链接数通常只改变 ctime。Linux 对 birthtime 的支持取决于内核、文件系统和用户空间工具,因此 stat 可能显示 -。普通文件 API 通常不能像 mtime 一样设置 birthtime,复制到新文件还会产生新的创建时间。
mtime 也不等于可信的审计记录。touch -m file 可以改变 mtime,rsync —archive 可以保留源文件 mtime。安全审计应使用审计日志或其他不可由普通文件操作轻易改写的记录。
GNU stat 的格式选项适合脚本提取字段:
stat -c '%Y' filename
stat -c '%W' filename
stat -c '%x %y %z %w' filename
stat -c 'mtime=%Y atime=%X ctime=%Z' /etc/passwd
%Y、%X 与 %Z 分别输出 mtime、atime 与 ctime 的 Unix 秒数;%W 输出 birthtime 秒数,不支持时通常为 0。macOS 的 BSD stat 使用 -f,格式字符也与 GNU 版本不同,跨平台脚本必须分别处理。
1.2 atime 的用途与限制
atime 可用于近似判断文件最近是否被读取。部分邮件客户端比较 atime 和 mtime 判断邮件是否读过,临时文件清理工具可能按照访问时间保留活跃文件,缓存系统也可能参考访问时间制定淘汰策略。
atime 只保存最近一次访问时间,不记录读取者、读取次数或完整历史,而且 relatime 与 noatime 会进一步降低精度。因此 atime 不能单独承担安全取证或合规审计。需要可靠访问记录时,应配置 Linux Audit、应用日志或存储系统自身的审计功能。
1.3 strictatime、relatime 与 noatime
更新 atime 会把原本的读取工作扩展为 inode 元数据写入。日志、静态资源或大量小文件扫描可能因此增加写入放大。Linux 提供三种常见挂载策略:
| 模式 | 更新规则 | 适用场景与代价 |
|---|---|---|
| strictatime | 按严格访问时间语义更新 atime | 时间最精确,但读取密集负载会增加元数据写入 |
| relatime | atime 早于 mtime 或 ctime,或距离上次更新超过一定时间时才更新 | 保留大多数兼容需求并显著减少写入,现代 Linux 常用默认值 |
| noatime | 不因读取更新 atime | 适合不依赖 atime 的读取密集型文件系统 |
可以用 findmnt -no OPTIONS /mountpoint 或检查 /proc/mounts 确认实际选项。选择 noatime 前要检查邮件、临时文件清理与备份工具是否依赖访问时间;只有明确要求精确 atime 时才值得采用 strictatime。
1.4 文件操作如何影响时间戳
常见操作对时间戳的影响如下。实际 atime 是否落盘仍受挂载选项影响,编辑器的“保存”也可能通过写临时文件再重命名实现,所以表格描述的是常见结果,不是所有文件系统和工具的绝对保证。
| 操作 | atime | mtime | ctime | 说明 |
|---|---|---|---|---|
echo “data” >> file | 通常不变 | 更新 | 更新 | 只以写模式追加,不需要读取原内容 |
echo “data” > file | 通常不变 | 更新 | 更新 | 截断并写入同一文件 |
cat file | 按挂载策略更新 | 不变 | 不变 | 读取文件内容 |
touch file | 更新 | 更新 | 更新 | 设置 atime 与 mtime,同时引起 ctime 变更 |
touch -m file | 不变 | 更新 | 更新 | 显式设置 mtime |
chmod file | 不变 | 不变 | 更新 | 修改 inode 元数据 |
sed -i ‘s/a/b/’ file | 依实现而定 | 更新 | 更新 | 常见实现读取旧文件并用新文件替换 |
写模式追加本身不会因为 strictatime 而更新 atime;atime 由访问文件内容触发。脚本若先读取再写入、文件同时被其他进程读取,或工具采用不同保存流程,最终时间戳仍可能变化。必须保留原 atime 的程序可以先读取时间戳,再在写入后用 touch -a -d 恢复;底层程序也可以在满足权限条件时使用 O_NOATIME 避免自身读取更新 atime。
下列实验展示只写追加的典型结果:
$ stat -c 'atime=%x mtime=%y ctime=%z' test.txt
atime=2026-07-31 20:00:00 mtime=2026-07-31 20:00:00 ctime=2026-07-31 20:00:00
$ echo "new line" >> test.txt
$ stat -c 'atime=%x mtime=%y ctime=%z' test.txt
atime=2026-07-31 20:00:00 mtime=2026-07-31 21:45:00 ctime=2026-07-31 21:45:00
1.5 date 与 Linux 时间工具
GNU date 可以显示、解析和转换时间:
date '+%Y-%m-%dT%H:%M:%S%z'
date -u '+%Y-%m-%dT%H:%M:%SZ'
date '+%s'
date -d @1785498540 '+%Y-%m-%d %H:%M:%S %z'
date -d '2026-07-31 21:39:00' '+%s'
TZ='America/Los_Angeles' date -d '2026-07-31 21:39:00 UTC' '+%Y-%m-%d %H:%M:%S %Z'
date -d 'now + 3 days - 2 hours' '+%Y-%m-%d %H:%M:%S'
date -d 'next monday 09:00' '+%s'
date -d 是 GNU 扩展,不属于 POSIX。BusyBox 和 macOS 的 date 语法不同,脚本不能假设所有系统都支持同一组选项。
| 工具或库 | 用途 | 备注 |
|---|---|---|
date | Shell 中查询与转换时间 | GNU、BusyBox 与 BSD 版本存在差异 |
timedatectl | 管理系统时区、NTP 与 RTC | 由 systemd 提供 |
hwclock | 读写硬件时钟 | 需要区分 UTC 与本地时间模式 |
chronyc、ntpq | 检查时间同步 | 可查看同步源、偏移与漂移 |
tzselect | 选择 IANA 时区 | 可辅助确定 TZ 值 |
zdump | 查看时区转换规则 | 适合诊断 tzdata 问题 |
C time.h | 应用程序时间 API | 包含 gmtime_r、strftime 等接口 |
1.6 使用 touch 修改文件时间
touch 可以设置 atime 和 mtime,ctime 则由内核在 inode 状态变化时自动更新:
touch -m -t 202301011200.00 filename
touch -a -t 202301011200.00 filename
touch -t 202301011200.00 filename
touch -r reference_file target_file
第一条命令只指定 mtime,第二条只指定 atime,第三条同时指定两者,第四条把参考文件的 atime 与 mtime 复制到目标文件。touch 不能直接指定 ctime 或 birthtime;不应在生产文件系统中使用 debugfs 篡改 inode 时间戳。
2. Unix 时间与时区转换
2.1 Unix 时间戳是有符号时间轴
Unix 时间戳以 1970-01-01T00:00:00Z 为零点。零点以前的时间使用负数,零点不是可表示时间的起点。
| UTC 时间 | Unix 时间戳(秒) | 说明 |
|---|---|---|
| 2026-07-31T13:39:00Z | 1785498540 | epoch 以后 |
| 1970-01-01T00:00:00Z | 0 | epoch |
| 1949-10-01T00:00:00Z | -636249600 | epoch 以前 |
| 1900-01-01T00:00:00Z | -2208988800 | epoch 以前 |
存储历史时间时要检查数据类型和数据库范围。无符号整数不能表示负数;32 位有符号 Unix 秒数还会在 2038 年溢出,通常应使用有明确范围的 64 位类型。MySQL TIMESTAMP 的范围受版本和实现限制,历史资料可根据业务语义考虑 DATETIME 或整数时间戳。
IANA 数据库对较早年代的本地规则可能只能提供近似值,并可能使用 LMT(Local Mean Time)。前端组件、序列化库和监控工具也不一定正确处理负时间戳。处理 1970 年以前的数据时,应对存储、API 和显示链路分别测试。
2.2 固定偏移、缩写与 IANA 时区
军事或 NATO 字母时区把字母映射到固定 UTC 偏移,Z 表示 UTC,J 表示观察者本地时间。除 ISO 8601 接纳的 Z 外,这套通信简写不包含夏令时和政治规则,不适合程序存储用户时区。
PDT、CST、IST 等人类缩写也有歧义。CST 可能表示 China Standard Time 或 Central Standard Time,IST 可能表示印度、爱尔兰或以色列的时间。缩写只能用于明确上下文中的显示,不能作为可靠的时区标识符。
应用程序应使用 Area/Location 形式的 IANA 名称,例如 Asia/Shanghai、America/Los_Angeles 与 Asia/Pyongyang。IANA tzdata 保存一个地区的历史偏移、夏令时切换与规则变更;固定的 +08:00 只能表示某一时刻的偏移,不能表示完整时区。
NATO 固定偏移字母如下,字母 J 没有固定偏移:
| 字母 | UTC 偏移 | 字母 | UTC 偏移 |
|---|---|---|---|
| A / Alpha | +01:00 | N / November | -01:00 |
| B / Bravo | +02:00 | O / Oscar | -02:00 |
| C / Charlie | +03:00 | P / Papa | -03:00 |
| D / Delta | +04:00 | Q / Quebec | -04:00 |
| E / Echo | +05:00 | R / Romeo | -05:00 |
| F / Foxtrot | +06:00 | S / Sierra | -06:00 |
| G / Golf | +07:00 | T / Tango | -07:00 |
| H / Hotel | +08:00 | U / Uniform | -08:00 |
| I / India | +09:00 | V / Victor | -09:00 |
| K / Kilo | +10:00 | W / Whiskey | -10:00 |
| L / Lima | +11:00 | X / X-ray | -11:00 |
| M / Mike | +12:00 | Y / Yankee | -12:00 |
| Z / Zulu | ±00:00 | J / Juliet | 本地时间 |
2.3 时区规则会发生变化
朝鲜在 2015 年 8 月 15 日从 UTC+09:00 改为 UTC+08:30,又在 2018 年 5 月 5 日恢复 UTC+09:00。把朝鲜时区永久写成 +08:30 会让 2018 年以后的本地时间错误;使用 Asia/Pyongyang 并及时更新 tzdata,标准库才能根据目标日期应用相应规则。
相同问题也会出现在夏令时、行政区边界和政府临时决策中。业务数据通常需要分别保存“发生的瞬间”和“用户选择的时区”:前者可以用 UTC instant 表示,后者用 IANA 名称保留显示与日历计算所需的规则。
2.4 存储、传输和显示原则
系统内部可以使用 64 位 UTC 时间戳或带明确 UTC 语义的 datetime 类型。API 应输出 ISO 8601,例如 2026-07-31T13:39:00Z 或带偏移的 2026-07-31T21:39:00+08:00。数据库若需要恢复用户当地的未来日程,还应保存 IANA 时区,而不是只保存当前偏移。
显示层根据用户偏好把 instant 转为本地时间。服务应定期更新 tzdata,并对夏令时跳转、重复时间、历史负时间戳和 2038 年边界建立回归测试。UTC 时间戳适合标识瞬间,但“每月最后一天 09:00”属于日历规则,不能只靠固定秒数间隔表达。
| 场景 | 容易出错的做法 | 推荐做法 |
|---|---|---|
| 存储时区 | PDT 或整数 +8 | America/Los_Angeles 等 IANA 名称 |
| 表示 UTC | 2026-07-31 13:39:00 | 2026-07-31T13:39:00Z |
| 1970 年以前 | UINT32 | 有明确范围的有符号 64 位类型 |
| 美西本地时间 | 永久写死 UTC-7 | 使用 America/Los_Angeles |
| 查看文件时间 | 只运行 ls -l | 运行 stat filename |
| 读取密集型挂载 | 默认选择 strictatime | 评估 relatime 或 noatime |
| Shell 时间转换 | 手工加减偏移 | 使用 TZ 与时区数据库 |
时间戳把瞬间映射到数值轴,时区规则把瞬间映射到当地日历。文件系统时间与应用时间都应先明确目标语义,再选择数据类型、命令和时区规则。