Advisory locks with fcntl() — coordinating read and write access across multiple processes.
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.
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.
Click processes to request locks. Watch how read locks coexist but write locks demand exclusivity.
Advisory locks come in two flavors, and the distinction is critical:
The rules are simple:
| Existing Lock | Read Request | Write Request |
|---|---|---|
| None | Granted | Granted |
| Read lock(s) | Granted (shared) | Blocked (must wait) |
| Write lock | Blocked | Blocked |
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:
| Field | Purpose | Common Values |
|---|---|---|
l_type | Lock type | F_RDLCK (read), F_WRLCK (write), F_UNLCK (unlock) |
l_whence | Where l_start is relative to | SEEK_SET (file start), SEEK_CUR (current pos), SEEK_END (file end) |
l_start | Starting byte offset of the lock | 0 for beginning |
l_len | Number of bytes to lock | 0 means "from l_start to end-of-file" |
l_pid | PID of the locking process | Set with getpid() |
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 */
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:
| Command | Behavior |
|---|---|
F_SETLKW | Set the lock. If it can't be obtained (someone else holds a conflicting lock), wait (block) until it can. |
F_SETLK | Set the lock. If it can't be obtained, return -1 immediately (non-blocking). |
F_GETLK | Don'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);
| Lock Type | Required open() Mode |
|---|---|
| F_RDLCK | O_RDONLY or O_RDWR |
| F_WRLCK | O_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."
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.
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 */
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:
l_type set to F_UNLCK.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; }
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.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.
Add processes with read or write lock requests. Press "Step" to advance the simulation and watch the lock manager resolve conflicts.
File locking with fcntl() is the portable, POSIX way to coordinate file access. But it has limitations:
| Feature | fcntl() Locks | flock() | Semaphores |
|---|---|---|---|
| Granularity | Byte-range | Whole-file | Arbitrary resource |
| Portability | POSIX | BSD/Linux | System V |
| Read/Write distinction | Yes | Shared/Exclusive | No (just counters) |
| Works across NFS | Sometimes | Usually not | No |
"There can be multiple readers simultaneously, but there can only be one writer at a time." — Beej