Two processes can open the same file and both write to it successfully. That is often exactly what Unix applications need, but sometimes the programs are supposed to coordinate: only one worker should update a state file, several readers may share a resource, or a process must avoid changing a byte range while another process is using it.

Linux offers several advisory file-locking mechanisms. The confusing part is not how to request a lock. The confusing part is what owns the lock and when that lock disappears.

A program that chooses the wrong ownership model can release a lock earlier than expected, accidentally keep it alive after fork(), or assume that an unrelated process is prevented from writing when advisory locking does not provide that guarantee.

This article compares three important choices:

flock()                whole-file style, open-file-description ownership
POSIX fcntl() locks    byte ranges, process ownership
OFD fcntl() locks      byte ranges, open-file-description ownership

The goal is not to declare one API universally best. The goal is to match the lock semantics to the coordination problem.

First understand what advisory locking guarantees

Linux file locks are normally advisory. Cooperating programs ask for locks and respect conflicts. A process that ignores the locking protocol can still access the file if ordinary permissions allow it.

That changes the mental model from:

lock file
    -> kernel prevents every other access

into:

all cooperating participants
    -> use the same locking protocol
    -> conflicting participants wait or fail

This distinction matters for security. An advisory lock is a coordination mechanism, not an access-control boundary. Use filesystem permissions, process isolation, and other authorization controls when untrusted processes must be prevented from reading or modifying data.

It also means every participant needs a consistent rule about what is locked and for how long.

The key concept is the open file description

Calling open() creates a kernel object called an open file description. The process receives a file descriptor that refers to that object.

process
  fd 3 ─────> open file description ─────> file

Calling dup() creates another file descriptor that refers to the same open file description:

process
  fd 3 ──┐
         ├──> same open file description ──> file
  fd 7 ──┘

Calling open() again for the same pathname creates a different open file description:

fd 3 ──> open file description A ──┐
                                   ├──> same file
fd 7 ──> open file description B ──┘

This difference is central to flock() and open-file-description locks. A duplicated descriptor shares their lock ownership because it shares the same open file description. A separately opened descriptor does not.

Traditional POSIX record locks use a different ownership model: they are associated with the process instead.

Use flock() for simple cooperating whole-file locks

flock() is often the easiest choice when a program wants to coordinate access to an entire file.

A minimal exclusive lock is:

#include <sys/file.h>
#include <unistd.h>

if (flock(fd, LOCK_EX) == -1) {
    /* handle error */
}

/* protected work */

if (flock(fd, LOCK_UN) == -1) {
    /* handle error */
}

LOCK_EX requests an exclusive lock. Another incompatible flock() request normally blocks until the lock becomes available.

For a shared reader lock, use LOCK_SH:

if (flock(fd, LOCK_SH) == -1) {
    /* handle error */
}

Multiple shared locks can coexist. An exclusive lock conflicts with both shared and exclusive locks from other open file descriptions.

Make lock acquisition nonblocking when waiting is wrong

Add LOCK_NB when the caller should fail immediately instead of waiting:

#include <errno.h>
#include <sys/file.h>

if (flock(fd, LOCK_EX | LOCK_NB) == -1) {
    if (errno == EWOULDBLOCK) {
        /* another holder currently conflicts */
    } else {
        /* another error */
    }
}

This is useful for single-instance jobs and opportunistic maintenance tasks where waiting indefinitely would be worse than skipping the work.

A blocking lock is appropriate only when the caller can safely wait. In services, also consider how cancellation and deadlines should interrupt the surrounding workflow.

flock() locks follow the open file description

On Linux, a flock() lock is associated with the open file description.

Suppose a program locks fd, then duplicates it:

int copy = dup(fd);

Both descriptors now refer to the same lock ownership. Closing only one descriptor does not release the lock while another descriptor referring to that same open file description remains open.

The lock is released by an explicit LOCK_UN, or when all file descriptors referring to that open file description have been closed.

This can be useful after fork(): the child inherits descriptors that refer to the same open file descriptions. It can also be surprising if a descriptor is duplicated into a library or inherited by a child unintentionally.

Descriptor lifetime therefore becomes part of lock lifetime.

Traditional fcntl() record locks support byte ranges

POSIX record locks, requested with fcntl(), can lock all or part of a file.

The lock is described with struct flock:

#include <fcntl.h>

struct flock lock = {
    .l_type = F_WRLCK,
    .l_whence = SEEK_SET,
    .l_start = 0,
    .l_len = 4096,
};

if (fcntl(fd, F_SETLK, &lock) == -1) {
    /* handle conflict or another error */
}

This requests a nonblocking write lock for the first 4096 bytes.

Use F_RDLCK for a read lock and F_UNLCK to unlock a range. F_SETLKW is the blocking form of F_SETLK.

A length of zero has a special meaning: the range extends from l_start through the end of the file, including bytes added later.

Byte-range locking is useful when independent parts of a large file can legitimately be used concurrently. If every participant always protects the whole file, byte ranges may only add complexity.

POSIX record locks have process-oriented lifetime rules

Traditional fcntl() record locks are associated with the process, not with one open file description.

That creates behavior that often surprises developers: closing any file descriptor for that file can remove the process’s record locks on that file, even if the closed descriptor was not the descriptor originally used to acquire the lock.

Consider a process that does this:

open file -> fd 3
lock range using fd 3
open same file again -> fd 7
close fd 7

With traditional POSIX record locks, that unrelated close(fd 7) can release the process’s locks for the file.

This becomes especially risky in large programs where libraries independently open and close the same file.

Traditional record locks also do not provide a good mechanism for synchronizing threads within one process. The locks are process-associated, so two threads in the same process do not behave like independent lock owners.

If you need byte ranges but want ownership to follow open file descriptions instead, Linux provides OFD locks.

OFD locks combine byte ranges with descriptor-oriented ownership

Linux open file description locks use the same struct flock range model but different fcntl() commands:

#define _GNU_SOURCE
#include <fcntl.h>

struct flock lock = {
    .l_type = F_WRLCK,
    .l_whence = SEEK_SET,
    .l_start = 0,
    .l_len = 0,
};

if (fcntl(fd, F_OFD_SETLK, &lock) == -1) {
    /* handle conflict or another error */
}

The blocking variant is F_OFD_SETLKW. F_OFD_GETLK can query whether another lock conflicts with a requested range.

These commands are Linux-specific. They are useful when an application needs byte-range locking but wants lock ownership to follow an open file description rather than the process.

Duplicated descriptors share the same OFD lock owner

If dup() produces another descriptor for the same open file description, both descriptors share the OFD lock ownership.

fd 3 ──┐
       ├──> open file description A -> OFD lock
fd 7 ──┘

Closing fd 3 does not release the lock if fd 7 still refers to the same open file description.

A second independent open() creates another open file description. Locks requested through it can conflict with locks held through the first description, even inside the same process.

That property makes OFD locks useful for some multithreaded designs: threads can obtain independent lock ownership by independently opening the file.

A small OFD example shows the lifetime rule

The following program verifies three behaviors:

  1. a duplicated descriptor shares the same lock owner;
  2. an independently opened descriptor conflicts with that lock;
  3. the lock remains until the last descriptor for the original open file description is closed.
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

static int set_ofd_lock(int fd, short type) {
    struct flock lock = {
        .l_type = type,
        .l_whence = SEEK_SET,
        .l_start = 0,
        .l_len = 0,
    };

    return fcntl(fd, F_OFD_SETLK, &lock);
}

int main(void) {
    char path[] = "/tmp/lock-demo-XXXXXX";
    int fd = mkstemp(path);
    if (fd == -1) {
        perror("mkstemp");
        return EXIT_FAILURE;
    }

    if (set_ofd_lock(fd, F_WRLCK) == -1) {
        perror("lock");
        return EXIT_FAILURE;
    }

    int copy = dup(fd);
    if (copy == -1) {
        perror("dup");
        return EXIT_FAILURE;
    }

    int other = open(path, O_RDWR);
    if (other == -1) {
        perror("open");
        return EXIT_FAILURE;
    }

    errno = 0;
    if (set_ofd_lock(other, F_WRLCK) != -1 || errno != EAGAIN) {
        fprintf(stderr, "expected a conflicting lock\n");
        return EXIT_FAILURE;
    }

    close(fd);

    errno = 0;
    if (set_ofd_lock(other, F_WRLCK) != -1 || errno != EAGAIN) {
        fprintf(stderr, "duplicate should keep the lock alive\n");
        return EXIT_FAILURE;
    }

    close(copy);

    if (set_ofd_lock(other, F_WRLCK) == -1) {
        perror("lock after last close");
        return EXIT_FAILURE;
    }

    close(other);
    unlink(path);
    return EXIT_SUCCESS;
}

Compile it on Linux with:

gcc -std=c11 -Wall -Wextra -Werror lock-demo.c -o lock-demo
./lock-demo

A successful run produces no output and exits with status zero.

The example uses /tmp only for demonstration. Real applications should choose a lock target whose location and permissions match their trust boundary.

Lock the resource, not merely a convenient pathname

A common pattern is to create a separate file such as:

/var/run/example.lock

and lock that file while modifying some other resource.

This can work when every participant agrees that the lock file represents the resource. But the relationship exists only by convention. Locking example.lock does not cause the kernel to protect a different data file automatically.

If the data file itself can be opened and locked safely, locking the actual resource reduces the number of conventions participants must share.

Separate lock files are still useful when the protected operation concerns several files, a directory-level workflow, or a resource that cannot itself be kept open for the full critical section.

When using a shared lock file, control who can replace, rename, or delete it. A pathname can later refer to a different inode, while an existing file descriptor continues to refer to the object that was originally opened.

Do not assume flock() and fcntl() always coordinate

On local Linux filesystems, flock() locks and traditional fcntl() record locks are normally separate mechanisms. A process using one API should not assume it conflicts with a process using the other.

That means a coordination protocol should standardize on one locking family.

Network filesystems complicate the picture further. Linux may emulate flock() with byte-range locks for NFS or SMB, which can make the mechanisms interact and can change other details of the behavior.

If correctness depends on locking across machines, test the actual filesystem, client, server, and mount configuration you deploy. Do not infer remote semantics only from behavior observed on a local filesystem.

Be careful when converting shared and exclusive flock() locks

A process holding a shared flock() lock can request an exclusive lock, and vice versa.

It is tempting to treat that conversion as an atomic upgrade. On Linux, the conversion is not guaranteed to be atomic: the existing lock can be removed before the new lock is established. Another waiter may acquire a lock in that gap.

So this pattern requires care:

hold shared lock
    -> decide to write
    -> upgrade to exclusive lock
    -> assume protected state never changed

That assumption is unsafe if another participant can acquire the lock during conversion.

A safer design is often to acquire the final lock mode before reading state whose validity depends on that mode, or to revalidate the state after obtaining the exclusive lock.

Keep the critical section small and explicit

A lock should normally cover the operation whose correctness depends on exclusive or shared ownership, not unrelated work.

Avoid holding a file lock while performing slow network calls, waiting for user input, or doing CPU-heavy work that does not require the resource.

Long lock duration increases contention and makes failures harder to diagnose.

Structure code so acquisition and release are visually close:

open resource
acquire lock
    read current state
    validate state
    update state
release lock
close resource

Also define error behavior. If a write fails halfway through, the lock prevents cooperating writers from overlapping the failed operation, but it does not automatically restore the previous file contents. Locking and atomic/durable update techniques solve different problems.

Decide what should happen after fork() and exec()

Descriptor-oriented locks can outlive the control flow that originally acquired them if descriptors are inherited.

flock() locks are associated with open file descriptions and are preserved across execve() when the descriptor itself remains open. OFD locks similarly follow the open file description.

If a child process must not inherit the descriptor, open it with close-on-exec behavior or set FD_CLOEXEC before executing another program.

After fork(), decide explicitly whether parent and child are intended to share the descriptor-oriented lock. Accidental inheritance can keep a lock alive after the parent closes its copy.

Traditional process-associated record locks have different inheritance rules, which is another reason not to switch casually between locking families.

Choose based on ownership and range requirements

A practical decision guide is:

Need simple whole-file coordination on Linux?
    -> flock()

Need portable POSIX byte-range locking and can manage
process-oriented close semantics carefully?
    -> traditional fcntl() record locks

Need Linux byte-range locking with ownership tied to
an open file description?
    -> OFD fcntl() locks

Then check the surrounding constraints:

  • Do all participants use the same locking protocol?
  • Is the filesystem local or remote?
  • Can file descriptors be duplicated or inherited?
  • Does one process open the same file in unrelated code?
  • Are byte ranges genuinely useful?
  • Can the operation safely block while waiting for a lock?

The answers are more important than API familiarity.

Common mistakes

Treating advisory locking as permission enforcement

A noncooperating process can ignore advisory locks on ordinary local Linux filesystems. Use access controls for security boundaries.

Closing an unrelated descriptor while using POSIX record locks

Traditional process-associated record locks can disappear when the process closes another descriptor referring to the same file. This is a major reason to understand their ownership model before using them in large programs.

Mixing flock() and fcntl() among participants

Do not assume the two APIs conflict on local Linux filesystems. Standardize the coordination protocol.

Forgetting inherited descriptors

A duplicated or inherited descriptor can keep a descriptor-oriented lock alive longer than expected.

Holding the lock around unrelated slow work

The longer the critical section, the greater the contention and the larger the operational impact of a stalled process.

Assuming local filesystem behavior applies over NFS or SMB

Remote locking semantics depend on the filesystem protocol and configuration. Test the deployed environment.

When not to use file locks

File locks are a good fit when cooperating processes already share a filesystem resource and need modest coordination around that resource.

They are a poorer fit when coordination spans hosts without a filesystem whose locking semantics you trust, when participants do not share the same protocol, or when the protected state actually lives in a database or another service that already provides stronger transactional primitives.

Do not add a lock file merely because concurrency exists. Use the synchronization mechanism closest to the state whose invariant you are protecting.

Conclusion

Linux file-locking APIs differ most importantly in ownership and lifetime.

flock() gives simple whole-file advisory coordination whose lifetime follows an open file description. Traditional POSIX fcntl() record locks support byte ranges but have process-oriented lifetime rules that make descriptor closing surprisingly important. Linux OFD locks keep byte-range locking while moving ownership back to the open file description.

Choose the mechanism by asking who should own the lock, what range must be protected, how descriptors are duplicated or inherited, and whether the filesystem preserves the semantics you rely on. Once those questions are explicit, the API choice becomes much easier to reason about.