Linux Character Driver ioctl Tutorial
Free Linux Device Drivers Course — Kernel 6.x Edition
This lecture in our free linux kernel development course explains the ioctl system call linux device driver interface: how to define your own custom commands for a character device beyond the standard open/read/write/llseek/poll operations you have already built in this free linux device drivers course. You will learn the _IO/_IOR/_IOW/_IOWR macros, how to pick a safe magic number, and how to implement the modern .unlocked_ioctl callback on kernel 6.x.
Focus Keywords
free linux device drivers course
free embedded systems course
free linux kernel development course
What You Will Learn
- Why ioctl() exists alongside read()/write()
- The _IO, _IOW, _IOR, _IOWR macros and magic numbers
- Writing a shared ioctl header for kernel and user space
- Implementing .unlocked_ioctl on kernel 6.x
- copy_to_user()/copy_from_user() inside ioctl handlers
- 32-bit compatibility with .compat_ioctl
Prerequisites
- struct cdev registration and file_operations basics
- copy_to_user()/copy_from_user() and put_user()/get_user()
- A working kernel 6.x build environment
- Comfort compiling and loading kernel modules with insmod/rmmod
Why Character Drivers Need ioctl()
A typical Linux system exposes a few hundred system calls in total, but only a small number of them are file-operation calls like read(), write(), and lseek(). Real hardware frequently needs commands that do not fit this data-streaming model — resetting a device, querying a size, changing a mode, or renaming an internal partition. The ioctl system call linux device driver mechanism exists exactly for this: it lets a driver author define an open-ended set of custom commands without adding new system calls to the kernel.
If a driver does not implement ioctl() at all, the kernel returns -ENOTTY to any user-space ioctl() call against that device, which you will use later as your own “unknown command” fallback.
Where ioctl() Fits Among File Operations
The Four ioctl Command-Building Macros
Every ioctl command must be encoded into a single unsigned integer that is unique across the system, so the kernel can tell devices and commands apart safely. Linux provides four helper macros, defined in linux/ioctl.h, to build this integer:
| Macro | Meaning | Typical copy direction |
|---|---|---|
| _IO(magic, seq) | No data transfer | none |
| _IOW(magic, seq, type) | Driver reads data from user space | copy_from_user() / get_user() |
| _IOR(magic, seq, type) | Driver writes data to user space | copy_to_user() / put_user() |
| _IOWR(magic, seq, type) | Data flows both directions | both |
Each macro packs three pieces of information: an 8-bit magic number identifying your driver, an 8-bit sequence number identifying the specific command, and (for W/R/WR variants) the C type used to compute the transfer size. The official list of magic numbers already claimed by in-tree drivers lives in the kernel source under Documentation/userspace-api/ioctl/ioctl-number.rst — always check it before picking your own magic number, to avoid colliding with an existing driver.
Step 1: A Shared ioctl Header
Best practice is to define your commands in one header file that both the kernel module and any user-space test program include, so the numbers can never drift out of sync. Here is an original header for this lecture’s demo driver:
ep_ioctl.h
#define EP_IOCTL_H#include <linux/ioctl.h>#define EP_MAGIC ‘K’#define EP_RESET_COUNTER _IO(EP_MAGIC, 1)
#define EP_SET_VALUE _IOW(EP_MAGIC, 2, int)
#define EP_GET_VALUE _IOR(EP_MAGIC, 3, int)
#define EP_SWAP_VALUE _IOWR(EP_MAGIC, 4, int)#endif
Save this file once and place a copy next to your kernel module source and next to your user-space test program, or use a symlink so both sides always agree on the exact same numbers.
Step 2: An Original ioctl-Capable Driver
Here is a fresh, kernel-6.x-clean driver called ep_ioctl_demo. It keeps one internal integer value in the driver and exposes four commands to reset it, set it, read it back, and atomically swap it — all without needing read() or write() at all.
ep_ioctl_demo.c
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/mutex.h>
#include “ep_ioctl.h”#define DEV_NAME “ep_ioctl_demo”static dev_t devno;
static struct cdev ep_cdev;
static DEFINE_MUTEX(ep_lock);
static int ep_value;static int ep_open(struct inode *inode, struct file *filp)
{
return 0;
}static long ep_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
int tmp, old_value;
switch (cmd) {
case EP_RESET_COUNTER:
mutex_lock(&ep_lock);
ep_value = 0;
mutex_unlock(&ep_lock);
break;
case EP_SET_VALUE:
if (copy_from_user(&tmp, (int __user *)arg, sizeof(tmp)))
return -EFAULT;
mutex_lock(&ep_lock);
ep_value = tmp;
mutex_unlock(&ep_lock);
break;
case EP_GET_VALUE:
mutex_lock(&ep_lock);
tmp = ep_value;
mutex_unlock(&ep_lock);
if (copy_to_user((int __user *)arg, &tmp, sizeof(tmp)))
return -EFAULT;
break;
case EP_SWAP_VALUE:
if (copy_from_user(&tmp, (int __user *)arg, sizeof(tmp)))
return -EFAULT;
mutex_lock(&ep_lock);
old_value = ep_value;
ep_value = tmp;
mutex_unlock(&ep_lock);
if (copy_to_user((int __user *)arg, &old_value, sizeof(old_value)))
return -EFAULT;
break;
default:
return -ENOTTY;
}
return 0;
}
static const struct file_operations ep_fops = {
.owner = THIS_MODULE,
.open = ep_open,
.unlocked_ioctl = ep_ioctl,
.compat_ioctl = ep_ioctl,
};
static int __init ep_ioctl_init(void)
{
int ret;
ret = alloc_chrdev_region(&devno, 0, 1, DEV_NAME);
if (ret)
return ret;
cdev_init(&ep_cdev, &ep_fops);
ret = cdev_add(&ep_cdev, devno, 1);
if (ret) {
unregister_chrdev_region(devno, 1);
return ret;
}
pr_info(“ep_ioctl_demo: loaded, major=%d\n”, MAJOR(devno));
return 0;
}
static void __exit ep_ioctl_exit(void)
{
cdev_del(&ep_cdev);
unregister_chrdev_region(devno, 1);
pr_info(“ep_ioctl_demo: unloaded\n”);
}
module_init(ep_ioctl_init);
module_exit(ep_ioctl_exit);
MODULE_LICENSE(“GPL”);
MODULE_AUTHOR(“EmbeddedPathashala”);
MODULE_DESCRIPTION(“Original ioctl demo driver”);
Kernel 6.x note: the old .ioctl field with the Big Kernel Lock disappeared long ago. Every modern driver must use .unlocked_ioctl, and provide its own locking (here, a plain mutex) around any shared state instead of relying on a kernel-wide lock.
Step 3: Building and Loading
Terminal Commands
$ sudo insmod ep_ioctl_demo.ko
$ dmesg | tail -n 2
ep_ioctl_demo: loaded, major=239
$ sudo mknod /dev/ep_ioctl_demo c 239 0
$ sudo chmod 666 /dev/ep_ioctl_demo
Step 4: A Complete User-Space Test Program
ep_ioctl_test.c
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include “ep_ioctl.h”#define DEV_PATH “/dev/ep_ioctl_demo”int main(void)
{
int fd, val, old_val;fd = open(DEV_PATH, O_RDWR);
if (fd < 0) {
perror(“open”);
return EXIT_FAILURE;
}if (ioctl(fd, EP_RESET_COUNTER) < 0)
perror(“EP_RESET_COUNTER”);
val = 42;
if (ioctl(fd, EP_SET_VALUE, &val) < 0)
perror(“EP_SET_VALUE”);
if (ioctl(fd, EP_GET_VALUE, &val) < 0)
perror(“EP_GET_VALUE”);
printf(“Current value: %d\n”, val);
val = 100;
if (ioctl(fd, EP_SWAP_VALUE, &val) < 0)
perror(“EP_SWAP_VALUE”);
old_val = val;
printf(“Swapped in 100, old value was: %d\n”, old_val);
close(fd);
return EXIT_SUCCESS;
}
Expected Output
$ ./ep_ioctl_test
Current value: 42
Swapped in 100, old value was: 42
Notice that EP_SWAP_VALUE uses the same buffer for both directions: the user writes 100 in, and the driver overwrites that same memory with the old value 42 before returning — a classic use of _IOWR.
unlocked_ioctl vs the Legacy .ioctl Field
| Aspect | Old .ioctl (removed) | Modern .unlocked_ioctl |
|---|---|---|
| Locking | Big Kernel Lock held automatically | Driver author must add explicit locking |
| Signature | int ioctl(struct inode *, struct file *, …) | long unlocked_ioctl(struct file *, unsigned int, unsigned long) |
| Concurrency | Serialized system-wide | Can run concurrently on multiple CPUs |
| Status on kernel 6.x | Removed, will not compile | Required field |
Common Mistakes and Troubleshooting
| Mistake | Symptom | Fix |
|---|---|---|
| Passing arg directly without copy_from_user() | Kernel oops or garbage values | Always copy from user space, never dereference arg directly |
| Reusing a magic number from another driver | Silent command collisions on shared systems | Check Documentation/userspace-api/ioctl/ioctl-number.rst first |
| Missing default case with -ENOTTY | Unknown commands return success incorrectly | Always return -ENOTTY for unrecognized cmd values |
| No locking around shared driver state | Race conditions under concurrent ioctl() calls | Protect state with a mutex or spinlock as appropriate |
| Forgetting .compat_ioctl | 32-bit user-space app fails ioctl() on 64-bit kernel | Point .compat_ioctl at the same handler when the layout is compatible |
Best Practices
- Keep one shared header for command numbers between kernel and user space
- Pick a magic number not already listed in the kernel’s ioctl-number documentation
- Validate cmd with a switch statement and always default to -ENOTTY
- Use copy_to_user()/copy_from_user() for structs, put_user()/get_user() for single values
- Prefer ioctl() only for control operations; keep bulk data transfer in read()/write()
Performance Considerations
ioctl() calls are control-plane operations, not data-plane ones, so they are typically infrequent compared to read()/write() traffic. Keep the handler itself short: avoid long-held locks, avoid sleeping unnecessarily, and never perform large data copies through ioctl() when a proper read()/write() or mmap() interface would be more appropriate for high-throughput data.
Security Considerations
ioctl() is a common attack surface because it accepts an arbitrary unsigned long arg that user space fully controls. Never trust arg as a pointer without copy_from_user()/copy_to_user(), never trust a size value from user space without bounds-checking it, and consider whether a given command should be restricted with capable(CAP_SYS_ADMIN) or similar checks if it can affect other users or system state.
Summary / Key Takeaways
- ioctl() lets a driver define custom commands beyond read/write/llseek/poll
- _IO/_IOW/_IOR/_IOWR encode direction and data type into one command number
- A shared header keeps kernel and user-space command numbers in sync
- .unlocked_ioctl replaces the removed legacy .ioctl field on kernel 6.x
- Always validate cmd, copy data safely, and lock shared state explicitly
Conclusion
You have now completed the core file_operations methods covered in this free linux device drivers course: open, release, read, write, llseek, poll, select, and finally ioctl(). The ioctl system call linux device driver interface gives your driver an extensible command channel for anything that does not fit the streaming read()/write() model, and the shared-header, magic-number, and locking patterns shown here scale directly to real production drivers. Keep following this free linux kernel development course for the next chapter, where we move from character drivers into kernel synchronization internals.
Frequently Asked Questions
Why was the old .ioctl file_operations field removed?
It relied on the Big Kernel Lock for automatic serialization, which limited scalability. Modern kernels require .unlocked_ioctl so each driver manages its own concurrency explicitly.
How do I choose a safe magic number for _IO macros?
Check Documentation/userspace-api/ioctl/ioctl-number.rst in the kernel source tree first, and pick an unused character to minimize the chance of colliding with another driver.
What does the kernel return if my driver has no ioctl handler?
It returns -ENOTTY to the calling user-space program, which is also the correct value your own handler should return for unrecognized command numbers.
Can I pass a struct through ioctl() instead of a single int?
Yes, use the struct type as the third argument to _IOW/_IOR/_IOWR and copy the whole struct with copy_to_user()/copy_from_user() inside your handler.
Do I need .compat_ioctl if my system is fully 64-bit?
If you never expect 32-bit user-space binaries to call your driver, you can omit it, but setting it to the same handler is a cheap safeguard when the data layout is identical on both architectures.
Is ioctl() considered good design for modern Linux drivers?
It remains the standard mechanism for device-specific control operations, though sysfs and debugfs are often preferred for simple configuration values, reserving ioctl() for actions and structured data exchange.
What happens if I forget copy_from_user() and dereference arg directly?
Treating a user-space pointer as a kernel pointer can crash the kernel or leak/corrupt memory; always use the copy_to_user()/copy_from_user() family for any pointer argument.
Should ioctl() commands require special permissions?
Any command that can affect other processes, system-wide state, or hardware in a disruptive way should check capabilities such as CAP_SYS_ADMIN before proceeding.
Continue This Free Linux Kernel Development Course
You have completed the character driver file operations series. Explore kernel synchronization next.
