How would you make sure that a shell script (or a portion thereof) does not run (it waits or it terminates with error) when another instance of it is already running?
The simplest way would be to use a lock file - let the contents of a text file act as a traffic cop.
Hmmm... contents of the file?
How would you make sure that, in between the point in time you check the file and the point in time you set the lock, no other process does the same resulting in two processes running?
I'd use the presence or absence of an interlock file and record your shell's process id in the file.
Create the file with /usr/bin/touch for speed and refer to the full hierarchial filename throughout the script (i.e. avoid directory searches). After creating the file append your shell's process id. Read back the file and check the process id to be 100% sure that your instance holds the interlock before processing, otherwise back off.
I like using 'fuser' - making sure there's only ONE pid 'locking' the script.
One problem with this method is someone may be running "cat <script>" and
fuser will show this . So you might have to check the processes that "fuser"
returns and make sure they are indeed running the script.
Something along these lines:
#!/bin/ksh
thisFILE="$(whence ${0})"
progName="${0##*/}"
myPID="$$"
FUSERout=$(fuser ${thisFILE} 2>/dev/null)
typeset -i numProc=$(echo "${FUSERout}" | nawk '{print NF}')
if [[ "${numProc}" -gt 1 ]]; then
echo "${progName}: another instance(s) of [${thisFILE}] is currently still running [$(echo ${FUSERout} | sed -e 's/ */ /g')] - exiting THIS ${myPID}] run."
exit 1
fi
Depends on what you mean for "back off".
Suppose that while you write the pid another process is doing the same. This results in the other pid appearing inside the file before or after your pid, the position does not matter here.
Then you check and see another pid:
- You wait, leaving in place your pid. Deadlock: both processes waiting for the other pid to disappear.
- You delete your pid (not a trivial task) and wait for some interval, then you write your pid again and check. The other process is doing the same, therefore it is likely that both processes again see another pid and have to wait again. A pseudo-random wait interval would take care of the problem... it reminds me of Ethernet collision detection. It would perhaps work, but it seems just too involved.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Ok, now I got it: the serialization protocol dictates that the pid appearing in line 1 of the interlock file is the one whose process is allowed to go on, while the other processes are inside a wait-and-check loop (well, perhaps it is smarter to just wait $pid1 where pid1 is read from line 1); when the process ends, it removes its pid; then another pid is in line 1... and so on, until the pid queue is empty.
Not bad. Still a bit involved though.
You have to be absolutely sure that the script cannot end without having removed its pid from the queue. An exit handler implemented with trap would be enough, but a SIGKILL would bypass the trap.
I like using fuser too, but:
- it works only when the processes to serialize are instances of the exact same script file
- it is not very portable
- as you said, any process reading the script file would block all the script instances (perhaps forever)
On the plus side, the lock is automatically removed when the process ends or is terminated in any way.
progname=${0##*/}
lockdir=/tmp/$progname.lockdir
if mkdir -m 400 "$lockdir" 2>/dev/null
then
trap 'rmdir "$lockdir"' EXIT
else
printf "%s is already running\n" "$progname" >&2
exit 1
fi
## script follows
sleep 20
Yup, this is the standard solution.
Check lock and set lock is an atomic operation.
Simple and working... except of course when a SIGKILL hits.
I am puzzled about -m 400, what is the pourpose of it?
If the directory already exists, the mkdir command would fail regardless of the mode bits.
Nah, there are still problems.
To remove its pid from the top, a shell script has to rewrite the whole interlock file. What if in the meantime another instance is appending its pid?
This issue is basically a consequence of the fact that unix provides no automatic exclusive write access to files.
I believe it would go away if we make the following substitutions:
- interlock file <= directory
- lines of interlock file <= filenames inside the directory
Anyone throwing SIGKILLs around should expect to have to do some manual cleanup!
It's intended to prevent anyone putting a file in there and preventing rmdir from working. rm -rf "$lockdir" would fix that, too.
Let see if I can add something useful:
progname=${0##*/}
lockdir=/tmp/$progname.lockdir
if mkdir -m 755 "$lockdir" 2>/dev/null
then
: > "$lockdir/$$"
trap 'rm -rf "$lockdir"' EXIT
else
pid=$(echo "$lockdir"/*); pid=${pid##*/}
printf "%s is already running with pid %s\n" "$progname" $pid >&2
exit 1
fi
## script follows
sleep 20
And a version that doesn't drop the ball:
progname=${0##*/}
lockdir=/tmp/$progname.lockdir
trap '[[ -f "$lockdir/$$" ]] && rm -rf "$lockdir"' EXIT
until mkdir -m 755 "$lockdir" 2>/dev/null
do
pid=$(echo "$lockdir"/*); pid=${pid##*/}
printf "%s is already running with pid %s\n" "$progname" $pid >&2
sleep 1
done
: > "$lockdir/$$"
## script follows
sleep 20