Home / Device drivers

Device drivers

Write your first Linux character driver from scratch

Build a loadable kernel module, register a character device, and talk to it from user space with open, read and write.

By LLD Coder 14 min read

What we are building

A character driver exposes a device as a stream of bytes. Programs open a file such as /dev/hello, then use read and write on it. Terminals, serial ports and random number sources all work this way.

By the end of this article you will have a driver that stores a message in kernel memory. Writing to the device replaces the message, and reading from it returns the message.

  • A kernel module that loads and unloads cleanly
  • A device number and a /dev entry created automatically
  • open, read, write and release handlers
  • A small shell test to prove it works

Set up your machine

Use a virtual machine or a spare computer. A bug in kernel code can freeze the whole system, so never develop drivers on a machine that holds work you can’t lose.

On Debian or Ubuntu, install the compiler and the headers that match your running kernel:

terminal
sudo apt update
sudo apt install build-essential linux-headers-$(uname -r)

The smallest module

A module is a piece of code the kernel can load at run time. It needs an init function that runs on load and an exit function that runs on removal.

hello.c
#include <linux/init.h>
#include <linux/module.h>

static int __init hello_init(void)
{
    pr_info("hello: loaded
");
    return 0;
}

static void __exit hello_exit(void)
{
    pr_info("hello: unloaded
");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("LLD Coder");
MODULE_DESCRIPTION("A simple character driver");

You also need a Makefile. It hands the build to the kernel’s own build system:

Makefile
obj-m += hello.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

Tabs matterLines under all: and clean: must start with a tab, not spaces.

Register the device

Every character device is identified by a pair of numbers: a major number that picks the driver and a minor number that picks one device handled by that driver. We ask the kernel to choose a free major number for us.

Then we create a cdev object that links that number to our file operations, and a device class so that udev creates the /dev/hello node for us.

hello.c (additions)
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/fs.h>
#include <linux/uaccess.h>

#define DEV_NAME "hello"
#define BUF_SIZE 256

static dev_t devno;
static struct cdev hello_cdev;
static struct class *hello_class;

static char message[BUF_SIZE] = "Hello from the kernel
";
static size_t message_len = 22;

The new init function sets everything up in order. If any step fails, it undoes the steps before it, so nothing is left behind.

hello.c (replace init and exit)
static int __init hello_init(void)
{
    int ret;

    ret = alloc_chrdev_region(&devno, 0, 1, DEV_NAME);
    if (ret)
        return ret;

    cdev_init(&hello_cdev, &hello_fops);
    ret = cdev_add(&hello_cdev, devno, 1);
    if (ret)
        goto err_region;

    hello_class = class_create(DEV_NAME);
    if (IS_ERR(hello_class)) {
        ret = PTR_ERR(hello_class);
        goto err_cdev;
    }

    device_create(hello_class, NULL, devno, NULL, DEV_NAME);
    pr_info("hello: major %d
", MAJOR(devno));
    return 0;

err_cdev:
    cdev_del(&hello_cdev);
err_region:
    unregister_chrdev_region(devno, 1);
    return ret;
}

static void __exit hello_exit(void)
{
    device_destroy(hello_class, devno);
    class_destroy(hello_class);
    cdev_del(&hello_cdev);
    unregister_chrdev_region(devno, 1);
    pr_info("hello: unloaded
");
}

Kernel versionSince Linux 6.4, class_create takes only the name. On older kernels it takes the module pointer first: class_create(THIS_MODULE, DEV_NAME).

Read and write

The kernel can’t trust pointers from user programs, so data must be moved with copy_to_user and copy_from_user. Both return the number of bytes they failed to copy, so anything other than zero is an error.

hello.c (place above hello_init)
static ssize_t hello_read(struct file *f, char __user *buf,
                          size_t count, loff_t *pos)
{
    if (*pos >= message_len)
        return 0;                      /* end of file */

    if (count > message_len - *pos)
        count = message_len - *pos;

    if (copy_to_user(buf, message + *pos, count))
        return -EFAULT;

    *pos += count;
    return count;
}

static ssize_t hello_write(struct file *f, const char __user *buf,
                           size_t count, loff_t *pos)
{
    if (count > BUF_SIZE - 1)
        count = BUF_SIZE - 1;

    if (copy_from_user(message, buf, count))
        return -EFAULT;

    message_len = count;
    return count;
}

static const struct file_operations hello_fops = {
    .owner = THIS_MODULE,
    .read  = hello_read,
    .write = hello_write,
};

Returning 0 from read tells the caller there is no more data. Without the position check, a tool like cat would loop forever.

A driver is mostly careful bookkeeping: who owns this memory, and who may touch it right now.

Build and test

Compile, load the module, and look at the kernel log:

terminal
make
sudo insmod hello.ko
sudo dmesg | tail -n 2
ls -l /dev/hello

Then read and write the device like any other file:

terminal
sudo cat /dev/hello
echo "LLD Coder was here" | sudo tee /dev/hello
sudo cat /dev/hello
Hello from the kernel
LLD Coder was here

When you’re done, remove the module with sudo rmmod hello. The /dev/hello entry disappears with it.

Where to go next

This driver has one shared buffer and no locking, so two programs writing at the same time can corrupt it. These are good next steps:

  1. Protect the buffer with a mutex
  2. Add an ioctl handler to clear the message
  3. Support poll so programs can wait for new data
  4. Move from a fake device to real hardware with a platform driver

I’ll cover each of these in later posts.

LLD Coder

Writes about Linux, system programming, device drivers and Python.