Est.

File Locking With Flock and Fcntl

Two Linux tools solve file-locking problems in fundamentally different ways.

Contributing Editor · · 11 min read
Cover illustration for “File Locking With Flock and Fcntl”
Filesystem Internals · September 20, 2026 · 11 min read · 2,363 words

Two processes write to the same file at the same time, and nobody coordinated. That's the entire problem this article is about: torn writes, corrupted counters, updates that vanish because two writers stepped on each other. Linux gives you two tools to stop this from happening, flock and fcntl, and they solve the problem in genuinely different ways, not just with different function names.

Locking on Linux is advisory, which is a polite way of saying it's an honor system. The kernel doesn't force anything. A process with the right permissions can walk right past a lock and read or write the file anyway, no questions asked. Advisory locking works only because every process involved agrees to check the lock first. It's less like a deadbolt and more like a "please don't sit here" jacket draped over a chair at a coffee shop: it works great, right up until someone decides it doesn't apply to them.

The two lineages come from different families. flock() traces back to 4.2BSD. fcntl() record locking comes out of POSIX and System V. They were not designed together, and on Linux, since kernel 2.0, flock() is its own system call rather than a wrapper around fcntl(). That matters more than it sounds: the two lock types don't see each other on a local filesystem. No shared bookkeeping, no cross-checking, and no deadlock detection between the two. A process using flock() and another using fcntl() on the same local file can both believe they hold the floor.

How flock locks work: whole-file locks tied to the open file description

flock() locks the entire file. There's no concept of "just this chunk." Three operations run the whole show: LOCK_SH for a shared read lock, LOCK_EX for an exclusive write lock, and LOCK_UN to let go. Adding LOCK_NB to any of these stops the call from blocking; it just fails immediately if the lock isn't free.

The lock belongs to the open file description, not the file descriptor number. Fork a process, or dup() a descriptor, and every copy points back to the same open file description, and therefore the same lock. Close every one of those descriptors, or call LOCK_UN explicitly, and the lock goes away. But open the same file twice with two separate open() calls, and you get two independent open file descriptions. The kernel treats them as strangers. One can block the other, lock against lock, even though it's the same process poking the same file from two directions.

Lock conversion is where flock() gets genuinely dangerous. Asking to switch from a shared lock to an exclusive one (or back) is not atomic. The kernel drops the old lock first, then goes looking for the new one. In that gap, another process's pending request can slide in and get granted first. So the conversion you thought was instant actually has a keyhole-sized window where someone else can walk through.

flock() also survives execve(), and it does not detect deadlocks. Two blocking flock() calls can lock each other out forever, silently, with no EDEADLK to warn either side. And release timing has its own wrinkle: the lock isn't guaranteed to release only after every effect of close() is visible to other processes. The timing of lock release relative to write visibility is not guaranteed to be strictly ordered.

How fcntl record locks work: byte-range precision tied to the process

fcntl() locks bytes, not files. You hand it a struct flock with l_start, l_len, and l_whence, and it locks exactly the range you specify. Setting l_len to 0 gives you "from here to end of file, however large the file grows later." Byte-range locking lets you target specific regions of a file with precise offsets and lengths. Since Linux 2.4.21 (and 2.5.49 on the other branch), negative l_len is legal too, covering the bytes before l_start.

Three commands do the work. Acquiring or releasing without blocking is what F_SETLK does. F_SETLKW blocks until it can get the lock. F_GETLK just asks a question: could this lock be placed? It doesn't actually place anything. And that's exactly the trap: checking with F_GETLK and then acquiring with F_SETLK is two separate calls, not one atomic operation, so another process can slip in between them and take the lock you were just told was available.

Ownership here is the (inode, pid) pair. The lock belongs to the process, not to any particular file descriptor. Applying a new lock on a byte range you already lock converts the old one instead of stacking on top of it. And here's the mechanism that causes the most damage in real codebases: closing any file descriptor the process holds on that file releases every POSIX lock that process holds on the file, even a lock acquired through a completely different descriptor. In a multithreaded program, one thread closing its own descriptor can quietly strip the locks every other thread was depending on. Nobody gets an error. The locks are just gone.

On the upside, F_SETLKW actually does deadlock detection. Crossing a deadlock cycle makes the kernel hand back EDEADLK instead of hanging both processes forever. For anyone dealing with big files, the original fcntl() syscall wasn't built for large offsets. Linux 2.4 added fcntl64() with a flock64 structure and matching F_GETLK64, F_SETLK64, and F_SETLKW64 commands to address that.

The ownership gap that OFD locks were introduced to fill

The thread-closing-a-descriptor flaw above isn't a rare edge case, it's a structural flaw for any thread-per-task design. Traditional fcntl() locks are owned by the process as a whole, so threads can't have independent locks on the same file, and one thread's cleanup can nuke another thread's lock without warning.

Open file description locks, added in Linux 3.15 (2015), fix exactly this. F_OFD_SETLK, F_OFD_SETLKW, and F_OFD_GETLK tie ownership to the open file description (the actual struct file underneath) rather than the pid. Two threads that each call their own open() get two independent lock owners, even though they're in the same process. They can lock against each other, contend with each other, and behave exactly the way most people assume record locks already worked, before finding out the hard way that they didn't.

The trade-off: OFD locks drop deadlock detection. F_SETLKW gives you EDEADLK, F_OFD_SETLKW does not. And OFD locks are Linux-specific, so picking them means picking correctness over portability. Every codebase using record locks has to choose a side: the older, portable API with the thread-fd trap built in, or the newer, Linux-only API that actually behaves the way you'd expect.

Anyone scripting instead of coding directly against the syscalls can use the flock(1) command-line tool, which exposes OFD locking through --fcntl, and the --fcntl flag lets you do OFD-based locking straight from a shell script.

What changes when the filesystem is remote: NFS, SMB, and cluster filesystems

Everything above assumes a local disk. Networked storage changes the rules, and the rules have changed more than once depending on which kernel is running.

On NFS, anything before Linux 2.6.11 simply didn't lock with flock() across the network at all, it stayed local to the machine that called it. fcntl() byte-range locks, by contrast, did work over NFS, provided the kernel was recent enough and the server actually supported locking. Starting with Linux 2.6.12, NFS clients began emulating flock() by turning it into a whole-file fcntl() lock under the hood. The two lock types that ignore each other locally now interact with each other over NFS. It also means an exclusive flock() lock requires the file to be opened for writing. Then Linux 2.6.37 added a local_lock mount option, letting you opt back out and keep locks local instead of shipping them to the server.

SMB followed a similar arc but later. Up through Linux 5.4, flock() wasn't propagated over SMB at all, so a remote client just saw an unlocked file, lock or no lock. Since Linux 5.5, flock() gets emulated as a whole-file SMB byte-range lock, and fcntl() and flock() start interacting the way they do on NFS. The SMB protocol's own locking semantics are mandatory. Once that emulation kicks in, any I/O on a locked region from a separate file descriptor fails outright with EACCES, whether that process ever asked for a lock or not.

Putting those two together leaves no clean answer. flock() support over NFS has been unreliable across versions, and fcntl() doesn't behave reliably on SMB. Some software has historically hedged by grabbing both lock types at once, on the theory that at least one of them will actually work on whatever filesystem shows up underneath it.

Cluster filesystems add their own twist. GFS2 offers a localflocks mount option that makes both fcntl and flock locks local to the node instead of coordinated across the whole cluster. That's a real performance win, but it comes with homework: every application touching that filesystem needs auditing to make sure nothing depends on cluster-wide coordination that just quietly stopped happening.

Distributed filesystems aimed at AI and ML workloads tend to support both flock and POSIX fcntl locking deliberately, because the tools sitting on top, the tools sitting on top were written assuming POSIX semantics exist and work.

Mandatory locking: why it exists, why the Linux documentation says not to use it

Advisory locking, the model behind everything above, only coordinates the processes that opt in. The kernel does nothing to stop you from ignoring the lock.

Mandatory locking flips that: the kernel enforces it, and any process doing I/O on a locked region gets blocked or errors out, whether or not it ever called a locking function. On Linux, mandatory locking is only reachable through fcntl() or lockf(), never through flock(). BSD-style whole-file locks never become mandatory on a local Linux filesystem, full stop.

The Linux documentation itself calls mandatory locking unreliable. The Linux documentation itself calls mandatory locking unreliable, and that is the project's own assessment of its own feature. Advisory locking is the model that actually holds up, and that is settled rather than a matter of taste.

SMB is the one place mandatory-style behavior appears anyway, by accident. Since Linux 5.5, SMB byte-range locks enforce access the way the protocol requires, which produces mandatory semantics as a side effect of how SMB works, not because Linux decided mandatory locking was suddenly a good idea.

Don't build a locking strategy around mandatory enforcement on Linux. Build it around advisory locks, making sure every process touching the file actually follows the convention. A lock that everyone respects works. A lock that the kernel enforces on Linux, outside of the SMB accident above, mostly doesn't exist.

Selecting between flock, fcntl, and OFD locks based on what your code requires

Start with granularity. Coordinating access to a file as a whole, a single-writer log, a singleton process guard, a config file that gets rewritten occasionally, flock() is simpler and does the job without extra ceremony.

Need multiple processes or threads touching different, non-overlapping regions of the same file at once, like database-style record locking or an append-only file with pre-allocated slots? That calls for byte-range locking: fcntl() or OFD locks, not flock().

Threading model decides which byte-range API fits. Single-threaded or multi-process code can use traditional F_SETLK and F_SETLKW, as long as everyone remembers that closing any descriptor on the file drops every lock the process holds on it. Multithreaded code should reach for OFD locks (F_OFD_SETLK, F_OFD_SETLKW) if Linux 3.15 or newer is a safe assumption, with each thread opening its own descriptor and owning its own lock independently.

Deadlock detection is a real factor in the decision. F_SETLKW has it built in. Blocking flock() calls do not. Build your error handling around that fact rather than discovering it during an incident.

Portability and correctness pull in opposite directions here. flock() runs across BSD and Linux but carries that non-atomic conversion gap. fcntl() POSIX locks are standardized everywhere but carry the thread-and-descriptor trap. OFD locks fix that trap but only run on Linux.

If any part of this is running against NFS or SMB mounts, treat neither mechanism as trustworthy by default. Check the mount options for the specific filesystem in play, and consider coordinating outside the filesystem instead, using a lock server, a database row used as a mutex, or a lock file that lives on local storage rather than the network mount.

For shell scripts, flock(1) wraps flock(2) and fits singleton-script patterns and cron job serialization well. When a script needs finer granularity than "lock the whole file," the --fcntl flag exposes OFD locking without writing a line of C.

The gotchas that cause bugs: a map of where each mechanism breaks in practice

flock()'s worst habit is the non-atomic lock conversion. The gap between dropping a shared lock and acquiring an exclusive one means code that assumes the switch happens instantly is code that will eventually hand the lock to a process that snuck in during that window. Treat every conversion as a moment of genuine vulnerability.

The duplicate-open trap catches people constantly: two separate open() calls on the same file produce two open file descriptions, and flock() treats them as unrelated, even from the same process. If your code opens a file twice out of habit, you may be locking against yourself, and contention under load will reveal it.

fcntl()'s signature bug is the close-releases-everything behavior. Any descriptor closing anywhere in the process wipes out every POSIX lock that process holds on that file, regardless of which descriptor originally acquired it. In a multithreaded program, this is the mechanism that makes locks disappear with no error message and no warning, just a lock that used to be there and now isn't.

OFD locks close that specific gap but reopen the risk of deadlock, since neither F_OFD_SETLKW nor blocking flock() will tell you when two lock holders are waiting on each other forever. And across all of it, the flock-versus-fcntl divide on local filesystems means a program mixing both lock types on the same file is coordinating with itself using two systems that have never been introduced to each other. Pick one mechanism per file, know its specific failure mode, and build around that failure mode deliberately rather than discovering it in production.

Sources

  1. fcntl_locking(2) - Linux manual page
  2. flock(1) - Linux manual page
  3. flock(2) - Linux manual page
  4. utcc.utoronto.ca
  5. The GNU C Library - GNU Project - Free Software Foundation (FSF)
  6. gnu.org
  7. kernel.org
  8. gavv.net

More in Filesystem Internals