在驱动开发中,通常会为每个设备创建一个设备结构体,用来存放该设备的硬件信息。本章将讲解设备结构体的工作原理以及如何处理文件中的私有数据。
一、什么是“私有数据”?
在 Linux 驱动开发中,struct file 结构体 代表 “ 一个被打开的文件对象 ”。 Linux 为了让驱动能 “ 记录自己专属的信息 ”,专门给 struct file 结构体腾出了一个叫 void *private_data; 的字段(域)。这一结构体定义在linux/fs.h头文件中。
二、为什么要用私有数据?
驱动里每个设备可能都硬件信息不同、状态不同,还可能多个进程或线程同时打开同一个设备。我们通常会:
- 为每个设备,定义自己的结构体(比如 my_device)。
- 在
open设备时,把自己的结构体指针塞进 file->private_data。 - 以后别的操作,直接从 file->private_data 找回属于当前 open 的设备结构。
这样,驱动就像面向对象编程一样,每次操作都能区分好自己手里的“对象”信息。
- open函数中:将设备结构体的地址赋值给
private_data; - read/write函数中:通过
private_data获取设备信息,执行具体操作。
这种方式让设备信息在驱动的不同函数之间传递变得简单直接:
struct device test dev1;
static int cdev_test_open(struct inode *inode,struct file *file){
file->private_data=&dev1;
return O;
};2
3
4
5
在代码中,我们首先创建了一个名为 dev1 的设备信息结构。当设备被打开时(在 open 函数里),我们把设备的私有数据指针( private_data )设置为指向这个 dev1 结构。这样在后续的读操作( read 函数)和写操作( write 函数)中,就可以直接通过这个私有数据指针访问到设备的 dev1 结构里的数据了。
static ssize _t cdev test_write(struct file *file,const char user *buf, size_t size,loff t *off t){
struct device_test *test_dev=(struct device_test *)file->private_data;
return O;2
3
三、私有数据的使用
我们来设计一个实验,首先编写一个驱动程序,将用户APP写进来的数据利用驱动write接口保存在 file→private_data 结构体中,然后用户APP进行read读取的时候,再利用驱动read接口将存储在 file→private_data 中的数据返还给用户。
1、驱动的编写
我们创建一个 07_private_data/ 文件夹,在里面创建一个 private_data.c 驱动文件,并写入以下内容:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/device.h>
// 设备名和类名的宏定义
#define DEV_NAME "mychardev" // 设备节点名称
#define CLASS_NAME "class_mychardev" // 设备类名称
static dev_t dev_num; // 保存设备号
static struct cdev mychardev; // 字符设备结构体
static struct class *class_mychardev; // 设备类指针
// private data 结构体定义
struct my_device {
uint16_t device_id; // 设备ID
int buff_len; // 缓存区长度
char device_buff[32]; // 缓存区
// 可以扩展更多成员,比如寄存器基址、缓冲区等
};
// 打开设备时调用的函数
// inode: 指向文件的 inode 结构体指针
// file: 文件结构体指针
static int chrdev_open(struct inode *inode, struct file *file)
{
struct my_device *mydev;
// 用 kzalloc 申请内存,为每次 open 分配一个结构体
mydev = kzalloc(sizeof(struct my_device), GFP_KERNEL);
if (!mydev) {
printk(KERN_ERR "mychardev: open, kzalloc for private data failed\n");
return -ENOMEM;
}
mydev->device_id = 0xFFA8; // 填写一个设备号ID
// 填写缓存区大小
mydev->buff_len = sizeof(mydev->device_buff) - 1;
// 在 device_buff 字段初始化数据
memset(mydev->device_buff, 0, sizeof(mydev->device_buff));
// 重要步骤:把结构体指针记在 file->private_data 里
file->private_data = mydev;
printk(KERN_INFO "mychardev: open, create private data %p\n", mydev);
return 0; // 返回0表示成功
}
// 读设备时调用的函数
// file: 文件结构体指针
// buf: 用户空间的缓冲区指针
// size: 期望读取的字节数
// off: 偏移量指针
static ssize_t chrdev_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
// 因为 private_data 结构体默认是void类型的,想要实际调用,需要强制转换
struct my_device *mydev = (struct my_device *)file->private_data;
// 没有数据可读或已经读完,则返回0(EOF)
if (*off >= mydev->buff_len)
return 0;
// 只返回剩余部分,避免越界
if (size > mydev->buff_len - *off)
size = mydev->buff_len - *off;
// 将缓存区的数据复制到用户空间
if (copy_to_user(buf, mydev->device_buff + *off, size)) {
printk(KERN_ERR "mychardev: copy_to_user failed\n");
return -EFAULT; // 复制失败,返回错误码
}
printk(KERN_INFO "mychardev: chrdev_read called\n"); // 打印读操作信息
*off += size; // 更新偏移量
return size;
}
// 写设备时调用的函数
// file: 文件结构体指针
// buf: 用户空间的缓冲区指针
// size: 要写入的字节数
// off: 偏移量指针
static ssize_t chrdev_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
// 因为 private_data 结构体默认是void类型的,想要实际调用,需要强制转换
struct my_device *mydev = (struct my_device *)file->private_data;
// 限制写入长度
if(size > sizeof(mydev->device_buff) - 1)
size = sizeof(mydev->device_buff) - 1;
// 复制数据从用户空间到file->private_data的缓冲区
if (copy_from_user(mydev->device_buff, buf, size)) {
printk(KERN_ERR "mychardev: copy_from_user failed\n");
return -EFAULT; // 复制失败,返回错误码
}
mydev->device_buff[size] = '\0'; // 添加字符串结束符
printk(KERN_INFO "mychardev: Received from user [%s]\n", mydev->device_buff); // 打印接收到的数据
return size;
}
// 关闭设备时调用的函数
// inode: 指向文件的 inode 结构体指针
// file: 文件结构体指针
static int chrdev_release(struct inode *inode, struct file *file)
{
// 释放 open 时分配的私有数据结构体
kfree(file->private_data);
printk(KERN_INFO "mychardev: chrdev_release called\n"); // 打印关闭信息
return 0; // 返回0表示成功
}
// file_operations 结构体,指明本设备支持的操作
static struct file_operations cdev_mychardev = {
.owner = THIS_MODULE, // 拥有者,一般为 THIS_MODULE
.open = chrdev_open, // open 操作
.read = chrdev_read, // read 操作
.write = chrdev_write, // write 操作
.release = chrdev_release, // release 操作
};
// 模块加载时自动调用的初始化函数
static int __init mychardev_init(void)
{
int ret;
int major, minor;
// 1. 自动申请设备号,主设备号和次设备号由内核分配
ret = alloc_chrdev_region(&dev_num, 0, 1, DEV_NAME);
if (ret < 0) {
printk(KERN_ERR "mychardev: alloc_chrdev_region failed\n"); // 申请失败
return ret;
}
major = MAJOR(dev_num); // 获取主设备号
minor = MINOR(dev_num); // 获取次设备号
printk(KERN_INFO "mychardev: alloc_chrdev_region ok! [major=%d] [minor=%d]\n", major, minor);
// 2. 初始化 cdev 结构体,并添加到内核
cdev_init(&mychardev, &cdev_mychardev); // 初始化 cdev
ret = cdev_add(&mychardev, dev_num, 1); // 注册 cdev 到内核
if (ret < 0) {
printk(KERN_ERR "mychardev: cdev_add failed\n");
unregister_chrdev_region(dev_num,1); // 失败时释放设备号
return ret;
}
// 3. 创建设备类,便于自动创建设备节点
class_mychardev = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(class_mychardev)) {
printk(KERN_ERR "mychardev: class_create failed\n");
cdev_del(&mychardev);
unregister_chrdev_region(dev_num,1);
return PTR_ERR(class_mychardev);
}
// 4. 创建设备节点 /dev/device_test
if (device_create(class_mychardev, NULL, dev_num, NULL, DEV_NAME) == NULL) {
printk(KERN_ERR "mychardev: device_create failed\n");
class_destroy(class_mychardev);
cdev_del(&mychardev);
unregister_chrdev_region(dev_num,1);
return -ENOMEM;
}
printk(KERN_INFO "mychardev: chrdev driver loaded successfully\n"); // 驱动加载成功
return 0;
}
// 模块卸载时自动调用的清理函数
static void __exit mychardev_exit(void)
{
device_destroy(class_mychardev, dev_num); // 删除设备节点
class_destroy(class_mychardev); // 删除设备类
cdev_del(&mychardev); // 注销 cdev
unregister_chrdev_region(dev_num, 1); // 释放设备号
printk(KERN_INFO "mychardev: chrdev driver unloaded\n"); // 卸载信息
}
// 指定模块的初始化和退出函数
module_init(mychardev_init); // 加载模块时调用
module_exit(mychardev_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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
- 每当应用层
open("/dev/mychardev"),驱动就用kzalloc为每个文件实例分配独立的struct my_device结构体。 - 结构体通过
file->private_data,让后续所有操作(read/write/ioctl等)都能找到自己"专属"的数据区。 - 只有
release时才释放这块区域,保证每个文件对象独立,不冲突。
完整流程:
- open
kzalloc分配struct my_device- 赋值到
file->private_data
- write
- 从用户空间拷贝数据到
device_buff - 限制拷贝长度,不允许溢出
- 返回实际写入长度
- 从用户空间拷贝数据到
- read
- 从
my_device->device_buff的*off起始位置拷贝 - 如果
*off>=buff_len说明没数据了,返回0(EOF) copy到用户空间后,更新*off- 返回实际读取的数据长度
- 从
- release
kfree清理私有数据
off参数详细介绍:
1. off参数的背景
- 内核中的
read(file, buf, size, *off)和write(file, buf, size, *off)中的off称为" 文件偏移量 "。 off本质上就是虚拟文件的"游标",表示当前用户读到/写到哪里了。
2. 为什么要维护off?
- 对于普通文件而言,
off就是读写的位置;对于字符设备,如果你要实现类似“剩余多次分段读”功能的时候,也要合理更新off。 - 这让一个程序可以一次读几字节,每次调用
read都能自动从没读过的位置继续,直到文件(内容)读完。
在驱动里off怎么操作?
read实现重点:
判断
*off >= 数据长度,如果是,说明内容已经读完,返回0(代表EOF,cat等工具会因此停止)。每次实际成功
read多少字节,*off要+=多少(*off += size;),确保多次读不会重复返回同样的数据。读取区间使用
数据首地址 + *off,这样下次多次读能"断点续读"。这样,cat/read等工具,如果用小buffer多次read,驱动也能正确分段输出内容,不会死循环,也不会丢失数据。
2、Makefile编写
继续在 07_private_data/ 文件夹,在里面创建一个 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 += private_data.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 几乎一摸一样!
这不过这里变为了 private_data.o
CROSS_COMPILE:依旧是SDK中的编译器路径前缀。KDIR:依旧是内核源码目录。
3、驱动编译
make这样就会直接生成 .ko 文件。
4、APP的编写
继续在 07_private_data/ 文件夹,在里面创建一个 private_data_app.c 文件,并写入以下内容:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#define DEV_PATH "/dev/mychardev"
int main(void)
{
int fd;
char wbuf[] = "Hello file private data show!";
char rbuf[128] = {0};
ssize_t ret;
// 1. 打开设备
fd = open(DEV_PATH, O_RDWR);
if (fd < 0)
{
perror("open");
return 1;
}
printf("设备打开成功: %s\n", DEV_PATH);
// 2. 写入一段内容
ret = write(fd, wbuf, strlen(wbuf));
if (ret < 0)
{
perror("write");
close(fd);
return 2;
}
printf("写入内容: %s\n", wbuf);
// 3. 读回设备内容
// 根据驱动实现,read会返回之前写入的内容
lseek(fd, 0, SEEK_SET); // 通常不需要,但为保险起见,移到开始
ret = read(fd, rbuf, sizeof(rbuf) - 1);
if (ret < 0)
{
perror("read");
close(fd);
return 3;
}
rbuf[ret] = '\0';
printf("读回内容: %s\n", rbuf);
close(fd);
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
- 打开设备文件
fd = open("/dev/mychardev", O_RDWR);- 尝试以可读可写方式打开字符设备节点。
- 打开后,内核驱动的
.open方法会被调用。
- 写入数据
write(fd, wbuf, strlen(wbuf));- 向设备写入一段字符串。
- 内核驱动的
.write方法会被调用,通常数据被拷贝进驱动的缓冲区(如 file->private_data 所指结构体里的成员)。
- 读取数据
read(fd, rbuf, sizeof(rbuf) - 1);- 从设备读取内容回到用户空间缓冲区。
- 内核驱动的
.read方法会被调用,它通常把驱动中的数据(例如上一步写入的内容)拷贝到用户空间。
- 关闭设备
close(fd);- 关闭设备文件,内核的
.release方法被调用,清理相关资源。
5、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 private_data_app.c -o private_data_app命令格式是:
<SDK的gcc交叉编译器> <源码.c文件> -o <最终生成的可执行文件名字>
-o重名的意思,后面紧跟着最终想要生成的名字。
- SDK的gcc交叉编译器:这个就和之前我们在Makefile中编写的路径一致只不过变为了
aarch64-none-linux-gnu-gcc,不单单是只有前缀了。
最终就是这样的:
6、运行测试
将 private_data.ko 和 private_data_app 复制到开发板上面(U盘、TF卡或者SSH都可以)。
首先挂载驱动:
sudo insmod private_data.ko运行APP程序:
sudo ./private_data_app从现象中我们可以看到,我们写入的 Hello file private data show! 被存储在了 private_data 中,在APP读取的时候又被返还了回来。