Linux Character Driver ioctl Tutorial- Free Linux Device Drivers Course

Linux Character Driver ioctl Tutorial

Free Linux Device Drivers Course — Kernel 6.x Edition

Lecture 4.12
Beginner Friendly
Kernel 6.x Ready

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

ioctl system call linux device driver
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

read() / write() — stream data in and out
llseek() — move the file position
poll() — report readiness to select()/poll()/epoll()
unlocked_ioctl() — everything else: reset, configure, query

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

#ifndef 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/module.h>
#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

$ make
$ 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 <stdio.h>
#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

$ gcc -o ep_ioctl_test ep_ioctl_test.c
$ ./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.

 

Leave a Reply

Your email address will not be published. Required fields are marked *