Skip to content
← ENGINEERING NOTES
KERNEL7 min read

Writing a Linux Character Driver

A field guide to the character-device interface: registering a device, wiring up file_operations, and talking to it from userspace.

BY ASHLIN DARIUS GOVINDASAMY

Most application code never has to think about drivers. Everything below the syscall boundary is somebody else's problem — until the hardware, or the abstraction, doesn't do what you need. A character driver is the smallest useful unit of Linux kernel engineering: enough to be real, small enough to actually finish.

What a character driver actually is

A character device exposes a stream interface to userspace — read(), write(), ioctl() — backed by whatever the driver wants on the other side: hardware registers, a virtual buffer, a protocol. The kernel doesn't care what's behind file_operations. It only cares that the struct is filled in correctly.

c
static struct file_operations fops = {
    .owner   = THIS_MODULE,
    .open    = adg_open,
    .release = adg_release,
    .read    = adg_read,
    .write   = adg_write,
};
Minimal file_operations table

Registering the device

Two calls do the real work: alloc_chrdev_region() reserves a major/minor number pair from the kernel, and cdev_add() wires your file_operations table into that number so the VFS knows what to call when userspace opens /dev/adg0.

c
static int __init adg_init(void)
{
    int ret = alloc_chrdev_region(&dev_num, 0, 1, "adg_chardev");
    if (ret < 0)
        return ret;

    cdev_init(&adg_cdev, &fops);
    ret = cdev_add(&adg_cdev, dev_num, 1);
    if (ret < 0) {
        unregister_chrdev_region(dev_num, 1);
        return ret;
    }

    return 0;
}
module_init(adg_init);
Registration in module_init

Where it actually gets hard

  • Concurrency — read()/write() can be called from multiple contexts; the driver owns its own locking.
  • Copying data across the kernel/user boundary correctly (copy_to_user / copy_from_user), never dereferencing user pointers directly.
  • Cleanup ordering in the error path of module_init — every partial success has to unwind exactly once.
  • Debugging without a debugger attached to the failure — dmesg and printk discipline matter more than in userspace code.
A driver bug doesn't throw an exception. It corrupts memory, hangs the box, or silently returns the wrong byte. The discipline is different because the consequences are different.

None of this requires exotic hardware to learn properly — a virtual character device is enough to internalise the registration lifecycle, the file_operations contract, and the debugging discipline that carries over directly to real drivers.

TAGS

LinuxKernelCDrivers

RELATED

ENGINEERING NOTES

Things we built.Things we broke.Things we learned.