Category: Device Drivers

  • Linux Watchdog Driver: The Complete Beginner’s Guide (2024)

    Learn how the Linux watchdog driver works, how to write one, configure /dev/watchdog, use ioctl commands, and set up the watchdog daemon. Code included.

    1. What Is a Watchdog Driver in Linux?

    Imagine you have a robot that is supposed to check in with you every 30 seconds. If it stops checking in, you assume something went wrong and you reset it. That is exactly what a watchdog timer does for your Linux system.

    A Linux watchdog driver is a kernel-level component that monitors system health. It works by requiring your application or a daemon to send a periodic signal — commonly called a “ping” or “keepalive” — to the watchdog device. If that signal stops arriving within a configured timeout window, the watchdog assumes the system has frozen or hung, and it automatically triggers a reboot.

    This is not just a nice-to-have feature for servers. In embedded Linux systems — think routers, industrial controllers, automotive ECUs, medical devices — a watchdog timer is often the only safety net between a production system and a permanent hang that nobody can fix remotely.

    The Linux kernel has had watchdog support since very early days, but the unified Linux watchdog framework introduced in kernel version 3.5 cleaned things up significantly and gave driver developers a proper, consistent API to work with.

    So when people talk about the “watchdog driver in Linux,” they typically mean one of two things:

    • The hardware watchdog driver: a kernel module that talks to actual hardware circuitry on your board, which is independent of the CPU. Even if the CPU hangs completely, the hardware watchdog can fire and reset the system.
    • The software watchdog driver (usually softdog): a kernel module that uses Linux kernel timers to simulate watchdog behavior, without requiring dedicated hardware.

    Both of these expose themselves to user space through the /dev/watchdog device file, and both follow the same watchdog driver API defined by the Linux kernel watchdog framework.


    2. How Does the Watchdog Timer Work in Linux?

    Let’s trace the full lifecycle so you really understand the mechanics, not just the buzzwords.

    Step 1 — Open the device.
    When your application (or the watchdog daemon) opens /dev/watchdog, the watchdog timer starts. This is the trigger point. From this moment on, the countdown begins.

    Step 2 — Keepalive pings keep it alive.
    Your application must periodically write something to /dev/watchdog — even a single byte — or send an ioctl call with WDIOC_KEEPALIVE. This resets the countdown timer back to its full timeout value. As long as these pings keep coming, nothing bad happens.

    Step 3 — Missing a ping triggers a reset.
    If the system hangs, goes into an infinite loop, panics in kernel space, or your application crashes and nobody else is pinging — the watchdog timer reaches zero and kicks in. For a hardware watchdog, this means the hardware directly asserts a reset line on the SoC. For the software watchdog (softdog), it calls the kernel’s emergency restart function.

    Step 4 — Close behavior with the “magic close” feature.
    One gotcha that trips up beginners: you cannot just close /dev/watchdog to safely stop the timer in all cases. The Linux watchdog driver API includes a concept called “magic close.” If you write the character 'V' to /dev/watchdog before closing it, the driver understands you intentionally stopped the watchdog and will not trigger a reboot. If you just close it without the magic close, many drivers will still trigger a reset — which is intentional. It prevents crashes from accidentally leaving the watchdog unguarded.

    Here is a minimal C example of how this works at the user-space level:

    #include <stdio.h>
    #include <fcntl.h>
    #include <unistd.h>
    #include <sys/ioctl.h>
    #include <linux/watchdog.h>
    
    int main(void)
    {
        int fd = open("/dev/watchdog", O_RDWR);
        if (fd == -1) {
            perror("open /dev/watchdog");
            return 1;
        }
    
        printf("Watchdog started. Pinging every 10 seconds...\n");
    
        while (1) {
            /* This write resets the watchdog timer — the "ping" or keepalive */
            write(fd, "1", 1);
            sleep(10);
        }
    
        /* If you want to safely stop the watchdog, send the magic close character */
        write(fd, "V", 1);
        close(fd);
        return 0;
    }
    

    This is the most fundamental Linux watchdog example. Your daemon does this in a loop, and if the system hangs, the loop stops, the ping stops arriving, and the watchdog reboots the system automatically.


    3. Hardware Watchdog vs Software Watchdog in Linux

    This is one of the most common questions people have when they first get into Linux watchdog timers, so let’s clear it up properly.

    Hardware Watchdog Linux

    A hardware watchdog is a dedicated circuit built into your SoC (System-on-Chip) or available as a separate IC on your PCB. It operates completely independently from the main CPU. The watchdog hardware has its own oscillator, its own timer counter, and direct electrical control over the system reset line.

    Why does this matter? Because even if your CPU completely locks up — kernel panic, dead interrupt handlers, whatever — the hardware watchdog circuit is still ticking. It does not care what your CPU is doing. When its counter reaches zero, it asserts the reset signal and brings the system back to life.

    Practically every embedded SoC you will encounter — TI AM335x, NXP i.MX6, Allwinner, Raspberry Pi’s BCM2835, Xilinx Zynq — has a hardware watchdog built in. The Linux kernel includes specific drivers for most of them (under drivers/watchdog/ in the kernel source tree).

    Software Watchdog Linux

    The software watchdog, implemented by the softdog kernel module, does not rely on external hardware. It uses the kernel’s internal timer subsystem to simulate watchdog behavior.

    The critical limitation: if the kernel itself freezes — say, a scheduling bug prevents timer interrupts from firing, or you hit a really bad deadlock — the software watchdog cannot save you. It is software running on the same CPU that just died.

    That said, softdog is incredibly useful for:

    • Development machines where you have no hardware watchdog
    • Virtual machines (VMs) — most hypervisors do not expose hardware watchdog timers
    • Quick testing of watchdog behavior without needing specific hardware

    Which one should you use?

    If you are building a production embedded Linux system for anything critical (industrial, automotive, medical, networking infrastructure), you want a hardware watchdog. Always. The software watchdog is good for development and testing, but it is not a substitute for real hardware-level protection.

    FeatureHardware WatchdogSoftware Watchdog (softdog)
    Survives kernel freezeYesNo
    Requires specific hardwareYesNo
    Works in VMsSometimes (paravirt)Yes
    Production embedded systemsStrongly recommendedNot recommended
    Available as /dev/watchdogYesYes
    Uses same APIYesYes

    4. The Linux Watchdog Framework (Kernel Architecture)

    Prior to kernel 3.5, every watchdog driver in the Linux kernel basically did its own thing. Each driver had its own implementation of the file_operations struct, its own ioctl handling, its own timeout management. This led to inconsistencies, bugs, and a lot of duplicated code.

    The Linux watchdog framework (introduced by Wim Van Sebroeck and others) standardized all of this. Now, writing a Linux watchdog driver is much cleaner. Here is how the architecture looks:

     User Space Application
            |
            | open/write/ioctl
            v
       /dev/watchdog  (character device, major 10, minor 130)
            |
            v
       watchdog_dev.c  (watchdog core — handles file_operations)
            |
            v
       watchdog_ops   (your driver implements these)
            |
            v
       Hardware / softdog timer
    

    The core lives in drivers/watchdog/watchdog_core.c and drivers/watchdog/watchdog_dev.c. These files handle everything generic — the device registration, the character device operations, the ping tracking, the ioctl dispatch. Your driver just needs to fill in a watchdog_ops struct and a watchdog_device struct.

    The key data structures you need to know are:

    struct watchdog_device — describes your watchdog device to the framework:

    struct watchdog_device {
        int id;
        struct device *parent;
        const struct watchdog_info *info;
        const struct watchdog_ops *ops;
        unsigned int timeout;       /* current timeout in seconds */
        unsigned int min_timeout;   /* minimum allowed timeout */
        unsigned int max_timeout;   /* maximum allowed timeout */
        unsigned long status;       /* status flags */
        /* ... more fields */
    };
    

    struct watchdog_ops — the operations your driver must implement:

    struct watchdog_ops {
        struct module *owner;
        /* mandatory */
        int (*start)(struct watchdog_device *);
        int (*stop)(struct watchdog_device *);
        /* optional but highly recommended */
        int (*ping)(struct watchdog_device *);
        unsigned int (*status)(struct watchdog_device *);
        int (*set_timeout)(struct watchdog_device *, unsigned int);
        long (*ioctl)(struct watchdog_device *, unsigned int, unsigned long);
    };
    

    struct watchdog_info — information exposed to user space via the WDIOC_GETSUPPORT ioctl:

    struct watchdog_info {
        __u32 options;          /* what the watchdog can do */
        __u32 firmware_version;
        __u8  identity[32];     /* name of the watchdog */
    };
    

    This clean framework is what makes the modern Linux watchdog driver API elegant. You implement maybe five functions, fill two structs, call watchdog_register_device(), and the framework handles the rest.


    5. What Is /dev/watchdog in Linux?

    /dev/watchdog is the userspace interface to the Linux watchdog subsystem. It is a character device with major number 10 and minor number 130. There is also /dev/watchdog0, /dev/watchdog1, etc., for systems with multiple watchdog devices.

    When you do ls -la /dev/watchdog on a system with a watchdog enabled, you get something like:

    crw------- 1 root root 10, 130 Jan 10 08:22 /dev/watchdog
    

    Only root can access it by default, which makes sense — you do not want random processes accidentally (or intentionally) triggering a system reboot.

    Opening /dev/watchdog starts the timer. That is worth saying again because it is a source of confusion. The moment any process opens this device, the watchdog starts counting down. If you open it just to check something and then forget to ping it, your system will reboot. Respect the device.

    Only one process can open it at a time. The watchdog device is exclusive. If you try to open it from two processes simultaneously, the second one gets -EBUSY. This prevents race conditions where two processes both think they are managing the watchdog.

    The device supports a standard set of operations:

    • open() — starts the watchdog
    • write() — sends a keepalive ping (resets the timer)
    • ioctl() — queries/sets timeout, gets status, triggers reboot, etc.
    • close() — if preceded by writing 'V' (magic close), stops the watchdog safely

    Here is a quick example of querying the watchdog timeout using ioctl:

    #include <linux/watchdog.h>
    #include <sys/ioctl.h>
    #include <fcntl.h>
    #include <stdio.h>
    
    int main(void)
    {
        int fd = open("/dev/watchdog", O_RDWR);
        int timeout;
    
        ioctl(fd, WDIOC_GETTIMEOUT, &timeout);
        printf("Current watchdog timeout: %d seconds\n", timeout);
    
        /* Set a new timeout */
        timeout = 30;
        ioctl(fd, WDIOC_SETTIMEOUT, &timeout);
        printf("New watchdog timeout: %d seconds\n", timeout);
    
        /* Send magic close and stop */
        write(fd, "V", 1);
        close(fd);
        return 0;
    }
    

    6. The Linux Watchdog Driver API Explained

    The watchdog driver API is the set of kernel interfaces defined in include/linux/watchdog.h that you use both when writing a driver and when interacting with the watchdog from user space.

    Let’s look at both sides.

    Kernel-Side API (Writing a Driver)

    When you write a Linux watchdog driver, you interact with these key kernel functions:

    /* Register your watchdog with the framework */
    int watchdog_register_device(struct watchdog_device *wdd);
    
    /* Unregister (call in your module's exit function) */
    void watchdog_unregister_device(struct watchdog_device *wdd);
    
    /* Check if the watchdog is running */
    bool watchdog_active(struct watchdog_device *wdd);
    
    /* Check if the watchdog is stopped */
    bool watchdog_stopped(struct watchdog_device *wdd);
    
    /* Set the watchdog as running (use in your start() op) */
    void watchdog_set_running(struct watchdog_device *wdd);
    
    /* For hardware with nowayout (cannot be stopped once started) */
    WATCHDOG_NOWAYOUT_INIT_STATUS(status);
    

    User-Space API (Using /dev/watchdog)

    From user space, you interact via the standard ioctl() system call. The commands are defined in linux/watchdog.h:

    /* Get support information (struct watchdog_info) */
    #define WDIOC_GETSUPPORT   _IOR(WATCHDOG_IOCTL_BASE, 0, struct watchdog_info)
    
    /* Get watchdog status */
    #define WDIOC_GETSTATUS    _IOR(WATCHDOG_IOCTL_BASE, 1, int)
    
    /* Get boot status (was last reset caused by watchdog?) */
    #define WDIOC_GETBOOTSTATUS _IOR(WATCHDOG_IOCTL_BASE, 2, int)
    
    /* Trigger an immediate reboot via watchdog */
    #define WDIOC_SETOPTIONS   _IOR(WATCHDOG_IOCTL_BASE, 4, int)
    
    /* Send a keepalive ping */
    #define WDIOC_KEEPALIVE    _IOR(WATCHDOG_IOCTL_BASE, 5, int)
    
    /* Set the timeout (in seconds) */
    #define WDIOC_SETTIMEOUT   _IOWR(WATCHDOG_IOCTL_BASE, 6, int)
    
    /* Get the current timeout */
    #define WDIOC_GETTIMEOUT   _IOR(WATCHDOG_IOCTL_BASE, 7, int)
    
    /* Get time remaining before reset */
    #define WDIOC_GETTIMELEFT  _IOR(WATCHDOG_IOCTL_BASE, 10, int)
    

    The WATCHDOG_IOCTL_BASE is defined as 'W' (0x57), so WDIOC_KEEPALIVE expands to _IOR('W', 5, int).

    Watchdog Options Flags

    The watchdog_info.options field tells you what capabilities your watchdog hardware/driver supports. Common flags include:

    #define WDIOF_SETTIMEOUT    0x0001  /* Can set timeout */
    #define WDIOF_MAGICCLOSE    0x0100  /* Supports magic close char */
    #define WDIOF_KEEPALIVEPING 0x8000  /* Keepalive ping reply */
    #define WDIOF_CARDRESET     0x0020  /* Card previously reset the CPU */
    

    7. Watchdog ioctl Commands — WDIOC_KEEPALIVE, SETTIMEOUT, and More

    Let’s dig into the most important ioctl commands with real code examples.

    WDIOC_KEEPALIVE — Send a Ping

    This is the most important ioctl. Instead of writing to the device, you can send a keepalive using ioctl:

    int fd = open("/dev/watchdog", O_RDWR);
    
    /* Send keepalive via ioctl */
    if (ioctl(fd, WDIOC_KEEPALIVE, 0) < 0) {
        perror("WDIOC_KEEPALIVE failed");
    }
    

    Note: writing any byte to /dev/watchdog is equivalent and often simpler. Most real implementations just do write(fd, "\0", 1) in a loop.

    WDIOC_SETTIMEOUT and WDIOC_GETTIMEOUT — Watchdog Timeout Linux

    int timeout = 60;  /* 60 seconds */
    
    /* Set the timeout */
    if (ioctl(fd, WDIOC_SETTIMEOUT, &timeout) < 0) {
        perror("WDIOC_SETTIMEOUT failed");
    }
    
    /* The driver may round to the nearest supported value,
       so always read back what was actually set */
    if (ioctl(fd, WDIOC_GETTIMEOUT, &timeout) < 0) {
        perror("WDIOC_GETTIMEOUT failed");
    }
    printf("Watchdog timeout set to: %d seconds\n", timeout);
    

    Important: not all hardware watchdogs support arbitrary timeout values. Some only support specific values (e.g., powers of two based on clock dividers). The driver rounds your requested timeout to the nearest supported value. Always read back the actual timeout after setting it.

    WDIOC_GETBOOTSTATUS — Did the Watchdog Cause the Last Reboot?

    This is incredibly useful for diagnosing production issues:

    int boot_status;
    if (ioctl(fd, WDIOC_GETBOOTSTATUS, &boot_status) < 0) {
        perror("WDIOC_GETBOOTSTATUS failed");
    }
    
    if (boot_status & WDIOF_CARDRESET) {
        printf("System was reset by the watchdog!\n");
        /* Log this, alert your monitoring system, etc. */
    } else {
        printf("Normal boot (not watchdog-triggered)\n");
    }
    

    WDIOC_GETSUPPORT — Query Driver Capabilities

    struct watchdog_info ident;
    if (ioctl(fd, WDIOC_GETSUPPORT, &ident) < 0) {
        perror("WDIOC_GETSUPPORT failed");
    }
    
    printf("Watchdog identity: %s\n", ident.identity);
    printf("Firmware version: %u\n", ident.firmware_version);
    printf("Options: 0x%08x\n", ident.options);
    
    if (ident.options & WDIOF_SETTIMEOUT)
        printf("  - Can set timeout\n");
    if (ident.options & WDIOF_MAGICCLOSE)
        printf("  - Supports magic close\n");
    if (ident.options & WDIOF_CARDRESET)
        printf("  - Can detect watchdog reset\n");
    

    WDIOC_GETTIMELEFT — Time Until Reboot

    int timeleft;
    if (ioctl(fd, WDIOC_GETTIMELEFT, &timeleft) < 0) {
        perror("WDIOC_GETTIMELEFT failed");
    }
    printf("Time left before watchdog fires: %d seconds\n", timeleft);
    

    8. How to Write a Watchdog Driver in Linux (Step-by-Step with Code)

    This is the part where things get really interesting. Let’s write a complete, minimal Linux watchdog driver that uses the modern watchdog framework. This is a great tutorial starting point.

    We will write a simple platform driver that simulates hardware watchdog behavior using a kernel timer (essentially how softdog works, but structured properly with the watchdog framework API).

    Step 1 — Set Up the Module Skeleton

    // File: my_watchdog.c
    #include <linux/module.h>
    #include <linux/kernel.h>
    #include <linux/init.h>
    #include <linux/platform_device.h>
    #include <linux/watchdog.h>
    #include <linux/timer.h>
    
    #define DRIVER_NAME     "my_watchdog"
    #define WDT_DEFAULT_TIMEOUT  30   /* 30 seconds */
    #define WDT_MIN_TIMEOUT      1
    #define WDT_MAX_TIMEOUT      120
    
    MODULE_LICENSE("GPL");
    MODULE_AUTHOR("Your Name");
    MODULE_DESCRIPTION("A simple Linux watchdog driver example");
    

    Step 2 — Define Driver Private Data

    struct my_wdt_dev {
        struct watchdog_device wdd;
        struct timer_list timer;
        spinlock_t lock;
        bool running;
    };
    

    Step 3 — Implement the Watchdog Operations

    static int my_wdt_start(struct watchdog_device *wdd)
    {
        struct my_wdt_dev *wdev = watchdog_get_drvdata(wdd);
    
        spin_lock(&wdev->lock);
        wdev->running = true;
        /* In real hardware: write to the hardware register to start the timer */
        /* Here we use a kernel timer to simulate it */
        mod_timer(&wdev->timer,
                  jiffies + msecs_to_jiffies(wdd->timeout * 1000));
        spin_unlock(&wdev->lock);
    
        pr_info("%s: watchdog started (timeout=%u seconds)\n",
                DRIVER_NAME, wdd->timeout);
        return 0;
    }
    
    static int my_wdt_stop(struct watchdog_device *wdd)
    {
        struct my_wdt_dev *wdev = watchdog_get_drvdata(wdd);
    
        spin_lock(&wdev->lock);
        wdev->running = false;
        del_timer(&wdev->timer);
        spin_unlock(&wdev->lock);
    
        pr_info("%s: watchdog stopped\n", DRIVER_NAME);
        return 0;
    }
    
    static int my_wdt_ping(struct watchdog_device *wdd)
    {
        struct my_wdt_dev *wdev = watchdog_get_drvdata(wdd);
    
        spin_lock(&wdev->lock);
        if (wdev->running) {
            /* Reset the countdown — this is the "keepalive ping" */
            mod_timer(&wdev->timer,
                      jiffies + msecs_to_jiffies(wdd->timeout * 1000));
        }
        spin_unlock(&wdev->lock);
        return 0;
    }
    
    static int my_wdt_set_timeout(struct watchdog_device *wdd,
                                   unsigned int timeout)
    {
        wdd->timeout = timeout;
        if (watchdog_active(wdd))
            my_wdt_ping(wdd);  /* Restart with new timeout */
        return 0;
    }
    
    /* This fires when the watchdog expires (no ping received in time) */
    static void my_wdt_timer_callback(struct timer_list *t)
    {
        struct my_wdt_dev *wdev = from_timer(wdev, t, timer);
    
        if (wdev->running) {
            pr_crit("%s: watchdog timer expired! Triggering system reboot.\n",
                    DRIVER_NAME);
            emergency_restart();
        }
    }
    

    Step 4 — Fill in the Framework Structures

    static const struct watchdog_info my_wdt_info = {
        .options = WDIOF_SETTIMEOUT | WDIOF_MAGICCLOSE | WDIOF_KEEPALIVEPING,
        .identity = "My Example Watchdog",
        .firmware_version = 1,
    };
    
    static const struct watchdog_ops my_wdt_ops = {
        .owner      = THIS_MODULE,
        .start      = my_wdt_start,
        .stop       = my_wdt_stop,
        .ping       = my_wdt_ping,
        .set_timeout = my_wdt_set_timeout,
    };
    

    Step 5 — Probe and Remove Functions

    static int my_wdt_probe(struct platform_device *pdev)
    {
        struct my_wdt_dev *wdev;
        int ret;
    
        wdev = devm_kzalloc(&pdev->dev, sizeof(*wdev), GFP_KERNEL);
        if (!wdev)
            return -ENOMEM;
    
        spin_lock_init(&wdev->lock);
        timer_setup(&wdev->timer, my_wdt_timer_callback, 0);
    
        /* Set up the watchdog_device structure */
        wdev->wdd.info     = &my_wdt_info;
        wdev->wdd.ops      = &my_wdt_ops;
        wdev->wdd.timeout  = WDT_DEFAULT_TIMEOUT;
        wdev->wdd.min_timeout = WDT_MIN_TIMEOUT;
        wdev->wdd.max_timeout = WDT_MAX_TIMEOUT;
        wdev->wdd.parent   = &pdev->dev;
    
        watchdog_set_drvdata(&wdev->wdd, wdev);
        watchdog_set_nowayout(&wdev->wdd, false); /* allow stopping */
    
        /* Register with the watchdog framework */
        ret = watchdog_register_device(&wdev->wdd);
        if (ret) {
            dev_err(&pdev->dev, "Failed to register watchdog: %d\n", ret);
            return ret;
        }
    
        platform_set_drvdata(pdev, wdev);
        dev_info(&pdev->dev, "My watchdog driver initialized\n");
        return 0;
    }
    
    static int my_wdt_remove(struct platform_device *pdev)
    {
        struct my_wdt_dev *wdev = platform_get_drvdata(pdev);
    
        watchdog_unregister_device(&wdev->wdd);
        del_timer_sync(&wdev->timer);
        return 0;
    }
    

    Step 6 — Module Registration

    static struct platform_driver my_wdt_driver = {
        .probe  = my_wdt_probe,
        .remove = my_wdt_remove,
        .driver = {
            .name  = DRIVER_NAME,
            .owner = THIS_MODULE,
        },
    };
    
    module_platform_driver(my_wdt_driver);
    

    Step 7 — The Makefile

    obj-m += my_watchdog.o
    
    KDIR ?= /lib/modules/$(shell uname -r)/build
    
    all:
    	make -C $(KDIR) M=$(PWD) modules
    
    clean:
    	make -C $(KDIR) M=$(PWD) clean
    

    Build with make, insert with insmod my_watchdog.ko, and you should see /dev/watchdog0 appear.


    9. How to Enable Watchdog in Linux Kernel (Step-by-Step)

    If you are building a custom Linux kernel (common in embedded work), you need to make sure watchdog support is compiled in. Here is how to do it step by step.

    Step 1 — Open Kernel Configuration

    cd /path/to/linux-kernel-source
    make menuconfig
    

    Step 2 — Navigate to Watchdog Support

    In menuconfig, go to:

    Device Drivers --->
      [*] Watchdog Timer Support --->
    

    Step 3 — Enable Relevant Drivers

    Under “Watchdog Timer Support” you will see:

    <*> Software watchdog (softdog)
    < > WDT Watchdog timer
    ... (platform-specific drivers for your hardware)
    

    For a Raspberry Pi, look for BCM2835 watchdog. For an x86 machine, iTCO watchdog is the Intel TCO watchdog driver. For ARM-based SoCs, look for your specific SoC name.

    Enable the one you need as either built-in (<*>) or module (<M>). For embedded systems where you need the watchdog very early in boot, built-in is safer.

    Step 4 — Enable CONFIG Options Directly (Alternative)

    If you prefer editing .config directly:

    # Core watchdog support
    CONFIG_WATCHDOG=y
    
    # Software watchdog
    CONFIG_SOFT_WATCHDOG=y
    
    # Intel TCO watchdog (x86)
    CONFIG_ITCO_WDT=y
    
    # For embedded ARM (example: i.MX watchdog)
    CONFIG_IMX2_WDT=y
    

    Step 5 — Check if Watchdog Is Already Running

    On a running system, you can check the watchdog status without a custom tool:

    # Check if /dev/watchdog exists
    ls -la /dev/watchdog*
    
    # Load the softdog module if no hardware watchdog is present
    sudo modprobe softdog
    
    # Check kernel messages for watchdog activity
    dmesg | grep -i watchdog
    
    # Check if the watchdog daemon is running
    systemctl status watchdog
    

    Step 6 — Test Basic Watchdog Functionality

    # This starts the watchdog. WARNING: If you do not ping it or kill it properly,
    # your system WILL reboot after the timeout.
    sudo bash -c 'echo "test" > /dev/watchdog && sleep 5 && echo V > /dev/watchdog'
    

    The final echo V sends the magic close character, safely stopping the watchdog.


    10. Linux Watchdog Module Parameters

    When loading watchdog modules (either your custom driver or built-in ones like softdog), you can pass parameters to configure their behavior at load time.

    softdog Module Parameters

    # Set timeout to 60 seconds when loading
    sudo modprobe softdog soft_margin=60
    
    # Enable noboot (do not reboot, just log)
    sudo modprobe softdog soft_noboot=1
    
    # Nowayout mode: once started, cannot be stopped
    sudo modprobe softdog nowayout=1
    

    The key parameters for softdog:

    • soft_margin — the default timeout in seconds (default: 60)
    • soft_noboot — if set to 1, the watchdog logs an expiry but does not reboot (useful for testing)
    • nowayout — prevents the watchdog from being stopped once started (boot parameter security)

    Setting Module Parameters Persistently

    To make these persistent across reboots, edit or create a file in /etc/modprobe.d/:

    # /etc/modprobe.d/softdog.conf
    options softdog soft_margin=60 nowayout=0
    

    Parameters via Kernel Command Line

    For built-in watchdog drivers (compiled as y not m), you set parameters via the kernel command line in your bootloader (GRUB, U-Boot, etc.):

    # In GRUB config / U-Boot environment
    linux /vmlinuz ... softdog.soft_margin=60
    

    Checking Currently Active Parameters

    # See what parameters a loaded module supports
    modinfo softdog
    
    # Check current values for loaded module
    cat /sys/module/softdog/parameters/soft_margin
    cat /sys/module/softdog/parameters/nowayout
    

    11. Linux Watchdog Daemon Configuration with systemd

    For production systems, you rarely ping /dev/watchdog directly from your application. Instead, you use a dedicated watchdog daemon (watchdogd) that handles the keepalive pings and can also perform additional health checks before deciding whether to keep the system alive.

    Installing the Watchdog Daemon

    On Debian/Ubuntu:

    sudo apt-get install watchdog
    

    On RHEL/Fedora:

    sudo dnf install watchdog
    

    The Watchdog Daemon Configuration File

    The main config file is /etc/watchdog.conf. Here is a well-documented example covering the most important options:

    # /etc/watchdog.conf
    
    # The watchdog device to use
    watchdog-device = /dev/watchdog
    
    # Timeout for the watchdog hardware (seconds)
    # Must be greater than the interval
    watchdog-timeout = 60
    
    # How often the daemon sends a keepalive ping (seconds)
    # Should be significantly less than watchdog-timeout
    interval = 10
    
    # Maximum load average before the daemon triggers a reboot
    max-load-1  = 24
    max-load-5  = 18
    max-load-15 = 12
    
    # Reboot if available memory drops below this (pages)
    min-memory = 1
    
    # Ping these files — if they do not get written, reboot
    # (useful for application-level health checks)
    # file = /var/run/my-app.heartbeat
    # change = 180
    
    # Test network connectivity by pinging this host
    # If it becomes unreachable, reboot
    # ping = 192.168.1.1
    
    # Log to syslog
    log-dir = /var/log/watchdog
    
    # User tests — run this script, reboot if it returns non-zero
    # test-binary = /usr/local/bin/system-health-check.sh
    
    # Repair binary — try this first before rebooting
    # repair-binary = /usr/local/bin/attempt-recovery.sh
    
    # Temperature monitoring
    # temperature-sensor = /dev/temperature-sensor-device
    # max-temperature = 90
    

    Enabling the Watchdog Daemon with systemd

    # Enable and start the watchdog daemon
    sudo systemctl enable watchdog
    sudo systemctl start watchdog
    
    # Check its status
    sudo systemctl status watchdog
    
    # Check the daemon logs
    sudo journalctl -u watchdog -f
    

    Writing a systemd Service That Feeds the Watchdog

    Alternatively, for simple embedded use cases, you might write your own minimal watchdog feeder as a systemd service:

    # /etc/systemd/system/wdt-feeder.service
    [Unit]
    Description=Watchdog Keepalive Feeder
    After=local-fs.target
    
    [Service]
    Type=simple
    ExecStart=/usr/local/bin/wdt-feeder
    Restart=always
    RestartSec=5
    
    [Install]
    WantedBy=multi-user.target
    

    And the wdt-feeder script:

    #!/bin/bash
    # /usr/local/bin/wdt-feeder
    
    exec 3>/dev/watchdog
    trap "echo V >&3; exec 3>&-; exit 0" SIGTERM SIGINT
    
    while true; do
        echo "1" >&3          # Ping the watchdog
        sleep 15              # Must be less than the watchdog timeout
    done
    

    The trap on SIGTERM ensures that when systemd stops this service gracefully, it sends the magic close character and does not trigger an unwanted reboot.


    12. Watchdog Timeout and What Happens When You Stop Pinging

    Let’s be very explicit about this because it is critical and people get it wrong.

    Watchdog Timeout Linux — How It Works Exactly

    The watchdog timeout (set via WDIOC_SETTIMEOUT or module parameters) is the number of seconds the watchdog will wait for a keepalive ping before triggering a system reset.

    If you set watchdog-timeout = 60 in your daemon config and interval = 10, then:

    • Every 10 seconds, the daemon pings the watchdog
    • The watchdog resets its 60-second countdown
    • If the daemon dies and no ping arrives for 60 seconds, the system reboots

    What happens during the gap? The kernel (or hardware) is decrementing the timer. For a hardware watchdog, this is happening in dedicated circuitry. For softdog, a kernel timer callback is scheduled, and it calls emergency_restart() when it fires.

    The nowayout Flag

    The nowayout parameter is a security feature. When nowayout=1:

    • Once the watchdog is started (i.e., /dev/watchdog is opened), it cannot be stopped
    • Writing 'V' (magic close) has no effect
    • Even closing the file descriptor does not stop it
    • The only way to stop it is a system reboot or a hardware reset

    This is ideal for production systems where you absolutely never want the watchdog to be accidentally disabled. A misbehaving process cannot disable the watchdog when nowayout=1.

    What If the System Partially Hangs?

    This is where the watchdog daemon’s health checks become really valuable. The daemon itself might still be running and pinging /dev/watchdog, but your critical application might have crashed or stopped responding.

    This is why /etc/watchdog.conf supports options like:

    • Monitoring file timestamps (file, change)
    • Running test binaries (test-binary)
    • Checking network connectivity (ping)
    • Monitoring load average (max-load-1, max-load-5)

    The daemon only pings the watchdog hardware if all these checks pass. If any check fails, the daemon stops pinging (or optionally runs a repair script first), and the hardware watchdog eventually fires and reboots the system.


    13. Watchdog in Embedded Linux — Why It Matters

    If you work on embedded Linux, the watchdog timer is not optional. It is one of those things that experienced embedded engineers treat as fundamental as memory management or interrupt handling.

    Here is why embedded systems rely so heavily on hardware watchdog Linux functionality:

    No human operator: Your device is deployed somewhere — a factory floor, a remote industrial site, an aircraft, a medical monitor. Nobody is sitting there ready to push the power button if it hangs.

    Harsh environments: Embedded systems face electrical noise, power glitches, temperature extremes, and all kinds of real-world conditions that can cause transient software faults, memory corruption, or timing issues that lead to system hangs.

    Long uptime requirements: A router or industrial controller might need to run for years without planned downtime. Without a watchdog, a single hang at 3 AM means everything is down until someone physically shows up.

    Remote management difficulty: Even with SSH or serial console access, a fully frozen kernel means no remote recovery. The watchdog is your last resort.

    Typical Embedded Watchdog Setup

    In a typical embedded Linux system:

    1. The bootloader (U-Boot) configures the hardware watchdog early in the boot sequence
    2. U-Boot pings the watchdog while it is loading the kernel
    3. The kernel driver takes over and keeps the watchdog fed during boot
    4. Once user space is up, the watchdog daemon or application takes over the keepalive
    5. If any stage fails to feed the watchdog, the system automatically recovers

    This is called a watchdog handoff chain, and getting it right is important. If there is any gap in the chain — say, the kernel takes too long to boot and the watchdog fires before the daemon starts — you get an early reboot loop.


    14. Kernel Space vs User Space Watchdog

    This is an important distinction that comes up when architecting watchdog-based reliability systems.

    User Space Watchdog

    Most production setups run the watchdog feeder in user space — either via the watchdog daemon or your own application directly writing to /dev/watchdog. This approach is great because:

    • If the user-space process crashes, the watchdog fires (which is what you want)
    • You can add application-level health checks easily
    • No kernel module modifications needed

    But there is a subtle problem: if your kernel schedules the watchdog process properly but your application is deadlocked in kernel space (stuck in a driver, waiting on an I/O lock, etc.), the watchdog daemon can still run and ping the watchdog — even though your system is effectively unusable.

    Kernel Space Watchdog

    In some embedded designs, particularly safety-critical systems, the watchdog is fed from within the kernel itself — either from a kernel thread or from a critical subsystem. This gives stronger guarantees because you can ensure the kernel scheduler itself is still running, not just user space.

    The hung_task_detection and softlockup kernel mechanisms complement the watchdog timer by detecting cases where the scheduler itself is stuck.

    In the kernel config:

    CONFIG_SOFTLOCKUP_DETECTOR=y
    CONFIG_DETECT_HUNG_TASK=y
    CONFIG_HARDLOCKUP_DETECTOR=y
    

    When a softlockup or hardlockup is detected, the kernel can automatically trigger a watchdog reset — giving you kernel-level watchdog reboot protection even when user space is fine.


    15. Linux Watchdog Configuration File Examples

    Here are some real-world watchdog configuration scenarios.

    Minimal Embedded System Config

    # /etc/watchdog.conf - minimal embedded config
    watchdog-device    = /dev/watchdog
    watchdog-timeout   = 30
    interval           = 10
    

    Server with Load and Memory Monitoring

    # /etc/watchdog.conf - server config with health checks
    watchdog-device    = /dev/watchdog
    watchdog-timeout   = 60
    interval           = 15
    
    # Load average thresholds (for 1/4/8 CPU system)
    max-load-1         = 24
    max-load-5         = 18
    max-load-15        = 12
    
    # Minimum free memory (in pages, 1 page = 4KB)
    min-memory         = 1
    
    # Ping the gateway to confirm network is alive
    ping               = 192.168.1.1
    ping-count         = 3
    
    # Check that application is writing its heartbeat file
    file               = /var/run/myapp.heartbeat
    change             = 60
    
    # Try to recover before rebooting
    repair-binary      = /usr/local/bin/try-recovery.sh
    
    log-dir            = /var/log/watchdog
    

    Industrial Embedded with Nowayout

    # /etc/watchdog.conf - industrial system, nowayout
    watchdog-device    = /dev/watchdog
    watchdog-timeout   = 15
    interval           = 5
    
    # Test the critical application
    test-binary        = /opt/myapp/health-check.sh
    test-timeout       = 60
    
    # No repair attempt — just reboot fast
    

    And the modprobe config to match:

    # /etc/modprobe.d/watchdog.conf
    options imx2_wdt nowayout=1
    

    16. Common Mistakes and How to Debug Your Watchdog Driver

    If you are writing or debugging a Linux watchdog driver, here are the pitfalls that bite almost everyone at least once.

    Mistake 1 — Forgetting the Magic Close

    Your test program opens /dev/watchdog, does something, then exits without writing 'V'. The system reboots 60 seconds later and you think your kernel is unstable. It is not — you just forgot the magic close.

    Fix: Always write 'V' before closing the device in test programs. Use the trap pattern in bash scripts.

    Mistake 2 — Setting Interval Greater Than Timeout

    If your ping interval is longer than the watchdog timeout, the watchdog fires before you ping it. Your system keeps rebooting every X seconds and you cannot figure out why.

    Fix: Make sure interval (in watchdog.conf or your code’s sleep time) is significantly less than the watchdog hardware timeout. A ratio of 3:1 or higher is recommended (e.g., ping every 10 seconds, timeout at 60 seconds).

    Mistake 3 — Not Handling SETTIMEOUT Return Value

    The kernel driver may round your requested timeout. If you request 45 seconds but the hardware only supports multiples of 8 seconds, you get 48. Always read back with WDIOC_GETTIMEOUT.

    Mistake 4 — Race Condition in Driver’s start/stop

    If your driver uses spinlocks or mutexes incorrectly in the start() and ping() operations, you can get a race between the timer callback and the ping — leading to either a missed reset or a spurious reset.

    Fix: Use consistent locking in all paths that touch the timer.

    Debugging Tips

    # Check kernel messages for watchdog events
    dmesg | grep -i "watchdog\|wdt"
    
    # Check if the device is being used
    lsof /dev/watchdog
    
    # Check what the watchdog daemon is doing
    sudo journalctl -u watchdog --no-pager -n 50
    
    # Check loaded watchdog kernel modules
    lsmod | grep -i "wdt\|watchdog"
    
    # View /proc for watchdog info (if supported by driver)
    cat /proc/sys/kernel/watchdog
    cat /proc/sys/kernel/watchdog_thresh
    

    17. FAQ — People Also Ask

    What is watchdog driver in Linux?

    A Linux watchdog driver is a kernel module that manages a watchdog timer — either hardware-based or software-based. It exposes the /dev/watchdog device to user space. Applications or the watchdog daemon periodically write to this device (“ping” it) to prevent the system from rebooting. If the ping stops coming within the configured timeout, the watchdog triggers an automatic system reset. It is a fundamental reliability mechanism used in servers, embedded systems, and anywhere Linux needs to self-recover from hangs.

    How does watchdog timer work in Linux?

    When you open /dev/watchdog, the countdown timer starts. Your application (or the watchdog daemon) must write to the device before the timer reaches zero. Each write resets the countdown. If the system hangs, the ping stops, the timer expires, and the watchdog triggers a reboot. For hardware watchdogs, this reset happens at the hardware level, independent of the CPU state. For software watchdogs (softdog), a kernel timer calls emergency_restart().

    What is /dev/watchdog in Linux?

    /dev/watchdog is a character device file (major 10, minor 130) that provides user-space access to the Linux watchdog subsystem. Opening it starts the watchdog timer. Writing to it sends a keepalive ping. Using ioctl() on it lets you get/set the timeout, query driver capabilities, check boot status, and more. Only one process can open it at a time. To safely stop the watchdog, write 'V' (the magic close character) before closing the file descriptor.

    How to enable watchdog in Linux kernel?

    Run make menuconfig in your kernel source directory, navigate to Device Drivers -> Watchdog Timer Support, and enable the relevant driver for your hardware (or softdog for software watchdog). Set it as built-in (y) for embedded systems. On a running system, load the module with sudo modprobe softdog (or your hardware-specific module name), and verify /dev/watchdog appears.

    Why watchdog is used in embedded Linux?

    Embedded Linux devices are often deployed without human operators nearby, need to run for months or years without planned maintenance, and operate in harsh environments that can cause transient faults. A hardware watchdog timer provides an automatic recovery mechanism — if the system hangs for any reason, the watchdog fires and reboots it, restoring normal operation without any human intervention. This is critical for routers, industrial controllers, medical devices, and automotive systems.

    How to reset system using watchdog Linux?

    The watchdog automatically resets the system when no keepalive ping is received within the timeout period. You can also trigger an intentional immediate reset by using the WDIOC_SETOPTIONS ioctl with the WDIOS_FORCERESET flag (if supported by your driver). For testing, you can simply stop pinging and wait for the timeout to expire — but make sure you really want a reboot first.

    What is watchdog timeout in Linux?

    The watchdog timeout is the number of seconds the watchdog timer will count down without a keepalive ping before triggering a system reset. It is configurable via the WDIOC_SETTIMEOUT ioctl command or via module parameters. After setting it, always read back the actual value with WDIOC_GETTIMEOUT because hardware watchdogs often round to the nearest supported value. Common timeout values range from 10 seconds to several minutes depending on the use case.

    What happens if watchdog is not pinged?

    If the watchdog timer reaches zero without receiving a keepalive ping, it triggers a system reset. For a hardware watchdog, this is a hard hardware reset — exactly like pulling the power and plugging it back in. For the software watchdog (softdog), the kernel calls emergency_restart(). After the reboot, you can check whether the watchdog caused the reset using the WDIOC_GETBOOTSTATUS ioctl, which returns WDIOF_CARDRESET if the last boot was watchdog-triggered.

    How to write watchdog driver in Linux?

    To write a Linux watchdog driver: include linux/watchdog.h, fill in a watchdog_ops struct with at minimum start(), stop(), and ping() functions, fill in a watchdog_device struct with your watchdog_ops, min/max timeout, and driver info, then call watchdog_register_device() in your probe function and watchdog_unregister_device() in your remove function. The watchdog framework handles all the character device boilerplate for you. See the full example code in Section 8 of this guide.


    18. Real-World Watchdog Scenarios and Patterns

    Before wrapping up, let’s look at a few real patterns that experienced engineers use in production systems. These go slightly beyond the basics but are important to know.

    Pattern 1 — Two-Stage Watchdog Timeout

    Some systems use a two-stage timeout approach. The first stage gives the system time to attempt recovery. The second stage triggers the hard reboot. You can simulate this with the repair-binary option in watchdog.conf:

    # Stage 1: attempt repair (90 seconds)
    repair-binary = /usr/local/bin/attempt-recovery.sh
    # Stage 2: reboot if repair fails (overall watchdog timeout)
    watchdog-timeout = 120
    interval = 10
    

    The repair script runs first. If it exits with 0, the daemon continues pinging and no reboot happens. If the script fails or times out, the daemon stops pinging, and the hardware watchdog fires.

    Pattern 2 — Application Heartbeat Through watchdog.conf

    Instead of your application writing to /dev/watchdog directly (which requires root and exclusive access), have it write a timestamp to a file. The watchdog daemon monitors that file and only pings the hardware watchdog if the file is being updated:

    # /etc/watchdog.conf
    watchdog-device  = /dev/watchdog
    watchdog-timeout = 60
    interval         = 10
    file             = /var/run/myapp.heartbeat
    change           = 30    # File must change within 30 seconds
    

    Your application just does:

    # In your app's main loop (no root needed, no exclusive device access)
    touch /var/run/myapp.heartbeat
    sleep 15
    

    This is a much cleaner architecture for complex applications and means you do not need to give your application root privileges just for watchdog access.

    Pattern 3 — Detecting Watchdog Resets in Your Application

    When your system reboots due to a watchdog timeout, you almost certainly want to know about it. Here is a startup check pattern:

    #include <linux/watchdog.h>
    #include <sys/ioctl.h>
    #include <fcntl.h>
    #include <syslog.h>
    
    void check_watchdog_reset(void)
    {
        int fd = open("/dev/watchdog", O_RDWR);
        int boot_status = 0;
    
        if (fd < 0) return;
    
        ioctl(fd, WDIOC_GETBOOTSTATUS, &boot_status);
    
        if (boot_status & WDIOF_CARDRESET) {
            syslog(LOG_CRIT, "WATCHDOG RESET DETECTED — system recovered from hang");
            /* Send alert to your monitoring system */
            /* Save a crash report */
            /* Increment a persistent reboot counter */
        }
    
        /* Now set up normal keepalive logic */
        /* ... */
    }
    

    Logging watchdog resets to persistent storage (or sending alerts) is essential for understanding your system’s reliability profile over time.

    Pattern 4 — Watchdog in a Multi-Process System

    In a complex system with multiple critical processes, none of them have exclusive access to /dev/watchdog (only one can open it). The standard pattern is:

    1. A single lightweight watchdog manager process opens /dev/watchdog and owns the keepalive loop.
    2. Each critical process writes to a named pipe, shared memory flag, or Unix socket to report it is alive.
    3. The watchdog manager only pings /dev/watchdog if all critical processes have checked in within the expected interval.
    4. If any process dies or stops responding, the manager stops pinging, and the watchdog fires.

    This gives you system-level watchdog protection that is aware of application-level health, without every application needing direct watchdog access.

    Working with Multiple Watchdog Devices

    Modern systems sometimes expose multiple watchdog timers (for example, a SoC might have both a hardware watchdog and a PMU watchdog). In that case, you get /dev/watchdog0, /dev/watchdog1, etc. The /dev/watchdog symlink points to /dev/watchdog0 by default.

    # Check all available watchdog devices
    ls /dev/watchdog*
    
    # The watchdog daemon can be configured for a specific device
    watchdog-device = /dev/watchdog1
    

    Each device is independent and managed by its own driver. You would typically only manage one in a given system configuration.


    19. Conclusion

    The Linux watchdog driver is one of those subsystems that is easy to overlook until you actually need it — and then you are really glad it exists. Whether you are working on a high-availability server that needs to self-recover from kernel hangs, or an industrial embedded system deployed in the middle of nowhere, the watchdog timer is your silent guardian.

    Here is a quick recap of what we covered:

    The Linux watchdog framework gives you a clean, standardized API. Hardware watchdogs are always preferred for production embedded systems because they survive kernel freezes. Software watchdog (softdog) is great for development, VMs, and systems without dedicated hardware. The /dev/watchdog device is your interface from user space — open it, ping it, close it with 'V' if you want to stop it gracefully. The watchdog daemon (watchdogd) plus /etc/watchdog.conf is the standard production setup on most Linux distributions. Writing your own driver is straightforward with the watchdog_ops and watchdog_device structures — fill in five functions and register with watchdog_register_device(). Module parameters like soft_margin and nowayout let you tune behavior at load time. Systemd integrates cleanly with the watchdog daemon for reliable production deployments.

    If you are just getting started, load softdog on your development machine right now (sudo modprobe softdog), play with the example code from Section 2, and get comfortable with how /dev/watchdog behaves. Once you understand the keepalive ping mechanism and the magic close character, the rest falls into place naturally.

    For embedded developers, spend time understanding the hardware watchdog specific to your SoC, configure nowayout=1 for production builds, and think carefully about your keepalive chain from bootloader to user space.

    The Linux watchdog ecosystem is mature, well-documented in the kernel source under Documentation/watchdog/, and used in millions of production deployments worldwide. It is worth understanding deeply.

    For detailed understanding of Platform Devices and Drivers on Linux, refer to the Linux documentation on Platform Devices and Drivers .

  • Linux Platform Driver Tutorial: Probe, Device Tree & Resources

    Updated October 8, 2026. This tutorial explains the Linux platform-driver model and shows a minimal driver that can bind to a matching firmware-described device. It is a learning example, not a driver for real hardware: the compatible string and binding must be supplied by a board or device-tree binding that actually describes the device.

    A platform driver does not create or discover arbitrary hardware by itself. The kernel enumerates a platform device—commonly from Device Tree, ACPI, or board setup—and the driver core matches it to a registered platform_driver. Only then does the core call probe().

    When to use a platform driver

    The platform bus is commonly used for SoC-integrated peripherals and some legacy devices with directly addressable resources. Devices on buses with their own discovery and driver models, such as PCI, USB, I2C, or SPI, should normally use that bus’s driver model instead. See the current Linux platform-device documentation before choosing the bus.

    1. Prepare a safe development environment

    Build against the headers and configuration for the target kernel. A virtual machine or QEMU test system is preferable while learning: an incorrect kernel module can crash or corrupt the running system. Do not load experimental modules on a production device.

    sudo apt update
    sudo apt install build-essential linux-headers-$(uname -r)
    ls /lib/modules/$(uname -r)/build

    On a cross-compiled embedded target, use that target’s kernel source, configuration, and toolchain rather than the headers for your development PC. Kernel APIs change over time, so the target kernel tree is the final authority.

    2. Write a minimal matching platform driver

    This example only logs when the driver binds and unbinds; it does not access registers or control hardware. The placeholder compatible value must be replaced with a real binding string already used by the target device tree. Without a matching enumerated device, the module can load successfully and probe() will not run.

    #include <linux/module.h>
    #include <linux/of.h>
    #include <linux/platform_device.h>
    
    static int example_probe(struct platform_device *pdev)
    {
        dev_info(&pdev->dev, "matched example platform device\n");
        return 0;
    }
    
    static void example_remove(struct platform_device *pdev)
    {
        dev_info(&pdev->dev, "example platform device removed\n");
    }
    
    static const struct of_device_id example_of_match[] = {
        { .compatible = "example,platform-device" },
        { }
    };
    MODULE_DEVICE_TABLE(of, example_of_match);
    
    static struct platform_driver example_driver = {
        .probe = example_probe,
        .remove = example_remove,
        .driver = {
            .name = "example-platform",
            .of_match_table = example_of_match,
        },
    };
    module_platform_driver(example_driver);
    
    MODULE_LICENSE("GPL");
    MODULE_DESCRIPTION("Minimal platform driver example");

    The empty final entry in the match table is required. MODULE_DEVICE_TABLE() makes the match information available to module tooling. Current upstream kernels use a void remove() callback; older kernel branches may use a different signature, so compile against the exact target headers and follow that tree’s platform_driver definition.

    3. Build the external module

    Save the source as example_platform.c and create a Makefile in the same directory:

    obj-m += example_platform.o
    KDIR ?= /lib/modules/$(shell uname -r)/build
    PWD := $(shell pwd)
    
    all:
    	$(MAKE) -C $(KDIR) M=$(PWD) modules
    
    clean:
    	$(MAKE) -C $(KDIR) M=$(PWD) clean

    Run make using the matching build tree. A successful build produces example_platform.ko. A module may still be rejected at load time if its vermagic, signing requirements, architecture, or kernel configuration do not match the running target.

    4. Describe real hardware with a binding

    For a Device Tree system, the device node is part of the board description and must follow the device’s documented binding. A node may describe resources such as registers, interrupts, clocks, resets, and power domains, but their addresses and specifiers are specific to the SoC and board. Do not copy fabricated addresses or interrupt numbers from a generic tutorial.

    For upstreamable drivers, add or update a YAML schema under Documentation/devicetree/bindings/ and validate it with the kernel’s Device Tree checks. Read the Device Tree usage model and the binding documentation for the target subsystem.

    Platform devices may also be enumerated through ACPI or board setup. An OF match table alone only matches Device Tree devices; add ACPI or platform-ID matching only when the hardware and target kernel require it.

    5. Acquire resources in probe()

    A real probe function should validate the device and acquire each resource through the device-managed APIs where appropriate. Common helpers include platform_get_resource() with devm_ioremap_resource() for a memory resource, platform_get_irq() for an IRQ, and the clock, reset, regulator, GPIO, and DMA frameworks for their respective resources. Check every return value, propagate errors, and use -EPROBE_DEFER when a required supplier is not ready yet.

    Do not dereference a missing resource, invent register layouts, or access MMIO before the resource has been reserved and mapped. Use the subsystem’s established helpers rather than raw addresses. Keep interrupt handlers short and move work that may sleep into a threaded handler or workqueue.

    6. Build, bind, and debug

    • Confirm that the device exists in the target’s firmware description and that its compatible string matches the driver table.
    • Check /sys/bus/platform/devices/ and /sys/bus/platform/drivers/ to inspect enumerated devices and registered drivers.
    • Read the exact kernel log message with dmesg or the system journal; distinguish a module-load failure from a device-match or probe failure.
    • Check the probe return value and resource errors. A deferred probe is different from a failed match.
    • Use a VM, QEMU, or a dedicated development board for module-load tests; remove the module and restore the test system if it becomes unstable.

    The official external-module build guide and platform bus documentation describe the supported build and driver-model interfaces.

    Common mistakes

    • Expecting probe() to run without an enumerated device that matches the driver.
    • Registering a fake platform device from the same driver module as a shortcut; device enumeration normally belongs to firmware or board setup.
    • Copying Device Tree register addresses, interrupt cells, or clock names from another board.
    • Ignoring errors from resource helpers or suppressing deferred-probe errors.
    • Using a remove() signature from a different kernel version or adding manual cleanup for resources already managed by devm APIs.
    • Treating a character device and a platform driver as alternatives: a platform driver binds hardware to a bus; a character-device interface is a separate userspace API that may be added when the hardware requires one.

    Further reading

    Start with the Linux platform driver model, the Device Tree usage model, and the Kbuild external-module documentation. For register-level work, continue with the documentation for the specific subsystem and board; a generic example cannot safely define hardware resources for you.

    For broader interview preparation, see our Linux device-driver interview roadmap.

  • Complete Linux Block Device Driver Project: Kernel Driver + Userspace Access Program

    Complete Linux Block Device Driver Project tutorial covering kernel module development and userspace access program. Learn how to build, register, and test a block device driver in Linux with step-by-step code examples, real-world concepts, and performance insights for beginners and advanced developers.

    This is a complete, working project. By the end of this guide you will have a Linux block device driver running as a kernel module, a userspace C program that opens the device and performs raw read/write operations on it, a working Makefile that builds both, and a clear understanding of every line of code.

    No hand-waving. No “left as an exercise.” Every file is here, complete and ready to compile.

    This is the kind of project that actually belongs in a portfolio if you are looking for embedded Linux or Linux kernel driver roles. It touches kernel module development, block layer internals, device node access from userspace, raw sector I/O, and ioctl communication all in one project.

    Before diving into implementation, it’s important to understand the fundamentals of how block device drivers work in Linux. If you’re new to this topic, check out this Linux Block Device Drivers: The Complete Beginner’s Guide, which explains core concepts like request queues, buffering, and kernel interactions in a simple and beginner-friendly way.

    Linux Block Device Driver Project

    1. Project Overview and Architecture

    Here is what we are building and how the pieces connect:

    
    +---------------------------+
    |     Userspace Program     |  userapp.c
    |  open() / read() / write()|
    |  ioctl() / lseek()        |
    +------------+--------------+
                 |
                 | /dev/myblkdev  (block device node)
                 |
    +------------+--------------+
    |      Linux Block Layer    |  Kernel block layer
    |  bio, request queue,      |
    |  blk-mq, scheduler        |
    +------------+--------------+
                 |
    +------------+--------------+
    |   myblkdev.ko             |  Our kernel module
    |   RAM-backed block driver |
    |   16MB virtual disk       |
    |   ioctl for device info   |
    +---------------------------+
                 |
         vmalloc'd RAM buffer
         (simulates disk storage)
    

    The kernel module registers a block device backed by a 16MB chunk of kernel RAM. It handles read and write requests through the blk-mq interface. It also exposes an ioctl command so userspace can query device information like size and sector count.

    The userspace program opens the device node directly using open(), writes a pattern to specific sectors using write(), reads it back and verifies it, and calls ioctl() to print device info.

    This is exactly how tools like dd, hdparm, and storage benchmark tools work at the lowest level.

    2. Prerequisites and Environment Setup

    Before you start, make sure your system has everything needed:

    # Install kernel headers for your running kernel
    sudo apt-get update
    sudo apt-get install linux-headers-$(uname -r) build-essential
    
    # Verify kernel headers are present
    ls /lib/modules/$(uname -r)/build
    # Should show a directory, not an error
    
    # Check your kernel version
    uname -r
    # This project is tested on 5.15+ and 6.x kernels

    You also need to run the load and test steps as root or with sudo. Block devices require root access for raw sector I/O from userspace.

    If you are on a VM (recommended for kernel development), make sure you can do sudo insmod and that your VM has at least 256MB of RAM free for the driver’s buffer plus kernel overhead.

    3. Project Directory Structure

    Create this layout on your machine:

    myblkdev/
    ├── myblkdev.c       # Kernel block device driver
    ├── myblkdev.h      # Shared header (kernel + userspace)
    ├── userapp.c         # Userspace test and access program
    └── Makefile           # Builds both kernel module and userapp
    mkdir myblkdev
    cd myblkdev

    All four files go in this single directory. The Makefile handles building the kernel module (which goes through the kernel build system) and the userspace binary (which is just a normal gcc compile) in one make command.

    4. Shared Header File : myblkdev.h

    This header is included by both the kernel driver and the userspace program. It defines the ioctl command numbers and the data structure used to exchange device information. Sharing this header between kernel and userspace is the standard kernel pattern — it ensures both sides agree on the exact binary layout of ioctl structures.

    /* myblkdev.h
     * Shared header between kernel driver and userspace program.
     * Defines ioctl interface for myblkdev block device driver.
     */
    
    #ifndef MYBLKDEV_H
    #define MYBLKDEV_H
    
    #include <linux/ioctl.h>
    
    /* Magic number for our driver's ioctl commands.
     * Pick something unlikely to conflict. Check Documentation/userspace-api/ioctl/ioctl-number.rst
     */
    #define MYBLKDEV_MAGIC  0xBD
    
    /* Device info structure returned by MYBLKDEV_GET_INFO ioctl */
    struct myblkdev_info {
        unsigned long total_bytes;    /* total device size in bytes */
        unsigned long sector_count;   /* total number of 512-byte sectors */
        unsigned int  sector_size;    /* sector size in bytes (always 512 here) */
        unsigned int  major;          /* device major number */
        unsigned int  minor;          /* device minor number */
    };
    
    /* ioctl command: get device info
     * Direction: kernel -> userspace (IOR = read from kernel's perspective)
     * Type: MYBLKDEV_MAGIC
     * Number: 1
     * Size: sizeof(struct myblkdev_info)
     */
    #define MYBLKDEV_GET_INFO  _IOR(MYBLKDEV_MAGIC, 1, struct myblkdev_info)
    
    /* ioctl command: reset device (zero all storage)
     * Direction: no data transfer
     * Type: MYBLKDEV_MAGIC
     * Number: 2
     */
    #define MYBLKDEV_RESET     _IO(MYBLKDEV_MAGIC, 2)
    
    #endif /* MYBLKDEV_H */

    The _IOR and _IO macros encode the direction, magic number, command number, and data size into a single 32-bit integer. This encoding prevents different drivers from accidentally sharing the same ioctl number, which would cause the wrong driver to handle the command.

    5. The Kernel Block Device Driver : myblkdev.c

    This is the full kernel driver. Read through it section by section — every part is commented to explain what it does and why.

    /* myblkdev.c
     * A RAM-backed Linux block device driver with ioctl support.
     * Registers /dev/myblkdev as a 16MB block device.
     *
     * Tested on Linux 5.15 - 6.x
     * License: GPL v2
     */
    
    #include <linux/module.h>
    #include <linux/kernel.h>
    #include <linux/init.h>
    #include <linux/fs.h>
    #include <linux/blkdev.h>
    #include <linux/blk-mq.h>
    #include <linux/genhd.h>
    #include <linux/hdreg.h>
    #include <linux/vmalloc.h>
    #include <linux/uaccess.h>
    #include "myblkdev.h"
    
    MODULE_LICENSE("GPL");
    MODULE_AUTHOR("myblkdev project");
    MODULE_DESCRIPTION("RAM-backed block device driver with ioctl");
    MODULE_VERSION("1.0");
    
    /* ===== Configuration ===== */
    #define MYBLKDEV_MAJOR       240        /* static major; 240-254 are for local use */
    #define MYBLKDEV_NAME        "myblkdev"
    #define MYBLKDEV_MINORS      1          /* 1 = no partitions; use 16 to allow partitions */
    #define DISK_SIZE_MB         16
    #define KERNEL_SECTOR_SIZE   512
    
    /* ===== Device State ===== */
    struct myblkdev_state {
        unsigned long    size;          /* device size in bytes */
        u8              *data;          /* vmalloc'd storage buffer */
        spinlock_t       lock;          /* protects data access */
        struct blk_mq_tag_set  tag_set;
        struct request_queue  *queue;
        struct gendisk        *gd;
        int              major;
        int              minor;
    };
    
    /* Single global device instance for this example */
    static struct myblkdev_state *mydev;
    
    /* ===== Request Handler ===== */
    /*
     * This is the core of the driver. The block layer calls this function
     * for every I/O request. We iterate over each segment of the request,
     * copy data to/from our RAM buffer, and signal completion.
     */
    static blk_status_t myblkdev_queue_rq(struct blk_mq_hw_ctx *hctx,
                                           const struct blk_mq_queue_data *bd)
    {
        struct request        *req  = bd->rq;
        struct myblkdev_state *dev  = req->q->queuedata;
        struct bio_vec         bvec;
        struct req_iterator    iter;
        loff_t                 pos;
        unsigned long          flags;
    
        /* Mark request as started — required by blk-mq before any processing */
        blk_mq_start_request(req);
    
        /* Reject non-filesystem requests (SCSI passthrough, etc.) */
        if (blk_rq_is_passthrough(req)) {
            pr_notice(MYBLKDEV_NAME ": skip passthrough request\n");
            blk_mq_end_request(req, BLK_STS_IOERR);
            return BLK_STS_OK;
        }
    
        /* Convert starting sector to byte offset.
         * blk_rq_pos() returns offset in 512-byte units always. */
        pos = (loff_t)blk_rq_pos(req) * KERNEL_SECTOR_SIZE;
    
        spin_lock_irqsave(&dev->lock, flags);
    
        /* Iterate over each scatter-gather segment in the request */
        rq_for_each_segment(bvec, req, iter) {
            void   *buf = page_address(bvec.bv_page) + bvec.bv_offset;
            size_t  len = bvec.bv_len;
    
            /* Bounds check — never go past our buffer */
            if (pos + len > dev->size) {
                pr_err(MYBLKDEV_NAME ": I/O beyond device size "
                       "(pos=%lld len=%zu size=%lu)\n",
                       pos, len, dev->size);
                spin_unlock_irqrestore(&dev->lock, flags);
                blk_mq_end_request(req, BLK_STS_IOERR);
                return BLK_STS_OK;
            }
    
            if (rq_data_dir(req) == WRITE) {
                /* WRITE: copy from bio page into our RAM buffer */
                memcpy(dev->data + pos, buf, len);
            } else {
                /* READ: copy from our RAM buffer into bio page */
                memcpy(buf, dev->data + pos, len);
            }
    
            pos += len;
        }
    
        spin_unlock_irqrestore(&dev->lock, flags);
    
        /* Signal successful completion to the block layer */
        blk_mq_end_request(req, BLK_STS_OK);
        return BLK_STS_OK;
    }
    
    /* blk-mq operations table — only queue_rq is mandatory */
    static const struct blk_mq_ops myblkdev_mq_ops = {
        .queue_rq = myblkdev_queue_rq,
    };
    
    /* ===== ioctl Handler ===== */
    /*
     * Userspace programs can call ioctl() on the block device fd to
     * get device info or trigger a reset. We handle our custom commands
     * here and return -ENOTTY for anything we don't recognize.
     */
    static int myblkdev_ioctl(struct block_device *bdev, fmode_t mode,
                               unsigned int cmd, unsigned long arg)
    {
        struct myblkdev_state *dev = bdev->bd_disk->private_data;
        struct myblkdev_info   info;
        unsigned long          flags;
    
        switch (cmd) {
    
        case MYBLKDEV_GET_INFO:
            /* Fill info structure and copy to userspace */
            info.total_bytes  = dev->size;
            info.sector_count = dev->size / KERNEL_SECTOR_SIZE;
            info.sector_size  = KERNEL_SECTOR_SIZE;
            info.major        = dev->major;
            info.minor        = dev->minor;
    
            if (copy_to_user((void __user *)arg, &info, sizeof(info)))
                return -EFAULT;
            return 0;
    
        case MYBLKDEV_RESET:
            /* Zero the entire storage buffer */
            spin_lock_irqsave(&dev->lock, flags);
            memset(dev->data, 0, dev->size);
            spin_unlock_irqrestore(&dev->lock, flags);
            pr_info(MYBLKDEV_NAME ": device reset (zeroed)\n");
            return 0;
    
        default:
            /* Unknown command — return standard "not a typewriter" error */
            return -ENOTTY;
        }
    }
    
    /* ===== Block Device Operations Table ===== */
    static const struct block_device_operations myblkdev_fops = {
        .owner          = THIS_MODULE,
        .ioctl          = myblkdev_ioctl,
    };
    
    /* ===== Module Init ===== */
    static int __init myblkdev_init(void)
    {
        int ret;
    
        pr_info(MYBLKDEV_NAME ": initializing %d MB RAM block device\n",
                DISK_SIZE_MB);
    
        /* Allocate our device state structure */
        mydev = kzalloc(sizeof(*mydev), GFP_KERNEL);
        if (!mydev) {
            pr_err(MYBLKDEV_NAME ": failed to allocate device state\n");
            return -ENOMEM;
        }
    
        mydev->size  = DISK_SIZE_MB * 1024 * 1024;
        mydev->major = MYBLKDEV_MAJOR;
        mydev->minor = 0;
        spin_lock_init(&mydev->lock);
    
        /* Allocate the RAM storage buffer using vmalloc.
         * We use vmalloc (not kmalloc) because we need a large contiguous
         * virtual mapping but don't need physically contiguous memory. */
        mydev->data = vmalloc(mydev->size);
        if (!mydev->data) {
            pr_err(MYBLKDEV_NAME ": failed to allocate %d MB storage\n",
                   DISK_SIZE_MB);
            ret = -ENOMEM;
            goto err_free_dev;
        }
        memset(mydev->data, 0, mydev->size);
    
        /* Register the block device major number */
        ret = register_blkdev(MYBLKDEV_MAJOR, MYBLKDEV_NAME);
        if (ret < 0) {
            pr_err(MYBLKDEV_NAME ": register_blkdev failed (%d)\n", ret);
            goto err_free_data;
        }
    
        /* Configure blk-mq tag set.
         * nr_hw_queues=1: single hardware queue (fine for RAM, no real HW)
         * queue_depth=128: up to 128 in-flight requests at once */
        mydev->tag_set.ops          = &myblkdev_mq_ops;
        mydev->tag_set.nr_hw_queues = 1;
        mydev->tag_set.queue_depth  = 128;
        mydev->tag_set.numa_node    = NUMA_NO_NODE;
        mydev->tag_set.cmd_size     = 0;
        mydev->tag_set.flags        = BLK_MQ_F_SHOULD_MERGE;
    
        ret = blk_mq_alloc_tag_set(&mydev->tag_set);
        if (ret) {
            pr_err(MYBLKDEV_NAME ": blk_mq_alloc_tag_set failed (%d)\n", ret);
            goto err_unreg_blkdev;
        }
    
        /* Create the request queue linked to our tag set */
        mydev->queue = blk_mq_init_queue(&mydev->tag_set);
        if (IS_ERR(mydev->queue)) {
            ret = PTR_ERR(mydev->queue);
            pr_err(MYBLKDEV_NAME ": blk_mq_init_queue failed (%d)\n", ret);
            goto err_free_tag_set;
        }
    
        /* Store device pointer in queue for access from request handler */
        mydev->queue->queuedata = mydev;
    
        /* Set queue limits — tell the block layer what our "hardware" supports */
        blk_queue_logical_block_size(mydev->queue, KERNEL_SECTOR_SIZE);
        blk_queue_max_hw_sectors(mydev->queue, 256); /* max 128KB per request */
    
        /* Allocate the gendisk structure.
         * MYBLKDEV_MINORS=1 means no partition support.
         * Use 16 if you want to allow up to 15 partitions. */
        mydev->gd = alloc_disk(MYBLKDEV_MINORS);
        if (!mydev->gd) {
            pr_err(MYBLKDEV_NAME ": alloc_disk failed\n");
            ret = -ENOMEM;
            goto err_free_queue;
        }
    
        /* Fill in gendisk fields */
        mydev->gd->major        = MYBLKDEV_MAJOR;
        mydev->gd->first_minor  = 0;
        mydev->gd->fops         = &myblkdev_fops;
        mydev->gd->queue        = mydev->queue;
        mydev->gd->private_data = mydev;
        snprintf(mydev->gd->disk_name, sizeof(mydev->gd->disk_name),
                 MYBLKDEV_NAME);
    
        /* Set disk capacity in 512-byte sectors */
        set_capacity(mydev->gd, mydev->size / KERNEL_SECTOR_SIZE);
    
        /* add_disk() makes the device visible — /dev/myblkdev appears after this */
        add_disk(mydev->gd);
    
        pr_info(MYBLKDEV_NAME ": ready — /dev/%s, major=%d, size=%lu MB\n",
                mydev->gd->disk_name, MYBLKDEV_MAJOR,
                mydev->size / (1024 * 1024));
        return 0;
    
    /* Error cleanup — reverse order of initialization */
    err_free_queue:
        blk_cleanup_queue(mydev->queue);
    err_free_tag_set:
        blk_mq_free_tag_set(&mydev->tag_set);
    err_unreg_blkdev:
        unregister_blkdev(MYBLKDEV_MAJOR, MYBLKDEV_NAME);
    err_free_data:
        vfree(mydev->data);
    err_free_dev:
        kfree(mydev);
        return ret;
    }
    
    /* ===== Module Exit ===== */
    static void __exit myblkdev_exit(void)
    {
        /* Cleanup in strict reverse order of init */
        del_gendisk(mydev->gd);          /* remove from block layer */
        put_disk(mydev->gd);             /* release gendisk reference */
        blk_cleanup_queue(mydev->queue); /* destroy request queue */
        blk_mq_free_tag_set(&mydev->tag_set);
        unregister_blkdev(MYBLKDEV_MAJOR, MYBLKDEV_NAME);
        vfree(mydev->data);              /* free RAM storage */
        kfree(mydev);                    /* free device state */
    
        pr_info(MYBLKDEV_NAME ": unloaded\n");
    }
    
    module_init(myblkdev_init);
    module_exit(myblkdev_exit);
    

    6. The Makefile

    This single Makefile builds both the kernel module and the userspace program. Run make once and you get both.

    # Makefile for myblkdev project
    # Builds kernel module (myblkdev.ko) and userspace program (userapp)
    
    # Kernel build directory — uses headers for currently running kernel
    KDIR ?= /lib/modules/$(shell uname -r)/build
    
    # Kernel module object
    obj-m += myblkdev.o
    
    # Default target: build kernel module then userspace app
    all: module userapp
    
    module:
    	$(MAKE) -C $(KDIR) M=$(PWD) modules
    
    # Userspace app: plain gcc, no kernel headers needed
    # _GNU_SOURCE for pread/pwrite, O_DIRECT etc.
    userapp: userapp.c myblkdev.h
    	gcc -Wall -Wextra -O2 -D_GNU_SOURCE -o userapp userapp.c
    
    clean:
    	$(MAKE) -C $(KDIR) M=$(PWD) clean
    	rm -f userapp
    
    .PHONY: all module userapp clean

    The kernel module build goes through the kernel’s own build system (make -C $(KDIR)). The userspace binary is just a normal gcc compile. The shared myblkdev.h header is included by both — that dependency is declared on the userapp line so it rebuilds if the header changes.

    7. The Userspace Access Program : userapp.c

    This program demonstrates every way a userspace application can interact with a raw block device. It opens the device node directly, queries info via ioctl, writes test patterns to specific sectors, reads them back, and verifies the data.

    /* userapp.c
     * Userspace program to test and demonstrate myblkdev block driver access.
     * Performs: ioctl info query, raw sector write, raw sector read, verify.
     *
     * Run as root: sudo ./userapp /dev/myblkdev
     */
    
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <fcntl.h>
    #include <errno.h>
    #include <sys/ioctl.h>
    #include <sys/types.h>
    
    /* Include our shared header.
     * For userspace, linux/ioctl.h is included via sys/ioctl.h already.
     * We redefine to use userspace-safe types. */
    #include "myblkdev.h"
    
    #define SECTOR_SIZE     512
    #define TEST_SECTORS    8       /* write/read 8 sectors = 4KB at a time */
    #define TEST_PATTERN    0xAB    /* byte pattern to write for verification */
    
    /* ===== Helper: print device info from ioctl ===== */
    static void print_device_info(int fd)
    {
        struct myblkdev_info info;
    
        printf("\n--- Device Info (via ioctl) ---\n");
    
        if (ioctl(fd, MYBLKDEV_GET_INFO, &info) < 0) {
            perror("ioctl MYBLKDEV_GET_INFO failed");
            return;
        }
    
        printf("  Total Size   : %lu bytes (%lu MB)\n",
               info.total_bytes, info.total_bytes / (1024 * 1024));
        printf("  Sector Count : %lu sectors\n", info.sector_count);
        printf("  Sector Size  : %u bytes\n", info.sector_size);
        printf("  Major Number : %u\n", info.major);
        printf("  Minor Number : %u\n", info.minor);
        printf("-------------------------------\n\n");
    }
    
    /* ===== Helper: write pattern to a sector range ===== */
    static int write_sectors(int fd, off_t start_sector,
                              int num_sectors, unsigned char pattern)
    {
        size_t   buf_size = num_sectors * SECTOR_SIZE;
        unsigned char *buf;
        ssize_t  written;
        off_t    offset = start_sector * SECTOR_SIZE;
    
        buf = malloc(buf_size);
        if (!buf) {
            fprintf(stderr, "malloc failed\n");
            return -1;
        }
    
        /* Fill buffer with test pattern */
        memset(buf, pattern, buf_size);
    
        /* Seek to the correct byte offset on the block device */
        if (lseek(fd, offset, SEEK_SET) < 0) {
            perror("lseek failed");
            free(buf);
            return -1;
        }
    
        /* Write the buffer — this goes through VFS -> block layer -> our driver */
        written = write(fd, buf, buf_size);
        if (written != (ssize_t)buf_size) {
            fprintf(stderr, "write: expected %zu bytes, got %zd (%s)\n",
                    buf_size, written, strerror(errno));
            free(buf);
            return -1;
        }
    
        printf("[WRITE] Sector %ld - %ld: wrote %zu bytes, pattern=0x%02X\n",
               (long)start_sector, (long)(start_sector + num_sectors - 1),
               buf_size, pattern);
    
        free(buf);
        return 0;
    }
    
    /* ===== Helper: read from sector range and verify pattern ===== */
    static int read_and_verify(int fd, off_t start_sector,
                                int num_sectors, unsigned char expected_pattern)
    {
        size_t    buf_size = num_sectors * SECTOR_SIZE;
        unsigned char *buf;
        ssize_t   bytes_read;
        off_t     offset = start_sector * SECTOR_SIZE;
        int       errors = 0;
        size_t    i;
    
        buf = malloc(buf_size);
        if (!buf) {
            fprintf(stderr, "malloc failed\n");
            return -1;
        }
    
        /* Seek to sector offset */
        if (lseek(fd, offset, SEEK_SET) < 0) {
            perror("lseek failed");
            free(buf);
            return -1;
        }
    
        /* Read back the data */
        bytes_read = read(fd, buf, buf_size);
        if (bytes_read != (ssize_t)buf_size) {
            fprintf(stderr, "read: expected %zu bytes, got %zd (%s)\n",
                    buf_size, bytes_read, strerror(errno));
            free(buf);
            return -1;
        }
    
        /* Verify every byte matches the expected pattern */
        for (i = 0; i < buf_size; i++) {
            if (buf[i] != expected_pattern) {
                fprintf(stderr, "[VERIFY] Mismatch at byte %zu: "
                        "got 0x%02X, expected 0x%02X\n",
                        i, buf[i], expected_pattern);
                errors++;
                if (errors > 5) {
                    fprintf(stderr, "[VERIFY] Too many errors, stopping\n");
                    break;
                }
            }
        }
    
        if (errors == 0) {
            printf("[READ]  Sector %ld - %ld: read %zu bytes — VERIFIED OK "
                   "(pattern=0x%02X)\n",
                   (long)start_sector,
                   (long)(start_sector + num_sectors - 1),
                   buf_size, expected_pattern);
        } else {
            printf("[READ]  Sector %ld - %ld: VERIFICATION FAILED "
                   "(%d errors)\n",
                   (long)start_sector,
                   (long)(start_sector + num_sectors - 1),
                   errors);
        }
    
        free(buf);
        return errors ? -1 : 0;
    }
    
    /* ===== Helper: dump first 64 bytes of a sector as hex ===== */
    static void hexdump_sector(int fd, off_t sector)
    {
        unsigned char buf[64];
        ssize_t       n;
        size_t        i;
    
        lseek(fd, sector * SECTOR_SIZE, SEEK_SET);
        n = read(fd, buf, sizeof(buf));
        if (n <= 0) {
            perror("hexdump read failed");
            return;
        }
    
        printf("\n[HEXDUMP] First 64 bytes of sector %ld:\n", (long)sector);
        for (i = 0; i < (size_t)n; i++) {
            if (i % 16 == 0) printf("  %04zx: ", i);
            printf("%02x ", buf[i]);
            if (i % 16 == 15) printf("\n");
        }
        printf("\n");
    }
    
    /* ===== Helper: reset device via ioctl ===== */
    static void reset_device(int fd)
    {
        printf("[RESET] Sending MYBLKDEV_RESET ioctl...\n");
        if (ioctl(fd, MYBLKDEV_RESET) < 0) {
            perror("ioctl MYBLKDEV_RESET failed");
            return;
        }
        printf("[RESET] Device zeroed successfully\n");
    }
    
    /* ===== Main ===== */
    int main(int argc, char *argv[])
    {
        const char *devpath;
        int         fd;
        int         ret = 0;
    
        if (argc < 2) {
            fprintf(stderr, "Usage: %s \n", argv[0]);
            fprintf(stderr, "Example: sudo %s /dev/myblkdev\n", argv[0]);
            return EXIT_FAILURE;
        }
    
        devpath = argv[1];
    
        printf("===========================================\n");
        printf("  myblkdev Userspace Test Program\n");
        printf("  Device: %s\n", devpath);
        printf("===========================================\n");
    
        /* Open the block device.
         * O_RDWR: we need both read and write access.
         * O_SYNC: write() calls wait for I/O completion before returning.
         *         Important for testing — without this, writes may be
         *         cached by the page cache and not reach the driver immediately. */
        fd = open(devpath, O_RDWR | O_SYNC);
        if (fd < 0) {
            fprintf(stderr, "open(%s) failed: %s\n", devpath, strerror(errno));
            fprintf(stderr, "Did you load the module? Are you running as root?\n");
            return EXIT_FAILURE;
        }
    
        printf("[OPEN]  Opened %s successfully (fd=%d)\n\n", devpath, fd);
    
        /* Step 1: Query device info via ioctl */
        print_device_info(fd);
    
        /* Step 2: Write pattern 0xAB to sectors 0-7 (first 4KB) */
        printf("--- Test 1: Write and Read First 4KB ---\n");
        if (write_sectors(fd, 0, TEST_SECTORS, TEST_PATTERN) < 0) {
            ret = 1;
            goto out;
        }
    
        /* Step 3: Read back and verify */
        if (read_and_verify(fd, 0, TEST_SECTORS, TEST_PATTERN) < 0) {
            ret = 1;
            goto out;
        }
    
        /* Step 4: Hexdump sector 0 to visually confirm */
        hexdump_sector(fd, 0);
    
        /* Step 5: Write different pattern to a sector in the middle of the disk */
        printf("\n--- Test 2: Write and Read Middle of Disk ---\n");
        {
            off_t mid_sector = (DISK_SIZE_MB * 1024 * 1024 / SECTOR_SIZE) / 2;
            printf("[INFO]  Mid-disk sector: %ld\n", (long)mid_sector);
    
            if (write_sectors(fd, mid_sector, TEST_SECTORS, 0xCD) < 0) {
                ret = 1;
                goto out;
            }
            if (read_and_verify(fd, mid_sector, TEST_SECTORS, 0xCD) < 0) {
                ret = 1;
                goto out;
            }
            hexdump_sector(fd, mid_sector);
        }
    
        /* Step 6: Verify sector 0 still has original pattern
         * (mid-disk write must not have corrupted sector 0) */
        printf("\n--- Test 3: Re-verify Sector 0 (Isolation Check) ---\n");
        if (read_and_verify(fd, 0, TEST_SECTORS, TEST_PATTERN) < 0) {
            ret = 1;
            goto out;
        }
    
        /* Step 7: Reset device and verify sector 0 is now zeroed */
        printf("\n--- Test 4: Reset Device and Verify Zeroed ---\n");
        reset_device(fd);
        if (read_and_verify(fd, 0, TEST_SECTORS, 0x00) < 0) {
            ret = 1;
            goto out;
        }
    
        printf("\n===========================================\n");
        if (ret == 0)
            printf("  ALL TESTS PASSED\n");
        else
            printf("  SOME TESTS FAILED — check output above\n");
        printf("===========================================\n");
    
    out:
        close(fd);
        return ret ? EXIT_FAILURE : EXIT_SUCCESS;
    }
    

    8. How the ioctl Interface Works

    The ioctl path is worth understanding clearly because it is the standard way userspace programs communicate non-I/O control information to block and character drivers.

    When userspace calls:

    ioctl(fd, MYBLKDEV_GET_INFO, &info);

    The kernel routes this through VFS to blkdev_ioctl() in the block layer, which checks if it is a standard block ioctl (like BLKGETSIZE, BLKRRPART). If not recognized, it calls your driver’s .ioctl function pointer — which is myblkdev_ioctl() in our driver.

    Inside the driver, copy_to_user() safely copies data from kernel space to the userspace buffer pointer that was passed in as the third argument. You must always use copy_to_user and copy_from_user for this — never dereference a userspace pointer directly in kernel code. It will either fault or silently access wrong memory.

    The _IOR, _IOW, _IOWR, and _IO macros encode direction and size into the command number so the kernel’s ioctl dispatch can do basic sanity checking on the argument size before even calling your handler.

    9. Build, Load, and Run : Step by Step

    # Step 1: Build everything
    cd myblkdev
    make
    
    # You should see:
    # CC [M] /path/to/myblkdev/myblkdev.o
    # LD [M] /path/to/myblkdev/myblkdev.ko
    # gcc -Wall -Wextra -O2 -D_GNU_SOURCE -o userapp userapp.c
    
    # Step 2: Load the kernel module
    sudo insmod myblkdev.ko
    
    # Step 3: Verify it loaded and device node exists
    dmesg | tail -5
    # Expected: myblkdev: ready — /dev/myblkdev, major=240, size=16 MB
    
    ls -la /dev/myblkdev
    # Expected: brw-rw---- 1 root disk 240, 0 ... /dev/myblkdev
    
    cat /proc/devices | grep myblkdev
    # Expected: 240 myblkdev
    
    # Step 4: Run the userspace test program
    sudo ./userapp /dev/myblkdev
    
    # Step 5: When done, unload the module
    sudo rmmod myblkdev
    dmesg | tail -3
    # Expected: myblkdev: unloaded

    10. Expected Output

    When you run sudo ./userapp /dev/myblkdev you should see exactly this:

    ===========================================
      myblkdev Userspace Test Program
      Device: /dev/myblkdev
    ===========================================
    [OPEN]  Opened /dev/myblkdev successfully (fd=3)
    
    --- Device Info (via ioctl) ---
      Total Size   : 16777216 bytes (16 MB)
      Sector Count : 32768 sectors
      Sector Size  : 512 bytes
      Major Number : 240
      Minor Number : 0
    -------------------------------
    
    --- Test 1: Write and Read First 4KB ---
    [WRITE] Sector 0 - 7: wrote 4096 bytes, pattern=0xAB
    [READ]  Sector 0 - 7: read 4096 bytes — VERIFIED OK (pattern=0xAB)
    
    [HEXDUMP] First 64 bytes of sector 0:
      0000: ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab
      0010: ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab
      0020: ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab
      0030: ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab
    
    --- Test 2: Write and Read Middle of Disk ---
    [INFO]  Mid-disk sector: 16384
    [WRITE] Sector 16384 - 16391: wrote 4096 bytes, pattern=0xCD
    [READ]  Sector 16384 - 16391: read 4096 bytes — VERIFIED OK (pattern=0xCD)
    
    [HEXDUMP] First 64 bytes of sector 16384:
      0000: cd cd cd cd cd cd cd cd cd cd cd cd cd cd cd cd
      ...
    
    --- Test 3: Re-verify Sector 0 (Isolation Check) ---
    [READ]  Sector 0 - 7: read 4096 bytes — VERIFIED OK (pattern=0xAB)
    
    --- Test 4: Reset Device and Verify Zeroed ---
    [RESET] Sending MYBLKDEV_RESET ioctl...
    [RESET] Device zeroed successfully
    [READ]  Sector 0 - 7: read 4096 bytes — VERIFIED OK (pattern=0x00)
    
    ===========================================
      ALL TESTS PASSED
    ===========================================

    11. How Userspace Actually Talks to the Block Driver

    This is worth tracing through the full kernel call path for both the write and ioctl cases, because understanding this path is what separates someone who just ran the code from someone who actually understands it.

    write() Call Path

    userapp: write(fd, buf, 4096)
      |
      v
    syscall: sys_write()
      |
      v
    VFS: vfs_write() -> block_write_iter()
      |
      v
    Page Cache: data written to page cache first
      |
      v (O_SYNC forces immediate flush)
    Block Layer: submit_bio()
      -> bio created with our data pages
      -> bio submitted to request queue
      -> blk-mq merges/dispatches
      |
      v
    myblkdev_queue_rq()  <-- OUR DRIVER
      -> rq_for_each_segment loops over bio_vec segments
      -> memcpy(dev->data + pos, buf, len)  for WRITE
      -> blk_mq_end_request(req, BLK_STS_OK)
      |
      v
    Block layer signals completion
      -> page cache marks page clean (O_SYNC path)
      |
      v
    write() returns 4096 to userspace

    ioctl() Call Path

    userapp: ioctl(fd, MYBLKDEV_GET_INFO, &info)
      |
      v
    syscall: sys_ioctl()
      |
      v
    VFS: vfs_ioctl() -> blkdev_ioctl()
      |
      v (not a standard block ioctl)
    myblkdev_ioctl()  <-- OUR DRIVER
      -> copy_to_user(&info, kernel_info, sizeof(info))
      -> return 0
      |
      v
    ioctl() returns 0 to userspace
    userspace info struct now has device data

    The key thing to notice: userspace never directly accesses the driver’s memory. Everything goes through the kernel’s VFS and block layer. The copy_to_user() in the driver is the only point where kernel-owned data crosses into user-owned memory, and it does so safely through kernel-controlled copy routines that check permissions.

    12. Extending the Project

    Here are four concrete things you can add to this project to take it further:

    Add Partition Support

    Change MYBLKDEV_MINORS from 1 to 16. Then after loading, run fdisk /dev/myblkdev to create partitions. The kernel will automatically create /dev/myblkdev1, /dev/myblkdev2, etc. Your driver handles them with zero additional code because the block layer translates offsets for you.

    Add a Module Parameter for Disk Size

    static int disk_size_mb = DISK_SIZE_MB;
    module_param(disk_size_mb, int, 0444);
    MODULE_PARM_DESC(disk_size_mb, "Disk size in MB (default 16)");

    Then load with: sudo insmod myblkdev.ko disk_size_mb=64

    Add Multiple Device Instances

    Change the single global device to an array and support creating multiple devices at load time, like the real brd driver does with /dev/ram0, /dev/ram1, etc.

    Format and Mount It

    sudo mkfs.ext4 /dev/myblkdev
    sudo mkdir /mnt/myblkdev
    sudo mount /dev/myblkdev /mnt/myblkdev
    echo "data survives in RAM until rmmod" | sudo tee /mnt/myblkdev/note.txt
    cat /mnt/myblkdev/note.txt
    sudo umount /mnt/myblkdev

    13. Troubleshooting Common Errors

    insmod: ERROR: could not insert module myblkdev.ko: Invalid module format

    Your kernel headers do not match your running kernel. Run uname -r and verify that /lib/modules/$(uname -r)/build exists and matches. On Ubuntu after a kernel update, run sudo apt-get install linux-headers-$(uname -r) again.

    open(/dev/myblkdev) failed: No such file or directory

    The module is not loaded or load failed. Run dmesg | tail -20 to see the error. Also check lsmod | grep myblkdev.

    open(/dev/myblkdev) failed: Permission denied

    Run with sudo. Block devices require root for raw access. Alternatively: sudo chmod 666 /dev/myblkdev for testing only.

    ioctl MYBLKDEV_GET_INFO failed: Inappropriate ioctl for device

    This means the ioctl command number does not match between userapp and the loaded driver. Make sure you rebuilt both after any header change. Run make clean && make, then sudo rmmod myblkdev && sudo insmod myblkdev.ko.

    Kernel panic on rmmod

    Almost always caused by a cleanup order bug. The most common one: calling put_disk() before del_gendisk(), or calling unregister_blkdev() while requests are still in flight. Make sure your exit function matches the exact order in the code above.

    VERIFICATION FAILED in userapp output

    Check dmesg for any error messages from the driver. The most common causes are wrong sector arithmetic (off-by-one in byte offset calculation) or a missing spinlock that lets two requests race on the same buffer region.

    Project File Summary

    FilePurposeLines
    myblkdev.hShared ioctl definitions, magic numbers, info struct~50
    myblkdev.cKernel block device driver — full blk-mq + ioctl~220
    userapp.cUserspace test program — open, write, read, ioctl, verify~230
    MakefileBuilds kernel module and userspace binary together~15

    Final Thoughts

    This project gives you a working end-to-end example of how the Linux block I/O stack operates from the userspace open() call all the way down to your driver’s request handler. Every layer in between VFS, page cache, block layer, blk-mq scheduler, bio structure, scatter-gather segments is exercised by running this code.

    The most important habit to build from here: whenever something does not work, start with dmesg. The kernel logs everything your driver prints and every oops or warning. It is your primary debugging window and it will tell you exactly where something went wrong almost every time.

    From this foundation, the next logical project is adding DMA support to talk to real hardware, or building a stacked driver using the Device Mapper framework that encrypts data before passing it to this RAM disk. Both use exactly the same block layer interfaces you just learned.

    For detailed understanding of Platform Devices and Drivers on Linux, refer to the Linux documentation on Platform Devices and Drivers .

  • Linux Block Device Drivers : The Complete Beginner’s Guide

    Learn Linux block device drivers from scratch how they work, how to write one, request queues, bio structures, blk-mq, and real kernel code examples. The most complete beginner guide

    you’ve ever wondered how your Linux system actually talks to a hard disk, an SSD, or even a RAM-based virtual disk block device drivers are the answer. They sit right in the middle, between the kernel’s file system layer and the raw hardware, quietly doing one of the most critical jobs in the entire OS.

    This guide is for people who already know C, have some basic Linux kernel awareness, and want to actually understand block device drivers not just skim a Wikipedia summary. We’re going to go deep. We’ll look at the architecture, write real code, understand request queues, dissect the bio structure, and by the end you’ll have a working skeleton driver you can actually compile and test.

    No fluff. No corporate jargon. Just kernel internals explained like we’re sitting at a coffee shop with a whiteboard between us

    1. What Is a Block Device in Linux?

    A block device is any storage device that Linux treats as a collection of fixed-size blocks typically 512 bytes or 4096 bytes each. You can read or write any individual block independently, in any order. That’s the defining characteristic.

    Your /dev/sda, /dev/nvme0n1, /dev/mmcblk0 these are all block devices. Even /dev/loop0 (a loopback device backed by a file) is a block device. The kernel doesn’t care if the actual storage is a spinning HDD, an NVMe SSD, an SD card, or a chunk of RAM. As long as the driver exposes the right interface, the kernel’s VFS and file system layers talk to it identically.

    The kernel represents block devices under /dev/ and exposes them with major and minor numbers. The major number identifies the driver, and the minor number identifies the specific device or partition that driver manages.

    $ ls -l /dev/sda*
    brw-rw---- 1 root disk 8, 0 Mar 30 09:15 /dev/sda
    brw-rw---- 1 root disk 8, 1 Mar 30 09:15 /dev/sda1
    brw-rw---- 1 root disk 8, 2 Mar 30 09:15 /dev/sda2

    The b at the start means block device. Major number 8 belongs to the sd driver (SCSI disk). Minor 0 is the whole disk; 1, 2 are partitions.

    2. Block Devices vs Character Devices : What’s the Real Difference?

    People learning Linux device drivers always hit this question early. Here’s the honest answer:

    Character devices move data as a stream, byte by byte, sequentially. Think serial ports, keyboards, audio devices. You can’t seek to byte 4000 on a keyboard that makes no sense. The kernel doesn’t buffer data for character devices in any sophisticated way; what you write is what gets sent.

    Block devices are random-access, block-addressable storage. The kernel’s block layer sits between the file system and the driver. It buffers I/O requests, reorders them for efficiency (that’s the I/O scheduler), merges adjacent requests, and handles caching through the page cache. The driver never talks directly to a file system it talks to the block layer, which handles all that complexity above it.

    This distinction matters a lot when you’re writing a driver. A character driver implements file_operations like read() and write(). A block device driver implements a request handler (or a make_request_fn) and registers a gendisk structure. The kernel does the rest of the heavy lifting.

    Another practical difference: block devices support partitioning. You can take /dev/sda and carve it into /dev/sda1, /dev/sda2. The partition table is parsed by the kernel when you register the disk. Character devices don’t have this concept.

    3. The Linux Kernel I/O Stack : The Big Picture

    Before you write a single line of driver code, you need this mental model. The Linux I/O stack has several layers, and knowing where your driver sits changes everything about how you write it.

    From top to bottom:

    1. User Space : your application calls read() or write() on a file
    2. VFS (Virtual File System) : the generic file system interface. Routes calls to the correct file system (ext4, xfs, btrfs, etc.)
    3. Page Cache : kernel-managed RAM buffer. If data is already cached, it never even hits the driver
    4. File System Layer : ext4, xfs, etc. convert file offsets to block addresses
    5. Block Layer : this is where your driver interfaces. It takes block I/O requests, schedules them, merges them, and dispatches to the driver
    6. Block Device Driver : your code. Translates kernel requests into hardware commands
    7. Hardware : the actual disk controller, SSD, or RAM

    Your block device driver lives at layer 6. It never sees file names, file offsets, or inodes. All it gets are sector numbers and memory buffers. “Read 8 sectors starting at sector 1024 into this memory address.” That’s the level of abstraction you work at.

    This is why writing block drivers is actually simpler in some ways than file systems you don’t deal with the complexity of the FS. But it’s harder in other ways because you deal directly with kernel memory management, DMA, and hardware timing.

    4. Block Layer Internals : How I/O Requests Actually Flow

    The block layer is one of the most sophisticated pieces of the Linux kernel. Here’s what happens between “user calls read()” and “driver gets the request”.

    Step 1: The File System Submits a bio

    When a file system needs to read or write blocks, it creates a bio (Block I/O) structure and submits it to the block layer using submit_bio(). The bio contains the target block device, the starting sector, the data buffer (as a list of memory pages), and the direction (read or write).

    Step 2: The Block Layer Processes the bio

    The block layer does several things with incoming bios:

    • Merging: If two requests target adjacent sectors, merge them into one. Fewer requests = better throughput, especially on spinning disks.
    • Sorting: Reorder requests to minimize seek time on HDDs (the classic “elevator algorithm”).
    • Batching: Group requests and dispatch them together to reduce per-request overhead.

    Step 3: Driver Receives and Processes the Request

    The driver’s request function gets called. It reads the sector address and data buffer from the request, does the actual I/O (memory copy for RAM disks, DMA transfer for real hardware), and signals completion by calling blk_mq_end_request().

    The block layer then updates the page cache, wakes up any waiting processes, and the user-space read() call finally returns.

    5. The bio Structure : The Kernel’s Fundamental I/O Unit

    If you want to write block device drivers, you need to understand struct bio cold. It’s defined in include/linux/blk_types.h and it’s the main data structure that flows through the entire block layer.

    struct bio {
        struct bio          *bi_next;       /* request queue link */
        struct block_device *bi_bdev;       /* target block device */
        unsigned int         bi_opf;        /* operation and flags */
        blk_status_t         bi_status;     /* error status */
        struct bvec_iter     bi_iter;       /* current position */
        bio_end_io_t        *bi_end_io;    /* completion callback */
        void                *bi_private;    /* driver private data */
        unsigned short       bi_vcnt;       /* number of bio_vecs */
        struct bio_vec       bi_io_vec[0]; /* the actual data vectors */
    };
    

    The key parts:

    bi_iter : Where You Are in the I/O

    struct bvec_iter {
        sector_t    bi_sector;   /* device sector (512-byte units) */
        unsigned int bi_size;    /* remaining bytes */
        unsigned int bi_idx;     /* current bio_vec index */
        unsigned int bi_bvec_done; /* bytes done in current bvec */
    };
    

    bi_iter.bi_sector tells you which sector on disk this I/O starts at. Always work with this, never with raw byte offsets.

    bio_vec : The Scatter-Gather List

    A bio doesn’t always point to a single contiguous memory buffer. It uses a scatter-gather list of bio_vec structures, each pointing to a page of memory plus an offset and length within that page.

    struct bio_vec {
        struct page *bv_page;   /* pointer to the page */
        unsigned int bv_len;    /* length of this segment */
        unsigned int bv_offset; /* offset within the page */
    };
    

    To iterate over all segments in a bio, use the bio_for_each_segment macro:

    struct bio_vec bvec;
    struct bvec_iter iter;
    
    bio_for_each_segment(bvec, bio, iter) {
        void *kaddr = kmap_atomic(bvec.bv_page);
        /* do your I/O with kaddr + bvec.bv_offset, length bvec.bv_len */
        kunmap_atomic(kaddr);
    }
    

    bi_opf : Operation Flags

    This field tells you what kind of I/O to do. Use bio_data_dir(bio) to get the direction: READ or WRITE. For writes, you read data from the bio’s pages and write to storage. For reads, you write data into the bio’s pages from storage.

    Never try to directly check bi_opf for direction always use the helper macro. The flags field has multiple bits packed together and you’ll get it wrong if you poke at it directly.

    6. Request Queues and the I/O Scheduler

    Every block device in Linux has an associated request queue struct request_queue. This is the object that sits between the block layer and your driver. It holds pending I/O requests, scheduler configuration, queue limits, and a reference to your driver’s request-handling function.

    Queue Limits : Telling the Kernel What Your Device Can Do

    One of the most important things you set up when registering a block driver is the queue limits. These tell the block layer what your hardware can and can’t handle, so it can merge and split requests appropriately.

    blk_queue_max_hw_sectors(queue, max_sectors);
    blk_queue_max_segments(queue, max_segments);
    blk_queue_logical_block_size(queue, 512);
    blk_queue_physical_block_size(queue, 4096);
    

    If you say your device supports max 256 sectors per request, the block layer will never send you a request larger than that. If you support scatter-gather with up to 128 segments, the layer respects that too. Getting these wrong leads to subtle corruption or panics, so set them carefully.

    I/O Schedulers

    The I/O scheduler (also called the block scheduler) decides in what order to dispatch requests from the queue to the driver. Linux has had several over the years:

    • CFQ (Completely Fair Queuing) : removed in kernel 5.0, was the default for years
    • Deadline : prioritizes requests by deadline, good for SSDs and databases
    • BFQ (Budget Fair Queuing) : interactive workload friendly, groups processes
    • mq-deadline : the multi-queue version of Deadline, common default today
    • none : no reordering, FIFO, used for NVMe and fast storage

    You can check and change the scheduler at runtime:

    $ cat /sys/block/sda/queue/scheduler
    mq-deadline [kyber] bfq none
    
    $ echo none > /sys/block/nvme0n1/queue/scheduler

    For simple drivers (RAM disks, virtual devices), you usually use BLK_MQ_F_SHOULD_MERGE flags and let the block layer handle scheduling without worrying about it yourself.

    7. Writing Your First Linux Block Device Driver

    Enough theory. Let’s write a simple RAM-backed block device driver essentially a software-defined disk that lives in kernel memory. This is the classic learning exercise for block driver development because it has no hardware complexity, just the pure kernel interfaces.

    Our driver will:

    • Register a block device with a 16MB RAM disk
    • Handle read and write requests
    • Be loadable as a kernel module
    • Be accessible as /dev/myblk

    Header Includes

    #include &lt;linux/module.h&gt;
    #include &lt;linux/kernel.h&gt;
    #include &lt;linux/fs.h&gt;
    #include &lt;linux/blkdev.h&gt;
    #include &lt;linux/blk-mq.h&gt;
    #include &lt;linux/genhd.h&gt;
    #include &lt;linux/hdreg.h&gt;
    #include &lt;linux/vmalloc.h&gt;
    
    MODULE_LICENSE("GPL");
    MODULE_AUTHOR("Your Name");
    MODULE_DESCRIPTION("Simple RAM Block Device Driver");
    

    Device State Structure

    #define MY_BLOCK_MAJOR    240
    #define MY_BLOCK_MINORS   1
    #define MY_BLOCK_NAME     "myblk"
    #define KERNEL_SECTOR_SIZE 512
    #define MY_DISK_SIZE_MB   16
    
    static int major_num = MY_BLOCK_MAJOR;
    static int disk_size_mb = MY_DISK_SIZE_MB;
    
    struct my_block_dev {
        unsigned long size;          /* size in bytes */
        u8 *data;                    /* the RAM storage */
        struct blk_mq_tag_set tag_set;
        struct request_queue *queue;
        struct gendisk *gd;
    };
    
    static struct my_block_dev *dev;
    

    The Request Handler

    This is the heart of any block driver. The kernel calls this function when it has I/O work for you to do. In blk-mq style (modern kernel), each request is a struct request that wraps one or more bio structures.

    static blk_status_t my_block_request(struct blk_mq_hw_ctx *hctx,
                                          const struct blk_mq_queue_data *bd)
    {
        struct request *req = bd->rq;
        struct my_block_dev *bdev = req->q->queuedata;
        struct bio_vec bvec;
        struct req_iterator iter;
        loff_t pos = blk_rq_pos(req) * KERNEL_SECTOR_SIZE;
        loff_t dev_size = bdev->size;
    
        blk_mq_start_request(req);
    
        if (blk_rq_is_passthrough(req)) {
            pr_notice("Skip non-fs request\n");
            blk_mq_end_request(req, BLK_STS_IOERR);
            return BLK_STS_OK;
        }
    
        rq_for_each_segment(bvec, req, iter) {
            size_t len = bvec.bv_len;
            void *buf = page_address(bvec.bv_page) + bvec.bv_offset;
    
            if (pos + len > dev_size) {
                pr_err("Request beyond device size\n");
                blk_mq_end_request(req, BLK_STS_IOERR);
                return BLK_STS_OK;
            }
    
            if (rq_data_dir(req) == WRITE)
                memcpy(bdev->data + pos, buf, len);
            else
                memcpy(buf, bdev->data + pos, len);
    
            pos += len;
        }
    
        blk_mq_end_request(req, BLK_STS_OK);
        return BLK_STS_OK;
    }
    
    static const struct blk_mq_ops my_mq_ops = {
        .queue_rq = my_block_request,
    };
    

    Notice: rq_data_dir(req) returns WRITE or READ. For writes, copy from the bio’s pages into our RAM buffer. For reads, copy from the RAM buffer into the bio’s pages. That’s the entire I/O operation for a RAM disk just memcpy.

    8. The gendisk Structure and Disk Registration

    The struct gendisk is how you tell the kernel “here is a disk.” It holds the device name, capacity, major/minor numbers, and function pointers for partition-related operations. You allocate it with alloc_disk() and register it with add_disk().

    static const struct block_device_operations my_fops = {
        .owner = THIS_MODULE,
    };
    
    static int __init my_block_init(void)
    {
        int ret;
    
        /* Allocate device state */
        dev = kzalloc(sizeof(*dev), GFP_KERNEL);
        if (!dev)
            return -ENOMEM;
    
        dev->size = disk_size_mb * 1024 * 1024;
        dev->data = vmalloc(dev->size);
        if (!dev->data) {
            ret = -ENOMEM;
            goto err_free_dev;
        }
        memset(dev->data, 0, dev->size);
    
        /* Set up the tag set for blk-mq */
        dev->tag_set.ops = &my_mq_ops;
        dev->tag_set.nr_hw_queues = 1;
        dev->tag_set.queue_depth = 128;
        dev->tag_set.numa_node = NUMA_NO_NODE;
        dev->tag_set.cmd_size = 0;
        dev->tag_set.flags = BLK_MQ_F_SHOULD_MERGE;
    
        ret = blk_mq_alloc_tag_set(&dev->tag_set);
        if (ret)
            goto err_free_data;
    
        /* Create the request queue */
        dev->queue = blk_mq_init_queue(&dev->tag_set);
        if (IS_ERR(dev->queue)) {
            ret = PTR_ERR(dev->queue);
            goto err_free_tag_set;
        }
        dev->queue->queuedata = dev;
    
        /* Allocate the gendisk */
        dev->gd = alloc_disk(MY_BLOCK_MINORS);
        if (!dev->gd) {
            ret = -ENOMEM;
            goto err_free_queue;
        }
    
        dev->gd->major = major_num;
        dev->gd->first_minor = 0;
        dev->gd->fops = &my_fops;
        dev->gd->queue = dev->queue;
        dev->gd->private_data = dev;
        snprintf(dev->gd->disk_name, 32, MY_BLOCK_NAME);
    
        /* Set capacity in 512-byte sectors */
        set_capacity(dev->gd, dev->size / KERNEL_SECTOR_SIZE);
    
        /* Register the major number */
        ret = register_blkdev(major_num, MY_BLOCK_NAME);
        if (ret < 0)
            goto err_free_gd;
    
        /* Add the disk to the system */
        add_disk(dev->gd);
    
        pr_info("myblk: registered with %d MB RAM disk\n", disk_size_mb);
        return 0;
    
    err_free_gd:
        put_disk(dev->gd);
    err_free_queue:
        blk_cleanup_queue(dev->queue);
    err_free_tag_set:
        blk_mq_free_tag_set(&dev->tag_set);
    err_free_data:
        vfree(dev->data);
    err_free_dev:
        kfree(dev);
        return ret;
    }
    

    The Cleanup Function

    static void __exit my_block_exit(void)
    {
        del_gendisk(dev->gd);
        put_disk(dev->gd);
        blk_cleanup_queue(dev->queue);
        blk_mq_free_tag_set(&dev->tag_set);
        unregister_blkdev(major_num, MY_BLOCK_NAME);
        vfree(dev->data);
        kfree(dev);
        pr_info("myblk: unregistered\n");
    }
    
    module_init(my_block_init);
    module_exit(my_block_exit);
    

    Order matters in cleanup you always reverse the registration order. del_gendisk before unregister_blkdev. Free memory last. A single wrong order here causes a kernel panic on module unload.

    9. The make_request Approach : Bypassing the Request Queue

    There are two main styles of block driver in Linux. The request-queue style (what we’ve been building with blk-mq) buffers requests, lets the scheduler sort them, and dispatches them to your handler. This is good for real hardware where reordering matters.

    The other style uses blk_queue_make_request() to register a custom function that bypasses the scheduler entirely. Every bio comes straight to your function with no buffering. This is ideal for:

    • RAM disks (seek time is zero, no benefit to reordering)
    • Stacked drivers (like dm-crypt, RAID) that pass I/O through to other devices
    • Virtual devices where latency matters more than throughput optimization
    static blk_qc_t my_make_request(struct request_queue *q, struct bio *bio)
    {
        struct my_block_dev *bdev = q->queuedata;
        struct bio_vec bvec;
        struct bvec_iter iter;
        loff_t pos = bio->bi_iter.bi_sector * KERNEL_SECTOR_SIZE;
    
        bio_for_each_segment(bvec, bio, iter) {
            void *buf = kmap_atomic(bvec.bv_page) + bvec.bv_offset;
            size_t len = bvec.bv_len;
    
            if (bio_data_dir(bio) == WRITE)
                memcpy(bdev->data + pos, buf, len);
            else
                memcpy(buf, bdev->data + pos, len);
    
            kunmap_atomic(buf - bvec.bv_offset);
            pos += len;
        }
    
        bio_endio(bio);
        return BLK_QC_T_NONE;
    }
    
    /* During init, instead of blk_mq_init_queue: */
    dev->queue = blk_alloc_queue(GFP_KERNEL);
    blk_queue_make_request(dev->queue, my_make_request);
    

    Note the use of kmap_atomic() this temporarily maps a high-memory page into the kernel’s virtual address space so you can access it. Always pair with kunmap_atomic(). Forgetting this causes silent data corruption on 32-bit systems with more than 1GB RAM, or just panics outright.

    10. Multi-Queue Block Layer (blk-mq) : The Modern Way

    Before kernel 3.13, Linux had a single request queue per device. All requests serialized through one lock. This was fine when a disk could do maybe 100-200 IOPS. Modern NVMe SSDs do millions of IOPS. A single queue becomes a catastrophic bottleneck.

    The multi-queue block layer (blk-mq) solved this. It gives each CPU (or NUMA node) its own software queue, and maps those to multiple hardware queues if the device supports it. NVMe SSDs commonly have 32+ hardware queues. blk-mq can feed all of them in parallel with zero lock contention between CPUs.

    The architecture looks like this:

    • Software Queues : one per CPU, handles submissions from that CPU without locking others
    • Hardware Queues : one per device queue (NVMe namespace/queue), your driver processes these
    • Mapping : blk-mq maps many software queues to fewer (or more) hardware queues automatically

    When you call blk_mq_alloc_tag_set(), you configure this mapping:

    dev->tag_set.nr_hw_queues = num_online_cpus(); /* one HW queue per CPU */
    dev->tag_set.queue_depth = 256;                /* max outstanding requests per queue */
    

    For a RAM disk or simple driver, nr_hw_queues = 1 is fine. For a real NVMe driver, you’d query the hardware for the actual number of queues it supports and set this accordingly.

    Tags and Request Tracking

    blk-mq uses a tag system to track in-flight requests. Each request gets a tag (an integer ID) when it’s submitted, and you use that tag to identify and complete it later. This is crucial for asynchronous I/O where you submit a request to hardware and get an interrupt later when it’s done.

    /* In your queue_rq handler: */
    blk_mq_start_request(req);   /* mark it as started, tag is active */
    
    /* ... submit to hardware, store req somewhere indexed by tag ... */
    
    /* In your interrupt handler or completion poll: */
    blk_mq_end_request(req, BLK_STS_OK);  /* or BLK_STS_IOERR on error */
    

    11. Handling Partitions in Block Drivers

    When you register a disk with add_disk(), the kernel automatically reads the partition table from the device’s first sectors (or GPT header) and creates the partition device nodes for you. You don’t manually create /dev/myblk1 — the kernel does it.

    The number of minors you allocate when calling alloc_disk() controls how many partitions your device can have. If you call alloc_disk(16), you get minor numbers 0 through 15, meaning 15 possible partitions (minor 0 is the whole disk).

    For a partition, the block layer translates the sector offset automatically. If partition 1 starts at sector 2048, and a request comes in for sector 0 of partition 1, your driver sees sector 2048. The translation is completely transparent. This is one of the great things about the kernel’s partition handling — your driver doesn’t need to know anything about partitions.

    To force the kernel to re-read a partition table after you’ve modified it (like after writing a new MBR), use:

    ioctl(fd, BLKRRPART, 0);
    /* or from command line: */
    /* blockdev --rereadpt /dev/myblk */

    12. Compiling, Loading, and Testing Your Driver

    The Makefile

    obj-m += myblk.o
    
    KDIR ?= /lib/modules/$(shell uname -r)/build
    
    all:
        make -C $(KDIR) M=$(PWD) modules
    
    clean:
        make -C $(KDIR) M=$(PWD) clean
    
    $ make
    $ sudo insmod myblk.ko
    $ dmesg | tail -5
    [12345.678] myblk: registered with 16 MB RAM disk
    $ ls /dev/myblk
    /dev/myblk
    

    Creating a File System and Mounting

    $ sudo mkfs.ext4 /dev/myblk
    $ sudo mkdir /mnt/myblk
    $ sudo mount /dev/myblk /mnt/myblk
    $ df -h /mnt/myblk
    Filesystem      Size  Used Avail Use% Mounted on
    /dev/myblk       15M  152K   14M   2% /mnt/myblk
    $ echo "hello kernel world" | sudo tee /mnt/myblk/test.txt
    $ cat /mnt/myblk/test.txt
    hello kernel world

    If that works congratulations. You have a working Linux block device driver. The kernel’s ext4 file system is writing to and reading from your driver’s RAM buffer through the full kernel I/O stack.

    Stress Testing with fio

    $ sudo fio --name=test --filename=/dev/myblk --rw=randrw \
        --bs=4k --size=8M --numjobs=4 --time_based --runtime=10 \
        --group_reporting

    This hits your driver with random 4KB reads and writes from 4 parallel threads. Watch for kernel panics, data corruption, or lockups. If it runs clean for 30 seconds, your locking and memory handling are probably okay.

    Checking /proc and /sys

    $ cat /proc/devices | grep myblk
    240 myblk
    
    $ ls /sys/block/myblk/
    alignment_offset  capability  dev  holders  inflight  power  
    queue  range  removable  ro  size  slaves  stat  subsystem  uevent
    
    $ cat /sys/block/myblk/size
    32768           # in 512-byte sectors = 16MB
    
    $ cat /sys/block/myblk/queue/logical_block_size
    512

    13. Real-World Block Driver Examples in the Kernel

    The best way to get better at block driver development after writing your own toy driver is to read real drivers in the kernel tree. Here are the ones worth studying, in order of increasing complexity:

    drivers/block/brd.c — RAM Disk Driver

    This is the official kernel RAM disk driver (/dev/ram0, etc.). It’s very similar to what we built, but handles multiple devices, proper memory management with radix trees, and the make_request style. Read this first. About 400 lines, very approachable.

    drivers/block/loop.c — Loopback Driver

    This backs a block device with a regular file. When you do losetup /dev/loop0 disk.img, this driver reads/writes to disk.img in userspace. It uses make_request style and shows how to do asynchronous I/O with kernel threads. Medium complexity.

    drivers/block/null_blk/ — Null Block Driver

    This is a high-performance benchmarking driver that discards all writes and returns zeroes on reads. It’s used to benchmark the block layer overhead itself. It supports blk-mq with multiple queues, optional latency injection, and configurable parameters via sysfs. This is a goldmine for learning advanced blk-mq patterns.

    drivers/nvme/host/ — NVMe Driver

    The real NVMe driver is one of the most sophisticated block drivers in the kernel. Multiple hardware queues, PCI DMA, interrupt-based completion, NVMe command sets — it’s all there. Read this only after you’re comfortable with the basics. But when you are, it shows you exactly how production-quality block drivers handle real hardware.

    drivers/md/ — Software RAID and dm

    The MD (Multiple Devices) layer and Device Mapper (dm) are stacked block drivers — they sit on top of other block devices and provide RAID, encryption (dm-crypt), LVM, thin provisioning, etc. These are fascinating because they show how to stack block devices and how the kernel handles the same bio being processed by multiple drivers in sequence.

    14. Debugging Linux Block Device Drivers

    Kernel debugging is a whole different world from userspace debugging. You can’t attach GDB and step through kernel code easily (well, you can with KGDB, but it’s painful). Here are the practical techniques that actually work.

    pr_info, pr_err, printk

    Your first tool is always printk-based logging. Use pr_info(), pr_warn(), pr_err() — these prepend your module name automatically if you define pr_fmt at the top of your file:

    #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

    Then all your pr_info() calls show up in dmesg as myblk: your message here. Check dmesg -w in a terminal while loading and exercising your module.

    QEMU + GDB for Real Debugging

    The real way to debug kernel drivers without destroying your actual system is to run Linux in QEMU and attach GDB to it. QEMU supports -s -S flags that start it with a GDB stub paused at the first instruction.

    $ qemu-system-x86_64 -kernel bzImage -initrd initrd.img \
        -append "console=ttyS0" -nographic -s -S
    
    # In another terminal:
    $ gdb vmlinux
    (gdb) target remote :1234
    (gdb) break my_block_request
    (gdb) continue

    This lets you set breakpoints in your driver code, inspect kernel data structures, and single-step through the request handler. It’s the closest thing to real debugging you’ll get in the kernel world.

    CONFIG_DEBUG_BLOCK_EXT_DEVT and Block-Specific Debugging

    The kernel has several Kconfig options specifically for block layer debugging:

    • CONFIG_BLK_DEBUG_FS — exposes detailed blk-mq debugging info under /sys/kernel/debug/block/
    • CONFIG_BLK_DEV_IO_TRACE — enables blktrace, a powerful tool for recording every I/O event
    • CONFIG_FAIL_MAKE_REQUEST — fault injection for block requests

    blktrace and blkparse

    blktrace is a dedicated block I/O tracing tool. It hooks into the kernel’s block trace points and records every event — bio submission, queue insertion, driver dispatch, completion — with nanosecond timestamps.

    $ sudo blktrace -d /dev/myblk -o trace
    $ sudo blkparse trace.blktrace.0 | head -50

    This output shows you exactly what the block layer is doing with your device. Great for diagnosing scheduler behavior, finding unexpected I/O patterns, and verifying that your driver is completing requests correctly.

    15. Common Mistakes Beginners Make With Block Device Drivers

    These mistakes have caused panics for probably every kernel developer who’s gone through this learning curve. Learn from them instead of experiencing them firsthand.

    Mistake 1: Not Calling blk_mq_start_request

    In blk-mq, you must call blk_mq_start_request(req) before doing any processing. Skipping this causes the timeout mechanism to fire incorrectly and you’ll see mysterious “request timeout” errors or double-completions that panic the kernel.

    Mistake 2: Wrong Sector Arithmetic

    Sectors are always 512 bytes in the block layer’s address space, even if your device uses 4096-byte physical blocks. blk_rq_pos(req) returns the offset in 512-byte units. Always multiply by KERNEL_SECTOR_SIZE (512) to get bytes. Getting this wrong causes reads to return garbage and writes to corrupt data in non-obvious ways.

    Mistake 3: Sleeping in Atomic Context

    Your request handler might be called in an atomic context (interrupts disabled, spinlock held). Calling anything that can sleep — kmalloc(GFP_KERNEL), mutex_lock(), copy_to_user() — in this context causes a lockdep warning or a kernel crash. Always use GFP_ATOMIC for allocations in the request path, and do all blocking work in a separate kernel thread or workqueue.

    Mistake 4: Not Handling Bio Completion Correctly

    Every bio that comes to your driver must be completed — either with bio_endio(bio) (success), bio_io_error(bio) (error), or through request-level completion via blk_mq_end_request(). Forgetting to complete a bio hangs the process that submitted it forever. It won’t crash the kernel immediately — it’ll just hang, which is often harder to debug than a crash.

    Mistake 5: Cleanup Order Errors

    The exit path cleanup order is the reverse of the init path setup order. This sounds obvious but it’s easy to get wrong when you’re debugging and adding/removing init steps. A wrong cleanup order causes use-after-free bugs, double-free crashes, or hung processes when you do rmmod.

    Mistake 6: Ignoring Request Flush and FUA

    If your device claims to have a write cache (and some real hardware does), you need to handle flush requests. When an application calls fsync(), the file system submits a special REQ_PREFLUSH or REQ_FUA (Force Unit Access) request to make sure data is actually on stable storage. Ignoring these means fsync() appears to succeed but data could be lost on power failure. For a simple RAM disk driver this doesn’t matter, but for any driver that interfaces with real hardware with write caches, this is critical for data integrity.

    16. What to Learn Next

    You’ve got the fundamentals of Linux block device driver development down. Here’s where to go from here depending on your goals:

    If You Want to Work on Real Storage Hardware

    Learn DMA (Direct Memory Access). Real drivers don’t use memcpy — they program the hardware’s DMA controller to transfer data directly between device and RAM without CPU involvement. Study the kernel’s DMA API: dma_alloc_coherent(), dma_map_sg(), dma_sync_for_device(). The NVMe driver is the best example of DMA in block drivers.

    If You’re Interested in Storage Stacking (LVM, RAID, Encryption)

    Learn the Device Mapper framework (drivers/md/dm.c). Device mapper lets you build complex storage topologies by stacking block devices. dm-crypt (full disk encryption), dm-thin (thin provisioning), and LVM all use it. Writing a simple dm target is a great exercise — you implement a transform function that maps bio addresses from the virtual device to the underlying real device.

    If You’re Targeting Embedded or Automotive Systems

    Look at UFS (Universal Flash Storage) and eMMC drivers in drivers/scsi/ufs/ and drivers/mmc/. These are the storage interfaces on embedded SoCs, phones, and automotive infotainment systems. The MMC subsystem has its own host controller abstraction on top of the block layer, and understanding it opens up a huge class of embedded storage work.

    Resources Worth Reading

    • Linux Device Drivers, 3rd Edition (Corbet, Rubini, Kroah-Hartman) : free online, chapter 16 is specifically on block drivers. Note: it’s old (2.6 era), but the concepts are solid even if the specific APIs changed.
    • The kernel source itself under Documentation/block/ : especially blk-mq.rst and biodoc.rst
    • Jonathan Corbet’s LWN.net articles on the block layer : search “LWN block layer” and you’ll find years of excellent coverage of how the block layer evolved
    • The null_blk driver source for advanced blk-mq patterns

    Quick Reference: Block Driver API Cheat Sheet

    Function / MacroPurpose
    register_blkdev(major, name)Register a major number for your driver
    unregister_blkdev(major, name)Unregister on module exit
    alloc_disk(minors)Allocate a gendisk structure
    add_disk(gd)Make disk visible to the kernel
    del_gendisk(gd)Remove disk from the kernel
    put_disk(gd)Release gendisk reference
    set_capacity(gd, sectors)Set disk size in 512-byte sectors
    blk_mq_alloc_tag_set(ts)Allocate blk-mq tag set
    blk_mq_init_queue(ts)Create a request queue
    blk_cleanup_queue(q)Destroy a request queue
    blk_mq_start_request(req)Mark request as in-flight
    blk_mq_end_request(req, status)Complete a request
    blk_rq_pos(req)Get starting sector of a request
    rq_data_dir(req)Get direction: READ or WRITE
    rq_for_each_segment(bvec, req, iter)Iterate over request segments
    bio_data_dir(bio)Get bio direction: READ or WRITE
    bio_for_each_segment(bvec, bio, iter)Iterate over bio segments
    bio_endio(bio)Complete a bio (success)
    submit_bio(bio)Submit a bio to block layer
    kmap_atomic(page)Map a page into kernel virtual space
    kunmap_atomic(addr)Unmap after use

    Wrapping Up

    Block device drivers are where kernel development gets really interesting. You’re operating at the exact boundary between software and hardware — translating abstract kernel requests into concrete I/O operations. Every filesystem on every Linux system depends on block drivers working correctly, which means this is some of the most impactful kernel code you can write.

    The path forward is clear: start with the RAM disk driver we built here, get it compiling and running, then start reading the real kernel drivers. brd.c, then null_blk, then NVMe if you’re aiming for high-performance storage. Each one will teach you something the simpler ones don’t cover.

    The kernel community is also genuinely welcoming to new contributors. If you find a bug in a block driver or have a performance improvement, submitting a patch to the linux-block mailing list is a realistic goal — even for someone who’s been doing this for just a few months.

    Good luck, and keep reading dmesg.

    Have questions about a specific part of block driver development? Drop a comment below happy to go deeper on any of these topics.

    Frequently Asked Questions : Linux Block Device Drivers

    Q1. What is a block device driver in Linux? A block device driver is a kernel module that allows Linux to communicate with storage hardware like hard disks, SSDs, and SD cards by exposing them as fixed-size, randomly accessible block devices under /dev/.

    Q2. What is the difference between a block device and a character device in Linux? Block devices support random access and are buffered through the kernel’s page cache. Character devices stream data sequentially with no buffering. Storage devices use block drivers while serial ports and keyboards use character drivers.

    Q3. What is the bio structure in the Linux kernel? bio stands for Block I/O and is the fundamental data structure the kernel uses to represent a single I/O operation. It contains the target device, starting sector, data buffers as scatter-gather page vectors, and a completion callback function.

    Q4. What is blk-mq in Linux? blk-mq stands for Multi-Queue Block Layer and is the modern Linux block I/O framework that replaced the old single-queue design. It gives each CPU its own software queue and maps them to multiple hardware queues, enabling millions of IOPS for NVMe SSDs.

    Q5. How do I write a simple Linux block device driver? You allocate a gendisk structure, set up a blk-mq tag set and request queue, implement a queue_rq handler that processes read and write requests, register a major number with register_blkdev(), and call add_disk() to make the device visible to the kernel.

    Q6. What is gendisk in the Linux kernel? struct gendisk is the kernel structure that represents a physical or virtual disk. It holds the device name, major and minor numbers, capacity in sectors, partition information, and a pointer to the block_device_operations function table.

    Q7. What is a request queue in Linux block drivers? A request queue sits between the block layer and your driver. It holds pending I/O requests, manages the I/O scheduler, enforces hardware limits like max sectors and max segments, and dispatches ready requests down to your driver’s handler function.

    Q8. What is the difference between make_request and request queue style drivers? Request queue style buffers and schedules I/O before dispatching to your driver which is good for real spinning hardware. make_request style bypasses the scheduler entirely and sends every bio straight to your function which is ideal for RAM disks, virtual devices, and stacked drivers.

    Q9. How does Linux handle disk partitions in block drivers? When you call add_disk(), the kernel automatically reads the partition table and creates all partition device nodes for you. The block layer translates partition-relative sector offsets to absolute disk offsets transparently so your driver never needs to handle any partition logic itself.

    Q10. What is the I/O scheduler in Linux and how does it affect block drivers? The I/O scheduler reorders and merges block requests to improve throughput and reduce seek time on spinning disks. Common schedulers today include mq-deadline, BFQ, and none. For fast NVMe storage none is usually best. Your driver influences scheduling behavior through queue limits and flags you set during initialization.

    Q11. How do I debug a Linux block device driver? Use pr_info and pr_err with dmesg for basic logging. Use blktrace to record every block I/O event with timestamps. Use QEMU combined with GDB for proper breakpoint-level debugging without risking your real system. Enable CONFIG_BLK_DEBUG_FS to expose detailed blk-mq internals under /sys/kernel/debug/block/.

    Q12. What are the most common mistakes in block device driver development? The most common mistakes are forgetting to call blk_mq_start_request(), getting sector-to-byte arithmetic wrong, sleeping inside atomic context, failing to complete every bio, using the wrong cleanup order in the module exit function, and ignoring flush and FUA requests on hardware with write caches.

    Q13. What kernel source files should I study to learn block driver development? Start with drivers/block/brd.c for the RAM disk reference driver, then move to drivers/block/loop.c for the loopback driver, then drivers/block/null_blk/ for advanced blk-mq patterns, and finally drivers/nvme/host/ when you are ready to see a full DMA-based production driver.

    Q14. Can I format and mount a custom block device driver like a real disk? Yes. Once your driver is loaded and the device node appears under /dev/, you can run mkfs.ext4 on it, mount it to any directory, and read and write files normally through it. The kernel file system layer is completely unaware it is talking to a custom driver.

    Q15. What should I learn after mastering Linux block device drivers? The natural next steps are the kernel DMA API for real hardware data transfers, the Device Mapper framework for building stacked drivers like dm-crypt and LVM, and the UFS and eMMC subsystems if you are targeting embedded or automotive storage platforms.

    For detailed understanding of Platform Devices and Drivers on Linux, refer to the official Linux documentation on Platform Devices and Drivers .

    If you’re interested in going beyond theory and building a real-world implementation, check out this Complete Linux Block Device Driver Project that covers both kernel-level driver development and userspace interaction. It walks you step-by-step through creating a functional block driver, handling I/O requests, and building a userspace program to communicate with the kernel module. This is perfect for beginners who want hands-on experience with Linux device drivers.

  • Linux Character Driver from Scratch (Beginner to Pro)

    Learn how to build a Linux Character Driver from scratch with this beginner-friendly guide. Step-by-step tutorial covering kernel modules, file operations, device files, and real-world driver examples.

    If you’ve ever wanted to understand how Linux actually talks to hardware, learning a Linux Character Driver is one of the best places to start.

    This guide is not just theory. We’ll go step by step, from zero to a working driver, and explain things like you’re sitting next to me with a coffee asking real questions.

    By the end, you’ll:

    • Understand what a Linux Character Driver is
    • Know where it’s used in real systems (UART, GPIO, etc.)
    • Build a complete driver from scratch
    • Write a user-space program to interact with it
    • Learn how real production drivers are structured

    What is a Linux Character Driver?

    A Linux Character Driver is a type of device driver that communicates with hardware one byte (character) at a time.

    Think of it like reading a book letter by letter instead of jumping pages.

    Examples:

    • UART (serial communication)
    • Keyboard input
    • GPIO pins
    • Sensors

    These are all handled using character device drivers in Linux.

    Character vs Block Drivers

    Let’s make this super simple.

    Imagine you’re reading data like you read content in real life.

    Character Driver (One by One Data)

    A Linux Character Driver works like reading a message letter by letter.

    You don’t jump around. You go in order.

    Example:

    Think of:

    • Typing on keyboard
    • Reading from a serial port
    • Getting sensor data

    All of these send data continuously and sequentially.

    That’s why they use a character device driver in Linux

    Real Devices:

    • /dev/ttyS0 (UART)
    • /dev/input/event0 (keyboard/mouse)
    • /dev/gpiochip0 (GPIO)

    Block Driver (Data in Chunks)

    A Block Driver works like reading a book where you can:

    • Jump to page 10
    • Then page 50
    • Then page 2

    No need to go in order.

    Example:

    Think of:

    • Hard disk
    • SSD
    • USB storage

    You can directly access any part of data.

    That’s why they use block device drivers

    Real Devices:

    • /dev/sda (hard disk)
    • /dev/mmcblk0 (SD card)

    Simple Comparison

    FeatureCharacter DriverBlock Driver
    Data AccessSequentialRandom
    Data TypeStream (byte-by-byte)Blocks (chunks)
    ExampleUART, GPIOHard disk
    SpeedUsually slowerFaster (optimized)
    BufferingMinimalHeavy buffering

    Easy Analogy (Best Way to Remember)

    • Character Driver → Like listening to a song live 🎧
      You hear it as it plays (no jumping)
    • Block Driver → Like YouTube video 📺
      You can skip anywhere

    Golden Rule

    Ask this question:

    👉 “Do I need to access data randomly or sequentially?”

    • Sequential → Linux Character Driver
    • Random → Block Driver

    In real linux driver development beginner journey:

    • First drivers you learn → Character Drivers
    • Advanced storage systems → Block Drivers

    Because character drivers are:

    • Easier to write
    • Closer to hardware
    • Great for learning linux kernel programming basics

    If data flows like a stream → Character Driver
    If data is accessed like storage → Block Driver

    Where Are Character Drivers Used?

    A Linux Character Driver isn’t just a textbook concept. It’s used everywhere Linux needs to handle stream-based, byte-by-byte communication with hardware.

    Think of any device where data flows continuously rather than in chunks. That’s your signal that a character device driver in Linux is probably involved.

    Let’s break down the most common real-world use cases.

    UART Driver

    Used for serial communication between devices.

    GPIO Driver

    Used to control pins (LED, buttons).

    Sensors

    Temperature, pressure sensors send data continuously.

    So yes — when you asked earlier:

    “Is UART, GPIO implemented using character driver?”

    ==> Yes, most of the time : absolutely.

    Basic Architecture of Linux Character Driver

    High-Level Architecture

    Here’s the overall flow:

    User Space Application
            │
            ▼
    System Calls (open, read, write, close)
            │
            ▼
    VFS (Virtual File System)
            │
            ▼
    Character Driver (Your Code)
            │
            ▼
    Hardware Device
    

    Key Components Explained

    1. User Space

    This is where your application runs.

    Example:

    int fd = open("/dev/my_device", O_RDWR);
    write(fd, "Hello", 5);
    read(fd, buffer, 5);
    close(fd);
    

    These are system calls, not direct hardware access.

    2. System Calls Interface

    Linux provides standard APIs:

    • open()
    • read()
    • write()
    • close()

    These calls go through the kernel and reach your driver.

    3. VFS (Virtual File System)

    VFS acts as a bridge between system calls and drivers.

    It ensures:

    • Everything in Linux is treated as a file
    • Your driver is accessed via /dev/my_device

    4. Device File (/dev)

    Example:

    /dev/my_device

    Created using:

    • mknod (manual)
    • udev (automatic)

    It connects user space to your driver.

    5. Character Driver Core Structure

    Your driver mainly consists of:

    a) File Operations Structure

    This is the heart of the driver.

    struct file_operations fops = {
        .open = my_open,
        .read = my_read,
        .write = my_write,
        .release = my_close,
    };

    It tells the kernel:

    “When user calls read(), call my_read()”

    b) Major & Minor Number

    • Major Number → Identifies driver
    • Minor Number → Identifies device

    Example:

    Major: 240 → my driver
    Minor: 0   → device instance

    c) Device Registration

    alloc_chrdev_region(&dev, 0, 1, "my_device");
    

    Registers device with kernel

    d) cdev Structure

    struct cdev my_cdev;
    cdev_init(&my_cdev, &fops);
    cdev_add(&my_cdev, dev, 1);
    

    Links file operations with device

    e) Class & Device Creation

    class_create(THIS_MODULE, "my_class");
    device_create(my_class, NULL, dev, NULL, "my_device");
    

    Creates /dev/my_device

    Driver Entry & Exit

    Init Function

    static int __init my_init(void)
    {
        // register device
        return 0;
    }
    

    Exit Function

    static void __exit my_exit(void)
    {
        // cleanup
    }
    

    Registered using:

    module_init(my_init);
    module_exit(my_exit);

    Complete Flow (Step-by-Step)

    1. User runs program
    2. Calls open("/dev/my_device")
    3. VFS finds your driver
    4. Calls my_open()
    5. User calls write()
    6. Kernel calls my_write()
    7. Driver interacts with hardware
    8. Data flows back via read()

    Pro Tip

    “Character drivers are synchronous, stream-oriented, and accessed via file operations.”

    Linux Kernel Module Basics

    Before writing a driver, we need a kernel module.

    Simple Hello World Module

    #include <linux/module.h>
    #include <linux/kernel.h>
    
    static int __init hello_init(void) {
        printk(KERN_INFO "Hello Driver Loaded\n");
        return 0;
    }
    
    static void __exit hello_exit(void) {
        printk(KERN_INFO "Driver Removed\n");
    }
    
    module_init(hello_init);
    module_exit(hello_exit);
    
    MODULE_LICENSE("GPL");
    

    Compile

    make
    

    Insert Module

    sudo insmod hello.ko

    Remove Module

    sudo rmmod hello

    What is #include?

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

    In C, #include means:

    “Bring definitions and declarations from another file so I can use them.”

    1. #include <linux/module.h>

    What it is:

    This header is used for loadable kernel modules (LKM).

    A Linux character driver is actually a kernel module, so this is mandatory.

    What it provides:

    Module Macros

    module_init(my_init);
    module_exit(my_exit);
    

    These tell the kernel:

    • Which function to run when module is loaded
    • Which function to run when module is removed

    License Declaration

    MODULE_LICENSE("GPL");
    

    Important because:

    • Avoids kernel warnings
    • Enables access to some kernel features

    Author & Description

    MODULE_AUTHOR("Raj");
    MODULE_DESCRIPTION("Simple Character Driver");

    Simple Meaning:

    This header makes your C file behave like a kernel module

    2. #include <linux/kernel.h>

    What it is:

    This header provides kernel-level utilities and logging functions

    What it provides:

    printk() (Very Important)

    printk(KERN_INFO "Hello Kernel\n");
    

    This is like printf() but for the kernel.

    Log Levels

    KERN_INFO
    KERN_WARNING
    KERN_ERR
    

    Example:

    printk(KERN_ERR "Something went wrong\n");

    Difference Between Them

    Feature<linux/module.h><linux/kernel.h>
    PurposeModule handlingKernel utilities
    Needed forDriver lifecycleDebugging/logging
    Key Featuremodule_init()printk()

    static int __init hello_init(void) function

    static int __init hello_init(void) {
        printk(KERN_INFO "Hello Driver Loaded\n");
        return 0;
    }
    

    What is this function?

    This is the initialization function (entry point) of your kernel module (driver).

    It runs when you load the driver using:

    insmod driver.ko
    

    Line-by-Line Explanation

    1. static

    Meaning:

    • Limits the scope of this function only to this file

    Other files in the kernel cannot access it

    Why use it?

    • Avoid name conflicts
    • Good coding practice in kernel development

    2. int

    Meaning:

    • Function returns an integer value

    Important:

    • 0 → Success
    • Negative value → Failure

    Example:

    return -ENOMEM;  // Memory allocation failed
    

    3. __init

    What is this?

    A special kernel macro

    What it does:

    • Marks this function as initialization-only code
    • After driver loads → memory is freed automatically

    Saves kernel memory

    Important Rule:

    ❌ Never call an __init function after initialization
    (its memory is gone!)

    4. hello_init

    Function Name:

    • You can name it anything
    • Convention: drivername_init

    5. (void)

    Meaning:

    • Function takes no arguments

    6. { ... } (Function Body)

    a) printk(KERN_INFO "Hello Driver Loaded\n");

    This prints a message inside the kernel log

    • printk → Kernel version of printf
    • KERN_INFO → Log level (informational message)

    Where to see output?

    dmesg
    

    Example output:

    Hello Driver Loaded
    

    b) return 0;

    Tells kernel:

    “Driver loaded successfully”

    Full Flow

    1. You run:insmod driver.ko
    2. Kernel calls:hello_init()
    3. Message printed:Hello Driver Loaded
    4. Kernel accepts module (since return = 0)

    What if it fails?

    If you return:

    return -1;
    

    Then:

    • Driver will NOT load
    • Kernel shows error

    Real-World Use of hello_init

    In real drivers, this function is used to:

    • Register device (alloc_chrdev_region)
    • Initialize cdev
    • Create device file (/dev/...)
    • Allocate memory
    • Setup hardware

    static void __exit hello_exit(void) function

    static void __exit hello_exit(void) {
        printk(KERN_INFO "Driver Removed\n");
    }
    

    What is this function?

    This function runs when you remove/unload the driver:

    rmmod driver
    

    So:

    hello_init() = when driver loads
    hello_exit() = when driver unloads

    Line-by-Line Explanation

    1. static

    Meaning:

    • Function is only visible inside this file

    Prevents conflicts with other kernel code

    2. void

    Meaning:

    • Function does not return anything

    Unlike init, kernel doesn’t expect success/failure here
    (you must clean up properly no matter what)

    3. __exit

    What it is:

    A kernel macro similar to __init

    What it does:

    • Marks function as exit/cleanup code
    • Used only when module is removed
    • If driver is built into kernel (not module) → this code is ignored

    Important:

    If built-in:

    Exit function may never run

    4. hello_exit

    Function name (can be anything)

    Convention:

    drivername_exit

    5. (void)

    No parameters

    6. Function Body

    printk(KERN_INFO "Driver Removed\n");

    Prints message to kernel log

    Check using:

    dmesg

    Output:

    Driver Removed

    Full Flow

    1. You run:rmmod driver
    2. Kernel calls:hello_exit()
    3. Cleanup happens
    4. Message printed:Driver Removed

    Real-World Importance

    In real drivers, this function is critical

    You must clean everything you created in init():

    Example Cleanup Tasks:

    device_destroy(...)
    class_destroy(...)
    cdev_del(...)
    unregister_chrdev_region(...)
    kfree(...)
    

    If you don’t clean properly:

    • Memory leaks
    • Device conflicts
    • Kernel crash (serious bug)

    Analogy

    Think of it like:

    “Shutdown procedure of your driver”

    If you turned ON things in init(),
    you MUST turn them OFF here.

    How It Connects

    You register it using:

    module_exit(hello_exit);
    

    This tells kernel:

    “Call this function when module is removed”

    Pro Tip (Interview Level)

    Always say:

    “Whatever is allocated in init must be freed in exit”

    What is a Device File in Linux?

    In Linux, everything is a file.

    Your driver becomes usable through:

    /dev/my_device

    This is called a device file in Linux.

    Core Components of a Linux Character Driver

    Now we move into real Linux driver development for beginners.

    Key parts:

    • dev_t → device number
    • cdev → character device structure
    • file_operations → driver functions

    File Operations Structure (Very Important)

    This is the heart of your driver.

    struct file_operations fops = {
        .open = my_open,
        .read = my_read,
        .write = my_write,
        .release = my_close,
    };
    

    This connects user-space calls to kernel functions.

    What is struct file_operations?

    It is a structure of function pointers defined by the kernel.

    It tells the kernel:
    “When a user performs a file operation, call these functions.”

    Big Picture

    When user runs:

    fd = open("/dev/my_device", O_RDWR);
    write(fd, "Hi", 2);
    read(fd, buf, 2);
    close(fd);
    

    Kernel maps them like this:

    User CallDriver Function
    open()my_open()
    read()my_read()
    write()my_write()
    close()my_close()

    Line-by-Line Breakdown

    struct file_operations fops

    Declares a variable fops of type file_operations

    This structure is defined in:

    #include <linux/fs.h>
    

    .open = my_open

    When user calls open() → kernel calls:

    my_open()
    

    Typical use:

    • Initialize device
    • Allocate resources
    • Check permissions

    .read = my_read

    When user calls read() → kernel calls:

    my_read()
    

    Typical use:

    • Send data from kernel → user
    • Use copy_to_user()

    .write = my_write

    When user calls write() → kernel calls:

    my_write()
    

    Typical use:

    • Receive data from user → kernel
    • Use copy_from_user()

    .release = my_close

    Called when user runs close()

    my_close()
    

    Note:

    • release = close (kernel naming)

    How It Works Internally

    1. You register your driver (cdev_add)
    2. Kernel stores pointer to fops
    3. When user performs operations:
      • Kernel looks into fops
      • Calls the corresponding function

    Example Function Prototypes

    int my_open(struct inode *inode, struct file *file);
    
    ssize_t my_read(struct file *file, char __user *buf, size_t len, loff_t *offset);
    
    ssize_t my_write(struct file *file, const char __user *buf, size_t len, loff_t *offset);
    
    int my_close(struct inode *inode, struct file *file);
    

    Simple Analogy

    Think of file_operations like:

    📞 A contact list for the kernel

    • open → call this number
    • read → call this number
    • write → call this number

    Important Notes

    You don’t need to implement all functions

    Example:

    struct file_operations fops = {
        .read = my_read,
    };
    

    Only read is supported

    If function is missing

    • Kernel may return error (-ENOSYS)
    • Operation fails

    Must Register This

    cdev_init(&my_cdev, &fops);
    

    This connects your driver with fops

    Real Driver Flow

    User → open() → my_open()
    User → write() → my_write()
    User → read() → my_read()
    User → close() → my_close()

    Full Linux Character Driver Example

    Now let’s build a complete character driver example in Linux.

    #include <linux/module.h>
    #include <linux/fs.h>
    #include <linux/cdev.h>
    #include <linux/uaccess.h>
    
    #define DEVICE_NAME "my_char_dev"
    
    static dev_t dev_num;
    static struct cdev my_cdev;
    
    char kernel_buffer[1024];
    
    static int my_open(struct inode *inode, struct file *file) {
        printk("Device Opened\n");
        return 0;
    }
    
    static int my_close(struct inode *inode, struct file *file) {
        printk("Device Closed\n");
        return 0;
    }
    
    static ssize_t my_read(struct file *file, char __user *buf, size_t len, loff_t *off) {
        copy_to_user(buf, kernel_buffer, len);
        return len;
    }
    
    static ssize_t my_write(struct file *file, const char __user *buf, size_t len, loff_t *off) {
        copy_from_user(kernel_buffer, buf, len);
        return len;
    }
    
    static struct file_operations fops = {
        .owner = THIS_MODULE,
        .open = my_open,
        .read = my_read,
        .write = my_write,
        .release = my_close,
    };
    
    static int __init driver_init(void) {
        alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
        cdev_init(&my_cdev, &fops);
        cdev_add(&my_cdev, dev_num, 1);
    
        printk("Driver Loaded\n");
        return 0;
    }
    
    static void __exit driver_exit(void) {
        cdev_del(&my_cdev);
        unregister_chrdev_region(dev_num, 1);
    
        printk("Driver Unloaded\n");
    }
    
    module_init(driver_init);
    module_exit(driver_exit);
    
    MODULE_LICENSE("GPL");
    

    Create Device File

    sudo mknod /dev/my_char_dev c <major> <minor>
    

    User Space Program

    Now let’s interact with the driver.

    #include <stdio.h>
    #include <fcntl.h>
    #include <unistd.h>
    
    int main() {
        int fd = open("/dev/my_char_dev", O_RDWR);
    
        write(fd, "Hello Driver", 12);
    
        char buf[100];
        read(fd, buf, 12);
    
        printf("Data: %s\n", buf);
    
        close(fd);
        return 0;
    }
    

    How It Works End-to-End

    1. User program calls write()
    2. Kernel calls my_write()
    3. Data stored in kernel buffer
    4. read() fetches it back

    This is a complete Linux device driver examples

    Why Exit Function is Important?

    You asked earlier:

    Why do we need exit function?

    Because kernel modules must clean up resources.

    Without cleanup:

    • Memory leaks
    • Device conflicts
    • System instability

    So always use:

    module_exit(driver_exit);

    Real Production Driver Structure

    In real embedded Linux driver development, drivers are more complex:

    • Use platform_driver
    • Device Tree (DTS)
    • Interrupt handling
    • DMA
    • Hardware registers

    Debugging Linux Drivers

    Essential for real work.

    Use:

    dmesg

    Add logs:

    printk(KERN_INFO "Debug Message\n");

    Advanced:

    • ftrace
    • kgdb

    Common Mistakes Beginners Make

    • Forgetting copy_to_user
    • Not checking return values
    • Memory leaks
    • Wrong device permissions

    Linux Driver Interview Questions

    You’ll definitely face these:

    Q1: What is character driver?

    Ans : Sequential data transfer driver

    Q2: What is file_operations?

    Ans : Structure linking user calls to kernel

    Q3: Difference between user and kernel space?

    Ans : Security and access level separation

    Best Practices

    • Always validate user input
    • Use snprintf instead of sprintf
    • Follow kernel coding style
    • Keep driver modular

    Advanced Topics (Next Step)

    Once you’re comfortable:

    • Interrupt handling
    • Platform drivers + DTS
    • SPI/I2C drivers
    • Kernel synchronization

    Final Thoughts

    Learning a Linux Character Driver is like opening the door to kernel-level programming.

    At first, it feels confusing:

    • Kernel space
    • File operations
    • Device numbers

    But once you build one working driver, everything clicks

    FAQ : Linux Character Driver

    1.What is a Linux Character Driver?

    A Linux Character Driver is a type of device driver that allows communication between user space and hardware devices one byte at a time (stream-based data transfer). These drivers are commonly used for devices like UART, keyboards, GPIO, and sensors.

    2.How does a Linux Character Driver work?

    A Linux Character Driver works through a layered architecture:

    • User application calls open(), read(), write()
    • System calls reach the kernel via VFS (Virtual File System)
    • Kernel invokes driver functions defined in file_operations
    • Driver interacts with hardware

    3.What is file_operations in a character driver?

    file_operations is a structure that maps user-space system calls to driver functions.

    Example:

    • open() → my_open()
    • read() → my_read()
    • write() → my_write()

    It acts as the core interface between user space and kernel space.

    4.What is the difference between Character Driver and Block Driver?

    FeatureCharacter DriverBlock Driver
    Data AccessByte-by-byteBlock (chunks)
    ExampleUART, GPIOHard Disk
    BufferingMinimalUses cache
    Access TypeSequentialRandom

    5.What are major and minor numbers?

    • Major Number → Identifies the driver
    • Minor Number → Identifies the device instance

    These numbers help the kernel route operations to the correct driver.

    6.What is /dev in Linux Character Drivers?

    /dev is a directory where device files are created.

    Example:

    /dev/my_device
    

    This file acts as the entry point for user applications to interact with the driver.

    7.What is printk() in Linux drivers?

    printk() is used for kernel-level logging, similar to printf() in user space.

    Example:

    printk(KERN_INFO "Driver Loaded\n");
    

    You can view logs using:

    dmesg
    

    8.What is __init and __exit in Linux drivers?

    • __init → Marks initialization function (freed after loading)
    • __exit → Marks cleanup function (used during module removal)

    These help optimize memory usage in the kernel.

    9.How do you load and unload a character driver?

    Load driver:

    sudo insmod driver.ko
    

    Unload driver:

    sudo rmmod driver
    

    Check logs:

    dmesg
    

    10.What is a kernel module in Linux?

    A kernel module is a piece of code that can be loaded/unloaded into the kernel dynamically. A Linux Character Driver is typically implemented as a kernel module.

    11.Where are Linux Character Drivers used?

    They are used in real-world scenarios like:

    • UART communication
    • GPIO control
    • Sensor interfacing
    • Embedded systems
    • Serial devices

    12.Do I need hardware to learn character drivers?

    No. You can start with a virtual character driver (dummy driver) and simulate read/write operations without real hardware.

    13.What are common mistakes beginners make?

    • Forgetting cleanup in __exit
    • Not handling copy_to_user() properly
    • Incorrect major/minor number handling
    • Missing error checks in init function

    14.Is Linux Character Driver development hard?

    Initially, it may feel complex, but with step-by-step practice:

    • Start with a simple module
    • Learn file_operations
    • Build a dummy driver

    It becomes much easier over time.

    15.What is the difference between read() and write() in drivers?

    • read() → Kernel → User space (sending data)
    • write() → User space → Kernel (receiving data)

    16.Can I build a production-level driver with character drivers?

    Yes. Many real-world drivers (UART, I2C user interfaces, debug interfaces) are built using character drivers.

    For detailed understanding of Platform Devices and Drivers on Linux, refer to the official Linux documentation on Platform Devices and Drivers .