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 (setting errno to EACCES or EAGAIN) 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

  1. Granularity: flock locks entire files only. fcntl supports byte-range locking, allowing multiple instances to write to different sections of the same file concurrently.
  2. Portability: fcntl is POSIX-compliant and universally supported across POSIX-conforming operating systems. flock originated in BSD and may have varying semantics across non-Linux systems or across NFS mounts.
  3. Descriptor and Process Associations:
    • flock locks are associated with the open file description. They are preserved across fork calls and can be shared among processes that inherit the descriptor.
    • fcntl locks are associated with the process and the underlying inode. If a process closes any file descriptor pointing to that file, all fcntl locks held by that process on that file are released, even if other descriptors for the file remain open.