Linux Driver Communication Interfaces Explained-Free Linux Device Drivers Course

Linux Driver Communication Interfaces Explained
mmap, UIO, SIGIO, debugfs, procfs and netlink — how a driver talks to user space, taught in this free Linux kernel development course
5 Interfaces Compared
1 Original Demo Driver
Latest Stable Kernel APIs

Every character device driver eventually asks the same question: how does data get from kernel space to a waiting application, and back again? read() and write() cover the simple cases, but real hardware needs more — memory-mapped buffers, asynchronous notifications, debug hooks, and legacy compatibility paths. This lecture is part of our free Linux kernel development course and walks through five interfaces the kernel offers for exactly this problem: mmap/UIO, SIGIO signalling, debugfs, procfs, and netlink sockets. If you are following our free linux device drivers course from the beginning, this builds directly on the character-device and sysfs lectures already covered.

What You Will Learn

mmap and the UIO framework SIGIO and kill_fasync() debugfs for debug-only data procfs — why it is now discouraged netlink sockets for kernel-to-daemon events Building an original debugfs demo driver

Why a Driver Needs More Than read/write

A simple character driver moves bytes through read() and write(), one buffer at a time. That model breaks down in three common situations: when an application needs to touch a large hardware buffer directly instead of copying it byte-by-byte, when the driver needs to tell an application “something happened” without the application constantly polling, and when a developer needs a quick window into internal driver state while debugging. This is exactly the gap this part of our free embedded linux course fills — matching the right kernel-to-user interface to the right problem.

Driver-to-User-Space Interface Map
Application (user space) | |– read()/write() ————-> normal I/O, small buffers |– mmap() ———————-> UIO: direct shared memory, zero-copy |– signal handler (SIGIO) debugfs: debug-only variables |– /proc/… ——————-> procfs: legacy, process-related only |– AF_NETLINK socket netlink: structured kernel events (e.g. udev)

mmap and the UIO Framework

mmap() lets an application map a region of kernel or device memory directly into its own address space. Once mapped, the application reads and writes hardware registers or a DMA buffer as ordinary memory, with no read()/write() system call overhead and no kernel-side copy. This is the fastest possible path between hardware and user space, but it also removes the kernel’s usual gatekeeping — a badly written user program can corrupt what it maps.

The UIO (Userspace I/O) framework builds a safer path around this idea. Instead of writing a full kernel driver, you write a thin UIO kernel module that exposes one or more memory regions and an interrupt handler; the rest of the driver logic — parsing registers, running state machines — lives in an ordinary user-space program. UIO is a good fit when a device has simple interrupt semantics and does not need the kernel to interpret the data it produces, since the kernel is really only needed to arm and acknowledge interrupts and to fence off the memory mapping.

On a current kernel, a UIO driver typically fills in a struct uio_info describing the memory region and interrupt handler, then calls uio_register_device(). The full API and worked examples live in the kernel’s UIO documentation and under drivers/uio/ in the kernel source tree — always check the tree for your target kernel version rather than trusting an old book’s function signatures, since UIO’s registration helpers have been refined over the years.

SIGIO — Asynchronous Notification

Sometimes an application does not want to block in read() waiting for data, and does not want to poll either. It would rather register interest once and then be interrupted with a signal the moment something is ready. That is what SIGIO delivers. The application opens the device, sets the O_ASYNC flag with fcntl(), and installs a handler for SIGIO. On the driver side, whenever new data becomes available — for example inside an interrupt handler — the driver calls kill_fasync() against a stored fasync_struct to deliver the signal.

Here is a minimal, original sketch of the driver-side plumbing (not copied from any book, written for this course):

static struct fasync_struct *ep_async_queue;

static int ep_fasync(int fd, struct file *file, int mode)
{
    return fasync_helper(fd, file, mode, &ep_async_queue);
}

/* called from the interrupt handler or a workqueue when new data lands */
static void ep_notify_data_ready(void)
{
    if (ep_async_queue)
        kill_fasync(&ep_async_queue, SIGIO, POLL_IN);
}

static const struct file_operations ep_fops = {
    .owner   = THIS_MODULE,
    .fasync  = ep_fasync,
    /* .open, .read, .write, .release also implemented */
};

SIGIO is elegant on paper, but writing a fully correct, race-free signal handler in the application is genuinely difficult — signal handlers can only safely call a small set of async-signal-safe functions. For that reason SIGIO remains a niche technique; most modern user-space code prefers epoll() or poll() on a file descriptor over installing a signal handler.

debugfs — Debug-Only Data, Not an API

debugfs is a pseudo filesystem, conventionally mounted at /sys/kernel/debug, that exists purely so kernel developers can expose internal state as files without designing a stable, versioned interface. The kernel documentation is explicit that anything an application depends on for normal operation must not live in debugfs — only sysfs and well-defined device nodes offer that guarantee. debugfs entries can be added, renamed, or removed between kernel releases with no deprecation warning.

Mounting it is a one-liner:

mount -t debugfs none /sys/kernel/debug

Most modern distributions mount debugfs automatically at boot for kernel developers; check with mount | grep debugfs. Creating a debug file from a driver is a few lines with helpers like debugfs_create_file(), debugfs_create_u32(), or debugfs_create_bool() — we build a full working example below.

procfs — Legacy, and Best Avoided for New Drivers

Historically, /proc was the easiest way for any driver to publish arbitrary text to user space, and — unlike sysfs and debugfs — its API has always been usable from non-GPL kernel modules. That flexibility is exactly why it fell out of favor: /proc was never meant to be a general driver interface, only a window into process and kernel-internal state (its name literally comes from “process”). Current kernel guidance is that new drivers should not add procfs entries unless the data genuinely relates to a running process; sysfs (structured, one-value-per-file) or debugfs (explicitly unstable) cover essentially every case a new driver has today.

netlink — Structured Kernel-to-Daemon Events

netlink is a full socket protocol family (AF_NETLINK), originally built so user-space networking tools could talk to the kernel’s networking code — reading routing tables, watching link state change, and so on. Because it is a real socket, it supports multicast: many listeners can subscribe to the same kernel events at once, which is exactly what udevd relies on to learn about device add/remove events. General-purpose device drivers rarely open a netlink socket directly — it is heavyweight for a single driver’s needs — but understanding it matters because so much of the kernel’s own hotplug and network-configuration machinery is built on top of it.

Choosing the Right Interface

InterfaceBest forStability
read/writeSimple byte-stream I/OStable ABI
mmap / UIOZero-copy access to large buffers, simple interrupt handling in user spaceStable, but no kernel protection
SIGIORare cases needing signal-driven notification instead of read()/poll()Stable, hard to use correctly
debugfsDeveloper-only debug/trace dataExplicitly unstable, can vanish
procfsProcess-related info only (new drivers should avoid it otherwise)Stable but discouraged for new use
netlinkMulticast kernel events to multiple daemonsStable, heavier to implement

This table is worth memorizing if you are working through this free linux device drivers course in order — exam-style questions in later chapters assume you can justify why a given interface fits a given hardware scenario.

Original Demo: An ep_debugfs Counter Driver

To make debugfs concrete, here is a small, original driver — not copied from any reference book — that exposes a single 32-bit debug counter under /sys/kernel/debug/ep_counter/value. Reading it returns the current count; writing a number resets it. This is exactly the kind of “debug-only” data debugfs is meant for — nothing here belongs in a stable ABI.

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

static u32 ep_counter_value;
static struct dentry *ep_debug_dir;

static int __init ep_debugfs_init(void)
{
    ep_debug_dir = debugfs_create_dir("ep_counter", NULL);
    if (IS_ERR(ep_debug_dir))
        return PTR_ERR(ep_debug_dir);

    debugfs_create_u32("value", 0644, ep_debug_dir, &ep_counter_value);

    pr_info("ep_debugfs: loaded, see /sys/kernel/debug/ep_counter/value\n");
    return 0;
}

static void __exit ep_debugfs_exit(void)
{
    debugfs_remove_recursive(ep_debug_dir);
    pr_info("ep_debugfs: unloaded\n");
}

module_init(ep_debugfs_init);
module_exit(ep_debugfs_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("EmbeddedPathashala debugfs demo driver");

Build it with a minimal Makefile:

obj-m += ep_debugfs.o

all:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

Load it and interact with the file directly — no custom application needed, since debugfs files behave like ordinary text files:

# make
# sudo insmod ep_debugfs.ko
# dmesg | tail -1
[ 1234.567890] ep_debugfs: loaded, see /sys/kernel/debug/ep_counter/value

# cat /sys/kernel/debug/ep_counter/value
0

# echo 42 > /sys/kernel/debug/ep_counter/value
# cat /sys/kernel/debug/ep_counter/value
42

# sudo rmmod ep_debugfs
# dmesg | tail -1
[ 1234.789012] ep_debugfs: unloaded

Notice how little code this took compared to writing a full character device with its own file_operations — debugfs_create_u32() wires up read and write for you. That convenience is precisely why debugfs is so popular for driver bring-up and instrumentation, and precisely why it must never carry data an application depends on to function.

Common Mistakes

  • Treating debugfs as a stable API and shipping a production application that parses it — it can change or disappear on the next kernel upgrade.
  • Adding a new procfs entry for driver configuration instead of using sysfs, when the data has nothing to do with a process.
  • Writing a SIGIO-based application without understanding async-signal-safety, leading to rare, hard-to-reproduce crashes inside the signal handler.
  • Using UIO for a device whose interrupt handling logic genuinely needs to run in kernel space (e.g. it must complete before an in-kernel subsystem can proceed) — UIO shines only when the kernel-side work is trivial.
  • Forgetting debugfs_remove_recursive() in the module’s exit path, leaving stale files behind after unload.

Best Practices

  • Default to sysfs for anything an application relies on; reach for debugfs only for developer-facing internals.
  • Check whether debugfs is even enabled (CONFIG_DEBUG_FS) — some production kernels disable it, and driver code that calls debugfs helpers must tolerate that gracefully (the helpers are safe no-ops when debugfs is compiled out).
  • Prefer poll()/epoll() over SIGIO in new user-space code unless there is a specific reason signals are required.
  • When you do need shared memory with hardware, evaluate UIO before writing a full custom character driver — it is far less code to maintain.

Summary

Five different doors connect a driver to user space, and each one trades off performance, safety, and stability differently. mmap/UIO gives you raw speed with no kernel protection. SIGIO gives you asynchronous notification at the cost of signal-handler complexity. debugfs gives you a fast way to expose internal state, explicitly unstable by design. procfs is legacy and should be left to process-related data. netlink is the right tool when multiple listeners need the same structured kernel events. Picking correctly is a core skill covered throughout this free linux kernel development course, and the debugfs demo above should already be running on your own machine.

Frequently Asked Questions

When should I use UIO instead of writing a normal kernel driver?

When the device’s interrupt handling is simple (acknowledge and wake up) and all the real logic — parsing data, running protocol state machines — can live safely in a user-space program. If the kernel needs to interpret the data before other kernel code can act on it, write a normal driver instead.

Is debugfs available in every Linux build?

Only if the kernel was built with CONFIG_DEBUG_FS. Many production/embedded kernels disable it for security and image-size reasons, so drivers must not depend on debugfs paths being present.

Why is procfs discouraged for new drivers?

Because /proc was designed for process-related kernel data, not general driver configuration. sysfs (one value per file, well-defined) and debugfs (explicitly unstable) now cover the cases procfs used to be misused for.

Can a single driver use more than one of these interfaces?

Yes — it’s common for a driver to expose control knobs through sysfs, debug counters through debugfs, and use SIGIO or poll() for readiness notification, all at the same time.

What is the safest way to test SIGIO delivery during development?

Write a tiny test program that opens the device, sets O_ASYNC via fcntl(), installs a signal handler that just increments a counter and returns, and prints the counter from the main loop — keep all real work outside the signal handler.

Does netlink replace ioctl() for driver control?

Not typically for individual device drivers — netlink is generally reserved for kernel subsystems (like networking or udev) that need to broadcast structured events to multiple user-space listeners at once.

Continue This Free Linux Kernel Development Course

Next, we put everything together and walk through the complete anatomy of a device driver, line by line.

Next Lecture Back to Course Index

3 Comments

Leave a Reply

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