File Locking on Unix: flock() vs fcntl() and What They Actually Guarantee

This article covers File Locking on Unix: flock() vs fcntl() and What They Actually Guarantee. File locking is deceptively subtle. Learn advisory locks, flock vs fcntl record locks, lease-like patterns, and how to implement a safe "singl...

File locking is often used for:

  • ensuring a single instance of a daemon
  • coordinating writers to a file
  • guarding critical sections across processes

But locking on Unix is mostly advisory: the kernel won’t stop you from writing unless programs cooperate.

1) Advisory vs mandatory locking

  • Advisory: processes voluntarily acquire locks; others must check.
  • Mandatory (rare): kernel enforces (Linux supports in limited scenarios; generally avoid).

In practice, assume advisory.

2) flock()

  • Simple interface.
  • Locks are associated with an open file description.
  • flock is whole-file (no byte-range).

Common use: lock a lockfile.

3) fcntl() record locks

  • Byte-range locks via struct flock.
  • More flexible.
  • Historically interacts with NFS differently (depending on server/client).

4) What locks do NOT guarantee

  • They do not automatically make file writes atomic.
  • They do not automatically flush data to disk.
  • They do not prevent a process from writing if it ignores locks.

Another subtle non-guarantee:

  • Locks do not define what you write. You still need a safe update pattern.

If you are locking because you want crash safety, pair locking with a durable update pattern like:

5) C: single-instance lockfile with flock

This pattern is widely used for daemons.

#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/file.h>
#include <unistd.h>

int main(void) {
    const char *lock_path = "/tmp/myapp.lock";

    int fd = open(lock_path, O_RDWR | O_CREAT | O_CLOEXEC, 0644);
    if (fd < 0) {
        fprintf(stderr, "open lock: %s\n", strerror(errno));
        return 1;
    }

    if (flock(fd, LOCK_EX | LOCK_NB) != 0) {
        if (errno == EWOULDBLOCK) {
            fprintf(stderr, "already running\n");
            return 2;
        }
        fprintf(stderr, "flock: %s\n", strerror(errno));
        return 1;
    }

    // Keep fd open for the life of the process.
    printf("lock acquired, running...\n");
    pause();
}

Important:

  • Keep the lock fd open. Closing releases the lock.

What happens with fork/exec/dup?

This is where people get confused:

  • dup() duplicates a file descriptor, but points to the same open file description.
  • fork() copies file descriptors into the child.

If you keep multiple fds referencing the same underlying open file description, the lock lifetime can be surprising.

Practical rule:

  • treat the lock fd as a unique owned resource and keep it open in exactly one place

If you exec() a new program, use O_CLOEXEC (as in the example) so the lock doesn’t leak across exec.

6) Zig: flock lockfile

const std = @import("std");
const os = std.os;

const c = @cImport({
    @cInclude("sys/file.h");
});

pub fn main() !void {
    var file = try std.fs.cwd().createFile("/tmp/myapp.lock", .{ .read = true, .truncate = false });
    defer file.close();

    const fd = file.handle;

    if (c.flock(fd, c.LOCK_EX | c.LOCK_NB) != 0) {
        return error.AlreadyRunning;
    }

    std.debug.print("lock acquired\n", .{});
    while (true) {
        std.time.sleep(1_000_000_000);
    }
}

7) Rust: lockfile with flock

use std::fs::OpenOptions;
use std::io;
use std::os::unix::io::AsRawFd;

fn main() -> io::Result<()> {
    let f = OpenOptions::new()
        .read(true)
        .write(true)
        .create(true)
        .open("/tmp/myapp.lock")?;

    let rc = unsafe { libc::flock(f.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
    if rc != 0 {
        let e = io::Error::last_os_error();
        eprintln!("lock failed: {e}");
        std::process::exit(2);
    }

    println!("lock acquired");
    loop { std::thread::park(); }
}

8) fcntl record-lock example (byte range)

Use cases:

  • locking regions in a shared data file
  • cooperative coordination between processes

Conceptually:

  • set l_type = F_WRLCK or F_RDLCK
  • specify l_start, l_len, l_whence

Two important semantics differences vs flock:

  • record locks are associated with the process, not the open file description
  • closing any fd for that file in the process may release locks (depends on exact pattern and platform)

This is why mixing multiple fds to the same file with record locks can be error-prone.

9) Deadlocks and lock ordering

It is easy to deadlock if you lock multiple resources in different orders.

Rules of thumb:

  • define a global lock order (by path name, inode number, or logical role)
  • keep lock hold times short
  • avoid calling out to other subsystems while holding a lock

10) Network filesystems and portability notes

Historically, file locking semantics over NFS can be different depending on server/client configuration.

If you rely on locking for correctness across machines:

  • test your exact deployment
  • consider a coordination service (e.g. etcd/consul) for distributed locks

For single-host coordination, flock/fcntl are usually enough.

References