Add a new daemonize API

Existing example/ file systems do the fuse_daemonize() after
fuse_session_mount() - i.e. after the mount point is already
established. Though, these example/ daemons do not start
extra threads and do not need network initialization either.

fuse_daemonize() also does not allow to return notification
from the forked child to the parent.

Complex fuse file system daemons often want the order of
1) fork - parent watches, child does the work

Child:
    2) start extra threads and system initialization (like network
       connection and RDMA memory registration) from the fork child.
    3) Start the fuse session after everything else succeeded

Parent:
    Report child initialization success or failure

A new API is introduced to overcome the limitations of
fuse_daemonize()

fuse_daemonize_start() - fork, but foreground process does not
terminate yet and watches the background.

fuse_daemonize_success() / fuse_daemonize_fail() - background
daemon signals to the foreground process success or failure.

fuse_daemonize_active() - helper function for the high level
interface, which needs to handle both APIs. fuse_daemonize()
is called within fuse_main(), which now needs to know if the caller
actually already used the new API itself.

The object 'struct fuse_daemonize *' is allocated dynamically
and stored a global variable in fuse_daemonize.c, because
- high level fuse_main_real_versioned() needs to know
if already daemonized
- high level daemons do not have access to struct fuse_session
- FUSE_SYNC_INIT in later commits can only be done if the new
API (or a file system internal) daemonization is used.

Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
This commit is contained in:
Bernd Schubert
2026-03-12 18:21:48 +01:00
parent 0dc666b11f
commit 33ea9ec898
11 changed files with 462 additions and 18 deletions
+12 -3
View File
@@ -55,6 +55,7 @@
#include <errno.h>
#include <ftw.h>
#include <fuse_lowlevel.h>
#include <fuse_daemonize.h>
#include <inttypes.h>
#include <string.h>
#include <sys/file.h>
@@ -1580,6 +1581,7 @@ int main(int argc, char *argv[])
{
struct fuse_loop_config *loop_config = NULL;
void *teardown_watchog = NULL;
unsigned int daemon_flags = 0;
// Parse command line options
auto options{ parse_options(argc, argv) };
@@ -1638,10 +1640,14 @@ int main(int argc, char *argv[])
fuse_loop_cfg_set_clone_fd(loop_config, fs.clone_fd);
if (fuse_session_mount(se, argv[2]) != 0)
/* Start daemonization before mount so parent can report mount failure */
if (fs.foreground)
daemon_flags |= FUSE_DAEMONIZE_NO_BACKGROUND;
if (fuse_daemonize_early_start(daemon_flags) != 0)
goto err_out3;
fuse_daemonize(fs.foreground);
if (fuse_session_mount(se, argv[2]) != 0)
goto err_out4;
if (!fs.foreground)
fuse_log_enable_syslog("passthrough-hp", LOG_PID | LOG_CONS,
@@ -1650,7 +1656,7 @@ int main(int argc, char *argv[])
teardown_watchog = fuse_session_start_teardown_watchdog(
se, fs.root.stop_timeout_secs, NULL, NULL);
if (teardown_watchog == NULL)
goto err_out3;
goto err_out4;
if (options.count("single"))
ret = fuse_session_loop(se);
@@ -1659,6 +1665,9 @@ int main(int argc, char *argv[])
fuse_session_unmount(se);
err_out4:
if (fuse_daemonize_early_is_active())
fuse_daemonize_early_fail(ret);
err_out3:
fuse_remove_signal_handlers(se);
err_out2:
+67
View File
@@ -0,0 +1,67 @@
/*
* FUSE: Filesystem in Userspace
* Copyright (C) 2026 Bernd Schubert <bsbernd.com>
*
* This program can be distributed under the terms of the GNU LGPLv2.
* See the file COPYING.LIB.
*
*/
#ifndef FUSE_DAEMONIZE_H_
#define FUSE_DAEMONIZE_H_
#include <stdint.h>
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* Flags for fuse_daemonize_start()
*/
#define FUSE_DAEMONIZE_NO_CHDIR (1 << 0)
#define FUSE_DAEMONIZE_NO_BACKGROUND (1 << 1)
/**
* Start daemonization process.
*
* Unless FUSE_DAEMONIZE_NO_BACKGROUND is set, this forks the process.
* The parent waits for a signal from the child via fuse_daemonize_success()
* or fuse_daemonize_fail().
* The child returns from this call and continues setup.
*
* Unless FUSE_DAEMONIZE_NO_CHDIR is set, changes directory to "/".
*
* Must be called before fuse_session_mount().
*
* @param flags combination of FUSE_DAEMONIZE_* flags
* @return 0 on success, negative errno on error
*/
int fuse_daemonize_early_start(unsigned int flags);
/**
* Signal daemonization failure to parent and cleanup.
*
* To be called from the child process on any kind of error.
*
* @param err Error code passed to the parent and used as process exit code.
*/
void fuse_daemonize_early_fail(int err);
/**
* Check if daemonization is active and waiting for signal.
*
* Can be called from the child process to check state of daemonization.
*
* @return true if active, false otherwise
*/
bool fuse_daemonize_early_is_active(void);
#ifdef __cplusplus
}
#endif
#endif /* FUSE_DAEMONIZE_H_ */
+2 -1
View File
@@ -1,4 +1,5 @@
libfuse_headers = [ 'fuse.h', 'fuse_common.h', 'fuse_lowlevel.h',
'fuse_opt.h', 'cuse_lowlevel.h', 'fuse_log.h' ]
'fuse_opt.h', 'cuse_lowlevel.h', 'fuse_log.h',
'fuse_daemonize.h' ]
install_headers(libfuse_headers, subdir: 'fuse3')
+304
View File
@@ -0,0 +1,304 @@
/*
* FUSE: Filesystem in Userspace
* Copyright (C) 2026 Bernd Schubert <bsbernd.com>
*
* This program can be distributed under the terms of the GNU LGPLv2.
* See the file COPYING.LIB.
*/
#define _GNU_SOURCE
#include "fuse_daemonize.h"
#include "fuse_daemonize_i.h"
#include <fcntl.h>
#include <poll.h>
#include <pthread.h>
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdbool.h>
#include <errno.h>
#include <err.h>
/**
* Status values for fuse_daemonize_success() and fuse_daemonize_fail()
*/
#define FUSE_DAEMONIZE_SUCCESS 0
#define FUSE_DAEMONIZE_FAILURE 1
/* Private/internal data */
struct fuse_daemonize {
unsigned int flags;
int signal_pipe_wr; /* write end for signaling parent */
int death_pipe_rd; /* read end, POLLHUP when parent dies */
int stop_pipe_rd; /* read end for stop signal */
int stop_pipe_wr; /* write end for stop signal */
pthread_t watcher;
bool watcher_started;
_Atomic bool active;
_Atomic bool daemonized;
_Atomic bool mounted;
};
/* Global daemonization object pointer */
static struct fuse_daemonize daemonize = {
.signal_pipe_wr = -1,
.death_pipe_rd = -1,
.stop_pipe_rd = -1,
.stop_pipe_wr = -1,
};
/* Watcher thread: polls for parent death or stop signal */
static void *parent_watcher_thread(void *arg)
{
struct fuse_daemonize *di = arg;
struct pollfd pfd[2];
pfd[0].fd = di->death_pipe_rd;
pfd[0].events = POLLIN;
pfd[1].fd = di->stop_pipe_rd;
pfd[1].events = POLLIN;
while (1) {
int rc = poll(pfd, 2, -1);
if (rc < 0)
continue;
/* Parent died - death pipe write end closed */
if (pfd[0].revents & (POLLHUP | POLLERR))
_exit(EXIT_FAILURE);
/* Stop signal received */
if (pfd[1].revents & POLLIN)
break;
}
return NULL;
}
static int start_parent_watcher(struct fuse_daemonize *dm)
{
int rc;
rc = pthread_create(&dm->watcher, NULL, parent_watcher_thread,
dm);
if (rc != 0) {
fprintf(stderr, "fuse_daemonize: pthread_create: %s\n",
strerror(rc));
return -rc;
}
dm->watcher_started = true;
return 0;
}
static void stop_parent_watcher(struct fuse_daemonize *dm)
{
char byte = 0;
if (dm && dm->watcher_started) {
/* Signal watcher to stop */
if (write(dm->stop_pipe_wr, &byte, 1) != 1)
perror("fuse_daemonize: stop write");
pthread_join(dm->watcher, NULL);
dm->watcher_started = false;
}
}
static int daemonize_child(struct fuse_daemonize *dm)
{
int stop_pipe[2], err = 0;
if (pipe(stop_pipe) == -1) {
err = -errno;
perror("fuse_daemonize_start: stop pipe");
return err;
}
dm->stop_pipe_rd = stop_pipe[0];
dm->stop_pipe_wr = stop_pipe[1];
if (setsid() == -1) {
err = -errno;
perror("fuse_daemonize_start: setsid");
goto err_close_stop;
}
/* Close stdin immediately */
int nullfd = open("/dev/null", O_RDWR, 0);
if (nullfd != -1) {
(void)dup2(nullfd, 0);
if (nullfd > 0)
close(nullfd);
}
/* Start watcher thread to detect parent death */
err = start_parent_watcher(dm);
if (err)
goto err_close_stop;
dm->daemonized = true;
return 0;
err_close_stop:
close(dm->stop_pipe_rd);
close(dm->stop_pipe_wr);
return err;
}
/* Fork and daemonize. Returns 0 in child, never returns in parent. */
static int do_daemonize(struct fuse_daemonize *dm)
{
int signal_pipe[2], death_pipe[2], err;
if (pipe(signal_pipe) == -1) {
err = -errno;
perror("fuse_daemonize_start: signal pipe");
return err;
}
if (pipe(death_pipe) == -1) {
err = -errno;
perror("fuse_daemonize_start: death pipe");
close(signal_pipe[0]);
close(signal_pipe[1]);
return err;
}
switch (fork()) {
case -1:
err = -errno;
perror("fuse_daemonize_start: fork");
close(signal_pipe[0]);
close(signal_pipe[1]);
close(death_pipe[0]);
close(death_pipe[1]);
return err;
case 0:
/* Child: signal write end, death read end */
close(signal_pipe[0]);
close(death_pipe[1]);
dm->signal_pipe_wr = signal_pipe[1];
dm->death_pipe_rd = death_pipe[0];
return daemonize_child(dm);
default: {
/* Parent: signal read end, death write end (kept open) */
int status;
ssize_t res;
close(signal_pipe[1]);
close(death_pipe[0]);
res = read(signal_pipe[0], &status, sizeof(status));
close(signal_pipe[0]);
close(death_pipe[1]);
if (res != sizeof(status))
_exit(EXIT_FAILURE);
_exit(status);
}
}
}
int fuse_daemonize_early_start(unsigned int flags)
{
struct fuse_daemonize *dm = &daemonize;
int err = 0;
dm->flags = flags;
dm->signal_pipe_wr = -1;
dm->death_pipe_rd = -1;
dm->stop_pipe_rd = -1;
dm->stop_pipe_wr = -1;
dm->active = true;
if (!(flags & FUSE_DAEMONIZE_NO_CHDIR))
(void)chdir("/");
if (!(flags & FUSE_DAEMONIZE_NO_BACKGROUND))
err = do_daemonize(dm);
return err;
}
static void close_if_valid(int *fd)
{
if (*fd != -1) {
close(*fd);
*fd = -1;
}
}
static void fuse_daemonize_early_signal(int status)
{
struct fuse_daemonize *dm = &daemonize;
int rc;
if (!dm->active)
errx(EINVAL, "%s: not active and cannot signal status", __func__);
/* Warn because there might be races */
if (status == FUSE_DAEMONIZE_SUCCESS && !dm->mounted)
fprintf(stderr, "fuse daemonize success without being mounted\n");
dm->active = false;
/* Stop watcher before signaling - parent will exit after this */
stop_parent_watcher(dm);
/* Signal status to parent */
if (dm->signal_pipe_wr != -1) {
rc = write(dm->signal_pipe_wr, &status, sizeof(status));
if (rc != sizeof(status))
fprintf(stderr, "%s: write failed\n", __func__);
}
/* Redirect stdout/stderr to /dev/null on success */
if (status == FUSE_DAEMONIZE_SUCCESS && dm->daemonized) {
int nullfd = open("/dev/null", O_RDWR, 0);
if (nullfd != -1) {
(void)dup2(nullfd, 1);
(void)dup2(nullfd, 2);
if (nullfd > 2)
close(nullfd);
}
}
close_if_valid(&dm->signal_pipe_wr);
close_if_valid(&dm->death_pipe_rd);
close_if_valid(&dm->stop_pipe_rd);
close_if_valid(&dm->stop_pipe_wr);
}
void fuse_daemonize_early_success(void)
{
/*
* Needs to be gracefully handled as automatically called libfuse
* internal from FUSE_INIT handler
*/
if (!fuse_daemonize_early_is_active())
return;
fuse_daemonize_early_signal(FUSE_DAEMONIZE_SUCCESS);
}
void fuse_daemonize_early_fail(int err)
{
fuse_daemonize_early_signal(err);
}
bool fuse_daemonize_early_is_active(void)
{
return daemonize.daemonized || daemonize.active;
}
void fuse_daemonize_early_set_mounted(void)
{
daemonize.mounted = true;
}
+35
View File
@@ -0,0 +1,35 @@
/*
* FUSE: Filesystem in Userspace
* Copyright (C) 2026 Bernd Schubert <bsbernd.com>
*
* This program can be distributed under the terms of the GNU LGPLv2.
* See the file COPYING.LIB.
*
*/
#ifndef FUSE_DAEMONIZE_I_H_
#define FUSE_DAEMONIZE_I_H_
#include <stdint.h>
#include <stdbool.h>
/**
* Set mounted flag.
*
* Called from fuse_session_mount().
*/
void fuse_daemonize_early_set_mounted(void);
/**
* Signal daemonization success to parent and cleanup.
*
* To be called from the child process after successful mount, when
* sychronous FUSE_INIT is used (FUSE_INIT as part of the mount)
* Automatically called for async FUSE_INIT.
*
* Not exposed to the ABI yet, as sync FUSE_INIT is not implemented yet.
*/
void fuse_daemonize_early_success(void);
#endif /* FUSE_DAEMONIZE_I_H_ */
+3 -1
View File
@@ -17,7 +17,6 @@
#include <semaphore.h>
#include <stdint.h>
#include <stdbool.h>
#include <errno.h>
#include <stdatomic.h>
#define MIN(a, b) \
@@ -110,6 +109,9 @@ struct fuse_session {
/* true if reading requests from /dev/fuse are handled internally */
bool buf_reallocable;
/* synchronous FUSE_INIT support */
bool want_sync_init;
/* io_uring */
struct fuse_session_uring uring;
+5
View File
@@ -19,6 +19,7 @@
#include "mount_util.h"
#include "util.h"
#include "fuse_uring_i.h"
#include "fuse_daemonize_i.h"
#include <pthread.h>
#include <stdatomic.h>
@@ -3022,6 +3023,8 @@ _do_init(fuse_req_t req, const fuse_ino_t nodeid, const void *op_in,
send_reply_ok(req, &outarg, outargsize);
if (enable_io_uring)
fuse_uring_wake_ring_threads(se);
fuse_daemonize_early_success();
}
static __attribute__((no_sanitize("thread"))) void
@@ -4454,6 +4457,8 @@ out:
se->fd = fd;
se->mountpoint = mountpoint;
fuse_daemonize_early_set_mounted();
return 0;
error_out:
+5
View File
@@ -227,6 +227,11 @@ FUSE_3.19 {
fuse_session_start_teardown_watchdog;
fuse_session_stop_teardown_watchdog;
fuse_lowlevel_notify_prune;
fuse_daemonize_early_start;
# Not exposed yet for now, as called automatically
# fuse_daemonize_early_success;
fuse_daemonize_early_fail;
fuse_daemonize_early_is_active;
} FUSE_3.18;
# Local Variables:
+26 -12
View File
@@ -15,6 +15,7 @@
#include "fuse_misc.h"
#include "fuse_opt.h"
#include "fuse_lowlevel.h"
#include "fuse_daemonize.h"
#include "mount_util.h"
#include <stdio.h>
@@ -252,6 +253,12 @@ int fuse_parse_cmdline_30(struct fuse_args *args,
int fuse_daemonize(int foreground)
{
/* Check if the NEW API is used */
if (fuse_daemonize_early_is_active()) {
fuse_log(FUSE_LOG_ERR, "fuse_daemonize_start() already used\n");
return -1;
}
if (!foreground) {
int nullfd;
int waiter[2];
@@ -266,7 +273,7 @@ int fuse_daemonize(int foreground)
* demonize current process by forking it and killing the
* parent. This makes current process as a child of 'init'.
*/
switch(fork()) {
switch (fork()) {
case -1:
perror("fuse_daemonize: fork");
close(waiter[0]);
@@ -279,7 +286,8 @@ int fuse_daemonize(int foreground)
break;
default:
/* parent */
(void) read(waiter[0], &completed, sizeof(completed));
(void)read(waiter[0], &completed,
sizeof(completed));
close(waiter[0]);
close(waiter[1]);
_exit(0);
@@ -291,20 +299,20 @@ int fuse_daemonize(int foreground)
return -1;
}
(void) chdir("/");
(void)chdir("/");
nullfd = open("/dev/null", O_RDWR, 0);
if (nullfd != -1) {
(void) dup2(nullfd, 0);
(void) dup2(nullfd, 1);
(void) dup2(nullfd, 2);
(void)dup2(nullfd, 0);
(void)dup2(nullfd, 1);
(void)dup2(nullfd, 2);
if (nullfd > 2)
close(nullfd);
}
/* Propagate completion of daemon initialization */
completed = 1;
(void) write(waiter[1], &completed, sizeof(completed));
(void)write(waiter[1], &completed, sizeof(completed));
close(waiter[1]);
waiter[1] = -1;
} else {
@@ -362,17 +370,23 @@ int fuse_main_real_versioned(int argc, char *argv[],
goto out1;
}
struct fuse_session *se = fuse_get_session(fuse);
if (fuse_mount(fuse,opts.mountpoint) != 0) {
res = 4;
goto out2;
}
if (fuse_daemonize(opts.foreground) != 0) {
res = 5;
goto out3;
/*
* fuse_daemonize() already checks and fails then, but we need to
* handle it gracefully here, as this is done libfuse internal
* and caller didn't ask to daemonize with old API.
*/
if (!fuse_daemonize_early_is_active()) {
if (fuse_daemonize(opts.foreground) != 0) {
res = 5;
goto out3;
}
}
struct fuse_session *se = fuse_get_session(fuse);
if (fuse_set_signal_handlers(se) != 0) {
res = 6;
goto out3;
+2 -1
View File
@@ -2,7 +2,8 @@ libfuse_sources = ['fuse.c', 'fuse_i.h', 'fuse_loop.c', 'fuse_loop_mt.c',
'fuse_lowlevel.c', 'fuse_misc.h', 'fuse_opt.c',
'fuse_signals.c', 'buffer.c', 'cuse_lowlevel.c',
'helper.c', 'modules/subdir.c', 'mount_util.c',
'fuse_log.c', 'compat.c', 'util.c', 'util.h' ]
'fuse_log.c', 'compat.c', 'util.c', 'util.h',
'fuse_daemonize.c' ]
if host_machine.system().startswith('linux')
libfuse_sources += [ 'mount.c' ]
+1
View File
@@ -8,6 +8,7 @@
#include <inttypes.h>
#include <stdbool.h>
#include <err.h>
#include <errno.h>
static void print_conn_info(const char *prefix, struct fuse_conn_info *conn)
{