如何在Unix中将任意脚本变成守护进程?

97

我需要一个能将任意通用脚本或命令转化为守护进程的程序。

有两种常见情况需要处理:

  1. 我有一个应该一直运行的脚本。如果它停止(或者重启),应该重新启动它。不要让其同时运行两个副本(如果已经有一个副本在运行,则检测并不启动)。

  2. 我有一个简单的脚本或命令行命令,我想一直执行下去(每次执行之间有短暂的暂停)。同样,不允许同时运行两个副本。

当然,在第二种情况下,将脚本放入"while(true)"循环中很容易,并应用第一种情况的解决方案,但是更通用的解决方案将直接解决第2种情况,因为这也适用于第1种情况中的脚本(如果脚本永远不会死亡,则您可能只希望短暂暂停或不暂停(当然,如果脚本确实永远不死亡,则暂停实际上并不重要))。

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

更具体地说,我希望有一个名为"daemonize"的程序,可以像这样运行:

% daemonize myscript arg1 arg2

或者,例如,
% daemonize 'echo `date` >> /tmp/times.txt'

这将使一个不断增长的日期列表添加到times.txt中。(请注意,如果daemonize的参数是一个像case 1一样永远运行的脚本,那么daemonize仍然会做正确的事情,在必要时重新启动它。) 然后我可以在我的.login中放置一个如上的命令,并每小时或每分钟执行它(具体取决于我对其意外死机的担忧程度)。

NB:daemonize脚本需要记住它正在守护进程的命令字符串,以便如果再次守护相同的命令字符串,它不会启动第二个副本。

此外,解决方案理想情况下应该适用于OS X和Linux,但欢迎针对其中之一的解决方案。

编辑:如果您必须使用sudo daemonize myscript myargs来调用它,也没问题。

(如果我对此的思考方式有误,或者有快速而肮脏的部分解决方案,我也很乐意听听。)


PS:如果有用的话,这里 有一个关于Python的类似问题。

这个回答给出了一种快速而简单的方法来使任意脚本成为守护进程的惯用语,看起来非常有用:


1
请参见 http://serverfault.com/questions/311593/keeping-a-linux-process-running-after-i-logout/312265#312265 获取纯Shell版本。 - w00t
13个回答

101
你可以使用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这个命令告诉shell将标准错误(文件描述符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 &

太棒了,谢谢。我在想它是否应该有一个重新启动延迟的选项。或者最好只是与这个一起使用:http://stackoverflow.com/questions/555116/repeat-a-unix-command-every-x-seconds-forever - dreeves
5
这只处理SIGHUP信号,还有其他(通常是致命的)信号应该被处理。 - Tim Post
改进这个脚本的另一种方法可能是让它自己想出一个好的位置来放置$PIDFILE,而不需要将其指定为参数。它甚至没有清理自己!(使用trap EXIT应该很简单) - Steven Lu
另外,请注意,在“test”中使用的“<”是ASCII比较,而不是整数比较。它可能仍然有效,但可能会导致错误。 - Steven Lu
我已经在这里发布了我对这个脚本的修复 here - Steven Lu
#!/bin/bash 是错误的;如果可能的话,请使用 #!/bin/sh 或者 #!/usr/bin/env bash。 - Good Person

35

很抱歉回答有点长(请见如何确定我的回答符合规范的评论)。我尽可能全面,这样你就能有更好的起点。:-)

如果你可以安装程序(拥有root访问权限),并且愿意进行一次性工作来设置脚本以执行守护进程(即比仅在命令行上指定命令行参数要复杂,但每个服务只需要做一次),那么我有一种更加强大的方式。

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

初始设置

  1. 按照如何安装daemontools中的说明操作。一些发行版(如Debian、Ubuntu)已经为其提供了包,所以只需使用该包即可。
  2. 创建一个名为/service的目录。安装程序应该已经完成了这个步骤,但要进行验证,或者手动安装。如果您不喜欢此位置,可以在您的svscanboot脚本中更改它,但大多数daemontools用户习惯于使用/service,如果您不使用它,他们就会感到困惑。
  3. 如果您使用Ubuntu或另一个不使用标准init(即不使用/etc/inittab)的发行版,则需要使用预安装的inittab作为安排svscanbootinit调用的基础。这并不难,但您需要知道如何配置您的操作系统所使用的initsvscanboot是一个调用svscan的脚本,在查找服务方面执行主要工作;它由init调用,因此如果出现任何原因导致其停止,init将安排重新启动它。

每个服务的设置

  1. Each service needs a service directory, which stores housekeeping information about the service. You can also make a location to house these service directories so they're all in one place; usually I use /var/lib/svscan, but any new location will be fine.
  2. I usually use a script to set up the service directory, to save lots of manual repetitive work. e.g.,

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

    where some-service-name is the name you want to give your service, user is the user to run that service as, and loguser is the user to run the logger as. (Logging is explained in just a little bit.)

  3. Your service has to run in the foreground. If your program backgrounds by default, but has an option to disable that, then do so. If your program backgrounds without a way to disable it, read up on fghack, although this comes at a trade-off: you can no longer control the program using svc.
  4. Edit the run script to ensure it's doing what you want it to. You may need to place a sleep call at the top, if you expect your service to exit frequently.
  5. When everything is set up right, create a symlink in /service pointing to your service directory. (Don't put service directories directly within /service; it makes it harder to remove the service from svscan's watch.)

日志记录

  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”会负责重新启动任何退出的服务。它在重新启动之间等待一秒钟;如果这对您来说不够时间,请在服务运行脚本的顶部放入一个休眠命令。
- C. K. Young
2a. supervise 本身由 svscan 支持,因此如果监管进程死亡,它将被重新启动。 2b. svscaninit 支持,init 将根据需要自动重新启动 svscan。 2c. 如果您的 init 由于任何原因而死亡,那么您就完了。:-P - C. K. Young
回答有关系统维护的其他问题,daemontools系统不使用PID文件,因为它们可能会过时。相反,所有进程信息都由支持给定服务的监督程序保留。监督程序在服务目录中维护一堆文件(和FIFO),像svstatsvc这样的工具可以使用它们。 - C. K. Young
3
SO和网络上应该有更多像这样的文章。不仅指导如何实现所需效果,而且要花心思解释方法。为什么我不能给这篇文章点赞超过一次呢? :| - skytreader

14
你应该看看 daemonize。它可以检测第二个副本(但它使用了文件锁定机制)。此外,它可以在不同的UNIX和Linux发行版上使用。
如果你需要自动启动你的应用程序作为守护进程,则需要创建适当的init脚本。
你可以使用以下模板:
#!/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

2
看起来这是正确答案的候选者。特别是考虑到它的“单实例检查”。 - Martin Wickman
这可能是最好的答案,但我不确定。如果您认为这是最佳答案,请您也能解释一下为什么我在问题中给出的规格是错误的吗? - dreeves
我不喜欢“killproc”命令在进程停止阶段的使用:举个例子,如果你有一个运行“java”的进程,“killproc” 命令将会杀死所有其他正在运行Java的进程。 - C. K. Young
1
从 /etc/rc.d/init.d/functions 中,daemonize 只是从新的 shell 启动二进制文件:$cgroup $nice /bin/bash -c $corelimit >/dev/null 2>&1 ; $*。因此我怀疑它不会使任何东西成为守护进程... - mbonnin
1
我知道这已经过时了,但对于任何后来发现这个问题的人...这是正确的。在/etc/init.d/functions中定义的“daemon”实际上并没有为您进行守护进程。它只是一个包装器,用于执行cgroups、检查进程是否已经运行、设置用户、设置好的和ulimit值等。它不会为您守护进程。这仍然是你自己的工作。 :) - jakem

12

我认为你可能想尝试使用start-stop-daemon(8)。在任何Linux发行版的/etc/init.d脚本中,都可以找到示例。它可以通过命令行或PID文件查找已启动的进程,因此它符合您所有要求,除了作为脚本看门狗之外。但是,您总是可以启动另一个守护进程看门狗脚本,如果必要,只需重新启动您的脚本。


在Fedora中没有start-stop-daemon,因此依赖它的脚本不具备可移植性。请参见:https://fedoraproject.org/wiki/Features/start-stop-daemon - Bengt
提醒一下OSX用户:截至10.9版本,也没有start-stop-daemon - mklement0
@mklement0 嗯...近5年来发生了很多变化。 - Alex B
时间过得真快啊。虽然在Linux上,“start-stop-daemon”依然活着并运转良好;但读完答案https://dev59.com/T3RB5IYBdhLWcg3wv5tA#525406后,我意识到OSX有它自己的东西:“launchd”。 - mklement0

7
作为 daemonizedaemontools 的替代方案,libslack 包中有 daemon 命令。 daemon 非常可配置,并且关心所有繁琐的守护进程事项,例如自动重启、日志记录或 pidfile 处理。

5

Daemontools(http://cr.yp.to/daemontools.html)是由dj bernstein编写的一组非常专业的工具,用于此类操作。我已经成功地使用了它。令人困扰的是,在运行脚本时,没有任何可见的结果返回,只有不可见的返回代码。但一旦它运行起来,就是无敌的。


是的,我本来也打算写一个使用daemontools的条目。我会写一篇自己的文章,因为我希望我的回答更加全面,希望以此获得奖励金。我们拭目以待。 :-) - C. K. Young

5
如果你使用的是OS X系统,我建议你了解一下launchd的工作原理。它会自动检查你的脚本是否在运行,并在必要时重新启动它。它还包括各种调度功能等,应该能够满足需求1和2。
至于确保只有一个副本的脚本在运行,你需要使用PID文件。通常我会将一个文件写入到/var/run/.pid,其中包含当前正在运行实例的PID。如果程序运行时该文件已经存在,则检查文件中的PID是否实际在运行(程序可能已经崩溃或忘记删除PID文件)。如果是,则终止操作。如果不是,则开始运行并覆盖PID文件。

3

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

然后是主要代码:

import subprocess
import sys
import time

createDaemon()

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

哦,谢谢!想要让它更通用一些,这样你就可以使用“daemonize foo arg1 arg2”和“daemonize 'foo arg1 arg2'”了吗? - dreeves
好的,现在它将连接参数 - 但是如果您想要在参数内部有空格,那么您必须更改它。 - Douglas Leeder
谢谢Douglas!不过有一个很大的缺陷:运行"daemonize foo"两次会启动两个foo的副本。 - dreeves
你可以添加一些PID记录代码,但最好只运行该脚本一次... - Douglas Leeder
1
我认为这是整个“daemonize”包装器概念的基础。例如,我可以每小时或每分钟cron它,以确保它始终在运行。我想错了吗?createDaemon是否已经保证了这一点?重启后呢? - dreeves

2
您可以尝试使用immortal,它是一款*nix跨平台(操作系统无关)的监控程序。
对于macOS的快速尝试:
brew install immortal

如果您是通过 ports 或使用 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

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

我认为如果我理解正确的话,这不太符合规范。在您的解决方案中(顺便说一下,谢谢!),您想要守护进程的程序必须被修改以将其PID写入PID文件。我希望有一个可以守护任意脚本的实用工具。 - dreeves
@dreeves:是的,但有两种方法可以解决这个问题:1.由keepAlive.pl调用的脚本(例如example.pl)可以简单地作为执行真正程序的包装器,或者2.keepAlive.pl可以解析活动系统进程表(使用CPAN的Proc::ProcessTable),以尝试找到相关进程及其pid。 - Ether

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接