Linux Boot Simulator: UEFI to login.
Power-on to login prompt in six stages: firmware POST, the bootloader,
kernel decompression, the initramfs dance with switch_root,
systemd walking a dependency graph, and finally
getty or gdm. Click Boot.
Three panels build up as the machine boots. The timeline on the left lights each of the six stages in turn, with the current one highlighted and a short note on what it's actually doing. The memory map stacks coloured blocks as firmware, kernel, initrd, and userspace claim RAM, while the process tree below it grows from two bare kernel threads into a full systemd family. The log at the bottom prints dmesg-style lines, and the counters track elapsed time and cumulative disk reads.
Click Boot with the defaults and watch the disk-read counter: it crawls through the first four stages, then jumps by hundreds the moment systemd starts reading unit files. That stage is also the longest on the clock — the kernel itself is done in half a second. Then flip root to LUKS and boot again; the initramfs stage more than doubles while cryptsetup waits on a passphrase, which is exactly the pause you feel on an encrypted laptop. Headless mode skips the desktop's 400-plus MB entirely.
From power-on to PID 1, in six stages
Every Linux box you've ever used did exactly this, in roughly this order, in eight to fifteen seconds.
The CPU comes out of reset in 16-bit real mode, fetches its first instruction from the
firmware ROM, and runs POST — memory test, PCIe walk, fan check. On a modern UEFI system that
takes 1–3 seconds; legacy BIOS systems often took twice that because their probes were less
parallel. The firmware then reads the GUID Partition Table on the boot disk, finds the EFI
System Partition (a small FAT32 volume), and loads BOOTX64.EFI into memory.
On most distros that file is either GRUB2 or systemd-boot.
The bootloader's job is small but irreplaceable: read its config (grub.cfg or
loader.conf), let the user pick a kernel, and load two files into RAM —
vmlinuz (the compressed kernel) and initrd.img (the initial
ramdisk). Then it jumps to the kernel's entry point with a populated
boot_params structure. The kernel decompresses itself in place
(typically a 12 MB compressed blob becomes a 50 MB image plus another 200 MB of slab and
page tables), brings up the rest of the cores via the APIC, parses ACPI tables to learn what
devices exist, and mounts the initrd as a tmpfs root.
Then it executes /init from that tmpfs — usually a script generated at install
time by dracut on Fedora/RHEL or mkinitcpio on Arch. That script
loads the kernel modules needed to mount the real root filesystem, unlocks any encrypted
volumes (cryptsetup talking to dm-crypt via dmsetup),
fsck's the root if needed, mounts it, and calls switch_root to pivot
/ to the real disk and exec /sbin/init. From that moment forward
PID 1 is systemd, the initramfs memory is freed, and the kernel never has to deal with the
boot ramdisk again.
Why initramfs exists
The chicken-and-egg of "the kernel needs the driver to mount the FS, but the driver lives in the FS."
A Linux kernel could in principle have every filesystem driver, every block-device driver,
every LUKS module, and every NIC driver compiled in — and historically some embedded kernels
do. The price is a 200 MB kernel that ships drivers for hardware you don't own and can't
be reduced without a rebuild. The mainline strategy is the opposite: the kernel knows how to
do almost nothing on its own, and the modules for ext4, btrfs, xfs, dm-crypt, lvm, mdraid,
nvme, virtio-blk, iscsi, nbd, and so on live as .ko files inside
/lib/modules/$(uname -r)/ on the root filesystem.
That creates a bootstrap problem. To mount the root filesystem the kernel needs the ext4
driver. The ext4 driver is on the root filesystem. The initramfs is the workaround: a small
cpio archive that the bootloader loads alongside the kernel and the kernel mounts as a
temporary root. It contains exactly the modules and userspace tools needed to find, decrypt,
and mount whatever the real root happens to be. dracut and
mkinitcpio are the tools that introspect your system and generate that archive at
install time — which is why your initrd is regenerated whenever you install a
new kernel.
switch_root is the unusual syscall that ends the dance. It moves the mount of
the new root to /, frees every file from the old tmpfs (so the initrd's RAM can
be reclaimed), and execs the new /sbin/init as PID 1 of the new root. The
running process is still PID 1; its mount namespace and root directory have been swapped out
from under it. If you ever see /init still running after the switch you've made
a serious mistake, because the kernel kept its mount of the old rootfs alive to keep the
binary mapped.
The systemd dependency graph
Targets are not runlevels. They're nodes in a DAG that the scheduler walks toward.
Once systemd takes over as PID 1 it reads every .service,
.socket, .target, .mount, and .timer file
under /lib/systemd/system and /etc/systemd/system, and builds an
in-memory directed graph. Edges come from After= and Before=
(ordering only — A starts after B but doesn't pull B in), from Wants= (soft
dependency — pull B in if you can, ignore failure), and from Requires= (hard
dependency — pull B in, fail A if B fails). It then computes a transaction toward the
default target (usually graphical.target) and starts everything it can in
parallel.
Targets replace the old sysvinit notion of runlevels but they are not the same thing. A
runlevel was a single integer; a target is a named synchronization point with its own
dependencies. basic.target means "filesystems mounted, sysctls applied, slices
created"; multi-user.target means "all server-style daemons up";
graphical.target pulls in multi-user.target plus the display
manager. You can stop at any of them by setting systemd.unit= on the kernel
command line, which is how rescue mode works.
The 2010-era war between sysvinit, Upstart (Ubuntu, event-driven), and systemd was settled by the dependency DAG. Upstart's event model was elegant but didn't compose: services that needed two events had to encode the conjunction themselves, and there was no easy way to express "start me after the network is online but only if a specific filesystem mounted." systemd's declarative graph let every unit say what it needed and let PID 1 figure out the schedule. By 2015 every major distro had switched. The technical argument was parallel startup; the political argument was a thousand pages on Phoronix.
What slow boots actually cost
The 12-second boot is rarely the kernel's fault. It's almost always one or two units holding the train.
Run systemd-analyze on a freshly booted machine and you'll see something like
Startup finished in 2.1s (firmware) + 0.8s (loader) + 0.6s (kernel) + 1.4s (initrd) +
7.2s (userspace) = 12.1s. The userspace number is the one that varies by orders of
magnitude. systemd-analyze blame ranks units by individual startup time;
systemd-analyze critical-chain shows the dependency path that actually
determined the total. Those two views often disagree, and the second is the one worth
optimising.
The chronic offender is NetworkManager-wait-online.service, which blocks
network-online.target until DHCP succeeds — and the default timeout is 90
seconds. Plenty of services list it in their After= and don't actually need it;
a laptop on a flaky wifi can spend a full minute frozen on a black screen while NetworkManager
retries a captive portal it will never reach. Disabling it (or marking it
Wants=network.target instead of network-online.target) is the
single biggest boot-time win on most desktops.
Other regulars: apt-daily-upgrade.timer can race the actual apt commands a user
runs and hold a lock during boot; fsck on a multi-terabyte ext4 root can add 5
seconds even on a clean filesystem; and on virtual machines the
systemd-fsck@.service instances run serially per mount because the underlying
block device is one queue. A typical well-tuned server boots in 3–8 seconds; a typical
untuned Ubuntu desktop, 12–25. The kernel is the same. Everything above PID 1 is choice.