用了几十年 Linux,我发现自己越来越依赖一个被很多人忽略的命令:stat。它能让你一眼看穿文件系统里每个文件、每个目录的真实状态,从 inode 编号、文件类型、权限细节到三个时间戳,全都摊开摆在面前。这篇文章就围绕 stat 命令,把它的使用场景、输出字段、格式化技巧和跨平台差异一次讲透,适合刚接触 Linux 的初学者,也适合每天写脚本、排查线上问题的运维和开发同学。
很多人习惯用 ls -l 看文件信息,但 ls 给出的只是个“简化摘要”。stat 是文件的“全身体检报告”,它直连接内核的文件对象元数据,把 ls 不展示的细节全部暴露出来。我最早被 stat 打动,是排查一个诡异问题:某文件明明刚修改过,ls 显示的时间却纹丝不动。后来才发现,ls 默认只管 mtime,而真正变化的是 ctime。这种坑,只有 stat 能帮你避开。
1. 命令定位与基础用法
1.1 stat 到底能干什么
stat 命令的本质,是调用 stat() 系列系统调用,从文件系统 inode 里读取元数据并格式化输出。它和 ls 最大的区别在于信息密度:ls 按用户友好的方式挑选少量字段,stat 则把 inode 里几乎所有关键字段都拉出来,包括 inode 号、硬链接数、文件类型、权限位、UID/GID、设备号、块大小,以及 atime、mtime、ctime 甚至 birth time。
日常使用中,我常把 stat 用于三类场景:
- 确认文件真实类型与权限细节,尤其是字符设备、符号链接这类容易被 ls 误导的对象。
- 查看文件时间戳的精确值到纳秒级,判断文件是否被重写或权限是否发生过变化。
- 在脚本里用自定义格式化输出,批量提取文件名、大小、修改时间等字段,生成巡检报告或备份清单。
语法很简单:
stat [选项] 文件默认不加参数,直接输出完整信息。加上 -f 可以查看文件系统本身的状态,-c 可以自定义输出格式,-L 可以跟随符号链接。
1.2 和 ls 对比:为什么 stat 更可靠
举一个典型案例。执行 ls -l 时,你会看到文件大小、权限、修改时间等信息。但 ls 有个隐藏逻辑:它默认展示的是 mtime(内容修改时间),且格式化到秒。而 stat 不仅区分了 Access(atime)、Modify(mtime)、Change(ctime),还能显示纳秒部分,以及文件出生时间。
有一次我在调试程序日志时,发现两份日志文件 ls 显示的时间完全一样,但内容明显不同。用 stat 一看,其中一份的 ctime 比另一份晚了半秒,原因是有人对文件执行过 chmod,改变了权限位。ls 完全看不出来,stat 却把这个“看不见的修改”暴露无遗。这就是我为什么强调:排查文件状态问题,第一反应应该是 stat,而不是 ls。
2. 输出字段逐项拆解:一行输出看懂文件全貌
2.1 默认输出的 8 个字段组
在 Linux 上直接执行 stat 文件名,输出通常分成多个逻辑段。我先拿一份实际输出示例逐段解读:
File: /var/log/syslog Size: 1289034 Blocks: 2528 IO Block: 4096 regular file Device: 801h/2049d Inode: 1573214 Links: 1 Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root) Access: 2025-01-18 10:23:45.123456789 +0800 Modify: 2025-01-18 10:22:10.987654321 +0800 Change: 2025-01-18 10:22:10.991234567 +0800 Birth: 2025-01-10 08:00:00.000000000 +0800第一行是路径和文件类型,regular file 表示普通文件,还会出现 directory、symbolic link、character special file 等类型。Size 是字节数,Blocks 是文件实际占用的 512 字节块数量,IO Block 是文件系统优化的 I/O 块大小。这里可能有人疑惑:文件大小 1289034,Blocks 却是 2528,换算下来约 1.29MB,说明文件占用略大于实际大小,因为最后一个块没有写满。
第二行最关键的是 Inode 和 Links。Inode 是文件在文件系统中的唯一身份证号,Links 是硬链接数量。普通文件 Links 通常是 1,目录的 Links 则至少是 2(. 和 .. 都会计数)。Device 显示的是文件所在设备号,801h 表示主设备号 8,次设备号 1,换算成 /dev/sda1 这类块设备。
2.2 Access、Modify、Change:三种时间戳的由来
这三个时间戳是最容易被混淆的部分:
- Access(atime):文件最后一次被读取的时间。每次 cat、less、grep 读取文件内容都可能更新它。很多系统会用 relatime 挂载选项来降低频繁更新带来的开销,因此 atime 并不总实时变化。
- Modify(mtime):文件内容最后一次被写入的时间。这是大家最熟悉的,ls -l 显示的就是它。
- Change(ctime):文件元数据最后一次被修改的时间。chmod、chown、ln 甚至 rename 都会更新 ctime。关键点在于:修改文件内容也会顺带更新 ctime,因为它属于元数据变化。
在排查“文件是否被动过手脚”时,我会同时对比 Modify 和 Change。如果 Modify 变化而 Change 保持不变,几乎不可能,因为写内容必然触发元数据变更。反过来,如果 Change 变化但 Modify 没变,说明有人改了权限、属主或硬链接,而内容原封未动。
Birth 字段显示文件创建时间,但并非所有文件系统都支持。ext4 支持,但某些网络文件系统或老式存储会显示空值。在多 MB 级日志轮转脚本里,我常靠 Birth 判断日志文件是否被重建,比在脚本里维护额外状态文件要省事得多。
3. 自定义格式化输出:让 stat 成为脚本利器
3.1 常用格式化占位符速查
stat 的 -c 参数允许你像 printf 一样定制输出,这是它作为脚本工具的核心价值。常用占位符如下:
| 占位符 | 含义 |
|---|---|
| %n | 文件名 |
| %F | 文件类型 |
| %s | 文件大小(字节) |
| %b | 占用块数 |
| %i | inode 编号 |
| %h | 硬链接数 |
| %U / %u | 用户名 / UID |
| %G / %g | 组名 / GID |
| %a | 权限八进制 |
| %A | 权限符号表示 |
| %x / %X | atime 人类可读 / epoch 秒 |
| %y / %Y | mtime 人类可读 / epoch 秒 |
| %z / %Z | ctime 人类可读 / epoch 秒 |
| %w / %W | birth time 可读 / epoch 秒 |
| %d / %D | 设备号十进制 / hex |
| %N | 加引号的文件名,符号链接时显示指向 |
用这些占位符,可以组合出任意需要的输出格式。例如在巡检脚本里,一行输出所有关键信息:
stat -c "%n|%s|%Y|%i" /etc/passwd输出可能为:
/etc/passwd|2781|1705567612|1310731这种管道分隔的格式,非常适合直接喂给 awk、cut 或 Python 做进一步处理。比如要批量检查一个目录下所有文件的大小和修改时间,可以配合 find:
find /var/log -type f -name "*.log" -exec stat -c "%n %s %y" {} \;注意这里的 %y 输出带时区的完整可读时间,中间有空格,如果后续处理要按空格分割,建议统一用 %Y 或换成 tab 分隔:
find /var/log -type f -name "*.log" -exec stat -c "%n\t%s\t%Y" {} \;3.2 自定义格式里的特殊转义:\n 和 \t 的坑
用 -c 拼接多字段输出时,我强烈建议用 \t 做分隔符,而不是空格,因为文件名本身可能包含空格。如果你的环境里有带空格的文件名,且你没有用 \t 或自定义分隔符,解析时必然出错。
stat 支持在格式字符串里直接使用 \n 换行和 \t 制表符。例如:
stat -c "文件名: %n\n大小: %s 字节\n修改时间: %y\n" somefile效果就是三行友好输出。这个技巧在生成人工巡检报告时非常实用。不过要注意,如果你在 shell 双引号里写 \t,需要确保 shell 不会先展开转义字符。在双引号里,bash 会把 \t 保留为两个字符传递给 stat,stat 自己会解释成制表符,因此没问题。真正要小心的是你在单引号里写,状态一致,但在某些平台上 stat 的转义支持并不完美,比如 macOS 的 stat 语法就和 Linux 不同,这点后面专门讲。
3.3 实战:一行命令导出备份清单
我在做日志备份时会用 stat 输出每个文件的绝对路径、大小、mtime 的 epoch 值,然后排序输出:
find /data/logs -type f -name "*.log" -exec stat -c "%n %s %Y" {} \; | sort -k3 -n > backup_manifest.txt这行命令做了三件事。第一,用 find 枚举出所有日志文件;第二,用 stat 提取文件名、字节数、mtime epoch 秒;第三,按时间排序生成清单。后续同步脚本读取该清单,把 7 天前的文件归档即可。我之所以不用 ls,是因为 stat 的 %Y 能稳定输出 epoch 时间,避免各种本地化格式差异,跨服务器执行结果一致。
4. 文件系统视角:-f 选项与存储细节
4.1 查看文件系统总体状态
stat 不仅能看文件,还能看文件系统。命令 stat -f 目录,会输出所在文件系统的基本信息:
File: "/data" ID: 912c0b7f1a2d3e4f Namelen: 255 Type: ext2/ext3 Block size: 4096 Fundamental block size: 4096 Blocks: Total: 1024000 Free: 523000 Available: 487000 Inodes: Total: 1048576 Free: 998222第一行里,Type 显示文件系统类型,ID 是文件系统标识,Namelen 是文件名最大长度。最下方的 Blocks 和 Inodes 信息很关键:Blocks Total 是文件系统总块数,Free 是空闲块数,Available 是非 root 用户可用的块数,二者差异来自保留块;Inodes Total 和 Free 则直接反映 inode 是否耗尽。
遇到“磁盘快满了但 df 看还有空间”的诡异问题,第一反应就应该是查 inode。用 stat -f 看 Inodes.Free,如果这个值接近 0,说明文件系统 inode 耗尽,磁盘再大也创建不了新文件。经典事故现场往往是邮件服务器或消息队列的临时目录,海量小文件把 inode 吃光。
4.2 块大小与 I/O 性能的关系
IO Block 是文件系统层面的块大小,默认多为 4096 字节。这也是大多数文件系统优化 I/O 的原子单位。理解这个数字的意义在于:当文件大小不是块大小的整数倍时,文件系统仍要分配整个块。我们系统里有大量 1 字节的小文件,每个文件至少占 4096 字节物理空间,所以磁盘浪费率极高。
判断一个文件是否稀疏文件也可以结合字段来看。稀疏文件是那些逻辑大小很大,但实际数据块很少的文件,典型代表是虚拟机磁盘镜像和数据库预分配文件。stat 输出中,Size 字段巨大,但 Blocks 字段很小,就能确认文件是稀疏的。例如一个 10GB 的稀疏镜像可能只有 1GB 的实际块,这种文件用 tar 打包时如果不用 -S 选项,会把它整个展开,既慢又占空间。
5. 符号链接与目录:stat 的隐藏逻辑
5.1 默认不跟随符号链接的含义
Linux 下执行 stat 一个符号链接,默认显示的是链接本身的信息,而不是链接指向的目标文件信息。看输出开头:
File: /usr/bin/python -> /usr/bin/python3 Size: 18 Blocks: 0 IO Block: 4096 symbolic linkSize 显示 18,因为链接的目标路径字符串占用 18 字节,而 Block 为 0,说明符号链接不占数据块。这跟 ls -l 看到的行为一致。想查看链接指向的真实文件,需要加 -L 参数,或不加区分直接用真实路径。
一个常见坑:stat 链接,然后用 %s 想获取目标文件大小,在脚本里计算就会出错。正确的做法是 stat -L 链接文件确定目标信息。类似地,在 find 里如果用 -type l 找到的路径直接传给 stat 而不加 -L,得到的只是链接的元数据。线上排查时,这两者之间的差异可能产生严重误判。
5.2 通过 Links 字段判断目录结构
对目录执行 stat,Links 字段有特殊含义。新建一个空目录,Links 是 2。为什么?因为目录自己的条目算一个,子目录条目 . 算一个。再创建一个子目录后,Links 变成 3:目录自己、. 子目录、子目录自带的 .. 指向父目录。所以 Links 数量等于直接子目录数加 2。
利用这个规律,可以用一条命令估算某个目录下子目录的数量,而不必列出全部内容:
stat -c "%h" /path/to/dir这个方法在目录包含海量文件时特别有用,因为 ls -l 目录时只需要读取目录项统计,但 stat 直接读取 inode,速度快得多。当然 Links 只统计子目录数量,不统计普通文件。
6. 常见问题排查与跨平台差异
6.1 为什么 stat 的时间与 ls 不一致
这个问题我至少被问过十次。核心在于 ls 的默认行为和 stat 展示的字段范围不同。ls -l 默认显示 mtime,而 stat 默认输出的第一眼时间是 atime。如果只看 stat 输出里的第一行时间,自然和 ls 对不上。
还有更隐蔽的情况:ls -l 显示的时间精确到秒,而 stat 显示到纳秒。一个文件在 10:22:10.99 被改动,ls 显示 10:22:10,stat 显示 10:22:10.99。如果你在两个服务器之间做基于时间戳的同步,建议统一用 stat -c "%Y" 生成的 epoch 秒来判断,否则秒级以下的差异会直接导致文件反复同步。
另一个坑:用 touch 修改文件时间。touch -m 修改 mtime 后,ctime 也会变为当前时间,因为元数据发生了变更。你看到 mtime 是你设定的旧时间,ctime 却是当前时间,而 ls 显示的恰好是 mtime,所以一切正常;但 stat 会展示 ctime 变化,这常让初学者误以为文件被改动过。实际上“有东西变了”是正确的,变的是元数据而非内容。
6.2 macOS 和 BusyBox 的兼容性问题
Linux 发行版基本都遵循 GNU coreutils 的 stat 语法,但 macOS 的 stat 源自 BSD,参数完全不同。macOS 上默认 stat 的输出字段就大不相同,而且自定义格式用的是 -f,占位符前缀也是 %,但具体语义略有差别。我踩过的最经典坑:把 Linux 脚本里的 stat -c "%n %s" 放到 macOS 上执行,直接报错。
BusyBox 环境(常见于路由器、容器精简镜像)的 stat 也不支持全部 GNU 扩展。如果需要写一个跨平台脚本,最稳妥的方案是避免直接依赖 stat 的输出格式,改用其他工具组合。例如用 wc -c 获取文件字节数,用 date -r 文件获取 mtime 的 epoch 值。尽管效率略低,但兼容性极佳。
我自己的习惯是:生产环境脚本固定在 Linux 上跑,用 stat -c;本地开发若涉及 macOS,单独封装一层函数,检测到系统类型后切换不同的参数。
6.3 在使用 stat 时保护文件名安全
在脚本里处理文件名是很需要注意的,stat 的输出同样可能被空格、换行甚至引号影响。按行解析 stat 输出时,带空格的文件名会把一列拆成多列,导致解析失败。更极端的情况是文件名本身包含换行符,这会直接割裂整段输出。
推荐的做法是用空字节 \0 作为记录分隔符。GNU 工具集普遍支持 -print0、-0 这类选项,但 stat 本身不支持。所以我在脚本里通常用 find -print0 生成文件列表,再用 while read -d '' 循环读取,在循环内部逐个执行 stat。例如:
find /data -type f -print0 | while IFS= read -r -d '' f; do stat -c "%n|%s" "$f" done这样无论文件名里有什么特殊字符,都不会破坏整个循环流程。
7. 深入脚本:统计目录、检查系统调用
7.1 用 stat 统计超大目录文件分布
有人问过我怎么快速统计一个目录下文件大小分布。用 du 只能看总量,用 find 和 awk 也行,但 stat 可以一次性获取更精确的信息。下面这段脚本输出每个文件的大小,并按 MB 单位做简单分桶:
find /var/log -type f -name "*.log" -exec stat -c "%s" {} \; | awk '{if ($1 < 1024) small++; else if ($1 < 1048576) medium++; else large++} END {print "small:" small, "medium:" medium, "large:" large}'这里实际是从管道读入 stat 输出的字节数,再交给 awk 分桶。虽然是基础用法,但胜在链条短、依赖少,任何 Linux 环境都能跑。
要找出目录里最大的 10 个文件,同样可以交给 stat:
find /data -type f -exec stat -c "%s %n" {} \; | sort -rn | head -10这一行在手动排查磁盘占用时非常顺手,比 du 输出更直观,能直接看到精确文件大小和路径。
7.2 用 strace 观察 stat 的系统调用
如果你想理解 stat 命令到底做了什么,可以顺手用 strace 观察:
strace -e stat,lstat,statx stat /etc/hostname你会看到内核返回的文件状态数据结构。这一层对开发人员尤其有价值:Linux 内核从 stat 系统调用到 statx 的演进,带来了更丰富的时间戳字段和更一致的文件类型查询接口。现代工具链里,stat 底层已经在逐步迁移到 statx,但用户层 API 行为保持兼容。理解这层,你就明白为什么不同版本的 Linux 对出生时间的支持程度不同:取决于内核和文件系统是否提供相应字段。
8. 结合文件时间戳做篡改检测
8.1 一次真实的排查过程
有一回,某个系统里的配置文件总被“自动还原”,ls 看不出任何异常。我用 stat 对比了一份手动修改后的文件和另一份备份文件:
- 修改后的文件 mtime 正常,ctime 略晚于 mtime。
- 一份被“自动还原”的文件 mtime 却早于 ctime,说明内容写入后,还有另一进程对文件执行了 chmod 或 chown。
顺着 ctime 的线索,我在监控日志里找到了一个定期执行 chmod 的部署脚本。它把文件权限重置为 0644,ctime 因此变化。如果没有 stat,单看 mtime 永远定位不到根因。
类似地,检测攻击者是否替换过系统二进制文件,除了校验哈希,还可以对比 stat 输出的 Birth 时间。如果某个系统文件的 Birth 时间晚于系统安装时间,基本可以断定文件被删除重建过。这不是唯一的证据,但结合 inode 变化就能形成一条完整证据链。
8.2 在监控脚本里用 stat 做完整性快速判断
写一个最简单的文件完整性监控,不需要复杂的 AIDE 工具,先看关键字段:
stat -c "%s %Y %i" /etc/passwd记录基准值,下次对比如果 size、mtime、inode 任意一个变化,就触发告警。这套方案没法检测内容被修改但大小和 mtime 被刻意恢复的情况,但作为轻量级巡检已经够用。真正对抗性场景还是要上哈希校验,stat 只负责快速初筛。
9. 最后一组实用技巧与心得
9.1 用 stat 获取设备的文件类型
排查设备节点时,stat 的 %F 输出能直接告诉你节点类型。比如 /dev/null 显示 character special file,而普通存储盘分区显示 block special file。在写管理脚本时,根据类型决定操作路径,可以避免用 -c "$1" 误操作设备文件。设备文件的 Size 字段也有特殊含义,字符设备显示的不是字节数,而是设备号的高低位编码,这块新手容易看懵。
9.2 通配符与批量输出的注意事项
stat 支持同时接收多个文件参数,会为每个文件输出一段。如果你想输出成紧凑的单行格式,配合 -c 和参数列表非常高效:
stat -c "%n|%s|%Y" /etc/passwd /etc/group /etc/hosts但要小心,当参数里混入不存在的路径时,stat 会报错,但已存在的文件仍会输出结果,返回码的非零状态可能会被脚本误判。所以批量统计前,我习惯先用 ls 或 find 确认路径存在。
9.3 关于纳秒精度与文件系统
stat 输出的纳秒精度由文件系统决定。ext4 与 XFS 都支持纳秒时间戳,但某些老旧的或网络文件系统可能只保留到秒。如果你发现同一条命令在不同服务器上输出的时间戳精度不一致,多半是底层文件系统的能力差异,不是 stat 的问题。
另外,在容器环境里,/proc、/sys 这类伪文件系统的 stat 输出会显得很“特异”:Size 多为 0,Blocks 也为 0,Inode 数值很大。看到这些不必惊慌,这是内核虚拟文件的固有形态,读取内容才有实际意义。
9.4 一点经验总结
我在实际工作中养成的习惯是:要用文件信息时,先问自己“我需要的是 ls 的摘要,还是 stat 的完整元数据”。写给人看的用 ls,写给脚本用的用 stat,这是铁律。
如果只能记住三个关键点,我会建议:第一,mtime 代表内容变化,ctime 代表元数据变化,排查问题先分清二者;第二,脚本里取时间用 %Y 的 epoch 值,不要解析人类可读时间;第三,符号链接默认不跟随,需要目标信息时一定加 -L。
最后分享一个小技巧:在 bash 里设置 alias,把高频使用的 stat 格式固化下来。我自己常年轮换的两条 alias 是:
alias statf='stat -c "%n | %s bytes | mtime=%y | inode=%i"' alias statd='stat -c "%n type=%F perms=%A"'一行命令快速拿到最关心的字段,省得每次敲一长串格式串。这个习惯帮我省下了大量重复输入的时间,也降低了格式写错的风险。希望这篇文章能让你重新认识 stat,并且在实际运维和脚本里用得更顺手。