当启动器因为 PID 文件已存在而拒绝启动 OpenVPN 时,最自然的解读是隧道正在某处运行。这种解读出错的比例相当高,所以在删除任何东西之前都值得先核实。运行时目录里的文件只是一份记账,而记账会在崩溃、重启和中断的关机中残留下来,哪怕当时并没有留下任何进程。真正决定结论的是两个问题:是否存在存活的进程,以及那个进程是否属于当前这个实例。

先从服务入手,而不是先看文件

拒绝启动通常来自启动器,而不是 OpenVPN 本身。它读取一个路径,在那里发现了东西,于是在解析配置之前就停下了。所以第一个问题关于启动器的约定,而不是关于隧道。

读一读打印这条消息的 unit 或脚本,注意三件事:它检查的路径、它声明的服务类型,以及它到底是否负责写这个文件。现代 unit 可能使用 simple 或 notify 监督方式,根本不碰 PID 文件;而更老的 init 脚本可能自己创建 PID 文件。把一种模型的检查照搬到另一种模型,恰好会产生这类拒绝。在解释任何东西之前,先弄清楚当前用的是哪种模型。

只有到这一步,问题才变成“某个实例是否真的在运行”。

核对那个标识符是否属于你的实例

文件里的一个数字,只证明有人写下了这个数字。它不证明现在存在具有该标识符的进程,更不证明那进程就是你的,因为标识符在重启之后会被复用。

把进程启动时间与文件时间戳比对,把可执行文件与 unit 的命令比对,把服务归属与 unit 名称比对。这三项里任何一项对不上,你看到的就是记账,而不是隧道。

在运行容器或多个命名空间的主机上,同一个标识符可能在一处视图里有意义,在另一处视图里却不存在。看不到进程,也不能证明它不存在,前提是你的账户无权检查。下结论之前,先从启动器使用的上下文里去核对。

systemctl status [email protected]
ps -o pid,lstart,args -p "$(cat /run/openvpn/vpn.pid)"

启动器对文件承诺过什么

PIDFile= 告诉服务管理器从哪里读取主进程的标识符。管理器并不写这个文件,但它会在关机后删除残留的那个文件,这正是一次被中断的退出会留下文件的原因。

这种职责划分解释了两种常被当作 bug 报告的现象。文件从不出现,意味着守护进程或其脚本没能写出它,而不是监督方式配置错了。文件在停止后仍然残留,意味着关机流程从未走完。

这也解释了为什么现行指导建议:当监督机制能直接跟踪主进程时,应当避开这种约定。那份指导并不意味着可以随意改动打包 unit 的类型,因为守护进程的前台、fork 或通知行为必须与 unit 的声明相符。只改类型不改守护进程,你就会得到一个在隧道运行时报告失败、或者恰好相反的 unit。要么把两者配对,要么都别动。systemd 服务参考描述了这种归属划分。

干净关机与中断退出

干净停止会请求守护进程退出,等它结束,并让管理器收拾自己的记账。中断退出、进程被杀,或关机途中重启,都会跳过这段流程的一部分,剩下的就是那个文件。崩溃之后发现它,说明关机没有走完,而不是说明有什么在监听。

把文件的时间戳与上次启动、unit 上次停止的时间比对。

然后直接检查活证据。绑定在 VPN 端口上的 socket,以及带地址的隧道接口,才是“有东西在跑”的证据,而运行时目录里的文本文件从来不是。两者都不在,那就什么都没在跑,无论文件怎么写。

别再习惯性地删掉这个文件

在确认没有任何实例持有它、且启动器明确记录了这种行为之后,删除才是正当的恢复步骤。

如果把它当成每次启动前的固定习惯,反而会废掉你想绕过的那项检查。倘若每次干净停止之后过期文件都会回来,那就是写入方与关机生命周期对“谁拥有这个路径”的判断不一致,而这种不一致才是值得修掉的 bug。

保留一条独立进入主机的通道,用常规管理器启动一个实例,再读第一个结果。下一个人拿到的 unit,要么能用,要么能自解释。

相关文章