← Previous Lecture | Next Lecture →
If you are searching for a free Linux kernel programming course that actually explains how user space talks to a device driver, this lecture is for you. In this tutorial we break down the ioctl() system call — one of the most commonly used, and most misunderstood, interfaces in Linux device driver development — in plain, beginner-friendly language.
This is Part 1 of our free Linux device drivers course on the ioctl interface. We will cover
the “why” and the “how” from the user-space point of view, then in Part 2 we will move into the kernel-space
driver implementation using the modern unlocked_ioctl approach used on current Linux kernels.
What You Will Learn
- Why the ioctl() system call exists and when to use it in embedded Linux driver development
- How the kernel identifies which driver an ioctl request belongs to (the “magic number” scheme)
- How to build safe ioctl command codes with the encoding helper macros
- How a user-space application issues ioctl requests to a character device
- What changed in modern kernels and why old ioctl tutorials can be misleading today
Prerequisites
Before starting this free Linux kernel development course lecture, you should be comfortable with:
- Basic C programming (structures, pointers, macros)
- Basic Linux file operations:
open(),read(),write(),close() - A general idea of what a character device driver is (we cover this in earlier lectures of this free embedded systems course)
Why Do We Need ioctl() At All?
Linux device drivers are built around a very small, very generic set of operations: open, read, write, close, seek. That is deliberately minimal — it lets almost every device look like a file. But real hardware often needs to do things that don’t fit into “read some bytes” or “write some bytes.” A driver may need to:
- Reset the device to a known state
- Query a hardware status flag (is the device powered on?)
- Change a configuration parameter (baud rate, sampling frequency, power mode)
- Retrieve device-specific metadata that has no place in a normal read stream
None of these are naturally a “stream of bytes.” That is exactly the gap that ioctl() (short for Input/Output Control) fills. It is a general-purpose, device-specific control channel between user space and a driver, and it remains heavily used across the kernel — from GPIO and I2C subsystems to custom embedded drivers — even on the latest 6.x kernel releases.
|
User Space Application calls ioctl(fd, CMD, arg) |
➜ |
Kernel VFS Layer routes the fd to its driver |
➜ |
Device Driver .unlocked_ioctl handler runs |
The Magic Number: How the Driver Trusts a Command
Because ioctl() is so generic, the kernel needs a way to stop a command meant for one driver from accidentally being processed by a completely different driver. The convention used across the kernel is simple: every driver picks a unique “magic number” (a single byte value) and embeds it into every ioctl command it defines. When a request arrives, the driver’s first job is to check that the magic number matches its own before doing anything else. If it doesn’t match, the driver rejects the request instead of guessing at what it means.
Because many drivers in the kernel tree need a unique magic number, the kernel documentation maintains an official registry so driver authors don’t clash with each other. This is why picking a magic number for a production driver should always be checked against the current kernel source tree rather than reused from an old book or blog post — the registry evolves as new drivers are added.
Building ioctl Command Codes the Right Way
You should never hand-craft an ioctl command number yourself. The kernel exposes four helper macros (available
in both user space via <sys/ioctl.h> and in the kernel via the generic ioctl header) that
encode the command type, the magic number, a sequence number, and — where relevant — the size of the data being
transferred, all into one integer.
| Macro | Purpose |
|---|---|
_IO(type, nr) |
A command that carries no data at all (e.g. a reset) |
_IOR(type, nr, datatype) |
A command that reads data from the driver into user space |
_IOW(type, nr, datatype) |
A command that writes data from user space into the driver |
_IOWR(type, nr, datatype) |
A command that transfers data in both directions |
Best practice: define your ioctl commands in a single shared header file, and include that exact same header in both the user-space application and the kernel driver source. This guarantees the command numbers on both sides are always identical and prevents an entire class of hard-to-debug mismatches.
Here is an example header for a small demo character driver we’ll build across this course. Note this is original example code written for this course — the naming and layout are ours:
// epdrv_ioctl.h — shared by app and driver
#ifndef __EPDRV_IOCTL_H__
#define __EPDRV_IOCTL_H__
#define EPDRV_MAGIC 'e'
#define EPDRV_IOC_RESET _IO(EPDRV_MAGIC, 1)
#define EPDRV_IOC_GET_MODE _IOR(EPDRV_MAGIC, 2, int)
#define EPDRV_IOC_SET_MODE _IOW(EPDRV_MAGIC, 3, int)
#define EPDRV_IOC_MAXNR 3
#endif
Issuing an ioctl() Call From User Space
The user-space prototype of ioctl() looks deceptively simple:
#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);
It takes the open file descriptor, the encoded command, and an optional third argument. That third argument is usually either an integer value you’re sending to the driver, or a pointer the driver will fill in for you — the classic “pass by reference” pattern in C.
A small, self-contained example of calling our demo driver from user space:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include "epdrv_ioctl.h"
int main(void)
{
int fd, mode;
fd = open("/dev/epdrv0", O_RDWR);
if (fd < 0) {
perror("open");
return 1;
}
/* reset the device to a known state */
if (ioctl(fd, EPDRV_IOC_RESET) == -1) {
perror("ioctl RESET");
close(fd);
return 1;
}
/* ask the driver what mode it is currently in */
if (ioctl(fd, EPDRV_IOC_GET_MODE, &mode) == -1) {
perror("ioctl GET_MODE");
close(fd);
return 1;
}
printf("Current mode: %d\n", mode);
/* switch the device into mode 1 */
mode = 1;
if (ioctl(fd, EPDRV_IOC_SET_MODE, &mode) == -1) {
perror("ioctl SET_MODE");
close(fd);
return 1;
}
close(fd);
return 0;
}
A Note on Old Tutorials and Modern Kernels
Plenty of ioctl tutorials online (and in older books) still show a four-argument ioctl driver callback that
relies on a kernel-wide lock. That approach was removed from the mainline kernel well over a decade ago. If a
tutorial you’re reading shows a four-argument ioctl handler, treat it as outdated — we will show the correct,
current three-argument unlocked_ioctl style in Part 2 of this free Linux kernel development course,
which is what every actively maintained driver in the 6.x kernel tree uses today.
Common Mistakes Beginners Make With ioctl()
| Mistake | Why It’s a Problem |
|---|---|
| Hand-picking a magic number without checking the kernel registry | Risk of colliding with an existing driver’s command numbers |
| Defining the same command header separately in the app and the driver | The two copies drift apart over time and cause silent mismatches |
| Not checking the ioctl() return value | Failures are silently ignored, making bugs very hard to trace |
Real-World Use Cases
ioctl() is not just an academic exercise — it is used everywhere in real embedded Linux systems: GPIO chip configuration, V4L2 camera control, network interface configuration (via drivers under the hood), sensor calibration commands in industrial devices, and vendor-specific power management commands in SoC drivers. Understanding ioctl() properly is a core skill for any embedded systems or Linux device driver engineer.
Key Takeaways
- ioctl() is the standard way to send device-specific control commands that don’t fit read/write
- Every driver must validate a magic number before trusting a command
- Always build command codes with
_IO,_IOR,_IOW,_IOWR— never hardcode numbers - Share one command header between user space and the driver
- Modern kernels use the three-argument
unlocked_ioctlcallback — covered next in Part 2
Conclusion
You now understand what ioctl() is, why it exists, and how a user-space application issues ioctl requests to a
Linux device driver safely. This foundation matters — nearly every non-trivial character or platform driver you
will write in real embedded Linux work uses ioctl() for its control path. In Part 2 of this free Linux kernel
programming course, we move to the kernel side and build the matching driver implementation using the modern
unlocked_ioctl file operation, along with safe copying of data between user space and kernel space.
FAQ
Q1. What does ioctl stand for?
It stands for Input/Output Control — a system call used to send device-specific control commands to a driver.
Q2. Is ioctl() still used in modern Linux kernel development?
Yes. It remains a core mechanism in many subsystems and custom embedded drivers on current 6.x kernels, though
newer subsystem-specific interfaces (like sysfs or netlink) are preferred for certain use cases.
Q3. Why does every driver need its own magic number?
So that a command intended for one driver cannot be misinterpreted by a different driver sharing the same numeric
command value.
Q4. Can I just make up any command number for my ioctl?
No. Always build it using the _IO/_IOR/_IOW/_IOWR macros so
the type and direction are correctly encoded, and choose a magic number that doesn’t clash with existing kernel
drivers.
Q5. What is the difference between _IOR and _IOW?
_IOR is for commands where the driver sends data back to user space; _IOW is for
commands where user space sends data to the driver.
Q6. Do I need a shared header file between the app and driver?
It’s strongly recommended. It guarantees both sides always agree on the exact command values.
Q7. Is the four-argument ioctl driver callback still valid on modern kernels?
No, it was removed long ago. Modern drivers use the three-argument unlocked_ioctl callback, which we
cover in Part 2.
Q8. Where can I find officially registered ioctl magic numbers?
In the current Linux kernel source tree’s ioctl documentation, which is the authoritative and up-to-date
reference.
Head to Part 2 to implement the matching kernel-space driver with the modern unlocked_ioctl approach.

2 Comments