what the double-fork?
摘要
作者在构建 zmx 终端复用工具时,深入研究了 Linux 守护进程的 double-fork 技术。文章从 daemon 的定义讲起,解释了 fork、session、controlling terminal 等核心概念,并论证了为什么需要两次 fork 才能确保守护进程永远不会获得控制终端(因为只有会话首进程才能获取控制终端,而第二次 fork 产生的孙进程不是会话首进程)。随后介绍了 zmx 在 double-fork 之后进行的文件描述符清理、重定向标准流到 /dev/null 等额外步骤,并对比了 systemd 等 supervisor 方案与自实现 daemon 的取舍。
荐读理由
文章清晰解释了 double-fork 的 POSIX 可移植性理由,并给出了 zmx 中具体的 fd 清理和标准流重定向实现,这些可直接迁移到自己的守护进程或后台任务设计中;同时纠正了'只需一次 fork + setsid'的常见误解,提供了可验证的深度洞见。
原文
•
I built zmx as a multi-faceted learning exercise. I wanted to learn more about zig, posix, daemons, ptys, libghostty, and terminals. Creating a program that fits into my core dev toolchain was merely a side-effect. While researching about how to create a daemon, buried in the implementation of abduco, I discovered the double-fork.
•
•
•
Hidden behind the double-fork technique is a cascade of terms and concepts that are fundamental to how linux manages processes. This post is me coming to the surface and intended to be a reminder when I read zmx and say "what the double-fork?"
what is a daemon?
•
A daemon belongs to a session that has no controlling terminal. For example, when you background a process using ctrl-z+bg or {cmd} &, the session still has a controlling terminal which means it can control those background processes. This is not ideal for a daemon.
•
There are other ways to background a process that does disconnect it from the terminal, with varying degress of pros and cons:
•
nohup {cmd} &
•
setsid {cmd} &
•
{cmd} & disown
how does fork work in a program?
•
When using fork(2) in a program, it will create a child process in addition to still running the parent process that called fork. This was hard for me to grok when building out zmx. It is as-if the program clones itself at that exact line and both copies continue independently from there. Typically when creating a daemon you would use the child process of the fork and then kill the parent process.
•
fork()
│
┌────────────┴────────────┐
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ PARENT │ │ CHILD │
│ PID = 42 │ │ PID = 55 │
│ PPID = 10 │ │ PPID = 42 │
│ │ │ │
│ (continues │ │ (continues │
│ from the │ │ from the │
│ fork point) │ │ fork point) │
└─────────────────┘ └─────────────────┘
•
Identical memory at fork point (copy-on-write)
•
Identical open file descriptors (shared offsets)
•
Different return value from fork():
•
parent → child's PID (55)
•
child → 0
•
Both run independently from here
what is a session?
•
In linux, a session is a container for process groups.
•
Process groups are a container for processes that receives signals from the same terminal.
•
At-most one process group in a session can be in the foreground.
•
It receives signals and can write to the terminal.
•
All other process groups in the session are background, and SIGTTOU/SIGTTIN will stop them if they write to or read from the terminal.
•
session/
├── process group (foreground)
│ ├── process
│ ├── process
│ └── process
└── process group (background)
├── process
└── process
what is a controlling terminal?
•
Only a session leader can create a controlling terminal for the session.
•
A session can have at-most one controlling terminal.
•
The session leader that opens the terminal becomes the controlling process.
why double-fork?
•
When launching a daemon, you normally set the child process of the fork to be the session leader via setsid() which creates a new session that removes the current controlling terminal. This is important because we don't want a controlling terminal for our daemon or else it could receive signals to shutdown when the controlling terminal closes.
•
However, if the first fork's child process is also the daemon process, then it's technically possible for the daemon to open a terminal device (e.g. open("/dev/console", O_RDWR)) and then it would acquire a controlling terminal! A controlling terminal would expose the daemon to terminal-generated signals (e.g. SIGINT) or SIGHUP from terminal disconnect which could kill the daemon.
•
The first fork produces a child guaranteed not to be a group leader, so setsid() will succeed. By forking a second time, the grandchild process (the daemon) is not the session leader. Per POSIX, only a process that is the session leader can acquire a controlling terminal.
•
Apparently this is "a bit paranoid" and on Linux it is arguable since a session leader only acquires a controlling terminal under implementation-defined conditions. But the double-fork is the portable way to guarantee the daemon can never acquire one, regardless of how a given POSIX implementation behaves. So we baked it into zmx.
•
PID=42 SID=10 PGID=10 ← original (PG leader, has tty)
│
│ fork #1
├──────────┐
│ exit │ PID=55 SID=10 PGID=10 (not a PG leader)
✝ │
│ setsid()
▼
PID=55 SID=55 PGID=55 (session leader, no tty)
│
│ fork #2
├──────────┐
│ exit │ PID=73 SID=55 PGID=55
✝ │ PID≠SID → can't get a tty
▼
DAEMON ✓
beyond the double-fork
•
The double-fork detaches the daemon from any controlling terminal, but that's only one piece of a robust daemon. A daemon also needs to clean up everything it inherited from the parent that launched it like file descriptors, working directory, and standard streams.
•
In zmx, after the second fork, the daemon does two more things:
•
First, it redirects stdin, stdout, and stderr to /dev/null via dup2(2). This is critical: if the parent was launched by a test harness like bats, those file descriptors might be pipes. As long as the daemon holds the write end open, the harness will hang waiting for EOF. Redirecting to /dev/null closes that loop.
•
Second, it closes all inherited file descriptors from 3 up to 64 (skipping any that the caller explicitly wants to keep, like a server socket). This releases resources the daemon doesn't need and prevents the same hang on higher-numbered FDs.
•
We do not chdir because opening a shell in the cwd is a feature of zmx
•
We do not umask mainly because it has never been an issue
•
[1] fork() ── child is not a PG leader
[2] setsid() ── new session, drop tty, become leader
[3] fork() ── daemon is not the leader, can't get tty
─────────── double-fork ends, cleanup begins ───────────
[4] chdir("/") ── don't pin a mount point
[5] umask(0) ── predictable file-creation modes
[6] dup2 /dev/null ── stdin/stdout/stderr → /dev/null
[7] close fds 3-64 ── release inherited resources
preferred ways to daemon a process
•
Ideally you would be able to create a daemon using your OS supervisor like systemd, openrc, runit, etc. This was not a great option for zmx because I wanted it to be portable on linux and mac. I also wanted zmx to "just work" by auto-daemonizing the session during attach. So in the case of zmx, when we fork, the child process becomes the daemon and the parent process becomes the client that connects to the daemon via a unix socket.
refs
•
•
zmx/daemonize.zig (stdio + fd cleanup)
•
•
1.7 How do I get my program to act like a daemon?
•
Why fork twice? (StackOverflow)
•
[#zmx](https://bower.sh/?tag=zmx)
last updated: 2026-07-30
We're all in the gutter, but some of us are looking at the stars. -- Oscar Wilde
这条对你有帮助吗?