(Unix) daemonization turns out to be quite old

October 3, 2024

In the Unix context, 'daemonization' means a program that totally detaches itself from how it was started. It was once very common and popular, but with modern init systems they're often no longer considered to be all that good an idea. I have some views on the history here, but today I'm going to confine myself to a much smaller subject, which is that in Unix, daemonization goes back much further than I expected. Some form of daemonization dates to Research Unix V5 or earlier, and an almost complete version appears in network daemons in 4.2 BSD.

As far back as Research Unix V5 (from 1974), /etc/rc is starting /etc/update (which does a periodic sync()) without explicitly backgrounding it. This is the giveaway sign that 'update' itself forks and exits in the parent, the initial version of daemonization, and indeed that's what we find in update.s (it wasn't yet a C program). The V6 update is still in assembler, but now the V6 update.s is clearly not just forking but also closing file descriptors 0, 1, and 2.

In the V7 /etc/rc, the new /etc/cron is also started without being explicitly put into the background. The V7 update.c seems to be a straight translation into C, but the V7 cron.d has a more elaborate version of daemonization. V7 cron forks, chdir's to /, does some odd things with standard input, output, and error, ignores some signals, and then starts doing cron things. This is pretty close to what you'd do in modern daemonization.

The first 'network daemons' appeared around the time of 4.2 BSD. The 4.2BSD /etc/rc explicitly backgrounds all of the r* daemons when it starts them, which in theory means they could have skipped having any daemonization code. In practice, rlogind.c, rshd.c, rexecd.c, and rwhod.c all have essentially identical code to do daemonization. The rlogind.c version is:

#ifndef DEBUG
	if (fork())
		exit(0);
	for (f = 0; f < 10; f++)
		(void) close(f);
	(void) open("/", 0);
	(void) dup2(0, 1);
	(void) dup2(0, 2);
	{ int tt = open("/dev/tty", 2);
	  if (tt > 0) {
		ioctl(tt, TIOCNOTTY, 0);
		close(tt);
	  }
	}
#endif

This forks with the parent exiting (detaching the child from the process hierarchy), then the child closes any (low-numbered) file descriptors it may have inherited, sets up non-working standard input, output, and error, and detaches itself from any controlling terminal before starting to do rlogind's real work. This is pretty close to the modern version of daemonization.

(Today, the ioctl() stuff is done by calling setsid() and you'd probably want to close more than the first ten file descriptors, although that's still a non-trivial problem.)


Comments on this page:

By bud at 2024-10-04 12:33:04:

Linux and FreeBSD also have close_range(), in sufficiently recent versions. But if you're writing portable POSIX-ish code, there's no trivial way to know at compile-time whether either of those is supported. (I'll side-step the question of whether autoconf is a good way to handle such things, and just recycle Chris's term "non-trivial" to describe it.)

There's also libbsd, which contains various closefrom() implementations, chosen depending on what's available; note its mess of pre-processor checks. Most of its methods have some kind of "catch". For example, reading procfs requires the calling process to have procfs mounted at the usual location, and to be able to temporarily open one more file descriptor. The ultimate fallback, the close() loop, won't handle file descriptors above the limit specified by RLIMIT_NOFILE, because nobody wants to make 2 billion syscalls. (If your process is lowering RLIMIT_NOFILE itself, it should be smart enough to do a closefrom() first. If a parent process had lowered it while a "high FD" was still open, fighting it may not be worthwhile, and may even be counter-indicated per the POSIX note. But I won't be surprised to eventually see some interesting security impact.)

closefrom() itself isn't even implemented consistently. OpenBSD's "will fail if… An interrupt was received." So I guess the caller's gotta check for that, and try again. For a security-focused distribution, I think they made a bad choice defining it this way; they should promise that it won't fail with EINTR, as POSIX did with the pthread functions. If you do check the return value, your code will fail to compile with FreeBSD or libbsd headers, which define it with a void return type. So even "the best way" is kind of a pain.

Anyway, I agree that processes mostly shouldn't be daemonizing anymore. Operating systems without systemd could provide a systemd-style wrapper easily enough. See "man 3 sd-daemon" and "man 3 sd_notify"; the interfaces are simple, portable, and self-contained (no need to pull in dbus, for example). Such a wrapper would mostly need to forward prefixed stdout/stderr messages to syslog, and daemonize on receiving the "READY=1". This would save the "daemon" author from having to do that work—and potentially doing it wrong, like by only single-forking, which could lead to it accidentally acquiring a controlling terminal later.

Written on 03 October 2024.
« Go's new small language features from 1.22 and 1.23 are nice
Traditionally, init on Unix was not a service manager as such »

Page tools: View Source, View Normal.
Search:
Login: Password:

Last modified: Thu Oct 3 22:51:21 2024
This dinky wiki is brought to you by the Insane Hackers Guild, Python sub-branch.