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:
Daan De Meyer
2026-05-21 09:55:04 +00:00
parent cf4c65afa8
commit 7bc793e21f
9 changed files with 763 additions and 23 deletions
+1
View File
@@ -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;
+51
View File
@@ -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);
}
+34
View File
@@ -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
View File
@@ -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)
+1
View File
@@ -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
View File
@@ -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)