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.

stage
MB loaded
units active

root: gui: init: elapsed: 0.0s · disk reads: 0
timeline
— click "Boot" to begin —
memory map
— RAM is empty —
process tree
— no PIDs yet —

What you're looking at

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.

Found this useful?