每次开机都报 swap 单元已存在——一次 fstab 重复项排查
systemd-fstab-generator 每次开机都失败,原因只是 fstab 里多了一行。记录用生成器二进制复现、用时间窗对比日志来证明修复的方法,以及一处我自己的取证失误。
现象
每次开机,journal 里都有两条:
Failed to create unit file /run/systemd/generator/swapfile.swap, as it already exists. Duplicate entry in /etc/fstab?
systemd-fstab-generator failed with exit status 1
它自己的提示已经说得很清楚了,但这类"提示即答案"的报错反而值得先确认——因为按提示改了之后如果报错还在,你需要知道是没改对,还是另有原因。
第一步:确认功能是否真的受损
swapon --show
systemctl status swapfile.swap
free -h
结果:swap 正常挂载(/swapfile 1024 MiB,active)。所以生成器失败没有导致功能失效——systemd 遇到了第二个重复条目,跳过它,第一个已经生成的单元继续工作。
这一步决定了后面要不要紧急处理:不紧急,但值得修。原因不是功能,而是它会掩盖真正的 generator 报错。一个开机必现的已知噪音,会让人习惯性忽略这个位置,而下次出现在这里的可能是真问题。
第二步:不猜,直接复现
systemd 的 generator 就是普通可执行文件,可以用任意输出目录直接调用——这是"开机时会执行的东西"本身,比读文档或推测语义都可靠:
rm -rf /tmp/g{1,2,3} && mkdir -p /tmp/g{1,2,3}
/usr/lib/systemd/system-generators/systemd-fstab-generator /tmp/g1 /tmp/g2 /tmp/g3
echo "exit=$?"
三个参数对应 generator 的三个输出目录(normal / early / late)。修改前:exit=1 且 stderr 有那条报错。修改后:exit=0、stderr 为空,并在 /tmp/g1/ 里生成了正确的单元:
# /tmp/g1/swapfile.swap
[Unit]
Documentation=man:fstab(5) man:systemd-fstab-generator(8)
SourcePath=/etc/fstab
[Swap]
What=/swapfile
以及依赖链接 swap.target.requires/swapfile.swap -> ../swapfile.swap。
第三步:修,并证明修好了
问题就是 fstab 里 /swapfile 出现了两次。只删重复的那一行(保留第一条出现),其余内容一字不动:
awk '!($0=="/swapfile swap swap defaults 0 0" && seen++)' /etc/fstab > /tmp/fstab.new
diff /etc/fstab /tmp/fstab.new # 先看差异,确认只少了那一行
cp /tmp/fstab.new /etc/fstab
然后需要证明下次开机不会再报。直接重启代价太大,用 daemon-reload 代替——它会重新运行全部 generator:
systemctl daemon-reload
再用时间窗对比日志计数,把"改前"和"改后"分开:
journalctl --since '2026-09-09 07:47:00' --until '2026-09-09 07:55:00' | grep -c 'as it already exists' # 1
journalctl --since '2026-09-09 07:56:00' | grep -cic 'as it already exists' # 0
改后那一侧之所以有意义,是因为窗口内确实触发了两次 daemon-reload(journal 里有 Reloading. 记录)。如果窗口内什么都没发生,"0 条"就只是空证据。 这一点比数字本身重要。
一处我自己的取证失误
第一次跑生成器时我写的是:
/usr/lib/systemd/system-generators/systemd-fstab-generator /tmp/g1 /tmp/g2 /tmp/g3 2>&1 | tee /tmp/gen.err; echo "exit=$?"
这个 $? 取的是 tee 的退出码,恒为 0——和生成器是否成功毫无关系。当时输出的 exit=0 是完全无效的证据,而我差点就据此下结论了。
修正后:
/usr/lib/systemd/system-generators/systemd-fstab-generator /tmp/g1 /tmp/g2 /tmp/g3 2>/tmp/gen.err
rc=$?; echo "rc=$rc stderr_bytes=$(stat -c%s /tmp/gen.err)"
这暴露了一个普遍模式:管道会把退出码换成最后一个命令的退出码。任何"看某命令是否成功"的检查,只要中间有管道就可能失真。set -o pipefail 或者拆开是解法。
顺带还犯了个低级错误:路径写成了 /usr/lib/systemd/systemd-fstab-generator(少了 -generators/),得到 command not found。与系统状态无关,纯粹是手误——但它提醒了一件事:排查时出现的错误信息,先分清是自己命令的问题还是系统的问题,否则很容易把后者当成新故障。
为什么值得修
| 理由 | 说明 |
|---|---|
| 掩盖真实报错 | 开机必现的已知噪音会训练人忽略该位置 |
| 状态诚实 | systemctl --failed 里有一项,每次排查都要先排除它 |
| 成本极低 | 一行删除,且不涉及重启 |
有一条经验值得记下来:生成器失败不等于功能失效。fstab 的重复项、sysctl 的非法值、unit 文件里的未知指令——这些往往只让生成器/解析器报错并跳过,服务照常运行。所以遇到这类报错,先确认功能,再决定优先级。
顺带:判断"惰性残留"
这次还顺手清理了几个宿主机上已卸载但仍有配置文件占位(dpkg 状态为 rc)的包,以及它们留下的模块目录。判断标准同样是"是否在生效路径上":
# 状态为 rc 的包(已卸载,配置残留)
dpkg-query -W -f='${db:Status-Abbrev}\n' | grep -c '^rc '
# 残留目录是否属于任何包
dpkg -S /lib/modules/<版本>-generic
后者有个细节:dpkg -S 可能返回包名,但 dpkg-query -L(该包的文件清单)里只有目录本身,没有那些文件。也就是说那些文件是 depmod 运行时生成的,不属于任何包——所以 apt autoremove 删不掉它们,需要手动清理或直接 purge 掉那个 rc 状态的包。
先确认包没有 conffiles(dpkg-query -W -f='${Conffiles}\n' <包名> 输出为空),purge 才是纯粹的清目录、零风险。有 conffiles 的包 purge 会删掉你可能还想保留的配置。
小结
- 报错文本往往就是答案,但仍要确认功能影响、再用生成器二进制复现
- 证明修复要用时间窗对比 + 窗口内确有触发事件,只有"0 条"没有意义
- 管道会替换退出码:
cmd | tee log; echo $?给出的是tee的 - 生成器报警 ≠ 功能失效;
systemctl --failed里的常驻项值得清掉,因为它会掩盖下一个真问题