When you write code in user space, you use printf() to print output to the terminal. Inside the kernel, there is no terminal and no stdout. Instead, the kernel has its own logging infrastructure built around printk(). This part covers everything you need to know about kernel logging — from basic usage to advanced patterns used in production drivers.
1. How Kernel Logging Works
When you call printk() (or any of its wrappers), the message is written to the kernel ring buffer — a circular buffer in kernel memory. It is called a “ring buffer” because once it fills up, new messages overwrite the oldest ones.
Your code callspr_info("Hello")
|
→ |
Kernel Ring Buffer /dev/kmsg (circular, in kernel memory) |
→ |
Console output (if level < console_loglevel) |
|||||
|
|||||||||
Three ways to read the kernel log:
# Most common: dmesg (reads the ring buffer directly)
dmesg
dmesg | tail -20 # last 20 lines
dmesg -w # follow in real time (like tail -f)
dmesg -T # show human-readable timestamps
dmesg -l info,err # filter by log level
# systemd journal (persistent across reboots)
journalctl -k # kernel messages only
journalctl -k -f # follow kernel messages live
# Raw ring buffer
cat /dev/kmsg
2. Log Levels — How the Kernel Classifies Messages
Every printk message has a log level — a number from 0 to 7 that indicates the severity of the message. Level 0 is the most severe (emergency), level 7 is the least (debug).
| Macro | Level | pr_*() Wrapper | When to Use |
|---|---|---|---|
KERN_EMERG |
0 | pr_emerg() |
System is unusable, about to crash |
KERN_ALERT |
1 | pr_alert() |
Immediate action required |
KERN_CRIT |
2 | pr_crit() |
Critical hardware or software failures |
KERN_ERR |
3 | pr_err() |
Error conditions (I/O errors, failed ops) |
KERN_WARNING |
4 | pr_warn() |
Warnings about unexpected situations |
KERN_NOTICE |
5 | pr_notice() |
Normal but significant events |
KERN_INFO |
6 | pr_info() |
Informational — hardware probed, module loaded |
KERN_DEBUG |
7 | pr_debug() |
Debug — compiled out unless DEBUG is defined |
3. printk() vs pr_*() — Which Should You Use?
You can use either printk() directly or the pr_*() wrapper macros. In modern Linux 6.x driver code, the convention is to use the wrappers. Here is why:
/* Old style — printk() with level string concatenated */
printk(KERN_INFO "mydriver: device found at address 0x%x\n", addr);
/* Modern style — pr_info() wrapper — cleaner, same result */
pr_info("mydriver: device found at address 0x%x\n", addr);
/* pr_err for errors */
pr_err("mydriver: failed to map registers, error %d\n", ret);
/* pr_warn for warnings */
pr_warn("mydriver: timeout waiting for hardware, retrying\n");
/* pr_debug — compiled out unless DEBUG is defined */
pr_debug("mydriver: entering state machine, state=%d\n", state);
Both approaches produce identical kernel log entries. The pr_*() family is preferred because it is shorter and supports the pr_fmt() prefix mechanism cleanly.
4. Adding a Consistent Prefix with pr_fmt()
When reading kernel logs with dmesg, you want every message from your module to be immediately identifiable. The pr_fmt() macro lets you define a prefix that automatically appears before every pr_*() call in your file.
/*
* Define pr_fmt() BEFORE any #include directives.
* KBUILD_MODNAME is automatically set to your module's name by kbuild.
*/
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/module.h>
#include <linux/init.h>
#include <linux/printk.h>
MODULE_LICENSE("GPL");
static int __init mymod_init(void)
{
pr_info("module loaded\n");
/* Produces: "mymod: module loaded" in dmesg */
pr_warn("no device found\n");
/* Produces: "mymod: no device found" in dmesg */
return 0;
}
static void __exit mymod_exit(void)
{
pr_info("module unloaded\n");
/* Produces: "mymod: module unloaded" in dmesg */
}
module_init(mymod_init);
module_exit(mymod_exit);
pr_fmt() at the top of every kernel source file. It saves you from repeating the module name in every print statement and makes log filtering much easier — you can do dmesg | grep "mymod:" to see only your module’s messages.
5. The console_loglevel — Controlling What Appears on Screen
The ring buffer stores all messages regardless of level. But the serial console (or virtual terminal) only prints messages whose level is numerically lower than console_loglevel. This is confusing because lower number = higher priority:
# Read current log level settings:
cat /proc/sys/kernel/printk
# Output: 4 4 1 7
# Fields: current_loglevel default_msg_level min_console_loglevel boot_time_loglevel
# Only messages with level < 4 (KERN_WARNING and above) appear on console.
# pr_info (level 6) would NOT appear on the console with this setting.
# Temporarily print ALL messages including debug to console:
echo 8 | sudo tee /proc/sys/kernel/printk
# Show only critical and above on console:
echo 3 | sudo tee /proc/sys/kernel/printk
# Or use dmesg's -n option:
sudo dmesg -n 7 # same as echo 7 > /proc/sys/kernel/printk
| console_loglevel = 4 (default on most systems) | |||||||
| EMERG 0 ✅ shown |
ALERT 1 ✅ shown |
CRIT 2 ✅ shown |
ERR 3 ✅ shown |
WARN 4 ❌ hidden |
NOTICE 5 ❌ hidden |
INFO 6 ❌ hidden |
DEBUG 7 ❌ hidden |
| level < 4 → printed to console | level ≥ 4 → ring buffer only (use dmesg) | ||||||
6. Rate-Limited Logging — Avoiding Log Floods
In device drivers, logging inside interrupt handlers, timers, or hot paths can flood the ring buffer with thousands of identical messages per second. The kernel provides rate-limited variants of all pr_*() macros for exactly this situation.
#include <linux/ratelimit.h>
/* Rate-limited variants — automatically limits print frequency */
static irqreturn_t my_interrupt_handler(int irq, void *dev_id)
{
/* BAD: This could print millions of times per second */
/* pr_err("interrupt error condition\n"); */
/* GOOD: rate-limited version — max ~10 times every 5 seconds */
pr_err_ratelimited("interrupt: error condition detected\n");
return IRQ_HANDLED;
}
/* One-time print variants — print only the first time */
static void check_hardware(void)
{
/* Prints only once, even if called many times */
pr_warn_once("hardware quirk detected — applying workaround\n");
/* Also available: pr_info_once, pr_err_once, pr_debug_once */
}
7. Format Specifiers — Kernel vs Standard C
printk() supports most standard C printf format specifiers but with some important differences for kernel-specific types:
/* Standard types — same as printf */
pr_info("integer: %d\n", my_int);
pr_info("unsigned: %u hex: %x\n", my_uint, my_uint);
pr_info("string: %s\n", my_string);
pr_info("long: %ld unsigned long: %lu\n", my_long, my_ulong);
/* Kernel-specific — do NOT use %d for these */
/* size_t and ssize_t — use %zu and %zd */
size_t len = sizeof(my_struct);
pr_info("size: %zu bytes\n", len);
/* Pointers — use %p (kernel hashes address for security) */
pr_info("pointer: %p\n", my_ptr);
/* Physical addresses (phys_addr_t, dma_addr_t) — use %pa */
phys_addr_t paddr = virt_to_phys(my_buffer);
pr_info("physical address: %pa\n", &paddr); /* note: pass by reference */
/* Resource size (resource_size_t) — use %pr */
/* IPv4 address from u32 — use %pI4 */
/* IPv6 address — use %pI6c */
/* NEVER use %f or %e — floating point is forbidden in kernel code */
8. pr_debug() and Dynamic Debug
pr_debug() is special. Unlike other log level macros, it is compiled out entirely unless one of these conditions is met:
/* pr_debug() is compiled out by default.
To enable it for your module, add this to your Makefile: */
ccflags-y += -DDEBUG
/* Or define it before includes in your .c file: */
/* #define DEBUG */
/* (place this BEFORE any #include to activate pr_debug globally) */
In Linux 6.x, there is a more powerful mechanism: Dynamic Debug. It lets you enable/disable specific debug messages at runtime without recompiling, using the debugfs interface:
# Enable pr_debug() for a specific module at runtime
echo 'module mymod +p' | sudo tee /sys/kernel/debug/dynamic_debug/control
# Enable pr_debug() for a specific file
echo 'file mymod.c +p' | sudo tee /sys/kernel/debug/dynamic_debug/control
# Enable for a specific function
echo 'func mymod_init +p' | sudo tee /sys/kernel/debug/dynamic_debug/control
# Disable again
echo 'module mymod -p' | sudo tee /sys/kernel/debug/dynamic_debug/control
# Check what is currently enabled
cat /sys/kernel/debug/dynamic_debug/control | grep mymod
CONFIG_DYNAMIC_DEBUG=y in the kernel config, which is enabled in most distribution kernels. It is extremely useful in production — you can enable verbose debug logging on a live system to diagnose a problem, then disable it without rebooting.
9. A Production-Quality Kernel Module Makefile
The basic Makefile from Part 2 works fine for learning. For real development, you want a Makefile that also runs kernel coding style checks, static analysis, and handles cross-compilation cleanly. Here is a practical starting point:
# Production Makefile for out-of-tree kernel module development
# Works with Linux 6.x
# Module name (no .ko suffix here)
MODULE_NAME := mydriver
# Source files (space-separated for multiple .c files)
obj-m := $(MODULE_NAME).o
# For a module built from multiple .c files:
# $(MODULE_NAME)-objs := file1.o file2.o file3.o
# Kernel build directory (for the currently running kernel)
KDIR := /lib/modules/$(shell uname -r)/build
# Your module source directory
PWD := $(shell pwd)
# Optional: add debug prints (-DDEBUG enables pr_debug)
# ccflags-y += -DDEBUG
# Optional: enable extra compiler warnings (highly recommended)
# ccflags-y += -Wall -Wextra
.PHONY: all clean install checkpatch
# Default target: build the module
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
# Remove all build artifacts
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
# Install module to /lib/modules/$(uname -r)/extra/ and run depmod
install: all
$(MAKE) -C $(KDIR) M=$(PWD) modules_install
sudo depmod -a
# Run kernel coding style checker (requires checkpatch.pl from kernel source)
checkpatch:
$(KDIR)/scripts/checkpatch.pl --no-tree -f *.c *.h
| File | What It Is | Keep It? |
|---|---|---|
mydriver.ko |
The final kernel module binary — this is what you load | ✅ Yes |
mydriver.o |
Compiled object file from your .c source | Build artifact |
mydriver.mod.c |
Auto-generated C file containing module metadata (license, vermagic, etc.) | Build artifact |
mydriver.mod.o |
Compiled form of mydriver.mod.c | Build artifact |
Module.symvers |
Symbol version file — needed if other modules depend on yours | If exporting symbols |
modules.order |
Lists modules in build order — used by kbuild | Build artifact |
.tmp_versions/ |
Temporary directory used during build, discarded after | Build artifact |
Run make clean to remove all build artifacts listed above.
10. Cross-Compiling a Module for Embedded Targets
When writing drivers for embedded boards (like a Raspberry Pi, BeagleBone, or custom ARM SoC), you often build on an x86 development host but target an ARM device. This is cross-compilation:
# Cross-compilation Makefile additions:
# Target architecture
ARCH := arm64
# Cross-compiler prefix (install arm64 toolchain first)
CROSS_COMPILE := aarch64-linux-gnu-
# Path to the kernel build tree for the target (not the host kernel!)
# This must be pre-built or the vendor SDK for your target board
KDIR := /path/to/target/kernel/build
all:
$(MAKE) -C $(KDIR) \
ARCH=$(ARCH) \
CROSS_COMPILE=$(CROSS_COMPILE) \
M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) \
ARCH=$(ARCH) \
CROSS_COMPILE=$(CROSS_COMPILE) \
M=$(PWD) clean
# Install cross-compilation toolchain on Ubuntu:
sudo apt install gcc-aarch64-linux-gnu
# Verify:
aarch64-linux-gnu-gcc --version
11. Practical dmesg Tips for Module Development
# Clear the ring buffer before testing (so you see only your module's messages)
sudo dmesg -C
# Load your module
sudo insmod mydriver.ko
# See all messages from your module:
dmesg | grep "mydriver:"
# Watch messages in real time as you test:
dmesg -w &
sudo insmod mydriver.ko
# Show timestamps in human-readable format:
dmesg -T | grep "mydriver:"
# Save full dmesg to a file for analysis:
dmesg > /tmp/kernel_log_$(date +%Y%m%d_%H%M%S).txt
# Show messages since last boot (useful after crash testing on a VM):
journalctl -k -b 0 # current boot
journalctl -k -b -1 # previous boot
🎯 Interview Questions — printk and Kernel Logging
✅ Part 3 Summary — What You Now Know
- printk() writes to the kernel ring buffer, readable with dmesg or /dev/kmsg
- 8 log levels from KERN_EMERG (0) to KERN_DEBUG (7) — lower number = higher severity
- Use pr_info(), pr_err(), pr_warn() wrappers instead of raw printk() in modern code
- Define pr_fmt() at the top of each file to add a consistent prefix to all messages
- console_loglevel controls which levels appear on the console — the ring buffer gets everything
- Use pr_*_ratelimited() in hot paths to prevent log flooding
- pr_debug() is compiled out by default; enable with -DDEBUG or dynamic debug
- A production Makefile adds checkpatch support and a clean install target
- Run make clean to remove all build artifacts — only the .ko matters for deployment
You can now write, build, load, and debug Linux kernel modules on Linux 6.x. The next step is learning about character device drivers — the foundation of hardware interaction in the kernel.
← Back to Part 1 ← Back to Part 2EmbeddedPathashala — Free Linux Kernel Programming Course | Updated for Linux 6.x
