Building a Misc Character Device Driver in Linux Kernel-Free Linux Device Drivers Course

Building a Misc Character Device Driver in Linux Kernel
Learn to design a driver private context structure and implement real read/write behaviour
Level: Beginner to Intermediate
Reading Time: 16 min
Category: Linux Device Drivers

Once you understand how data safely crosses the user-kernel boundary, the next natural step is building a complete misc character device driver in Linux kernel space that actually does something useful with that data. The misc framework is the fastest, simplest way to register a working character device without hand-rolling major and minor number management yourself.

In this lesson from our free Linux device drivers course, we design an original driver around a small private “driver context” structure, then implement init, read, write, and cleanup logic around it on a modern kernel.

Topics Covered in This Lesson
misc_register()
Driver Private Context
devm_kzalloc()
file_operations
Read and Write Callbacks
Cleanup and Unregistration

What You Will Learn

  • Why the misc framework is a great starting point for simple character drivers
  • How to design a private driver context structure to hold state cleanly
  • How to allocate that context safely using managed memory allocation
  • How to wire up read and write file operations around your context
  • How to clean up correctly when the module is removed

Prerequisites

  • Comfort with copy_to_user() and copy_from_user(), covered in the previous lesson
  • Basic understanding of loadable kernel modules and the module init/exit pattern
  • Familiarity with the C struct and pointer syntax

Why Use the Misc Framework?

Linux offers several ways to register a character device, but the misc framework is deliberately the simplest. Instead of manually requesting a major number and managing a device class, a single call to misc_register() handles device node creation for you, and the kernel automatically shares one common major number across all misc devices. This makes it an ideal teaching tool and a genuinely practical choice for small, simple drivers in real projects.

Misc Driver Registration Flow
Module Load
insmod
→ misc_register()
creates /dev node
→ Allocate Context
devm_kzalloc()
→ Driver Ready
read/write active

Designing an Original Driver Context Structure

Rather than scattering global variables throughout a driver, professional driver code groups all shared state into a single structure. This is often called a private context or driver context. It makes locking, debugging, and future maintenance far easier because every piece of relevant state lives in one predictable place.

Here is an original context structure for a small “counter and message” driver we will build in this lesson:

struct counter_drv_ctx {
    struct device *dev;
    unsigned int read_count;
    unsigned int write_count;
    char message[64];
};

static struct counter_drv_ctx *ctx;

This driver keeps a small message buffer that user space can update, along with two simple counters that track how many times the device has been read from and written to. It is intentionally simple so the pattern is easy to reuse in your own projects.

Allocating the Context During Init

Modern drivers should prefer managed allocation APIs such as devm_kzalloc() over plain kzalloc() wherever a device pointer is available. Managed allocation ties the memory’s lifetime to the device itself, so the kernel automatically frees it when the device is removed, reducing the chance of a memory leak in your cleanup path.

#include <linux/module.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/slab.h>

static struct miscdevice counter_miscdev;

static int __init counter_drv_init(void)
{
    struct device *dev;
    int ret;

    ret = misc_register(&counter_miscdev);
    if (ret) {
        pr_err("counter_drv: misc_register failed\n");
        return ret;
    }

    dev = counter_miscdev.this_device;

    ctx = devm_kzalloc(dev, sizeof(struct counter_drv_ctx), GFP_KERNEL);
    if (!ctx) {
        misc_deregister(&counter_miscdev);
        return -ENOMEM;
    }

    ctx->dev = dev;
    strscpy(ctx->message, "no message set yet", sizeof(ctx->message));

    dev_info(dev, "counter_drv: driver initialised\n");
    return 0;
}

Notice the use of strscpy() rather than strcpy() for the initial message. On current kernels, strscpy() is the recommended, bounds-safe way to copy a fixed-size string into a buffer, and it always null-terminates the destination correctly.

Implementing the Read Method

The read method returns the current message stored in the context, using copy_to_user() to hand it safely to the calling application, and increments our read counter for bookkeeping.

static ssize_t counter_drv_read(struct file *filp, char __user *ubuf,
                                 size_t count, loff_t *offp)
{
    size_t msg_len = strlen(ctx->message);
    size_t to_copy;

    if (*offp >= msg_len)
        return 0;

    to_copy = min(count, msg_len - (size_t)*offp);

    if (copy_to_user(ubuf, ctx->message + *offp, to_copy))
        return -EFAULT;

    *offp += to_copy;
    ctx->read_count++;

    return to_copy;
}

Implementing the Write Method

The write method accepts a new message from user space, clamps it to the context buffer’s capacity, and stores it for future reads.

static ssize_t counter_drv_write(struct file *filp, const char __user *ubuf,
                                  size_t count, loff_t *offp)
{
    size_t max_len = sizeof(ctx->message) - 1;
    size_t to_copy = min(count, max_len);

    if (copy_from_user(ctx->message, ubuf, to_copy))
        return -EFAULT;

    ctx->message[to_copy] = '\0';
    ctx->write_count++;

    dev_info(ctx->dev, "counter_drv: message updated (write #%u)\n",
             ctx->write_count);

    return to_copy;
}

Wiring Up file_operations

Finally, the read and write callbacks are attached through the standard file_operations structure, which is then linked into the miscdevice registration structure.

static const struct file_operations counter_drv_fops = {
    .owner = THIS_MODULE,
    .read  = counter_drv_read,
    .write = counter_drv_write,
};

static struct miscdevice counter_miscdev = {
    .minor = MISC_DYNAMIC_MINOR,
    .name  = "counter_drv",
    .fops  = &counter_drv_fops,
    .mode  = 0666,
};

Cleaning Up on Module Exit

Because the context memory was allocated with devm_kzalloc(), the exit path is refreshingly short. There is no manual kfree() call needed for the context itself; the kernel releases it automatically once the device is deregistered.

static void __exit counter_drv_exit(void)
{
    dev_info(ctx->dev, "counter_drv: read_count=%u write_count=%u\n",
             ctx->read_count, ctx->write_count);
    misc_deregister(&counter_miscdev);
}

module_init(counter_drv_init);
module_exit(counter_drv_exit);
MODULE_LICENSE("GPL");

Testing the Driver from the Command Line

Once the module is loaded, you can interact with the device node directly using standard shell commands:

sudo insmod counter_drv.ko
echo "hello kernel" | sudo tee /dev/counter_drv
cat /dev/counter_drv
sudo rmmod counter_drv
dmesg | tail

Common Mistakes and Troubleshooting

Mistake Fix
Using global variables instead of a context struct Group state in one struct for cleaner locking and maintenance
Forgetting to check misc_register() return value Always check and bail out cleanly on failure
Not clamping write size to buffer capacity Always subtract 1 for the null terminator and clamp with min()
Manually freeing devm_kzalloc() memory Managed allocation is freed automatically; manual free causes a double-free

Security Considerations

Because the device node in this example is created with permissive mode 0666, any local user can read and write it. In a real product, tighten the permissions with udev rules or a stricter mode, and never trust the contents written by user space beyond basic length validation.

Performance Considerations

For small control-style drivers like this one, performance is rarely a concern. If you later extend this pattern to handle larger data transfers, consider whether a fixed-size buffer is still appropriate or whether you need dynamic buffering strategies.

Best Practices Checklist

  • Group driver state into a single private context structure
  • Prefer devm_ managed allocation APIs when a device pointer is available
  • Always check the return value of misc_register()
  • Clamp all user-supplied lengths against real buffer capacity
  • Use strscpy() instead of strcpy() for fixed-size string copies

Summary and Key Takeaways

  • The misc framework is the simplest way to register a working character device
  • A private driver context structure keeps state organised and maintainable
  • devm_kzalloc() ties memory lifetime to the device, simplifying cleanup
  • Read and write callbacks should always clamp lengths and check copy_to_user/copy_from_user return values

Conclusion

You have now built a complete, original misc character device driver in Linux kernel space, from registration through to a clean exit path. This context-driven design pattern scales well into much more complex drivers as you continue through this free Linux kernel programming course.

Frequently Asked Questions

1. What is the difference between a misc device and a full character device with its own major number?

A misc device shares a common major number managed by the kernel, so you avoid manually allocating and tracking your own major number, which makes it faster to set up for simple drivers.

2. Why use devm_kzalloc() instead of kzalloc()?

devm_kzalloc() ties the allocated memory’s lifetime to the device, so the kernel frees it automatically when the device is removed, reducing the risk of forgetting a manual kfree() call.

3. Can I have multiple minor devices with one misc driver?

The misc framework is designed for single-instance devices. If you need multiple independent instances, a full character device with dynamic minor number allocation is usually more appropriate.

4. What does MISC_DYNAMIC_MINOR do?

It tells the kernel to automatically assign an available minor number for the device instead of requiring you to pick one manually.

5. Why did I choose strscpy() over strcpy() in this example?

strscpy() is bounds-aware and guarantees null-termination within the destination buffer size, which avoids the classic buffer overflow risks associated with strcpy().

6. Do I need a spinlock around the context in this simple example?

For a single-threaded demonstration driver like this one it is not strictly required, but any driver that may be accessed concurrently by multiple processes should protect shared context fields with an appropriate lock.

7. What permission mode should I use for a production device node?

Avoid 0666 in production. Use udev rules or a tighter mode combined with proper group ownership so only authorised users or processes can access the device.

Continue Your Free Linux Device Drivers Course

This lesson is part of EmbeddedPathashala’s free Linux kernel programming and device drivers course. Try extending this driver with an ioctl() interface as practice before the next lesson.

2 Comments

Leave a Reply

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