Process · Linux userspace / kernel ABI

Service supervision instead of daemonization

Compares the double-fork convention with a foreground service under a supervisor such as systemd, and designs signal, readiness, fd, and shutdown ordering.

Series
10 / 38
Build
cc -std=c17 -Wall -Wextra -O2 service_loop.c -o service_loop
Run
./service_loop
Kernel
Linux 6.18.37 LTS

Must a modern Linux service always double-fork?

Double-forking is a convention from execution environments where a daemon detached from the controlling terminal and session and left an orphan to init. When a service manager owns the PID, cgroup, stdout/stderr, and restart policy, self-daemonization instead makes main-PID tracking and readiness detection harder.

A reliable service runs in the foreground and explicitly coordinates startup completion, shutdown requests, child reaping, and log-fd policy with its supervisor. What matters more than the daemon form is who owns the process lifetime.

Structure diagram

Figure 1. Processes and fds owned by a service manager
Service managercgroup · restart policy · readiness deadline

Main process

  • foreground PID
  • signalfd
  • shutdown owner

Inherited resources

  • listener fd
  • stdout/stderr
  • configuration fd

Workers

  • child pidfd
  • active request count
  • reap with waitid

Stop contract

  • SIGTERM
  • drain deadline
  • exit status

A foreground service does not sever its relationship with the supervisor. The main PID, cgroup, listener, logs, and readiness remain in one management unit.

Call path

Figure 2. From userspace code to observable results
service manager prepare fds/env/cgroup
exec service run in foreground
initialize acquire resources
ready notify through protocol
SIGTERM graceful shutdown

Make startup and shutdown observable events exchanged with the supervisor, rather than flags hidden inside a function. Do not accept requests before ready, and do not accept new work after TERM.

Figure 3. Major points along the kernel-internal path
execve service image
signalfd/epoll receive signal as event
waitid reap workers
close/fsync flush output
exit_group report status

This diagram is not for memorizing function names. Follow which return value, file descriptor, memory mapping, or wait queue is passed to the next stage.

Linux 6.18.37 LTS source locations

Go beyond the glibc function to the files where the syscall implementation meets kernel objects. Each link points to the original file at the same tag.

FileFunction / structureWhat to inspect
kernel/signal.c do_send_sig_info(), get_signal() Deliver shutdown requests such as SIGTERM
kernel/exit.c do_exit(), do_wait() Terminate and reap the service and workers
kernel/cgroup/cgroup.c cgroup_attach_task_all() The supervisor tracks the process tree as a cgroup

Complete runnable example

The code below is not pseudocode with explanatory lines omitted. It is a minimal example that can be built and run as a file.

Buildcc -std=c17 -Wall -Wextra -O2 service_loop.c -o service_loop
01#define _GNU_SOURCE
02#include <signal.h>
03#include <stdio.h>
04#include <sys/signalfd.h>
05#include <unistd.h>
06
07int main(void)
08{
09    sigset_t mask;
10    sigemptyset(&mask);
11    sigaddset(&mask, SIGTERM);
12    sigaddset(&mask, SIGINT);
13    if (sigprocmask(SIG_BLOCK, &mask, NULL) < 0)
14        return 1;
15
16    int sfd = signalfd(-1, &mask, SFD_CLOEXEC);
17    if (sfd < 0)
18        return 1;
19    puts("READY");
20    fflush(stdout);
21
22    struct signalfd_siginfo info;
23    if (read(sfd, &info, sizeof(info)) != sizeof(info))
24        return 1;
25    printf("stopping on signal %u\n", info.ssi_signo);
26    close(sfd);
27    return 0;
28}

Code notes

Source line 11sigaddset(&mask, SIGTERM)

First creates the target signal set so a normal service-manager shutdown request can be received as an event fd rather than by an asynchronous handler.

Source line 13sigprocmask(SIG_BLOCK

Blocks the signals before creating signalfd, eliminating the window in which the default action could terminate the process. In a multithreaded program, apply pthread_sigmask in the initial thread.

Source line 16SFD_CLOEXEC

Prevents a supervisor-facing signal fd from being inherited accidentally when a worker execs.

Source line 19puts("READY")

In the example, a line on stdout is the readiness protocol. In a real environment, use a mechanism understood by the supervisor, such as sd_notify, closing a pipe, or socket activation.

Source line 23read(sfd, &info

Consumes a signal as a fixed-size record. If several signals are pending, read repeatedly.

Detailed behavior

01

Readiness is not the same as process existence

A successful fork/exec does not mean configuration parsing, socket binding, or database recovery is complete. A startup race occurs if the supervisor sends traffic merely because a PID exists.

Send one ready notification only after the required resources are acquired and the request-processing loop can actually run.

02

Shutdown has an order

On SIGTERM, stop accepting new requests on the listener, give in-flight work a deadline, reap children, flush durable data, and then exit. Do not perform all of this directly in a signal handler.

SIGKILL performs no cleanup, so the supervisor timeout must match the acceptable data-loss envelope.

03

Document fd ownership

With socket activation or a supervisor pipe, fds are already open before exec. Instead of assuming fd numbers, check a protocol such as LISTEN_FDS and the CLOEXEC state.

Changing a pathname during log rotation does not redirect an already-open file description. Define either SIGHUP handling or a journald/stdout ownership model.

Objects and lifetimes

ObjectCreation and releaseValues to inspect
main processThe supervisor execs the service and tracks its exit statusPID/cgroup, readiness, watchdog
listener fdBound by the service or inherited from the supervisorCLOEXEC, accept ownership
shutdown deadlineBegins when TERM is received and lasts until cleanup completes or forced termination occursmonotonic expiry, active work count

Failure conditions and common misconceptions

Observed symptomLikely causesHow to verify
The service is active but requests failTraffic was delivered before readinessCheck the ready point and listener state
Stopping takes too longNew work is still accepted or workers are not reapedaccept gate, waitid loop, deadline
The port is in use after restartAn old child/cgroup still owns the listenerss -lptn, /proc/PID/fd

Verify it yourself

  1. Run the program, send SIGTERM from another terminal, and verify the signalfd read and normal exit status.
  2. Insert a 3-second sleep before READY and write a small parent that distinguishes process start from readiness.
  3. Have the parent create a listener fd and pass it to an execed child to inspect the fd lifetime of socket activation.
Run./service_loop
Tracestrace -e trace=rt_sigprocmask,signalfd4,poll,read,exit_group ./service_loop

Primary sources