Where to start First steps track · 15 stages
Linux Labs

flock — preventing a second run

9 min Ubuntu 24.04 · util-linux 2.39

flock holds a file lock while a command runs, so a second copy of the script waits for it or exits at once.

Cheat sheet

6 commands Printable version
  • flock -n /run/lock/clean.lock clean.sh busy — exit with code 1
  • flock -w 30 /run/lock/clean.lock clean.sh wait up to 30 seconds
  • flock /run/lock/clean.lock clean.sh wait as long as it takes
  • flock -n -E 75 /run/lock/clean.lock cmd busy — code 75
  • flock -n -o /run/lock/app.lock cmd children get no lock
  • lslocks -o COMMAND,PID,TYPE,PATH who holds which locks

Overview

A scheduled script can start while the previous run is still going: the cleanup took longer than usual, someone ran it by hand, or the job sits in both cron and a timer. Two copies delete the same files and interleave their lines in one log.

flock opens a lock file, takes a lock on it from the kernel, and only then runs the command. While the command runs, the lock is held. A second flock on the same file waits or, with -n, exits at once with code 1. It prints nothing when it does:

text
student@ubuntu:~$ flock -n /run/lock/job.lock sleep 3 &
student@ubuntu:~$ flock -n /run/lock/job.lock echo second; echo "rc=$?"
rc=1

Once it holds the lock, flock returns the command's own exit code.

Called as Lock is held Code if it gives up
flock FILE cmd waits for release —
flock -n FILE cmd exits at once 1 or the -E value
flock -w 30 FILE cmd waits up to 30 seconds 1 or the -E value

The lock lives in an open file descriptor. When the process ends in any way, even by kill -9, the kernel closes the descriptor and releases the lock. So a crash never leaves a stale lock behind. The file itself stays on disk; there is no need to delete it.

The lock is advisory: the kernel does not stop anyone from writing to the file and ignores programs that never call flock. The protection works when every run takes the lock on the very same file.

/run/lock (also reachable as /var/lock) is a good home for lock files: it lives in memory, any user can write there, and it is empty after a reboot.

Options

10

The main options are in the table below. The full list is in man flock.

Option What it does Example
-n --nonblock Do not wait; if the lock is busy, exit with code 1 silently. flock -n /run/lock/clean.lock clean.sh
-w --timeout SECONDS Wait for the lock at most this many seconds, then exit with 1. flock -w 60 /run/lock/backup.lock backup.sh
-E --conflict-exit-code N Exit code when the lock was not acquired (instead of 1). flock -n -E 75 /run/lock/x.lock cmd
-o --close Close the descriptor before running the command, so children do not hold the lock. flock -n -o /run/lock/app.lock app-start.sh
-c --command STRING Run a string through the shell — several commands under one lock. flock -n /run/lock/x.lock -c 'step1 && step2'
FD Lock a descriptor already open in the script instead of a file and command. exec 9>/run/lock/x.lock; flock -n 9
-o --output LIST Which lslocks columns to show; PATH gives the full lock file path. lslocks -o COMMAND,PID,TYPE,PATH
-s --shared Shared lock — several readers may hold it at once. flock -s /run/lock/data.lock cat data.csv
-x --exclusive Exclusive lock; this is the default. flock -x /run/lock/data.lock update.sh
-u --unlock FD Release the lock on a descriptor before the script exits. flock -u 9

Nothing found. Try a shorter query.

Recipes

5

A crontab line that never overlaps

text
*/5 * * * * flock -n /run/lock/sync.lock /usr/local/bin/sync.sh

A run that lands on a still-running previous one is quietly skipped.

A lock inside the script

bash
#!/bin/bash
exec 9>/run/lock/clean.lock
if ! flock -n 9; then
  echo "cleanup is already running" >&2
  exit 1
fi
# the work goes here; the lock is released when the script exits

exec 9>file opens descriptor 9 for the whole script, and flock -n 9 locks it. The script is protected however it is started: by hand, from cron or by a timer. Pick the code for a busy lock on purpose: 1 makes the attempt show up as a failure in systemctl status and monitoring, 0 treats the skip as normal.

A systemd service

ini
[Service]
Type=oneshot
ExecStart=/usr/bin/flock -n /run/lock/clean.lock /usr/local/bin/clean.sh

systemd never starts a second copy of the same unit on its own: while the service runs, another systemctl start joins the current run. flock matters when the same script also runs by hand, from cron or from another unit. The schedule itself is covered in systemd-timers.

Wait, but not forever

bash
flock -w 60 /run/lock/backup.lock /usr/local/bin/backup.sh \
  || echo "the lock has been busy for over a minute" >&2

Who holds the lock

text
student@ubuntu:~$ lslocks -o COMMAND,PID,TYPE,PATH | grep demo
flock            1031 FLOCK /run/lock/job.lock
student@ubuntu:~$ lsof /run/lock/job.lock
COMMAND  PID    USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
flock   1031 student    3rW  REG   0,27        0    6 /run/lock/job.lock
sleep   1033 student    3r   REG   0,27        0    6 /run/lock/job.lock

lsof also shows the child process: sleep inherited the descriptor from flock and holds the lock too.

Troubleshooting

4

cannot open lock file: Permission denied

The lock file cannot be opened. A common case: student once ran the script by hand, the file in /run/lock now belongs to student, and the root service gets refused — in a shared sticky directory Ubuntu does not let root open another user's file (fs.protected_regular = 2). flock exits with code 66:

text
student@ubuntu:~$ flock -n /run/lock/p.lock true
student@ubuntu:~$ sudo flock -n /run/lock/p.lock true; echo "rc=$?"
flock: cannot open lock file /run/lock/p.lock: Permission denied
rc=66

Delete the file while nobody holds it and run the script as one user. A root service can use a path of its own: /run/clean.lock.

The second run silently vanished

That is how -n behaves: busy means code 1 and no message. To see skips in the log, add || echo "skipped: the previous run is still going" >&2, or print a message inside the script as in the recipe above.

failed to execute: No such file or directory

flock could not find the command (code 69). Cron and services have a shorter PATH than your terminal — use the full path: /usr/local/bin/clean.sh.

The lock does nothing, runs still overlap

The runs lock different files, or one of them bypasses flock. Compare the path everywhere: the crontab, ExecStart=, the script itself. /var/lock and /run/lock are the same directory.

Danger zone

2

Deleting the lock file

While the first copy holds the lock, rm removes the file from the directory. The next flock creates a new file and happily locks that one — two copies run side by side:

text
student@ubuntu:~$ flock -n /run/lock/r.lock sleep 3 &
student@ubuntu:~$ rm /run/lock/r.lock
student@ubuntu:~$ flock -n /run/lock/r.lock echo "second got it"
second got it

Never delete a lock file — neither by hand nor at the end of the script.

A background child keeps the lock

Every process the command starts inherits the descriptor. If the command leaves a process running in the background, the lock lives as long as that process — even after kill -9 of flock itself. The -o option closes the descriptor before running the command, so only flock holds the lock.

Terms

1
file lock
A kernel mark on an open file: while one process holds it, another cannot get the same lock. It is released when the file is closed or the process ends. Also appears in: lsof, /proc: processes and the kernel as files, strace

See also

  • systemd-timers — running a script on a schedule.
  • cron — the classic scheduler.
  • bash-scripting — the whole script: arguments, conditions, set -e.
  • exit-codes — what codes 1, 66 and 69 mean.
  • lsof — which processes hold a file open.
  • processes — parents, children and inherited descriptors.
  • kill — stopping a stuck copy.
  • test-conditions — why [ -f lock ] does not prevent a second run.
Loading…