File Locking in C Using flock and fcntl
File locking is a mechanism used to coordinate concurrent file access
across multiple processes or application instances to prevent race
conditions and data corruption. In the C programming language on
Unix-like systems, developers primarily use the flock and
fcntl system calls to establish shared or exclusive locks.
This article explains how file locks function, details the
implementation of both flock and fcntl, and
compares their behavior in synchronizing file access.
Understanding File Locks
A file lock restricts access to a file among multiple processes. On Unix-like operating systems, most file locks are advisory, meaning the kernel tracks the locks, but does not actively prevent processes from reading or writing if they ignore the lock. Processes must explicitly cooperatively query and respect the lock status before performing I/O operations.
Locks generally fall into two categories:
- Shared Lock (Read Lock): Multiple processes can hold a shared lock concurrently, provided none hold an exclusive lock. This is used when processes only need to read data.
- Exclusive Lock (Write Lock): Only a single process can hold an exclusive lock at any given time. No other shared or exclusive locks can be granted until it is released. This is used for write operations to prevent inconsistent states.
File Synchronization Using
flock
The flock system call originates from BSD and operates
on the entire file via an open file descriptor. It is straightforward
and best suited for simple synchronization where byte-range locking is
not required.
Syntax and Flags
The function signature is defined in
<sys/file.h>:
int flock(int fd, int operation);The operation argument accepts the following flags:
LOCK_SH: Place a shared lock.LOCK_EX: Place an exclusive lock.LOCK_UN: Release an existing lock.LOCK_NB: Non-blocking request; returns immediately with an error (EWOULDBLOCK) if the lock cannot be acquired. Can be bitwise OR'd (e.g.,LOCK_EX | LOCK_NB).
Example Implementation
#include <stdio.h>
#include <stdlib.h>
#include <sys/file.h>
#include <unistd.h>
void write_data(const char *filename) {
int fd = open(filename, O_WRONLY | O_CREAT, 0666);
if (fd == -1) {
perror("open failed");
return;
}
// Acquire an exclusive lock (blocking)
if (flock(fd, LOCK_EX) == -1) {
perror("flock failed");
close(fd);
return;
}
// Critical section
dprintf(fd, "Process %d writing\n", getpid());
sleep(2); // Simulate work
// Unlock and close
flock(fd, LOCK_UN);
close(fd);
}Locks created via flock are associated with the open
file table entry rather than the file descriptor itself. Consequently,
duplicated descriptors (such as via dup or
fork) reference the same lock.
File Synchronization Using
fcntl
The fcntl system call is standardized by POSIX and
provides advanced locking features, notably record
locking (also known as byte-range locking). This enables
processes to lock specific segments of a file rather than the entire
file, allowing concurrent writes to non-overlapping regions.
Syntax and the
flock Structure
The function signature is defined in
<fcntl.h>:
int fcntl(int fd, int cmd, struct flock *lock);Common commands (cmd) include:
F_SETLK: Acquire or release a lock. Returns-1(settingerrnotoEACCESorEAGAIN) if blocked.F_SETLKW: Acquire a lock, blocking (waiting) until it is available.F_GETLK: Determine whether another process holds a conflicting lock.
The struct flock controls lock parameters:
struct flock {
short l_type; // F_RDLCK (shared), F_WRLCK (exclusive), F_UNLCK (release)
short l_whence; // SEEK_SET, SEEK_CUR, SEEK_END
off_t l_start; // Offset relative to l_whence
off_t l_len; // Number of bytes to lock (0 means to EOF)
pid_t l_pid; // Returned by F_GETLK: PID of process holding conflicting lock
};Example Implementation
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
void update_record(int fd, off_t offset, size_t length) {
struct flock fl;
fl.l_type = F_WRLCK; // Exclusive lock
fl.l_whence = SEEK_SET; // Absolute offset
fl.l_start = offset; // Start of record
fl.l_len = length; // Length of record
// Acquire lock, wait if necessary
if (fcntl(fd, F_SETLKW, &fl) == -1) {
perror("fcntl lock failed");
return;
}
// Critical section for specific byte range
pwrite(fd, "DATA", 4, offset);
// Release lock
fl.l_type = F_UNLCK;
if (fcntl(fd, F_SETLK, &fl) == -1) {
perror("fcntl unlock failed");
}
}Key Differences Between
flock and fcntl
- Granularity:
flocklocks entire files only.fcntlsupports byte-range locking, allowing multiple instances to write to different sections of the same file concurrently. - Portability:
fcntlis POSIX-compliant and universally supported across POSIX-conforming operating systems.flockoriginated in BSD and may have varying semantics across non-Linux systems or across NFS mounts. - Descriptor and Process Associations:
flocklocks are associated with the open file description. They are preserved acrossforkcalls and can be shared among processes that inherit the descriptor.fcntllocks are associated with the process and the underlying inode. If a process closes any file descriptor pointing to that file, allfcntllocks held by that process on that file are released, even if other descriptors for the file remain open.