暗色模式

Linux 文件时间戳:atime、mtime、ctime 与 find 时间筛选

技术教程
2026-09-12
2
0
本文要点
  • stat 一次给出四个时间戳:Access(atime)、Modify(mtime)、Change(ctime)、Birth(创建时间 btime)。实测新建文件时四者完全相同,但它们的用途和可信度完全不同
  • touch -d 能把 mtime 改成 2020-01-01ctime 却留在当下:ctime 是「inode 最后变更时间」,改权限、改属主、改名都会刷新它,而且无法被伪造——这正是判断文件有没有被动过的关键证据
  • 默认的 relatime 并不是「每次读都更新」:实测首次读会更新 atime,24 小时内第二次读不更新,文件被修改后下一次读又会更新;strictatime 才每次读都更新,noatime 则永不更新
  • find -mtime -1 会漏掉 25 小时前的文件——它按整数天截断,25 小时算 1 天,而 -1 要求「不足 1 天」;要精确到小时得用 -mmin -1560-newermt '30 hours ago'
  • cp 默认把 mtime 重置为当下,只有 cp -ptouch -rtar 解包才保留原值;ls -l / -lu / -lc 分别显示 mtime / atime / ctime 三列

Linux 文件时间戳:atime、mtime、ctime 与 find 时间筛选

「这个文件到底是什么时候被改的?」——备份脚本要不要重跑、日志能不能删、find 该筛哪一批、上线包有没有被替换过,最后都会落到这个问题上。而回答它的第一道坎,是很多人把 ls -l 里那一列时间当成了「修改时间」,却不知道那其实是 mtime,也不知道系统其实记了四个时间。

第二个坎是 find 的时间筛选。-mtime -1 看起来是「最近一天」,跑出来的结果却总少几个文件;换 -mtime +1 又多了不该有的。这不是 find 有 bug,而是它的时间单位被截断成整数天了。

本文在一台 Ubuntu 22.04 云服务器(KVM,1 核、1.9G 内存,内核 5.15.0-30-generic,机器时区为 UTC)上把这两件事跑了一遍:先用 stat 看清四个时间戳,再用 touch 伪造一次 mtime,看 ctime 为什么骗不了人;接着用 loop 挂载的 ext4 镜像分别以 relatime / strictatime / noatime 实测 atime 的更新时机;最后用三个不同年龄的文件验证 find 的整数天陷阱与三种精确写法。文中命令与输出均为真实执行结果。

第一步:stat 一次看全四个时间戳

先造一个文件,然后用 stat 看它:

cd /mnt/tsdemo && stat demo.txt

stat 输出:Access、Modify、Change、Birth 四项时间戳,刚创建的文件四者完全相同

四个时间戳的含义差别很大,值得逐一对上号:

字段名称什么时候变ls 里怎么看
Accessatime文件内容被读取时(受挂载选项限制,见下节)ls -lu
Modifymtime文件内容被写入时(echo >>、编辑器保存)ls -l(默认这一列)
Changectimeinode 变更时:内容、权限、属主、文件名、链接数ls -lc
Birthbtime文件创建时间,之后不再变(ext4 的 crtime无(ls 不显示)

注意截图里四项时间一模一样——因为这是一个刚 echo 出来的新文件,创建、写入、inode 变更、首次读取挤在了同一个瞬间。这恰恰说明:想区分它们,必须让文件经历「先创建,再被改,再被读」的不同阶段。

另外注意 Birth 这一行:它来自内核的 statx 接口,需要文件系统支持(ext4 从内核 4.11 起提供),stat 版本较老时不会显示这一行。它的价值在于它是唯一一个「只能被创建、无法被修改」的时间戳——crtime 在 inode 里落定后就再没有系统调用能改它(debugfs 之类的离线工具除外),所以判断「这个文件是不是那个时间点出生的」只能看它。

第二步:伪造一次 mtime,看 ctime 怎么拆穿

touch -d 可以随意指定时间,改的是 atime 和 mtime。那改完之后,能不能看出文件被动过手脚?把 mtime 直接改成 2020 年试试:

date -u '+现在   : %F %T'; cd /mnt/tsdemo && chmod 640 demo.txt && touch -d '2020-01-01 08:00:00' demo.txt && stat --printf='mtime  : %y\nctime  : %z\nbirth  : %w\n' demo.txt

伪造 mtime 之后:mtime 显示 2020-01-01,ctime 却是当下的 21:09:54,birth 也停在创建时刻

结果很直白:

  • mtime 成功伪装成 2020-01-01 08:00:00
  • ctime当下2026-09-11 21:09:54)——因为 touch 写元数据这件事本身就变更了 inode;
  • birth 停在文件真正被创建的那一刻(21:09:40),一动不动。

所以「文件时间对不对」要有两个层次的理解:mtime 是可以被任何程序随手改的,备份软件、解压工具、rsync -t、甚至 git checkout 都会按自己的需要设置 mtime,拿它当审计证据并不可靠;而 ctime 是内核自动维护的、应用层改不了的,只要 inode 有任何变更它就会前进。想确认一个文件「最近有没有被动过」,看 ctime 比看 mtime 靠谱得多。

⚠️ 顺带说清一个常见误解:ctime 不是「创建时间」(Create time 的错觉)。它是 Change time,文件 20 年前创建、今天改一次权限,ctime 就是今天。真正的创建时间是 Birth

第三步:atime 到底什么时候更新?三种挂载策略实测

atime 最容易让人误判,因为它不是每次读都更新——具体行为由文件系统的挂载选项决定,而大多数发行版默认给的是 relatime

要干净地对比三种策略,用一个 loop 挂载的 ext4 镜像最方便,互不干扰:

dd if=/dev/zero of=/opt/ts.img bs=1M count=30 status=none
mkfs.ext4 -q -F /opt/ts.img
mkdir -p /mnt/tsdemo
mount -o loop,relatime /opt/ts.img /mnt/tsdemo

然后在每种策略下做同一套动作:创建文件 → 读一次 → 隔 1 秒再读一次 → 追加内容(改 mtime)→ 再读一次,每次都记录 atime 的 epoch:

run_case() {
  opt=$1
  mount -o loop,$opt /opt/ts.img /mnt/tsdemo
  cd /mnt/tsdemo
  echo hello > f.txt
  t0=$(stat -c %X f.txt)
  sleep 1.1; cat f.txt >/dev/null; t1=$(stat -c %X f.txt)
  sleep 1.1; cat f.txt >/dev/null; t2=$(stat -c %X f.txt)
  sleep 1.1; echo hi >> f.txt; cat f.txt >/dev/null; t3=$(stat -c %X f.txt)
  echo "[$opt] 创建后=$t0  读1=$t1  读2=$t2  改mtime后读=$t3"
  cd /; umount /mnt/tsdemo
}
run_case relatime
run_case strictatime
run_case noatime
[relatime] 创建后=1789161063  读1=1789161064  读2=1789161064  改mtime后读=1789161066
[strictatime] 创建后=1789161066  读1=1789161067  读2=1789161068  改mtime后读=1789161069
[noatime] 创建后=1789161069  读1=1789161069  读2=1789161069  改mtime后读=1789161069

整理成表格更清楚:

挂载策略第一次读24 小时内再读文件被修改后再读开销
relatime(默认)✅ 更新❌ 不更新✅ 更新低,只在必要时写一次
strictatime✅ 更新✅ 更新✅ 更新每次读都写 inode
noatime❌ 不更新❌ 不更新❌ 不更新

relatime 的规则可以概括成一句话:当 atime 比 mtime 或 ctime 旧,或者已经超过 24 小时没更新时,才写一次。新创建的文件三者相等,所以第一次读就会更新 atime;此后 24 小时内再读,条件不满足,就不再写——这正是上面「读1 变了、读2 没变」的原因。而追加内容让 mtime 前进了,atime 立刻又「比 mtime 旧」,于是下一次读又更新。

两个实测踩到的坑:

  • 同一时钟滴答内的读取看不到变化。ext4 的时间戳精度约 4ms,两次读挨得太近时 atime 的秒数根本不变,会让人误以为「没更新」。所以上面每次读之间都 sleep 1.1 隔开——排查 atime 问题时,先把时间拉开再比较
  • mount -o remount,relatime 清不掉 noatime。测试时我从 noatime 切回 relatime 用过 remount,结果 atime 依旧不动:remount 是叠加新选项,不会移除已经生效的 noatime。要换策略只能先 cd /(shell 的当前目录在挂载点里时 umount 会报 target is busy),umount 之后再带显式选项重新 mount

另外,截图里的 strictatime 那行 mount 选项只显示 rw 而没有 strictatime 字样——因为它是内核的默认值,/proc/mounts 不会把默认值单独列出来,别以为没生效。

第四步:find 的整数天陷阱

先造三个年龄不同的文件(用 touch -d 直接指定 mtime),然后做最直觉的那个查询:

cd /mnt/tsdemo/find-demo; ls -l --time-style=long-iso; echo '== find -mtime -1 =='; find . -name '*.txt' -mtime -1 -printf '%f\n' | sort

find -mtime -1 的结果:只命中 1 小时前的文件,25 小时前的 trap-25h.txt 被漏掉

三个文件的年龄分别是 1 小时、10 天、25 小时-mtime -1 的直觉含义是「最近一天」,结果却只吐出了 new-1h.txt——那个 25 小时前的 trap-25h.txt 被悄悄漏掉了。

原因是 -mtime 的时间单位是整数天,且向零截断:25 小时 ÷ 24 = 1.04,截断后是 1 天;而 -n 的语义是「不足 n 天」,-1 要求截断值 < 1,也就是必须为 0。所以 -mtime -1 实际筛的是「不足 24 小时」,25 小时正好落在门外。

把几种写法放在一起对比(同一个目录、同一批文件):

表达式命中实际语义
-mtime -1new-1h.txt截断后不足 1 天,即 < 24 小时
-mtime 0new-1h.txt截断后等于 0 天,同样是 < 24 小时
-mtime +1old-10d.txt截断后大于 1 天,10 天的命中,25 小时的不命中
-mmin -1560new-1h.txttrap-25h.txt分钟算,26 小时内
-newermt '30 hours ago'new-1h.txttrap-25h.txt直接和「30 小时前」这个时间点比
-newer old-10d.txtnew-1h.txttrap-25h.txt比参照文件 old-10d.txt

结论很实用:-mtime 只适合「天」级别的粗筛,一旦边界要求精确(清理 24 小时前的日志、捞最近 6 小时的备份),就换成 -mmin(按分钟)或 -newermt(按时间点,可读性最好)。-newer 则适合「比某个基准文件新」的场景,比如只同步比上次快照更新的文件。

还有两个细节:

  • 目录本身也会被匹配find . -mtime -1 会把当前目录 . 一起列出来(目录的 mtime 是它最近一次增删文件的时刻),所以要么加 -type f,要么像截图里那样用 -name '*.txt' 限定。
  • -newermt 的绝对时间按本地时区解释。这台演示机是 UTC(date+0000),所以 -newermt '30 hours ago' 这种相对时间怎么写都安全;但写成 -newermt '2026-09-10 20:00' 这种绝对时间时,find 会按机器的本地时区去理解它——跨时区排查(比如日志是 UTC、机器是 CST)时,这里差 8 小时就是差一批文件。

第五步:复制、打包之后 mtime 还在吗

把同一个文件用四种方式复制/打包一遍,看 mtime 谁守住了:

echo data > src.txt
touch -d '2020-01-01 08:00:00' src.txt
cp src.txt plain.txt
cp -p src.txt pres.txt
touch -r src.txt reffile.txt
tar cf t.tar src.txt; mkdir out; tar xf t.tar -C out
stat -c '%n  mtime=%y' src.txt plain.txt pres.txt reffile.txt out/src.txt
src.txt  mtime=2020-01-01 08:00:00.000000000 +0000
plain.txt  mtime=2026-09-11 21:11:13.341318045 +0000
pres.txt  mtime=2020-01-01 08:00:00.000000000 +0000
reffile.txt  mtime=2020-01-01 08:00:00.000000000 +0000
out/src.txt  mtime=2020-01-01 08:00:00.000000000 +0000
动作mtime说明
cp❌ 重置为当下复制是「新建文件」,mtime 就是写入时刻
cp -p✅ 保留-p = --preserve=mode,ownership,timestamps
touch -r 参照文件✅ 保留把参照文件的时间戳整套盖过来(含 atime)
tar 打包再解包✅ 保留tar 默认记录并还原 mtime(这正是增量备份的基础)

这解释了为什么「迁移完数据,备份脚本却把全量文件又传了一遍」——中间用了不带 -pcp,所有 mtime 都变成了迁移那一刻,增量判断自然全部失效。同理,rsync 要保住时间戳得带 -t-a 已含),scp 想保住得用 -p。归档类工具的细节可参考 Linux tar 打包与压缩 里增量备份那部分。

第六步:ls -l、-lu、-lc 分别看的是哪个时间

同一个文件,三条命令三个时间——因为默认那一列只是 mtime:

ls -l  --time-style=long-iso src.txt
ls -lu --time-style=long-iso src.txt
ls -lc --time-style=long-iso src.txt
-rw-r--r-- 1 root root 5 2020-01-01 08:00 src.txt
-rw-r--r-- 1 root root 5 2026-09-11 21:11 src.txt
-rw-r--r-- 1 root root 5 2026-09-11 21:11 src.txt

三行分别是 -l(mtime,2020 年那个伪造值)、-lu(atime,刚被 cp 读过所以是当下)、-lc(ctime,touch -d 改元数据时刷新成当下)。atime 和 ctime 在这台机器上撞成了同一分钟,属正常现象——它俩恰好都被同一批操作触发了。

习惯上 ls -l 用得最多,但要提醒一句:ls -l 那一列不是「文件创建时间」。很多人拿它当创建时间用,看到老文件的 mtime 是最近几天就以为文件是新的,其实可能只是被人 cp 过、或者解压时重新落了盘。真要创建时间,用 statBirth;要「有没有被动过」,看 ctime

另外 ls 还有两个冷门但好用的开关:-lu 看 atime 能帮你找出「被读过但没被改过」的文件(比如排查某个配置到底有没有被加载),--time-style=long-iso 则让时间变成 2026-09-11 21:11 这种好读的固定格式,比默认的「Sep 11 21:11」更适合贴进排查记录。

时间戳排查清单

把上面这些压成一张可以照着敲的表:

想做什么命令要点
看全四个时间戳stat 文件Access/Modify/Change/Birth 四行
判断文件有没有被动过stat -c '%z' 文件ctime,它无法被应用层伪造
看真正的创建时间stat -c '%w' 文件即 Birth,需要 ext4 + 较新 coreutils
看某文件最近有没有被读ls -lu 文件注意 relatime 下 24 小时内可能不更新
精确筛最近 N 小时find . -type f -mmin -360别用 -mtime,它按整数天截断
按时间点筛find . -type f -newermt '30 hours ago'可读性最好;绝对时间按本地时区解释
只同步比基准新的文件find . -type f -newer 基准文件增量备份常用
复制时保住时间戳cp -p / touch -r / tar不带 -pcp 会把 mtime 重置
换 atime 策略cd /umount + mount -o loop,<策略>remount 清不掉 noatime

时间戳的坑说到底集中在两点:把哪个时间当成哪个时间,以及把天当成小时。前者让你误判文件的历史(拿 mtime 当创建时间、拿 ctime 当创建时间),后者让你漏掉本该处理的文件(-mtime -1 漏掉 25 小时前的日志)。想再往深处走,atime/mtime 都存在 inode 里,理解 inode 的结构可以看 Linux 软链接与硬链接;挂载选项与文件系统选型的完整清单见 Linux 文件系统:从 ext4 到 XFS;如果问题是「时间根本不对」而不是「时间戳含义不清」,那就是时钟同步的事了,见 Linux 时间同步;文件查找的其它用法(按名字、大小、权限组合筛选)见 Linux 文件查找:find 与 locate

清理演示产物

演示只产生一个 30MB 的 loop 镜像和几个空文件,确认后删掉即可:

cd / && umount /mnt/tsdemo
rm -f /opt/ts.img
rm -rf /mnt/tsdemo

收个尾:四个时间戳里,mtime 是给工具用的(备份、同步、构建系统都靠它判断新旧),atime 是给内核做缓存决策用的(所以默认的 relatime 才要省着写),ctime 是给排查用的(应用层改不了),Birth 是给归档用的(记录了文件真正的出生时刻)。分清楚「谁维护、谁可信、什么时候变」,statfind 这两个命令就不会再给出让你意外的答案。

发表评论

暂无评论,快来抢沙发吧!