There were none: fcntl answered F_SETLK with EINVAL and flock was not a system call. Alpine's apk, which runs unmodified on SchOS through Alpine's own musl, locks its database before reading it and stopped there. kernel/fs/lock.c keeps one table of advisory locks keyed by (dev, inum): flock() for the whole file, owned by the open file description; F_SETLK/F_SETLKW/F_GETLK for byte ranges, owned by the process and released when it closes any descriptor on the file or exits; and the F_OFD_* forms, ranges owned by the description. flock and record locks do not see each other, as on Linux. Waiters sleep until any release and are interrupted by a signal. cmd/locktest checks each between a parent and a child, including range splitting, F_GETLK's report, and a blocked waiter being woken. shell.nix gains pkg-config, meson, ninja, m4, bison, flex and xz, for the X ports. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
152 lines
4.6 KiB
C
152 lines
4.6 KiB
C
// locktest: flock() and fcntl() record locks, each property a program
|
|
// relies on checked between a parent and a child -- the only way to see a
|
|
// lock is to be someone it keeps out.
|
|
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <sys/file.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
|
|
static int bad;
|
|
|
|
#define CHECK(cond, ...) do { \
|
|
if (!(cond)) { printf("locktest: FAIL " __VA_ARGS__); printf("\n"); bad++; } \
|
|
} while (0)
|
|
|
|
#define PATH "/tmp/locktest"
|
|
|
|
// Run `fn` in a child and return its exit status.
|
|
static int
|
|
child(int (*fn)(void))
|
|
{
|
|
pid_t p = fork();
|
|
if (p == 0)
|
|
_exit(fn());
|
|
int st = 0;
|
|
waitpid(p, &st, 0);
|
|
return WIFEXITED(st) ? WEXITSTATUS(st) : 99;
|
|
}
|
|
|
|
static int
|
|
try_flock_ex(void)
|
|
{
|
|
int fd = open(PATH, O_RDWR);
|
|
return flock(fd, LOCK_EX | LOCK_NB) == 0 ? 0 : (errno == EWOULDBLOCK ? 1 : 2);
|
|
}
|
|
|
|
static int
|
|
try_flock_sh(void)
|
|
{
|
|
int fd = open(PATH, O_RDWR);
|
|
return flock(fd, LOCK_SH | LOCK_NB) == 0 ? 0 : 1;
|
|
}
|
|
|
|
static int
|
|
set(int fd, int cmd, short type, off_t start, off_t len)
|
|
{
|
|
struct flock fl = { .l_type = type, .l_whence = SEEK_SET,
|
|
.l_start = start, .l_len = len };
|
|
return fcntl(fd, cmd, &fl);
|
|
}
|
|
|
|
static int
|
|
try_range_10_19(void)
|
|
{
|
|
int fd = open(PATH, O_RDWR);
|
|
return set(fd, F_SETLK, F_WRLCK, 10, 10) == 0 ? 0 : 1;
|
|
}
|
|
|
|
static int
|
|
try_range_50_59(void)
|
|
{
|
|
int fd = open(PATH, O_RDWR);
|
|
return set(fd, F_SETLK, F_WRLCK, 50, 10) == 0 ? 0 : 1;
|
|
}
|
|
|
|
static int
|
|
wait_range_10_19(void)
|
|
{
|
|
int fd = open(PATH, O_RDWR);
|
|
return set(fd, F_SETLKW, F_WRLCK, 10, 10) == 0 ? 0 : 1;
|
|
}
|
|
|
|
static int
|
|
lock_and_exit(void)
|
|
{
|
|
int fd = open(PATH, O_RDWR);
|
|
return set(fd, F_SETLK, F_WRLCK, 0, 0) == 0 ? 0 : 1;
|
|
}
|
|
|
|
int
|
|
main(void)
|
|
{
|
|
int fd = open(PATH, O_RDWR | O_CREAT | O_TRUNC, 0644);
|
|
CHECK(fd >= 0, "open: %s", strerror(errno));
|
|
write(fd, "0123456789012345678901234567890123456789", 40);
|
|
|
|
// flock: exclusive keeps everyone out, shared lets shared in.
|
|
CHECK(flock(fd, LOCK_EX) == 0, "flock LOCK_EX");
|
|
CHECK(child(try_flock_ex) == 1, "a second LOCK_EX is refused with EWOULDBLOCK");
|
|
CHECK(flock(fd, LOCK_SH) == 0, "convert to LOCK_SH");
|
|
CHECK(child(try_flock_sh) == 0, "LOCK_SH alongside LOCK_SH");
|
|
CHECK(child(try_flock_ex) == 1, "LOCK_EX against LOCK_SH");
|
|
CHECK(flock(fd, LOCK_UN) == 0 && child(try_flock_ex) == 0, "LOCK_UN releases");
|
|
|
|
// Closing the last descriptor on the description drops a flock.
|
|
int fd2 = open(PATH, O_RDWR);
|
|
CHECK(flock(fd2, LOCK_EX) == 0, "flock on a second description");
|
|
close(fd2);
|
|
CHECK(child(try_flock_ex) == 0, "close releases flock");
|
|
|
|
// flock and fcntl locks do not see each other.
|
|
CHECK(flock(fd, LOCK_EX) == 0 && child(try_range_10_19) == 0,
|
|
"fcntl ignores flock");
|
|
flock(fd, LOCK_UN);
|
|
|
|
// Ranges: a lock on 0..39 with 15..16 unlocked is two locks.
|
|
CHECK(set(fd, F_SETLK, F_WRLCK, 0, 40) == 0, "F_SETLK 0..39");
|
|
CHECK(child(try_range_50_59) == 0, "outside the range is free");
|
|
CHECK(child(try_range_10_19) == 1, "inside the range is not");
|
|
CHECK(set(fd, F_SETLK, F_UNLCK, 10, 10) == 0, "unlock 10..19");
|
|
CHECK(child(try_range_10_19) == 0, "the hole is free");
|
|
|
|
struct flock q = { .l_type = F_WRLCK, .l_whence = SEEK_SET, .l_start = 0, .l_len = 5 };
|
|
pid_t p = fork();
|
|
if (p == 0) {
|
|
int cfd = open(PATH, O_RDWR);
|
|
_exit(fcntl(cfd, F_GETLK, &q) == 0 && q.l_type == F_WRLCK &&
|
|
q.l_pid == getppid() && q.l_start == 0 && q.l_len == 10 ? 0 : 1);
|
|
}
|
|
int st;
|
|
waitpid(p, &st, 0);
|
|
CHECK(WIFEXITED(st) && WEXITSTATUS(st) == 0, "F_GETLK reports the holder");
|
|
|
|
// A process's record locks go when it closes *any* descriptor on the file.
|
|
int fd3 = open(PATH, O_RDONLY);
|
|
close(fd3);
|
|
CHECK(child(try_range_10_19) == 0 && child(try_range_50_59) == 0,
|
|
"closing another descriptor releases the process's locks");
|
|
|
|
// A blocked waiter is woken when the lock goes.
|
|
CHECK(set(fd, F_SETLK, F_WRLCK, 0, 40) == 0, "relock");
|
|
p = fork();
|
|
if (p == 0)
|
|
_exit(wait_range_10_19());
|
|
usleep(100000);
|
|
CHECK(waitpid(p, &st, WNOHANG) == 0, "F_SETLKW waits");
|
|
set(fd, F_SETLK, F_UNLCK, 0, 0);
|
|
waitpid(p, &st, 0);
|
|
CHECK(WIFEXITED(st) && WEXITSTATUS(st) == 0, "F_SETLKW is woken by the unlock");
|
|
|
|
// And they go when the process exits.
|
|
CHECK(child(lock_and_exit) == 0 && set(fd, F_SETLK, F_WRLCK, 0, 0) == 0,
|
|
"exit releases record locks");
|
|
|
|
close(fd);
|
|
unlink(PATH);
|
|
printf(bad ? "locktest: %d failed\n" : "locktest: ok\n", bad);
|
|
return bad != 0;
|
|
}
|