Ashok G

Linux, Observed · Linux 7.3 · Draft

4 — PID 1

Requirements: chapters 1 and 3, qemu-system-x86_64, and a kernel. There’s an ordering catch: this chapter boots a kernel, and chapter 5 builds yours. Either copy a readable distro kernel to /lo/out/bzImage, if it has the serial driver built in, or do chapter 5 first and come back.

When the kernel finishes booting, it executes a single program as process 1. Everything else, every daemon, every shell, every container, is a descendant of whatever that program decided to do. systemd is one answer to “what should PID 1 do?”, at about 2.06 million lines in its repository at the end of 2024 (Phoronix). This chapter writes a very small answer, not because systemd is wrong, but because you will never understand what an init is until you’ve been one.

What PID 1 must do

  1. Mount the basics: /proc, /sys, /dev (devtmpfs).
  2. Start something. With an initramfs, the kernel runs /init first. Without one, it tries init=, then /sbin/init, /etc/init, /bin/init and /bin/sh, and otherwise panics with “No working init found” (init/main.c).
  3. Reap. When a process dies and its parent is gone, it gets a new parent: first another thread in its group, then a subreaper, then init (exit.c). If init doesn’t wait(), these stay as zombies. This is the one job it can’t skip.
  4. Handle signals. The kernel only delivers signals to PID 1 that it has handlers for (signal.c).
  5. Shut down cleanly, using sync and reboot(2).
  6. Never exit. If PID 1 exits, the kernel panics with “Attempted to kill init!”

The kernel protects PID 1 from signals, but not from itself.

Hands-on: write and boot it

Step 1: write the init

Create /lo/out/init.c. Two details matter. reboot(RB_DISABLE_CAD) is needed because, by default, Ctrl-Alt-Del reboots at once rather than sending SIGINT (reboot(2)). And children must restore the signal mask, or they’d inherit it with everything blocked.

#define _GNU_SOURCE
#include <signal.h>
#include <stdio.h>
#include <sys/mount.h>
#include <sys/reboot.h>
#include <sys/wait.h>
#include <unistd.h>

int main(void) {
    sigset_t all, old;
    if (getpid() != 1) { fprintf(stderr, "not PID 1\n"); return 1; }
    mount("proc", "/proc", "proc", 0, 0);
    mount("devtmpfs", "/dev", "devtmpfs", 0, 0);
    reboot(RB_DISABLE_CAD);                     /* CAD now sends SIGINT */
    sigfillset(&all);
    sigprocmask(SIG_BLOCK, &all, &old);
    if (fork() == 0) {                          /* child restores the mask */
        sigprocmask(SIG_SETMASK, &old, 0);
        setsid();
        execl("/bin/sh", "sh", (char *)0);
        _exit(127);
    }
    for (;;) {
        int sig;
        sigwait(&all, &sig);
        if (sig == SIGCHLD) {
            int st; pid_t p;
            while ((p = waitpid(-1, &st, WNOHANG)) > 0)
                printf("reaped %d\n", (int)p);
        } else if (sig == SIGTERM || sig == SIGINT) {
            sync();
            reboot(RB_POWER_OFF);
        }
    }
}

Step 2: build an initramfs

cd /lo/out
$TGT-gcc -static -Os init.c -o init
rm -rf ramfs && mkdir -p ramfs/bin ramfs/proc ramfs/dev ramfs/sys
cp init ramfs/init
cp /lo/rootfs/bin/busybox ramfs/bin/busybox
ln -s busybox ramfs/bin/sh
( cd ramfs && find . | cpio -o -H newc ) | gzip > initramfs.cpio.gz

Expected: gzip -dc initramfs.cpio.gz | cpio -it | grep -x init prints init.

Step 3: boot it

qemu-system-x86_64 -m 512M -nographic -no-reboot \
  -kernel /lo/out/bzImage -initrd /lo/out/initramfs.cpio.gz \
  -append "console=ttyS0 panic=-1"

Expected: a shell prompt appears. Exit QEMU with Ctrl-A then X.

Step 4: record the first number

Note how long boot-to-shell took. For a cheap first measurement, add a printf using CLOCK_BOOTTIME to your init. Bpftrace inside the guest comes later and would disturb the boot anyway.

Step 5: prove reaping works

Inside the guest, make a real orphan:

(/bin/busybox sleep 1 &) ; /bin/busybox sleep 3

The subshell exits at once, so the first sleep is reparented to init. Expected: a reaped line should appear about a second later.

Step 6: make a zombie on purpose

  1. Comment out the waitpid loop, then rebuild and boot.
  2. Repeat Step 5, then run /bin/busybox ps.
  3. Expected: the orphan stays in state Z, forever. This is the one job PID 1 can’t delegate.
  4. Restore the loop.

Step 7: try to kill init

kill -9 1
kill -TERM 1

Expected: the first does nothing. The second powers off, because your program waits for it.

Step 8: let init exit

  1. Change the loop to return 0;, then rebuild and boot.
  2. Expected: the console shows “Attempted to kill init!” with an exit code.
  3. Restore the loop.

Trying it without a VM

A chroot won’t work, because it doesn’t change your PID, and this init refuses to run unless it’s PID 1. Use a PID namespace:

sudo unshare -pf --mount-proc ./init

It differs in that reboot and signals behave differently, and exiting kills only the namespace (pid_namespaces(7)).

If it fails

What you seeCauseFix
Blank consoleNo console=ttyS0, or no serial driverAdd it, or use a kernel with SERIAL_8250_CONSOLE=y
“No working init found”/init missing or not executableRun the cpio -it check
Failed to execute /init (error -2)Dynamic binary with no interpreterRebuild with -static
“unable to open an initial console”No /dev/console before init runsAdd sudo mknod ramfs/dev/console c 5 1
No job control in the shellThe shell is on /dev/consoleExpected. Open /dev/ttyS0 as an extension

Principle: the first process is the contract

PID 1 defines what “the system is up” means: which filesystems exist, which services run, what happens when they die, what happens on shutdown. Every process on the machine inherits those decisions. That is why init design is architecture, not plumbing, and why the init you choose determines the operational model of everything above it: supervision (does a dead service come back?), ordering (what can assume what?), and the failure story (what does “half-up” look like?).

The general form: in any system, the component that starts everything else is the one whose assumptions are inherited by all of them. The orchestrator, the bootstrap script, the first service in the dependency graph: decide those carefully, because they’re the decisions nobody downstream gets to revisit.