When your Linux kernel module needs to send a message — whether it is a critical hardware failure or a simple debug trace — it uses printk(). Unlike printf() in userspace programs, printk() works inside the kernel and supports a priority system called log levels.
Understanding log levels is one of the most important skills for any Linux kernel developer or embedded Linux engineer. This tutorial explains every log level, how the kernel decides what to print on the console, and how to control it for your development workflow.
What is printk and Why Does It Have Log Levels?
Inside the Linux kernel, there is no standard library. You cannot call printf(). The kernel has its own logging function: printk(). Every message sent through printk() carries a log level that tells the kernel how urgent or important that message is.
Think of it like this: a fire alarm is more urgent than a daily status report. The kernel needs to know which messages should immediately appear on the system console (like a fire alarm) and which can just be quietly stored in the log buffer (like the status report).
The pr_* Helper Macros — What You Should Actually Use
Calling printk() directly with a log level string like KERN_ERR works, but it is verbose. The Linux kernel provides a cleaner set of helper macros — one for each log level — that are preferred in modern kernel code.
| Macro | Log Level | Numeric Value | When to Use |
|---|---|---|---|
| pr_emerg() | KERN_EMERG | 0 | System is unusable — kernel panic territory |
| pr_alert() | KERN_ALERT | 1 | Immediate action needed by an operator |
| pr_crit() | KERN_CRIT | 2 | Critical hardware or software failure |
| pr_err() | KERN_ERR | 3 | Driver or subsystem error — recoverable |
| pr_warn() | KERN_WARNING | 4 | Something suspicious happened but system is OK |
| pr_notice() | KERN_NOTICE | 5 | Normal events worth logging (e.g., device registered) |
| pr_info() | KERN_INFO | 6 | General operational messages — most commonly used |
| pr_debug() | KERN_DEBUG | 7 | Detailed debug info — only active with DEBUG defined |
| pr_devel() | KERN_DEBUG | 7 | Same as pr_debug() but only in development builds |
These two macros are compiled out completely unless
DEBUG is defined for your module or CONFIG_DYNAMIC_DEBUG is enabled in your kernel configuration. Even if your console log level is set to 8, you will not see their output unless the kernel was built with debugging enabled for that module. This is a common source of confusion for beginners.
Writing a Kernel Module That Uses All Log Levels
Here is a complete, minimal kernel module that demonstrates all the pr_* macros. Each pr_* call goes to the kernel log buffer and potentially to the console, depending on the current log level setting.
// File: loglevel_demo.c
// Purpose: Demonstrate all printk log levels in Linux kernel 6.x
// EmbeddedPathashala - Free Linux Kernel Programming Course
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("printk log level demonstration module");
MODULE_VERSION("1.0");
static int __init loglevel_demo_init(void)
{
pr_emerg ("EP Demo: KERN_EMERG [0] - System unusable\n");
pr_alert ("EP Demo: KERN_ALERT [1] - Act immediately\n");
pr_crit ("EP Demo: KERN_CRIT [2] - Critical condition\n");
pr_err ("EP Demo: KERN_ERR [3] - Error occurred\n");
pr_warn ("EP Demo: KERN_WARNING[4] - Warning\n");
pr_notice("EP Demo: KERN_NOTICE [5] - Normal but notable\n");
pr_info ("EP Demo: KERN_INFO [6] - Informational\n");
pr_debug ("EP Demo: KERN_DEBUG [7] - Debug (needs DEBUG)\n");
pr_devel ("EP Demo: pr_devel() [7] - Dev build only\n");
return 0;
}
static void __exit loglevel_demo_exit(void)
{
pr_info("EP Demo: Module unloaded successfully\n");
}
module_init(loglevel_demo_init);
module_exit(loglevel_demo_exit);
Save the file, create a Makefile, then run
make followed by sudo insmod loglevel_demo.ko. View the output with dmesg | tail -20.
How the Kernel Console Decides Which Messages to Display
Not every printk() message appears on the console (the terminal or serial port you are looking at). The kernel uses a threshold called the current console log level to decide what shows up in real time.
The rule is simple: if a message’s log level number is less than the current console log level, it appears on the console. Messages at or above the threshold go only into the kernel ring buffer (readable via dmesg).
dmesg
Reading and Understanding /proc/sys/kernel/printk
The kernel exposes its current log level settings through a proc pseudo-file. You can read it at any time from your terminal:
$ cat /proc/sys/kernel/printk
4 4 1 7
There are exactly four numbers separated by spaces. Each number has a specific meaning:
The Kernel Ring Buffer and dmesg — Your Best Friend for Debugging
Every printk() message — regardless of log level — is stored in a fixed-size memory region called the kernel ring buffer. When the buffer fills up, older messages are overwritten. The dmesg command lets you read this buffer from userspace.
# View all kernel messages
dmesg
# View only the last 20 lines
dmesg | tail -20
# Watch kernel messages live as they come in (Linux 3.5+ / util-linux 2.21+)
dmesg -w
# Show messages with human-readable timestamps
dmesg -T
# Filter by log level — show only errors and above
dmesg --level=err,crit,alert,emerg
# Clear the ring buffer (requires root)
sudo dmesg -C
dmesg -w in a separate terminal window before loading your module. This way, you see all kernel messages in real time as insmod runs your init function.
The dev_* Family — Log Levels for Device Drivers
When you write an actual device driver, you typically have access to a struct device *dev pointer. In that case, prefer the dev_* macros over the plain pr_* macros. These automatically include the device name in the log message, making it much easier to identify which device generated the log output in a system with multiple devices.
// dev_* macros automatically prepend the device name to the message
// Example: "my_driver my_device: Probe successful"
dev_emerg(dev, "Critical failure in device %s\n", dev_name(dev));
dev_err (dev, "Register read failed: error %d\n", ret);
dev_warn (dev, "Unexpected state, resetting...\n");
dev_info (dev, "Probe successful, IRQ=%d\n", irq);
dev_dbg (dev, "Register dump: 0x%08x\n", reg_val);
The dev_dbg() macro, like pr_debug(), is compiled out unless DEBUG is defined. In production drivers, use dev_info() and dev_err() for the most important messages and keep verbose logging under dev_dbg().
The Special Rule: KERN_EMERG Always Gets Through
There is one unconditional rule in the kernel’s logging system: a message at level KERN_EMERG (0) always appears on the console and on every open terminal window, no matter what the current console log level is set to. This cannot be overridden.
This makes sense: if the kernel is about to crash or has detected a catastrophic hardware failure, you need to know about it immediately regardless of how the system is configured. This is also why kernel panics always produce visible output on the console.
pr_emerg() for routine messages in your drivers just to make them appear on the console. Reserve it for genuine system-level catastrophes. Misusing high-priority log levels makes real emergencies harder to spot.
Interview Questions — printk Log Levels
/proc/sys/kernel/printk. On most systems this defaults to 4 (KERN_WARNING).pr_debug() is conditionally compiled. Unless the macro DEBUG is defined for that translation unit, or CONFIG_DYNAMIC_DEBUG is enabled in the kernel build, the macro expands to an empty no-op and never generates any code at all.dev_err() takes a struct device * pointer and automatically prepends the device name to the log message. This is very helpful in drivers that manage multiple device instances because you can immediately identify which device produced the error.printk() is safe to call from interrupt context, softirqs, and tasklets. It uses a lock-free ring buffer internally to handle this. However, frequently calling printk from interrupt context can impact interrupt latency, so use it only for debugging in production drivers.CONFIG_LOG_BUF_SHIFT. The actual size is 2 ^ CONFIG_LOG_BUF_SHIFT bytes. When the buffer is full, new messages overwrite the oldest ones. You can see the current buffer size with dmesg --buffer-size.Continue Your Free Linux Kernel Programming Journey
EmbeddedPathashala offers 100% free courses on Linux Kernel Programming, Linux Device Drivers, Bluetooth/BLE, and Embedded Systems.
Visit EmbeddedPathashala