If you have ever wondered what makes a Linux board go from a blinking cursor to a working # prompt, the answer is the root filesystem. This lecture is part of our free embedded Linux course and kicks off a new chapter dedicated entirely to root filesystems — arguably the most misunderstood of the four pieces of embedded Linux (bootloader, kernel, toolchain, and root filesystem). By the end of this lecture you will know exactly what has to exist inside a root filesystem for a board to be usable, and why.
Topics covered in this free embedded systems course lecture
init process
rootfs components
kernel command line
embedded Linux boot
What You Will Learn
- How the kernel hands control to the root filesystem
- The minimum set of components every root filesystem needs
- How init= and rdinit= change the boot sequence
- A hands-on demo that boots to a custom init with QEMU
- Common rootfs mistakes that leave boards stuck at boot
Prerequisites
You should already be comfortable with the material from our earlier chapters on the toolchain and the bootloader — specifically, you should know how a bootloader passes control to the kernel and how the kernel is configured. A working Linux host with QEMU installed will let you follow the demo in this lecture directly, no target board required.
From Kernel Boot to a Shell Prompt
Embedded Linux is really four separate pieces working together: a bootloader, a kernel, a toolchain that built everything, and a root filesystem. The first three get a board to the point where the kernel is running in memory — but a running kernel with nothing to execute is not a usable device. The kernel’s very last job during boot is to locate a root filesystem, mount it, and hand control to a single program inside it. Everything that happens after that point — networking, logging, your application logic — is the root filesystem’s responsibility, not the kernel’s.
| |
| passes root= or | decompresses,
| initrd pointer | initializes drivers
v v
[ Kernel mounts root filesystem ]
|
| executes first user-space program
v
[ init (PID 1) ] –starts–> [ shell, daemons, services ]
The kernel locates the root filesystem in one of two ways. Either it is told about a block device using the root= kernel command-line parameter, or it is handed a ramdisk image directly by the bootloader. Once mounted, the kernel looks for a program to execute — by default named init — and that single act completes the kernel’s involvement in booting. From that instant on, every process on the system is a descendant of that first program, traditionally running as process ID 1.
The Minimum Set of Components
A root filesystem that only contains an init program is not very useful. To get a genuinely usable embedded system, a handful of categories of content need to exist together:
| Component | Purpose |
|---|---|
| init | The first program the kernel runs; responsible for bringing the rest of the system up, usually by running scripts |
| Shell | Gives you an interactive prompt, and is also what runs the shell scripts that init and other tools depend on |
| Daemons | Background server processes — networking, logging, device management — started by init |
| Shared libraries | The C library and any other libraries that init, the shell, and daemons are dynamically linked against |
| Configuration files | Plain text files, usually under /etc, that tell init and daemons how to behave |
| Device nodes | Special files that let user-space programs talk to kernel device drivers |
| /proc and /sys | Pseudo filesystems exposing kernel data structures as files — many tools read these directly |
| Kernel modules | Any driver built as a loadable module lives here, typically under /lib/modules/[kernel version] |
On top of this baseline sit the actual application(s) that make your device do its job, plus whatever runtime data those applications generate or consume. A retail barcode scanner, an industrial gateway, and a smart thermostat all share the same eight-item skeleton above — what differs is the application layer bolted on top of it.
Collapsing Everything Into One Program
It is worth knowing that the kernel does not actually require any of the categories above except “a program to run.” Nothing stops you from statically linking a single binary that does absolutely everything — reads sensors, drives a display, handles networking — and telling the kernel to run only that. You would point the kernel at it directly on the command line instead of the conventional init path.
kernel command line -> init=/sbin/ep_initRoot filesystem loaded as a ramdisk:
kernel command line -> rdinit=/sbin/ep_init
This “single statically linked program” approach trades away every convenience tool that a conventional root filesystem gives you — no shell to debug with, no standard utilities, nothing. It is rarely the right choice for production, but understanding that it is possible makes the rest of this chapter click: everything else we build is really just infrastructure layered on top of “the kernel runs one program.”
Hands-On: Watching init= Change Boot Behavior
Let’s prove the handoff mechanism to ourselves using QEMU instead of real hardware, so this works on any Linux host. We will build a tiny statically linked program called ep_init and boot a kernel straight into it, bypassing a conventional root filesystem entirely.
$ cat > ep_init.c << 'EOF'
#include <unistd.h>
#include <sys/reboot.h>
int main(void)
{
write(1, "ep_init: I am PID 1, running as the only program\n", 51);
while (1)
pause();
return 0;
}
EOF
$ gcc -static -o ep_init ep_init.c
$ file ep_init
# Expected output:
ep_init: ELF 64-bit LSB executable, x86-64, statically linked, ...
Package that single binary into a minimal initramfs cpio archive:
$ mkdir ep_initramfs
$ cp ep_init ep_initramfs/
$ cd ep_initramfs
$ find . | cpio -H newc -o > ../ep_initramfs.cpio
$ cd ..
Now boot a stock kernel image in QEMU, pointing directly at our program with rdinit=:
$ qemu-system-x86_64 -kernel bzImage \
-initrd ep_initramfs.cpio \
-append "console=ttyS0 rdinit=/ep_init" \
-nographic
# Expected console output (tail):
...
Run /ep_init as init process
ep_init: I am PID 1, running as the only program
Notice the kernel log line Run /ep_init as init process — that is the exact moment the kernel’s job ends and ours begins. Everything you build for the rest of this chapter is really about making that first program smarter: instead of an infinite pause loop, it will mount /proc and /sys, start a shell, and bring up services.
Real-World Use Cases
- Full conventional rootfs — general-purpose gateways, industrial HMIs, and anything with a shell-based debug workflow
- Ramdisk-only rootfs — recovery images, installer environments, and systems that boot from a read-only NOR flash bootloader
- Single-static-binary init — ultra-constrained RAM budgets, secure appliances that must expose zero shell access, and fast-boot demo hardware
Common Mistakes and Troubleshooting
Kernel panics with “no init found”
This almost always means the path passed to init= or rdinit= does not exist inside the filesystem the kernel actually mounted, or the binary is not statically linked against a C library the kernel environment can resolve. Double check the binary’s linkage with file and the exact path with ls inside the staged rootfs before packaging it.
init exits immediately and the kernel panics
PID 1 is special: if it ever exits, the kernel considers the system unrecoverable and panics. Any custom init, even a throwaway test program like ep_init above, must never return — always end in a loop, an exec of a real init system, or a controlled reboot call.
Shared library not found at boot
If your init or shell is dynamically linked, every library it depends on must already be present in the rootfs before that program runs — there is no package manager available yet to fetch it. Statically linking your very first init binary, as we did above, sidesteps this entirely.
Best Practices
- Keep your very first init program dead simple and statically linked while you are bringing up new hardware — swap in a full init system only once basic boot is proven.
- Always test with
rdinit=against a ramdisk before committing to a block-device root filesystem; it is far faster to iterate on. - Log early boot output to a serial console — once /proc and syslog exist you have better tools, but you need visibility before that.
Security Considerations
A statically linked, minimal init reduces your attack surface dramatically compared to a full init system pulling in dozens of shared libraries at boot. If a device genuinely never needs an interactive shell in production, consider shipping it with no shell binary in the rootfs at all rather than merely restricting access to one.
Summary and Key Takeaways
- The kernel’s last boot responsibility is mounting a root filesystem and executing one program — everything after that is user space.
- A genuinely usable root filesystem needs init, a shell, daemons, shared libraries, configuration files, device nodes, /proc and /sys, and any kernel modules.
- init= and rdinit= let you point the kernel at any program, proving that the “root filesystem” concept is really just “whatever PID 1 needs to do its job.”
- PID 1 must never exit, or the kernel will panic.
Conclusion
Understanding the root filesystem as “the minimum set of things PID 1 needs” rather than “a mysterious directory tree” makes the rest of this free Linux kernel development course chapter far easier to follow. In the next lecture we will look at the Filesystem Hierarchy Standard and start staging a real, conventional root filesystem directory by directory.
Frequently Asked Questions
What is the difference between a root filesystem and a Linux distribution?
A distribution bundles a package manager, a huge set of prebuilt software, and a root filesystem layout around a kernel. An embedded root filesystem is usually a hand-picked, minimal subset built for one specific device rather than general-purpose use.
Can the root filesystem be read-only?
Yes, and it is common in embedded systems for reliability — writable data is usually redirected to a separate partition such as /var or /data so power loss cannot corrupt the core system.
What happens if the kernel cannot find a root filesystem at all?
The kernel panics with a message reporting it was unable to mount a root filesystem, since there is no program left to hand control to.
Do I always need a shell in my root filesystem?
No. Production devices with no debug requirement can ship without one, which also improves security, though most developers keep a shell available during bring-up and development.
What is the difference between init= and rdinit=?
init= tells the kernel which program to run on a conventionally mounted root filesystem (typically from a block device), while rdinit= is used specifically when the root filesystem is supplied as a ramdisk/initramfs image.
Why does PID 1 get special treatment from the kernel?
PID 1 is the ancestor of every other process on the system and is responsible for reaping orphaned processes; if it exits, there is nothing left to manage the system, so the kernel treats this as a fatal condition.
Is this the same as what Buildroot or Yocto produce?
Yes — Buildroot and the Yocto Project automate exactly the process described in this chapter. Understanding it manually first makes those tools far easier to configure and debug later.
Continue This Free Embedded Linux Course
Next up: the Filesystem Hierarchy Standard and staging your first real root filesystem directory tree.
