How Linux Implements File Locking Mechanisms
File locking in Linux is a synchronization mechanism designed to
prevent race conditions and data corruption when multiple processes
access the same file concurrently. The Linux operating system implements
this functionality at the kernel level using both advisory and mandatory
locking paradigms, exposed to userspace through key system calls
including flock(), fcntl(), and
lockf(). These mechanisms allow applications to negotiate
shared or exclusive access to entire files or specific byte ranges,
coordinating input/output operations while maintaining system stability
and performance.
Advisory vs. Mandatory Locking
Linux supports two distinct locking philosophies:
- Advisory Locking: This is the default and most
widely used model in Linux. Advisory locks are cooperative; they are
only effective if all participating processes query and respect the lock
before accessing the file. If an uncooperative process ignores the lock
and issues a standard
read()orwrite()call, the kernel will permit the operation. - Mandatory Locking: Under this model, the kernel
enforces the lock on every read and write attempt, blocking unauthorized
access even from processes that do not explicitly check for locks.
Historically, Linux enabled mandatory locking on file systems mounted
with the
mandoption for files configured with specific permission bits (set-group-ID enabled, group-execute disabled). However, because mandatory locking is prone to race conditions, denial-of-service vulnerabilities, and deadlocks, it has been deprecated and largely removed from modern Linux kernels (starting with kernel 5.15).
Core System Calls for File Locking
Linux provides several programmatic interfaces to manage file locks:
1. flock()
Originating from BSD, flock() applies locks to whole
files only. It accepts two primary lock modes:
- Shared Lock (
LOCK_SH): Allows multiple processes to read the file simultaneously. - Exclusive Lock (
LOCK_EX): Restricts access to a single process for write operations.
flock() locks are associated with the underlying open
file table entry (the struct file in the kernel), rather
than the process or the inode alone. This means that if a file
descriptor is duplicated via fork() or dup(),
the child or duplicate shares the same lock reference.
2. fcntl() (POSIX Locks)
POSIX record locking via the fcntl() system call
provides fine-grained control, allowing processes to lock arbitrary byte
ranges within a file rather than the entire file.
Traditional POSIX locks possess unique ownership semantics:
- Locks are associated with both the process ID (
PID) and the file'sinode. - If a process closes any file descriptor pointing to a file, all POSIX locks held by that process on that file are immediately released.
- POSIX locks are not inherited across a
fork().
To resolve the limitation where closing one descriptor drops all
locks across the process (which poses issues in multi-threaded
programs), modern Linux kernels introduce Open File Description
(OFD) locks via fcntl(). OFD locks combine
byte-range precision with flock() inheritance semantics,
binding the lock to the open file description rather than the
PID.
3. lockf()
The lockf() function is a standard C library interface
that acts as a simplified wrapper around fcntl(). It
operates strictly on byte ranges starting from the current file
offset.
Kernel Implementation Details
Under the hood, the Linux Virtual File System (VFS) handles file locking using internal data structures:
struct file_lock: When a process requests a lock, the kernel allocates astruct file_lockinstance containing metadata such as the lock type (F_RDLCK,F_WRLCK), the byte range (fl_start,fl_end), the owning process or file description, and wait queues.- Inode Tracking: The kernel attaches active locks to
linked lists stored inside the file's
struct inode(specificallyi_flctxvia the file-lock context). When a new lock is requested, the kernel traverses this list to verify that the request does not conflict with existing locks. - Wait Queues and Blocking: If a process requests a
conflicting lock without the non-blocking flag (
LOCK_NB), the kernel places the calling task into a sleep state on a wait queue until the conflicting lock is released viaflock()orfcntl(). - Deadlock Detection: For POSIX locks, the Linux
kernel maintains a dependency graph of processes waiting for locks. If
granting a lock or putting a process to sleep would result in a circular
wait condition, the kernel aborts the request and returns the
EDEADLKerror code to the caller.