flock - Locks in Shell Scripts
Greetings!

In this note, we will talk about file locks in Bash scripts🔒 using the specialized utility - flock.

Once I had a task to write a script with the ability to prevent it from being launched again if another instance was already running. This is quite a common case. Most often, this is implemented by creating a regular flag file with the .lock suffix or something like that. This variant is quite common, but far from ideal🙂.

An obvious problem can be the so-called race condition. This is when, for example, several script instances start asynchronously and can interfere with each other’s work or something worse😳.

To solve this task in Linux scripts, there is a standard (often preinstalled) utility flock - so let’s work with it🧑💻.

Suppose we have script_1.sh with the following content:

BASH
#!/bin/bash

SCRIPT_LOCK="${BASH_SOURCE[0]%.*}.lock"

if [[ -f "$SCRIPT_LOCK" ]]; then
    echo "Script 1 is already running. Exiting."
    exit 1
else
    touch "$SCRIPT_LOCK"
    echo "Starting script 1..."
    sleep 999
    rm -f "$SCRIPT_LOCK"
fi
Click to expand and view more
  • $BASH_SOURCE[0] - a bash system variable containing the script path and name
  • %.* - removes .sh from the file name.

At first glance, everything is logical and clear. When launched:

BASH
chmod +x ./script_1.sh

./script_1.sh
Click to expand and view more

It checks for the presence of a lock file and, if it finds one, exits. If not, it creates it and performs further actions. If, while the script is running, you start it again in a neighboring terminal, it will accordingly complain that another instance is already running (the lock file exists) and exit.

On normal completion, the script removes the lock file so that it does not fail on the next launch. But what if the script is interrupted, for example with Ctrl+c, and the lock file is not removed? This case will have to be handled separately. We will talk about this later.

Let’s stop (Ctrl+c) the running script and consider asynchronous launch, that is, create a condition for a race condition:

BASH
rm -f ./script_1.lock

./script_1.sh & ./script_1.sh &
Click to expand and view more

The & operator in bash sends the process to run in the background.

And we will see that both started successfully?!:

BASH
[1] 1696362
[2] 1696363
Starting script 1...
Starting script 1...
Click to expand and view more

You can view processes running in the current session using the jobs command:

BASH
jobs
Click to expand and view more

Here is our pair🤨:

BASH
[1]  - running    ./script_1.sh
[2]  + running    ./script_1.sh
Click to expand and view more

To solve the problem, you can use flock. Create a 2nd script, script_2.sh, with the following content:

BASH
#!/bin/bash

SCRIPT_LOCK="${BASH_SOURCE[0]%.*}.lock"

exec {fd_lock}> "$SCRIPT_LOCK"

if ! flock -n "$fd_lock"; then
    echo "Script 2 is already running. Exiting."
    exit 1 
fi

echo "Staring script 2..."
sleep 999
Click to expand and view more

{fd_lock}> - bash syntax that creates the $fd_lock variable and assigns it the number of the nearest free descriptor.

In this variant, we open the $SCRIPT_LOCK file for writing and assign it a named descriptor from $fd_lock (for example 10).

Then, using the flock command, we try to lock the resulting descriptor.

If successful, we continue working; if not, we exit with an error.

With a running script instance:

BASH
chmod +x ./script_2.sh

./script_2.sh
Click to expand and view more

And when it is launched again, our script will also complain that a lock is present and exit.

If the running instance is interrupted, the lock file will not be removed, but this does not matter because the descriptor is locked. And in the case of a race condition, as last time:

BASH
./script_2.sh & ./script_2.sh &
Click to expand and view more

Our script will behave as expected:

BASH
[1] 1734870
[2] 1734871
Staring script 2...
Script 2 is already running. Exiting.                                                                                                         9ms
[2]  + 1734871 exit 1     ./script_2.sh
Click to expand and view more

The first instance started, the second exited with an error. Check with jobs:

BASH
jobs
Click to expand and view more

We see 1 instance🥳:

BASH
[1]  + running    ./script_2.sh
Click to expand and view more

But the question of the physical lock file remains🤔 It would be nice to remove it correctly too… even though this is not mandatory😉.

Below is an example of bash code for configuring a locking mechanism with explicit closing of the descriptor for flock (&-) and automatic removal of the .lock file, both on correct script completion and in case of an error or interruption⚡️. You can save it as a template and use it when needed. Of course, everything is at your own risk⚠️.

BASH
vim script.sh && chmod  +x ./script.sh
Click to expand and view more
BASH
#!/usr/bin/env bash

SCRIPT_LOCK="${BASH_SOURCE[0]%.*}.lock"

# cleanup function in case handlers are triggered
cleanup() {
    # disables all previously set traps
    trap - SIGINT SIGTERM ERR EXIT

    # closes the lock file descriptor
    [[ -n "${fd_lock:-}" ]] && exec {fd_lock}>&-

    # removes the lock file if it belongs to the current PID
    if [[ -f "$SCRIPT_LOCK" && $(<"$SCRIPT_LOCK") -eq $$ ]]; then
        rm -f "$SCRIPT_LOCK"
    fi
}

# sets handlers (trap)
trap cleanup SIGINT SIGTERM ERR EXIT

# opens the file for writing and saves the descriptor in $fd_lock
exec {fd_lock}>> "${SCRIPT_LOCK}"

# tries to acquire an exclusive lock (without waiting)
if ! flock -n "$fd_lock"; then
    echo "Script is already running. Exiting."
    exit 1
fi

# writes the current PID to the lock file
echo "$$" > "$SCRIPT_LOCK"

echo "Staring script..."
# your script logic
sleep 999 # command for example
Click to expand and view more
  • $(<"$SCRIPT_LOCK") - substitution syntax that allows assigning the file contents to a variable;
  • {fd_lock}>> - the append operator » is used to prevent overwriting the $SCRIPT_LOCK file on repeated launches.

If you have a more elegant solution to the task, please write it in the comments to the post👇

P.S. Honestly, I still have not fully understood how exactly locking through descriptors works🤯 it seems the kernel is involved in the process. Well, fine.

Another example of using flock with “interesting” comments can be viewed in this Habr article📝

Copyright Notice

Author: Ivan Cherniy

Link: https://r4ven.me/en/automation/flock-blokirovki-v-shell-skriptah/

License: CC BY-NC-SA 4.0

Blog materials may be used with attribution to the author and source, for non-commercial purposes, and under the same license.

Start searching

Enter keywords to search articles

↑↓
ESC
⌘K Shortcut