【问题标题】:Are PID-files still flawed when doing it 'right'?PID 文件在“正确”执行时是否仍然存在缺陷?
【发布时间】:2014-11-12 09:38:30
【问题描述】:

重新启动服务通常通过 PID 文件实现 - 即进程 ID 被写入某个文件,并根据该数字停止命令将终止进程(或在重新启动之前)。

当您考虑它时(或者如果您不喜欢这个,那么search)您会发现这是有问题的,因为每个 PID 都可以重复使用。想象一个完整的服务器重新启动,您在启动时调用“./your-script.sh start”(例如 crontab 中的 @reboot)。现在 your-script.sh 将杀死一个 任意 PID,因为它已经存储了来自实时 before 重启的 PID。

我可以想象的一种解决方法是存储附加信息,以便您可以执行 'ps -pid | grep ' 并且只有当这返回一些东西时,你才能杀死它。还是在可靠性和/或简单性方面有更好的选择?

#!/bin/bash

function start() {
  nohub java -jar somejar.jar >> file.log 2>&1 &
  PID=$!
  # one could even store the "ps -$PID" information but this makes the
  # killing too specific e.g. if some arguments will be added or similar
  echo "$PID somejar.jar" > $PID_FILE
}

function stop() {
  if [[ -f "$PID_FILE" ]]; then
    PID=$(cut -f1 -d' ' $PID_FILE)
    # now get the second information and grep the process list with this
    PID_INFO=$(cut -f2 -d' ' $PID_FILE)
    RES=$(ps -$PID | grep $PID_INFO)
    if [[ "x$RES" != "x" ]]; then
       kill $PID
    fi
  fi
}

【问题讨论】:

  • 顺便说一句,请不要为不是从环境中导入的变量使用大写的变量名,不要在测试中使用“x”废话(你甚至知道你为什么这样做吗?这个?)并使用参数扩展而不是分叉cut。另外,引用所有参数扩展。
  • 感谢您的提示!我在 bash 脚本中真的很痛苦。我在遇到空变量问题后读到的'x'废话,但可能原因是缺少“”或其他东西。我在哪里错过了报价?您能详细说明如何避免割伤吗?
  • @Karussell - re: where missing the quote? 在类似的地方:ps -$PID | grep $PIDINFO 应该是 ps "-$PID" | grep "$PIDINFO"。如果变量中有空格,那么它的值可能会混淆命令。

标签: linux bash


【解决方案1】:

PID 文件的问题是多方面的,不仅限于回收和重启。

更大的问题是 PID 文件中的信息与进程状态之间存在不可避免的断开/竞争。

这是使用PID文件的流程:

  1. 你 fork & exec 一个进程。 “父”进程知道分叉的 PID,并保证此 PID 专为其分叉保留。
  2. 您的父级将分叉的 PID 写入文件。
  3. 您的父母去世,同时保证 PID 独占性。
  4. 一个不同的进程读取PID文件中的数字。
  5. 不同的进程检查系统上是否存在与他读取的PID相同的进程。
  6. 不同的进程使用他读取的 PID 向进程发送信号。

在 (1) 中,一切都很好。我们有一个 PID,内核向我们保证该数字是为我们的预期进程保留的。

在 (2) 中,您将 PID 的控制权交给没有此保证的其他进程。本身不是问题,但这样的行为即使没有过错也很少。

在 (3) 中,您的父进程死亡。它本身就具有 PID 独占性的内核保证。它可能会也可能不会在 PID 上执行 wait(2)。预期进程的真实状态丢失了,我们只剩下 PID 文件中的一个标识符,它可能引用也可能不引用预期进程。

在(4)中,一个没有任何保证的进程读取PID文件,任何使用这个数字都只能任意成功。

在 (5) 中,一个没有任何保证的进程实际上使用标识符来做某事,这是我们实际上做坏事的第一点:我们使用一个进程标识符查询内核,该标识符可能引用也可能不引用预期的过程。我们将得到的答案将是具有该 PID 的进程的状态,根本不一定是我们预期的进程。

在 (6) 中,我们犯了最严重的错误:我们实际上是在执行变异操作,旨在影响我们最初启动的流程,但绝不保证该意图。我们可以向任何随机系统进程发出信号。

这是为什么?什么样的事情会弄乱 PID?

在 (1) 之后的任何地方,真正的进程都可能终止。只要父进程保持对 PID 的排他性的保证,内核就不会回收 PID。它仍然存在并引用您曾经的进程(我们称其为“僵尸”进程,您的真实进程已死,但 PID 仍为它单独保留)。没有其他进程可以使用此 PID 并表示它根本不会到达任何进程。

一旦父进程释放他的保证或在(3)之后,内核回收死进程的PID。僵尸消失了,PID 现在可以被任何其他分叉的新进程免费使用。假设您正在编译某些东西,就会产生数千个小进程。内核为每个随机或顺序(取决于其配置)选择新的 PID。你已经完成了,现在你重新启动 apache。内核会将已释放的死进程的 PID 重用于重要的事情。

不过,PID 文件仍然包含 PID。任何读取 PID 文件 (4) 的进程都假定这个数字是指您长期死掉的进程。

您对读取的数字执行的任何操作 (5) (6) 都将针对新流程,而不是旧流程。

不仅如此,您还不能在操作之前执行任何检查,因为在您可以执行的任何检查和您可以执行的任何操作之间存在不可避免的竞争。如果您首先查看ps 以查看您的进程的“名称”是什么(并不是说这是对任何事情的真正令人敬畏的保证,请不要这样做),然后发出信号,您的@987654326 之间的时间@check 并且您的信号仍然可以看到进程死亡,和/或被新进程回收。所有这些问题的根源在于内核没有在 PID 上给你任何独占使用保证,因为你不是它的父级。

故事的寓意:不要将您孩子的 PID 提供给其他任何人。父级且只有父级应该使用它,因为他是系统上唯一(保存内核)对其存在和身份有任何保证的人。

这通常意味着保持父进程存活,而不是发出终止进程的信号,而是与父进程对话;通过插座等。请参阅http://smarden.org/runit/ 等人。

【讨论】:

【解决方案2】:

作为runit 的替代方案,libslack 库中的@987654321@ 命令可以在客户端程序终止时自动重新生成 - 无需使用 PID 文件。

使用带有daemon 命令的命名守护程序允许您手动重新启动客户端程序;但是,这将创建一个 PID 文件,该文件可能会导致 race conditions,正如 lhunath 已经指出的那样。

# daemon example without PID file
daemon --respawn --acceptable=10 --delay=10 bash -- -c 'sleep 30'

# from: man daemon
# "If started with the --respawn option, the client process 
# will be restarted after it is killed by the SIGTERM signal."
#
# (Problem would be to reliably get e.g. the bash pid in the daemon example above.)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-01-15
    • 1970-01-01
    • 2021-10-06
    • 2019-11-19
    相关资源
    最近更新 更多