lock-util: add a new lock_generic_with_timeout() helper

This is just like lock_generic(), but applies the lock with a timeout.
This requires jumping through some hoops by executing things in a child
process, so that we can abort if necessary via a timer. Linux after all
has no native way to take file locks with a timeout.
This commit is contained in:
Lennart Poettering
2023-11-02 14:19:32 +01:00
parent e9ccae3135
commit e5f1e8b894
4 changed files with 119 additions and 0 deletions
+89
View File
@@ -2,10 +2,12 @@
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/file.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include "alloc-util.h"
#include "fd-util.h"
@@ -14,6 +16,7 @@
#include "macro.h"
#include "missing_fcntl.h"
#include "path-util.h"
#include "process-util.h"
int make_lock_file_at(int dir_fd, const char *p, int operation, LockFile *ret) {
_cleanup_close_ int fd = -EBADF, dfd = -EBADF;
@@ -186,3 +189,89 @@ int lock_generic(int fd, LockType type, int operation) {
assert_not_reached();
}
}
int lock_generic_with_timeout(int fd, LockType type, int operation, usec_t timeout) {
_cleanup_(sigkill_waitp) pid_t pid = 0;
int r;
assert(fd >= 0);
/* A version of lock_generic(), but with a time-out. We do this in a child process, since the kernel
* APIs natively don't support a timeout. We set a SIGALRM timer that will kill the child after the
* timeout is hit. Returns -ETIMEDOUT if the time-out is hit, and 0 on success.
*
* This only works for BSD and UNPOSIX locks, as only those are fd-bound, and hence can be acquired
* from any process that has access to the fd. POSIX locks OTOH are process-bound, and hence if we'd
* acquire them in a child process they'd remain unlocked in the parent. */
if (type == LOCK_NONE)
return 0;
if (!IN_SET(type, LOCK_BSD, LOCK_UNPOSIX)) /* Not for POSIX locks, see above. */
return -EOPNOTSUPP;
/* First, try without forking anything off */
r = lock_generic(fd, type, operation | (timeout == USEC_INFINITY ? 0 : LOCK_NB));
if (r != -EAGAIN || timeout == 0 || FLAGS_SET(operation, LOCK_NB))
return r;
/* If that didn't work, try with a child */
r = safe_fork("(sd-flock)", FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGKILL, &pid);
if (r < 0)
return log_error_errno(r, "Failed to flock block device in child process: %m");
if (r == 0) {
struct sigevent sev = {
.sigev_notify = SIGEV_SIGNAL,
.sigev_signo = SIGALRM,
};
timer_t id = 0;
if (timer_create(CLOCK_MONOTONIC, &sev, &id) < 0) {
log_error_errno(errno, "Failed to allocate CLOCK_MONOTONIC timer: %m");
_exit(EXIT_FAILURE);
}
struct itimerspec its = {};
timespec_store(&its.it_value, timeout);
if (timer_settime(id, /* flags= */ 0, &its, NULL) < 0) {
log_error_errno(errno, "Failed to start CLOCK_MONOTONIC timer: %m");
_exit(EXIT_FAILURE);
}
if (lock_generic(fd, type, operation) < 0) {
log_error_errno(errno, "Unable to get an exclusive lock on the device: %m");
_exit(EXIT_FAILURE);
}
_exit(EXIT_SUCCESS);
}
siginfo_t status;
r = wait_for_terminate(pid, &status);
if (r < 0)
return r;
TAKE_PID(pid);
switch (status.si_code) {
case CLD_EXITED:
if (status.si_status != EXIT_SUCCESS)
return -EPROTO;
return 0;
case CLD_KILLED:
if (status.si_status == SIGALRM)
return -ETIMEDOUT;
_fallthrough_;
case CLD_DUMPED:
return -EPROTO;
default:
assert_not_reached();
}
}
+2
View File
@@ -41,3 +41,5 @@ typedef enum LockType {
} LockType;
int lock_generic(int fd, LockType type, int operation);
int lock_generic_with_timeout(int fd, LockType type, int operation, usec_t timeout);
+1
View File
@@ -273,6 +273,7 @@ libbasic = static_library(
include_directories : basic_includes,
dependencies : [libcap,
libm,
librt,
threads,
userspace],
c_args : ['-fvisibility=default'],