Implementing a mutex lock is a difficult problem for any parallel computation model. Leslie Lamport’s bakery algorithm is probably something we should use. But if the goal is merely to run a script in bash shell but not allowing duplicated parallels, there are some simple techniques that we can use.

flock

The easiest way is to invoke the shell script via flock command. This comes from the util-linux package in Debian. The syntax is

flock -x -n $LOCKFILE -c "command_line_here"

which the lockfile will be created at start and removed at exit. If it already exists, the flock command will terminate immediately. The -n option is for non-blocking. If omitted, flock will wait until the lockfile can be created. A compromise is to add -w 10 for a 10 second timeout. The -x option is for exclusive lock.

If in a shell script, this can be done too (from the flock manpage):

#!/bin/bash

(
    flock -x -n 99 || exit 1

    # work is done here
    sleep 10

) 99>/var/lock/myscript.lock

which the number 99 is used as the file descriptor.

pgrep

Not a robust way but works. The syntax is

pgrep -f <keyword> || command_line_here

The -f means to match the entire command line rather than just the executable name. The pgrep command will print all PID that matched the search but return exit code 1 if nothing is found. Hence this can invoke a command only when the search failed.

mkdir

In UNIX, there are only two atomic operations, mkdir and symlink. Therefore the only robust way to make a mutex lock in shell script is the following:

#!/bin/bash

if ! mkdir /var/lock/myscript.lock 2>/dev/null; then
    exit 1
fi

# work is done here
sleep 10

rmdir /var/lock/myscript.lock

But there would be a problem of stale lock.

pid file

A script from stackoverflow:

LOCKFILE=/var/run/myscript.pid
if [ -e ${LOCKFILE} ] && kill -0 `cat ${LOCKFILE}`; then
    exit 1
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 10

rm -f ${LOCKFILE}

It keeps the current process PID $$ in a file and register trap to remove it so mitigated the stale lock issue. If the lock is found, and kill command cannot find the corresponding PID, then the lock will be overwritten. But this may have race condition issue because the test and set is not atomic.

There is a way to make this atomic by leveraging the bash’s capability of “noclobber”, namely:

#!/bin/bash

set -C   # set noclobber
LOCKFILE=/var/run/myscript.pid
if echo $$ > ${LOCKFILE}; then
    set +C    # unset noclobber
    trap "rm -f ${LOCKFILE}; exit" INT TERM EXIT
    # work here
    sleep 10

    rm -f ${LOCKFILE}
else
    exit 1
fi

The set -C or set -o noclobber at the beginning will make echo $$ > ${LOCKFILE} fail if the file already exsits.

Side note: Template for robust bash script

#!/usr/bin/env bash

set -o errexit    # same as `set -e`; script exists on command fail
set -o nounset    # same as `set -u`; fail the script whenever reading an unset variable
set -o pipefail   # either side of the pipe fail is the entire command fail
if [[ "${TRACE-0}" == "1" ]]; then
   set -o xtrace  # same as `set -x`; print each command before execute
fi

if [[ "${1-}" =~ ^-*h(elp)?$ ]]; then
    echo 'Usage: ./script.sh arg-one arg-two

This is the help screen for your script.
'
    exit
fi

# move workdir to this script's directory
cd "$(dirname "$0")"

main() {
    local x="do awesome stuff"    # variables should be declared with `local`
    echo $x
}

main "$@"