POSIX File Locking (fcntl Advisory & OFD Locks vs. BSD flock)
In Unix and POSIX operating systems, concurrent processes writing to the same file require synchronization to avoid race conditions and data corruption.
Unlike Windows—where file locking is strictly mandatory and enforced at the kernel file-system layer—Unix file locking is historically advisory: cooperating processes must explicitly check for and honor locks before performing I/O.
Unix provides three distinct mechanisms for locking files, each with radically different lifetime and inheritance rules:
- BSD
flock(): Whole-file locks bound to the Open File Description. - POSIX Classic Record Locking (
fcntlF_SETLK): Byte-range locks bound to the(Process, Inode)pair. - POSIX Open File Description (OFD) Locks (
fcntlF_OFD_SETLK): Modern byte-range locks bound to the Open File Description.
1. The Classic POSIX fcntl Record Lock
Standardized by POSIX.1 under <fcntl.h>, record locking allows locking arbitrary byte ranges within a file (from single records to the entire file):
#include <fcntl.h>
#include <unistd.h>
struct flock fl = {
.l_type = F_WRLCK, /* F_RDLCK (read/shared), F_WRLCK (write/exclusive), F_UNLCK */
.l_whence = SEEK_SET, /* SEEK_SET, SEEK_CUR, SEEK_END */
.l_start = 0, /* Offset start */
.l_len = 0, /* Length (0 means lock until EOF, including future appended bytes) */
};
// F_SETLK: Non-blocking (fails with EAGAIN/EACCES if locked)
// F_SETLKW: Blocking (waits for lock to become available; checks for deadlocks and returns EDEADLK)
if (fcntl(fd, F_SETLK, &fl) == -1) {
perror("fcntl locking failed");
}
The Infamous POSIX Record Locking Flaw
The original POSIX record locking design suffers from a notorious, decades-old flaw: locks are associated with the (process, inode) pair, NOT the file descriptor.
This produces two severe hazards in production software:
- The Accidental Unlock Hazard:
If a process holds a POSIX lock on a file and opens the same file a second time (or a third-party library opens it), closing ANY file descriptor pointing to that file immediately and silently drops ALL POSIX locks held by the process on that file:
int fd1 = open("database.db", O_RDWR); fcntl(fd1, F_SETLK, &lock); // Lock acquired! int fd2 = open("database.db", O_RDONLY); close(fd2); // DISASTER: Drops the lock acquired on fd1! - Lost Across
fork(): POSIX record locks are not inherited by child processes acrossfork().
2. BSD flock(2)
Originating from BSD 4.2, flock() locks the entire file (no byte-range support).
#include <sys/file.h>
int flock(int fd, int operation);
// Operations: LOCK_SH (shared), LOCK_EX (exclusive), LOCK_UN (unlock), LOCK_NB (non-blocking)
Unlike POSIX fcntl locks, flock() locks are associated directly with the Open File Description (struct file in the kernel):
- Closing a separate descriptor to the same file (
fd2) does not release the lock onfd1. - Locks are preserved and shared across
fork()and descriptor duplication (dup()). - Limitation: Cannot lock sub-ranges or single records.
3. The Modern Solution: Open File Description (OFD) Locks
To resolve the flaws of classic POSIX locks without losing byte-range capability, Linux 3.15 introduced Open File Description (OFD) Locks, standardized in POSIX.1-2024 (Issue 8).
OFD locks use the same struct flock structure but pass F_OFD_SETLK, F_OFD_SETLKW, and F_OFD_GETLK:
struct flock fl = {
.l_type = F_WRLCK,
.l_whence = SEEK_SET,
.l_start = 0,
.l_len = 0,
.l_pid = 0, // Must be initialized to 0 for OFD locks
};
// Acquires byte-range lock tied to the Open File Description
fcntl(fd, F_OFD_SETLK, &fl);
Why OFD Locks Win
- No Accidental Unlocks: Closing another descriptor (
fd2) opened to the same file will never release an OFD lock held onfd1. - Inherited Across
fork(): Children share the exact same lock if they inherit the descriptor. - Multithreading Safe: Two threads in the same process can use separate
open()descriptors to arbitrate locks against each other (impossible with classic POSIX locks).
Comprehensive Comparison Matrix
| Feature | Classic POSIX (F_SETLK) |
BSD flock() |
POSIX OFD (F_OFD_SETLK) |
|---|---|---|---|
| Lock Association | (Process, Inode) |
Open File Description | Open File Description |
| Lock Granularity | Byte-range (records) | Entire file only | Byte-range (records) |
| Close Behavior | Closing ANY fd unlocks ALL | Only unlocks when all dups closed | Only unlocks when all dups closed |
Across fork() |
Not inherited | Inherited & shared | Inherited & shared |
| Thread Contention | Threads in same process cannot lock against each other | Threads can lock with separate open() |
Threads can lock with separate open() |
| Deadlock Detection | Supported (returns EDEADLK) |
Not supported | Not supported |
| Standards Status | Standard POSIX (1003.1) | Non-POSIX (de facto BSD/Linux) | Linux 3.15+, POSIX.1-2024 (Issue 8) |
Frequently Asked Questions (FAQ)
Why does closing an unrelated file descriptor release a POSIX fcntl lock?
Because classic POSIX fcntl locks are bound to the process and the underlying inode, not to the individual file descriptor. As specified by the POSIX standard, when any file descriptor pointing to an inode is closed by a process, all POSIX record locks held on that inode by that process are immediately and unconditionally released.
What is the difference between advisory and mandatory file locking?
Advisory locking requires all cooperating processes to explicitly query and honor the lock via API calls, while mandatory locking causes the kernel to automatically block or reject uncoordinated read() and write() calls. Unix and POSIX operating systems rely almost exclusively on advisory locking; Linux has deprecated mandatory file locking due to performance overhead and race condition liabilities.
When should you use Open File Description (OFD) locks instead of flock()?
Use OFD locks when you need byte-range (record) granularity or need multiple threads in a multithreaded process to lock distinct sections of a file independently. While flock() is simple, it can only lock the entire file; OFD locks combine the range granularity of fcntl with the sane descriptor-bound lifetime semantics of flock().
Related Topics
- POSIX Standards Overview: Portability contracts across Unix systems.
- Linux - Inode vs File Descriptor: Understanding the relationship between descriptors and open file descriptions.
- POSIX Threads (pthreads): In-memory synchronization primitives.
