Linux Kernel Logging with printk(): Log Levels, pr_*() & dmesg

Kernel Logging with printk – Free Linux Kernel Programming Course | EmbeddedPathashala

Kernel Logging with printk
Part 3 — Log Levels, pr_*() Macros, dmesg & Production Makefile | Linux 6.x
🆓 100% Free
🔧 Practical
🐧 Linux 6.x
⏱ ~20 min read
🔑 Key Topics in This Part
printk() pr_info / pr_err / pr_warn Log Level Constants dmesg command console_loglevel Rate-Limited Logging pr_fmt() prefix Better Makefile

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.

Path of a Kernel Log Message
Your code calls
pr_info("Hello")
Kernel Ring Buffer
/dev/kmsg (circular, in kernel memory)
Console output
(if level < console_loglevel)
dmesg command
Reads /dev/kmsg, formats and displays buffer contents
journald (systemd)
Also reads /dev/kmsg, stores persistently
/var/log/syslog
Via syslog daemon (traditional systems)

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).

Kernel Log Level Reference
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
Memory aid: Think of the levels like urgency at a hospital. Level 0 (EMERG) is a cardiac arrest — everything stops. Level 3 (ERR) is a broken bone — serious, needs attention. Level 6 (INFO) is routine check-in. Level 7 (DEBUG) is a researcher writing in a lab notebook.

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);
Best Practice: Always define 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 Filtering — Which Messages Reach the Console
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 */
}
Why this matters: A log flood is not just an annoyance — the ring buffer has a fixed size. If a flood overwrites older messages, you lose the diagnostic information you actually need. Rate-limited variants print a summary like “X callbacks suppressed” after the rate limit kicks in, so you know messages were dropped.

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 */
No floating point in kernel code. The kernel does not save and restore FPU state on context switches by default (for performance). Using floating point in a kernel module corrupts user-space FPU state. Use fixed-point arithmetic instead.

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
Dynamic Debug requires 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
Build Output Files — What Each One Is
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

Q1. What is the kernel ring buffer and what happens when it fills up?
The kernel ring buffer is a fixed-size circular buffer in kernel memory where all printk messages are stored. When it fills up, new messages overwrite the oldest ones. Its size is controlled by CONFIG_LOG_BUF_SHIFT (default is typically 128KB to 1MB on modern kernels). You can read it via /dev/kmsg or with the dmesg command.
Q2. Why is pr_debug() not visible by default even when you call it in your module?
pr_debug() is conditionally compiled — it expands to nothing unless the DEBUG macro is defined at compile time, or unless CONFIG_DYNAMIC_DEBUG is enabled. When DEBUG is not defined, the pr_debug() call literally disappears from the compiled binary, generating zero overhead. Enable it by adding -DDEBUG to ccflags-y in your Makefile, or use dynamic debug at runtime via /sys/kernel/debug/dynamic_debug/control.
Q3. What is console_loglevel and how does it differ from the ring buffer?
The ring buffer stores every printk message regardless of level. console_loglevel is a threshold — only messages with a level number strictly less than this value are also printed to the console (serial port or VGA terminal) in real time. If console_loglevel is 4, only KERN_EMERG (0), KERN_ALERT (1), KERN_CRIT (2), and KERN_ERR (3) appear on screen. The ring buffer always receives everything.
Q4. When would you use pr_err_ratelimited() instead of pr_err()?
Whenever a print statement could be triggered at very high frequency — inside interrupt handlers, timer callbacks, network receive paths, or any hot code path. Without rate limiting, a burst of errors could fill the ring buffer in milliseconds and consume significant CPU time in logging overhead. pr_err_ratelimited() automatically limits the print rate to roughly 10 messages per 5 seconds and logs how many were suppressed.
Q5. What does obj-m mean in a kernel module Makefile?
obj-m tells the kbuild build system to compile the specified .c files as an out-of-tree loadable kernel module (producing a .ko file). The “m” stands for “module.” In contrast, obj-y compiles files as part of a built-in kernel object. The kbuild system uses this variable to determine build targets, compiler flags, and how to link the final binary.
Q6. What does make clean do in a kernel module build directory?
It removes all build artifacts generated during compilation: the .ko binary, .o object files, .mod.c generated metadata files, .mod.o compiled metadata, Module.symvers (symbol versions), modules.order (build order list), and the .tmp_versions/ temporary directory. It does not touch your source .c files or Makefile.

✅ 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’ve completed the LKM fundamentals series!

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 2

EmbeddedPathashala — Free Linux Kernel Programming Course | Updated for Linux 6.x

Leave a Reply

Your email address will not be published. Required fields are marked *