Beginner to Intermediate
Linux 6.x (LTS)
100% Free
What You Will Learn
When you write a Linux kernel module, you cannot use printf() like you would in a normal C program. The kernel runs in a completely separate world — there is no standard C library, no file descriptors, and no terminal attached. Instead, the kernel provides its own logging API called printk.
In this tutorial, you will deeply understand how printk works internally, where kernel messages go, how to control their priority using log levels, how to read them using dmesg, and the real limitations you must work around as a kernel developer. All examples are verified against Linux 6.x.
1. printk vs printf — The Core Difference
Many beginners assume printk is just a kernel version of printf. The format string syntax is similar, but everything else is fundamentally different. Let’s understand why.
When you call printf() in a user-space C program, here is what happens internally:
Now look at what happens when you call printk() inside a kernel module:
__log_buf[] in RAM
/var/log/syslog etc.
ttyS0 / VGA / UART
A basic printk call in a kernel module looks like this:
#include <linux/printk.h>
/* Inside your module's init function */
printk(KERN_INFO "EmbeddedPathashala: module loaded successfully\n");
Notice that KERN_INFO and the string are not separated by a comma. They are two adjacent string literals that the C compiler automatically concatenates into one. This is intentional — the log level is embedded directly into the format string as a prefix.
2. Kernel Log Levels — Controlling Priority
Every printk call must specify a log level. The log level tells the kernel how important or urgent the message is. Think of it like email priority — you can mark an email as low importance or urgent. The kernel uses these levels to decide whether to display the message on the console immediately, or just save it quietly to the log buffer.
Linux 6.x defines 8 log levels, numbered from 0 (most critical) to 7 (most verbose):
| Macro | Numeric Level | When to Use |
|---|---|---|
KERN_EMERG | 0 | System is unusable — total crash imminent |
KERN_ALERT | 1 | Immediate action required by operator |
KERN_CRIT | 2 | Critical hardware or software failure |
KERN_ERR | 3 | Error condition — driver/subsystem error |
KERN_WARNING | 4 | Warning — something unusual, not yet broken |
KERN_NOTICE | 5 | Normal but significant event to record |
KERN_INFO | 6 | General informational messages |
KERN_DEBUG | 7 | Debug-level detail — only useful during development |
These macros are defined in <linux/kern_levels.h>, which is included via <linux/printk.h>. In Linux 6.x, each macro expands to a short string like “\001” “6” — a SOH (Start of Header) byte followed by the digit. The kernel’s internal log parser strips this prefix before displaying the message.
/* Correct usage — one printk per severity */
printk(KERN_EMERG "System is going down NOW!\n");
printk(KERN_ERR "Failed to allocate DMA buffer\n");
printk(KERN_WARNING "Unexpected GPIO state detected\n");
printk(KERN_INFO "BLE stack initialized on hci0\n");
printk(KERN_DEBUG "register value = 0x%08x\n", reg_val);
Instead of writing printk(KERN_INFO “…”), you can use level-specific wrappers that are shorter and cleaner:
pr_info("Module loaded\n");pr_err("Failed with error %d\n", ret);pr_warn("Buffer almost full\n");pr_debug("Loop counter = %d\n", i);These are the recommended style in modern kernel code. They map exactly to the corresponding KERN_* levels.
2.1 Console Log Level — What Gets Printed to the Screen
Here is something critical to understand: not all printk messages appear on the console (your terminal or serial port). The kernel maintains a console log level threshold. Only messages whose priority number is lower than (i.e., more critical than) this threshold are printed to the console in real time.
You can check and change the console log level at runtime:
# Read current log level settings
# Format: current | default | minimum | boot-time-default
$ cat /proc/sys/kernel/printk
4 4 1 7
# Change console log level to 7 (show all messages including KERN_DEBUG)
$ echo 7 > /proc/sys/kernel/printk
# Or use dmesg -n
$ dmesg -n 7
3. The Kernel Ring Buffer — Where Messages Actually Live
Every printk message is first written to a special area in kernel memory called the kernel ring buffer. This is not a regular file — it is a fixed-size circular memory buffer allocated in the kernel’s own address space when the system boots.
3.1 How the Ring Buffer Works
The ring buffer is implemented as a global character array in the kernel source at kernel/printk/printk.c. It has a fixed maximum size. Once the buffer fills up completely, new messages start overwriting the oldest ones from the beginning. This is why it is called a “ring” — it wraps around in a circle.
__log_buf[]
256 KB default
3.2 Buffer Size and Kernel Configuration
The ring buffer size is controlled by a kernel configuration option called CONFIG_LOG_BUF_SHIFT. The actual size in bytes is calculated as 2 raised to the power of this value.
/* From kernel/printk/printk.c */
#define __LOG_BUF_LEN (1 << CONFIG_LOG_BUF_SHIFT)
static char __log_buf[__LOG_BUF_LEN] __aligned(LOG_ALIGN);
| CONFIG_LOG_BUF_SHIFT value | Buffer Size | Typical Use |
|---|---|---|
| 17 | 128 KB | Minimal embedded systems |
| 18 (default) | 256 KB | Standard desktop/server kernels |
| 20 | 1 MB | Systems with heavy kernel logging |
| 21 | 2 MB | Large SMP / debug builds |
3.3 Overriding Buffer Size at Boot Time
You do not need to recompile the kernel to change the ring buffer size. You can pass a kernel boot parameter via the bootloader (GRUB, U-Boot, etc.):
# In GRUB config (/etc/default/grub), add to GRUB_CMDLINE_LINUX:
log_buf_len=4M
# The value must be a power of 2: 1M, 2M, 4M, 8M, etc.
# Apply changes:
sudo update-grub
This boot parameter overrides CONFIG_LOG_BUF_SHIFT completely at runtime, giving you a larger ring buffer without needing to rebuild the kernel.
3.4 The Two Big Limitations of the Ring Buffer
The ring buffer lives in RAM. If your system crashes, panics, or loses power at any point, all messages in the buffer are permanently lost. This is a serious problem during kernel debugging because a crash often destroys the very log that would explain what caused it.
At only 256 KB by default, the ring buffer fills up quickly when you have verbose printk statements. Once it overflows, older messages are silently overwritten. You lose the early-boot messages that are often the most important for debugging driver initialization.
Both of these limitations are solved by persistent logging — covered in detail in Part 2 of this tutorial.
4. Reading Kernel Logs with dmesg
The dmesg command is the primary tool for reading the kernel ring buffer from user space. It reads the contents of /dev/kmsg (Linux 6.x) and displays all messages currently in the ring buffer.
# Show all messages in the ring buffer
$ dmesg
# Show with human-readable timestamps
$ dmesg -T
# Show only the last 20 lines
$ dmesg | tail -20
# Follow in real-time (like tail -f)
$ dmesg -w
# Filter by log level — show only errors and above
$ dmesg --level=err,crit,alert,emerg
# Clear the ring buffer (requires root)
$ sudo dmesg -C
# Show with color-coded severity
$ dmesg -L
[Wed Jun 18 10:23:41 2025] ep_hello: Driver version 1.0 — init complete
[Wed Jun 18 10:24:01 2025] ep_hello: ERROR: IRQ registration failed, ret=-16
[Wed Jun 18 10:25:10 2025] ep_hello: EmbeddedPathashala: module removed
4.1 Reading /proc/kmsg directly
An alternative to dmesg is reading /proc/kmsg directly. Unlike dmesg (which reads a snapshot), /proc/kmsg is a blocking read — it waits for new messages and streams them as they arrive, similar to tail -f. Only one process can open it at a time.
# Stream kernel messages live (only one reader allowed)
$ sudo cat /proc/kmsg
# On Linux 6.x, /dev/kmsg is preferred:
$ sudo cat /dev/kmsg
5. Writing Effective printk Messages
Kernel logs are read by many people — maintainers, users, embedded engineers debugging crashes. Writing clear, informative messages is a skill. Here are the patterns used in production kernel code:
#include <linux/module.h>
#include <linux/printk.h>
#include <linux/init.h>
#define DRIVER_NAME "ep_uart"
#define DRIVER_VER "1.0.0"
static int __init ep_driver_init(void)
{
/* Always identify your module and version */
pr_info("%s: EmbeddedPathashala UART driver v%s loaded\n",
DRIVER_NAME, DRIVER_VER);
/* Report errors with meaningful context */
int ret = setup_hardware();
if (ret < 0) {
pr_err("%s: Hardware setup failed, errno=%d\n",
DRIVER_NAME, ret);
return ret;
}
/* Warnings for non-fatal abnormal conditions */
if (buffer_size < MIN_BUFFER) {
pr_warn("%s: Buffer size %d is below recommended minimum %d\n",
DRIVER_NAME, buffer_size, MIN_BUFFER);
}
/* Debug info — only visible when KERN_DEBUG level is active */
pr_debug("%s: IRQ=%d, base_addr=0x%lx\n",
DRIVER_NAME, irq_num, base_address);
return 0;
}
• Always include the module/driver name as a prefix so the log message is identifiable
• Always end with \n — the kernel does not add newlines automatically
• Never use printk without a log level — the default fallback level is KERN_WARNING, which can be confusing
• Use pr_fmt macro to set a module-wide prefix so you don’t repeat the driver name in every printk call
/* Define pr_fmt BEFORE including linux/printk.h */
/* This prefix is automatically prepended to all pr_* calls */
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/printk.h>
/* Now you don't need to manually add the module name */
pr_info("Initialized successfully\n");
/* Produces: "ep_uart: Initialized successfully" */
pr_err("DMA mapping failed: %d\n", ret);
/* Produces: "ep_uart: DMA mapping failed: -12" */
