bootargs
free linux kernel development course
free embedded linux course
free linux device drivers course
console loglevel
If you’ve ever built your own embedded Linux image, you already know that the
kernel command line is one of the first things you need to get right, or your board
simply won’t boot to a shell. This lecture is part of our free linux kernel development
course, and it explains exactly what the kernel command line is, how the bootloader hands
it to the kernel, and how you can use it to control everything from boot speed to which root
filesystem gets mounted. This is the kind of practical, board-bring-up knowledge you need whether
you’re following a free embedded linux course or debugging a production board at 2 AM.
What You Will Learn
- What the kernel command line actually is, and the three ways it reaches the kernel
- How to inspect and modify bootargs on a running or QEMU-emulated board
- The most useful kernel command line parameters for embedded bring-up and debugging
- How console log levels work, and how to control kernel boot verbosity
- How to shave real milliseconds off boot time using a calibrated
lpjvalue - Common mistakes engineers make with bootargs, and how to avoid them
Prerequisites
- A cross-compiled ARM kernel and root filesystem (see earlier lectures in this course)
- Basic familiarity with U-Boot environment variables, or a device tree
chosennode - QEMU installed on your host if you want to follow the demo without real hardware
What Is the Kernel Command Line?
The kernel command line is a single string of space-separated options that is handed to the
Linux kernel at the very start of boot, before any driver has probed, before init has run,
before anything userspace-related exists. It tells the kernel things it cannot discover on its
own: which device holds the root filesystem, how noisy the console should be, what the very first
user-space program should be called, and dozens of other low-level policy decisions.
There are three places this string can come from, and the kernel merges them in a defined
order:
| 1. Bootloader (highest |
| priority in practice) |
| U-Boot “bootargs” env |
| var, passed via ATAGs |
| or the DT /chosen node |
+—————+————-+
|
v
+—————————–+
| 2. Device tree |
| chosen { bootargs = …} |
| baked into the .dtb |
+—————+————-+
|
v
+—————————–+
| 3. Kernel .config |
| CONFIG_CMDLINE=”…” |
| built into the kernel |
| image itself |
+—————+————-+
|
v
Final merged string
seen by the kernel
at “Kernel command
line: …” in dmesg
In the U-Boot case, the flow looks like this: U-Boot reads the bootargs environment
variable, patches it into either the legacy ATAG list or the device tree’s /chosen/bootargs
property, and jumps into the kernel. The kernel then parses that string during
start_kernel(), long before any driver subsystem is initialised.
Inspecting the Command Line on a Running Kernel
Every running Linux system exposes the exact string it booted with. This is the fastest way to
confirm what actually reached the kernel, as opposed to what you think you set in U-Boot:
$ cat /proc/cmdline
console=ttyAMA0,115200 root=/dev/mmcblk0p2 rootwait rw
If this doesn’t match what you expected, the bug is almost always in the bootloader stage
(wrong environment variable, wrong device tree overlay applied) rather than in the kernel
itself.
Setting bootargs From U-Boot
On a U-Boot-based board, you set and save the command line like this:
=> setenv bootargs 'console=ttyAMA0,115200 root=/dev/mmcblk0p2 rootwait rw'
=> saveenv
=> boot
Setting bootargs From the Device Tree
If your board has no interactive bootloader prompt, or you want the command line to travel
with the device tree itself, set it in the chosen node of your board’s .dts file:
/ {
chosen {
bootargs = "console=ttyAMA0,115200 root=/dev/mmcblk0p2 rootwait rw";
};
};
Rebuild the device tree blob and it will be baked in on the next boot:
$ make ARCH=arm dtbs
Reference Table: Useful Kernel Command Line Parameters
| Parameter | Effect |
|---|---|
console=<dev>,<baud> |
Selects which serial device carries kernel and early userspace output |
root=<device> |
Device node holding the root filesystem |
rootwait |
Waits indefinitely for the root device to appear — required for MMC/SD boot |
rootdelay=<N> |
Fixed N-second wait before mounting root — a crude alternative to rootwait |
rootfstype=<fs> |
Forces the root filesystem type; required for filesystems the kernel can’t auto-detect, such as JFFS2 |
ro / rw |
Mounts root read-only or read-write at boot (has no effect on an initramfs, which is always read-write) |
init=<path> |
First userspace program to run from the root filesystem; defaults to /sbin/init |
rdinit=<path> |
First program to run from an initramfs; defaults to /init |
quiet |
Raises the console log level so only emergency messages print — the biggest single lever for reducing boot-time serial I/O |
debug |
Lowers the console log level so every kernel message, including KERN_DEBUG, prints |
loglevel=<N> |
Sets an explicit console log level, 0 (emergency only) through 8 (everything) |
panic=<N> |
Seconds to wait before auto-rebooting after a kernel panic; 0 (default) waits forever, negative reboots immediately |
lpj=<N> |
Skips the ~250ms delay-loop calibration by supplying a pre-measured loops_per_jiffy constant |
Console Log Levels: Where Messages Actually Go
Every kernel log message carries a priority, from KERN_EMERG (0) to KERN_DEBUG (7). All
of them are always written into an internal ring buffer called __log_buf, whose size is
2^CONFIG_LOG_BUF_SHIFT bytes — with CONFIG_LOG_BUF_SHIFT=16, that’s a 64 KiB buffer. You
can dump the full buffer at any time with dmesg, regardless of what actually hit the
console.
Whether a message also prints live to the serial console is a separate decision, controlled by
the console log level. Only messages with a priority numerically lower than the console log
level appear on the console. The default console log level is 7, so priority-7 (KERN_DEBUG)
messages are logged to the buffer but suppressed from the live console.
# View current console log level, default level, min level, boot-time default
$ cat /proc/sys/kernel/printk
7 4 1 7
# Change it live without rebooting
$ dmesg -n 5
# Or dump everything captured so far
$ dmesg | tail -20
Hands-On Demo: Measuring the Effect of lpj
During early boot, the kernel spends roughly 250 ms spinning a busy-loop to calibrate a
delay constant called loops_per_jiffy. On real, fixed hardware this value never changes
between boots, so you can precompute it once and skip the calibration entirely on every
subsequent boot.
Boot once normally and capture the calibrated value from the kernel log:
$ dmesg | grep -i bogomips
[ 0.098234] Calibrating delay loop... 996.14 BogoMIPS (lpj=4980736)
Now feed that value back in as a boot parameter on every future boot, either via U-Boot or the
device tree chosen node from earlier:
=> setenv bootargs 'console=ttyAMA0,115200 root=/dev/mmcblk0p2 rootwait rw lpj=4980736'
=> saveenv
Reboot and confirm the calibration step is skipped:
$ dmesg | grep -i bogomips
[ 0.000041] Calibrating delay loop (skipped) already calibrated this CPU
That’s roughly 250 ms saved on every single boot — meaningful on a battery-powered device
that needs to wake, do something quick, and go back to sleep.
Real-World Use Cases
- Fast-boot embedded appliances — combine
quietwith a fixedlpjto cut boot time on
devices where every millisecond of startup matters - Field debugging — temporarily add
debug loglevel=8to see every driver probe message
when diagnosing a board that hangs during boot - Read-only production images —
ro root=/dev/mmcblk0p2mounts the root filesystem
read-only at boot, protecting flash from unclean power loss, with an application-level overlay
mounted read-write afterwards - Initramfs-based recovery images —
rdinit=/bin/recovery-shellboots straight into a
recovery tool instead of the normal init system
Common Mistakes and Troubleshooting
| Symptom | Likely Cause | Fix |
|---|---|---|
| Kernel panics with “VFS: Unable to mount root fs” | Missing rootwait on an MMC/SD root device that isn’t ready the instant the kernel probes it |
Add rootwait, or a generous rootdelay= as a fallback |
| No console output at all | Wrong console= device name or baud rate for the actual UART wired to your debug header |
Double-check against the board schematic or SoC UART numbering, not the Linux device name from a different board |
| bootargs edits in U-Boot have no effect | The device tree’s chosen/bootargs is silently overriding what U-Boot passes, depending on the DT source setup used |
Check /proc/cmdline on the running system to see which string actually won, and edit that source |
Applied an lpj value and boot got slower or CPU-bound code misbehaves |
Reused an lpj value measured on a different board revision or clock configuration |
Recalibrate on the exact hardware and firmware you’re shipping; never copy an lpj value between boards |
Best Practices
- Keep one authoritative bootargs source per board — don’t split the string across both U-Boot
and the device tree, or you’ll spend hours debugging which one won - Always confirm the effective command line via
/proc/cmdlineafter any bootloader or
device tree change — never trust what you think you set - Use
rootwaitrather than a fixedrootdelay=for removable/MMC media — it’s both faster
on a good boot and more robust on a slow one - Only fix an
lpj=value on hardware whose clock configuration is truly locked down for
production; recalibrate whenever you change bootloader clock setup
Security Considerations
Treat the kernel command line as part of your board’s trusted boot chain, not just a
convenience string. An attacker with physical access to a U-Boot prompt can append arbitrary
kernel parameters — including init=/bin/sh, which drops straight to a root shell bypassing
your normal init and any userspace access control. If physical security matters for your product,
lock the U-Boot console with a password or disable interactive boot entirely, and consider a
verified/measured boot chain that authenticates the command line along with the kernel image.
Summary and Key Takeaways
- The kernel command line is a single string, sourced from the bootloader, the device tree, or
CONFIG_CMDLINE, that configures the kernel before any driver runs /proc/cmdlinealways shows you the truth about what the kernel actually received- Console log level (default 7) controls what prints live; the ring buffer
__log_bufalways
captures everything regardless, anddmesgreads it back - A precomputed
lpj=value removes a real, measurable chunk of boot time on fixed hardware rootwait,root=, androotfstype=are the three parameters that make or break booting
from removable/MMC storage
Conclusion
The kernel command line looks like a trivial detail the first time you copy one from a tutorial,
but understanding exactly where it comes from and what each parameter does is what separates
“my board eventually boots” from “I can debug why it doesn’t.” Once you’re comfortable reading
/proc/cmdline, tuning console verbosity, and shaving boot time with lpj, you have real
control over how your embedded Linux system starts up — a core skill in this free linux
device drivers course and any serious embedded Linux bring-up work.
Frequently Asked Questions
What’s the difference between rootwait and rootdelay?
rootwait waits indefinitely, polling until the root device actually appears — it’s the
correct choice for MMC/SD, which can take a variable amount of time to enumerate. rootdelay=N
just sleeps a fixed N seconds regardless of whether the device is ready yet, which wastes time on
a fast boot and can still fail on a slow one.
Does changing the console log level with dmesg -n affect what’s stored in the log buffer?
No. dmesg -n only changes what gets printed live to the console going forward. Every
message at every priority is still written into __log_buf regardless of the console log
level, and you can always retrieve the full history with dmesg.
Can I set the kernel command line without touching the bootloader or device tree?
Yes — CONFIG_CMDLINE in your kernel .config bakes a default string directly into the
kernel image. Depending on CONFIG_CMDLINE_OVERRIDE/CONFIG_CMDLINE_EXTEND settings, this
either gets replaced by or appended to whatever the bootloader supplies.
Is quiet safe to use in production?
Generally yes, and it’s recommended for boot-time-sensitive products — it suppresses routine
console spam, not the underlying log buffer. Just make sure you still have a way to retrieve
dmesg output in the field (over SSH, a debug UART, or a log-collection agent) for when you
actually need to diagnose a problem.
Why does my lpj value change after a bootloader update?
Because loops_per_jiffy calibration depends on the CPU clock speed at the moment the
kernel measures it. If your bootloader update changes PLL/clock-tree configuration before jumping
to the kernel, the effective CPU frequency changes too, and any previously fixed lpj= value
becomes stale. Recalibrate after any change to early clock setup.
What happens if I set an invalid or nonsensical kernel parameter?
Unrecognised parameters that don’t match any registered kernel option are simply passed through
to init as environment-style arguments and otherwise ignored by the kernel itself — they
won’t normally crash the boot, but a typo in a parameter the kernel does recognise (like a bad
root= device name) will cause a boot failure such as an unmountable root filesystem.
Want to keep going?
Continue this free embedded Linux course with the next lecture on porting Linux to a new board.
