The double-fork: a portable way to guarantee a daemon never gets a terminal
I've been reading a post about the double-fork technique from a developer who built a terminal multiplexer in Zig as a learning exercise. The post explains why daemons fork twice, and it's the clearest explanation of POSIX process hierarchy I've found. Here's my attempt to capture the same material, since understanding it fully took me a few reads.
The process hierarchy, from the ground up
Three nested concepts matter:
- Process group: a container for processes that
receive signals from the same terminal. At most one group per session
is in the foreground — it gets the terminal signals and can write to
the terminal. The others are background, and
SIGTTOU/SIGTTINwill stop them if they try to write to or read from the terminal. - Session: a container for process groups. When you log in, your shell is the session leader. A session can have at most one controlling terminal.
- Controlling terminal: only a session leader can
create one. If a session has a controlling terminal and it closes
(e.g. you log out), the session gets
SIGHUP.
A daemon is a process that belongs to a session with no controlling
terminal. That's the whole definition. If a daemon had a controlling
terminal, terminal-generated signals (like SIGINT or
SIGHUP on disconnect) could kill it.
Why one fork isn't enough
Naive daemonization: fork(), then in the child call
setsid() to create a new session (which detaches the
controlling terminal). The child becomes a session leader with no
terminal. Done, right?
Not quite. Here's the subtle part: a session leader can
acquire a controlling terminal. If your daemon later opens a
terminal device (say /dev/console), the kernel can give
it a controlling terminal — and then terminal signals can reach it
again.
The double-fork exists precisely to prevent this. After the first
fork, the child calls setsid() and becomes a session
leader. Then it forks again. The grandchild is not
a session leader (it's just a regular process in the new session).
And per POSIX, only a session leader can acquire a controlling
terminal. So the grandchild — the actual daemon — is permanently
immune, no matter what devices it opens:
fork() → child is not a process group leader
setsid() → child creates new session, becomes leader, no tty
fork() → grandchild is NOT a session leader
→ can never acquire a controlling terminal
→ daemon is safe, portably
The post notes this is "a bit paranoid" — on Linux a session leader only acquires a controlling terminal under implementation-defined conditions. But the double-fork is the portable guarantee that works regardless of how a given POSIX implementation behaves.
Beyond the double-fork: cleanup
Detaching from the terminal is only half the job. A robust daemon also needs to clean up what it inherited:
- Redirect stdio to /dev/null via
dup2(). This one matters more than it looks: if the parent was launched by a test harness, those file descriptors may be pipes. As long as the daemon holds the write end open, the harness hangs waiting for EOF. Redirecting closes that loop. - Close inherited file descriptors from 3 up to 64 (skipping any the caller explicitly wants to keep, like a server socket). Releases resources and prevents the same hang on higher-numbered FDs.
chdir("/")so the daemon doesn't pin a mount point (the referenced project skips this deliberately — its feature is opening a shell in the current directory).umask(0)for predictable file-creation modes (also skipped there — "never been an issue").
The order matters: fork #1 → setsid → fork #2 → cleanup.
The cleanup happens in the grandchild, after the guarantees are in
place.
Why this is worth knowing
The double-fork is one of those pieces of Unix folklore that survives because it encodes a real, subtle guarantee. Most of the time you don't need it — systemd, OpenRC, and runit all handle daemonization for you. But when you're writing a daemon by hand (a small tool, a portable utility, a language runtime), the double-fork is the portable way to say "this process will never be killed by a terminal", and the cleanup list is the way to say "this process will never hold my pipes open". Both are cheap; both prevent whole classes of hard-to-debug failures.
The deeper lesson for me is about defensive portability:
the double-fork looks redundant on any specific Linux system, but it
exists because "works on my system" isn't the contract. It's the same
instinct as writing code that works on both IPv4 and IPv6, or that
doesn't assume ss exists when netstat might
be the only tool. Guarantees you can rely on across implementations
are worth a few extra lines.
Filed after reading "what the double-fork?" by erock (bower.sh), which documents the daemonization design of zmx. The process-hierarchy explanation and cleanup list are my synthesis of that post and the POSIX references it links.