views:

2315

answers:

10

What's a quick-and-dirty way to make sure that only one instance of a shell script is running at a given time?

+5  A: 

Create a lock file in a known location and check for existence on script start? Putting the PID in the file might be helpful if someone's attempting to track down an errant instance that's preventing execution of the script.

Rob
+5  A: 

Quick and dirty?

#!/bin/sh

if[ -f sometempfile ]
  echo "Already running... will now terminate."
  exit
else
  touch sometempfile
fi

..do what you want here..

rm sometempfile
Aupajo
This may or may not be an issue, depending on how it's used, but there's a race condition between testing for the lock and creating it, so that two scripts could both be started at the same time. If one terminates first, the other will stay running with no lock file.
TimB
C News, which taught me much about portable shell scripting, used to make a lock.$$ file, and then attempt to link it with "lock" - if the link succeeed, you had the lock, otherwise you removed lock.$$ and exited.
Paul Tomblin
That's a really good way to do it, except you still suffer the need to remove the lockfile manually if something goes wrong and the lockfile isn't deleted.
Matthew Scharley
Quick and dirty, that's what he asked for :)
Aupajo
+3  A: 

PID and lockfiles are definitely the most reliable. When you attempt to run the program, it can check for the lockfile which and if it exists, it can use ps to see if the process is still running. If it's not, the script can start, updating the PID in the lockfile to its own.

Drew Stephens
+18  A: 

Here's an implementation that uses a lockfile and echos a pid into it. This protects if the process is killed before removing the pidfile:

LOCKFILE=/tmp/lock.txt
if [ -e ${LOCKFILE} ] && kill -0 `cat ${LOCKFILE}`; then
    echo "already running"
    exit
fi

# make sure the lockfile is removed when we exit and then claim it
trap "rm -f ${LOCKFILE}; exit" INT TERM EXIT
echo $$ > ${LOCKFILE}

# do stuff
sleep 1000

rm -f ${LOCKFILE}

The trick here is the kill -0 which doesn't deliver any signal but just checks if a pid is running. Also the call to trap will ensure that the lockfile is removed even when your process is killed (except kill -9).

bmdhacks
As already mentioned in a comment on anther answer, this has a fatal flaw - if the other script starts up between the check and the echo, you're toast.
Paul Tomblin
The symlink trick is neat, but if the owner of the lockfile is kill -9'd or the system crashes, there's still a race condition to read the symlink, notice the owner is gone, and then delete it. I'm sticking with my solution.
bmdhacks
Ok, so the only way I can think of avoiding the race condition is to implement Lamport's bakery algorithm in shell script: http://en.wikipedia.org/wiki/Lamport%27s_bakery_algorithmThis is not a trivial task. The other option is to write a C or perl app that uses flock to claim the lockfile for you.
bmdhacks
Atomic check and create is available in the shell using either flock (1) or lockfile (1). See other answers.
dmckee
Can you update your answer to use $LOCKFILE consistently?
raldi
done, although I think dmckee's recommendation above to use flock (1) or lockfile (1) is superior to my script above.
bmdhacks
See my reply for a portable way of doing an atomic check and create without having to rely on utilities such as flock or lockfile.
lhunath
+12  A: 

There's a wrapper around the flock(2) system call called, unimaginatively, flock(1). This makes it relatively easy to reliably obtain exclusive locks without worrying about cleanup etc. There are examples on the man page as to how to use it in a shell script.

Cowan
+1  A: 

The issues with some of the above answers is that they are not atomic, so you could still run into issues if two scripts tried to run at about the same time.

Try this instead: http://books.google.com/books?id=QYu_v2R6fIQC&pg=PA772&lpg=PA772&dq=atomic+shell+script+operation&source=web&ots=yMC-nWkByX&sig=wwGuoCYPH6NtXp9UfZnNrWfs-Gc#PPA772,M1

spdevsolutions
+1  A: 

Some unixes have lockfile which is very similar to the already mentioned flock.

From the manpage:

lockfile can be used to create one or more semaphore files. If lock- file can't create all the specified files (in the specified order), it waits sleeptime (defaults to 8) seconds and retries the last file that didn't succeed. You can specify the number of retries to do until failure is returned. If the number of retries is -1 (default, i.e., -r-1) lockfile will retry forever.

dmckee
+1  A: 

To make locking reliable you need an atomic operation. Many of the above proposals are not atomic. The proposed lockfile(1) utility looks promising as the man-page mentioned, that its "NFS-resistant". If your OS does not support lockfile(1) and your solution has to work on NFS, you have not many options....

NFSv2 has two atomic operations:

  • symlink
  • rename

With NFSv3 the create call is also atomic.

Directory operations are NOT atomic under NFSv2 and NFSv3 (please refer to the book 'NFS Illustrated' by Brent Callaghan, ISBN 0-201-32570-5; Brent is a NFS-veteran at Sun).

Knowing this, you can implement spin-locks for files and directories (in shell, not PHP):

lock current dir:

while ! ln -s . lock; do :; done

lock a file:

while ! ln -s ${f} ${f}.lock; do :; done

unlock current dir (assumption, the running process really acquired the lock):

mv lock deleteme && rm deleteme

unlock a file (assumption, the running process really acquired the lock):

mv ${f}.lock ${f}.deleteme && rm ${f}.deleteme

Remove is also not atomic, therefore first the rename (which is atomic) and then the remove.

For the symlink and rename calls, both filenames have to reside on the same filesystem. My proposal: use only simple filenames (no paths) and put file and lock into the same directory.

+7  A: 

All approaches that use lock files are flawed.

Why? Because there is no way to check whether a file exists and create it in a single atomic action. Because of this; there is a race condition that WILL make your attempts at mutual exclusion break.

Instead, you need to use mkdir. mkdir creates a directory if it doesn't exist yet, and if it does, it sets an exit code. More importantly, it does all this in a single atomic action making it perfect for this scenario.

if ! mkdir /tmp/myscript.lock 2>/dev/null; then
    echo "Myscript is already running." >&2
    exit 1
fi

For all details, see the excellent BashFAQ: http://mywiki.wooledge.org/BashFAQ/045

lhunath
A: 

When targeting a Debian machine I find the lockfile-progs package to be a good solution. procmail also comes with a lockfile tool. However sometimes I am stuck with neither of these.

Here's my solution which uses mkdir for atomic-ness and a PID file to detect stale locks. This code is currently in production on a Cygwin setup and works well.

To use it simply call exclusive_lock_require when you need get exclusive access to something. An optional lock name parameter lets you share locks between different scripts. There's also two lower level functions (exclusive_lock_try and exclusive_lock_retry) should you need something more complex.

function exclusive_lock_try() # [lockname]
{

    local LOCK_NAME="${1:-`basename $0`}"

    LOCK_DIR="/tmp/.${LOCK_NAME}.lock"
    local LOCK_PID_FILE="${LOCK_DIR}/${LOCK_NAME}.pid"

    if [ -e "$LOCK_DIR" ]
    then
        local LOCK_PID="`cat "$LOCK_PID_FILE" 2> /dev/null`"
        if [ ! -z "$LOCK_PID" ] && kill -0 "$LOCK_PID" 2> /dev/null
        then
            # locked by non-dead process
            echo "\"$LOCK_NAME\" lock currently held by PID $LOCK_PID"
            return 1
        else
            # orphaned lock, take it over
            ( echo $$ > "$LOCK_PID_FILE" ) 2> /dev/null && local LOCK_PID="$$"
        fi
    fi
    if [ "`trap -p EXIT`" != "" ]
    then
        # already have an EXIT trap
        echo "Cannot get lock, already have an EXIT trap"
        return 1
    fi
    if [ "$LOCK_PID" != "$$" ] &&
        ! ( umask 077 && mkdir "$LOCK_DIR" && umask 177 && echo $$ > "$LOCK_PID_FILE" ) 2> /dev/null
    then
        local LOCK_PID="`cat "$LOCK_PID_FILE" 2> /dev/null`"
        # unable to acquire lock, new process got in first
        echo "\"$LOCK_NAME\" lock currently held by PID $LOCK_PID"
        return 1
    fi
    trap "/bin/rm -rf \"$LOCK_DIR\"; exit;" EXIT

    return 0 # got lock

}

function exclusive_lock_retry() # [lockname] [retries] [delay]
{

    local LOCK_NAME="$1"
    local MAX_TRIES="${2:-5}"
    local DELAY="${3:-2}"

    local TRIES=0
    local LOCK_RETVAL

    while [ "$TRIES" -lt "$MAX_TRIES" ]
    do

        if [ "$TRIES" -gt 0 ]
        then
            sleep "$DELAY"
        fi
        local TRIES=$(( $TRIES + 1 ))

        if [ "$TRIES" -lt "$MAX_TRIES" ]
        then
            exclusive_lock_try "$LOCK_NAME" > /dev/null
        else
            exclusive_lock_try "$LOCK_NAME"
        fi
        LOCK_RETVAL="${PIPESTATUS[0]}"

        if [ "$LOCK_RETVAL" -eq 0 ]
        then
            return 0
        fi

    done

    return "$LOCK_RETVAL"

}

function exclusive_lock_require() # [lockname] [retries] [delay]
{
    if ! exclusive_lock_retry "$@"
    then
        exit 1
    fi
}
Jason Weathered