sd-future: make src/basic blocking helpers fiber-aware
Some helpers in src/basic — ppoll_usec_full() (used by fd_wait_for_event()), loop_read(), loop_read_exact(), loop_write_full() and pidref_wait_for_terminate_full() — block the calling thread. That's the right behaviour outside a fiber but not inside one, where blocking the thread also stalls every other fiber running on the same event loop. Rewriting every caller to pick a fiber or non-fiber variant explicitly would be a lot of churn and would split otherwise-shared code paths in two. Instead, the helpers detect at runtime whether they're running on a fiber and dispatch to a suspending variant when they are. FiberOps in fiber-ops.h holds five function pointers (ppoll, read, write, timeout, cancel_wait_unref); a fiber_ops global constant is populated whenever we enter a fiber with functions that delegate to suspending variants of common syscalls. With this approach, the variants themselves stay in libsystemd which is required because they make use of sd-event. - loop_read()/loop_read_exact() take the fiber read hook on a fiber unless the caller asked for a non-blocking attempt (do_poll=false) and the fd is already non-blocking — in that case we fall through to read() to preserve the existing return-EAGAIN-immediately semantic. The hook itself suspends on EAGAIN until data is available, so neither the do_poll knob nor the explicit fd_wait_for_event() retry loop are needed on the fiber path. - loop_write_full() likewise takes the fiber write hook on a fiber, except when timeout=0 with an already-non-blocking fd (preserving the fast-return-EAGAIN semantic). The fiber path runs inside a FIBER_OPS_TIMEOUT() scope so the caller's timeout is honoured via a deadline future, mirroring SD_FIBER_TIMEOUT() but reachable from src/basic without pulling in sd-future.h. - pidref_wait_for_terminate_full() polls the pidfd via fd_wait_for_event() before each waitid() when either a finite timeout is set or we're on a fiber, and requires pidref->fd >= 0 in those cases (returning -ENOMEDIUM otherwise — extending the rule that already applied to finite timeouts). The poll suspends the fiber via the ppoll hook above; the subsequent waitid() doesn't block because the pidfd is already signalled.
This commit is contained in:
@@ -112,6 +112,7 @@ typedef enum UnitType UnitType;
|
||||
typedef enum WaitFlags WaitFlags;
|
||||
|
||||
typedef struct Fiber Fiber;
|
||||
typedef struct FiberOps FiberOps;
|
||||
typedef struct Hashmap Hashmap;
|
||||
typedef struct HashmapBase HashmapBase;
|
||||
typedef struct IteratedCache IteratedCache;
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
#include <poll.h>
|
||||
#include <threads.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "errno-util.h"
|
||||
#include "fiber-ops.h"
|
||||
|
||||
static thread_local const FiberOps *fiber_ops = NULL;
|
||||
|
||||
bool fiber_ops_is_set(void) {
|
||||
return fiber_ops != NULL;
|
||||
}
|
||||
|
||||
void fiber_ops_set(const FiberOps *ops) {
|
||||
fiber_ops = ops;
|
||||
}
|
||||
|
||||
int fiber_ops_ppoll(struct pollfd *fds, size_t n_fds, const struct timespec *timeout, const sigset_t *sigmask) {
|
||||
if (fiber_ops)
|
||||
return fiber_ops->ppoll(fds, n_fds, timeout, sigmask);
|
||||
|
||||
return RET_NERRNO(ppoll(fds, n_fds, timeout, sigmask));
|
||||
}
|
||||
|
||||
ssize_t fiber_ops_read(int fd, void *buf, size_t count) {
|
||||
if (fiber_ops)
|
||||
return fiber_ops->read(fd, buf, count);
|
||||
|
||||
ssize_t n = read(fd, buf, count);
|
||||
return n < 0 ? -errno : n;
|
||||
}
|
||||
|
||||
ssize_t fiber_ops_write(int fd, const void *buf, size_t count) {
|
||||
if (fiber_ops)
|
||||
return fiber_ops->write(fd, buf, count);
|
||||
|
||||
return RET_NERRNO(write(fd, buf, count));
|
||||
}
|
||||
|
||||
sd_future* fiber_ops_timeout(uint64_t timeout) {
|
||||
assert(fiber_ops);
|
||||
|
||||
return fiber_ops->timeout(timeout);
|
||||
}
|
||||
|
||||
sd_future* fiber_ops_cancel_wait_unref(sd_future *f) {
|
||||
assert(fiber_ops);
|
||||
|
||||
return fiber_ops->cancel_wait_unref(f);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
#pragma once
|
||||
|
||||
#include "basic-forward.h"
|
||||
|
||||
typedef struct sd_future sd_future;
|
||||
|
||||
/* Hooks installed on a fiber so that functions in src/basic can transparently defer to the suspending
|
||||
* variants in sd-future when invoked from a running fiber. Populated by sd_fiber_new() with pointers to the
|
||||
* implementations in fiber-ops.c. */
|
||||
typedef struct FiberOps {
|
||||
int (*ppoll)(struct pollfd *fds, size_t n_fds, const struct timespec *timeout, const sigset_t *sigmask);
|
||||
ssize_t (*read)(int fd, void *buf, size_t count);
|
||||
ssize_t (*write)(int fd, const void *buf, size_t count);
|
||||
sd_future* (*timeout)(uint64_t timeout);
|
||||
sd_future* (*cancel_wait_unref)(sd_future *f);
|
||||
} FiberOps;
|
||||
|
||||
bool fiber_ops_is_set(void);
|
||||
void fiber_ops_set(const FiberOps *fiber_ops);
|
||||
|
||||
int fiber_ops_ppoll(struct pollfd *fds, size_t n_fds, const struct timespec *timeout, const sigset_t *sigmask);
|
||||
ssize_t fiber_ops_read(int fd, void *buf, size_t count);
|
||||
ssize_t fiber_ops_write(int fd, const void *buf, size_t count);
|
||||
|
||||
/* Mirror of SD_FIBER_TIMEOUT() for code under src/basic that doesn't include sd-future.h: dispatches
|
||||
* through FiberOps so the actual sd_fiber_timeout() implementation lives in libsystemd. */
|
||||
sd_future* fiber_ops_timeout(uint64_t timeout);
|
||||
sd_future* fiber_ops_cancel_wait_unref(sd_future *f);
|
||||
DEFINE_TRIVIAL_CLEANUP_FUNC(sd_future*, fiber_ops_cancel_wait_unref);
|
||||
|
||||
#define FIBER_OPS_TIMEOUT(timeout) _FIBER_OPS_TIMEOUT(UNIQ, (timeout))
|
||||
#define _FIBER_OPS_TIMEOUT(uniq, timeout) \
|
||||
_unused_ _cleanup_(fiber_ops_cancel_wait_unrefp) sd_future *UNIQ_T(_fot_, uniq) = fiber_ops_timeout(timeout)
|
||||
+67
-17
@@ -1,5 +1,6 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <poll.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
@@ -8,6 +9,7 @@
|
||||
#include <unistd.h>
|
||||
|
||||
#include "errno-util.h"
|
||||
#include "fiber-ops.h"
|
||||
#include "io-util.h"
|
||||
#include "time-util.h"
|
||||
|
||||
@@ -69,25 +71,44 @@ ssize_t loop_read(int fd, void *buf, size_t nbytes, bool do_poll) {
|
||||
if (nbytes > (size_t) SSIZE_MAX)
|
||||
return -EINVAL;
|
||||
|
||||
/* do_poll == false means "don't wait, return what we have if EAGAIN". If the fd is already
|
||||
* non-blocking, read() can't block the thread, so the non-fiber path satisfies that semantic
|
||||
* correctly even from a fiber. Only use the fiber path when the fd is blocking (where read()
|
||||
* would otherwise block the entire event loop). */
|
||||
int flags = 0;
|
||||
if (fiber_ops_is_set() && !do_poll) {
|
||||
flags = fcntl(fd, F_GETFL);
|
||||
if (flags < 0)
|
||||
return -errno;
|
||||
}
|
||||
|
||||
do {
|
||||
ssize_t k;
|
||||
|
||||
k = read(fd, p, nbytes);
|
||||
if (k < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
if (fiber_ops_is_set() && (do_poll || !FLAGS_SET(flags, O_NONBLOCK))) {
|
||||
/* On a fiber the read op suspends on EAGAIN until data is available, so we don't
|
||||
* need a separate poll step or the do_poll knob. */
|
||||
k = fiber_ops_read(fd, p, nbytes);
|
||||
if (k < 0)
|
||||
return n > 0 ? n : k;
|
||||
} else {
|
||||
k = read(fd, p, nbytes);
|
||||
if (k < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
|
||||
if (errno == EAGAIN && do_poll) {
|
||||
if (errno == EAGAIN && do_poll) {
|
||||
|
||||
/* We knowingly ignore any return value here,
|
||||
* and expect that any error/EOF is reported
|
||||
* via read() */
|
||||
/* We knowingly ignore any return value here,
|
||||
* and expect that any error/EOF is reported
|
||||
* via read() */
|
||||
|
||||
(void) fd_wait_for_event(fd, POLLIN, USEC_INFINITY);
|
||||
continue;
|
||||
(void) fd_wait_for_event(fd, POLLIN, USEC_INFINITY);
|
||||
continue;
|
||||
}
|
||||
|
||||
return n > 0 ? n : -errno;
|
||||
}
|
||||
|
||||
return n > 0 ? n : -errno;
|
||||
}
|
||||
|
||||
if (k == 0)
|
||||
@@ -137,6 +158,37 @@ int loop_write_full(int fd, const void *buf, size_t nbytes, usec_t timeout) {
|
||||
p = buf;
|
||||
}
|
||||
|
||||
/* timeout == 0 means "don't wait, return -EAGAIN if not ready". If the fd is already
|
||||
* non-blocking, write() can't block the thread, so the non-fiber path satisfies that
|
||||
* semantic correctly even from a fiber. Only use the fiber path when the fd is blocking
|
||||
* (where write() would otherwise block the entire event loop). */
|
||||
int flags = 0;
|
||||
if (fiber_ops_is_set() && timeout == 0) {
|
||||
flags = fcntl(fd, F_GETFL);
|
||||
if (flags < 0)
|
||||
return -errno;
|
||||
}
|
||||
|
||||
if (fiber_ops_is_set() && !FLAGS_SET(flags, O_NONBLOCK)) {
|
||||
/* On a fiber the write op suspends on EAGAIN until the fd is writable; honor the
|
||||
* caller's timeout via a deadline scope. */
|
||||
FIBER_OPS_TIMEOUT(timestamp_is_set(timeout) ? timeout : USEC_INFINITY);
|
||||
|
||||
while (nbytes > 0) {
|
||||
ssize_t k = fiber_ops_write(fd, p, nbytes);
|
||||
if (k < 0)
|
||||
return (int) k;
|
||||
if (_unlikely_(nbytes > 0 && k == 0)) /* Can't really happen */
|
||||
return -EIO;
|
||||
|
||||
assert((size_t) k <= nbytes);
|
||||
p += k;
|
||||
nbytes -= k;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* When timeout is 0 or USEC_INFINITY this is not used. But we initialize it to a sensible value. */
|
||||
end = timestamp_is_set(timeout) ? usec_add(now(CLOCK_MONOTONIC), timeout) : USEC_INFINITY;
|
||||
|
||||
@@ -220,11 +272,9 @@ int ppoll_usec_full(struct pollfd *fds, size_t n_fds, usec_t timeout, const sigs
|
||||
if (n_fds == 0 && timeout == 0)
|
||||
return 0;
|
||||
|
||||
r = ppoll(fds, n_fds, timeout == USEC_INFINITY ? NULL : TIMESPEC_STORE(timeout), ss);
|
||||
if (r < 0)
|
||||
return -errno;
|
||||
if (r == 0)
|
||||
return 0;
|
||||
r = fiber_ops_ppoll(fds, n_fds, timeout == USEC_INFINITY ? NULL : TIMESPEC_STORE(timeout), ss);
|
||||
if (r <= 0)
|
||||
return r;
|
||||
|
||||
for (size_t i = 0, n = r; i < n_fds && n > 0; i++) {
|
||||
if (fds[i].revents == 0)
|
||||
|
||||
@@ -36,6 +36,7 @@ basic_sources = files(
|
||||
'ether-addr-util.c',
|
||||
'extract-word.c',
|
||||
'fd-util.c',
|
||||
'fiber-ops.c',
|
||||
'fileio.c',
|
||||
'filesystems.c',
|
||||
'format-ifname.c',
|
||||
|
||||
+18
-5
@@ -7,6 +7,7 @@
|
||||
#include "alloc-util.h"
|
||||
#include "errno-util.h"
|
||||
#include "fd-util.h"
|
||||
#include "fiber-ops.h"
|
||||
#include "format-util.h"
|
||||
#include "hash-funcs.h"
|
||||
#include "io-util.h"
|
||||
@@ -466,16 +467,28 @@ int pidref_wait_for_terminate_full(PidRef *pidref, usec_t timeout, siginfo_t *re
|
||||
if (pidref->pid == 1 || pidref_is_self(pidref))
|
||||
return -ECHILD;
|
||||
|
||||
if (timeout != USEC_INFINITY && pidref->fd < 0)
|
||||
if (pidref->fd < 0 && (timeout != USEC_INFINITY || fiber_ops_is_set()))
|
||||
return -ENOMEDIUM;
|
||||
|
||||
usec_t ts = timeout == USEC_INFINITY ? USEC_INFINITY : usec_add(now(CLOCK_MONOTONIC), timeout);
|
||||
|
||||
/* Poll the pidfd before waitid() if either there's a finite timeout (so we can honor it) or
|
||||
* we're on a fiber (so fd_wait_for_event() can suspend us instead of blocking the event loop
|
||||
* inside waitid()). Otherwise let waitid() block directly. The precondition above guarantees
|
||||
* pidref->fd >= 0 in both cases. */
|
||||
bool poll_first = ts != USEC_INFINITY || fiber_ops_is_set();
|
||||
|
||||
for (;;) {
|
||||
if (ts != USEC_INFINITY) {
|
||||
usec_t left = usec_sub_unsigned(ts, now(CLOCK_MONOTONIC));
|
||||
if (left == 0)
|
||||
return -ETIMEDOUT;
|
||||
if (poll_first) {
|
||||
usec_t left;
|
||||
|
||||
if (ts == USEC_INFINITY)
|
||||
left = USEC_INFINITY;
|
||||
else {
|
||||
left = usec_sub_unsigned(ts, now(CLOCK_MONOTONIC));
|
||||
if (left == 0)
|
||||
return -ETIMEDOUT;
|
||||
}
|
||||
|
||||
r = fd_wait_for_event(pidref->fd, POLLIN, left);
|
||||
if (r == 0)
|
||||
|
||||
Reference in New Issue
Block a user