暗色模式

Linux 临时文件自动清理:从 /tmp 生命周期到 systemd-tmpfiles 策略

技术教程
2026-09-14
3
0
本文要点
  • Ubuntu 上 /tmp 默认不按时间清理:出厂规则是 D /tmp 1777 root root -,老化字段是 -(永不老化),所以每天跑一次的清理定时器对 /tmp 什么也不做——实测本机连续运行 4 天,/tmp 里躺着 3 天前的文件
  • 规则每行固定七段:类型 路径 权限 属主 属组 老化时间 参数- 表示该项不设置;文件放 /etc/tmpfiles.d/同名会覆盖 /usr/lib/tmpfiles.d/ 里的出厂规则
  • d = 建目录并清理内容,D = 建目录并在 --remove/开机时连同内容一起处理;真正「连目录本身一起删」的是 r/R
  • ⚠️ 老化判定取 atime、mtime、ctime 三者中最新的那个:只要有一个比「现在 − 老化时间」更新就不删。所以 touch -d '2 hours ago' 造不出「老文件」,ctime 伪造不了
  • ⚠️ --remove 只清 D/R 目录里的内容,不删目录本身,且完全无视老化时间;d 目录里的文件它一个都不动
  • 本机 systemd 249 没有 --dry-run,别照新版文档敲;安全测试用 --root=/tmp/xxx 把整套规则跑在替身目录上

Linux 临时文件自动清理:从 /tmp 生命周期到 systemd-tmpfiles 策略

服务器的磁盘被写满,十次里有八次不是业务数据涨得太快,而是某个临时目录——上传的中间文件、解压残留、会话缓存、程序崩溃留下的 core——只进不出。清理临时文件这件事听起来简单,落到「谁来清、多久清一次、清的时候哪些能删哪些不能删」上,就变成了一套需要显式声明的策略。

大多数发行版把这件事交给了 systemd-tmpfiles。它管的不只是 /tmp:开机时要重建的 /run 目录树、需要修正权限的日志文件、要按天老化的缓存目录,全都记在它的规则文件里。而它最容易踩的坑是——你以为它在清理,其实它什么都没做

本文在一台 Ubuntu 22.04 云服务器(KVM,1 核、内存 1.9G,内核 5.15.0-30-generic,systemd 249)上把整套机制跑了一遍,包括老化判定和 --remove 这两个语义和直觉不符的地方。

第一步:先确认你的 /tmp 到底会不会被清理

先看这台机器的实际状态:

uptime -s; uptime -p; ls -lt --time-style=long-iso /tmp/ | head -5
2026-09-09 13:56:46
up 4 days, 7 hours, 8 minutes
drwxr-xr-x 5 root root    4096 2026-09-11 17:33 rsync-demo
-rw-r--r-- 1 root root 1120822 2026-09-11 17:06 blog_home.png
drwx------ 3 root root    4096 2026-09-11 14:16 snap-private-tmp

机器从 09-09 开机到现在连续运行了 4 天,而 /tmp 里躺着 09-11(3 天前)的文件。这期间每天一次的清理定时器跑了 3 次,一个文件都没删。

原因在出厂规则里:

cat /usr/lib/tmpfiles.d/tmp.conf
systemctl list-timers systemd-tmpfiles-clean.timer --no-pager | head -2
D /tmp 1777 root root -
#q /var/tmp 1777 root root 30d
NEXT                        LEFT     LAST                        PASSED UNIT
Mon 2026-09-14 14:14:34 UTC 17h left Sun 2026-09-13 14:14:34 UTC 6h ago systemd-tmpfiles-clean.timer

关键在最后一列那个 -:它是老化字段,- 的意思是「永不老化」。--clean 只处理配了老化时间的条目,所以对 /tmp 而言,每天触发的那次清理是空转。

/tmp 里的东西什么时候才会消失?看开机时执行了什么:

grep -E 'ExecStart|Description' /lib/systemd/system/systemd-tmpfiles-setup.service
Description=Create Volatile Files and Directories
ExecStart=systemd-tmpfiles --create --remove --boot --exclude-prefix=/dev

开机时跑的是 --create --remove --boot——--remove 会清空 D 类型目录的内容/tmp 正是 D 类型。所以结论很明确:

Ubuntu 的 /tmp 是「开机清空」,不是「按时间清理」。 一台长期不重启的服务器,/tmp 会一直涨下去。

这也解释了一个常见现象:很多人以为 /tmp 有系统兜底就随便往里写,结果半年不重启的机器上 /tmp 撑爆了根分区。磁盘占满后的救火流程见 Linux 磁盘空间排查与清理:从 df 到 journald 的完整救火流程,但更好的做法是先把策略配好。

第二步:规则文件长什么样

所有出厂规则都在 /usr/lib/tmpfiles.d/,管理员自己的规则放 /etc/tmpfiles.d/。这台机器上有 23 个出厂文件:

ls /usr/lib/tmpfiles.d/ | head -8; echo ---; ls /etc/tmpfiles.d/
00rsyslog.conf
apport.conf
cryptsetup.conf
dbus.conf
debian.conf
home.conf
journal-nocow.conf
legacy.conf
---
screen-cleanup.conf

注意 screen-cleanup.conf 同时存在于两个目录。看实际生效的是哪一份:

systemd-tmpfiles --cat-config 2>/dev/null | grep -n 'tmpfiles.d/screen-cleanup.conf'
146:# /etc/tmpfiles.d/screen-cleanup.conf

只有 146 行那一处,/usr/lib/tmpfiles.d/screen-cleanup.conf 被同名的 /etc 版本顶掉了。这是覆盖机制的规则:同名文件,/etc。所以你要改一个出厂规则,不需要编辑 /usr/lib 下的文件(那会在包升级时被覆盖),而是复制一份到 /etc/tmpfiles.d/ 改内容。

--cat-config 输出的每一段前面都有一行 # <文件路径>,告诉你这条规则是谁写的——排查「这条规则哪来的」时非常有用:

# /usr/lib/tmpfiles.d/00rsyslog.conf
z /var/log 0775 root syslog -
z /var/log/auth.log 0640 syslog adm -
d /var/spool/rsyslog 0700 syslog adm -

# /usr/lib/tmpfiles.d/apport.conf
d /var/lib/apport 0755 root root -
d /var/lib/apport/coredump 0755 root root 3d

每行的七段格式是固定的:

含义说明
1类型单个字母,决定「做什么」(见下表)
2路径绝对路径,可带通配符(z/Z/x/X 等类型)
3权限八进制,如 0755- 表示不动
4属主用户名;- 表示不动
5属组组名;- 表示不动
6老化时间10d30d1h- 表示永不老化
7参数内容、符号链接目标、设备号等,大多数类型是 -

惯用的类型里,日常最常用的是这四个:

类型作用会不会删东西
d建目录,并清理目录里的内容按老化时间清内容,目录本身保留
D建目录,内容和目录一起交给 --remove/开机处理开机时清空内容
f建文件并写入第 7 段的内容只建不删
z调整已存在路径的权限/属主只改属性,不建不删
X把路径排除在清理范围外保护用的

第三步:写一条自己的规则并让它生效

给应用建两个目录:一个是运行时的(在 /run,重启即消失),一个是缓存(在 /tmp,需要按时间老化):

printf 'd /run/demo-app 0755 root root 10s\nD /tmp/demo-cache 0700 root root 30s\n' > /etc/tmpfiles.d/demo.conf; cat /etc/tmpfiles.d/demo.conf; systemd-tmpfiles --create /etc/tmpfiles.d/demo.conf; ls -ld /run/demo-app /tmp/demo-cache

规则文件内容与 --create 之后两个目录的实际权限:0755 与 0700,属主属组都是 root

几个要点:

  • --create 是幂等的,重复执行不会报错,已经存在且属性正确的目录它直接跳过;属性不对时会按规则里的值修正(这正是 z 类型存在的意义,d 也带修正能力)。
  • 权限位是精确生效的0755/run/demo-app0700/tmp/demo-cachels -ld 输出的 mode 与规则完全一致。这一点比「跑个 mkdirchmod」的脚本可靠——规则是声明式的,每次执行都会把状态拉回声明值。
  • 演示里用的是 10s / 30s 这种夸张的老化值,为的是几秒钟内就能观察到清理行为。语义和生产上的 10d / 30d 完全一样,s/m/h/d/w 都是合法单位。
  • --create 默认不会去创建 /etc/tmpfiles.d/所有规则里声明的路径——本机把它限制在指定的配置文件上(命令最后带上了 /etc/tmpfiles.d/demo.conf),这也是调试时的安全做法:只跑你刚写的那一条规则,不要一上来就全量 --create

顺带说一句和开机的关系:systemd-tmpfiles-setup.service 执行的是 --create --remove --boot,所以 d/D 声明的目录在每次开机时都会被重新建立/run 是 tmpfs,本来就没了),而带 ! 前缀的条目只会在这个开机阶段执行。想在开机时做一次性动作(比如清空某个缓存),用 ! 而不是写进 ExecStartPre

第四步:老化判定——为什么 touch -d 骗不过它

这是整套机制里最反直觉的一块。造一个「两天前的老文件」,看第一次清理会不会删它:

touch -d '2 hours ago' /tmp/demo-cache/aged.txt; stat -c '%n mtime=%y ctime=%z' /tmp/demo-cache/aged.txt; systemd-tmpfiles --clean /etc/tmpfiles.d/demo.conf; echo 第一次 clean:; ls /tmp/demo-cache; sleep 32; systemd-tmpfiles --clean /etc/tmpfiles.d/demo.conf; echo 32 秒后再 clean:; ls -l /tmp/demo-cache

第一次 clean 时 aged.txt 仍在,尽管 mtime 是两小时前、老化阈值只有 30 秒;等 32 秒后 ctime 也超出阈值,文件才被删除

结果值得逐行读:stat 显示这个文件的 mtime 是 19:05(两小时前),但 ctime 是 21:05(刚刚)——touch -d 只能改 atime 和 mtime,ctime 由内核维护,用户改不了。第一次 --clean 时老化阈值是 30 秒,文件的 mtime 明明老得多,却没有被删。等到 32 秒后 ctime 自己也超过了 30 秒阈值,第二次 --clean 才把它清掉。

规则写在 man 里(man 5 tmpfiles.d):

The age of a file system entry is determined from its last modification timestamp (mtime), its last access timestamp (atime), and (except for directories) its last status change timestamp (ctime). By default, any of these three (or two) values will prevent cleanup if it is more recent than the current time minus the age field.

翻译过来就是:取三个时间里最新的那个来判断。只要有一个比「现在 − 老化时间」更新,文件就保留。默认参与判定的组合是 abcmABM(小写针对文件、大写针对目录),其中目录的 ctime 被排除在外——因为清理目录内容这个动作本身就会改目录的 ctime,如果把它算进去,下一轮判定会被自己影响,永远清不干净。

由此得到两条实用推论:

  • 你不能用 touch 伪造「文件很老」。 想让一个文件被清掉,只能真的等够时间;这也意味着任何写入、改权限、改属主的动作都会刷新 ctime 从而重置老化计时——一个每天被程序重新写一次的缓存文件,永远不会到期。
  • 反过来,读文件也可能重置计时(atime)。在 relatime 挂载(现在的默认)下,atime 的更新被限制在「atime 早于 mtime」或「超过 24 小时」时才发生,所以影响有限,但排查「为什么这个文件清不掉」时,stat 的三个时间都要看一遍。三者的细节区别见 Linux 文件时间戳:atime、mtime、ctime 与 find 时间筛选

如果确实只想按某一种时间判定,新版 systemd 支持 age-by 前缀:把老化字段写成 bmA:1h 这种形式,表示只按 mtime/atime 判定(目录只按 mtime)、阈值为 1h。本机的 systemd 249 认识这个语法,但用 d 类型时会附带一条提示:

printf 'd /tmp/ab2 - - - - bmA:1h\n' > /etc/tmpfiles.d/ab2.conf; systemd-tmpfiles --create /etc/tmpfiles.d/ab2.conf; echo "rc=$?"; ls -ld /tmp/ab2
/etc/tmpfiles.d/ab2.conf:1: d lines don't take argument fields, ignoring.
rc=0
drwxr-xr-x 2 root root 4096 Sep 13 21:08 /tmp/ab2

目录照建、退出码依然是 0,只是那句 d lines don't take argument fields, ignoring 会让人心里没底——d 类型本来就不接受第 7 段参数,这里是在提醒你那一列被忽略了。真要用 age-by,先在 --root 替身目录里确认一遍判定结果是否符合预期。

第五步:--remove 的真实语义

--create--remove 看起来是一对,很容易以为「--remove 就是把 --create 建出来的东西删掉」。实际不是:

echo pay > /tmp/demo-cache/keep.txt; echo pay > /run/demo-app/keep.txt; systemd-tmpfiles --remove /etc/tmpfiles.d/demo.conf; echo 'D 类型的 /tmp/demo-cache:'; ls -l /tmp/demo-cache; echo 'd 类型的 /run/demo-app:'; ls -l /run/demo-app

--remove 之后 D 目录里的 keep.txt 消失了但目录本身还在,d 目录里的 keep.txt 完全没动

两个目录各放了一个 keep.txt--remove 之后:

  • D 类型的 /tmp/demo-cache:里面的 keep.txt 被删了,但目录本身还在
  • d 类型的 /run/demo-appkeep.txt 完全没被动

man 里对 --remove 的定义只有一句,正好解释了这两种表现:

If this option is passed, the contents of directories marked with D or R, and files or directories themselves marked with r or R are removed.

也就是说,--remove 只做两件事:清空 D/R 目录的内容,以及删除 r/R 标记的路径本身。它不会删除 d 目录里的文件,也不会删除 D 目录本身——想「连目录一起删」必须显式用 r(空目录)或 R(递归)。如果你写了一条 D 规则,指望它在部署回滚时把整个目录抹掉,那是抹不掉的。

还有一点:--remove 完全无视老化时间。上面的 keep.txt 是刚创建的(老化阈值 30 秒),照样被清掉——它执行的是「清空」,不是「清理」。

第六步:怎么安全地试规则

规则写错最坏的情况是删掉不该删的东西,所以验证手段比规则本身更重要。

⚠️ 先说一个版本坑:本机的 systemd 249 没有 --dry-run

systemd-tmpfiles --clean --dry-run /etc/tmpfiles.d/demo.conf 2>&1
systemd-tmpfiles: unrecognized option '--dry-run'

--dry-run 是后来版本才加上的,照着新文档在老机器上敲会直接报错——这不算安全,因为如果你把 --dry-run 拼错或者被 shell 吃掉,剩下的命令会老老实实执行。老版本上更可靠的做法是用 --root 把整套操作搬到一个替身目录里:

mkdir -p /tmp/roottest; printf 'D /var/cache/demo 0755 root root 5s\n' > /tmp/roottest/test.conf; systemd-tmpfiles --root=/tmp/roottest --create /tmp/roottest/test.conf; ls -ld /tmp/roottest/var/cache/demo; ls -ld /var/cache/demo
drwxr-xr-x 2 root root 4096 Sep 13 21:08 /tmp/roottest/var/cache/demo
ls: cannot access '/var/cache/demo': No such file or directory

--root=PATH 把 PATH 当成文件系统根,规则里的 /var/cache/demo 被解释成 /tmp/roottest/var/cache/demo真实的 / 一个字节都没被碰(最后一行确认了 /var/cache/demo 并不存在)。这是老版本上最接近「演习」的手段:规则语法、类型行为、老化判定都能验证,风险为零。

其他几个调试开关:

  • --prefix=PATH:只处理路径以该前缀开头的规则(可重复指定),适合「只想动 /var/cache 下的东西」时收窄范围;
  • --cat-config:看合并后的最终配置及每条规则的来源文件,排查「谁覆盖了谁」;
  • 直接把配置文件当参数传给命令(本文全程这么用):只执行这一个文件里的规则,避免误伤出厂规则;
  • --boot:加上才会执行带 ! 前缀的条目,手动调试时通常不需要。

另外,日常巡检可以用 systemd-tmpfiles --cat-config 输出里带老化时间的条目做个清单,确认每个「会删东西」的规则都符合预期——尤其是那些路径带通配符的 z/Z/x/X,写宽一个字符,作用范围就完全不同

小结

把位置、类型、时机三个维度理一遍,这套机制就不难用了:

你要的效果该写什么什么时候生效
建目录,内容按时间清理d /path mode user group 30d -每天一次的 --clean
建目录,开机时清空内容D /path mode user group - -开机 --create --remove --boot
目录连同内容一起删掉R /path - - - - -只在 --remove
修正已存在路径的权限z /path mode user group - -每次 --create
把某个路径排除在清理之外X /path - - - - -每次处理时

最后回到开头那个结论:默认配置下 /tmp 只靠开机清空。如果你的业务往 /tmp 写东西,要么接受「重启才清」,要么自己加一条带老化时间的规则(注意别把老化值设得太激进,把正在用的临时文件删掉会引发比磁盘占满更难查的故障)。真正需要长期保留又需要定期瘦身的目录,更适合放在 /var/cache/var/tmp 下用 d + 老化时间来管——那才是 systemd-tmpfiles 设计上最擅长的场景。

配套的两件事值得一起做:日志目录的轮转归 Linux logrotate 日志轮转:从轮转策略到防止磁盘写满 管,而清理任务本身的调度与排查(包括为什么你的定时任务没跑)见 Linux crontab 定时任务:从五字段语法到日志排查与常见坑systemd-tmpfiles-clean.timer 这类 systemd 定时器的写法与 journalctl 排查,则在 systemd 服务管理实战:从 service 单元到 timer 定时与 journalctl 日志排查 里有完整说明。

发表评论

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