Choosing Kernel Driver Interfaces-Free Linux Device Drivers Course

Choosing Kernel Driver Interfaces

Choosing Kernel Driver Interfaces

ioctl, sysfs, configfs, and netlink compared – how to design the userspace-facing side of a Linux character device driver, in EmbeddedPathashala’s free Linux kernel development course.

Chapter 8 – Lecture 9
Device Drivers Series
~20 min read

Once you’ve exhausted the userspace options – sysfs GPIO control, i2c-dev, spidev, and the LED class covered earlier in this chapter – you’ll eventually hit hardware that needs an actual kernel driver. Writing the driver’s hardware-facing half is its own deep topic, but the decision that shapes everything downstream is simpler and easier to get right: how will userspace talk to your driver? This lecture in our free linux kernel development course walks through that decision.

free linux kernel development course free linux device drivers course free embedded linux course free embedded systems course

What You Will Learn

  • Why a byte stream (read/write) isn’t enough for many devices
  • How ioctl works and why upstream maintainers discourage new uses
  • sysfs as the modern default, and its atomicity limitation
  • When configfs and netlink are the better fit
  • A working demo driver exposing both a sysfs attribute and an ioctl
  • A decision framework for your own driver’s userspace interface

Prerequisites

You should already understand character device basics – major/minor numbers and node creation – covered earlier in this chapter, and be comfortable building and loading a kernel module. Familiarity with copy_to_user()/copy_from_user() is helpful but not required; we’ll explain it inline.

Beyond Read and Write

The default character device interface is a stream of bytes, which maps naturally onto devices like serial ports. But plenty of hardware doesn’t fit that shape at all – a robot-arm controller needs discrete “move” and “rotate” operations per joint, not a byte stream to interpret. The kernel gives driver authors several ways to expose structured, non-stream operations to userspace beyond plain read(2) and write(2), and picking the right one affects how maintainable, portable, and upstream-friendly your driver ends up being.

ioctl: Flexible, But Increasingly Discouraged

The ioctl() system call passes a command number and an opaque pointer to your driver – a blank canvas that lets you design any request/response shape you like. Commands are conventionally built with the _IO, _IOR, _IOW, and _IOWR macros from <asm-generic/ioctl.h>, which encode direction, size, and a magic number into the command value so the kernel can sanity-check calls automatically.

#define EP_CTRL_MAGIC   'e'
#define EP_CTRL_SET_CFG _IOW(EP_CTRL_MAGIC, 1, struct ep_ctrl_cfg)
#define EP_CTRL_GET_CFG _IOR(EP_CTRL_MAGIC, 2, struct ep_ctrl_cfg)

The upside is atomicity: a single ioctl() call can pass a whole structure of related values and trigger an action in one shot. The downside is exactly why the kernel’s own ioctl documentation lists several alternatives before you reach for it – custom ioctls tightly couple kernel and application code, make 32-bit/64-bit compatibility a recurring headache (fixed-width types like __u32 and __s64 are mandatory for a reason), and are hard to keep in step across kernel versions since each driver effectively invents its own private ABI.

sysfs: The Modern Default

sysfs is the interface you’ve already used throughout this chapter for GPIO and LEDs, and it’s the preferred choice for new drivers today. Each value lives in its own file under /sys/class/<your-class>/, created with a struct device_attribute and a pair of show/store callbacks. It’s self-documenting – a well-named file tells you what it does – and trivially scriptable since every value is a plain ASCII string readable with cat and writable with echo.

The tradeoff is atomicity. Because every file holds exactly one value, an operation that needs to set two values and then trigger an action requires three separate writes – two for the inputs, one to fire the action – and nothing stops another process from changing one of those values in between. ioctl(), by contrast, passes everything in a single structure in one function call, so this is a genuine tradeoff rather than sysfs being a strict upgrade.

configfs and netlink: When to Reach Further

Two more interfaces are worth knowing about even though they solve narrower problems:

  • configfs flips sysfs’s direction – userspace creates kernel objects itself via mkdir(2), and the driver responds by creating the matching attribute files. It fits configuration-heavy subsystems well; USB gadget drivers are a well-known example, letting the same USB device present itself as a mass-storage drive, a keyboard, or something else entirely, purely based on what the user configured from userspace.
  • netlink is a full-duplex, socket-based channel and is the kernel’s preferred mechanism specifically for network-related configuration. Unlike ioctl’s strictly synchronous request/response model, netlink supports asynchronous and multicast delivery, so the kernel can push events to multiple listeners without being polled.

There’s also debugfs, but it’s explicitly meant for ad-hoc debugging output with no stable-ABI guarantee – never build a driver’s real userspace contract on it.

Building a Demo: Both Interfaces Side by Side

To make the tradeoff concrete, here’s an original demo misc-device driver, ep_ctrl_demo, that exposes the same underlying state two ways: a single sysfs attribute for reading the current mode, and an ioctl for atomically setting a two-field configuration in one call.

/* ep_ctrl_demo.c - misc device with a sysfs attribute and an ioctl
 * Demonstrates the atomicity tradeoff between the two interfaces.
 */
#include <linux/module.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/device.h>

#define EP_CTRL_MAGIC   'e'

struct ep_ctrl_cfg {
    __u32 threshold;
    __u32 mode;
};

#define EP_CTRL_SET_CFG _IOW(EP_CTRL_MAGIC, 1, struct ep_ctrl_cfg)

static struct ep_ctrl_cfg cfg = { .threshold = 0, .mode = 0 };

/* --- sysfs: read-only view of current mode --- */
static ssize_t mode_show(struct device *dev, struct device_attribute *attr,
                          char *buf)
{
    return sysfs_emit(buf, "%u\n", cfg.mode);
}
static DEVICE_ATTR_RO(mode);

static struct attribute *ep_ctrl_attrs[] = {
    &dev_attr_mode.attr,
    NULL,
};
ATTRIBUTE_GROUPS(ep_ctrl);

/* --- ioctl: atomically set threshold + mode together --- */
static long ep_ctrl_ioctl(struct file *f, unsigned int cmd, unsigned long arg)
{
    struct ep_ctrl_cfg new_cfg;

    switch (cmd) {
    case EP_CTRL_SET_CFG:
        if (copy_from_user(&new_cfg, (void __user *)arg, sizeof(new_cfg)))
            return -EFAULT;
        cfg = new_cfg; /* single atomic assignment, both fields together */
        return 0;
    default:
        return -ENOTTY;
    }
}

static const struct file_operations ep_ctrl_fops = {
    .owner          = THIS_MODULE,
    .unlocked_ioctl = ep_ctrl_ioctl,
};

static struct miscdevice ep_ctrl_dev = {
    .minor  = MISC_DYNAMIC_MINOR,
    .name   = "ep_ctrl_demo",
    .fops   = &ep_ctrl_fops,
    .groups = ep_ctrl_groups,
};

static int __init ep_ctrl_init(void)
{
    return misc_register(&ep_ctrl_dev);
}

static void __exit ep_ctrl_exit(void)
{
    misc_deregister(&ep_ctrl_dev);
}

module_init(ep_ctrl_init);
module_exit(ep_ctrl_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("EmbeddedPathashala sysfs vs ioctl demo driver");

Build and load it:

$ make -C /lib/modules/$(uname -r)/build M=$PWD modules
$ sudo insmod ep_ctrl_demo.ko
$ cat /sys/class/misc/ep_ctrl_demo/mode
0

Setting threshold and mode together, atomically, requires the ioctl path – there’s no sysfs equivalent that avoids a two-write race:

/* ep_ctrl_set.c - userspace side, calls EP_CTRL_SET_CFG */
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <stdio.h>
#include <linux/types.h>

struct ep_ctrl_cfg { __u32 threshold; __u32 mode; };
#define EP_CTRL_SET_CFG _IOW('e', 1, struct ep_ctrl_cfg)

int main(void)
{
    int fd = open("/dev/ep_ctrl_demo", O_RDWR);
    struct ep_ctrl_cfg cfg = { .threshold = 42, .mode = 2 };
    ioctl(fd, EP_CTRL_SET_CFG, &cfg);
    close(fd);
    return 0;
}
$ gcc -o ep_ctrl_set ep_ctrl_set.c
$ sudo ./ep_ctrl_set
$ cat /sys/class/misc/ep_ctrl_demo/mode
2

Notice both interfaces coexist on the same driver: sysfs for a simple, scriptable read, ioctl for the one case that genuinely needs multiple values applied together.

Decision Framework

InterfaceBest forAtomic multi-value updatesScriptable
sysfsExposing device state and simple single-value configNoYes, plain text
ioctlTightly-coupled driver/app pairs, structured multi-field callsYesNo, needs a compiled program
configfsUserspace-driven object creation and complex configurationPer object, via mkdirYes, via mkdir/file writes
netlinkNetwork-related config, async/multicast event deliveryDepends on message designNo, needs a socket-aware program

Common Mistakes

Inventing new ioctls for a driver you plan to upstream

Kernel maintainers routinely push back on new ioctl-based ABIs precisely because they’re hard to evolve safely. If you intend to submit the driver upstream, budget time to redesign around sysfs or configfs.

Skipping copy_to_user/copy_from_user error checks

These calls can fail if the userspace pointer is invalid. Always check the return value and propagate -EFAULT – never trust a pointer from userspace directly.

Using architecture-dependent types in ioctl structures

long and pointers change size between 32-bit and 64-bit userspace. Stick to fixed-width types (__u32, __s64) in any structure crossing the ioctl boundary.

Best Practices

  • Default to sysfs for new drivers unless you have a specific, documented reason to need ioctl’s atomicity or richer payloads.
  • If you do need ioctl, always build command numbers with _IO/_IOR/_IOW/_IOWR rather than raw integers.
  • Keep ioctl structures fixed-width and self-contained; avoid embedding pointers inside them where possible.
  • Reach for configfs when userspace needs to create or reconfigure objects dynamically, not just read/write existing state.

Summary and Key Takeaways

There’s no single right answer for a driver’s userspace interface – only the right tradeoff for what your driver actually needs. sysfs is the sensible default: simple, self-documenting, scriptable. ioctl earns its place when you genuinely need atomic multi-value operations or a tightly coupled driver/application pair, but it comes with real portability and maintainability costs. configfs and netlink solve narrower problems well. Understanding all four means you’ll reach for the right one instead of defaulting to whichever you learned first.

Frequently Asked Questions

Is ioctl actually deprecated in the Linux kernel?

Not formally deprecated, but new ioctl-based interfaces face heavy scrutiny from maintainers, who typically ask whether sysfs, configfs, or netlink would fit instead.

Can a single driver use more than one interface at once?

Yes, as the demo above shows – it’s common to expose simple state via sysfs while reserving ioctl for the specific operations that genuinely need atomicity.

Why can’t sysfs update two files atomically?

Each sysfs file corresponds to a single independent show/store callback pair; there’s no built-in mechanism to lock multiple files together across separate write() calls.

When should I use configfs instead of sysfs?

When userspace needs to create or destroy kernel-side objects itself, not just read or write values on objects the driver already created.

Is netlink only for networking drivers?

It’s the preferred mechanism for network-related configuration specifically; other subsystems generally use sysfs, configfs, or ioctl instead.

Do I need locking around my ioctl handler?

Yes – protect shared driver state with an appropriate lock (mutex or spinlock depending on context) just as you would for any concurrently accessed kernel data.

What’s the safest way to define ioctl command numbers?

Always use the _IO/_IOR/_IOW/_IOWR macros with a unique magic character for your driver, never raw hand-picked integers.

Keep Building Your Kernel Skills

This lecture is part of EmbeddedPathashala’s free Linux kernel development course, covering device drivers from first principles to real hardware.

Continue to Next Lecture Browse the Full Course