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
/deventry created automatically open,read,writeandreleasehandlers- 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:
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.
#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:
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.
#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.
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.
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:
make sudo insmod hello.ko sudo dmesg | tail -n 2 ls -l /dev/hello
Then read and write the device like any other file:
sudo cat /dev/hello echo "LLD Coder was here" | sudo tee /dev/hello sudo cat /dev/hello
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:
- Protect the buffer with a mutex
- Add an
ioctlhandler to clear the message - Support
pollso programs can wait for new data - Move from a fake device to real hardware with a platform driver
I’ll cover each of these in later posts.