Beej's Guide to Unix IPC — Chapter 6

File Locking

Advisory locks with fcntl() — coordinating read and write access across multiple processes.

Prerequisites: fork() (Ch. 2) + File descriptors. That's it.
9
Chapters
3+
Simulations
0
Assumed Knowledge

Chapter 0: Why File Locking?

Imagine two processes writing to the same log file at the same time. Process A writes "ERROR: disk full" while process B writes "INFO: backup started." Without coordination, you might get "ERROR: INFO: diskbackup full started" — a mangled mess.

File locking solves this by letting a process claim exclusive (or shared) access to a file — or even a region of a file. Other processes that play by the rules will wait until the lock is released before touching the data.

The mental model: Think of a bathroom door with a lock. If someone is inside (write lock), nobody else can enter. But if the sign says "reading room" (read lock), multiple people can browse simultaneously — you only lock the door when someone needs to write on the whiteboard.

There are two flavors of locking: mandatory (the kernel enforces it — read/write calls actually block) and advisory (processes cooperate voluntarily). Beej's guide focuses entirely on advisory locks, and so will we. Advisory locks are sufficient for nearly every real-world use case.

Lock Contention

Click processes to request locks. Watch how read locks coexist but write locks demand exclusivity.

What kind of file locking does Beej's guide focus on?

Chapter 1: Read Locks vs Write Locks

Advisory locks come in two flavors, and the distinction is critical:

Read lock (F_RDLCK) — also called a shared lock. Multiple processes can hold a read lock on the same region simultaneously. This makes sense: if nobody is modifying the data, it's safe for everyone to read.
Write lock (F_WRLCK) — also called an exclusive lock. Only one process can hold a write lock on a region. No other process can hold any lock (read or write) on the same region while a write lock is active.

The rules are simple:

Existing LockRead RequestWrite Request
NoneGrantedGranted
Read lock(s)Granted (shared)Blocked (must wait)
Write lockBlockedBlocked
Key insight: This is a classic readers-writer lock pattern. It maximizes concurrency: many readers at once, but writers get exclusive access. You'll see this pattern everywhere in databases, operating systems, and concurrent programming.
Can a process acquire a read lock while another process holds a read lock on the same region?

Chapter 2: struct flock — Describing a Lock

To set a lock with fcntl(), you fill out a struct flock that describes exactly what you want. It's declared in <fcntl.h>:

c
struct flock {
    short l_type;    /* F_RDLCK, F_WRLCK, or F_UNLCK */
    short l_whence;  /* SEEK_SET, SEEK_CUR, or SEEK_END */
    off_t l_start;   /* offset from l_whence */
    off_t l_len;     /* length of region, 0 = to EOF */
    pid_t l_pid;     /* PID of the lock owner */
};

Let's break down each field:

FieldPurposeCommon Values
l_typeLock typeF_RDLCK (read), F_WRLCK (write), F_UNLCK (unlock)
l_whenceWhere l_start is relative toSEEK_SET (file start), SEEK_CUR (current pos), SEEK_END (file end)
l_startStarting byte offset of the lock0 for beginning
l_lenNumber of bytes to lock0 means "from l_start to end-of-file"
l_pidPID of the locking processSet with getpid()
Key insight: Setting l_len to 0 is the most common pattern — it locks from the start offset all the way to the end of the file, even if the file grows later. This is the "lock the whole file" shortcut.

Here's the typical setup for locking an entire file for writing:

c
struct flock fl;
fl.l_type   = F_WRLCK;     /* write lock */
fl.l_whence = SEEK_SET;   /* relative to beginning */
fl.l_start  = 0;          /* start at byte 0 */
fl.l_len    = 0;          /* lock to EOF */
fl.l_pid    = getpid();   /* our PID */
What does setting l_len to 0 mean?

Chapter 3: Setting a Lock with fcntl()

Once you've filled out the struct flock, you pass it to fcntl() to actually set the lock. The fcntl() call is the Swiss Army knife of file operations, but for locking, you only need to know three commands:

CommandBehavior
F_SETLKWSet the lock. If it can't be obtained (someone else holds a conflicting lock), wait (block) until it can.
F_SETLKSet the lock. If it can't be obtained, return -1 immediately (non-blocking).
F_GETLKDon't set a lock — just check if one exists. Returns info about any conflicting lock.

Here's the complete sequence to lock a file for writing:

c
struct flock fl;
int fd;

fl.l_type   = F_WRLCK;
fl.l_whence = SEEK_SET;
fl.l_start  = 0;
fl.l_len    = 0;
fl.l_pid    = getpid();

fd = open("filename", O_WRONLY);

/* F_SETLKW: block until the lock is available */
fcntl(fd, F_SETLKW, &fl);
Critical detail: The file must be opened with a mode that matches the lock type. F_RDLCK requires O_RDONLY or O_RDWR. F_WRLCK requires O_WRONLY or O_RDWR. If you get this wrong, fcntl() returns -1 and sets errno to EBADF.
Lock TypeRequired open() Mode
F_RDLCKO_RDONLY or O_RDWR
F_WRLCKO_WRONLY or O_RDWR

The "W" in F_SETLKW stands for "wait." It's the blocking version. Most of the time, this is what you want — you're telling the kernel "I need this lock, I'll wait as long as it takes."

What does F_SETLKW do if the lock can't be obtained immediately?

Chapter 4: Clearing a Lock

Unlocking is delightfully simple compared to locking. You change l_type to F_UNLCK and call fcntl() again with F_SETLK:

c
/* ... after doing work on the locked file ... */

fl.l_type = F_UNLCK;            /* set to unlock */
fcntl(fd, F_SETLK, &fl);       /* release the lock */

That's it. Leave all the other fields (l_whence, l_start, l_len) the same as when you set the lock, and just flip l_type to F_UNLCK. Note that we use F_SETLK here (not F_SETLKW) because unlocking never needs to wait — you're releasing, not acquiring.

Automatic cleanup: When a process closes a file descriptor, all locks held by that process on that file are released automatically. When a process exits, all its locks are released. This is a safety net, but you should still unlock explicitly — relying on cleanup is sloppy.

Here's the full lock-then-unlock pattern:

c
struct flock fl;
int fd;

fl.l_type   = F_WRLCK;
fl.l_whence = SEEK_SET;
fl.l_start  = 0;
fl.l_len    = 0;
fl.l_pid    = getpid();

fd = open("filename", O_WRONLY);
fcntl(fd, F_SETLKW, &fl);   /* lock it */

/* ... do critical work ... */

fl.l_type = F_UNLCK;
fcntl(fd, F_SETLK, &fl);    /* unlock it */
How do you unlock a file region with fcntl()?

Chapter 5: F_GETLK — Checking for Locks

Sometimes you don't want to set a lock — you just want to see if one exists. That's what F_GETLK does. You fill out a struct flock describing the lock you would want, and fcntl() tells you if there's a conflict:

c
struct flock fl;
fl.l_type   = F_WRLCK;      /* "I want to write-lock..." */
fl.l_whence = SEEK_SET;
fl.l_start  = 0;
fl.l_len    = 0;
fl.l_pid    = getpid();

fcntl(fd, F_GETLK, &fl);    /* ask the kernel */

if (fl.l_type == F_UNLCK) {
    printf("No conflicting lock. Go ahead!\n");
} else {
    printf("Conflict! PID %d holds a %s lock\n",
           fl.l_pid,
           fl.l_type == F_RDLCK ? "read" : "write");
}

Here's how F_GETLK works:

Key insight: F_GETLK does NOT set a lock. It only checks. Between the time you check and the time you attempt to set the lock, another process could grab it. This is a classic TOCTOU (time-of-check-to-time-of-use) race condition. If you need the lock, just use F_SETLKW.
What does F_GETLK set l_type to when there is no conflicting lock?

Chapter 6: lockdemo.c — The Full Program

Beej provides a complete demo program that you can run in multiple windows to see locking in action. With no arguments, it requests a write lock. With any argument, it requests a read lock:

c
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    /*         l_type   l_whence  l_start  l_len  l_pid */
    struct flock fl = {F_WRLCK, SEEK_SET, 0,      0,     0 };
    int fd;

    fl.l_pid = getpid();
    if (argc > 1)
        fl.l_type = F_RDLCK;   /* any arg = read lock */

    if ((fd = open("lockdemo.c", O_RDWR)) == -1) {
        perror("open");
        exit(1);
    }

    printf("Press <RETURN> to try to get lock: ");
    getchar();
    printf("Trying to get lock...");

    if (fcntl(fd, F_SETLKW, &fl) == -1) {
        perror("fcntl");
        exit(1);
    }
    printf("got lock\n");

    printf("Press <RETURN> to release lock: ");
    getchar();

    fl.l_type = F_UNLCK;
    if (fcntl(fd, F_SETLK, &fl) == -1) {
        perror("fcntl");
        exit(1);
    }
    printf("Unlocked.\n");

    close(fd);
    return 0;
}
Try it yourself: Run lockdemo in two terminal windows. In one, run with no args (write lock). In another, run with any argument (read lock). Notice: multiple read locks coexist happily, but a write lock blocks everyone else. A write lock request will starve if readers keep piling on.
Window 1: ./lockdemo
Requests F_WRLCK → gets exclusive lock
↓ Window 2 blocks, waiting for Window 1 to release
Window 2: ./lockdemo x
Requests F_RDLCK → blocked by write lock
In lockdemo, what determines whether a read or write lock is requested?

Chapter 7: Lock Contention Simulator

Watch multiple processes compete for locks on the same file region. Read locks (shared) coexist, but a write lock (exclusive) forces everyone else to wait.

Readers-Writer Lock Simulation

Add processes with read or write lock requests. Press "Step" to advance the simulation and watch the lock manager resolve conflicts.

File: unlocked
Writer starvation: Notice that if readers keep arriving, a waiting writer may never get the lock. The kernel doesn't guarantee fairness. In practice, you should keep lock durations short to minimize this risk.
What happens if a process requests a write lock while three read locks are active?

Chapter 8: Beyond File Locking

File locking with fcntl() is the portable, POSIX way to coordinate file access. But it has limitations:

Featurefcntl() Locksflock()Semaphores
GranularityByte-rangeWhole-fileArbitrary resource
PortabilityPOSIXBSD/LinuxSystem V
Read/Write distinctionYesShared/ExclusiveNo (just counters)
Works across NFSSometimesUsually notNo
Coming up: When you need to synchronize access to resources that aren't files — like shared memory segments — you'll want semaphores. They provide the same kind of mutual exclusion but in a more general form.

"There can be multiple readers simultaneously, but there can only be one writer at a time." — Beej

What is the main limitation of advisory file locks?