Create your own
Lesson illustration

From Driver Model Population to PID 1: Kernel Threads and Root Filesystem Mounting

Welcome back. The previous lesson established the memory-management substrate that lets the kernel allocate pages and objects reliably: memblock protects early regions, buddy manages physical pages, and SLUB supplies small kernel objects such as task_struct, VFS objects, and driver-model objects.

This lesson follows the next transition. Once allocation, scheduling foundations, and core kernel subsystems are sufficiently ready, Linux must populate its device model, create the first schedulable kernel contexts, establish a usable root filesystem namespace, and finally replace the kernel-side PID 1 context with the first user-space program. This is the boundary at which a board that can print early kernel messages becomes an operating system capable of launching your gateway software.


One boot, three related transitions

It is tempting to describe the end of boot as a single event: “the kernel starts init.” In reality, three interdependent transitions overlap:

TransitionKernel questionKey result
Driver-model populationWhat devices and drivers exist, and which pairs bind?Hardware becomes represented by kernel objects, drivers probe, and sysfs entries become available.
Scheduler activationWhat execution contexts can run now?PID 1, PID 2, and the idle task acquire distinct roles.
Root filesystem setupWhere is the initial filesystem namespace, and what executable should start?The kernel has a root directory and can execute the first user-space process.

The ordering matters. A root filesystem on eMMC cannot be mounted until the storage controller, pin multiplexing, regulators, clocks, MMC block layer, and filesystem driver are functional. In a device-tree system, each of these dependencies is represented in some form by the DTB and then materialized through the driver model.

At the same time, Linux does not wait for every possible driver probe to finish before starting user space. Some devices probe asynchronously; other drivers are modules loaded later. The important requirement is narrower: the kernel must have enough infrastructure and enough required drivers to find a root filesystem and launch a valid init program.

A useful boot-state model is:

  1. start_kernel() establishes core architecture, memory, scheduler, interrupt, and VFS foundations.
  2. rest_init() creates the first two kernel-managed execution contexts and turns the original boot context into idle.
  3. The PID 1 kernel context performs remaining boot work, including initcalls that register buses, devices, and drivers.
  4. Linux uses the VFS to expose an initial root, either an in-memory root filesystem or a storage-backed root filesystem.
  5. PID 1 executes an init program and continues as user-space PID 1.

The Linux driver model: objects first, driver binding second

Linux needs a uniform way to describe a UART, Ethernet MAC, MMC controller, CAN controller, regulator, USB device, or virtual device. The driver model provides that uniform structure.

At its core are three related object types:

ObjectMeaningTypical AM62x example
struct deviceOne discovered or declared hardware/function instanceA specific MMC controller or CAN controller
struct device_driverSoftware capable of controlling compatible devicesA TI MMC controller driver
struct bus_typeA matching domain that connects devices and driversPlatform bus, I2C bus, SPI bus, PCI bus

A driver is not useful merely because it was compiled into the kernel. It must be registered with an appropriate bus, and a device instance must be registered on that bus. The bus supplies a matching rule. For Device Tree based platform devices, matching commonly uses the device node’s compatible property and the driver’s of_match_table.

For example, a simplified chain for an SD or eMMC controller is:

  1. The kernel processes the hardware description passed by U-Boot.
  2. Device Tree population creates a platform-device representation for an enabled controller node.
  3. The relevant driver registers with the platform bus.
  4. The platform bus compares compatible strings and finds a match.
  5. The driver’s probe() routine runs.
  6. probe() obtains clocks, resets, regulators, pinctrl states, interrupts, DMA channels, and memory-mapped register ranges.
  7. If successful, the driver registers its higher-level functionality, such as a block device.

The same relationship applies whether the driver registers first or the device appears first. The bus attempts matching whenever either side is added.

This sequence diagram shows the generic Linux device-model lifecycle: a driver and a device register with a bus, the bus matches them, the driver probes the device, and later removal triggers the driver’s remove callback and further device events. During boot, registration and probe commonly occur through built-in kernel initcalls rather than physical hot-plugging.

The diagram includes udev and a user-mode helper. Treat that portion as a conceptual illustration of kernel-to-user-space device events, not as an exact boot timeline. Modern systems commonly deliver kernel uevents over netlink to a user-space device manager such as systemd-udevd. That daemon cannot handle events until user space itself is running. The kernel’s device model, however, exists before the device manager starts.

Driver registration is not the same as a successful probe

A practical distinction during bring-up is:

  • Registered driver: Linux knows the driver is available.
  • Bound driver: the bus found a matching device and driver.
  • Successful probe: the driver acquired every required dependency and initialized the device.
  • Usable functional device: the driver has registered a higher-level interface, such as mmcblk0, can0, or eth0.

A storage-controller probe may legitimately return -EPROBE_DEFER if its regulator, clock provider, PHY, or pinctrl dependency is not ready. Linux records the deferred probe and retries later. This avoids falsely treating boot ordering as a static list, but it also means a missing dependency can surface later as a root-mount failure.

For an automotive gateway, this is why a message such as “cannot mount root filesystem” may originate in a pinctrl, clock, regulator, DT status, or compatible-string problem rather than in ext4 itself.

Initcalls populate the built-in system

Built-in kernel subsystems and drivers use initcalls: functions placed into ordered linker sections and run during boot. Kernel code uses macros such as core_initcall, subsys_initcall, fs_initcall, device_initcall, and late_initcall to express broad initialization stages.

The precise ordering varies by kernel version and vendor patch set, so do not treat the macro names as a complete dependency-management system. They establish a coarse boot order. Actual hardware dependencies are resolved through mechanisms such as deferred probing, device links, and subsystem-specific registration.

Several foundational actions occur before normal user-space services exist:

  • core device, bus, class, and firmware infrastructure is initialized;
  • the platform bus and Device Tree population support are available;
  • filesystems such as ext4 are registered with the VFS;
  • storage and network drivers register and probe;
  • kobjects representing devices, drivers, buses, and classes are created.

sysfs exposes many of these kernel objects in a navigable filesystem, normally mounted at /sys by the init system. Mounting sysfs makes objects visible to user space; it does not create the underlying device-model relationships from scratch.


rest_init(): PID 0, PID 1, and PID 2 acquire their jobs

Late in start_kernel(), Linux calls rest_init(). At this point the original boot context is still executing kernel code, but Linux needs three durable execution roles:

IdentityInitial function or roleLong-term purpose
PID 0Boot CPU’s idle contextRuns the CPU idle loop when no normal task is runnable
PID 1kernel_init, later user-space initCompletes kernel boot, then becomes the first user-space process
PID 2kthreaddCoordinates creation of many later kernel threads

The most important subtlety is that PID 1 starts as a kernel-side execution context. It does not begin life as systemd, BusyBox init, or /sbin/init. The kernel_init function first performs remaining boot work. It later calls the kernel execution path for a user-space binary. That successful exec transforms the PID 1 task into the chosen user-space init process.

Read the current upstream source now. It is more valuable to follow the actual logic than to memorize a historical call graph.

linux/init/main.c at master · torvalds/linux · GitHub

Read the upstream Linux init/main.c implementation of rest_init(). This is the point where the kernel reserves PID 1 for kernel_init, creates kthreadd, permits scheduling, and sends the boot CPU into its idle loop.

In init/main.c, locate the rest_init(void) definition. Begin at the comment explaining why Linux must reserve PID 1 first. Then continue through the creation of kthreadd_task, the assignment of SYSTEM_SCHEDULING, completion of kthreadd_done, and the final calls to schedule_preempt_disabled() and cpu_startup_entry(). Focus on why PID 1 is created before PID 2 but initially waits for it.

Why create PID 1 before kthreadd?

The source comment answers a question that otherwise looks contradictory:

  • PID 1 must be created first so it owns PID 1.
  • But it must not proceed far enough to request additional kernel threads until kthreadd exists.

rest_init() resolves this with a completion object:

  1. Linux creates the task that begins at kernel_init, reserving PID 1.
  2. It creates kthreadd, which conventionally becomes PID 2.
  3. It records the kthreadd task pointer for later kernel-thread creation requests.
  4. It changes the system state to permit scheduling.
  5. It signals kthreadd_done, allowing the PID 1 boot path to continue.
  6. The original boot task performs a scheduling point and enters the boot CPU idle loop.

The boot CPU’s idle context is associated with PID 0, but it is not an ordinary process that you manage with signals or inspect like a normal application. It runs only when no suitable normal task is runnable. On a running system, ordinary ps output generally begins at PID 1.

kthreadd is also often misunderstood. It is not “the thread that runs all kernel workers.” Instead, it is the kernel’s central service for creating many kernel threads safely. Later you will see daemon-like kernel threads such as writeback workers, workqueue workers, and device-specific threads; their ancestry often leads back through kthreadd.


What PID 1 does before becoming user space

Once released by kthreadd_done, kernel_init() calls code commonly centered on kernel_init_freeable(). Names and exact sequencing evolve across Linux releases, but its responsibilities are stable:

  1. Broaden allocation and CPU-placement permissions now that the scheduler is operational.
  2. Bring secondary CPUs online where configured.
  3. Run the remaining boot-time initcalls.
  4. Initialize driver infrastructure and execute registered initialization routines.
  5. Ensure the initial console is usable.
  6. Determine whether an initramfs supplies /init.
  7. If needed, mount the configured storage-backed root filesystem.
  8. Free memory used only by initialization code and apply final read-only protections.
  9. Execute the initial user-space program.

This is also where the previous lesson’s allocator story pays off. Initcalls allocate driver objects, workqueues, block-layer data structures, dentries, inodes, request queues, and task structures. Without buddy and SLUB functioning, the kernel could not meaningfully populate the subsystems that make the root device reachable.


VFS and the two common root-filesystem paths

The Virtual Filesystem Switch (VFS) is Linux’s common filesystem layer. It provides pathname resolution, mount handling, file operations, inodes, dentries, and file descriptors independently of whether the backing filesystem is ext4, SquashFS, NFS, tmpfs, or another filesystem.

Before user space runs, Linux needs a root directory from which it can resolve paths such as:

/init
/sbin/init
/dev/console

There are two common embedded boot paths.

Path A: an initramfs provides the initial root

An initramfs is an archive unpacked by the kernel into an in-memory root filesystem, commonly called rootfs. It normally contains an executable at /init.

In this path:

  1. The kernel unpacks the archive into the initial root filesystem.
  2. The kernel checks whether /init exists and is executable, unless rdinit= selects another path.
  3. PID 1 executes that program.
  4. The initramfs program may load additional drivers, unlock encrypted storage, run integrity checks, wait for a root device, mount the final root filesystem, and use switch_root or an equivalent handover.
  5. The same PID 1 process, or a replacement process it executes, becomes the long-running system init.

This design is common when the final root is not immediately mountable or must be verified first. Later in the course, it becomes relevant to verified boot, dm-verity, encrypted data, and A/B update policies.

Path B: the kernel mounts the final root directly

For a simple embedded image, U-Boot might pass boot arguments conceptually like:

root=/dev/mmcblk0p2 rootwait rootfstype=ext4 console=ttyS2,115200

Here the kernel must wait until the configured storage device appears, mount the requested filesystem as /, and then locate an init program there.

The broad responsibilities of prepare_namespace() include waiting for required device probing, resolving the root-device specification, handling the configured root, mounting it, and making it the active root namespace. Details differ for block devices, network roots, initrds, and distribution configurations.

rootwait is particularly relevant with SD and eMMC media. It tells the kernel to wait for the root block device rather than failing quickly because a controller or card has not completed initialization.

Console setup is part of a healthy handover

Before executing init, Linux needs a usable console. The kernel normally opens /dev/console and arranges standard file descriptors:

File descriptorUsual initial role
0Standard input
1Standard output
2Standard error

A correct console= parameter is therefore more than a convenience. It determines where user-space init diagnostics and early service logs appear. On a board, the actual UART name and configuration must match the kernel’s serial driver and Device Tree. A wrong console setting can leave a fully running system apparently silent.


Selecting and executing the first user-space process

The kernel command line can explicitly name an init executable:

  • rdinit=<path> selects the initial program when booting through an initramfs.
  • init=<path> selects the init executable after the final root is in place.

If no explicit final init= path is supplied, Linux tries conventional paths, typically including:

/sbin/init
/etc/init
/bin/init
/bin/sh

On a production Yocto image, /sbin/init may be systemd. On a BusyBox-based system, it may be BusyBox init. For an intentionally minimal initramfs, /init may be a static BusyBox binary or a small purpose-built static program.

A successful execution changes the nature of PID 1:

  • Before execution: PID 1 is running kernel_init in kernel context.
  • After execution: the same PID identity runs user-space instructions with an address space, file descriptors, and a user-space program image.

PID 1 is special. If it exits unexpectedly after user space starts, Linux cannot continue normally because PID 1 has essential process-management responsibilities. The usual visible consequence is a panic resembling “Attempted to kill init.”

The following short demonstration is useful because it turns the abstract execution boundary into observable source-level behavior.

How Linux Kernel Starts Initial Process

Watch Nir Lichtman’s “How Linux Kernel Starts Initial Process.” It packages a minimal initramfs and then traces the kernel’s attempt to execute /init under QEMU and GDB.

First watch the initramfs build. Focus on why the demonstration compiles /init statically: a minimal initial root may not contain a dynamic loader or shared C library. Then watch the execution trace. Observe the breakpoint at kernel_init, the inspection of the selected /init path, and the transition through run_init_process and the kernel execution path into the first user-space program.


Reading boot failures by phase

The serial log is the primary evidence source at this boundary. A useful triage rule is to classify the last successful milestone rather than searching immediately for one familiar error string.

Last successful evidenceLikely failing regionFirst checks
Early kernel messages, but no storage deviceDevice model or driver probeDT status and compatible strings, clocks, resets, regulators, pinctrl, MMC or NVMe driver configuration
Storage device appears, then “unable to mount root”Root selection or filesystem mountroot=, partition numbering, rootwait, filesystem driver, filesystem type, corrupted image
Root mounts, then “No working init found”Init executable resolution or execution/init or /sbin/init path, execute bit, shebang, ELF architecture, dynamic loader and shared libraries
Init begins, then kernel panics after it exitsUser-space PID 1 behaviorInit program error path, missing shell or service configuration, unintended return from PID 1
Kernel messages appear but no user-space outputConsole handoverconsole=, /dev/console, serial driver, UART DT configuration

The phrase “No working init found” deserves care. It does not always mean the init file is absent. An ELF executable can exist and still fail with an “ENOENT”-style error if its requested dynamic loader is missing. For example, an ARM64 binary might request a runtime interpreter that the mounted root filesystem does not provide.

That failure mode is central to cross-compilation and root filesystem assembly, which you will investigate more directly in the next module.

A source and log trace routine

When you later have a kernel tree and serial output, keep this compact routine in your private bring-up repository:

cd <kernel-source>

git grep -n "rest_init" init/main.c
git grep -n "kernel_init_freeable" init/main.c
git grep -n "do_basic_setup" init/main.c
git grep -n "prepare_namespace" init
git grep -n "run_init_process" init
git grep -n "driver_init" .

On a running development image, capture:

dmesg > boot-after-userspace.log
cat /proc/1/comm
cat /proc/2/comm
cat /proc/cmdline
mount

Keep raw boot logs, exact DTB hashes, kernel revision, boot arguments, and root filesystem image hashes private. A sanitized public portfolio artifact can instead show the boot-state model, a redacted timing chart, and the diagnosed class of failure without disclosing production memory addresses, partition UUIDs, credentials, or security configuration.


Key takeaways

Linux reaches user space through coordinated device, scheduling, and filesystem transitions:

  • The driver model represents devices, drivers, and buses independently of user-space device managers. Registration, matching, and successful probing are distinct events.
  • Built-in drivers and subsystems are activated through initcalls; deferred probing handles dependencies that become available later.
  • rest_init() reserves PID 1 for kernel_init, creates PID 2 as kthreadd, enables normal scheduling, and turns the original boot context into the PID 0 idle role.
  • PID 1 initially runs kernel code, completes late boot work, then executes an init program and becomes the first user-space process.
  • The VFS supplies the initial root namespace. That root may be an unpacked initramfs or a filesystem mounted from persistent storage using parameters such as root= and rootwait.
  • /dev/console, a valid init path, executable permissions, a compatible ELF binary, and any required dynamic loader are all part of the final handover contract.

Next, you will turn this end-to-end boot understanding into a practical failure-localization method: reading serial logs to distinguish bootloader, Device Tree, kernel, initramfs, console, root-mount, and PID 1 failures.

Can't find a good explanation? Sign up and we'll make it for you

Sign up