Developer workspace with Linux terminal displaying C code and daemon process monitoring
Linux System Programming
William  

Writing a Daemon in C: The Double-Fork Explained

Writing a daemon in C means doing the double-fork and setsid dance yourself, then proving it worked with ps and strace instead of trusting a tutorial's checklist. Call fork(), setsid(), fork() again, chdir("/"), reset the umask, and point file descriptors 0, 1, and 2 at /dev/null. A daemon that only looks backgrounded but still holds a controlling terminal or an inherited descriptor will bite you. It goes down the first time the parent shell exits or a signal arrives.

Last updated: 2026-07-21

I built a tiny service in C, then turned it into a background service that must behave predictably on real systems. My goal here is to show a practical path from a foreground prototype to a properly behaving daemon. The rest walks through process basics, the naive fork trick, the safety checklist, PID files and logging, systemd choices, and verification. This is for intermediate users who want maintainable code, not a theory-only overview.

What is a daemon, and how is it different from a background process?

A daemon is a long-running service that keeps working after you log out, with no controlling terminal attached. That last part is the real difference. When you run cmd &, the shell backgrounds the job but the process still belongs to your session and your terminal. Close the terminal and it can catch a SIGHUP and die.

A true daemon has cut that cord. It survives logout, it does not read from your keyboard, and it does not print to your screen. You already run several: sshd handles remote logins, cron fires scheduled jobs, and nginx serves requests. None of them has a terminal.

ModeTerminalDies on logout?Typical output
Foreground (cmd)attachedyesyour console
Backgrounded (cmd &)still attachedoften, via SIGHUPyour console
Daemonnonenosyslog or log files

I treat printing to the terminal from a daemon as a bug. If you want to understand how a process gets adopted by PID 1 when its parent exits, my write-up on the Linux process lifecycle covers the mechanics.

The traditional daemonization steps in Linux

Here is the canonical sequence, and every step exists for a reason. Run these in order:

  1. fork() and exit the parent, so the shell prompt returns and the child is no longer a process group leader.
  2. setsid() to start a new session with no controlling terminal.
  3. fork() again and exit the first child, so the daemon can never reacquire a terminal.
  4. chdir("/") so the daemon does not pin a mounted filesystem.
  5. umask(0) so inherited permission masks do not surprise you.
  6. Close or redirect file descriptors 0, 1, and 2 to /dev/null or a log file.

Skip step 3 and your daemon is still a session leader with no tty, which sounds fine until it opens a terminal device and grabs it as a controlling terminal. Skip the fd cleanup and you inherit whatever the shell left open. Each step closes a specific hole.

Why the double fork and setsid matter

setsid() only works if the caller is not already a process group leader. That is the whole reason for the first fork: the child is guaranteed not to be a group leader, so setsid succeeds. Call setsid from a shell without forking and you get (pid_t) -1 back with EPERM.

After setsid, the process is the leader of a fresh session with no controlling terminal. Good, but a session leader can still acquire one by opening a tty device. The second fork produces a child that is not a session leader, so it can never do that by accident. That is the part folklore forgets.

fork() returns 0 in the child and the child's PID in the parent, or -1 on error. I check every return. A parent that ignores a failed fork keeps running and you never get the daemon you wanted. The setsid(2) man page spells out the session rules if you want the authoritative version.

How do you write a daemon in C, step by step?

Terminal showing Linux process hierarchy and daemon fork operations in monospace text

Here is a compilable skeleton that does the full sequence. Read the comments; each one maps to a step above.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <signal.h>

int main(void) {
 pid_t pid = fork(); /* step 1: leave the process group */
 if (pid < 0) exit(EXIT_FAILURE);
 if (pid > 0) exit(EXIT_SUCCESS); /* parent returns the shell */

 if (setsid() < 0) exit(EXIT_FAILURE); /* step 2: new session */

 signal(SIGHUP, SIG_IGN);

 pid = fork(); /* step 3: never a session leader */
 if (pid < 0) exit(EXIT_FAILURE);
 if (pid > 0) exit(EXIT_SUCCESS);

 umask(0); /* step 5 */
 chdir("/"); /* step 4 */

 close(STDIN_FILENO);
 close(STDOUT_FILENO);
 close(STDERR_FILENO);
 int fd = open("/dev/null", O_RDWR); /* becomes fd 0 */
 dup2(fd, STDOUT_FILENO); /* fd 1 */
 dup2(fd, STDERR_FILENO); /* fd 2 */

 /* real work goes here */
 while (1) { sleep(60); }
 return 0;
}

Compile with gcc -Wall daemon.c -o mydaemon and run it. The prompt returns instantly because the parent exited. That is the point of the first fork.

Notice I open /dev/null right after closing the three standard descriptors. Because open always returns the lowest free descriptor, it lands on 0, and the two dup2 calls fill 1 and 2. Leave a gap here and some library call will write to a descriptor you did not mean to.

How do you confirm the daemon actually detached?

Check the process fields, do not assume. Run this against your daemon's PID:

ps -o pid,ppid,pgid,sid,tty -p <pid>

The TTY column should read ?. If it shows pts/0 or any real terminal, detachment failed and the daemon still dies with your shell. The PPID should be 1, because the second fork's parent exited and init adopted the child. The SID should equal the PID of the process that called setsid, which tells you the new session took.

For the ground truth, run strace -f -e trace=process ./mydaemon and watch the fork, setsid, and second fork calls fire in order. If setsid returns -1 in the trace, you called it from a group leader and skipped the first fork. That tells you exactly where the sequence broke, which is more than any checklist does.

How a daemon should redirect stdin, stdout, and stderr

Point all three at /dev/null unless you have a real log target. Leaving them open to the old terminal is where the nasty surprises live. Write to a closed pipe and the kernel sends you SIGPIPE, which kills the process by default. Hold the terminal open and a logout can deliver SIGHUP.

To see what your daemon actually has open, look at /proc/<pid>/fd/, a directory with one symlink per open descriptor named by its number:

ls -l /proc/<pid>/fd/

Each entry points at the real file. If you see fds 0, 1, and 2 all pointing at /dev/null, the redirect worked. If one still points at /dev/pts/0, you missed it. lsof -p <pid> gives the same answer with more detail. My guide on managing file descriptors goes deeper on inherited descriptors and leaks.

Why chdir("/") and umask(0) belong in the sequence

chdir("/") stops the daemon from holding a busy reference on some random directory. If your daemon starts in /mnt/backup and keeps that as its working directory, umount /mnt/backup fails with "device is busy" and you spend an hour hunting the culprit. Root is always mounted, so it is the safe anchor.

umask(0) clears whatever file-creation mask the daemon inherited. A daemon that creates files with an inherited restrictive mask writes logs or sockets nobody else can read, and you get confusing permission errors downstream. Set the mask to 0 and specify exact modes in your open and mkdir calls instead. Then permissions come from your code and stay predictable.

Should you use glibc's daemon() function or roll it yourself?

Hand-roll it for anything you plan to maintain. The glibc daemon() call does the fork and setsid for you in one line, and for a throwaway tool that is fine. But it does not do the second fork, so the process stays a session leader and can reacquire a terminal. It also predates the fd and signal cleanup that real services need.

I write the steps out because I want to control each one: which descriptors I keep, how I handle setsid failure, whether I redirect to a log instead of /dev/null. daemon() hides all of that. Worse, on some systems it is marked as not portable, so leaning on it ties you to glibc behavior you cannot inspect.

The honest tradeoff is fewer lines against knowing what your process holds. For a service, I take the lines.

How a daemon should handle signals for a clean shutdown

Wire cleanup to SIGTERM, because that is what a normal shutdown and most supervisors send. Install the handler with sigaction, not the old signal, which behaves differently across systems and hands you phantom bugs. In the handler, set a single volatile sig_atomic_t flag and nothing else, then check it in your main loop and do the real teardown there where it is safe.

SIGHUP is the convention for "reload your config" once a daemon has no terminal, since it can never mean "my terminal closed" anymore. Handle it if you have config to reload; ignore it otherwise. Remember SIGKILL cannot be caught, so if cleanup must run, it has to happen on SIGTERM first. My step-by-step on signal handling in Linux covers the async-safe rules in full.

What makes a daemon well-behaved

Production server environment with rack-mounted hardware and active monitoring systems

Three habits separate a shippable daemon from a science project. Write a PID file, guarantee a single instance, and log somewhere you can read after the fact.

  • PID file: write your PID to a file under /var/run, the standard location for them. Supervisors and your own scripts read it to send signals.
  • Single instance: take an exclusive flock on that PID file at startup. If the lock fails, another copy is already running, so exit. This is race-free in a way that "check if the file exists" never is.
  • Logging: send output to syslog or a dedicated log file, never to a stdout you already closed.

Then verify instead of hoping. pgrep -a mydaemon finds it, ps -o pid,ppid,sid,tty confirms it detached, and strace -f -p <pid> shows what it is actually doing when it misbehaves. The most common failure I see is a daemon that "works" in testing but dies on logout, and every time the fix is the same: the TTY column was never ? to begin with. Check the output, do not assume the code did its job.

FAQ

Why does my daemon still die when I close the terminal?

It never detached. Run ps -o tty -p <pid>; if that shows a real terminal instead of ?, your setsid either failed or you skipped the first fork and called it from a group leader. Add the first fork, check setsid's return, and confirm the tty is gone before you trust it.

What happens to a daemon's children if the parent exits early?

The kernel reparents them to PID 1 (init or systemd), which reaps them when they exit. That reparenting is exactly why the daemonize sequence works: the intermediate parents exit on purpose so init adopts the survivor. Orphaned children are the mechanism here, not a bug.

Can I use fork() alone without setsid to make a daemon?

No. A lone fork puts the process in the background but leaves it in your session with the same controlling terminal, so a logout or SIGHUP still reaches it. setsid is what severs the session tie, and it needs that first fork to succeed. One without the other leaves a hole.

How do I read a daemon's logs when it has no terminal?

Point it at syslog and read with journalctl -u <service> on a systemd box, or tail the log file you redirected fds 1 and 2 into. If you sent them to /dev/null, there is nothing to read, so switch the redirect to a real file while you debug and put it back after.

Do I even need to daemonize under systemd?

Often not. With Type=simple, systemd runs your process in the foreground and handles the backgrounding, logging, and restart itself, so you skip the fork dance entirely. Use Type=forking only when your program insists on daemonizing on its own and writes a PID file. For new code, letting systemd supervise a foreground process is less to get wrong.