L2- Linux Device Number Allocation Guide- Free Linux Device Drivers Course

 

Linux Device Number Allocation Guide

Part 2 of the Character Device Drivers chapter — free Linux kernel development course by EmbeddedPathashala

Kernel 6.x Ready
Complete Working Driver
Beginner Friendly

free embedded systems course
free linux development course
free linux device drivers course
free linux kernel development course
linux device number allocation

In Part 1 of this free linux device drivers course we saw how struct cdev and major/minor numbers identify a device. This lecture focuses on linux device number allocation: the two ways the kernel lets a driver claim a major and minor number, and then moves on to the file_operations structure — the table of callbacks that actually makes your device readable and writable from user space. We finish with a complete, original driver you can build and test end to end.

What You Will Learn

Static vs dynamic device number allocation
register_chrdev_region() vs alloc_chrdev_region()
The file_operations structure
Implementing open, read, write, release
Automatic /dev node creation with class_create() and device_create()
Testing a driver end to end

Prerequisites

Part 1 of this chapter (struct cdev, major/minor numbers)
Basic C pointers and buffers
A Linux machine with kernel headers installed

Two Ways to Perform Linux Device Number Allocation

Before your driver can register a struct cdev, it needs a device number — a major and minor pair. There are exactly two ways to get one, and the choice matters more than beginners expect.

Static Allocation with register_chrdev_region()

int register_chrdev_region(dev_t first, unsigned int count, const char *name);

Here you decide the major number in advance, using MKDEV(major, minor) to build first. count is how many consecutive minor numbers you need, and name shows up in /proc/devices. The function returns 0 on success or a negative error code on failure.

The problem: you are guessing a number that is free on your machine right now. Load your driver on a different system, or after another driver has claimed that same number, and register_chrdev_region() simply fails.

Dynamic Allocation with alloc_chrdev_region()

int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, const char *name);

This time the kernel chooses the major number for you and writes the result into dev. firstminor is normally 0, count is the number of minors you need, and name is your driver’s name. This is the method used in the Part 1 demo, and it is the recommended approach for essentially every modern driver.

Aspect register_chrdev_region() alloc_chrdev_region()
Who picks the major number You, the developer The kernel
Risk of conflict on another machine High None
Typical use case Learning, or reserving a well-known legacy major Production drivers
Recommended for new drivers No Yes
Choosing a Device Number Allocation Method
Need a device number
register_chrdev_region()
you pick the major
alloc_chrdev_region()
kernel picks the major — recommended

Note: in real subsystem drivers you rarely call these functions directly — frameworks like the input subsystem, IIO, or RTC wrap them behind their own registration APIs. Learning the raw calls here builds the foundation for understanding what those frameworks do underneath.

Introduction to the file_operations Structure

Once a device number exists and struct cdev is registered, the kernel needs to know what to actually do when a user calls open(), read(), write(), or close() on your device file. That mapping lives in struct file_operations.

struct file_operations {
    struct module *owner;
    ssize_t (*read)  (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    int     (*open)  (struct inode *, struct file *);
    int     (*release)(struct inode *, struct file *);
    /* many more callbacks exist for llseek, ioctl, mmap, poll, and so on */
};

You only fill in the callbacks your device actually needs — every field you leave out simply stays NULL, and the kernel handles that call with a sensible default or returns an error to user space.

Why copy_to_user() and copy_from_user() Matter

Kernel code and user-space code live in different memory spaces. You can never dereference a user-space pointer directly inside read() or write() — doing so can crash the kernel or leak memory. Instead you always use:

  • copy_to_user(to, from, n) — copies kernel data out to a user buffer, used inside read()
  • copy_from_user(to, from, n) — copies user data into a kernel buffer, used inside write()

Both return the number of bytes that could not be copied — 0 means full success.

Hands-On: A Complete Character Device Driver

This original example combines everything from both lectures: dynamic device number allocation, struct cdev, a full file_operations table, and automatic /dev node creation using class_create() and device_create() so you don’t need mknod.

// ep_chardev_full.c
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/device.h>

#define EP_DEVICE_NAME "ep_chardev_full"
#define EP_BUF_SIZE    128

static dev_t ep_devnum;
static struct cdev ep_cdev;
static struct class *ep_class;
static char ep_buffer[EP_BUF_SIZE];
static size_t ep_data_len;

static int ep_open(struct inode *inode, struct file *filp)
{
    pr_info("%s: opened\n", EP_DEVICE_NAME);
    return 0;
}

static int ep_release(struct inode *inode, struct file *filp)
{
    pr_info("%s: closed\n", EP_DEVICE_NAME);
    return 0;
}

static ssize_t ep_read(struct file *filp, char __user *ubuf,
                        size_t count, loff_t *offp)
{
    size_t remaining, to_copy;

    if (*offp >= ep_data_len)
        return 0; /* EOF */

    remaining = ep_data_len - *offp;
    to_copy = min(count, remaining);

    if (copy_to_user(ubuf, ep_buffer + *offp, to_copy))
        return -EFAULT;

    *offp += to_copy;
    return to_copy;
}

static ssize_t ep_write(struct file *filp, const char __user *ubuf,
                         size_t count, loff_t *offp)
{
    size_t to_copy = min(count, (size_t)(EP_BUF_SIZE - 1));

    if (copy_from_user(ep_buffer, ubuf, to_copy))
        return -EFAULT;

    ep_buffer[to_copy] = '\0';
    ep_data_len = to_copy;

    pr_info("%s: stored %zu bytes\n", EP_DEVICE_NAME, to_copy);
    return to_copy;
}

static const struct file_operations ep_fops = {
    .owner   = THIS_MODULE,
    .open    = ep_open,
    .release = ep_release,
    .read    = ep_read,
    .write   = ep_write,
};

static int __init ep_chardev_init(void)
{
    int ret;

    ret = alloc_chrdev_region(&ep_devnum, 0, 1, EP_DEVICE_NAME);
    if (ret < 0)
        return ret;

    cdev_init(&ep_cdev, &ep_fops);
    ep_cdev.owner = THIS_MODULE;

    ret = cdev_add(&ep_cdev, ep_devnum, 1);
    if (ret < 0)
        goto err_region;

    ep_class = class_create(EP_DEVICE_NAME);
    if (IS_ERR(ep_class)) {
        ret = PTR_ERR(ep_class);
        goto err_cdev;
    }

    if (IS_ERR(device_create(ep_class, NULL, ep_devnum, NULL, EP_DEVICE_NAME))) {
        ret = -ENODEV;
        goto err_class;
    }

    pr_info("%s: ready at major=%d minor=%d, /dev/%s created\n",
             EP_DEVICE_NAME, MAJOR(ep_devnum), MINOR(ep_devnum), EP_DEVICE_NAME);
    return 0;

err_class:
    class_destroy(ep_class);
err_cdev:
    cdev_del(&ep_cdev);
err_region:
    unregister_chrdev_region(ep_devnum, 1);
    return ret;
}

static void __exit ep_chardev_exit(void)
{
    device_destroy(ep_class, ep_devnum);
    class_destroy(ep_class);
    cdev_del(&ep_cdev);
    unregister_chrdev_region(ep_devnum, 1);
    pr_info("%s: unloaded\n", EP_DEVICE_NAME);
}

module_init(ep_chardev_init);
module_exit(ep_chardev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("Complete character device driver with read/write and automatic /dev node creation");

Kernel 6.4+ note: class_create() dropped its owner parameter and now takes only the class name, as used above. If you are on an older kernel, add THIS_MODULE as the first argument.

Building, Loading, and Testing

$ make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
$ sudo insmod ep_chardev_full.ko
$ dmesg | tail -n 2

Expected output:

[ 2001.114455] ep_chardev_full: ready at major=239 minor=0, /dev/ep_chardev_full created

Now write to it and read it back:

$ echo "hello kernel" | sudo tee /dev/ep_chardev_full
hello kernel
$ sudo cat /dev/ep_chardev_full
hello kernel

Expected dmesg entry after the write:

[ 2005.221190] ep_chardev_full: stored 13 bytes

Clean up when you’re done:

$ sudo rmmod ep_chardev_full
$ ls /dev/ep_chardev_full
ls: cannot access '/dev/ep_chardev_full': No such file or directory

Real-World Use Cases

  • Sensor drivers that expose raw readings through a simple read() interface
  • Debug/diagnostic interfaces for embedded boards
  • Simple IPC channels between a kernel module and a user-space daemon
  • Educational and prototype drivers before migrating to a proper subsystem framework

Common Mistakes and Troubleshooting

Mistake Symptom Fix
Dereferencing a user pointer directly Kernel oops / crash Always use copy_to_user()/copy_from_user()
Not checking IS_ERR() after class_create() Silent NULL pointer crash later Always validate with IS_ERR()/PTR_ERR()
Missing device_destroy() in exit path Stale /dev entry after rmmod Mirror every create call with its destroy call
Not updating *offp in read() cat hangs or loops infinitely Always advance the file offset and return 0 at EOF

Security Considerations

Never trust the size or content of data coming from user space. Always bound your copy lengths against your kernel buffer size, as shown with min(count, ...) above, and always use the copy_to_user()/copy_from_user() pair rather than a plain memcpy().

Summary and Key Takeaways

  • Use alloc_chrdev_region() for linux device number allocation in new drivers; avoid hardcoding a major number.
  • file_operations is the callback table that connects user-space system calls to your driver code.
  • Always validate user-space buffers through copy_to_user()/copy_from_user().
  • class_create() and device_create() automate /dev node creation so users don’t need mknod.
  • Every allocation (region, cdev, class, device) needs a matching cleanup call in the exit path.

Suggested Images for This Article

  • Screenshot of insmod + dmesg showing device registration — ALT text: “linux device number allocation dmesg output showing major minor registration”
  • Screenshot of echo/cat test on the device file — ALT text: “linux character device driver read write test using cat and echo”
  • Diagram comparing register_chrdev_region and alloc_chrdev_region — ALT text: “linux device number allocation static vs dynamic comparison diagram”

Frequently Asked Questions

What is the safest method for linux device number allocation?

alloc_chrdev_region() is the safest and recommended method because the kernel picks a free major number automatically, avoiding conflicts across different machines.

Why can’t I access a user-space pointer directly inside read() or write()?

User space and kernel space use separate memory mappings and protection levels. Directly dereferencing a user pointer can crash the kernel or corrupt memory, so copy_to_user() and copy_from_user() are required.

Do I always need class_create() and device_create()?

No, but without them you must manually create the device node with mknod using the major and minor numbers printed in dmesg. class_create() and device_create() automate this through udev.

What changed in class_create() on newer kernels?

Starting with kernel 6.4, class_create() takes only the class name and no longer requires the owner module argument.

What happens if I forget to update the file offset in read()?

User-space tools like cat will call read() repeatedly forever since the kernel never sees the end of the data, causing the command to hang.

Can a single file_operations table serve multiple minor numbers?

Yes. The same callbacks can serve several minor numbers; your driver code typically uses the minor number to distinguish which sub-device instance is being accessed.

Continue the Free Linux Kernel Development Course

Next up: ioctl, polling, and blocking I/O for character device drivers.

 

 

Leave a Reply

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