Chips

每次开机都报 swap 单元已存在——一次 fstab 重复项排查

· 约 4 分钟 · 1460 字

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 会删掉你可能还想保留的配置。

小结

← 全部文章