如何守护UNIX中的任意脚本?


93

我想要一个守护程序,它可以将任意的通用脚本或命令转换为守护程序

我有两种常见的情况要处理:

  1. 我有一个应该永远运行的脚本。如果它死了(或重新启动时),请重新启动它。永远不要让两个副本同时运行(检测一个副本是否已经在运行,在这种情况下不要启动它)。

  2. 我有一个简单的脚本或命令行命令,希望永久重复执行(两次运行之间有短暂的暂停)。再次,不允许脚本的两个副本同时运行。

当然,在案例2中围绕脚本编写一个“ while(true)”循环然后为案例1应用解决方案是很简单的,但是更通用的解决方案将直接解决案例2,因为这适用于案例1中的脚本,例如好(你可能只是想更短或没有停顿如果脚本不打算会死(当然,如果剧本真的没有永不再死暂停实际上并没有物质))。

请注意,该解决方案不应该涉及在现有脚本中添加文件锁定代码或PID记录。

更具体地说,我想要一个可以“运行”的程序“守护进程”

% daemonize myscript arg1 arg2

或者,例如

% daemonize 'echo `date` >> /tmp/times.txt'

它将不断增加的日期列表附加到times.txt。(请注意,如果要守护的参数是一个脚本,该脚本可以像上面的情况1那样永久运行,那么守护程序仍然会做正确的事,并在必要时重新启动它。)然后,可以在.login中输入上述命令和/或每小时或每分钟刷新一次(取决于我对它意外死亡的担心程度)。

注意:守护程序脚本将需要记住正在守护程序的命令字符串,这样,如果再次守护相同的命令字符串,它将不会启动第二个副本。

此外,理想情况下,该解决方案应该在OS X和linux上都可以工作,但是欢迎使用一种解决方案。

编辑:如果您必须使用调用它,这很好sudo daemonize myscript myargs

(如果我认为这一切都是错误的,或者有快速而肮脏的部分解决方案,我也很乐意听到。)


PS:如果有用,这是一个特定于python的类似问题。

这个回答类似的问题有什么似乎是一个任意脚本的快速和肮脏的妖魔化一个有用的成语:


1
请参阅serverfault.com/questions/311593/…以获取纯Shell版本
w00t 2014年

Answers:


90

您可以使用nohup和&运算符来守护Unix中的任何可执行文件:

nohup yourScript.sh script args&

nohup命令允许您关闭shell会话而不会杀死您的脚本,而&会将您的脚本置于后台,以便您获得shell提示以继续您的会话。唯一的小问题是标准输出和标准错误都发送到./nohup.out,因此,如果您在此庄园中启动多个脚本,它们的输出将交织在一起。更好的命令是:

nohup yourScript.sh script args >script.out 2>script.error&

这会将标准发送到您选择的文件,并将标准错误发送到您选择的其他文件。如果您只想将一个文件用于标准输出和标准错误,则可以使用以下方法:

nohup yourScript.sh script args >script.out 2>&1 &

2>&1告诉外壳程序将标准错误(文件描述符2)重定向到与标准输出(文件描述符1)相同的文件。

要只运行一次命令并在命令消失时重新启动,可以使用以下脚本:

#!/bin/bash

if [[ $# < 1 ]]; then
    echo "Name of pid file not given."
    exit
fi

# Get the pid file's name.
PIDFILE=$1
shift

if [[ $# < 1 ]]; then
    echo "No command given."
    exit
fi

echo "Checking pid in file $PIDFILE."

#Check to see if process running.
PID=$(cat $PIDFILE 2>/dev/null)
if [[ $? = 0 ]]; then
    ps -p $PID >/dev/null 2>&1
    if [[ $? = 0 ]]; then
        echo "Command $1 already running."
        exit
    fi
fi

# Write our pid to file.
echo $$ >$PIDFILE

# Get command.
COMMAND=$1
shift

# Run command until we're killed.
while true; do
    $COMMAND "$@"
    sleep 10 # if command dies immediately, don't go into un-ctrl-c-able loop
done

第一个参数是要使用的pid文件的名称。第二个参数是命令。其他所有参数都是命令的参数。

如果您将此脚本命名为restart.sh,则将使用以下方式:

nohup restart.sh pidFileName yourScript.sh script args >script.out 2>&1 &

太棒了 谢谢。我想知道它是否也应该有一个延迟重新启动的选项。或者,也许最好只与此结合使用: stackoverflow.com/questions/555116/...
dreeves

4
这仅处理SIGHUP,还有其他(通常)致命信号应处理。
蒂姆·波斯特

改进此脚本的另一种方法可能是让其找到一个合适的位置,以自行放置$ PIDFILE,而不是要求将其指定为arg。它甚至自己都无法清除!(使用a应该很简单trap EXIT
史蒂文·卢

此外,考虑到使用<test是一个ASCII比较不是整数的比较。它可能仍然有效,但是可能导致错误。
史蒂文·卢

我已经在这里发布了对此脚本的修复程序。
史蒂文·卢

33

对于很长的答案,我深表歉意(请查看有关我的答案如何符合规格的评论)。我正在尝试变得全面,因此您要尽可能地站稳脚跟。:-)

如果您能够安装程序(具有root用户访问权限),并且愿意一次性完成设置脚本以执行守护程序的工作(即,与简单地指定要在命令行上运行的命令行参数相比,则需要更多的精力,但每个服务只需要执行一次),我有一种更强大的方法。

它涉及使用daemontools。文章的其余部分描述了如何使用daemontools设置服务。

最初设定

  1. 请遵循如何安装daemontools中的指示信息。一些发行版(例如Debian,Ubuntu)已经有针对它的软件包,因此只需使用它即可。
  2. 创建一个名为的目录/service。安装程序应该已经执行了此操作,但是只需进行验证,或者是否手动安装即可。如果您不喜欢该位置,则可以在svscanboot脚本中进行更改,尽管大多数daemontools用户习惯使用,/service并且如果不使用它会感到困惑。
  3. 如果您使用的是Ubuntu或其他不使用标准发行版的发行版init(即,不使用/etc/inittab),则需要使用预安装inittab的基础作为安排svscanboot由调用init。这并不难,但是您需要知道如何配置init操作系统使用的。 svscanboot是一个调用的脚本svscan,它执行寻找服务的主要工作;它是从中调用的,init因此init如果它死于任何原因,它将安排重新启动它。

每服务设置

  1. 每个服务都需要一个服务目录,该目录存储有关该服务的内务管理信息。您也可以为这些服务目录提供一个位置,以便将它们全部放在一个位置。通常我使用/var/lib/svscan,但是任何新位置都可以。
  2. 我通常使用脚本来设置服务目录,以节省大量的手动重复工作。例如,

    sudo mkservice -d /var/lib/svscan/some-service-name -l -u user -L loguser "command line here"

    some-service-name您要提供服务的名称在哪里,user用户以该服务身份运行服务loguser的用户,以及用户以记录器身份运行服务的用户。(仅对日志进行了一点解释。)

  3. 您的服务必须在前台运行。如果您的程序默认为后台,但可以选择禁用它,则可以这样做。如果您的程序背景无法禁用,请继续阅读fghack,尽管这是一个折衷方案:您无法再使用来控制程序svc
  4. 编辑run脚本以确保它正在执行您想要的操作。sleep如果您希望服务频繁退出,则可能需要在顶部拨打电话。
  5. 一切设置正确后,创建/service指向您的服务目录的符号链接。(不要将服务目录直接放在其中/service;这会使从其svscan监视中删除服务变得更加困难。)

记录中

  1. daemontools的记录方式是让服务将日志消息写入标准输出(如果使用的是由生成的脚本,则将标准错误写入日志mkservice);svscan负责将日志消息发送到日志记录服务。
  2. 日志记录服务从标准输入中获取日志消息。由生成的日志记录服务脚本mkservice将在log/main目录中创建自动旋转的带有时间戳的日志文件。当前的日志文件称为current
  3. 日志记录服务可以独立于主服务启动和停止。
  4. 通过管道日志文件tai64nlocal会将时间戳转换为人类可读的格式。(TAI64N是具有纳秒计数的64位原子时间戳。)

控制服务

  1. 使用svstat得到一个服务的状态。请注意,日志记录服务是独立的,并且具有其自己的状态。
  2. 您可以使用来控制服务(启动,停止,重新启动等)svc。例如,要重新启动服务,请使用svc -t /service/some-service-name-t表示“发送SIGTERM”。
  3. 其他可用信号包括-hSIGHUP),-aSIGALRM),-1SIGUSR1),-2SIGUSR2)和-kSIGKILL)。
  4. 要关闭服务,请使用-d。您还可以通过down在服务目录中创建一个文件来阻止服务在启动时自动启动。
  5. 要启动服务,请使用-u。除非您先前已将其降低(或将其设置为不自动启动),否则不需要这样做。
  6. 要问主管退出,使用-x;通常也用于-d终止服务。这是允许删除服务的常用方法,但是您必须先取消链接服务/service,否则svscan将重新启动管理程序。另外,如果您使用日志记录服务(mkservice -l)创建了服务,请记住svc -dx /var/lib/svscan/some-service-name/log在删除服务目录之前也退出日志记录管理程序(例如)。

摘要

优点:

  1. daemontools提供了一种创建和管理服务的安全方式。我将其用于服务器,强烈建议使用。
  2. 它的日志记录系统非常强大,服务自动重启功能也非常强大。
  3. 因为它以您编写/调整的Shell脚本启动服务,所以您可以根据自己的喜好定制服务。
  4. 强大的服务控制工具:您可以将几乎任何信号发送到服务,并可以可靠地启动和关闭服务。
  5. 确保为您的服务提供干净的执行环境:它们将在提供的服务,环境,流程限制等相同的条件下执行init

缺点:

  1. 每个服务都需要一些设置。值得庆幸的是,每个服务只需要执行一次。
  2. 服务必须设置为在前台运行。另外,为了获得最佳结果,应该将它们设置为记录到标准输出/标准错误,而不是syslog或其他文件。
  3. 如果您不熟悉daemontools的处理方式,则学习曲线会很陡。您必须使用来重新启动服务svc,并且不能直接运行运行脚本(因为它们将不受主管的控制)。
  4. 许多内部管理文件,以及许多内部管理过程​​。每个服务都需要其自己的服务目录,并且每个服务都将使用一个管理程序进程来自动重启该服务(如果终止)。(如果你有很多的服务,你会看到大量supervise在你的进程表的过程。)

总而言之,我认为daemontools是满足您需求的出色系统。我欢迎有关如何设置和维护它的任何问题。


我的答案是如何规范的:1.您必须设置服务,因此,只要您不设置重复项(并且只要您的服务本身没有后台),就不会出现重复项。2. supervise主管负责重新启动退出的所有服务。两次重播之间等待一秒钟;如果您的时间不足,请在服务运行脚本顶部进入休眠状态。
克里斯·杰斯特·杨

2a。supervise由本身支持svscan,因此,如果主管死亡,它将重新启动。2b。svscan由提供支持init,它将svscan根据需要自动重启。2c。如果您init死于任何原因,无论如何您都会被搞砸。:-P
克里斯·杰斯特·杨

为了回答有关内务处理的其他问题,daemontools系统不使用PID文件,因为它们可能会过时。相反,所有过程信息都由支持特定服务的主管保留。监督员维持一堆在服务目录中的文件(和FIFO)的那个工具,如svstat并且svc可以工作。
克里斯·杰斯特·杨

3
在SO和整个网络中,我们应该有更多这样的帖子。不仅是关于如何达到所需效果的食谱,而且还需要麻烦地解释食谱。为什么我不能多次投票呢?:|
skytreader 2014年

12

我认为您可能想尝试一下start-stop-daemon(8)。查看/etc/init.d任何Linux发行版中的脚本以获取示例。它可以通过命令行调用或PID文件找到启动的进程,因此它可以满足您的所有要求,但可以作为脚本的看门狗。但是,您始终可以启动另一个守护程序看门狗脚本,该脚本将在必要时重新启动您的脚本。


Fedora中没有start-stop-daemon,因此依赖于它的脚本不能移植。请参阅:fedoraproject.org/wiki/Features/start-stop-daemon
Bengt

对于OSX用户而言,只是一个提示:start-stop-daemon两者都不存在(自10.9起)。
mklement0 2013年

@ mklement0好吧...近五年来发生了很多变化。
Alex B

我的,时间过得真快。start-stop-daemon仍然活着并在Linux上发展;但是在阅读了答案stackoverflow.com/a/525406/45375之后,我意识到OSX会做自己的事情:launchd
mklement0 2013年

12

您应该看看daemonize。它允许检测第二个副本(但它使用文件锁定机制)。它还适用于不同的UNIX和Linux发行版。

如果需要自动将应用程序作为守护程序启动,则需要创建适当的初始化脚本。

您可以使用以下模板:

#!/bin/sh
#
# mydaemon     This shell script takes care of starting and stopping
#               the <mydaemon>
#

# Source function library
. /etc/rc.d/init.d/functions


# Do preliminary checks here, if any
#### START of preliminary checks #########


##### END of preliminary checks #######


# Handle manual control parameters like start, stop, status, restart, etc.

case "$1" in
  start)
    # Start daemons.

    echo -n $"Starting <mydaemon> daemon: "
    echo
    daemon <mydaemon>
    echo
    ;;

  stop)
    # Stop daemons.
    echo -n $"Shutting down <mydaemon>: "
    killproc <mydaemon>
    echo

    # Do clean-up works here like removing pid files from /var/run, etc.
    ;;
  status)
    status <mydaemon>

    ;;
  restart)
    $0 stop
    $0 start
    ;;

  *)
    echo $"Usage: $0 {start|stop|status|restart}"
    exit 1
esac

exit 0

1
看起来像是正确答案的候选人。特别要考虑其“单实例检查”。
马丁·威克曼

这可能是最好的答案-我不确定-但是,如果您认为是这样,您是否还可以提供一个解释,说明我在问题中给出的说明为什么是错误的?
dreeves 2010年

我不喜欢killproc停下来的部分:如果您有一个进程,例如ran java,那killproc也会导致所有其他Java进程也被杀死。
克里斯·杰斯特·杨

1
从/etc/rc.d/init.d/functions,守护进程只是从一个新的shell启动二进制文件:$ cgroup $ nice / bin / bash -c $corelimit >/dev/null 2>&1 ; $*所以我怀疑它将要守护任何东西...
mbonnin

1
我知道这很旧,但是对于以后找到它的人来说……这是正确的。/etc/init.d/functions中定义的“守护程序” 实际上并没有为您守护程序。这只是一个包装做的cgroup,检查进程是否已经在运行,设置用户,组好和的ulimit值等,并没有守护进程的过程中为您服务。那仍然是你自己的工作。:)
jakem

7

作为已经提到的daemonize和的替代daemontools,还有libslack软件包的daemon命令。

daemon 是非常可配置的,并且确实关心所有繁琐的守护程序,例如自动重启,日志记录或pidfile处理。


5

如果您专门使用OS X,建议您看看启动的工作方式。它将自动检查以确保您的脚本正在运行,并在必要时重新启动它。它还包括各种调度功能等。它应同时满足要求1和2。

为了确保脚本只能运行一个副本,您需要使用PID文件。通常,我将文件写入/var/run/.pid,其中包含当前正在运行的实例的PID。如果文件在程序运行时存在,它将检查文件中的PID是否正在实际运行(程序可能已崩溃或以其他方式忘记了删除PID文件)。如果是,请中止。如果不是,请开始运行并覆盖PID文件。


5

Daemontools(http://cr.yp.to/daemontools.html)是由dj bernstein编写的一组相当强大的实用程序,用于执行此操作。我已经成功地使用了它。烦人的是,运行这些脚本时,没有一个脚本返回任何可见的结果-只是不可见的返回代码。但是一旦运行,它就可以防弹。


是的,我也打算编写一个使用daemontools的条目。我将写自己的帖子,因为我希望我的答案更加全面,并希望以这种方式获得赏金。我们拭目以待。:-)
克里斯·杰斯特·杨

3

首先createDaemon()http://code.activestate.com/recipes/278731/获取

然后是主要代码:

import subprocess
import sys
import time

createDaemon()

while True:
    subprocess.call(" ".join(sys.argv[1:]),shell=True)
    time.sleep(10)

哦,谢谢!是否想让它更通用一些,以便您可以“守护foo arg1 arg2”以及“守护'foo arg1 arg2”?
dreeves,

好的,它现在将加入参数-但是,如果您想在参数内部留空格,则必须更改它。
道格拉斯·里德

谢谢道格拉斯!但是有一个很大的缺陷:运行两次“ daemonize foo”会启动foo运行的两个副本。
dreeves,

您可以添加一些PID记录代码,但是最好只运行一次脚本...
Douglas Leeder'Feb

我有点认为这是整个“守护进程”包装器概念的基础。(例如,然后我可以每小时或每分钟对它进行一次计时,以确保它始终运行。)我是否在考虑这个错误?createDaemon是否已经保证以某种方式进行?重启后该怎么办?
09年

1

这是一个正常工作的版本,带有示例,您可以将其复制到一个空目录中并进行尝试(安装CPAN依赖项后,这些依赖项包括Getopt :: LongFile :: SpecFile :: PidIPC :: System: :Simple-非常漂亮,非常适合所有黑客:您可以使用cpan <modulename> <modulename> ...)一次安装它们。


keepAlive.pl:

#!/usr/bin/perl

# Usage:
# 1. put this in your crontab, to run every minute:
#     keepAlive.pl --pidfile=<pidfile> --command=<executable> <arguments>
# 2. put this code somewhere near the beginning of your script,
#    where $pidfile is the same value as used in the cron job above:
#     use File::Pid;
#     File::Pid->new({file => $pidfile})->write;

# if you want to stop your program from restarting, you must first disable the
# cron job, then manually stop your script. There is no need to clean up the
# pidfile; it will be cleaned up automatically when you next call
# keepAlive.pl.

use strict;
use warnings;

use Getopt::Long;
use File::Spec;
use File::Pid;
use IPC::System::Simple qw(system);

my ($pid_file, $command);
GetOptions("pidfile=s"   => \$pid_file,
           "command=s"   => \$command)
    or print "Usage: $0 --pidfile=<pidfile> --command=<executable> <arguments>\n", exit;

my @arguments = @ARGV;

# check if process is still running
my $pid_obj = File::Pid->new({file => $pid_file});

if ($pid_obj->running())
{
    # process is still running; nothing to do!
    exit 0;
}

# no? restart it
print "Pid " . $pid_obj->pid . " no longer running; restarting $command @arguments\n";

system($command, @arguments);

example.pl:

#!/usr/bin/perl

use strict;
use warnings;

use File::Pid;
File::Pid->new({file => "pidfile"})->write;

print "$0 got arguments: @ARGV\n";

现在,您可以使用以下代码调用上面的示例: ./keepAlive.pl --pidfile=pidfile --command=./example.pl 1 2 3并且pidfile将创建文件,并且您将看到输出:

Pid <random number here> no longer running; restarting ./example.pl 1 2 3
./example.pl got arguments: 1 2 3

如果我理解正确,我相信这还不符合规范。在您的解决方案中(感谢btw!),必须修改您要守护的程序,以将其PID写入PID文件。我希望有一个可以守护任意脚本的实用程序。
dreeves 2010年

@dreeves:是的,但是有两种方法可以解决:1. keepAlive.pl调用的脚本(例如example.pl)可以简单地用作执行实际程序的包装,或者2. keepAlive.pl可以解析表的内容。活动的系统进程(使用CPAN的Proc :: ProcessTable)以尝试找到相关的进程及其pid)。
以太2010年

1

您也可以尝试Monit。Monit是一项监视和报告其他服务的服务。尽管它主要用作通知(通过电子邮件和短信)有关运行时问题的方法,但它也可以执行此处建议的大多数其他建议。它可以自动(重新)启动和停止程序,发送电子邮件,启动其他脚本以及维护您可以提取的输出日志。另外,由于有可靠的文档,我发现安装和维护都很容易。


1

您可以尝试永生不朽这是一个* nix跨平台(与OS无关)的主管。

快速试用macOS:

brew install immortal

如果您从端口或通过pkg 使用FreeBSD

pkg install immortal

对于Linux,通过下载预编译的二进制文件或从源代码下载:https : //immortal.run/source/

您可以像这样使用它:

immortal -l /var/log/date.log date

或通过配置YAML文件为您提供更多选项,例如:

cmd: date
log:
    file: /var/log/date.log
    age: 86400 # seconds
    num: 7     # int
    size: 1    # MegaBytes
    timestamp: true # will add timesamp to log

如果您还希望将标准错误输出也保存在单独的文件中,则可以使用以下方法:

cmd: date
log:
    file: /var/log/date.log
    age: 86400 # seconds
    num: 7     # int
    size: 1    # MegaBytes
stderr:
    file: /var/log/date-error.log
    age: 86400 # seconds
    num: 7     # int
    size: 1    # MegaBytes
    timestamp: true # will add timesamp to log

0

我在其他答案上做了一系列改进。

  1. 此脚本中的stdout完全由其子级的stdout组成,除非它由于检测到该命令已在运行而退出
  2. 终止后在其pidfile之后清除
  3. 可选的可配置超时时间(接受任何正数值参数,发送至sleep
  4. 使用提示开启 -h
  5. 任意命令执行,而不是单个命令执行。最后一个arg或剩余的args(如果一个以上的arg)被发送到eval,因此您可以将任何形式的shell脚本构造为字符串,以作为最后一个arg(或尾部args)发送到此脚本,以供其守护
  6. 参数计数比较用-lt代替<

这是脚本:

#!/bin/sh

# this script builds a mini-daemon, which isn't a real daemon because it
# should die when the owning terminal dies, but what makes it useful is
# that it will restart the command given to it when it completes, with a
# configurable timeout period elapsing before doing so.

if [ "$1" = '-h' ]; then
    echo "timeout defaults to 1 sec.\nUsage: $(basename "$0") sentinel-pidfile [timeout] command [command arg [more command args...]]"
    exit
fi

if [ $# -lt 2 ]; then
    echo "No command given."
    exit
fi

PIDFILE=$1
shift

TIMEOUT=1
if [[ $1 =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
        TIMEOUT=$1
        [ $# -lt 2 ] && echo "No command given (timeout was given)." && exit
        shift
fi

echo "Checking pid in file ${PIDFILE}." >&2

#Check to see if process running.
if [ -f "$PIDFILE" ]; then
    PID=$(< $PIDFILE)
    if [ $? = 0 ]; then
        ps -p $PID >/dev/null 2>&1
        if [ $? = 0 ]; then
            echo "This script is (probably) already running as PID ${PID}."
            exit
        fi
    fi
fi

# Write our pid to file.
echo $$ >$PIDFILE

cleanup() {
        rm $PIDFILE
}
trap cleanup EXIT

# Run command until we're killed.
while true; do
    eval "$@"
    echo "I am $$ and my child has exited; restart in ${TIMEOUT}s" >&2
    sleep $TIMEOUT
done

用法:

$ term-daemonize.sh pidfilefortesting 0.5 'echo abcd | sed s/b/zzz/'
Checking pid in file pidfilefortesting.
azzzcd
I am 79281 and my child has exited; restart in 0.5s
azzzcd
I am 79281 and my child has exited; restart in 0.5s
azzzcd
I am 79281 and my child has exited; restart in 0.5s
^C

$ term-daemonize.sh pidfilefortesting 0.5 'echo abcd | sed s/b/zzz/' 2>/dev/null
azzzcd
azzzcd
azzzcd
^C

请注意,如果您从其他目录运行此脚本,则它可能使用不同的pidfile,并且不会检测到任何现有的运行实例。由于它旨在运行和重新启动通过参数提供的临时命令,因此无法知道某项是否已经启动,因为谁会说这是否是同一命令?为了改进仅运行某事的单个实例的强制执行,需要针对具体情况的解决方案。

另外,为了使其能够充当适当的守护程序,您还必须使用(最低限度)nohup,如其他答案所述。我没有做出任何努力来对流程可能收到的信号进行恢复。

还有一点需要注意的是,杀死该脚本(如果从另一个被杀死的脚本中调用了该脚本,或者带有信号)可能无法成功杀死该孩子,尤其是在该孩子是另一个脚本的情况下。我不确定为什么会这样,但似乎与eval工作方式有关,这对我来说是个神秘的事物。因此,将行替换为仅接受单个命令的内容(如其他答案)是明智的。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.