创建 shell 脚本守护进程的最佳方法?

我想知道是否有更好的方法来创建一个只使用 sh 的守护进程,而不是:

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


while true; do
sleep 1000
done

特别是,我想知道是否有办法摆脱循环并让它仍然监听信号。

219490 次浏览

Have a look at the daemon tool from the libslack package:

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

On Mac OS X use a launchd script for shell daemon.

Use your system's daemon facility, such as start-stop-daemon.

Otherwise, yes, there has to be a loop somewhere.

try executing using & if you save this file as program.sh

you can use

$. program.sh &
# double background your script to have it detach from the tty
# cf. http://www.linux-mag.com/id/5981
(./program.sh &) &

Just backgrounding your script (./myscript &) will not daemonize it. See http://www.faqs.org/faqs/unix-faq/programmer/faq/, section 1.7, which describes what's necessary to become a daemon. You must disconnect it from the terminal so that SIGHUP does not kill it. You can take a shortcut to make a script appear to act like a daemon;

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

will do the job. Or, to capture both stderr and stdout to a file:

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

Redirection explained (see bash redirection)

  • 0<&- closes stdin
  • &> file sends stdout and stderr to a file

However, there may be further important aspects that you need to consider. For example:

  • You will still have a file descriptor open to the script, which means that the directory it's mounted in would be unmountable. To be a true daemon you should chdir("/") (or cd / inside your script), and fork so that the parent exits, and thus the original descriptor is closed.
  • Perhaps run umask 0. You may not want to depend on the umask of the caller of the daemon.

For an example of a script that takes all of these aspects into account, see Mike S' answer.

It really depends on what is the binary itself going to do.

For example I want to create some listener.

The starting Daemon is simple task :

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

this is how we start the daemon (common way for all /etc/init.d/ staff)

now as for the listener it self, It must be some kind of loop/alert or else that will trigger the script to do what u want. For example if u want your script to sleep 10 min and wake up and ask you how you are doing u will do this with the

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

Here is the simple listener that u can do that will listen for your commands from remote machine and execute them on local :

listener :

#!/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

So to start UP it : /tmp/deamon_test/listener start

and to send commands from shell (or wrap it to script) :

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)

Hope this will help.

Some of the top-upvoted answers here are missing some important parts of what makes a daemon a daemon, as opposed to just a background process, or a background process detached from a shell.

This http://www.faqs.org/faqs/unix-faq/programmer/faq/ describes what is necessary to be a daemon. And this Run bash script as daemon implements the setsid, though it misses the chdir to root.

The original poster's question was actually more specific than "How do I create a daemon process using bash?", but since the subject and answers discuss daemonizing shell scripts generally, I think it's important to point it out (for interlopers like me looking into the fine details of creating a daemon).

Here's my rendition of a shell script that would behave according to the FAQ. Set DEBUG to true to see pretty output (but it also exits immediately rather than looping endlessly):

#!/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.

Output looks like this when DEBUG is set to true. Notice how the session and process group ID (SESS, PGID) numbers change:

<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

If I had a script.sh and i wanted to execute it from bash and leave it running even when I want to close my bash session then I would combine nohup and & at the end.

example: nohup ./script.sh < inputFile.txt > ./logFile 2>&1 &

inputFile.txt can be any file. If your file has no input then we usually use /dev/null. So the command would be:

nohup ./script.sh < /dev/null > ./logFile 2>&1 &

After that close your bash session,open another terminal and execute: ps -aux | egrep "script.sh" and you will see that your script is still running at the background. Of cource,if you want to stop it then execute the same command (ps) and kill -9 <PID-OF-YOUR-SCRIPT>

See Bash Service Manager project: https://github.com/reduardo7/bash-service-manager

Implementation example

#!/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

Usage example

$ ./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

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

Like many answers this one is not a "real" daemonization but rather an alternative to nohup approach.

echo "script.sh" | at now

There are obviously differences from using nohup. For one there is no detaching from the parent in the first place. Also "script.sh" doesn't inherit parent's environment.

By no means this is a better alternative. It is simply a different (and somewhat lazy) way of launching processes in background.

P.S. I personally upvoted carlo's answer as it seems to be the most elegant and works both from terminal and inside scripts

Here is the minimal change to the original proposal to create a valid daemon in Bourne shell (or 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


Explanation:

  • Line 2-7: A daemon must be forked so it doesn't have a parent. Using an artificial argument to prevent endless forking. "setsid" detaches from starting process and terminal.
  • Line 9: Our desired signal needs to be differentiated from other signals.
  • Line 10: Cleanup is required to get rid of dangling "sleep" processes.
  • Line 11-13: Redirect stdout, stderr and stdin of the script.
  • Line 16: sleep in the background
  • Line 18: wait waits for end of sleep, but gets interrupted by (some) signals.
  • Line 19: Kill sleep process, because that is still running when signal is caught.
  • Line 22: Do the work if SIGUSR1 has been caught.

Guess it does not get any simpler than that.