如何在 unix 中守护任意脚本?
Posted
技术标签:
【中文标题】如何在 unix 中守护任意脚本?【英文标题】:How do I daemonize an arbitrary script in unix? 【发布时间】:2010-10-06 05:32:48 【问题描述】:我想要一个可以将任意通用脚本或命令转换为daemon 的守护程序。
我想处理两种常见的情况:
我有一个应该永远运行的脚本。如果它死了(或在重新启动时),请重新启动它。不要让两个副本同时运行(检测一个副本是否已经在运行,在这种情况下不要启动它)。
我有一个简单的脚本或命令行命令,我想永远重复执行(运行之间有短暂的停顿)。同样,不要让脚本的两个副本同时运行。
当然,在案例 2 中围绕脚本编写“while(true)”循环然后为案例 1 应用解决方案是微不足道的,但更通用的解决方案将直接解决案例 2,因为这适用于情况 1 也是(如果脚本不打算死掉,你可能只需要更短的暂停或没有暂停(当然,如果脚本真的确实永远不会死,那么暂停实际上并不重要)) .
请注意,该解决方案不应涉及,例如,向现有脚本添加文件锁定代码或 PID 记录。
更具体地说,我想要一个可以运行的程序“守护进程”
% daemonize myscript arg1 arg2
或者,例如,
% daemonize 'echo `date` >> /tmp/times.txt'
这将保留不断增长的附加到 times.txt 的日期列表。 (请注意,如果 daemonize 的参数是一个像上面的情况 1 一样永远运行的脚本,那么 daemonize 仍然会做正确的事情,在必要时重新启动它。)然后我可以在我的 .login 中放置一个类似上面的命令和/或每小时或每分钟 cron 它(取决于我对它意外死亡的担心程度)。
注意:daemonize 脚本需要记住它正在守护的命令字符串,这样如果再次守护相同的命令字符串,它就不会启动第二个副本。
此外,理想情况下,该解决方案应该适用于 OS X 和 linux,但欢迎使用其中一种或另一种的解决方案。
编辑:如果你必须用sudo daemonize myscript myargs
调用它也没关系。
(如果我认为这一切都错了,或者有快速而肮脏的部分解决方案,我也很想听听。)
PS:如果有用的话,here's 一个类似的特定于 python 的问题。
this 对类似问题的回答似乎是一个有用的成语,可以快速而肮脏地妖魔化任意脚本:
【问题讨论】:
纯shell版本见serverfault.com/questions/311593/… 【参考方案1】:您可以使用 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 &
【讨论】:
太棒了;谢谢。我想知道它是否也应该有一个延迟重启的选项。或者最好将它与它结合使用:***.com/questions/555116/… 这只处理 SIGHUP,还有其他(通常)致命的信号需要处理。 另一种改进此脚本的方法可能是让它自己找到一个放置 $PIDFILE 的好位置,而不是要求将其指定为 arg。它甚至不会自行清理! (使用trap EXIT
应该很简单)
另外,考虑<
在test
中的使用是ASCII 比较而不是整数比较。它可能仍然有效,但可能会导致错误。
我已经发布了我对这个脚本的修复here。【参考方案2】:
对于冗长的答案,我深表歉意(请参阅 cmets,了解我的答案如何符合规范)。我试图做到全面,所以你有尽可能好的一面。 :-)
如果您能够安装程序(具有 root 访问权限),并且愿意一次性完成设置脚本以执行守护程序(即,比简单地指定要在命令行,但每个服务只需要执行一次),我有一种更强大的方法。
它涉及使用daemontools。这篇文章的其余部分描述了如何使用 daemontools 设置服务。
初始设置
-
按照How to install daemontools 中的说明进行操作。一些发行版(例如 Debian、Ubuntu)已经有它的软件包,所以就使用它吧。
创建一个名为
/service
的目录。安装程序应该已经这样做了,但只需验证,或者手动安装。如果你不喜欢这个位置,你可以在你的 svscanboot
脚本中更改它,尽管大多数 daemontools 用户习惯使用 /service
,如果你不使用它会感到困惑。
如果您使用的是 Ubuntu 或其他不使用标准 init
(即不使用 /etc/inittab
)的发行版,则需要使用预安装的 inittab
作为安排的基础svscanboot
将由 init
调用。这并不难,但您需要知道如何配置您的操作系统使用的init
。
svscanboot
是一个调用svscan
的脚本,主要工作是寻找服务;它是从init
调用的,所以init
将安排在它因任何原因死亡时重新启动它。
按服务设置
-
每个服务都需要一个服务目录,该目录存储有关服务的内务管理信息。您还可以创建一个位置来存放这些服务目录,以便它们都在一个地方;通常我使用
/var/lib/svscan
,但任何新位置都可以。
我通常使用a script来设置服务目录,以节省大量手动重复工作。例如,
sudo mkservice -d /var/lib/svscan/some-service-name -l -u user -L loguser "command line here"
some-service-name
是您要为服务提供的名称,user
是运行该服务的用户,loguser
是运行记录器的用户。 (稍微解释一下日志记录。)
fghack
,尽管这是一个权衡:您无法再使用svc
控制程序。
编辑run
脚本以确保它按照您的意愿运行。如果您希望您的服务经常退出,您可能需要在顶部拨打sleep
电话。
一切设置正确后,在/service
中创建一个指向您的服务目录的符号链接。 (不要将服务目录直接放在/service
中;这会使从svscan
的手表中删除服务变得更加困难。)
记录
-
daemontools 的日志记录方式是让服务将日志消息写入标准输出(或标准错误,如果您使用的是由
mkservice
生成的脚本); svscan
负责将日志消息发送到日志服务。
日志服务从标准输入中获取日志消息。 mkservice
生成的日志服务脚本将在log/main
目录中创建自动轮换的、带有时间戳的日志文件。当前日志文件名为current
。
日志服务可以独立于主服务启动和停止。
通过tai64nlocal
传送日志文件会将时间戳转换为人类可读的格式。 (TAI64N 是一个以纳秒为单位的 64 位原子时间戳。)
控制服务
-
使用
svstat
获取服务的状态。请注意,日志服务是独立的,并且有自己的状态。
您可以使用svc
控制您的服务(启动、停止、重新启动等)。例如,要重新启动您的服务,请使用svc -t /service/some-service-name
; -t
表示“发送SIGTERM
”。
其他可用信号包括-h
(SIGHUP
)、-a
(SIGALRM
)、-1
(SIGUSR1
)、-2
(SIGUSR2
) 和@9876543732@ (SIGKILL
) @)。
要关闭服务,请使用-d
。您还可以通过在服务目录中创建一个名为 down
的文件来防止服务在启动时自动启动。
要启动服务,请使用-u
。除非您之前已将其关闭(或将其设置为不自动启动),否则这不是必需的。
要让主管退出,请使用-x
;通常与-d
一起使用也可以终止服务。这是允许删除服务的常用方法,但您必须先从/service
取消链接服务,否则svscan
将重新启动主管。
此外,如果您使用日志记录服务 (mkservice -l
) 创建了服务,请记住在删除服务目录之前还要退出日志记录主管(例如,svc -dx /var/lib/svscan/some-service-name/log
)。
总结
优点:
-
daemontools 提供了一种创建和管理服务的防弹方法。我将它用于我的服务器,我强烈推荐它。
其日志记录系统非常强大,服务自动重启工具也是如此。
因为它使用您编写/调整的 shell 脚本启动服务,所以您可以根据需要定制服务。
强大的服务控制工具:您可以向服务发送几乎任何信号,并且可以可靠地启动和关闭服务。
保证您的服务有一个干净的执行环境:它们将在与
init
提供的环境、进程限制等相同的环境下执行。
缺点:
-
每个服务都需要一些设置。值得庆幸的是,每次服务只需执行一次。
必须将服务设置为在前台运行。此外,为了获得最佳结果,应将它们设置为记录到标准输出/标准错误,而不是 syslog 或其他文件。
如果您不熟悉 daemontools 的工作方式,学习曲线会很陡峭。您必须使用
svc
重新启动服务,并且不能直接运行运行脚本(因为它们将不受主管的控制)。
大量的内务管理文件和大量的内务处理流程。每个服务都需要自己的服务目录,每个服务使用一个主管进程在服务死亡时自动重启服务。 (如果您有许多服务,您将在进程表中看到 lots 个 supervise
进程。)
总的来说,我认为 daemontools 是满足您需求的优秀系统。我欢迎任何关于如何设置和维护它的问题。
【讨论】:
我的回答如何确定规范: 1. 您必须设置服务,因此只要您不设置重复项(并且只要您的服务本身没有背景),就没有重复项会发生。 2.supervise
,主管,负责重启任何退出的服务。它在重新启动之间等待一秒钟;如果时间不够,请在服务运行脚本的顶部进入睡眠状态。
2a。 supervise
本身由 svscan
支持,因此如果主管死了,它将重新启动。 2b。 svscan
由 init
支持,它会根据需要自动重启 svscan
。 2c。如果您的init
因任何原因而死,无论如何您都会被搞砸。 :-P
为了回答有关家务的其他问题,daemontools 系统不使用 PID 文件,因为它们可能会过时。相反,所有流程信息都由支持给定服务的主管保存。主管在服务目录中维护了一堆文件(和 FIFO),svstat
和 svc
等工具可以使用这些文件。
我们应该在 SO 和一般网络中发布更多这样的帖子。不仅仅是一个关于如何达到预期效果的食谱,而是一个不厌其烦地解释食谱的食谱。为什么我不能多次投票? :|【参考方案3】:
你应该看看daemonize。它允许检测第二个副本(但它使用文件锁定机制)。它还适用于不同的 UNIX 和 Linux 发行版。
如果您需要将应用程序作为守护进程自动启动,则需要创建适当的 init-script。
您可以使用以下模板:
#!/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
【讨论】:
看起来是正确答案的候选者。特别是考虑到它的“单实例检查”。 这可能是最好的答案——我不确定——但如果你认为是,你能否解释一下为什么我在问题中给出的规范是错误的? 我不喜欢停止部分的killproc
:如果你有一个进程运行java
,killproc
将导致所有其他Java 进程也被杀死.
从 /etc/rc.d/init.d/functions,daemonize 只是从一个新的 shell 启动二进制文件: $cgroup $nice /bin/bash -c $corelimit >/dev/null 2>&1 ; $*
所以我怀疑它会继续守护任何东西......
我知道这是旧的,但对于以后发现它的任何人......这是正确的。 /etc/init.d/functions 中定义的“守护进程”实际上 not 为您守护进程。它只是做 cgroups、检查进程是否已经在运行、设置用户、设置 nice 和 ulimit 值等的包装器。它不为你守护进程。那仍然是你自己的工作。 :)【参考方案4】:
我想你可能想试试start-stop-daemon(8)
。在任何 Linux 发行版中查看 /etc/init.d
中的脚本以获取示例。它可以通过调用的命令行或 PID 文件找到已启动的进程,因此它符合您的所有要求,除了作为脚本的看门狗。但您始终可以启动另一个守护程序看门狗脚本,在必要时重新启动您的脚本。
【讨论】:
Fedora 中还没有启动-停止-守护进程,因此依赖于它的脚本是不可移植的。见:fedoraproject.org/wiki/Features/start-stop-daemon 对于 OSX 用户的提醒:那里也没有start-stop-daemon
(从 10.9 开始)。
@mklement0 嗯...近 5 年发生了很多变化。
我的,时间过得真快。不过,start-stop-daemon
仍然活跃在 Linux 上;但在阅读答案***.com/a/525406/45375 后,我意识到OSX 做了自己的事情:launchd
。【参考方案5】:
作为已经提到的daemonize
和daemontools
的替代方案,还有libslack 包的daemon 命令。
daemon
非常可配置,并且确实关心所有繁琐的守护进程,例如自动重启、日志记录或 pidfile 处理。
【讨论】:
【参考方案6】:如果你专门使用 OS X,我建议你看看 launchd 是如何工作的。它将自动检查以确保您的脚本正在运行,并在必要时重新启动它。它还包括各种调度功能等。它应该同时满足要求1和2。
至于确保您的脚本只能运行一个副本,您需要使用 PID 文件。通常我将一个文件写入 /var/run/.pid ,其中包含当前正在运行的实例的 PID。如果程序运行时文件存在,它会检查文件中的 PID 是否实际运行(程序可能已经崩溃或忘记删除 PID 文件)。如果是,则中止。如果没有,开始运行并覆盖 PID 文件。
【讨论】:
【参考方案7】:Daemontools (http://cr.yp.to/daemontools.html) 是一组用于执行此操作的非常核心的实用程序,由 dj bernstein 编写。我已经成功地使用了它。令人讨厌的部分是,当您运行它们时,没有任何脚本返回任何可见的结果 - 只是不可见的返回码。但是一旦运行起来,它就是防弹的。
【讨论】:
是的,我也打算写一个使用 daemontools 的条目。我会写我自己的帖子,因为我希望我的答案更加全面,并希望以这种方式获得赏金。走着瞧。 :-)【参考方案8】:首先从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'”? 好的,它现在将加入参数 - 但是如果您想在参数中包含空格,则必须更改它。 谢谢道格拉斯!但是有一个很大的缺陷:运行两次“daemonize foo”会启动两个 foo 副本。 您可以添加一些 PID 记录代码,但最好只运行一次脚本... 我认为这是整个“守护进程”包装概念的基础。 (例如,我可以每小时或每分钟 cron 来确保它一直在运行。)我想错了吗? createDaemon 是否已经以某种方式保证了这一点?重启后呢?【参考方案9】:这是一个完整的工作版本,您可以将其复制到一个空目录并试用(在安装 CPAN 依赖项后,它们是 Getopt::Long、File::Spec、File::Pid 和 IPC::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: 是的,但是有两种方法可以解决这个问题:1. keepAlive.pl 调用的脚本(例如 example.pl)可能只是一个执行真实程序的包装器,或者 2. keepAlive.pl 可以解析活动系统进程表(使用 CPAN 的 Proc::ProcessTable)以尝试找到相关进程及其 pid)。【参考方案10】:您也可以试试Monit。 Monit 是一项监视和报告其他服务的服务。虽然它主要用作通知(通过电子邮件和短信)有关运行时问题的一种方式,但它也可以执行这里大多数其他建议所提倡的。它可以自动(重新)启动和停止程序、发送电子邮件、启动其他脚本以及维护您可以获取的输出日志。此外,我发现它很容易安装和维护,因为它有可靠的文档。
【讨论】:
【参考方案11】:你可以试试immortal 它是一个 *nix 跨平台(与操作系统无关)主管。
在 macOS 上快速尝试:
brew install immortal
如果您从端口使用 FreeBSD 或使用 pkg:
pkg install immortal
对于Linux,通过下载预编译的二进制文件或从源代码:https://immortal.run/source/
你可以像这样使用它:
immortal -l /var/log/date.log date
或通过configuration 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
【讨论】:
【参考方案12】:我对@987654321@做了一系列改进。
-
此脚本中的标准输出完全由来自其子级的标准输出组成,除非它因检测到该命令已在运行而退出
在其 pidfile 终止后清理
可选的可配置超时期限(接受任何正数参数,发送至
sleep
)
-h
上的使用提示
任意命令执行,而不是单个命令执行。最后一个 arg 或剩余的 args(如果有多个最后一个 args)被发送到eval
,因此您可以将任何类型的 shell 脚本作为字符串发送到该脚本作为最后一个 arg(或尾随 args)守护进程
使用-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 $TIMEOUTs" >&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
的工作方式有关,这对我来说很神秘。因此,谨慎的做法是将该行替换为仅接受单个命令的内容,例如另一个答案。
【讨论】:
【参考方案13】:还有一个非常简单的双叉 + setsid
方法可以将任何脚本与其父进程分离
( setsid my-regular-script arg [arg ...] 1>stdout.log 2>stderr.log & )
setsid
是标准util-linux
包的一部分,自诞生之日起就与Linux 一起使用。这在我知道的任何POSIX
兼容的 shell 中启动时都有效。
另一种基于双叉的方法甚至不需要任何额外的可执行文件或包,并且完全依赖于基于 POSIX
的 shell
( my-regular-script arg [arg ...] 1>stdout.log 2>stderr.log & ) &
当父进程离开阶段时,它也可以成为孤儿
【讨论】:
以上是关于如何在 unix 中守护任意脚本?的主要内容,如果未能解决你的问题,请参考以下文章