Managing Multiple Character Devices of the Same Type in a Single Driver
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.