← Previous Lecture | Next Lecture →
This final lesson of the series completes our free Linux kernel development course module on device control by writing the user-space ioctl application that talks to the character driver we built in the previous lesson. You will build, load, and test the complete Linux ioctl system call round trip on a modern 6.x kernel, and pick up debugging habits that apply to any free Linux device drivers course project.
What You Will Learn
- How to open a device file and call ioctl() correctly from user space
- How to build and load the matching kernel driver
- How to verify the round trip with a working test sequence
- How to debug common ioctl failures like -EFAULT and -ENOTTY
Prerequisites
- The epdrv character driver and epdrv_ioctl.h header from the previous lesson
- A Linux 6.x machine or VM with build-essential and kernel headers installed
- Root or sudo access to insert kernel modules and create device nodes
Step 1: Writing the User-Space IOCTL Application
The user-space side of any Linux ioctl system call interaction is straightforward: open the device file, then call ioctl() with the command and an argument pointer, exactly matching the header shared with the kernel driver.
/* epdrv_test.c - user-space test application */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <errno.h>
#include "epdrv_ioctl.h"
int main(void)
{
int fd, val, ret;
fd = open("/dev/epdrv0", O_RDWR);
if (fd < 0) {
perror("open");
exit(EXIT_FAILURE);
}
val = 42;
ret = ioctl(fd, EPDRV_SET_VALUE, &val);
if (ret < 0) {
perror("ioctl SET_VALUE");
close(fd);
exit(EXIT_FAILURE);
}
printf("Set value to %d\n", val);
val = 0;
ret = ioctl(fd, EPDRV_GET_VALUE, &val);
if (ret < 0) {
perror("ioctl GET_VALUE");
close(fd);
exit(EXIT_FAILURE);
}
printf("Read back value: %d\n", val);
ret = ioctl(fd, EPDRV_RESET);
if (ret < 0) {
perror("ioctl RESET");
close(fd);
exit(EXIT_FAILURE);
}
printf("Driver value reset\n");
close(fd);
return 0;
}
Notice that EPDRV_SET_VALUE and EPDRV_GET_VALUE pass the address of a local variable, while EPDRV_RESET is called with no third argument at all — this matches exactly how each command was defined with _IOW, _IOR, and _IO in the shared header.
Step 2: Building and Loading the Driver
Build the kernel module using a standard out-of-tree Makefile, then load it and confirm the device node was created.
obj-m += epdrv.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
$ make
$ sudo insmod epdrv.ko
$ ls -l /dev/epdrv0
crw------- 1 root root 240, 0 Jul 11 10:00 /dev/epdrv0
Step 3: Building and Running the Test Application
$ gcc -o epdrv_test epdrv_test.c
$ sudo ./epdrv_test
Set value to 42
Read back value: 42
Driver value reset
| epdrv_test ioctl(SET_VALUE, 42) |
→ | epdrv driver stores value = 42 |
→ | epdrv_test ioctl(GET_VALUE) |
→ | driver copies value back |
Debugging IOCTL Failures
| Symptom | Likely Cause | Fix |
|---|---|---|
| ioctl returns -1, errno = ENOTTY | Command number mismatch between app and driver | Confirm both sides include the same shared header |
| ioctl returns -1, errno = EFAULT | Invalid or NULL pointer passed as arg | Pass the address of a real variable for _IOR/_IOW/_IOWR commands |
| open() fails with “No such device” | Module not loaded or device node missing | Check dmesg and lsmod, confirm insmod succeeded |
| Permission denied opening device | Device node permissions default to root only | Run test with sudo, or adjust udev rules for the device class |
Best Practices for Testing IOCTL Drivers
- Check the return value of every ioctl() call in user space, never assume success
- Use dmesg alongside your test app to correlate kernel-side and user-side behavior
- Write a small test for every defined command, including invalid ones, to confirm -ENOTTY handling
- Remove the module with rmmod between test runs to catch any cleanup bugs in the exit path
Security Considerations
Even in a simple test driver like this one, remember that any process with permission to open the device node can issue any of its defined ioctl commands. In production drivers, consider whether certain commands should be restricted with capability checks (for example via capable(CAP_SYS_ADMIN)) before performing sensitive operations.
Summary and Key Takeaways
- User-space ioctl calls must match the kernel driver’s command definitions exactly
- Always check ioctl() return values and use perror() or errno for diagnostics
- dmesg is your best friend for correlating kernel-side driver behavior with user-space results
- Test both valid and invalid commands to confirm your driver’s error handling works as expected
Conclusion
With this lesson, you have completed a full, modern, kernel 6.x-compatible walkthrough of the Linux ioctl system call: why it exists, how to implement it in a character driver, and how to build and test a matching user-space application. These same patterns apply directly to real embedded systems work, whether you are configuring a sensor, a serial port, or a custom hardware accelerator. Continue exploring more lessons in this free Linux kernel development course to build on these foundations.
Frequently Asked Questions
This usually means the command number the app sent doesn’t match anything the driver recognizes — double-check that both sides include the identical shared header.
No. Commands defined with the plain _IO() macro, like EPDRV_RESET, take no third argument.
By default, device nodes created with device_create() are owned by root with restrictive permissions; you can relax this with udev rules if needed.
Add pr_info() or dev_info() calls inside the handler and watch them with dmesg -w while running your test application.
Yes, for sensitive operations. Use capability checks like capable(CAP_SYS_ADMIN) before performing privileged actions inside the handler.

2 Comments