本章详细讲解如何让一个内核驱动支持多个设备节点(比如 /dev/mychardev0、/dev/mychardev1),每个节点可以管理自己的私有状态和数据。这是驱动开发的一个重要能力,强化你的面向对象思想和内核编程基础。
一、基本场景与思路
在实际硬件或虚拟设备开发中,往往有多路相同类型的设备需要驱动,比如多个串口、多个LED、多个传感器等。如果为每个设备写一个驱动,重复且难维护。因此,一个驱动管理多个设备非常常见。
核心目标:
- 一个驱动模块,内部维护多个设备“对象”
- 每个设备节点(如
/dev/mychardevX)有自己的私有数据和资源 - 驱动操作能自动区分和管理哪个设备节点
二、关键点快速概述
- 定义设备结构体:用于保存一个设备的全部私有信息(如缓冲区、参数、cdev)。
- 为每个设备分配并注册一个cdev、设备号和设备节点。
- open时“识别”当前是哪个设备节点,然后设定好file->private_data。
- 后续所有操作都通过file->private_data访问该设备实例,彻底隔离不同设备状态。
核心思路:
- 用结构体数组管理多个设备,每个结构体各自保存缓冲区和状态;
- 初始化并注册多个
cdev,分别为每个设备节点创建设备号和设备节点; - 通过
open函数识别访问的是哪个设备节点,并把对应结构体赋给file->private_data; read/write就能自动隔离操作各自的设备内容。
三、典型代码结构详解
1. 设备结构体设计
struct mydev_info {
struct cdev cdev; // 字符设备结构体,必须
int index; // 设备编号,如0、1、2……
char buffer[64]; // 设备自己的缓冲区
int datalen; // 实际保存的数据长度
};2
3
4
5
6
2. 设备数量与设备号定义
#define DEV_NUM 2 // 设备数量,可以多个
#define DEV_NAME "mychardev" // 设备名前缀
static dev_t dev_numbers; // 首设备号
static struct mydev_info mydevices[DEV_NUM]; // 数组保存全部设备实例
static struct class *mydev_class; // 方便自动生成节点2
3
4
5
6
3. file_operations实现
static ssize_t my_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
// 因为每个设备都有自己的数据缓冲区和长度,所以需要通过 file->private_data 获取对应设备的信息
struct mydev_info *dev = file->private_data;
// 检查读取位置是否超过数据长度
if (*ppos >= dev->datalen)
return 0; // 没有更多数据可读则直接返回0(EOF)
// 调整读取长度,确保不超过数据长度
if (count > dev->datalen - *ppos)
count = dev->datalen - *ppos;
// 将数据从内核空间复制到用户空间
if (copy_to_user(buf, dev->buffer + *ppos, count))
return -EFAULT;
// 更新读取位置
*ppos += count;
printk(KERN_INFO "Device read %zu bytes: %s\n", count, dev->buffer);
// 返回实际读取的字节数
return count;
}
static ssize_t my_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
// 获取对应设备的信息
struct mydev_info *dev = file->private_data;
// 限制写入长度,确保不超过缓冲区大小
if (count > sizeof(dev->buffer) - 1)
count = sizeof(dev->buffer) - 1;
// 将数据从用户空间复制到内核空间
if (copy_from_user(dev->buffer, buf, count)) return -EFAULT;
// 添加字符串结束符
dev->buffer[count] = '\0';
// 更新数据长度
dev->datalen = count;
printk(KERN_INFO "Device wrote %zu bytes: %s\n", count, dev->buffer);
// 返回实际写入的字节数
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操作?
如何区分开多个节点?
关键:根据次设备号找到属于当前节点的 mydev_info 并记录到 file->private_data!
static int my_open(struct inode *inode, struct file *filp)
{
// 根据次设备号获取对应的设备信息结构体
int minor = MINOR(inode->i_rdev);
// 如果次设备号超出范围,则返回错误
if (minor >= DEV_NUM) return -ENODEV;
// 将设备信息结构体指针存储在文件私有数据中,供后续操作使用
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. 设备注册流程
- 先一次性申请一段连续设备号(alloc_chrdev_region)。
- 为每个设备初始化cdev并调用cdev_add。
- 为每个设备创建设备节点(device_create)。
- 卸载时对应释放即可。
static int __init mydrv_init(void)
{
int i, ret;
/*
alloc_chrdev_region函数用于动态分配设备号
注册DEV_NUM个连续的设备号,起始设备号存储在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;
}
// 创建设备类
mydev_class = class_create(THIS_MODULE, DEV_NAME);
// 为每个设备初始化cdev并创建设备节点
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. 卸载卸载时清理
static void __exit mydrv_exit(void)
{
int i;
// 删除每个设备节点并注销cdev
for(i = 0; i < DEV_NUM; i++) {
device_destroy(mydev_class, dev_numbers + i);
cdev_del(&mydevices[i].cdev);
}
// 删除设备类并释放设备号
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
四、驱动实验
接下来我们完整的编写一个驱动,以及配套的APP。
1、驱动编写
创建一个 09_multiple_devices_driver/ 文件夹,并在里面创建一个 multiple_devices_driver.c 文件,编写以下内容:
#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 // 设备的数量
#define DEV_NAME "mychardev" // 设备名称
struct mydev_info {
struct cdev cdev; // 字符设备结构体
char buffer[64]; // 设备数据缓冲区
int datalen; // 缓冲区数据长度
};
static dev_t dev_numbers; // 设备号起始值
static struct mydev_info mydevices[DEV_NUM]; // 设备信息数组
static struct class *mydev_class; // 设备类
static ssize_t my_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
// 因为每个设备都有自己的数据缓冲区和长度,所以需要通过 file->private_data 获取对应设备的信息
struct mydev_info *dev = file->private_data;
// 检查读取位置是否超过数据长度
if (*ppos >= dev->datalen)
return 0; // 没有更多数据可读则直接返回0(EOF)
// 调整读取长度,确保不超过数据长度
if (count > dev->datalen - *ppos)
count = dev->datalen - *ppos;
// 将数据从内核空间复制到用户空间
if (copy_to_user(buf, dev->buffer + *ppos, count))
return -EFAULT;
// 更新读取位置
*ppos += count;
printk(KERN_INFO "Device read %zu bytes: %s\n", count, dev->buffer);
// 返回实际读取的字节数
return count;
}
static ssize_t my_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
// 获取对应设备的信息
struct mydev_info *dev = file->private_data;
// 限制写入长度,确保不超过缓冲区大小
if (count > sizeof(dev->buffer) - 1)
count = sizeof(dev->buffer) - 1;
// 将数据从用户空间复制到内核空间
if (copy_from_user(dev->buffer, buf, count)) return -EFAULT;
// 添加字符串结束符
dev->buffer[count] = '\0';
// 更新数据长度
dev->datalen = count;
printk(KERN_INFO "Device wrote %zu bytes: %s\n", count, dev->buffer);
// 返回实际写入的字节数
return count;
}
static int my_open(struct inode *inode, struct file *filp)
{
// 根据次设备号获取对应的设备信息结构体
int minor = MINOR(inode->i_rdev);
// 如果次设备号超出范围,则返回错误
if (minor >= DEV_NUM) return -ENODEV;
// 将设备信息结构体指针存储在文件私有数据中,供后续操作使用
filp->private_data = &mydevices[minor];
printk(KERN_INFO "Device %s%d opened\n", DEV_NAME, minor);
return 0;
}
// 设备被close时调用的函数
static int my_release(struct inode *inode, struct file *filp)
{
// 根据次设备号获取对应的设备信息结构体
int minor = MINOR(inode->i_rdev);
// 如果次设备号超出范围,则返回错误
if (minor >= DEV_NUM) return -ENODEV;
printk(KERN_INFO "Device %s%d closed\n", DEV_NAME, minor);
return 0;
}
// 文件操作结构体,定义设备支持的操作
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函数用于动态分配设备号
注册DEV_NUM个连续的设备号,起始设备号存储在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;
}
// 创建设备类
mydev_class = class_create(THIS_MODULE, DEV_NAME);
// 为每个设备初始化cdev并创建设备节点
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;
// 删除每个设备节点并注销cdev
for(i = 0; i < DEV_NUM; i++) {
device_destroy(mydev_class, dev_numbers + i);
cdev_del(&mydevices[i].cdev);
}
// 删除设备类并释放设备号
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. 模块加载(初始化)
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++) {
// 初始化每个设备的cdev
cdev_init(&mydevices[i].cdev, &myfops);
mydevices[i].cdev.owner = THIS_MODULE;
// 把cdev注册到对应设备号
cdev_add(&mydevices[i].cdev, dev_numbers + i, 1);
// 自动创建设备节点 /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
- 一次性分配一段连续设备号(比如主设备号100, 次设备号0/1)。
- 初始化和注册多个
cdev。 - 自动创建多个
/dev/mychardev0、/dev/mychardev1节点。
2. 打开设备(open)
static int my_open(struct inode *inode, struct file *filp) {
int minor = MINOR(inode->i_rdev); // 取得次设备号
// 通过次设备号获得对应的设备结构体
filp->private_data = &mydevices[minor];
...
}2
3
4
5
6
- 利用次设备号自动把访问的节点和数组索引关联,实现操作互不干扰。
3. 读写操作
- 所有
read/write操作都读取自己的file->private_data,这样每个设备的内容完全隔离。 - read:按照 off 分段读,不会死循环或串数据。
- write:拷贝数据到自己的 buffer,并更新数据长度。
// write: 限制长度,拷贝入内核并加'\0'结尾
if (count > sizeof(dev->buffer) - 1) count = sizeof(dev->buffer) - 1;
// read: 超出实际数据则返回0
if (*ppos >= dev->datalen) return 0;2
3
4
5
4. 关闭设备(release)
static int my_release(struct inode *inode, struct file *filp) { ... }- 日志记录即可,每个节点独立,没有内存要释放。
5. 卸载驱动
- 逐一删除所有设备节点,注销 cdev,释放设备号和 class。
2、编写Makefile
在 09_multiple_devices_driver/ 文件夹里面创建一个 Makefile 文件,编写以下内容:
export ARCH=arm64
# 交叉编译器绝对路径前缀
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-
# 和源文件名一致
obj-m += multiple_devices_driver.o
# 内核源码目录
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
和之前编写的 Makefile 几乎一摸一样!
这不过这里变为了 multiple_devices_driver.o
CROSS_COMPILE:依旧是SDK中的编译器路径前缀。KDIR:依旧是内核源码目录。
3、编译驱动
在 09_multiple_devices_driver/ 目录,运行以下命令,生成 .ko 文件:
make4、编写APP应用
在 09_multiple_devices_driver/ 文件夹里面创建一个 multiple_devices_driver_app.c 文件,编写以下内容:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#define DEV_PATH_0 "/dev/mychardev0" // 第一个设备
#define DEV_PATH_1 "/dev/mychardev1" // 第二个设备
int main(void)
{
// 创建两个设备的文件描述符
int fd0, fd1;
// 设定设备1的写入内容
char wbuf0[] = "Hello This mychardev0 !!!";
// 设定设备2的写入内容
char wbuf1[] = "Hello This mychardev1 !!!";
// 缓冲区
char rbuf0[128] = {0};
char rbuf1[128] = {0};
// 返回值
ssize_t ret;
// 打开设备获取文件描述符
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("设备打开成功!!!\n");
// 分别写入,并分别检查写入结果
ssize_t wret0 = write(fd0, wbuf0, strlen(wbuf0)); // 写入设备1
if (wret0 < 0) {
perror("write fd0");
close(fd0);
close(fd1);
return 2;
}
ssize_t wret1 = write(fd1, wbuf1, strlen(wbuf1)); // 写入设备2
if (wret1 < 0) {
perror("write fd1");
close(fd0);
close(fd1);
return 2;
}
printf("向第一个设备写入的内容: %s\n", wbuf0);
printf("向第二个设备写入的内容: %s\n", wbuf1);
// 分别读取
// 原本不需要lseek,但为了保险起见,还是加上
lseek(fd0, 0, SEEK_SET);
lseek(fd1, 0, SEEK_SET);
// 读取第一个设备
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("读回第一个设备的内容: %s\n", rbuf0);
// 读取第二个设备
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("读回第二个设备的内容: %s\n", rbuf1);
// 关闭
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
APP应用程序的主要流程:
- 打开两个设备节点
- 分别写入不同内容到两个设备
- 分别读取内容,并打印出来
- 关闭文件描述符,结束程序
1. 打开设备
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
- 同时打开
/dev/mychardev0和/dev/mychardev1。 - 若有一个打开失败,资源回收后退出。
2. 写入操作
ssize_t wret0 = write(fd0, wbuf0, strlen(wbuf0)); // 写入设备1
// ...
ssize_t wret1 = write(fd1, wbuf1, strlen(wbuf1)); // 写入设备22
3
- 各自向两台设备写入各自字符串。
- 分别检测写入结果,遇到错误及时释放资源退出。
3. 读取操作
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);
// 末尾补\0,使其为合法字符串
rbuf0[ret0] = '\0';
rbuf1[ret1] = '\0';2
3
4
5
6
7
8
- 为了完整读取刚写入的数据(以及支持驱动内部有“文件偏移”机制),进行lseek到文件开头。
- 读取后打印内容出来,方便验证独立性和内容正确性。
4. 关闭文件
close(fd0);
close(fd1);2
- 关闭所有打开的文件描述符,释放资源。
5、编译APP应用
在 09_multiple_devices_driver/ 目录,运行以下命令,生成APP的可执行文件:
/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_app命令格式是:
<SDK的gcc交叉编译器> <源码.c文件> -o <最终生成的可执行文件名字>
-o重名的意思,后面紧跟着最终想要生成的名字。
- SDK的gcc交叉编译器:这个就和之前我们在Makefile中编写的路径一致只不过变为了
aarch64-none-linux-gnu-gcc,不单单是只有前缀了。
最终就是这样的:
6、运行测试
将 multiple_devices_driver.ko 和 multiple_devices_driver_app 复制到开发板上面,首先挂载驱动:
sudo insmod multiple_devices_driver.ko运行下面的命令,运行APP应用程序:
sudo ./multiple_devices_driver_app从输出的结果来看:
- 每个设备节点能单独操作:写入与读回不会互相影响,说明驱动多路隔离逻辑生效。
- 数据内容正确:写进去什么读出来什么,两组数据独立互不干扰。