← Previous Lecture | Next Lecture →
In this lesson of our free Linux kernel development course, we implement a Linux ioctl character driver from scratch, targeting modern 6.x kernels. You will learn how to define custom ioctl commands with the _IO macro family, register a handler in file_operations, and safely exchange data with user space. This is a hands-on continuation of the previous lesson on the Linux ioctl system call basics.
What You Will Learn
- How to define custom ioctl commands safely with _IO macros
- How to register an unlocked_ioctl handler in a character driver
- How to move data safely with copy_to_user and copy_from_user
- How to register a character device on kernel 6.x using the modern class_create() API
Prerequisites
- Completed the previous lesson on the Linux ioctl system call fundamentals
- Comfortable writing and inserting a basic loadable kernel module (LKM)
- A Linux VM or board running a 6.x kernel with headers installed
Step 1: Defining Custom IOCTL Commands
Every Linux ioctl character driver needs a shared header file that defines its custom command numbers. Never invent these numbers by hand — always build them using the _IO, _IOR, _IOW, or _IOWR macros from <linux/ioctl.h>. These macros pack a magic number, a sequence number, a direction, and a size into a single unsigned long, which drastically reduces the chance of two unrelated drivers colliding on the same command value.
/* epdrv_ioctl.h - shared between kernel driver and user app */
#ifndef EPDRV_IOCTL_H
#define EPDRV_IOCTL_H
#include <linux/ioctl.h>
#define EPDRV_MAGIC 'E'
#define EPDRV_SET_VALUE _IOW(EPDRV_MAGIC, 1, int)
#define EPDRV_GET_VALUE _IOR(EPDRV_MAGIC, 2, int)
#define EPDRV_RESET _IO(EPDRV_MAGIC, 3)
#endif
| Macro | Data Direction | Typical Use |
|---|---|---|
| _IO | No data | Simple trigger commands, e.g. reset |
| _IOW | User to kernel | Setting a configuration value |
| _IOR | Kernel to user | Reading a status or value |
| _IOWR | Both directions | Combined read/modify/write commands |
Step 2: Writing the Driver’s IOCTL Handler
With the command numbers defined, the next step of this Linux ioctl character driver is the handler function itself. It receives the file pointer, the command, and the argument, then switches on the command to decide what to do.
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include "epdrv_ioctl.h"
static int epdrv_value;
static long epdrv_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
int tmp;
switch (cmd) {
case EPDRV_SET_VALUE:
if (copy_from_user(&tmp, (int __user *)arg, sizeof(tmp)))
return -EFAULT;
epdrv_value = tmp;
break;
case EPDRV_GET_VALUE:
tmp = epdrv_value;
if (copy_to_user((int __user *)arg, &tmp, sizeof(tmp)))
return -EFAULT;
break;
case EPDRV_RESET:
epdrv_value = 0;
break;
default:
return -ENOTTY;
}
return 0;
}
| cmd == EPDRV_SET_VALUE copy_from_user() |
cmd == EPDRV_GET_VALUE copy_to_user() |
cmd == EPDRV_RESET no data transfer |
unknown cmd return -ENOTTY |
Notice that the default case returns -ENOTTY rather than silently ignoring an unrecognized command — this is the standard kernel convention for “inappropriate ioctl for device” and helps user-space code detect mismatched driver versions.
Step 3: Registering the Handler and Creating the Device Node
The handler is wired up through the file_operations structure using .unlocked_ioctl, and the character device is registered using the modern alloc_chrdev_region/cdev/class_create sequence.
static int epdrv_open(struct inode *inode, struct file *filp) { return 0; }
static int epdrv_release(struct inode *inode, struct file *filp) { return 0; }
static const struct file_operations epdrv_fops = {
.owner = THIS_MODULE,
.open = epdrv_open,
.release = epdrv_release,
.unlocked_ioctl = epdrv_ioctl,
};
static struct cdev epdrv_cdev;
static struct class *epdrv_class;
static dev_t epdrv_devno;
static int __init epdrv_init(void)
{
int ret = alloc_chrdev_region(&epdrv_devno, 0, 1, "epdrv");
if (ret)
return ret;
cdev_init(&epdrv_cdev, &epdrv_fops);
ret = cdev_add(&epdrv_cdev, epdrv_devno, 1);
if (ret)
goto unregister;
/* class_create() takes only the class name on kernel 6.4+ */
epdrv_class = class_create("epdrv_class");
if (IS_ERR(epdrv_class)) {
ret = PTR_ERR(epdrv_class);
goto del_cdev;
}
device_create(epdrv_class, NULL, epdrv_devno, NULL, "epdrv0");
return 0;
del_cdev:
cdev_del(&epdrv_cdev);
unregister:
unregister_chrdev_region(epdrv_devno, 1);
return ret;
}
static void __exit epdrv_exit(void)
{
device_destroy(epdrv_class, epdrv_devno);
class_destroy(epdrv_class);
cdev_del(&epdrv_cdev);
unregister_chrdev_region(epdrv_devno, 1);
}
module_init(epdrv_init);
module_exit(epdrv_exit);
MODULE_LICENSE("GPL");
class_create() dropped its owner/THIS_MODULE parameter starting with Linux kernel 6.4. If you are following older tutorials online for your Linux ioctl character driver, watch out for this exact mismatch — it’s one of the most common build errors reported on recent kernels.
Common Mistakes When Writing an IOCTL Handler
| Mistake | Consequence |
|---|---|
| Dereferencing the arg pointer directly | Kernel oops or security hole; always go through copy_to_user/copy_from_user |
| Not checking copy_to_user/copy_from_user return value | Silent data corruption on partial copy failures |
| Forgetting the default case in the switch statement | Driver silently succeeds on unsupported commands instead of returning -ENOTTY |
Best Practices
- Keep the ioctl command header shared and versioned between kernel and user space
- Protect shared driver state (like epdrv_value above) with a mutex if multiple processes can open the device concurrently
- Return standard errno values (-EFAULT, -ENOTTY, -EINVAL) so user space can react appropriately
- Avoid large structures passed by value through ioctl; prefer pointers with explicit size fields
Performance Considerations
The Linux ioctl system call itself has very low overhead — it is a direct syscall trap into the driver. The real performance cost usually comes from copy_to_user/copy_from_user when transferring large buffers. For high-frequency, high-volume data transfer, ioctl is not the right tool; consider mmap-based shared memory or a dedicated read/write path instead, and reserve ioctl for control-plane operations.
Summary and Key Takeaways
- Define ioctl commands with _IO/_IOR/_IOW/_IOWR, never with raw numbers
- Hook .unlocked_ioctl in file_operations on modern kernels
- Always use copy_to_user/copy_from_user for data exchange
- Return -ENOTTY for unrecognized commands
- class_create() takes only a name argument from kernel 6.4 onward
Conclusion
You now have a complete, kernel 6.x-compatible Linux ioctl character driver that safely exchanges data with user space through custom commands. In the next lesson of this free Linux device drivers course, we will write the matching user-space application, build it, and test the full round trip end to end.
Frequently Asked Questions
The macros encode direction, magic number, and size into the command value, which helps the kernel and drivers detect mismatched or colliding commands.
It signals “inappropriate ioctl for device” — the standard response for a command your driver does not recognize.
If more than one process can access the device concurrently and the handler touches shared state, yes — protect it with a mutex or spinlock as appropriate.
Kernel 6.4 removed the owner/THIS_MODULE argument from class_create(). Check your kernel version and update the call signature accordingly.
Not ideally. Use it for control operations; for bulk data transfer, prefer read/write or mmap-based approaches.

2 Comments