Register Linux Character Device Driver
Free Linux Device Drivers Course — Chapter 4: Character Device Drivers
Topics Covered
cdev_init cdev_add example
class_create device_create linux
copy_to_user copy_from_user example
free linux device drivers course
free embedded systems course
What You Will Learn
This lecture completes the character device basics in our free linux kernel development course. Using the file operations table from the previous lecture, you will now register a character device so it appears as a real node in /dev, and you will exchange real data with it using safe kernel-to-user-space copy functions.
- The four steps needed to bring a character device to life in the kernel
- How
alloc_chrdev_region(),cdev_init(), andcdev_add()fit together - The modern kernel 6.4+
class_create()signature (the owner argument is gone) - How
device_create()makes your driver show up in/devautomatically - A complete, working example driver you can build and test
Prerequisites
- Completed the previous lecture on struct file_operations in this free linux device drivers course
- A Linux 6.x machine or VM with kernel headers and build-essential installed
- Root or sudo access to load and unload kernel modules
The Four Steps To Register A Character Device
Bringing a character device online in modern Linux always follows the same pattern, regardless of what the device actually does:
Step 1: Reserving a Major and Minor Number
alloc_chrdev_region() asks the kernel to dynamically hand out a major number and a range of minor numbers, instead of you hardcoding one that might already be taken. This is the recommended approach for any modern driver.
alloc_chrdev_region(&ep_devnum, 0, 1, “ep_pulse”);
Step 2: Creating a Device Class (Kernel 6.4+ API)
Starting with kernel 6.4, class_create() no longer takes a struct module *owner argument — it now takes only the class name. If your reference material still shows two arguments, it was written for an older kernel and will fail to compile on 6.4 and newer.
struct class *cls = class_create(THIS_MODULE, “ep_class”);/* Kernel 6.4 and later */
struct class *cls = class_create(“ep_class”);
Step 3: cdev_init() and cdev_add()
This is where your file operations table from the previous lecture finally gets attached to a live device object. cdev_init() links the struct cdev to your fops table, and cdev_add() registers it with the kernel core so system calls can start reaching it.
Step 4: device_create() — Making /dev/ep_pulse Appear
device_create() is what actually triggers udev to create the device node file under /dev. Without this step, your device would be registered internally but invisible to user space.
Complete Working Example: ep_pulse Driver
Below is a complete, original example driver named ep_pulse. It stores a small internal message, lets user space read it back, and lets user space overwrite it with write(). All kernel-to-user data movement uses copy_to_user() and copy_from_user(), the safe functions referenced in the previous lecture.
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>#define EP_BUF_SIZE 64static dev_t ep_devnum;
static struct cdev ep_cdev;
static struct class *ep_class;
static char ep_msg[EP_BUF_SIZE] = “hello from ep_pulse\n”;
static size_t ep_msg_len = 20;static int ep_pulse_open(struct inode *inode, struct file *filp)
{
pr_info(“ep_pulse: opened\n”);
return 0;
}
static int ep_pulse_release(struct inode *inode, struct file *filp)
{
pr_info(“ep_pulse: closed\n”);
return 0;
}
static ssize_t ep_pulse_read(struct file *filp, char __user *buf,
size_t len, loff_t *pos)
{
if (*pos >= ep_msg_len)
return 0;
if (len > ep_msg_len – *pos)
len = ep_msg_len – *pos;
if (copy_to_user(buf, ep_msg + *pos, len))
return -EFAULT;
*pos += len;
return len;
}
static ssize_t ep_pulse_write(struct file *filp, const char __user *buf,
size_t len, loff_t *pos)
{
if (len >= EP_BUF_SIZE)
len = EP_BUF_SIZE – 1;
if (copy_from_user(ep_msg, buf, len))
return -EFAULT;
ep_msg[len] = ‘\0’;
ep_msg_len = len;
pr_info(“ep_pulse: stored %zu bytes\n”, len);
return len;
}
static const struct file_operations ep_pulse_fops = {
.owner = THIS_MODULE,
.open = ep_pulse_open,
.release = ep_pulse_release,
.read = ep_pulse_read,
.write = ep_pulse_write,
};
static int __init ep_pulse_init(void)
{
int ret;
ret = alloc_chrdev_region(&ep_devnum, 0, 1, “ep_pulse”);
if (ret)
return ret;
ep_class = class_create(“ep_class”);
if (IS_ERR(ep_class)) {
unregister_chrdev_region(ep_devnum, 1);
return PTR_ERR(ep_class);
}
cdev_init(&ep_cdev, &ep_pulse_fops);
ep_cdev.owner = THIS_MODULE;
ret = cdev_add(&ep_cdev, ep_devnum, 1);
if (ret) {
class_destroy(ep_class);
unregister_chrdev_region(ep_devnum, 1);
return ret;
}
device_create(ep_class, NULL, ep_devnum, NULL, “ep_pulse”);
pr_info(“ep_pulse: driver loaded, major=%d\n”, MAJOR(ep_devnum));
return 0;
}
static void __exit ep_pulse_exit(void)
{
device_destroy(ep_class, ep_devnum);
cdev_del(&ep_cdev);
class_destroy(ep_class);
unregister_chrdev_region(ep_devnum, 1);
pr_info(“ep_pulse: driver unloaded\n”);
}
module_init(ep_pulse_init);
module_exit(ep_pulse_exit);
MODULE_LICENSE(“GPL”);
MODULE_AUTHOR(“EmbeddedPathashala”);
MODULE_DESCRIPTION(“Minimal character device demo driver”);
Building and Testing the Driver
Create a matching Makefile in the same folder:
obj-m += ep_pulse.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
Now build, load, and test the driver from a terminal:
$ sudo insmod ep_pulse.ko
$ ls -l /dev/ep_pulse
crw——- 1 root root 240, 0 Jul 20 10:00 /dev/ep_pulse$ cat /dev/ep_pulse
hello from ep_pulse$ echo “test message” | sudo tee /dev/ep_pulse
test message$ cat /dev/ep_pulse
test message
$ dmesg | tail -n 4
[ 1234.567] ep_pulse: driver loaded, major=240
[ 1234.789] ep_pulse: opened
[ 1235.001] ep_pulse: stored 13 bytes
[ 1235.002] ep_pulse: closed
$ sudo rmmod ep_pulse
Notice that cat only prints the message once and then stops. That is the expected behavior — once the read position reaches the end of the stored message, ep_pulse_read() returns 0, which every standard tool interprets as end-of-file.
Real-World Use Cases
- Exposing a small configuration string to user space, similar to a simplified sysfs attribute
- Simple sensor drivers that report the latest reading as text on read()
- Debug interfaces used during hardware bring-up before a full driver is ready
Common Mistakes and Troubleshooting
| Mistake | Symptom | Fix |
|---|---|---|
| Passing THIS_MODULE to class_create() | Build error on kernel 6.4+ | Call class_create(“name”) with a single argument |
| Not checking IS_ERR() on class_create() | Crash later if class creation silently failed | Always check with IS_ERR() and unwind on failure |
| Never returning 0 from read() | cat or similar tools hang forever | Return 0 once *pos reaches the end of available data |
| Forgetting device_destroy()/cdev_del() in exit() | Stale /dev node or leaked device number on unload | Unwind every step from init() in reverse order |
Best Practices
- Always unwind setup steps in reverse order on both the error path and the exit path.
- Keep the internal buffer size fixed and always null-terminate after a write.
- Log meaningful messages with
pr_info()sodmesgtells a clear story during testing.
Performance Considerations
For small control-style devices like this one, the cost of copy_to_user()/copy_from_user() is negligible. These functions matter far more once you are moving large buffers frequently, which is a topic for later chapters on DMA and memory mapping.
Security Considerations
This example clamps the write length to the buffer size before calling copy_from_user(), preventing a user program from overflowing ep_msg. Always bound-check lengths supplied by user space before using them in any copy or memory operation.
Summary and Key Takeaways
- Registering a character device always follows the same four steps: reserve numbers, create a class, attach and add the cdev, then create the device node.
- Kernel 6.4 removed the owner argument from
class_create()— always check which kernel version your reference material targets. copy_to_user()andcopy_from_user()are the only safe way to move data across the kernel/user boundary.- Returning 0 from read() at the right time is what makes tools like
catbehave correctly.
Frequently Asked Questions
Why does class_create() only take one argument now?
Since kernel 6.4, the struct module *owner argument was removed from class_create(). Older tutorials showing two arguments are written for kernels before 6.4.
What major number will my device get?
Whatever the kernel assigns dynamically through alloc_chrdev_region(). Check it with cat /proc/devices or the dmesg output printed in this lecture’s example.
Why did cat only print the message once?
Because ep_pulse_read() returns 0 once the current read position reaches the end of the stored message, which every standard tool treats as end-of-file.
Do I need udev rules to see the device node?
No. device_create() triggers udev automatically on a standard desktop or server Linux distribution, so the node appears in /dev without any manual rule.
What happens if I forget to call device_destroy() on unload?
The /dev node can be left behind or become stale, and reloading the module may fail or behave unpredictably until the system is rebooted.
Can this driver support multiple devices at once?
Not as written, since it uses a single dev_t and a single cdev. Supporting several devices means allocating more than one minor number and looping the cdev_init/cdev_add/device_create steps, which is covered later in this free linux kernel development course.
Continue This Free Linux Device Drivers Course
You now have a complete, working character device driver. The next lecture builds on this foundation with ioctl commands and device-specific control operations.
