制作shell脚本守护进程的最佳方法?

Posted

技术标签:

【中文标题】制作shell脚本守护进程的最佳方法?【英文标题】:Best way to make a shell script daemon? 【发布时间】:2011-03-26 17:04:51 【问题描述】:

我想知道是否有更好的方法来制作一个只使用 sh 来等待某些东西的守护进程:

#! /bin/sh
trap processUserSig SIGUSR1
processUserSig() 
  echo "doing stuff"


while true; do
  sleep 1000
done

特别是,我想知道是否有任何方法可以摆脱循环并仍然让事物监听信号。

【问题讨论】:

您将需要一个循环,但请注意,您的示例可能不会以您期望的方式执行。 sleep 不是内置的 shell,shell 接收到的 SIGUSR1 不会传播到子进程。因此,在睡眠完成之前,您的信号处理程序不会得到处理。见mywiki.wooledge.org/SignalTrap#preview,第三部分。 【参考方案1】:

只是后台处理你的脚本 (./myscript &) 不会守护它。请参阅http://www.faqs.org/faqs/unix-faq/programmer/faq/,第 1.7 节,其中描述了成为守护程序所需的条件。您必须断开它与终端的连接,以便SIGHUP 不会杀死它。您可以采取捷径使脚本看起来像一个守护进程;

nohup ./myscript 0<&- &>/dev/null &

将完成这项工作。或者,将 stderr 和 stdout 都捕获到文件中:

nohup ./myscript 0<&- &> my.admin.log.file &

重定向解释(参见bash redirection)

0&lt;&amp;- 关闭标准输入 &amp;&gt; file 将 stdout 和 stderr 发送到文件

但是,您可能还需要考虑其他重要方面。例如:

您仍然有一个对脚本打开的文件描述符,这意味着它所挂载的目录将是不可挂载的。要成为真正的守护进程,您应该 chdir("/")(或在您的脚本中使用 cd /),然后 fork 以便父进程退出,从而关闭原始描述符。 也许运行umask 0。您可能不想依赖守护进程调用者的 umask。

有关考虑所有这些方面的脚本示例,请参阅Mike S' answer。

【讨论】:

首先,感谢您的回答。它对我来说主要工作得很好。但是我想附加到日志文件中,当我尝试“&>> log.txt”时,我得到了这个错误......“意外标记'>'附近的语法错误”对我有什么想法吗? 0&lt;&amp;- 是做什么的?这个字符序列完成了什么并不明显。 0&lt;&amp;- 的用途并不明显。我找到了this link,它解释了它。 没错,0&lt;&amp;- 关闭 stdin (fd 0)。这样,如果您的进程不小心从标准输入中读取(很容易做到),它将得到一个错误,而不是永远挂起等待数据出现。 nohup 自动从 /dev/null 重定向标准输入(参见手册)。关闭 std 文件描述符不是最佳做法。【参考方案2】:

这里的一些最受好评的答案缺少使守护程序成为守护程序的一些重要部分,而不仅仅是后台进程或与外壳分离的后台进程。

http://www.faqs.org/faqs/unix-faq/programmer/faq/ 描述了成为守护进程所必需的条件。而这个Run bash script as daemon 实现了setsid,虽然它错过了root 的chdir。

原始发帖人的问题实际上比“如何使用 bash 创建守护进程?”更具体,但由于主题和答案通常讨论守护 shell 脚本,我认为指出这一点很重要(对于像我这样的闯入者研究创建守护进程的细节)。

这是我根据常见问题解答运行的 shell 脚本的再现。将 DEBUG 设置为 true 以查看漂亮的输出(但它也会立即退出而不是无限循环):

#!/bin/bash
DEBUG=false

# This part is for fun, if you consider shell scripts fun- and I do.
trap process_USR1 SIGUSR1

process_USR1() 
    echo 'Got signal USR1'
    echo 'Did you notice that the signal was acted upon only after the sleep was done'
    echo 'in the while loop? Interesting, yes? Yes.'
    exit 0

# End of fun. Now on to the business end of things.

print_debug() 
    whatiam="$1"; tty="$2"
    [[ "$tty" != "not a tty" ]] && 
        echo "" >$tty
        echo "$whatiam, PID $$" >$tty
        ps -o pid,sess,pgid -p $$ >$tty
        tty >$tty
    


me_DIR="$( cd "$( dirname "$BASH_SOURCE[0]" )" && pwd )"
me_FILE=$(basename $0)
cd /

#### CHILD HERE --------------------------------------------------------------------->
if [ "$1" = "child" ] ; then   # 2. We are the child. We need to fork again.
    shift; tty="$1"; shift
    $DEBUG && print_debug "*** CHILD, NEW SESSION, NEW PGID" "$tty"
    umask 0
    $me_DIR/$me_FILE XXrefork_daemonXX "$tty" "$@" </dev/null >/dev/null 2>/dev/null &
    $DEBUG && [[ "$tty" != "not a tty" ]] && echo "CHILD OUT" >$tty
    exit 0
fi

##### ENTRY POINT HERE -------------------------------------------------------------->
if [ "$1" != "XXrefork_daemonXX" ] ; then # 1. This is where the original call starts.
    tty=$(tty)
    $DEBUG && print_debug "*** PARENT" "$tty"
    setsid $me_DIR/$me_FILE child "$tty" "$@" &
    $DEBUG && [[ "$tty" != "not a tty" ]] && echo "PARENT OUT" >$tty
    exit 0
fi

##### RUNS AFTER CHILD FORKS (actually, on Linux, clone()s. See strace -------------->
                               # 3. We have been reforked. Go to work.
exec >/tmp/outfile
exec 2>/tmp/errfile
exec 0</dev/null

shift; tty="$1"; shift

$DEBUG && print_debug "*** DAEMON" "$tty"
                               # The real stuff goes here. To exit, see fun (above)
$DEBUG && [[ "$tty" != "not a tty" ]]  && echo NOT A REAL DAEMON. NOT RUNNING WHILE LOOP. >$tty

$DEBUG || 
while true; do
    echo "Change this loop, so this silly no-op goes away." >/dev/null
    echo "Do something useful with your life, young padawan." >/dev/null
    sleep 10
done


$DEBUG && [[ "$tty" != "not a tty" ]] && sleep 3 && echo "DAEMON OUT" >$tty

exit # This may never run. Why is it here then? It's pretty.
     # Kind of like, "The End" at the end of a movie that you
     # already know is over. It's always nice.

DEBUG 设置为true 时,输出如下所示。请注意会话和进程组 ID(SESS、PGID)编号如何变化:

<shell_prompt>$ bash blahd

*** PARENT, PID 5180
  PID  SESS  PGID
 5180  1708  5180
/dev/pts/6
PARENT OUT
<shell_prompt>$ 
*** CHILD, NEW SESSION, NEW PGID, PID 5188
  PID  SESS  PGID
 5188  5188  5188
not a tty
CHILD OUT

*** DAEMON, PID 5198
  PID  SESS  PGID
 5198  5188  5188
not a tty
NOT A REAL DAEMON. NOT RUNNING WHILE LOOP.
DAEMON OUT

【讨论】:

这是一个非常好的答案!除了 SO(博客、社交网络等)之外,您(Mike S)是否有任何形式的在线形象?我在您的个人资料信息中没有看到类似的内容。 @MichaelGrünewald 不太正式。恐怕我没那么有趣;我很少写博客。我拥有 schwager.com 域(目前几乎没有),并且我确实有一些 Arduino 项目。一般来说,我的名字是 GreyGnome。你可以用 Google GreyGnome 或 GreyGnome + Arduino 找到我。 @MikeS 谢谢!我之所以这么问,是因为遇到真正对 shell 编程感兴趣的人并不常见,而且我总是很乐意就此交流想法和观点。感谢您提供的补充信息! :) @MikeS,为什么要分叉两次?在您的代码中,父脚本的孙子成为 setid 的新会话负责人,是吗?为什么不能只为第一个分叉执行此操作? 来自我在帖子中引用的常见问题解答:“以下是成为守护进程的步骤:... 1. fork()' so the parent can exit, this returns control to the command line or shell invoking your program. ... 2. setsid()' 成为进程组和会话组负责人.. . 我们的进程现在没有控制终端,这对守护进程来说是件好事...... 3. 再次执行“fork()”,这样父进程......可以退出。这意味着我们作为非会话组长,可以永远不要重新获得控制终端。”【参考方案3】:
# double background your script to have it detach from the tty
# cf. http://www.linux-mag.com/id/5981 
(./program.sh &) & 

【讨论】:

很酷的把戏!我通常不带 nohup,但我肯定会发现这个有用。 太棒了!我不知道这是否正确,但它就像一个魅力。 这似乎并没有断开stdinstdoutstderr。至少不是sh 这种分离方法用的比较多。它被称为“双叉”,并在神圣的 UNIX 经典之一 2nd Stevens (amazon.com/dp/0201433079) 中有更深入的解释。 有关双叉和 setsid() 的更多技术信息,请参阅 thelinuxjedi.blogspot.com/2014/02/… 。尤其是阅读“细分”部分,其中说,“双叉背后的真正步骤如下:”【参考方案4】:

使用您系统的守护程序设施,例如start-stop-daemon。

否则,是的,某处必须有一个循环。

【讨论】:

start-stop-daemon 使用起来非常方便 网址不再有效。【参考方案5】:

这真的取决于二​​进制文件本身要做什么。

例如我想创建一些监听器。

启动守护进程很简单:

lis_deamon:

#!/bin/bash

# We will start the listener as Deamon process
#    
LISTENER_BIN=/tmp/deamon_test/listener
test -x $LISTENER_BIN || exit 5
PIDFILE=/tmp/deamon_test/listener.pid

case "$1" in
      start)
            echo -n "Starting Listener Deamon .... "
            startproc -f -p $PIDFILE $LISTENER_BIN
            echo "running"
            ;;
          *)
            echo "Usage: $0 start"
            exit 1
            ;;
esac

这就是我们启动守护进程的方式(所有 /etc/init.d/staff 的通用方式)

现在至于听者自己, 它必须是某种循环/警报,否则会触发脚本 做你想做的事。例如,如果你想让你的脚本休眠 10 分钟 醒来后问你过得怎么样,你会用

while true ; do sleep 600 ; echo "How are u ? " ; done

这是你可以做的简单监听器,它将监听你的 来自远程机器的命令并在本地执行它们:

听众:

#!/bin/bash

# Starting listener on some port
# we will run it as deamon and we will send commands to it.
#
IP=$(hostname --ip-address)
PORT=1024
FILE=/tmp/backpipe
count=0
while [ -a $FILE ] ; do #If file exis I assume that it used by other program
  FILE=$FILE.$count
  count=$(($count + 1))
done

# Now we know that such file do not exist,
# U can write down in deamon it self the remove for those files
# or in different part of program

mknod $FILE p

while true ; do 
  netcat -l -s $IP -p $PORT < $FILE |/bin/bash > $FILE
done
rm $FILE

所以要启动它:/tmp/deamon_test/listener start

并从 shell 发送命令(或将其包装到脚本中):

test_host#netcat 10.184.200.22 1024
uptime
 20:01pm  up 21 days  5:10,  44 users,  load average: 0.62, 0.61, 0.60
date
Tue Jan 28 20:02:00 IST 2014
 punt! (Cntrl+C)

希望这会有所帮助。

【讨论】:

【参考方案6】:

$ ( cd /; umask 0; setsid your_script.sh </dev/null &>/dev/null & ) &

【讨论】:

【参考方案7】:

看看 libslack 包中的守护程序工具:

http://ingvar.blog.linpro.no/2009/05/18/todays-sysadmin-tip-using-libslack-daemon-to-daemonize-a-script/

在 Mac OS X 上,为 shell 守护进程使用 launchd 脚本。

【讨论】:

【参考方案8】:

如果我有一个 script.sh 并且我想从 bash 执行它并让它运行,即使我想关闭我的 bash 会话,那么我会在最后结合 nohup&amp;

示例:nohup ./script.sh &lt; inputFile.txt &gt; ./logFile 2&gt;&amp;1 &amp;

inputFile.txt 可以是任何文件。如果您的文件没有输入,那么我们通常使用/dev/null。所以命令是:

nohup ./script.sh &lt; /dev/null &gt; ./logFile 2&gt;&amp;1 &amp;

然后关闭您的 bash 会话,打开另一个终端并执行:ps -aux | egrep "script.sh",您将看到您的脚本仍在后台运行。当然,如果你想停止它,请执行相同的命令(ps)和kill -9 &lt;PID-OF-YOUR-SCRIPT&gt;

【讨论】:

【参考方案9】:

参见 Bash Service Manager 项目:https://github.com/reduardo7/bash-service-manager

实现示例

#!/usr/bin/env bash

export PID_FILE_PATH="/tmp/my-service.pid"
export LOG_FILE_PATH="/tmp/my-service.log"
export LOG_ERROR_FILE_PATH="/tmp/my-service.error.log"

. ./services.sh

run-script() 
  local action="$1" # Action

  while true; do
    echo "@@@ Running action '$action'"
    echo foo
    echo bar >&2

    [ "$action" = "run" ] && return 0
    sleep 5
    [ "$action" = "debug" ] && exit 25
  done


before-start() 
  local action="$1" # Action

  echo "* Starting with $action"


after-finish() 
  local action="$1" # Action
  local serviceExitCode=$2 # Service exit code

  echo "* Finish with $action. Exit code: $serviceExitCode"


action="$1"
serviceName="Example Service"

serviceMenu "$action" "$serviceName" run-script "$workDir" before-start after-finish

使用示例

$ ./example-service
# Actions: [start|stop|restart|status|run|debug|tail(-[log|error])]

$ ./example-service start
# Starting Example Service service...

$ ./example-service status
# Serive Example Service is runnig with PID 5599

$ ./example-service stop
# Stopping Example Service...

$ ./example-service status
# Service Example Service is not running

【讨论】:

【参考方案10】:

像许多答案一样,这不是“真正的”守护进程,而是nohup 方法的替代方案。

echo "script.sh" | at now

与使用nohup 有明显区别。对于一个人来说,一开始就没有与父母分离。 “script.sh”也不会继承父母的环境。

这绝不是更好的选择。这只是在后台启动进程的一种不同(而且有些懒惰)的方式。

附:我个人赞成 carlo 的回答,因为它似乎是最优雅的,并且适用于终端和内部脚本

【讨论】:

【参考方案11】:

这是对在 Bourne shell(或 Bash)中创建有效守护程序的原始提议的最小更改:

#!/bin/sh
if [ "$1" != "__forked__" ]; then
    setsid "$0" __forked__ "$@" &
    exit
else
    shift
fi

trap 'siguser1=true' SIGUSR1
trap 'echo "Clean up and exit"; kill $sleep_pid; exit' SIGTERM
exec > outfile
exec 2> errfile
exec 0< /dev/null

while true; do
    (sleep 30000000 &>/dev/null) &
    sleep_pid=$!
    wait
    kill $sleep_pid &>/dev/null
    if [ -n "$siguser1" ]; then
        siguser1=''
        echo "Wait was interrupted by SIGUSR1, do things here."
    fi
done

解释:

第 2-7 行:必须派生一个守护进程,这样它就没有父进程。使用人为参数来防止无休止的分叉。 "setsid" 与启动进程和终端分离。 第 9 行:需要将我们想要的信号与其他信号区分开来。 第 10 行:需要清理以摆脱悬空的“睡眠”进程。 第 11-13 行:重定向脚本的 stdout、stderr 和 stdin。 第 16 行:在后台休眠 第 18 行:wait 等待睡眠结束,但被(某些)信号中断。 第 19 行:终止睡眠进程,因为当信号被捕获时它仍在运行。 第 22 行:如果 SIGUSR1 已被捕获,则执行该工作。

猜它没有比这更简单的了。

【讨论】:

【参考方案12】:

尝试使用 & 执行 如果将此文件保存为 program.sh

你可以使用

$. program.sh &

【讨论】:

以上是关于制作shell脚本守护进程的最佳方法?的主要内容,如果未能解决你的问题,请参考以下文章

用shell写一个守护进程脚本

shell 脚本实现的 deamon 守护进程

shell 脚本实现的 deamon 守护进程

shell 脚本实现的 deamon 守护进程

Linux守护进程Shell脚本

inotifywait shell 脚本作为守护进程运行