mirror of
https://github.com/systemd/systemd.git
synced 2026-11-06 21:36:59 +00:00
Traditionally, asynchronous programming in systemd has been achieved using sd-event along with the asynchronous interfaces of sd-bus and sd-varlink. This works well when the system is reacting to events and all code triggered by those events can run without blocking. In these scenarios, the global Manager object is passed as userdata to the callback, and the callback can use the stack as usual, declaring local state and ensuring proper cleanup via _cleanup_. Control flow structures, such as loops, work as expected, and everything runs smoothly. However, challenges arise when the code needs to perform long-running operations within these callbacks. Since the system cannot block execution within the callback, we can't directly invoke a long-running operation and wait for its result without introducing complexities. Instead, we need to initiate the long-running task, register for completion with sd-event, sd-bus, or sd-varlink, and provide a callback to be invoked when the operation completes. This callback, however, only receives a single userdata pointer, which forces us to bundle all local variables into a struct and pass it along as part of the callback. On top of that, after queuing the asynchronous operation, the caller continues executing. As the caller's stack unwinds when the function exits, the resources and state within the local scope may be prematurely cleaned up. Therefore, the struct must store copies of the local variables or ensure proper reference counting to prevent premature resource cleanup. When multiple long-running operations need to be initiated within a loop, the complexity grows further. We must introduce additional shared state to track the completion of all operations before we can run any code that depends on their results. Furthermore, since the daemon may be shut down at any time, we must track the lifecycle of each long-running operation in the global Manager struct, ensuring proper cleanup even when stack unwinding can no longer manage the resources for us. Fibers, or green threads, provide a more natural way of handling asynchronous operations. By enabling cooperative multitasking within a single thread, fibers allow us to write code that looks like it’s running synchronously, but with the ability to yield control at predefined points, such as when waiting for long-running tasks to complete. With fibers, we can simplify the control flow by running asynchronous operations within a fiber, allowing us to "pause" execution while waiting for the long-running operation to finish and then "resume" the operation once it's complete. This eliminates the need for multiple callback chains, extensive state tracking, and the potential pitfalls of stack unwinding. This commit introduces the ability to execute long-running operations in a non-blocking manner while maintaining the simplicity and readability of synchronous code. The fiber-based approach will significantly improve the handling of complex workflows, making the code easier to write and maintain. The implementation is based on ucontext.h's makecontext() (with a fallback to the venerable sigaltstack() approach on musl), sigsetjmp()/siglongjmp() and sd-event. ucontext.h provides us with alternate stacks that we can switch between. We use sigsetjmp()/siglongjmp() instead of swapcontext() because the latter forcibly saves/restores a per context signal mask every time it is called. Using sigsetjmp()/siglongjmp(), we can avoid the unnecessary syscall and maintain a per thread signal mask, which makes much more sense than having a per fiber signal mask. The default stack size is the same as a regular thread. Because we use mmap() to allocate the stack, the memory won't actually be used until it is paged in by the kernel, so we don't actually use 8MB per fiber. To integrate fibers with the event loop, each fiber is assigned a deferred event source which resumes the fiber when enabled. The deferred event source is oneshot by default so the fiber will run immediately until it yields or suspends. If it yields, the deferred event source is enabled again (oneshot) immediately. If it suspends, before it suspends, one or more event sources are registered with sd-event that will enable the deferred event source (oneshot) to resume the fiber once the operation it is waiting for completes. Yielding or suspending the fiber is done by calling sd_fiber_yield() or sd_fiber_suspend() respectively. Both of these return zero on success or any error value from the async operation that caused the fiber to resume. This is also how fiber cancellation is implemented. When a fiber is cancelled, sd_fiber_yield() and sd_fiber_suspend() will return ECANCELED when the fiber is resumed, allowing the fiber to unwind its stack (which allows cleanup to happen automatically) and finish. Instead of having applications work directly with fibers, we hide them behind a generic futures interface to represent long-running operations, regardless of whether those operations are running on a fiber or not. Aside from fibers, the futures library (sd-future) will for example allow waiting for sd-event sources and doing sd-bus calls in the background as well. Fibers can suspend until a future is ready with sd_fiber_await() or by having the future wake up the fiber explicitly in its callback. A future always defaults to waking up the current fiber. Each future kind plugs into the library by providing an sd_future_ops vtable (alloc, free, cancel, set_priority). The library treats the impl pointer returned by alloc() as a black box. Future Implementations retrieve it via sd_future_get_private(). A future starts in SD_FUTURE_PENDING and transitions exactly once to SD_FUTURE_RESOLVED, carrying an integer result. Consumers can react to that transition either by installing a one-shot callback with sd_future_set_callback() (callback-style code) or by waiting on it from a fiber via sd_fiber_await() (synchronous-looking fiber code). sd_fiber_await() is itself built on a "wait future" that resolves when its target resolves; sd_future_new_wait() exposes the same primitive directly so non-fiber callers can chain futures without involving a fiber. Cancellation is cooperative: sd_future_cancel() invokes the future impl's cancel callback, which is responsible for tearing down its work and ultimately resolving the promise with -ECANCELED. For fiber futures this is what surfaces as the ECANCELED return from sd_fiber_yield()/sd_fiber_suspend() mentioned above. Fire-and-forget fibers — created by passing a NULL ret to sd_fiber_new() — take a self-reference on their future so they outlive the caller's scope. The self-ref is dropped when the fiber resolves. This floating mechanism (sd_fiber_set_floating()) is restricted to fiber futures because they uniquely guarantee resolution; allowing it for arbitrary future kinds would risk silent leaks for kinds that may never resolve. Note that fiber cleanup depends on the runtime operating normally. Each fiber's _cleanup_-style cleanups live on the fiber's own stack and run only when the fiber is resumed and allowed to unwind, which requires a working event loop to drive it to completion. The exit event source registered for top-level fibers ensures unwind on a normal sd_event_exit(), but if the event loop itself terminates abnormally (e.g. an unrecoverable allocation failure mid-dispatch) before all fibers have resolved, their stacks never unwind and any resources they own leak. The code lives in libsystemd as sd-future (not exported) for the following reasons: - We may want to make this a public libsystemd API in the future - The code can't live in src/basic as it makes heavy use of sd-event - The code can't live in src/shared as sd-bus and sd-event make use of it The log and log-context headers are updated with functions to allow fibers to have their own log prefix and log context.
165 lines
5.0 KiB
C
165 lines
5.0 KiB
C
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
|
#pragma once
|
|
|
|
/* IWYU pragma: always_keep */
|
|
|
|
#include <errno.h> /* IWYU pragma: export */
|
|
#include <inttypes.h> /* IWYU pragma: export */
|
|
#include <limits.h> /* IWYU pragma: export */
|
|
#include <paths.h> /* IWYU pragma: export */
|
|
#include <stdarg.h> /* IWYU pragma: export */
|
|
#include <stdbool.h> /* IWYU pragma: export */
|
|
#include <stddef.h> /* IWYU pragma: export */
|
|
#include <stdint.h> /* IWYU pragma: export */
|
|
#include <sys/types.h> /* IWYU pragma: export */
|
|
#include <uchar.h> /* IWYU pragma: export */
|
|
|
|
#include "assert-util.h" /* IWYU pragma: export */
|
|
#include "cleanup-util.h" /* IWYU pragma: export */
|
|
#include "macro.h" /* IWYU pragma: export */
|
|
|
|
#include "../fundamental/string-table.h" /* IWYU pragma: export */
|
|
|
|
/* Generic types */
|
|
|
|
typedef uint64_t usec_t;
|
|
typedef uint64_t nsec_t;
|
|
|
|
/* Libc/Linux forward declarations */
|
|
|
|
struct dirent;
|
|
struct ether_addr;
|
|
struct fiemap;
|
|
struct file_handle;
|
|
struct glob_t;
|
|
struct group;
|
|
struct icmp6_hdr;
|
|
struct in_addr;
|
|
struct in6_addr;
|
|
struct inotify_event;
|
|
struct iovec;
|
|
struct mount_attr;
|
|
struct msghdr;
|
|
struct passwd;
|
|
struct pollfd;
|
|
struct rlimit;
|
|
struct sgrp;
|
|
struct shadow;
|
|
struct signalfd_siginfo;
|
|
struct siphash;
|
|
struct sockaddr;
|
|
struct spwd;
|
|
struct stat;
|
|
struct statfs;
|
|
struct statx_timestamp;
|
|
struct statx;
|
|
struct termios;
|
|
struct tm;
|
|
struct ucred;
|
|
|
|
/* To forward declare FILE and DIR, we have to declare the internal struct names for them. Since these are
|
|
* used for C++ symbol name mangling, they're effectively part of the ABI and won't actually change. */
|
|
typedef struct _IO_FILE FILE;
|
|
typedef struct __dirstream DIR;
|
|
|
|
/* 3rd-party library forward declarations */
|
|
|
|
enum bpf_map_type;
|
|
|
|
struct fdisk_context;
|
|
struct fdisk_table;
|
|
struct crypt_device;
|
|
|
|
typedef struct buf_mem_st BUF_MEM;
|
|
typedef struct evp_pkey_st EVP_PKEY;
|
|
typedef struct evp_md_st EVP_MD;
|
|
typedef struct evp_md_ctx_st EVP_MD_CTX;
|
|
typedef struct ssl_st SSL;
|
|
typedef struct ssl_ctx_st SSL_CTX;
|
|
typedef struct ssl_session_st SSL_SESSION;
|
|
typedef struct x509_st X509;
|
|
|
|
/* basic/ forward declarations */
|
|
|
|
typedef void (*hash_func_t)(const void *p, struct siphash *state);
|
|
typedef int (*compare_func_t)(const void *a, const void *b);
|
|
typedef compare_func_t comparison_fn_t;
|
|
typedef int (*comparison_userdata_fn_t)(const void *, const void *, void *);
|
|
|
|
struct hash_ops;
|
|
struct hw_addr_data;
|
|
struct in_addr_data;
|
|
struct iovec_wrapper;
|
|
union in_addr_union;
|
|
union sockaddr_union;
|
|
|
|
typedef enum Architecture Architecture;
|
|
typedef enum CGroupFlags CGroupFlags;
|
|
typedef enum CGroupMask CGroupMask;
|
|
typedef enum ChaseFlags ChaseFlags;
|
|
typedef enum ConfFilesFlags ConfFilesFlags;
|
|
typedef enum ExtractFlags ExtractFlags;
|
|
typedef enum ForkFlags ForkFlags;
|
|
typedef enum Glyph Glyph;
|
|
typedef enum ImageClass ImageClass;
|
|
typedef enum JobMode JobMode;
|
|
typedef enum RuntimeScope RuntimeScope;
|
|
typedef enum StringSafeFlags StringSafeFlags;
|
|
typedef enum TimestampStyle TimestampStyle;
|
|
typedef enum UnitActiveState UnitActiveState;
|
|
typedef enum UnitDependency UnitDependency;
|
|
typedef enum UnitNameMangle UnitNameMangle;
|
|
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;
|
|
typedef struct Iterator Iterator;
|
|
typedef struct LogContext LogContext;
|
|
typedef struct OrderedHashmap OrderedHashmap;
|
|
typedef struct OrderedSet OrderedSet;
|
|
typedef struct Set Set;
|
|
|
|
typedef struct dual_timestamp dual_timestamp;
|
|
typedef struct triple_timestamp triple_timestamp;
|
|
typedef struct Compressor Compressor;
|
|
typedef struct ConfFile ConfFile;
|
|
typedef struct LockFile LockFile;
|
|
typedef struct PidRef PidRef;
|
|
typedef struct Prioq Prioq;
|
|
typedef struct RateLimit RateLimit;
|
|
typedef struct SocketAddress SocketAddress;
|
|
|
|
/* Constants */
|
|
|
|
/* We duplicate various commonly used constants here so we can keep most static inline functions without
|
|
* having to include the full header that provides these constants. */
|
|
|
|
/* glibc defines AT_FDCWD as -100, but musl defines it as (-100). */
|
|
#ifdef __GLIBC__
|
|
#define AT_FDCWD -100
|
|
#else
|
|
#define AT_FDCWD (-100)
|
|
#endif
|
|
#define AT_EMPTY_PATH 0x1000
|
|
#define AT_SYMLINK_FOLLOW 0x400
|
|
#define AT_SYMLINK_NOFOLLOW 0x100
|
|
|
|
#define MODE_INVALID ((mode_t) -1)
|
|
|
|
#define UID_INVALID ((uid_t) -1)
|
|
#define GID_INVALID ((gid_t) -1)
|
|
|
|
#define USEC_INFINITY ((usec_t) UINT64_MAX)
|
|
#define NSEC_INFINITY ((nsec_t) UINT64_MAX)
|
|
|
|
/* MAX_ERRNO is defined as 4095 in linux/err.h. We use the same value here. */
|
|
#define ERRNO_MAX 4095
|
|
|
|
/* This is MAX_ADDR_LEN as defined in linux/netdevice.h, but net/if_arp.h
|
|
* defines a macro of the same name with a much lower size. */
|
|
#define HW_ADDR_MAX_SIZE 32
|