QUESTION
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
Structure diagram
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
Call path
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.
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.
SOURCE COORDINATES
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.
| File | Function / structure | What 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 PROGRAM
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.
cc -std=c17 -Wall -Wextra -O2 service_loop.c -o service_loop01#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
Code notes
sigaddset(&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.
sigprocmask(SIG_BLOCKBlocks 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.
SFD_CLOEXECPrevents a supervisor-facing signal fd from being inherited accidentally when a worker execs.
puts("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.
read(sfd, &infoConsumes a signal as a fixed-size record. If several signals are pending, read repeatedly.
DETAILS
Detailed behavior
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.
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.
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
Objects and lifetimes
| Object | Creation and release | Values to inspect |
|---|---|---|
main process | The supervisor execs the service and tracks its exit status | PID/cgroup, readiness, watchdog |
listener fd | Bound by the service or inherited from the supervisor | CLOEXEC, accept ownership |
shutdown deadline | Begins when TERM is received and lasts until cleanup completes or forced termination occurs | monotonic expiry, active work count |
FAILURE PATH
Failure conditions and common misconceptions
| Observed symptom | Likely causes | How to verify |
|---|---|---|
| The service is active but requests fail | Traffic was delivered before readiness | Check the ready point and listener state |
| Stopping takes too long | New work is still accepted or workers are not reaped | accept gate, waitid loop, deadline |
| The port is in use after restart | An old child/cgroup still owns the listener | ss -lptn, /proc/PID/fd |
LAB
Verify it yourself
- Run the program, send SIGTERM from another terminal, and verify the signalfd read and normal exit status.
- Insert a 3-second sleep before READY and write a small parent that distinguishes process start from readiness.
- Have the parent create a listener fd and pass it to an execed child to inspect the fd lifetime of socket activation.
./service_loopstrace -e trace=rt_sigprocmask,signalfd4,poll,read,exit_group ./service_loopPRIMARY REFERENCES