Fading Coder

One Final Commit for the Last Sprint

Home > Notes > Content

Managing Multiple Character Devices of the Same Type in a Single Driver

Notes 1

Supporting several identical hardware instances from one driver module is a common requirement. The Linux character device subsystem allows a single driver to handle multiple minor numbers, each corresponding to a distinct device node while sharing the same file operations and major number. This approach avoids code duplication and simplifies maintenance.

Driver Implementation

Define a per-device structure that holds the cdev instance and any runtime data such as a buffer and its current length:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <asm/uaccess.h>
#include "mydevice.h"

#define BUF_SIZE         128
#define DEVICE_COUNT     3

static int base_major = 0;
static int base_minor = 0;

struct my_device {
    struct cdev cdev;
    char buffer[BUF_SIZE];
    int data_len;
};

static struct my_device dev_array[DEVICE_COUNT];

In the open method, store a pointer to the correct my_device structure using the inode's i_cdev field:

static int dev_open(struct inode *inode, struct file *filp)
{
    struct my_device *dev = container_of(inode->i_cdev, struct my_device, cdev);
    filp->private_data = dev;
    pr_debug("device opened\n");
    return 0;
}

Read and write callbacks operate on the device-specific buffer and length:

static ssize_t dev_read(struct file *filp, char __user *ubuf, size_t count, loff_t *off)
{
    struct my_device *dev = filp->private_data;
    size_t bytes = min(count, (size_t)dev->data_len);

    if (copy_to_user(ubuf, dev->buffer, bytes))
        return -EFAULT;

    memmove(dev->buffer, dev->buffer + bytes, dev->data_len - bytes);
    dev->data_len -= bytes;
    return bytes;
}

static ssize_t dev_write(struct file *filp, const char __user *ubuf, size_t count, loff_t *off)
{
    struct my_device *dev = filp->private_data;
    size_t space = BUF_SIZE - dev->data_len;
    size_t bytes = min(count, space);

    if (copy_from_user(dev->buffer + dev->data_len, ubuf, bytes))
        return -EFAULT;

    dev->data_len += bytes;
    return bytes;
}

I/O control commands report buffer capacity and current data length:

static long dev_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    struct my_device *dev = filp->private_data;
    int __user *uptr = (int __user *)arg;
    int val;

    switch (cmd) {
    case MYDEV_IOCTL_GET_BUFSZ:
        val = BUF_SIZE;
        if (copy_to_user(uptr, &val, sizeof(val)))
            return -EFAULT;
        break;
    case MYDEV_IOCTL_GET_DATALEN:
        if (copy_to_user(uptr, &dev->data_len, sizeof(dev->data_len)))
            return -EFAULT;
        break;
    default:
        return -ENOTTY;
    }
    return 0;
}

The release funcsion simply confirms closure:

static int dev_release(struct inode *inode, struct file *filp)
{
    pr_debug("device closed\n");
    return 0;
}

static struct file_operations fops = {
    .owner          = THIS_MODULE,
    .open           = dev_open,
    .read           = dev_read,
    .write          = dev_write,
    .unlocked_ioctl = dev_ioctl,
    .release        = dev_release,
};

During module initialisation, allocate a consecutive range of device numbers (major + DEVICE_COUNT minors). If static assignment fails, fall back to dynamic allocation:

static int __init dev_init(void)
{
    int ret, i;
    dev_t devno;

    devno = MKDEV(base_major, base_minor);
    ret = register_chrdev_region(devno, DEVICE_COUNT, "mydevice");
    if (ret) {
        ret = alloc_chrdev_region(&devno, 0, DEVICE_COUNT, "mydevice");
        if (ret) {
            pr_err("failed to obtain device number\n");
            return ret;
        }
        base_major = MAJOR(devno);
        base_minor = MINOR(devno);
    }

    for (i = 0; i < DEVICE_COUNT; i++) {
        devno = MKDEV(base_major, base_minor + i);
        cdev_init(&dev_array[i].cdev, &fops);
        dev_array[i].cdev.owner = THIS_MODULE;
        cdev_add(&dev_array[i].cdev, devno, 1);
    }

    pr_info("mydevice driver loaded\n");
    return 0;
}

On exit, remove each cdev and release the device number range:

static void __exit dev_exit(void)
{
    dev_t devno = MKDEV(base_major, base_minor);
    int i;

    for (i = 0; i < DEVICE_COUNT; i++)
        cdev_del(&dev_array[i].cdev);

    unregister_chrdev_region(devno, DEVICE_COUNT);
    pr_info("mydevice driver unloaded\n");
}

module_init(dev_init);
module_exit(dev_exit);
MODULE_LICENSE("GPL");

User-space Test Program

The application uses the device node passed as a command-line argument. It writes a string, queries metadata via ioctl, and reads back the data:

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include "mydevice.h"

int main(int argc, char *argv[])
{
    if (argc != 2) {
        fprintf(stderr, "Usage: %s <device_path>\n", argv[0]);
        return EXIT_FAILURE;
    }

    int fd = open(argv[1], O_RDWR);
    if (fd < 0) {
        perror("open");
        return EXIT_FAILURE;
    }

    int bufsz = 0;
    ioctl(fd, MYDEV_IOCTL_GET_BUFSZ, &bufsz);
    printf("buffer size = %d\n", bufsz);

    write(fd, "kernel", 7);

    int cur = 0;
    ioctl(fd, MYDEV_IOCTL_GET_DATALEN, &cur);
    printf("data length = %d\n", cur);

    char buf[8] = {0};
    read(fd, buf, sizeof(buf));
    printf("read data: %s\n", buf);

    close(fd);
    return EXIT_SUCCESS;
}

Create device nodes with appropriate minor numbers, for example:

mknod /dev/mydevice0 c <major> 0
mknod /dev/mydevice1 c <major> 1
mknod /dev/mydevice2 c <major> 2

Each node operates on its own independent buffer, demonstrating how one driver can cleanly manage multiple identical peripherals.

Related Articles

Designing Alertmanager Templates for Prometheus Notifications

How to craft Alertmanager templates to format alert messages, improving clarity and presentation. Alertmanager uses Go’s text/template engine with additional helper functions. Alerting rules referenc...

Deploying a Maven Web Application to Tomcat 9 Using the Tomcat Manager

Tomcat 9 does not provide a dedicated Maven plugin. The Tomcat Manager interface, however, is backward-compatible, so the Tomcat 7 Maven Plugin can be used to deploy to Tomcat 9. This guide shows two...

Skipping Errors in MySQL Asynchronous Replication

When a replica halts because the SQL thread encounters an error, you can resume replication by skipping the problematic event(s). Two common approaches are available. Methods to Skip Errors 1) Skip a...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.