Every earlier lecture in this free linux device drivers course introduced one piece of the puzzle — major/minor numbers, sysfs, GPIO, debugfs. This lecture puts the pieces together and builds one small, complete, original character driver from scratch, using the same modern cdev-based registration that real drivers in the current kernel tree use. By the end you will be able to read almost any simple character driver in the kernel source and know exactly what every line is doing — a core milestone in this free linux kernel development course.
What You Will Learn
Prerequisites
This lecture assumes you have already worked through the earlier chapters of this free linux device drivers course: what character devices are and how major/minor numbers work, and how sysfs exposes device attributes. You will need a Linux machine (a VM is fine) with kernel headers installed for your running kernel, since we compile a real loadable module against them.
The Five Moving Parts of a Character Driver
Strip away all the boilerplate and every character driver in the kernel is built from the same five pieces, wired together in the same order:
Get the teardown order wrong and you leak a device number, leave a stale /sys/class entry, or — worse — crash on module unload because you destroyed the class before the device node that references it. We will call this out explicitly in the exit function below.
Step 1 — Reserving a Device Number
Older code (and older books) hard-code a major number with register_chrdev(42, ...). That is fragile — major 42 might already be taken on a given machine. Current practice is to let the kernel pick a free major dynamically with alloc_chrdev_region(), and only fall back to a fixed number if your driver has a genuine reason to need one (it almost never does):
dev_t ep_devt;
int ret = alloc_chrdev_region(&ep_devt, 0, EP_NUM_DEVICES, "ep_anatomy");
ep_devt now encodes both the major number the kernel chose and a starting minor number of 0, for EP_NUM_DEVICES consecutive minors.
Step 2 — Describing the Operations
A struct file_operations is just a table of function pointers — the kernel calls through it whenever an application calls open(), read(), write(), or close() on the device node. Nothing here is specific to any one driver; it is the same contract every character driver implements.
Step 3 — Binding Operations to a cdev
The link between a device number and your file_operations table is a struct cdev. You initialize one with cdev_init() and register it with cdev_add(). This is the step that older books skip by calling the older, coarser register_chrdev() — modern drivers use cdev directly because it supports the dynamic, multi-minor allocation from Step 1 cleanly.
Step 4 — Making /dev Nodes Appear Automatically
cdev_add() alone only tells the kernel “here is a driver,” it does not create anything under /dev. Without an entry in /sys/class, udevd has nothing to react to, so no device node gets created. class_create() creates that /sys/class entry once, and device_create() — called once per minor — is what actually makes udevd create /dev/ep_anatomy0, /dev/ep_anatomy1, and so on.
Complete Original Driver: ep_anatomy
Here is the full, original driver — written for this course, not copied from any book — implementing all five steps for two devices, /dev/ep_anatomy0 and /dev/ep_anatomy1. Each device holds a tiny in-kernel message buffer that write() fills and read() drains, so you can observe real data moving between user space and kernel space.
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/slab.h>
#define EP_NUM_DEVICES 2
#define EP_BUF_SIZE 64
struct ep_anatomy_dev {
struct cdev cdev;
char buf[EP_BUF_SIZE];
size_t len;
};
static dev_t ep_devt;
static struct class *ep_class;
static struct ep_anatomy_dev ep_devices[EP_NUM_DEVICES];
static int ep_open(struct inode *inode, struct file *file)
{
struct ep_anatomy_dev *dev = container_of(inode->i_cdev,
struct ep_anatomy_dev, cdev);
file->private_data = dev;
pr_info("ep_anatomy: open (minor %d)\n", iminor(inode));
return 0;
}
static ssize_t ep_read(struct file *file, char __user *ubuf,
size_t count, loff_t *ppos)
{
struct ep_anatomy_dev *dev = file->private_data;
if (*ppos >= dev->len)
return 0; /* EOF */
if (count > dev->len - *ppos)
count = dev->len - *ppos;
if (copy_to_user(ubuf, dev->buf + *ppos, count))
return -EFAULT;
*ppos += count;
pr_info("ep_anatomy: read %zu bytes\n", count);
return count;
}
static ssize_t ep_write(struct file *file, const char __user *ubuf,
size_t count, loff_t *ppos)
{
struct ep_anatomy_dev *dev = file->private_data;
if (count > EP_BUF_SIZE)
count = EP_BUF_SIZE;
if (copy_from_user(dev->buf, ubuf, count))
return -EFAULT;
dev->len = count;
pr_info("ep_anatomy: write %zu bytes\n", count);
return count;
}
static int ep_release(struct inode *inode, struct file *file)
{
pr_info("ep_anatomy: release\n");
return 0;
}
static const struct file_operations ep_fops = {
.owner = THIS_MODULE,
.open = ep_open,
.read = ep_read,
.write = ep_write,
.release = ep_release,
};
static int __init ep_anatomy_init(void)
{
int i, ret;
ret = alloc_chrdev_region(&ep_devt, 0, EP_NUM_DEVICES, "ep_anatomy");
if (ret)
return ret;
ep_class = class_create("ep_anatomy");
if (IS_ERR(ep_class)) {
unregister_chrdev_region(ep_devt, EP_NUM_DEVICES);
return PTR_ERR(ep_class);
}
for (i = 0; i < EP_NUM_DEVICES; i++) {
dev_t devt = MKDEV(MAJOR(ep_devt), MINOR(ep_devt) + i);
cdev_init(&ep_devices[i].cdev, &ep_fops);
ep_devices[i].cdev.owner = THIS_MODULE;
cdev_add(&ep_devices[i].cdev, devt, 1);
device_create(ep_class, NULL, devt, NULL, "ep_anatomy%d", i);
}
pr_info("ep_anatomy: loaded, major %d\n", MAJOR(ep_devt));
return 0;
}
static void __exit ep_anatomy_exit(void)
{
int i;
for (i = 0; i < EP_NUM_DEVICES; i++) {
dev_t devt = MKDEV(MAJOR(ep_devt), MINOR(ep_devt) + i);
device_destroy(ep_class, devt);
cdev_del(&ep_devices[i].cdev);
}
class_destroy(ep_class);
unregister_chrdev_region(ep_devt, EP_NUM_DEVICES);
pr_info("ep_anatomy: unloaded\n");
}
module_init(ep_anatomy_init);
module_exit(ep_anatomy_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("Original anatomy-of-a-driver demo for the free linux kernel development course");
Note the exit function’s order: destroy each device node, then delete each cdev, then destroy the class, then release the device-number region — the exact reverse of how init created them. Reversing the order (for example destroying the class before the device nodes that reference it) is a classic bug that surfaces as a crash or a warning only on module removal, which makes it easy to miss during development.
Build It
obj-m += ep_anatomy.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
$ make
make -C /lib/modules/6.x.x-generic/build M=/home/user/ep_anatomy modules
CC [M] ep_anatomy.o
MODPOST Module.symvers
CC [M] ep_anatomy.mod.o
LD [M] ep_anatomy.ko
Load, Test, and Watch the Kernel Log
# sudo insmod ep_anatomy.ko
# dmesg | tail -1
[ 5011.001122] ep_anatomy: loaded, major 240
# ls -l /dev/ep_anatomy*
crw------- 1 root root 240, 0 Aug 19 10:02 /dev/ep_anatomy0
crw------- 1 root root 240, 1 Aug 19 10:02 /dev/ep_anatomy1
# echo "hello driver" | sudo tee /dev/ep_anatomy0
hello driver
# sudo cat /dev/ep_anatomy0
hello driver
# dmesg | tail -4
[ 5012.331200] ep_anatomy: open (minor 0)
[ 5012.331344] ep_anatomy: write 13 bytes
[ 5012.402511] ep_anatomy: open (minor 0)
[ 5012.402617] ep_anatomy: read 13 bytes
# sudo rmmod ep_anatomy
# dmesg | tail -1
[ 5015.550001] ep_anatomy: unloaded
The second device, /dev/ep_anatomy1, behaves identically and independently — try writing something different to it and confirm the two buffers don’t interfere. That independence is exactly what the container_of() call inside ep_open() is doing: recovering the correct ep_anatomy_dev instance from whichever inode/cdev the application opened.
The Principle of Least Astonishment
The one rule that ties this whole chapter together, from character devices through sysfs, GPIO, debugfs, and now this full driver: an application using your driver should never be surprised. read() should behave like read() everywhere else in Linux, write() like write(), and error codes should mean what they always mean. Every deviation you introduce is a trap for the next developer — including future you — so when in doubt, match the conventions of drivers already in the kernel tree rather than inventing your own.
Common Mistakes
- Tearing down resources in the wrong order in the exit function (see the ordering note above).
- Forgetting to check the return value of
class_create()orcdev_add(), leaving the module in a half-initialized state on error. - Copying data with plain pointer dereferences instead of
copy_to_user()/copy_from_user()— user pointers must never be dereferenced directly. - Hard-coding a major number “for simplicity” — it works on your machine and breaks on someone else’s.
- Not resetting
*pposhandling correctly inread(), causing tools likecatto loop forever because EOF is never signalled.
Best Practices
- Always use dynamic major allocation unless you have a documented, specific reason not to.
- Keep
file_operationscallbacks short and push real work to a workqueue or kthread if it might sleep for a long time. - Log at
pr_info()sparingly in production drivers — verbose logging on every read/write is fine for a teaching example like this one, but noisy in real deployments. - Mirror init and exit exactly in reverse — write the exit function first, as a checklist, before you finish init.
Summary
A character driver’s anatomy comes down to five steps: reserve a device number, describe your operations, bind them with a cdev, expose device nodes through a class, and tear everything down in reverse order. The ep_anatomy driver built in this lecture is under 90 lines and demonstrates all five, using the same modern APIs found throughout the current kernel tree. This closes out the “Introducing Device Drivers” material in this free linux kernel development course — from here, later chapters build on this exact skeleton for real hardware.
Frequently Asked Questions
Why use alloc_chrdev_region() instead of register_chrdev() with a fixed major number?
A fixed major number can collide with another driver already using it on a given system. alloc_chrdev_region() asks the kernel for a free major dynamically, which is the approach current in-tree drivers use.
What does container_of() do in the open() function?
It recovers a pointer to the enclosing ep_anatomy_dev struct given only a pointer to the cdev field inside it – this is how a single file_operations table serves multiple independent device instances.
Why must copy_to_user()/copy_from_user() be used instead of memcpy()?
User-space pointers are not directly dereferenceable from kernel code – they may be invalid or point to swapped-out memory. copy_to_user()/copy_from_user() safely validate and transfer the data, returning an error instead of crashing the kernel.
What happens if I unload the module without releasing resources in the right order?
You can leak the allocated device-number region, leave a stale /sys/class entry, or in some cases crash or produce kernel warnings, because a later teardown step referenced something already destroyed.
Do I need udev installed for /dev nodes to appear?
On virtually all modern desktop and server distributions, yes – device_create() only creates the /sys/class entry; a running udevd (or systemd’s equivalent) is what actually creates the /dev node in response.
Can this driver skeleton be reused for real hardware?
Yes – the five-step skeleton is identical for real drivers; you would replace the in-memory buffer with actual register access, DMA, or interrupt handling specific to the device.
Keep Going With This Free Linux Kernel Development Course
You now have a complete, working character driver skeleton. Use it as the starting point for the hardware-specific drivers coming up next.
Next Lecture Back to Course Index
4 Comments