R0KernelPath
module 094 / 262

Writing Your First Real Driver

Build a complete Linux character driver from zero — probe, sysfs, read/write, ioctl, and testing — and understand exactly why every line exists.

~18h focused study11 concepts

Overview

Writing a Linux device driver is the moment when theory becomes craft. Up until now you may have read about kernel data structures, memory management, and the VFS — but a driver is where all of that clicks into place. A driver is the kernel's ambassador to a piece of hardware (or a virtual device): it speaks the hardware's language on one side and the kernel's language on the other.

This module walks you through building a complete, real, loadable kernel module that exposes a character device, registers sysfs attributes, implements the open/read/write/ioctl file operations, handles probe and remove lifecycle correctly, and survives rmmod without panicking. We use a virtual "echo buffer" device so you need zero hardware — just a Linux machine or VM. Every concept is anchored to real, compilable kernel C.

By the end you will have written something that actually appears in /dev, that you can read and write with standard shell tools, and whose internal state you can inspect via /sys. That is not a toy — that is exactly how real production drivers in the kernel tree are structured, only simpler in scope.

The path through this module is: understand the driver model (how the kernel finds drivers), write the module skeleton, register a char device, wire up file operations, add sysfs attributes, test it end-to-end with a Makefile and a small test script, and then understand what would change when real hardware is involved. Each step builds on the last. Code is shown in the order you would actually write it.

why this matters for the kernel

The Linux driver subsystem is one of the most heavily contributed areas of the kernel — roughly two-thirds of all kernel code is drivers. Learning to write a driver means you understand the full stack: you touch kobjects, the unified device model, reference counting, interrupt handling, DMA, memory-mapped I/O, and the VFS simultaneously. Crucially, driver bugs live in kernel space: a use-after-free in your driver is a system crash, not a segfault you can catch. That pressure forces you to develop the discipline — careful resource tracking, symmetric init/exit, correct locking — that separates kernel contributors from userspace programmers.

Contributing a driver is also one of the most accessible entry points into the kernel community. The bar for a new staging driver, a device-tree binding fix, or a missing feature in an existing driver is lower than touching core scheduler or memory management code. Many long-term kernel maintainers started by scratching an itch with a piece of hardware they owned. This module gives you the skeleton, the vocabulary, and the habits to write something real and eventually get it reviewed on the linux-kernel mailing list.

Echo Buffer Driver: Lifecycle and Data Flow

rendering diagram…

Full lifecycle of the echo buffer driver: from insmod through open/read/write and sysfs access, to clean rmmod teardown. Notice the strict reverse-order unwind in exit mirroring the forward-order setup in init.

The concepts

01The Linux Driver Model: kobjects, buses, and the device tree

Before writing a single line of code, you need a mental model of how the kernel organises devices. The Linux driver model, introduced around 2.5, gives every device and driver a common ancestor: the kobject. A kobject is a reference-counted object that lives in a hierarchy mirrored directly in /sys. Every directory in /sys/bus, /sys/class, and /sys/devices is the shadow of a kobject.

Think of it like a family tree. At the root is the system bus. Hanging off it are bus types (PCI, USB, I2C, platform, …). Each bus type has a list of registered devices and a list of registered drivers. The bus's match() function decides whether a driver can handle a device — for platform devices this is usually a name string or a device-tree compatible string; for PCI it is a vendor/device ID table.

When you call platform_driver_register(), the kernel walks every unbound platform device looking for a match. When it finds one, it calls your driver's probe() function with a pointer to the device. When the device is removed (or the module is unloaded), remove() is called. This probe/remove symmetry is the first invariant you must never break: everything allocated in probe() must be freed in remove().

For this module we use platform devices because they need no real hardware — a platform device can be created programmatically from a module. Real SoC drivers use the device tree (a hardware description file compiled into a .dtb) to tell the kernel which platform devices exist, but for a virtual driver we register both the device and the driver ourselves.

/* Registering a minimal platform driver */
#include <linux/platform_device.h>
#include <linux/module.h>

static int my_probe(struct platform_device *pdev)
{
    dev_info(&pdev->dev, "probe called\n");
    return 0;
}

static int my_remove(struct platform_device *pdev)
{
    dev_info(&pdev->dev, "remove called\n");
    return 0;
}

static struct platform_driver my_driver = {
    .probe  = my_probe,
    .remove = my_remove,
    .driver = {
        .name  = "mydev",   /* must match device name */
        .owner = THIS_MODULE,
    },
};
module_platform_driver(my_driver);  /* expands to init/exit */

02Module skeleton: init, exit, and the MODULE_ macros

Every kernel module is a relocatable ELF object that the kernel loads with insmod/modprobe. It exports exactly two required hooks: an init function called when the module loads, and an exit function called on unload.

The module_init() and module_exit() macros place pointers to your functions in special ELF sections (.init.text and .exit.text) that the module loader reads. If your init function returns a non-zero errno, the load is aborted and exit is not called — so you never need a partial-cleanup path in init as long as you use goto-based unwinding or devm_* managed resources.

The MODULE_LICENSE("GPL") macro is not optional vanity. The kernel exports some symbols only to GPL-licensed code (for policy reasons). If you omit it or use a non-GPL string, your module will taint the kernel and some function calls will fail at link time.

MODULE_AUTHOR and MODULE_DESCRIPTION show up in modinfo output. MODULE_DEVICE_TABLE tells depmod which device IDs this module handles so udev can auto-load it.

The THIS_MODULE macro expands to a pointer to the current module's struct module — you pass it wherever the kernel needs to increment the module's reference count (preventing rmmod while the device is open).

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Echo buffer virtual driver");
MODULE_VERSION("0.1");

static int __init echobuf_init(void)
{
    pr_info("echobuf: loaded\n");
    return 0;  /* 0 = success; negative errno = failure */
}

static void __exit echobuf_exit(void)
{
    pr_info("echobuf: unloaded\n");
}

module_init(echobuf_init);
module_exit(echobuf_exit);

03Character devices: cdev, file_operations, and the major/minor split

A character device is a file in /dev that the kernel routes to a driver one byte at a time (as opposed to block devices, which work in fixed-size sectors). Almost all simple hardware interfaces — serial ports, sensors, GPIOs, ADCs — are char devices.

Every device file is identified by two numbers: a major number (which driver handles this device class) and a minor number (which specific instance). When you open /dev/ttyS0, the VFS looks up major 4 in a table and dispatches to the serial driver; minor 64 selects the first UART.

The modern API to register a char device:

  1. alloc_chrdev_region() — asks the kernel to assign a free major number and a range of minors. Prefer this over register_chrdev_region() with a hardcoded major.
  2. cdev_init() + cdev_add() — ties a struct cdev to your file_operations table and installs it in the kernel's chrdev map.
  3. class_create() + device_create() — creates a /sys/class/<name> entry and tells udev to create the /dev/<name> node automatically.

The file_operations struct is the central vtable. It contains function pointers for every operation a userspace program can perform on the file descriptor: open, release, read, write, llseek, unlocked_ioctl, mmap, and more. You only need to implement the ones you care about; the rest default to safe fallbacks.

A critical subtlety: the open file operation receives a struct inode * (the filesystem node, shared across opens) and a struct file * (per-open instance). You store per-open state in file->private_data.

#include <linux/cdev.h>
#include <linux/fs.h>

#define ECHOBUF_SIZE 4096

struct echobuf_dev {
    char        buf[ECHOBUF_SIZE];
    size_t      len;          /* valid bytes in buf */
    struct cdev cdev;
    struct mutex lock;
};

static struct echobuf_dev *echobuf;
static dev_t              devno;   /* major + first minor */
static struct class       *echobuf_class;

static int echobuf_open(struct inode *inode, struct file *filp)
{
    /* Recover device struct from cdev embedded in inode */
    struct echobuf_dev *dev =
        container_of(inode->i_cdev, struct echobuf_dev, cdev);
    filp->private_data = dev;
    return 0;
}

static const struct file_operations echobuf_fops = {
    .owner   = THIS_MODULE,
    .open    = echobuf_open,
    /* read/write added in next concept */
};

04Implementing read and write: copy_to_user and copy_from_user

Userspace memory and kernel memory live in different address spaces — and on modern CPUs with SMAP/SMEP enforced, the kernel will fault (and potentially panic) if it dereferences a raw userspace pointer. copy_to_user() and copy_from_user() are the safe bridges.

Beyond safety, these functions:

  • Check that the userspace pointer actually points into the process's mapped address space.
  • Handle page faults gracefully if the user page is swapped out.
  • On x86, temporarily clear the AC flag (disabling SMAP) for the duration of the copy, then restore it.

Both functions return the number of bytes NOT copied (0 on full success). Always check the return value.

The read file operation has the signature ssize_t (*read)(struct file *, char __user *, size_t, loff_t *). The __user annotation is not a real C type — it's a sparse checker hint that this pointer is userspace memory. If you pass it to a function that doesn't use copy_from_user, sparse will warn you.

The loff_t *ppos parameter is the file position. For a simple in-memory buffer you advance it on each read; userspace can then use lseek(2) to rewind. For a stream device (like a real serial port) you typically ignore ppos.

Locking: our buffer is accessed from multiple concurrent read/write calls (multiple processes could have the device open). A struct mutex serialises them. Never sleep with a spinlock held; a mutex is fine here because read/write don't run in interrupt context.

static ssize_t echobuf_read(struct file *filp, char __user *ubuf,
                            size_t count, loff_t *ppos)
{
    struct echobuf_dev *dev = filp->private_data;
    ssize_t retval = 0;

    if (mutex_lock_interruptible(&dev->lock))
        return -ERESTARTSYS;

    if (*ppos >= dev->len)   /* nothing left to read */
        goto out;

    count = min(count, dev->len - (size_t)*ppos);
    if (copy_to_user(ubuf, dev->buf + *ppos, count)) {
        retval = -EFAULT;
        goto out;
    }
    *ppos += count;
    retval = count;
out:
    mutex_unlock(&dev->lock);
    return retval;
}

static ssize_t echobuf_write(struct file *filp, const char __user *ubuf,
                              size_t count, loff_t *ppos)
{
    struct echobuf_dev *dev = filp->private_data;
    ssize_t retval;

    if (mutex_lock_interruptible(&dev->lock))
        return -ERESTARTSYS;

    count = min(count, ECHOBUF_SIZE - (size_t)*ppos);
    if (!count) { retval = -ENOSPC; goto out; }

    if (copy_from_user(dev->buf + *ppos, ubuf, count)) {
        retval = -EFAULT;
        goto out;
    }
    *ppos += count;
    if (*ppos > dev->len)
        dev->len = *ppos;
    retval = count;
out:
    mutex_unlock(&dev->lock);
    return retval;
}

05sysfs attributes: show, store, and DEVICE_ATTR

The /sys filesystem is the kernel's self-description interface. Every kobject in the driver model can expose attributes — virtual files under its sysfs directory — that userspace can read or write. This is how you expose device state without a full char device: temperature sensors, LED brightness, link speed, and buffer statistics all live in sysfs.

The API for a device attribute:

  • DEVICE_ATTR_RO(name) declares a read-only attribute backed by name_show().
  • DEVICE_ATTR_RW(name) adds name_store() for writes.
  • device_create_file(dev, &dev_attr_name) installs the attribute file under the device's sysfs directory.
  • device_remove_file() removes it in remove().

The show function writes into a kernel-allocated page buffer (at most PAGE_SIZE bytes) and returns the number of bytes written. Always end text with a newline — this matches the convention userspace tools expect.

The store function receives the string the user wrote. Use kstrtoul(), kstrtol(), or sscanf() to parse it. Never trust the input length blindly.

For groups of attributes, use attribute_group with sysfs_create_group() — this is what most production drivers do because it handles atomic creation and teardown cleanly.

A concrete example: we expose the current buffer length as a sysfs attribute. After loading the module, cat /sys/class/echobuf/echobuf0/buf_len shows how many bytes are in the buffer.

/* In the device struct we already have a 'struct device *sysdev' pointer
   set by device_create() — see the init concept for how. */

static ssize_t buf_len_show(struct device *d,
                             struct device_attribute *attr, char *buf)
{
    struct echobuf_dev *dev = dev_get_drvdata(d);
    return sysfs_emit(buf, "%zu\n", dev->len);
    /* sysfs_emit is the modern, bounds-checked replacement for sprintf */
}

static ssize_t buf_len_store(struct device *d,
                              struct device_attribute *attr,
                              const char *buf, size_t count)
{
    struct echobuf_dev *dev = dev_get_drvdata(d);
    mutex_lock(&dev->lock);
    dev->len = 0;   /* a write of any value resets the buffer */
    mutex_unlock(&dev->lock);
    return count;
}

static DEVICE_ATTR_RW(buf_len);  /* creates dev_attr_buf_len */

/* In probe/init: */
/* device_create_file(sysdev, &dev_attr_buf_len); */
/* In remove/exit: */
/* device_remove_file(sysdev, &dev_attr_buf_len); */

06Putting it all together: probe, init, and resource management with devm_

The hardest part of driver writing is not any individual API — it is making sure every resource is freed exactly once, in the right order, even on error paths. The kernel's devm (device-managed) family of allocators solves this elegantly.

devm_kzalloc(dev, size, GFP_KERNEL) allocates memory tied to a device. When the device is released (either by device_destroy() or a probe failure), all devm allocations for that device are freed automatically in reverse order. The same pattern applies to devm_ioremap(), devm_request_irq(), devm_clk_get(), and hundreds of other managed resource APIs.

For our virtual driver without a real bus, we combine a platform device (created with platform_device_register_simple()) and a platform driver. In probe() we allocate our echobuf_dev with devm_kzalloc, initialise the mutex, register the cdev, create the device node, and add sysfs files. The goto-unwind pattern handles partial failure:

if (step1 fails) return -EINVAL;
if (step2 fails) { undo step1; return -EINVAL; }

With devm, most of the cleanup disappears: if devm_kzalloc succeeds but cdev_add fails, the devm allocator frees the memory when we return the error. You still manually unwind non-devm resources (cdev, class, device node) because those are not managed.

The container_of() macro deserves special attention. It takes a pointer to a member field, the containing struct type, and the member name, and returns a pointer to the outer struct. It is implemented as pointer arithmetic using offsetof(). This is how echobuf_open() recovers the full echobuf_dev * from the inode->i_cdev pointer — the cdev is embedded inside echobuf_dev, not heap-allocated separately.

static int __init echobuf_init(void)
{
    int ret;

    /* 1. Allocate device struct */
    echobuf = kzalloc(sizeof(*echobuf), GFP_KERNEL);
    if (!echobuf) return -ENOMEM;
    mutex_init(&echobuf->lock);

    /* 2. Grab a major/minor number */
    ret = alloc_chrdev_region(&devno, 0, 1, "echobuf");
    if (ret) goto err_free;

    /* 3. Initialise and register the cdev */
    cdev_init(&echobuf->cdev, &echobuf_fops);
    echobuf->cdev.owner = THIS_MODULE;
    ret = cdev_add(&echobuf->cdev, devno, 1);
    if (ret) goto err_unreg;

    /* 4. Create /sys/class/echobuf and /dev/echobuf0 */
    echobuf_class = class_create(THIS_MODULE, "echobuf");
    if (IS_ERR(echobuf_class)) {
        ret = PTR_ERR(echobuf_class); goto err_cdev;
    }
    echobuf->sysdev = device_create(echobuf_class, NULL,
                                     devno, echobuf, "echobuf0");
    if (IS_ERR(echobuf->sysdev)) {
        ret = PTR_ERR(echobuf->sysdev); goto err_class;
    }

    /* 5. Sysfs attribute */
    device_create_file(echobuf->sysdev, &dev_attr_buf_len);

    pr_info("echobuf: major %d\n", MAJOR(devno));
    return 0;

err_class:  class_destroy(echobuf_class);
err_cdev:   cdev_del(&echobuf->cdev);
err_unreg:  unregister_chrdev_region(devno, 1);
err_free:   kfree(echobuf);
    return ret;
}

static void __exit echobuf_exit(void)
{
    device_remove_file(echobuf->sysdev, &dev_attr_buf_len);
    device_destroy(echobuf_class, devno);
    class_destroy(echobuf_class);
    cdev_del(&echobuf->cdev);
    unregister_chrdev_region(devno, 1);
    kfree(echobuf);
}

07The Makefile and building out-of-tree modules

Linux kernel modules are built using the kernel's own build system (kbuild). You cannot just run gcc — the kernel headers, compiler flags, and symbol tables the kernel provides are mandatory.

An out-of-tree module's Makefile has two parts:

  1. When invoked by the user (make), it re-invokes make targeting the running kernel's build directory, passing M=$(PWD) to say "build the module in this directory".
  2. When invoked by kbuild itself (the recursive invocation), it just lists the object files that make up the module: obj-m := echobuf.o.

The kernel source (or headers) must be installed. On Ubuntu: sudo apt install linux-headers-$(uname -r). The headers live at /lib/modules/$(uname -r)/build.

After make, you get echobuf.ko. The .ko file is a relocatable ELF object with a .modinfo section containing the MODULE_* strings and a __ksymtab for any symbols you export.

insmod ./echobuf.ko loads it. rmmod echobuf unloads it. dmesg | tail shows your pr_info output. modinfo echobuf.ko shows the metadata. lsmod | grep echo confirms it is loaded.

# Makefile for echobuf out-of-tree module

obj-m := echobuf.o

# If your module has multiple source files:
# echobuf-objs := echobuf_main.o echobuf_sysfs.o

KDIR := /lib/modules/$(shell uname -r)/build

all:
	$(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
	$(MAKE) -C $(KDIR) M=$(PWD) clean

install:
	$(MAKE) -C $(KDIR) M=$(PWD) modules_install
	depmod -a

08Testing the driver: a shell-based test suite

Kernel code is harder to unit-test than userspace code — you cannot just run it in a process. But a char driver exposes a clean interface you can test entirely from shell scripts using standard POSIX tools.

The test philosophy for a driver:

  1. Smoke test: insmod succeeds, /dev/echobuf0 appears, dmesg shows no WARN/BUG.
  2. Functional test: write known data, read it back, diff.
  3. Boundary test: write exactly ECHOBUF_SIZE bytes, try to write one more, expect ENOSPC.
  4. Concurrent test: two writers racing, verify no data corruption (use flock or check final length).
  5. Unload test: rmmod while device is open should fail with EBUSY (because .owner = THIS_MODULE holds a reference); rmmod after close should succeed.

For more rigorous testing, the kernel has kselftest (under tools/testing/selftests/) and KUnit (an in-kernel xUnit framework). For a char driver, a kselftest script that exercises /dev/echobuf0 is idiomatic.

For crash safety, run in a VM (QEMU + virtme is perfect) rather than on bare metal. A kernel panic in a VM is a quick reboot; on your laptop it might corrupt your filesystem.

#!/bin/bash
# test_echobuf.sh — basic functional test
set -e

DEV=/dev/echobuf0
SYS=/sys/class/echobuf/echobuf0/buf_len

# Load
insmod ./echobuf.ko
[ -c "$DEV" ] || { echo "FAIL: $DEV not created"; exit 1; }

# Write and read back
TEST_STR="Hello, kernel!"
printf '%s' "$TEST_STR" > "$DEV"
GOT=$(dd if="$DEV" bs=512 count=1 2>/dev/null)
[ "$GOT" = "$TEST_STR" ] || { echo "FAIL: got '$GOT'"; exit 1; }
echo "PASS: read/write"

# sysfs attribute
LEN=$(cat "$SYS")
[ "$LEN" -eq ${#TEST_STR} ] || { echo "FAIL: len $LEN"; exit 1; }
echo "PASS: sysfs buf_len=$LEN"

# Reset via sysfs store
echo 1 > "$SYS"      # any write resets buffer
LEN2=$(cat "$SYS")
[ "$LEN2" -eq 0 ] || { echo "FAIL: reset len $LEN2"; exit 1; }
echo "PASS: sysfs reset"

# Unload
rmmod echobuf
[ ! -c "$DEV" ] || { echo "FAIL: $DEV still exists"; exit 1; }
echo "PASS: all tests"

09ioctl: structured commands beyond read/write

Reading and writing bytes is powerful but limited. Sometimes you need to send structured commands to a device: "set baud rate to 115200", "trigger a one-shot capture", "query hardware status". The ioctl (I/O control) system call is the kernel's extension mechanism for this.

An ioctl is identified by a 32-bit integer command code. The kernel provides macros to construct these codes in a structured way that encodes the direction (read/write), size of the argument, and a type/number pair:

  • _IO(type, nr) — no argument.
  • _IOR(type, nr, datatype) — read from device (kernel writes to userspace pointer).
  • _IOW(type, nr, datatype) — write to device (userspace pointer read by kernel).
  • _IOWR(type, nr, datatype) — bidirectional.

The type is an arbitrary 8-bit magic number that distinguishes your driver's ioctls from others (look at Documentation/userspace-api/ioctl/ioctl-number.rst to pick an unused one).

In your driver's unlocked_ioctl handler, use a switch on the command. For arguments that are pointers, use copy_from_user/copy_to_user as before. For small integer arguments that fit in unsigned long, you can use them directly via the arg parameter.

ioctl definitions belong in a shared header that both the kernel driver and userspace programs include. This is typically installed to /usr/local/include and is the driver's public API contract.

/* echobuf_ioctl.h — shared between kernel and userspace */
#ifndef ECHOBUF_IOCTL_H
#define ECHOBUF_IOCTL_H
#include <linux/ioctl.h>   /* or <sys/ioctl.h> in userspace */

#define ECHOBUF_MAGIC  'E'
#define ECHOBUF_RESET  _IO(ECHOBUF_MAGIC,  0)   /* clear buffer */
#define ECHOBUF_GETLEN _IOR(ECHOBUF_MAGIC, 1, unsigned long)
#define ECHOBUF_SETLEN _IOW(ECHOBUF_MAGIC, 2, unsigned long)
#endif

/* In echobuf.c */
static long echobuf_ioctl(struct file *filp, unsigned int cmd,
                          unsigned long arg)
{
    struct echobuf_dev *dev = filp->private_data;
    unsigned long val;

    switch (cmd) {
    case ECHOBUF_RESET:
        mutex_lock(&dev->lock);
        dev->len = 0;
        mutex_unlock(&dev->lock);
        return 0;

    case ECHOBUF_GETLEN:
        val = dev->len;
        return put_user(val, (unsigned long __user *)arg);

    default:
        return -ENOTTY;  /* "not a typewriter" = invalid ioctl */
    }
}

10Debugging: pr_debug, dynamic debug, and KASAN

Debugging kernel code is a skill of its own. You cannot attach gdb easily (well, you can with KGDB, but that is a separate module). Your main tools:

printk and friends: pr_info(), pr_warn(), pr_err(), pr_debug() are formatted print functions that write to the kernel ring buffer (viewable with dmesg). dev_info(dev, ...) prepends the device name, which is invaluable when multiple device instances print concurrently. pr_debug is compiled out unless CONFIG_DYNAMIC_DEBUG is enabled or -DDEBUG is in CFLAGS. With dynamic debug, you can enable per-file, per-function, or per-line debug messages at runtime via /sys/kernel/debug/dynamic_debug/control without recompiling.

KASAN (Kernel Address Sanitizer): A compile-time instrumentation that detects out-of-bounds accesses and use-after-free bugs in kernel code. Enable it in your kernel config with CONFIG_KASAN=y. When KASAN catches a bug it prints a detailed report to dmesg with a stack trace and the shadow memory state. Running your driver under KASAN in QEMU catches whole classes of bugs before they cause silent data corruption on real hardware.

LOCKDEP: Enable CONFIG_PROVE_LOCKING. LOCKDEP tracks lock acquisition order across the entire kernel and will print a warning — with full stack traces — the first time it sees a lock ordering that could cause deadlock. If your driver acquires locks in inconsistent order, LOCKDEP will catch it immediately.

ftrace: The in-kernel tracer. echo function > /sys/kernel/debug/tracing/current_tracer and echo echobuf_read > /sys/kernel/debug/tracing/set_ftrace_filter let you trace every call into your function with timestamps, showing exactly what called your driver and when.

/* Use dev_dbg for device-context debug messages.
   They are free (no-op) when dynamic debug is off.
   Turn on at runtime without reboot: */

/*  echo 'file echobuf.c +p' \
       > /sys/kernel/debug/dynamic_debug/control  */

static ssize_t echobuf_read(struct file *filp, char __user *ubuf,
                            size_t count, loff_t *ppos)
{
    struct echobuf_dev *dev = filp->private_data;
    dev_dbg(dev->sysdev, "read: ppos=%lld count=%zu len=%zu\n",
            *ppos, count, dev->len);
    /* ... rest of implementation ... */
    return 0;
}

11What changes for real hardware: probe from device tree, MMIO, and interrupts

Everything you have learned applies directly to real hardware drivers. The differences are mechanical:

Device tree binding: On ARM/RISC-V SoCs, hardware is described in a device-tree source (.dts) file. Your platform driver matches on a compatible string. The driver's of_match_table tells the kernel which DT nodes to bind to:

static const struct of_device_id mydev_of_match[] = {
    { .compatible = "vendor,mydev" },
    {},
};
MODULE_DEVICE_TABLE(of, mydev_of_match);

Memory-mapped I/O: Real hardware registers live at a physical address. You get that address from the device tree via platform_get_resource(pdev, IORESOURCE_MEM, 0) and map it with devm_ioremap_resource(). Then use readl()/writel() (or readb/writeb for byte-wide registers) — never dereference the void __iomem * pointer directly, because the compiler might reorder or elide the access.

Interrupts: platform_get_irq(pdev, 0) gives you the IRQ number. devm_request_irq(dev, irq, handler, flags, name, devid) registers your handler. The handler runs in interrupt context: no sleeping, no mutex (use spinlock), do minimal work and schedule a tasklet or workqueue for anything heavy. Return IRQ_HANDLED if this interrupt was yours.

DMA: For transferring large blocks, use the DMA engine API or dma_alloc_coherent() for a shared buffer. This is a module in itself, but knowing it exists prevents you from copying data byte-by-byte to a DMA-capable peripheral.

The beauty is that all of sysfs, cdev, file_operations, and the module infrastructure you just learned are identical. The hardware-specific parts are contained in probe() and the interrupt handler.

/* Minimal MMIO register read/write pattern */
#include <linux/io.h>

struct mydev_hw {
    void __iomem *base;  /* remapped register base */
};

#define REG_STATUS  0x00
#define REG_DATA    0x04
#define REG_CTRL    0x08

static int mydev_probe(struct platform_device *pdev)
{
    struct mydev_hw *hw;
    struct resource *res;

    hw = devm_kzalloc(&pdev->dev, sizeof(*hw), GFP_KERNEL);
    if (!hw) return -ENOMEM;

    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    hw->base = devm_ioremap_resource(&pdev->dev, res);
    if (IS_ERR(hw->base)) return PTR_ERR(hw->base);

    /* Enable device via control register */
    writel(0x1, hw->base + REG_CTRL);

    /* Read status — compiler CANNOT reorder this past the write */
    u32 status = readl(hw->base + REG_STATUS);
    dev_info(&pdev->dev, "status=0x%08x\n", status);

    platform_set_drvdata(pdev, hw);
    return 0;
}

Key insights

  • ✦The probe/remove symmetry is non-negotiable: every resource claimed in probe must be released in remove, or you have a kernel resource leak that survives rmmod.
  • ✦copy_to_user and copy_from_user are not optional safety theatre — they are the only correct way to cross the kernel/user address space boundary, and skipping them is a security vulnerability and a crash waiting to happen.
  • ✦container_of() is the idiom that makes the kernel's embedded-struct pattern work: you embed a struct cdev inside your device struct, and recover the outer struct from any cdev pointer with zero extra allocation.
  • ✦sysfs is the right place for device configuration and status; the char device is the right place for data streams. Conflating them leads to awkward interfaces.
  • ✦devm_ allocators are not a shortcut — they are the correct engineering choice because they make the error path as simple as the happy path.
  • ✦A kernel module returning a negative errno from init prevents loading but does NOT call exit — your goto-unwind must handle partial initialization.
  • ✦Always test in a VM first. A KASAN-enabled QEMU kernel turns silent memory bugs into loud, actionable crash reports.
  • ✦The file_operations vtable is the same pattern as every other kernel subsystem: you fill in the operations you support and leave the rest NULL for safe defaults.

Common pitfalls

  • ⚠Forgetting to check the return value of copy_to_user/copy_from_user — they return the number of bytes NOT copied, and a partial copy is a silent data corruption bug.
  • ⚠Calling mutex_lock() (sleeping lock) from interrupt context — use spin_lock_irqsave() in interrupt handlers, not mutex.
  • ⚠Returning a positive value from init on failure — only return 0 for success; any non-zero is treated as an errno and must be negative.
  • ⚠Not setting .owner = THIS_MODULE in file_operations — without this, rmmod while a file descriptor is open will corrupt memory because the module is freed while code from it is still mapped.
  • ⚠Using sprintf instead of sysfs_emit in show() — sysfs_emit is bounds-checked to PAGE_SIZE; sprintf into a sysfs buffer can silently overflow.
  • ⚠Hardcoding a major device number with register_chrdev_region instead of alloc_chrdev_region — hardcoded numbers conflict with other drivers and will be rejected in upstream submissions.
  • ⚠Freeing memory in the wrong order on the error path — always unwind in reverse order of allocation; freeing the class before destroying devices that reference it causes use-after-free.
  • ⚠Directly dereferencing a __iomem pointer instead of using readl/writel — the compiler can reorder or eliminate such accesses because it does not know they have hardware side effects.

Practice

  1. 01
    easy

    Build and load the complete echobuf driver. Verify that /dev/echobuf0 appears, write the string 'KernelPath' to it, read it back with dd, and confirm the sysfs buf_len shows 10. Then reset the buffer via sysfs and confirm buf_len returns 0.

  2. 02
    easy

    Add an llseek file operation to echobuf that implements SEEK_SET, SEEK_CUR, and SEEK_END correctly. Verify that 'dd if=/dev/echobuf0 skip=5 bs=1' reads starting from byte 5.

  3. 03
    medium

    Implement the ECHOBUF_RESET and ECHOBUF_GETLEN ioctls from the ioctl concept. Write a small C userspace program that opens /dev/echobuf0, writes 'hello', calls ECHOBUF_GETLEN to confirm length is 5, calls ECHOBUF_RESET, then calls ECHOBUF_GETLEN again to confirm 0.

  4. 04
    medium

    Add a second sysfs attribute 'buf_data' that shows the current buffer contents as a hex dump (use hex_dump_to_buffer or just a simple loop with sysfs_emit_at). Make it read-only.

  5. 05
    hard

    Make echobuf support multiple instances: allocate N_DEVICES=4 minors, create /dev/echobuf0 through /dev/echobuf3, each with an independent buffer. Verify that writing to echobuf0 does not affect echobuf1.

  6. 06
    hard

    Build a QEMU virtual machine with CONFIG_KASAN=y and CONFIG_PROVE_LOCKING=y. Intentionally introduce a use-after-free bug (free dev->buf early, then write to it), load the driver, and trigger the bug. Study the KASAN report in dmesg. Then fix the bug and confirm KASAN is clean.

Resources

📕Linux Device Drivers, 3rd Edition (LDD3)
bookfree

The canonical free book on Linux driver development; chapters 3 (char drivers) and 14 (device model) are directly relevant to this module.

📄The Linux Kernel Module Programming Guide
docsfree

Actively maintained, covers modules, char devices, and sysfs with modern kernel APIs.

⚡Bootlin kernel driver labs
interactivefree

Free lab materials (PDF) for Bootlin's embedded Linux training; the driver labs walk through char devices and platform drivers on real hardware.

📄Linux Kernel Documentation: driver-api
docsfree

Authoritative, generated from kernel source; covers the device model, cdev, sysfs, and devm APIs with struct-level detail.

✎kernelnewbies.org: Device Drivers
articlefree

Concise, beginner-friendly summary of module and driver basics with links to further resources.

✎LWN.net: The platform device API
articlefree

Authoritative LWN article explaining platform devices and the driver model from first principles.

📕Linux Device Driver Development, 2nd Edition
bookpaidJohn Madieu, Packt Publishing, 2022 — covers modern kernel APIs including devm_, device tree, and regmap that LDD3 predates.

The most up-to-date driver development book; covers devm, device tree, and regmap that LDD3 predates.

🔧Elixir Cross Referencer — Linux kernel source browser
toolfree

Browse and cross-reference kernel source online; invaluable for finding how real drivers use the APIs you are learning.

🔧QEMU + virtme-ng for kernel development
toolfree

Boot a custom kernel in a VM in seconds using your host filesystem; the fastest way to test driver changes safely.

📄KUnit: Unit testing for the Linux kernel
docsfree

Official documentation for the in-kernel unit testing framework; use it to write fast, repeatable tests for driver logic.

Checkpoints — you can now…

  • ✓You can write a kernel module from scratch, build it with a kbuild Makefile, and load/unload it without panicking the kernel.
  • ✓You can register a character device with a dynamically assigned major number and verify /dev/echobuf0 appears after insmod.
  • ✓You can implement read() and write() file operations that safely cross the user/kernel boundary using copy_to_user and copy_from_user.
  • ✓You can expose device state as a sysfs attribute using DEVICE_ATTR_RW and verify it with cat and echo from the shell.
  • ✓You can implement a correct init/exit pair with goto-based error unwinding that frees all resources in reverse order on failure.
  • ✓You can write a shell-based test script that smoke-tests your driver's read, write, sysfs, and unload behavior.
  • ✓You can explain what container_of() does and why it is necessary for recovering a device struct from a cdev pointer in open().
  • ✓You can describe the three changes needed to adapt the virtual driver to real hardware: of_match_table for device tree, devm_ioremap_resource for registers, and devm_request_irq for interrupts.