If you are searching for a misc character device driver tutorial that actually explains things in plain English, you are in the right place. This lesson is part of EmbeddedPathashala’s free Linux kernel programming course, and it walks you through building a real, working misc character device driver from scratch on a modern Linux kernel. No prior driver-writing experience is assumed — we start from zero and build up, one concept at a time.
What You Will Learn
- What a misc character device driver is and why the kernel offers this framework
- The difference between a traditional character driver and a misc driver
- The key data structures:
struct miscdeviceandstruct file_operations - How to register and unregister a misc character device driver on a current kernel
- How to implement open, read, write, and release callbacks safely
- How to build, load, and test your driver from user space
- Common mistakes beginners make and how to avoid them
Prerequisites
- Basic C programming knowledge (pointers, structures, functions)
- A Linux machine or virtual machine (Ubuntu, Debian, or Fedora all work fine)
- Kernel headers installed for your running kernel
- Comfort with the Linux terminal
This tutorial targets modern kernels (6.x series). If you’re on an older kernel, most of the concepts still apply, but a few function signatures may differ slightly.
What Is a Misc Character Device Driver?
Every character device driver in Linux needs a major number and a minor number so that user space can talk to it through a device file. Traditionally, you had to call register_chrdev(), manage your own major number, and set up a cdev structure by hand. That’s a lot of boilerplate for a driver that only needs one device node.
This is exactly the problem the misc character device driver framework solves. The kernel already owns major number 10 for “miscellaneous” devices, and it hands out minor numbers for you automatically. Instead of managing major/minor numbers yourself, you simply describe your device with a small structure and call one function: misc_register(). Under the hood the kernel still creates a normal character device, but you skip almost all the setup work.
Traditional Char Driver vs Misc Driver
| Aspect | Traditional Character Driver | Misc Character Device Driver |
|---|---|---|
| Major number | You allocate one yourself | Shared major number 10, handled by the kernel |
| Minor number | You track and assign it | Usually dynamic, kernel assigns it |
| Setup code | cdev_init, cdev_add, class_create, device_create | One call: misc_register() |
| Best suited for | Drivers needing multiple device nodes or custom numbering | Simple, single-instance devices (sensors, small utility devices, IPC helpers) |
| /dev node creation | Manual | Automatic, if udev is running |
Core Data Structures
Two structures matter most when you write a misc character device driver: struct miscdevice, which describes your device to the kernel, and struct file_operations, which tells the kernel which functions to call when a user-space program opens, reads from, writes to, or closes your device file.
open() / read() / write()
callbacks
Step-by-Step: Registering a Misc Driver
Let’s build a small driver called ep_miscdrv. It keeps a fixed-size kernel buffer and lets a user-space program write a short message into it and read it back. Every line below is written fresh for this lesson.
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>
#include <linux/uaccess.h>
#include <linux/slab.h>
#define EP_BUF_SIZE 128
#define EP_DEV_NAME "ep_miscdrv"
static char *ep_kbuf;
static size_t ep_data_len;
static int ep_open(struct inode *inode, struct file *filp)
{
pr_info("%s: device opened\n", EP_DEV_NAME);
return 0;
}
static int ep_release(struct inode *inode, struct file *filp)
{
pr_info("%s: device closed\n", EP_DEV_NAME);
return 0;
}
static const struct file_operations ep_fops = {
.owner = THIS_MODULE,
.open = ep_open,
.release = ep_release,
};
static struct miscdevice ep_miscdevice = {
.minor = MISC_DYNAMIC_MINOR,
.name = EP_DEV_NAME,
.fops = &ep_fops,
.mode = 0666,
};
static int __init ep_miscdrv_init(void)
{
int ret;
ep_kbuf = kzalloc(EP_BUF_SIZE, GFP_KERNEL);
if (!ep_kbuf)
return -ENOMEM;
ret = misc_register(&ep_miscdevice);
if (ret) {
kfree(ep_kbuf);
pr_err("%s: misc_register failed\n", EP_DEV_NAME);
return ret;
}
pr_info("%s: registered, minor=%d\n", EP_DEV_NAME, ep_miscdevice.minor);
return 0;
}
static void __exit ep_miscdrv_exit(void)
{
misc_deregister(&ep_miscdevice);
kfree(ep_kbuf);
pr_info("%s: unregistered\n", EP_DEV_NAME);
}
module_init(ep_miscdrv_init);
module_exit(ep_miscdrv_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("A simple misc character device driver example");
Notice how little setup is required. We allocate a kernel buffer with kzalloc(), fill in a miscdevice structure, and call misc_register(). The kernel takes care of assigning a free minor number and, together with udev, creating /dev/ep_miscdrv automatically.
Implementing Read and Write Safely
Reading and writing is where most beginner misc character device driver code goes wrong, because kernel space and user space use completely different memory. You can never dereference a user-space pointer directly inside the kernel — you must use copy_from_user() and copy_to_user(), and you must always check their return value and always bound your copy length.
static ssize_t ep_write(struct file *filp, const char __user *ubuf,
size_t count, loff_t *off)
{
size_t to_copy = min_t(size_t, count, EP_BUF_SIZE - 1);
if (copy_from_user(ep_kbuf, ubuf, to_copy))
return -EFAULT;
ep_kbuf[to_copy] = '\0';
ep_data_len = to_copy;
pr_info("%s: stored %zu bytes\n", EP_DEV_NAME, to_copy);
return to_copy;
}
static ssize_t ep_read(struct file *filp, char __user *ubuf,
size_t count, loff_t *off)
{
size_t to_copy = min_t(size_t, count, ep_data_len);
if (*off >= ep_data_len)
return 0;
if (copy_to_user(ubuf, ep_kbuf, to_copy))
return -EFAULT;
*off += to_copy;
return to_copy;
}
Add .read = ep_read and .write = ep_write to the ep_fops structure shown earlier, and your driver can now safely accept and return data. Notice the use of min_t() to clamp the copy size to the buffer capacity — this single line is what keeps the driver from writing past the end of ep_kbuf. We’ll dig much deeper into this kind of bounds checking, and what happens when it’s missing, in the next lesson on kernel driver security.
Building and Loading the Module
Create a Makefile next to your source file:
obj-m += ep_miscdrv.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
Then build and load it:
$ make
$ sudo insmod ep_miscdrv.ko
$ dmesg | tail
$ ls -l /dev/ep_miscdrv
Testing From User Space
You don’t even need a custom C program to try this driver — plain shell commands work because a misc character device driver behaves like any other file:
$ echo "Hello from EmbeddedPathashala" | sudo tee /dev/ep_miscdrv
$ sudo cat /dev/ep_miscdrv
Real-World Use Cases
| Use Case | Why Misc Framework Fits |
|---|---|
| Simple sensor drivers | Single instance, no need for multiple minor numbers |
| Watchdog-style utility devices | Kernel already provides /dev/watchdog as a misc device |
| IPC helper devices | Small control interface between user space and a kernel subsystem |
| Debug/diagnostic interfaces | Quick way to expose kernel state to user space during development |
Common Mistakes and Troubleshooting
| Mistake | Symptom | Fix |
|---|---|---|
| Forgetting to check copy_from_user() return value | Silent data corruption | Always check the return value and return -EFAULT on failure |
| Not bounding the copy length | Kernel buffer overflow, crashes | Use min_t() against your buffer size |
| Forgetting misc_deregister() on exit | /dev node lingers or module can’t reload | Always pair register with deregister |
| Using kmalloc() without checking for NULL | NULL pointer dereference under memory pressure | Always check the return of kzalloc()/kmalloc() |
Best Practices
- Prefer
MISC_DYNAMIC_MINORunless you have a specific reason to hardcode a minor number - Keep your
file_operationscallbacks small and delegate real logic to helper functions - Always free allocated memory in your exit function
- Log meaningful messages with
pr_info()/pr_err()so debugging is easier
Performance Considerations
Misc character device drivers are not meant for high-throughput data paths — they go through the standard VFS read/write path, which adds some overhead per call. For low-frequency control or configuration data, this is negligible. If you need high-throughput transfers, consider mmap-based I/O or a dedicated subsystem instead of relying purely on read/write.
Security Considerations
Because a misc character device driver crosses the user/kernel boundary, it is a common place for security bugs to creep in. Unchecked copy lengths, missing return-value checks, and trusting user-supplied sizes are the most frequent causes of real kernel vulnerabilities. We cover this in full detail, with a dedicated example, in the next lesson of this free Linux kernel programming course.
Summary / Key Takeaways
- A misc character device driver reuses major number 10 and lets the kernel assign minor numbers automatically
misc_register()is the single call that replaces most of the manual char driver setupcopy_from_user()andcopy_to_user()must always be bounds-checked- The misc framework is ideal for simple, single-instance devices
Conclusion
You’ve now written, built, loaded, and tested a complete misc character device driver on a modern Linux kernel. This is one of the most practical building blocks in kernel and embedded Linux development, and it forms the foundation for far more advanced driver work later in this free Linux kernel programming course. Keep practicing by extending the buffer logic, adding an ioctl() handler, or wiring the driver into a real embedded systems project.
Frequently Asked Questions
misc_register().miscdevice.h and fs.h headers for any signature changes.More free lessons on Linux device drivers and embedded systems are on the way.
Previous Lecture Next Lecture
2 Comments