Building a Linux character device driver is only half the job. Until you write a small user-space program that opens the device node and exchanges data with it, you can never be sure your driver actually behaves the way you designed it to. In this lesson of our free Linux device drivers course we will build a compact, modern user-space test application that opens a character device, reads from it, writes to it, and reports exactly what happened — the same workflow professional embedded and kernel engineers use every day while bringing up new drivers.
What You Will Learn
- Why a driver needs a companion user-space test application
- How
open(),read(),write()andclose()map onto your driver’s file operations - How to build a small, reusable C test harness for any character device
- How to verify driver behaviour using
dmesgkernel logs - Common mistakes beginners make while testing drivers, and how to avoid them
- Security and performance points to keep in mind during driver testing
Prerequisites
- A working character or misc device driver already loaded on your system (basic
module_init/file_operationsknowledge) - Comfort with basic C and the Linux system call interface
- A Linux VM or board running a 5.x/6.x kernel with build tools installed (
build-essential, kernel headers)
Why You Need a User-Space Test Application
A character device driver only becomes useful once something in user space talks to it. The
kernel exposes your driver through a device node under /dev, and any process —
whether it is cat, dd, or your own program — interacts with that node
using ordinary POSIX calls. Writing your own test application, instead of only relying on shell
tools, gives you full control over buffer sizes, error handling, and timing, which is exactly
what you need while debugging a new driver.
| Test App open() / read() / write() |
→ | VFS Layer routes call to driver |
→ | Driver’s file_operations |
Designing the Test Application
A good driver test tool should be small, dependency-free, and flexible enough to run in both read and write mode from the command line. Below is an original, minimal test harness you can reuse for almost any character device — just change the device path.
// devtest.c - a small, reusable character device tester
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#define MAX_XFER 256
static void show_usage(const char *prog)
{
fprintf(stderr, "Usage: %s [message]\n", prog);
}
int main(int argc, char *argv[])
{
if (argc < 3) {
show_usage(argv[0]);
return 1;
}
const char *dev_path = argv[1];
char mode = argv[2][0];
int flags = (mode == 'w') ? O_WRONLY : O_RDONLY;
int fd = open(dev_path, flags);
if (fd < 0) {
fprintf(stderr, "open(%s) failed: %s\n", dev_path, strerror(errno));
return 1;
}
printf("Opened %s successfully, fd = %d\n", dev_path, fd);
if (mode == 'w') {
if (argc < 4) {
fprintf(stderr, "Write mode needs a message\n");
close(fd);
return 1;
}
ssize_t n = write(fd, argv[3], strlen(argv[3]) + 1);
if (n < 0)
fprintf(stderr, "write failed: %s\n", strerror(errno));
else
printf("Wrote %zd bytes to device\n", n);
} else {
char buf[MAX_XFER] = {0};
ssize_t n = read(fd, buf, sizeof(buf) - 1);
if (n < 0)
fprintf(stderr, "read failed: %s\n", strerror(errno));
else
printf("Read %zd bytes: \"%s\"\n", n, buf);
}
close(fd);
return 0;
}
Compile and run it against your driver’s device node like this:
$ gcc devtest.c -o devtest -Wall -O2
$ ./devtest /dev/mydriver r
Opened /dev/mydriver successfully, fd = 3
Read 12 bytes: "hello world"
$ ./devtest /dev/mydriver w "new value"
Opened /dev/mydriver successfully, fd = 3
Wrote 10 bytes to device
Verifying Behaviour With the Kernel Log
Every well-written driver should log entry into its open, read,
write and release callbacks using pr_debug() or
dev_dbg(). Running dmesg -w in a second terminal while your test app
runs lets you confirm that the exact number of bytes your app requested matches what the driver
actually transferred — a mismatch here is usually the first sign of a buffer or offset bug.
| Test App Action | Expected Driver Log |
|---|---|
open(O_RDONLY) |
driver_open(): read-only mode |
read(fd, buf, N) |
driver_read(): sent <=N bytes |
write(fd, buf, N) |
driver_write(): received N bytes |
close(fd) |
driver_release(): fd closed |
Real-World Use Cases
- Sensor bring-up: reading raw ADC or IMU samples from a driver before a full application stack exists
- Bootloader / firmware validation: small write-then-read loops that confirm a device holds state correctly across resets
- Automated CI on hardware farms: lightweight test binaries like this are ideal for shell-scripted regression suites on embedded boards
Common Mistakes and Troubleshooting
The device node under /dev was never created, or the module failed to load. Check lsmod and dmesg for registration errors first.
Your driver’s read callback may be returning 0 on the very first call instead of only at end-of-data. Confirm your offset (*ppos) handling.
Check the device node’s permissions and the udev rule or mode value used when the driver created it.
Best Practices
- Always check return values of
open(),read()andwrite()— never assume success - Keep your test buffer size explicit and never larger than what the driver documents as its maximum transfer size
- Test both the “happy path” and edge cases: zero-length writes, oversized reads, rapid open/close cycles
- Run your test app as a normal (non-root) user whenever your driver’s permissions allow it, to catch permission bugs early
Performance Considerations
For high-frequency testing, avoid opening and closing the device node on every iteration —
that path has real kernel overhead. Instead, open once and issue repeated read()/
write() calls in a loop when you are benchmarking throughput rather than testing
the open/close path itself.
Security Considerations
Never hard-code sensitive test data or credentials into a test application that might end up in
a shared repository. When testing drivers that touch hardware, also validate that your driver
correctly rejects out-of-range buffer sizes from user space — we cover this in depth in the
next lesson on copy_to_user() and copy_from_user() safety.
Summary / Key Takeaways
- A user-space test application is essential to validate any character device driver
- The core system calls involved are
open(),read(),write()andclose() - Cross-checking test app output against
dmesglogs catches most bugs quickly - A small, reusable C harness saves significant time across every driver you build
Conclusion
Testing is not an afterthought in driver development — it is part of the design. A disciplined, reusable test application, combined with careful kernel log review, will catch the vast majority of driver bugs long before they reach production hardware. In the next lesson of this free Linux kernel development course, we go one level deeper and look at the security-critical side of data transfer between user space and kernel space.
Frequently Asked Questions
Yes for simple checks, but a C test app gives you control over exact byte counts, error codes, and timing that shell tools cannot provide.
Only if the device node’s permissions require it. Well-designed drivers typically allow non-root testing for read/write operations.
This is normal POSIX behaviour — a short read simply means less data was available than the buffer size requested; always check the return value.
Extend the same test harness with an ioctl() branch and command-specific structures; the open/close logic remains identical.
Usually a buffering or flag mismatch — check whether your program opened the device with the correct flags (O_RDONLY/O_WRONLY/O_RDWR) that your driver expects.
No — misc devices use the exact same file_operations interface, so the same user-space test approach applies without modification.
More lessons on character devices, IPC, sockets and Bluetooth/BLE are waiting for you on EmbeddedPathashala.
Browse All Lectures
2 Comments