How to Test a Linux Character Device Driver Using a User-Space C Application-Best Linux Device Driver Training Online

How to Test a Linux Character Device Driver Using a User-Space C Application
Free Linux Device Drivers Course – Learn how a normal C program talks to your kernel driver through open(), read() and write()
Difficulty: Beginner–Intermediate
Kernel: 6.x LTS
Reading time: 12 min

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() and close() 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 dmesg kernel 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_operations knowledge)
  • 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.

User Space to Kernel Space – Call Flow
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

“open() fails with ENOENT”

The device node under /dev was never created, or the module failed to load. Check lsmod and dmesg for registration errors first.

“read() returns 0 immediately”

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.

“Permission denied on open”

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() and write() — 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() and close()
  • Cross-checking test app output against dmesg logs 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

Q1. Can I use “cat” and “echo” instead of writing a C test app?

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.

Q2. Do I need root privileges to test a character device?

Only if the device node’s permissions require it. Well-designed drivers typically allow non-root testing for read/write operations.

Q3. What does it mean if read() returns fewer bytes than requested?

This is normal POSIX behaviour — a short read simply means less data was available than the buffer size requested; always check the return value.

Q4. How do I test a driver that supports ioctl() calls?

Extend the same test harness with an ioctl() branch and command-specific structures; the open/close logic remains identical.

Q5. Why does my driver work with “cat” but not with my C program?

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.

Q6. Is this test methodology different for misc devices versus full character devices?

No — misc devices use the exact same file_operations interface, so the same user-space test approach applies without modification.

Continue Your Free Linux Kernel Development Course

More lessons on character devices, IPC, sockets and Bluetooth/BLE are waiting for you on EmbeddedPathashala.

Browse All Lectures

2 Comments

Leave a Reply

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