This chapter explains in detail how to make one kernel driver support multiple device nodes (such as /dev/mychardev0, /dev/mychardev1), where each node can manage its own private state and data. This is an important capability in driver development, strengthening your object-oriented thinking and kernel programming foundation.
1. Basic Scenario and Approach
In actual hardware or virtual device development, there are often multiple channels of the same type of device that need drivers, such as multiple serial ports, multiple LEDs, multiple sensors, etc. Writing a separate driver for each device would be repetitive and hard to maintain. Therefore, one driver managing multiple devices is very common.
Core objectives:
- One driver module, internally maintaining multiple device "objects"
- Each device node (such as
/dev/mychardevX) has its own private data and resources - Driver operations can automatically distinguish and manage which device node
2. Key Points Quick Overview
- Define device structure: Used to store all private information of a device (such as buffer, parameters, cdev).
- Allocate and register a cdev, device number, and device node for each device.
- During open, "identify" which device node is being accessed, then set file->private_data.
- All subsequent operations access the device instance through file->private_data, completely isolating different device states.
Core approach:
- Use a structure array to manage multiple devices, with each structure saving its own buffer and state;
- Initialize and register multiple
cdevs, create device numbers and device nodes for each device node; - Through the
openfunction, identify which device node is being accessed and assign the corresponding structure tofile->private_data; read/writecan then automatically isolate operations on their respective device contents.
3. Typical Code Structure Explained
1. Device Structure Design
struct mydev_info {
struct cdev cdev; // Character device structure, required
int index; // Device number, such as 0, 1, 2......
char buffer[64]; // Device's own buffer
int datalen; // Actual saved data length
};2
3
4
5
6
2. Device Count and Device Number Definition
#define DEV_NUM 2 // Device count, can be multiple
#define DEV_NAME "mychardev" // Device name prefix
static dev_t dev_numbers; // Starting device number
static struct mydev_info mydevices[DEV_NUM]; // Array to save all device instances
static struct class *mydev_class; // Convenient for auto-generating nodes2
3
4
5
6
3. file_operations Implementation
static ssize_t my_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
// Since each device has its own data buffer and length, we need to get the corresponding device info through file->private_data
struct mydev_info *dev = file->private_data;
// Check if read position exceeds data length
if (*ppos >= dev->datalen)
return 0; // Return 0 directly if no more data to read (EOF)
// Adjust read length to ensure it doesn't exceed data length
if (count > dev->datalen - *ppos)
count = dev->datalen - *ppos;
// Copy data from kernel space to user space
if (copy_to_user(buf, dev->buffer + *ppos, count))
return -EFAULT;
// Update read position
*ppos += count;
printk(KERN_INFO "Device read %zu bytes: %s\n", count, dev->buffer);
// Return actual bytes read
return count;
}
static ssize_t my_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
// Get corresponding device information
struct mydev_info *dev = file->private_data;
// Limit write length to not exceed buffer size
if (count > sizeof(dev->buffer) - 1)
count = sizeof(dev->buffer) - 1;
// Copy data from user space to kernel space
if (copy_from_user(dev->buffer, buf, count)) return -EFAULT;
// Add string terminator
dev->buffer[count] = '\0';
// Update data length
dev->datalen = count;
printk(KERN_INFO "Device wrote %zu bytes: %s\n", count, dev->buffer);
// Return actual bytes written
return count;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
4. open Operation
How to distinguish between multiple nodes?
Key: Find the mydev_info belonging to the current node based on the minor number and record it in file->private_data!
static int my_open(struct inode *inode, struct file *filp)
{
// Get corresponding device info structure based on minor number
int minor = MINOR(inode->i_rdev);
// If minor number exceeds range, return error
if (minor >= DEV_NUM) return -ENODEV;
// Store device info structure pointer in file private data for subsequent operations
filp->private_data = &mydevices[minor];
printk(KERN_INFO "Device %s%d opened\n", DEV_NAME, minor);
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
5. Device Registration Process
- First, apply for a contiguous range of device numbers at once (alloc_chrdev_region).
- Initialize cdev for each device and call cdev_add.
- Create device nodes for each device (device_create).
- During unload, release correspondingly.
static int __init mydrv_init(void)
{
int i, ret;
/*
alloc_chrdev_region function is used to dynamically allocate device numbers
Register DEV_NUM consecutive device numbers, starting device number stored in dev_numbers
*/
ret = alloc_chrdev_region(&dev_numbers, 0, DEV_NUM, DEV_NAME);
if (ret) {
printk(KERN_ERR "Failed to allocate char device region\n");
return ret;
}
// Create device class
mydev_class = class_create(THIS_MODULE, DEV_NAME);
// Initialize cdev and create device nodes for each device
for (i = 0; i < DEV_NUM; i++) {
mydevices[i].datalen = 0;
cdev_init(&mydevices[i].cdev, &myfops);
mydevices[i].cdev.owner = THIS_MODULE;
cdev_add(&mydevices[i].cdev, dev_numbers + i, 1);
device_create(mydev_class, NULL, dev_numbers + i, NULL, DEV_NAME"%d", i);
}
printk(KERN_INFO "Multiple devices driver loaded\n");
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
6. Unload Cleanup
static void __exit mydrv_exit(void)
{
int i;
// Delete each device node and unregister cdev
for(i = 0; i < DEV_NUM; i++) {
device_destroy(mydev_class, dev_numbers + i);
cdev_del(&mydevices[i].cdev);
}
// Delete device class and release device numbers
class_destroy(mydev_class);
unregister_chrdev_region(dev_numbers, DEV_NUM);
printk(KERN_INFO "Multiple devices driver unloaded\n");
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
4. Driver Experiment
Next, we will write a complete driver and a corresponding APP.
1. Driver Writing
Create a 09_multiple_devices_driver/ folder, and create a multiple_devices_driver.c file in it with the following content:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#define DEV_NUM 2 // Number of devices
#define DEV_NAME "mychardev" // Device name
struct mydev_info {
struct cdev cdev; // Character device structure
char buffer[64]; // Device data buffer
int datalen; // Buffer data length
};
static dev_t dev_numbers; // Starting device number
static struct mydev_info mydevices[DEV_NUM]; // Device info array
static struct class *mydev_class; // Device class
static ssize_t my_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
// Since each device has its own data buffer and length, we need to get the corresponding device info through file->private_data
struct mydev_info *dev = file->private_data;
// Check if read position exceeds data length
if (*ppos >= dev->datalen)
return 0; // Return 0 if no more data to read (EOF)
// Adjust read length to ensure it doesn't exceed data length
if (count > dev->datalen - *ppos)
count = dev->datalen - *ppos;
// Copy data from kernel space to user space
if (copy_to_user(buf, dev->buffer + *ppos, count))
return -EFAULT;
// Update read position
*ppos += count;
printk(KERN_INFO "Device read %zu bytes: %s\n", count, dev->buffer);
// Return actual bytes read
return count;
}
static ssize_t my_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
// Get corresponding device information
struct mydev_info *dev = file->private_data;
// Limit write length to not exceed buffer size
if (count > sizeof(dev->buffer) - 1)
count = sizeof(dev->buffer) - 1;
// Copy data from user space to kernel space
if (copy_from_user(dev->buffer, buf, count)) return -EFAULT;
// Add string terminator
dev->buffer[count] = '\0';
// Update data length
dev->datalen = count;
printk(KERN_INFO "Device wrote %zu bytes: %s\n", count, dev->buffer);
// Return actual bytes written
return count;
}
static int my_open(struct inode *inode, struct file *filp)
{
// Get corresponding device info structure based on minor number
int minor = MINOR(inode->i_rdev);
// If minor number exceeds range, return error
if (minor >= DEV_NUM) return -ENODEV;
// Store device info structure pointer in file private data for subsequent operations
filp->private_data = &mydevices[minor];
printk(KERN_INFO "Device %s%d opened\n", DEV_NAME, minor);
return 0;
}
// Function called when device is closed
static int my_release(struct inode *inode, struct file *filp)
{
// Get corresponding device info structure based on minor number
int minor = MINOR(inode->i_rdev);
// If minor number exceeds range, return error
if (minor >= DEV_NUM) return -ENODEV;
printk(KERN_INFO "Device %s%d closed\n", DEV_NAME, minor);
return 0;
}
// File operations structure, defining operations supported by the device
static struct file_operations myfops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.read = my_read,
.write = my_write,
};
static int __init mydrv_init(void)
{
int i, ret;
/*
alloc_chrdev_region function is used to dynamically allocate device numbers
Register DEV_NUM consecutive device numbers, starting device number stored in dev_numbers
*/
ret = alloc_chrdev_region(&dev_numbers, 0, DEV_NUM, DEV_NAME);
if (ret) {
printk(KERN_ERR "Failed to allocate char device region\n");
return ret;
}
// Create device class
mydev_class = class_create(THIS_MODULE, DEV_NAME);
// Initialize cdev and create device nodes for each device
for (i = 0; i < DEV_NUM; i++) {
mydevices[i].datalen = 0;
cdev_init(&mydevices[i].cdev, &myfops);
mydevices[i].cdev.owner = THIS_MODULE;
cdev_add(&mydevices[i].cdev, dev_numbers + i, 1);
device_create(mydev_class, NULL, dev_numbers + i, NULL, DEV_NAME"%d", i);
}
printk(KERN_INFO "Multiple devices driver loaded\n");
return 0;
}
static void __exit mydrv_exit(void)
{
int i;
// Delete each device node and unregister cdev
for(i = 0; i < DEV_NUM; i++) {
device_destroy(mydev_class, dev_numbers + i);
cdev_del(&mydevices[i].cdev);
}
// Delete device class and release device numbers
class_destroy(mydev_class);
unregister_chrdev_region(dev_numbers, DEV_NUM);
printk(KERN_INFO "Multiple devices driver unloaded\n");
}
module_init(mydrv_init);
module_exit(mydrv_exit);
MODULE_LICENSE("GPL");2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
1. Module loading (initialization)
ret = alloc_chrdev_region(&dev_numbers, 0, DEV_NUM, DEV_NAME);
mydev_class = class_create(THIS_MODULE, DEV_NAME);
for (i = 0; i < DEV_NUM; i++) {
// Initialize cdev for each device
cdev_init(&mydevices[i].cdev, &myfops);
mydevices[i].cdev.owner = THIS_MODULE;
// Register cdev to corresponding device number
cdev_add(&mydevices[i].cdev, dev_numbers + i, 1);
// Auto create device nodes /dev/mychardev0, /dev/mychardev1
device_create(mydev_class, NULL, dev_numbers + i, NULL, DEV_NAME"%d", i);
}2
3
4
5
6
7
8
9
10
11
- Allocate a contiguous range of device numbers at once (such as major number 100, minor numbers 0/1).
- Initialize and register multiple
cdevs. - Auto create multiple
/dev/mychardev0,/dev/mychardev1nodes.
2. Open device (open)
static int my_open(struct inode *inode, struct file *filp) {
int minor = MINOR(inode->i_rdev); // Get minor number
// Get corresponding device structure through minor number
filp->private_data = &mydevices[minor];
...
}2
3
4
5
6
- Use the minor number to automatically associate the accessed node with the array index, achieving interference-free operations.
3. Read/Write operations
- All
read/writeoperations read their ownfile->private_data, so each device's content is completely isolated. - read: Read by segment according to offset, no infinite loop or data mixing.
- write: Copy data to its own buffer and update data length.
// write: limit length, copy to kernel and add '\0' terminator
if (count > sizeof(dev->buffer) - 1) count = sizeof(dev->buffer) - 1;
// read: return 0 if exceeding actual data
if (*ppos >= dev->datalen) return 0;2
3
4
5
4. Close device (release)
static int my_release(struct inode *inode, struct file *filp) { ... }- Just log it; each node is independent, no memory to release.
5. Unload driver
- Delete all device nodes one by one, unregister cdev, release device numbers and class.
2. Write Makefile
Create a Makefile file in the 09_multiple_devices_driver/ folder with the following content:
export ARCH=arm64
# Cross compiler absolute path prefix
export CROSS_COMPILE=/home/lckfb/TaishanPi-3-Linux/prebuilts/gcc/linux-x86/aarch64/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu-
# Consistent with source file name
obj-m += multiple_devices_driver.o
# Kernel source directory
KDIR := /home/lckfb/TaishanPi-3-Linux/kernel-6.1
PWD ?= $(shell pwd)
all:
make -C $(KDIR) M=$(PWD) modules
clean:
make -C $(KDIR) M=$(PWD) clean2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
It's almost exactly the same as the Makefile we wrote before!
The only difference is it became multiple_devices_driver.o
CROSS_COMPILE: Still the compiler path prefix from the SDK.KDIR: Still the kernel source directory.
3. Compile Driver
In the 09_multiple_devices_driver/ directory, run the following command to generate the .ko file:
make4. Write APP Application
Create a multiple_devices_driver_app.c file in the 09_multiple_devices_driver/ folder with the following content:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#define DEV_PATH_0 "/dev/mychardev0" // First device
#define DEV_PATH_1 "/dev/mychardev1" // Second device
int main(void)
{
// Create file descriptors for two devices
int fd0, fd1;
// Set write content for device 1
char wbuf0[] = "Hello This mychardev0 !!!";
// Set write content for device 2
char wbuf1[] = "Hello This mychardev1 !!!";
// Buffers
char rbuf0[128] = {0};
char rbuf1[128] = {0};
// Return value
ssize_t ret;
// Open devices to get file descriptors
fd0 = open(DEV_PATH_0, O_RDWR);
fd1 = open(DEV_PATH_1, O_RDWR);
if (fd0 < 0 || fd1 < 0) {
perror("open");
if (fd0 >= 0) close(fd0);
if (fd1 >= 0) close(fd1);
return 1;
}
printf("Device opened successfully!\n");
// Write separately and check write results separately
ssize_t wret0 = write(fd0, wbuf0, strlen(wbuf0)); // Write to device 1
if (wret0 < 0) {
perror("write fd0");
close(fd0);
close(fd1);
return 2;
}
ssize_t wret1 = write(fd1, wbuf1, strlen(wbuf1)); // Write to device 2
if (wret1 < 0) {
perror("write fd1");
close(fd0);
close(fd1);
return 2;
}
printf("Content written to first device: %s\n", wbuf0);
printf("Content written to second device: %s\n", wbuf1);
// Read separately
// Usually not needed, but for safety, let's add lseek
lseek(fd0, 0, SEEK_SET);
lseek(fd1, 0, SEEK_SET);
// Read first device
ssize_t ret0 = read(fd0, rbuf0, sizeof(rbuf0) - 1);
if (ret0 < 0) {
perror("read fd0");
close(fd0);
close(fd1);
return 3;
}
rbuf0[ret0] = '\0';
printf("Content read back from first device: %s\n", rbuf0);
// Read second device
ssize_t ret1 = read(fd1, rbuf1, sizeof(rbuf1) - 1);
if (ret1 < 0) {
perror("read fd1");
close(fd0);
close(fd1);
return 3;
}
rbuf1[ret1] = '\0';
printf("Content read back from second device: %s\n", rbuf1);
// Close
close(fd0);
close(fd1);
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
Main flow of APP application:
- Open two device nodes
- Write different content to two devices separately
- Read content separately and print it out
- Close file descriptors and end program
1. Open devices
fd0 = open(DEV_PATH_0, O_RDWR);
fd1 = open(DEV_PATH_1, O_RDWR);
if (fd0 < 0 || fd1 < 0) {
perror("open");
if (fd0 >= 0) close(fd0);
if (fd1 >= 0) close(fd1);
return 1;
}2
3
4
5
6
7
8
9
- Open
/dev/mychardev0and/dev/mychardev1simultaneously. - If one fails to open, release resources and exit.
2. Write operation
ssize_t wret0 = write(fd0, wbuf0, strlen(wbuf0)); // Write to device 1
// ...
ssize_t wret1 = write(fd1, wbuf1, strlen(wbuf1)); // Write to device 22
3
- Each writes its own string to its device.
- Check write results separately, release resources and exit promptly on errors.
3. Read operation
lseek(fd0, 0, SEEK_SET);
lseek(fd1, 0, SEEK_SET);
ssize_t ret0 = read(fd0, rbuf0, sizeof(rbuf0) - 1);
ssize_t ret1 = read(fd1, rbuf1, sizeof(rbuf1) - 1);
// Add \0 at the end to make it a valid string
rbuf0[ret0] = '\0';
rbuf1[ret1] = '\0';2
3
4
5
6
7
8
- To fully read the data just written (and to support the internal "file offset" mechanism of the driver), lseek to the beginning of the file.
- Print content after reading for easy verification of independence and correctness.
4. Close file
close(fd0);
close(fd1);2
- Close all open file descriptors and release resources.
5. Compile APP Application
In the 09_multiple_devices_driver/ directory, run the following command to generate the APP executable file:
/home/lckfb/TaishanPi-3-Linux/prebuilts/gcc/linux-x86/aarch64/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu-gcc multiple_devices_driver_app.c -o multiple_devices_driver_appCommand format:
<SDK's gcc cross compiler> <source.c file> -o <final executable file name>
-omeans rename, followed directly by the name you want to generate.
- SDK's gcc cross compiler: This is the same as the path we wrote in the Makefile before, just changed to
aarch64-none-linux-gnu-gcc, not just the prefix.
The result is like this:
6. Running Test
Copy multiple_devices_driver.ko and multiple_devices_driver_app to the development board. First, mount the driver:
sudo insmod multiple_devices_driver.koRun the following command to run the APP application:
sudo ./multiple_devices_driver_appFrom the output results:
- Each device node can be operated independently: Write and read back don't affect each other, indicating the driver's multi-channel isolation logic is working.
- Data content is correct: What you write is what you read; the two sets of data are independent and don't interfere with each other.