Init, Shells, and BusyBox Explained
Free Embedded Linux Course — Chapter 5: Building a Root Filesystem
free embedded linux course covers the three ingredients every embedded
Linux root filesystem needs to become a working system: the init program that the kernel
hands control to first, a shell to interpret scripts and give you an interactive prompt, and
the dozens of small utility programs that scripts depend on. We’ll also unpack exactly how
BusyBox solves the size problem that made a full utility set impractical on early embedded
hardware — core knowledge in any free linux device drivers course or
free embedded systems course.
free embedded linux course
BusyBox applets
init process PID 1
embedded shell comparison
What You Will Learn
- Why init is always PID 1 and what special privileges that gives it
- How bash, ash, and hush compare for embedded use, and when to pick each
- Why a “basic” root filesystem still needs roughly 50 separate utility programs
- The exact mechanism BusyBox uses to merge dozens of tools into one binary
- How to write your own miniature multi-applet dispatcher to see the trick firsthand
Prerequisites
You should already have a staging root filesystem directory with correct ownership and
permissions in place (covered in the previous lecture of this
free linux kernel development course). Basic familiarity with C and the
Linux command line is helpful for the applet-dispatch demo below.
The init Program: PID 1
Once the kernel finishes mounting the root filesystem, it hands control to exactly one
program, and that program is always assigned process ID 1. By convention this program is
called init, though the specific implementation varies widely — it might be
classic SysV init, systemd, a minimal BusyBox init applet, or a custom-written binary tailored
to a single device.
PID 1 carries two properties no other process has. First, it always runs as root, giving it
unrestricted access to system resources during startup. Second, the kernel treats it as the
ultimate parent of every other process — if a process’s original parent exits, PID 1 adopts
it, which is why init must never itself exit for the lifetime of the system. In practice, init
reads a startup configuration, launches a sequence of shell scripts, and those scripts start
the background daemons — server-style programs with no attached terminal —
that make the device do useful work.
Choosing a Shell for the Target
A shell does two jobs on an embedded target: it interprets startup and maintenance scripts,
and it optionally gives a developer an interactive command prompt over a serial console or SSH
session. Not every shell is a good fit for a resource-constrained device, so it’s worth
understanding the real trade-offs.
| Shell | Heritage | Typical Footprint | Best Fit |
|---|---|---|---|
bash |
Extended Bourne shell, desktop Linux standard | Largest — many extensions (“bashisms”) | Development boards with ample storage, or the build host itself |
ash (BusyBox variant) |
Bourne shell lineage via BSD, extended for closer bash compatibility | Small | Most production embedded targets — the default in many BusyBox configs |
hush |
Purpose-built minimal shell | Smallest | Severely memory-constrained devices with very simple scripting needs |
The trap almost every embedded developer falls into at least once: writing and testing shell
scripts only on the host machine, where bash is available with all its extensions,
then discovering those scripts silently fail on the target running ash or
hush because a bash-only feature crept in. Always test scripts on the actual
shell that will run them in production, ideally by cross-testing directly on target hardware
or in a matching emulated environment before final image sign-off.
The Utility Problem
A shell on its own does almost nothing — it’s essentially a program launcher with flow
control. What makes a shell useful is the surrounding set of command-line utilities:
ls, cp, grep, mount, ps, and
dozens more. A genuinely minimal but usable root filesystem needs somewhere around fifty of
these. Sourcing, cross-compiling, and packaging fifty separate upstream utility projects by
hand is a significant engineering effort on its own, and the resulting binaries — each
statically linking its own copy of the C library and shared helper code — historically added
up to tens of megabytes, which was simply unaffordable storage on 1990s and early-2000s
embedded hardware.
How BusyBox Solves It
BusyBox reimplements the most commonly used 80% of functionality from each of those utilities
in roughly 20% of the code a full-featured version would need — a deliberate application of
the 80:20 rule. That alone shrinks the codebase enormously, but the more important trick is
architectural: instead of shipping fifty separate executables, BusyBox compiles every one of
its supported utilities — called applets — into a single binary.
/bin/cp —> symlink —> /bin/busybox
/bin/grep —> symlink —> /bin/busyboxbusybox_main(argv[0])
|
argv[0] == “ls” -> ls_main()
argv[0] == “cp” -> cp_main()
argv[0] == “grep” -> grep_main()
Every applet is compiled as an ordinary C function named [applet]_main — the
cat command’s logic, for instance, lives in a function called
cat_main. Only one real executable file exists on disk: busybox
itself. All the familiar command names — /bin/ls, /bin/cp, and so on
— are just symlinks pointing back at that single binary. When the kernel executes any of those
symlinks, the process’s argv[0] still contains the name it was invoked as, and
BusyBox’s own top-level main() reads that string and dispatches to the matching
applet function. Sharing one binary means all the applets share the same compiled code and
library routines instead of each duplicating them, which is what collapses tens of megabytes
down to a few hundred kilobytes.
Hands-On: Building a Miniature Applet Dispatcher
To really understand the mechanism rather than just read about it, here’s a small original
program, ep_multitool, that applies the exact same argv[0]-dispatch trick across
just two toy applets.
$ cat > ep_multitool.c << 'EOF'
#include <stdio.h>
#include <string.h>
#include <libgen.h>
static int hello_main(int argc, char **argv)
{
printf("ep_multitool: hello applet running\n");
return 0;
}
static int rev_main(int argc, char **argv)
{
if (argc < 2) {
printf("usage: rev <text>\n");
return 1;
}
int len = strlen(argv[1]);
for (int i = len - 1; i >= 0; i--)
putchar(argv[1][i]);
putchar('\n');
return 0;
}
int main(int argc, char **argv)
{
char *name = basename(argv[0]);
if (strcmp(name, "hello") == 0)
return hello_main(argc, argv);
if (strcmp(name, "rev") == 0)
return rev_main(argc, argv);
printf("ep_multitool: unknown applet '%s'\n", name);
return 1;
}
EOF
$ gcc -o ep_multitool ep_multitool.c
Now create symlinks for each applet name and invoke them, exactly like BusyBox does:
$ ln -s ep_multitool hello
$ ln -s ep_multitool rev
$ ./hello
ep_multitool: hello applet running
$ ./rev "embedded linux"
xunil deddebme
Only one binary — ep_multitool — exists on disk, yet it behaves as two separate
commands depending on which symlink name was used to launch it. Scale this pattern up to
fifty-odd applets, add careful size optimization throughout, and you have the essence of how
BusyBox works.
Real-World Use Case: A Minimal Boot Environment
A typical minimal target root filesystem uses BusyBox to provide ash as the
shell, an init applet to become PID 1, and the entire core utility set — all
from one compact binary — while init’s startup scripts bring up daemons like a network
manager or a logging service that may still be separate, purpose-built binaries. This
combination is what lets a device with a few megabytes of flash boot to a usable shell prompt.
Common Mistakes and Troubleshooting
- Testing scripts only under bash on the host, then finding they break under BusyBox’s ash on target — always validate against the actual target shell.
- Letting init exit or crash. Since PID 1 is the ultimate parent of every process, if it dies unexpectedly the kernel typically panics — init must be rock solid.
- Forgetting a symlink for a needed applet — BusyBox only responds to the applet name it was invoked as, so a missing symlink means “command not found” even though the logic exists inside the binary.
- Assuming every desktop flag works — BusyBox applets implement a useful subset of each tool’s options, not the full GNU coreutils feature set.
Best Practices
- Pick the smallest shell that satisfies your actual scripting needs — don’t default to bash out of habit.
- Keep BusyBox’s build configuration minimal, enabling only the applets your image actually uses, to save further space.
- Version-control your init scripts alongside your kernel and root filesystem configuration, since they’re just as critical to a working boot.
- Cross-test shell scripts on target hardware (or an accurate emulator) before treating an image as release-ready.
Performance Considerations
Because every BusyBox applet shares one resident binary, the kernel only needs to keep a
single executable’s code pages in memory even when many applets run concurrently, which
reduces both storage footprint and memory pressure compared to fifty independent executables
each linking their own copies of shared library code.
Summary and Key Takeaways
- Init is always PID 1, always runs as root, and must never unexpectedly exit.
- Shell choice is a size-vs-compatibility trade-off: bash, ash, and hush sit at different points on that spectrum.
- A minimal root filesystem still needs roughly fifty utility programs to be genuinely usable.
- BusyBox collapses all of them into one binary using an argv[0]-based applet dispatch mechanism.
- You can reproduce the exact same dispatch trick yourself in a few lines of C.
Conclusion
Init, the shell, and the BusyBox applet model together form the minimum viable software stack
that turns a bare root filesystem into an interactive, scriptable Linux system. Understanding
the argv[0] dispatch trick in particular demystifies what can otherwise feel like a black box,
and it’s a pattern worth remembering well beyond BusyBox itself. This wraps up the core
concepts of root filesystem construction covered in this
free embedded systems course.
FAQ
Why must init always be PID 1?
The kernel reserves PID 1 for the first userspace process and gives it special responsibilities, including becoming the adoptive parent of any orphaned process — no other PID gets this treatment.
Can I use a completely custom init program?
Yes — init just needs to be an executable the kernel can run as PID 1. Many embedded products use a small custom-written init instead of a full init system like systemd.
Is ash the same as bash?
No. Both derive from the Bourne shell lineage, and BusyBox’s ash has been extended for closer bash compatibility, but it does not implement the full set of bash extensions.
Why can’t I just symlink busybox to any name I want?
You can create the symlink, but BusyBox will only dispatch to an applet it actually implements. An unrecognized name falls through to an “unknown applet” style error.
Does BusyBox replace every Linux command?
No — it implements a useful subset of the most common utilities’ functionality, not every flag and edge case of the full GNU versions.
What happens if init crashes?
Since every other process depends on PID 1 remaining alive, an init crash typically causes the kernel to panic and the system to halt or reboot.
How does argv[0] dispatch actually work under the hood?
When a program is executed via a symlink, the kernel still passes the invoked path as argv[0] to the running process. The program reads that string itself and branches accordingly — there’s no special kernel support beyond passing the name through.
Continue the Free Embedded Linux Course
Explore more free embedded systems and Linux kernel development lectures.
