File Locking on Unix: flock() vs fcntl() and What They Actually Guarantee
This article covers File Locking on Unix: flock() vs fcntl() and What They Actually Guarantee. File locking is deceptively subtle. Learn advisory locks, flock vs fcntl record locks, lease-like patterns, and how to implement a safe "singl...
File locking is often used for:
- ensuring a single instance of a daemon
- coordinating writers to a file
- guarding critical sections across processes
But locking on Unix is mostly advisory: the kernel won’t stop you from writing unless programs cooperate.
1) Advisory vs mandatory locking
- Advisory: processes voluntarily acquire locks; others must check.
- Mandatory (rare): kernel enforces (Linux supports in limited scenarios; generally avoid).
In practice, assume advisory.
2) flock()
- Simple interface.
- Locks are associated with an open file description.
flockis whole-file (no byte-range).
Common use: lock a lockfile.
3) fcntl() record locks
- Byte-range locks via
struct flock. - More flexible.
- Historically interacts with NFS differently (depending on server/client).
4) What locks do NOT guarantee
- They do not automatically make file writes atomic.
- They do not automatically flush data to disk.
- They do not prevent a process from writing if it ignores locks.
Another subtle non-guarantee:
- Locks do not define what you write. You still need a safe update pattern.
If you are locking because you want crash safety, pair locking with a durable update pattern like:
5) C: single-instance lockfile with flock
This pattern is widely used for daemons.
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/file.h>
#include <unistd.h>
int main(void) {
const char *lock_path = "/tmp/myapp.lock";
int fd = open(lock_path, O_RDWR | O_CREAT | O_CLOEXEC, 0644);
if (fd < 0) {
fprintf(stderr, "open lock: %s\n", strerror(errno));
return 1;
}
if (flock(fd, LOCK_EX | LOCK_NB) != 0) {
if (errno == EWOULDBLOCK) {
fprintf(stderr, "already running\n");
return 2;
}
fprintf(stderr, "flock: %s\n", strerror(errno));
return 1;
}
// Keep fd open for the life of the process.
printf("lock acquired, running...\n");
pause();
}
Important:
- Keep the lock fd open. Closing releases the lock.
What happens with fork/exec/dup?
This is where people get confused:
dup()duplicates a file descriptor, but points to the same open file description.fork()copies file descriptors into the child.
If you keep multiple fds referencing the same underlying open file description, the lock lifetime can be surprising.
Practical rule:
- treat the lock fd as a unique owned resource and keep it open in exactly one place
If you exec() a new program, use O_CLOEXEC (as in the example) so the lock doesn’t leak across exec.
6) Zig: flock lockfile
const std = @import("std");
const os = std.os;
const c = @cImport({
@cInclude("sys/file.h");
});
pub fn main() !void {
var file = try std.fs.cwd().createFile("/tmp/myapp.lock", .{ .read = true, .truncate = false });
defer file.close();
const fd = file.handle;
if (c.flock(fd, c.LOCK_EX | c.LOCK_NB) != 0) {
return error.AlreadyRunning;
}
std.debug.print("lock acquired\n", .{});
while (true) {
std.time.sleep(1_000_000_000);
}
}
7) Rust: lockfile with flock
use std::fs::OpenOptions;
use std::io;
use std::os::unix::io::AsRawFd;
fn main() -> io::Result<()> {
let f = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open("/tmp/myapp.lock")?;
let rc = unsafe { libc::flock(f.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
if rc != 0 {
let e = io::Error::last_os_error();
eprintln!("lock failed: {e}");
std::process::exit(2);
}
println!("lock acquired");
loop { std::thread::park(); }
}
8) fcntl record-lock example (byte range)
Use cases:
- locking regions in a shared data file
- cooperative coordination between processes
Conceptually:
- set
l_type = F_WRLCKorF_RDLCK - specify
l_start,l_len,l_whence
Two important semantics differences vs flock:
- record locks are associated with the process, not the open file description
- closing any fd for that file in the process may release locks (depends on exact pattern and platform)
This is why mixing multiple fds to the same file with record locks can be error-prone.
9) Deadlocks and lock ordering
It is easy to deadlock if you lock multiple resources in different orders.
Rules of thumb:
- define a global lock order (by path name, inode number, or logical role)
- keep lock hold times short
- avoid calling out to other subsystems while holding a lock
10) Network filesystems and portability notes
Historically, file locking semantics over NFS can be different depending on server/client configuration.
If you rely on locking for correctness across machines:
- test your exact deployment
- consider a coordination service (e.g. etcd/consul) for distributed locks
For single-host coordination, flock/fcntl are usually enough.
References
flock(2): https://man7.org/linux/man-pages/man2/flock.2.htmlfcntl(2)record locks: https://man7.org/linux/man-pages/man2/fcntl.2.htmlopen(2): https://man7.org/linux/man-pages/man2/open.2.html- “Advanced Programming in the UNIX Environment” (Stevens & Rago)