一、为什么要创建 sysfs 属性文件?
Linux sysfs 不仅自动导出内核的设备信息,还能让驱动开发者主动添加自定义的属性接口,用于:
- 驱动调试(比如显示状态、调寄存器等)
- 配置参数(用户可在不重启设备/系统情况下修改部分驱动行为)
- 提供与用户空间的简单数据交互
这极大提升了驱动开发和维护的便捷性。
二、sysfs 属性文件的原理
- 每个 struct device(设备对象)都可拥有多个“属性文件/节点”。
- 属性文件本质上就是读写函数的钩子,用户空间通过
cat/echo等命令访问时,驱动代码自动运行对应的回调函数。
三、基本实现方法
1、定义属性的读写回调函数
驱动中常用这两个函数原型:
ssize_t show(struct device *dev, struct device_attribute *attr, char *buf);
ssize_t store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count);2
show():用户读属性文件时会被调用store():用户写属性文件时会被调用
2、声明和初始化属性对象
通过宏定义 DEVICE_ATTR 或更推荐的 DEVICE_ATTR_RW、DEVICE_ATTR_RO、DEVICE_ATTR_WO:
DEVICE_ATTR_RW(mymode);
// 等价于自动生成 mymode_show 和 mymode_store 的属性2
老写法(也能用):
static struct device_attribute dev_attr_mymode = __ATTR(mymode, 0664, mymode_show, mymode_store);3、驱动中注册和注销属性
属性文件需手工添加和移除:
device_create_file(dev, &dev_attr_mymode); // 注册(添加)属性
device_remove_file(dev, &dev_attr_mymode); // 注销(删除)属性2
其中 dev 是设备对象指针(struct device *)。
四、实例代码
这里我们直接复用《实现一个字符设备》章节中的 05_char_device 中 mychardev.c 驱动源码,创建一个 10_touch_sysfs_file/ 文件夹,并将 mychardev.c 复制到到其中。进行一些修改:
1、驱动结构与 sysfs 关联回顾
- 驱动通过
class_create和device_create创建了 class 设备- sysfs 下路径:
/sys/class/class_test/device_test/ - 这个目录就是该驱动在内核中的“设备节点”
- sysfs 下路径:
- 此目录下可定义属于设备的自定义属性文件,实现参数读写与调试
2、定义属性访问回调函数
我们先定义一个整型参数和对应的 show/store 回调函数。
static int myparam = 42; // 用于实验的参数
// 读操作:cat myparam 时调用
static ssize_t myparam_show(struct device *dev, struct device_attribute *attr, char *buf)
{
// 将 myparam 的值格式化为字符串,写入 buf 缓冲区
return sprintf(buf, "%d\n", myparam);
}
// 写操作:echo 88 > myparam 时调用
static ssize_t myparam_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count)
{
int val;
// kstrtoint 是内核帮助函数:用于将字符串(如 "88\n", "123", "007", 等等)解析为整型数值。
if (kstrtoint(buf, 10, &val) == 0) {
// 成功解析,更新 myparam 的值
myparam = val;
}
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
static int myparam = 42;:用于定义一个初始值myparam_show:读取当前值myparam_store:写入新值(带输入检查)
myparam_store 讲解:
kstrtoint是内核帮助函数:用于将字符串(如"88\n","123","007", 等等)解析为整型数值。- 参数说明:
buf:要解析的字符串指针。10:按“十进制”解释字符串(你可以用 16 表示十六进制)。&val:输出解析得到的整数。
- 返回值为 0 表示字符串成功解析成一个整数。
3、声明属性对象
推荐简洁写法(需要包含 <linux/device.h>):
此语句一般直接跟在回调函数之后(
myparam_show/myparam_store)
DEVICE_ATTR_RW(myparam);
// 或者老写法:
// static struct device_attribute dev_attr_myparam = __ATTR(myparam, 0664, myparam_show, myparam_store);2
3
这样自动生成了 dev_attr_myparam 这样一个对象
【特别说明】
不用自己手动声明 dev_attr_myparam,只要用了 DEVICE_ATTR_RW(myparam) 这个宏,编译阶段就自动生成了它!
我们后面调用 dev_attr_myparam 也可以直接引用这个对象,他不会报错!!!
DEVICE_ATTR_RW(myparam)这个宏实际上会自动地把**myparam_show**和**myparam_store**函数关联起来,它的执行机制就是自动拼接和调用。展开之后大致等于:
struct device_attribute dev_attr_myparam = {
.attr = { .name = "myparam", .mode = 0644 },
.show = myparam_show, // 这里自动拼接
.store = myparam_store, // 这里自动拼接
};2
3
4
5
也就是说:
宏参数
myparam会自动拼接_show,找myparam_show作为“读”的函数宏参数
myparam会自动拼接_store,找myparam_store作为“写”的函数所以需要事先声明好这两个函数,保证名字规范
4、注册与注销设备属性
在 chrdev_fops_init()(驱动加载时注册属性),在 chrdev_fops_exit()(驱动卸载时删除属性)。
// 在初始化最后添加:
if (device_create_file(class_test->dev_kobj, &dev_attr_myparam)) {
printk(KERN_ERR "mychardev: device_create_file failed\n");
// 处理错误略
}2
3
4
5
注意:现代写法应直接将属性注册到具体设备节点(推荐如下):
- 在文件开头声明一个设备指针
// device_create 返回的 struct device*,保存一份
static struct device *device_test_dev;2
- 在驱动加载函数执行 device_create 时接收一下指针
device_test_dev = device_create(class_test, NULL, dev_num, NULL, DEV_NAME);
if (IS_ERR(device_test_dev)) {
// 错误处理
}2
3
4
5
- 注册属性到该设备节点
device_create_file(device_test_dev, &dev_attr_myparam);驱动卸载时注销属性:
device_remove_file(device_test_dev, &dev_attr_myparam);特别说明
这里有人会有些疑问,为什么我没有声明 dev_attr_myparam 这个对象,为什么要直接引用?
- 在本章节的 声明属性对象 这一节点,已经讲了只要用了
DEVICE_ATTR_RW(myparam)这个宏,编译阶段就自动生成了它!不需要手动声明。
5、完整代码
完整的代码如下:
#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 "device_test" // 设备节点名称
#define CLASS_NAME "class_test" // 设备类名称
static dev_t dev_num; // 保存设备号
static struct cdev cdev_test; // 字符设备结构体
static struct class *class_test; // 设备类指针
static struct device *device_test_dev; // 设备指针
static int myparam = 42; // 用于实验的参数
// 读操作:cat myparam 时调用
static ssize_t myparam_show(struct device *dev, struct device_attribute *attr, char *buf)
{
// 将 myparam 的值格式化为字符串,写入 buf 缓冲区
return sprintf(buf, "%d\n", myparam);
}
// 写操作:echo 88 > myparam 时调用
static ssize_t myparam_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count)
{
int val;
// kstrtoint 是内核帮助函数:用于将字符串(如 "88\n", "123", "007", 等等)解析为整型数值。
if (kstrtoint(buf, 10, &val) == 0) {
// 成功解析,更新 myparam 的值
myparam = val;
}
// 返回写入的字节数
return count;
}
DEVICE_ATTR_RW(myparam); // 创建可读写的设备属性 myparam
// 打开设备时调用的函数
// inode: 指向文件的 inode 结构体指针
// file: 文件结构体指针
static int chrdev_open(struct inode *inode, struct file *file)
{
printk(KERN_INFO "mychardev: chrdev_open called\n"); // 打印打开信息到内核日志
return 0; // 返回0表示成功
}
// 读设备时调用的函数
// file: 文件结构体指针
// buf: 用户空间的缓冲区指针
// size: 期望读取的字节数
// off: 偏移量指针
static ssize_t chrdev_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
printk(KERN_INFO "mychardev: chrdev_read called\n"); // 打印读操作信息
return 0; // 返回0表示没有数据可读
}
// 写设备时调用的函数
// file: 文件结构体指针
// buf: 用户空间的缓冲区指针
// size: 要写入的字节数
// off: 偏移量指针
static ssize_t chrdev_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
printk(KERN_INFO "mychardev: chrdev_write called\n"); // 打印写操作信息
return size; // 返回写入的字节数,表示写入成功
}
// 关闭设备时调用的函数
// inode: 指向文件的 inode 结构体指针
// file: 文件结构体指针
static int chrdev_release(struct inode *inode, struct file *file)
{
printk(KERN_INFO "mychardev: chrdev_release called\n"); // 打印关闭信息
return 0; // 返回0表示成功
}
// file_operations 结构体,指明本设备支持的操作
static struct file_operations cdev_fops_test = {
.owner = THIS_MODULE, // 拥有者,一般为 THIS_MODULE
.open = chrdev_open, // open 操作
.read = chrdev_read, // read 操作
.write = chrdev_write, // write 操作
.release = chrdev_release, // release 操作
};
// 模块加载时自动调用的初始化函数
static int __init chrdev_fops_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(&cdev_test, &cdev_fops_test); // 初始化 cdev
ret = cdev_add(&cdev_test, 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_test = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(class_test)) {
printk(KERN_ERR "mychardev: class_create failed\n");
cdev_del(&cdev_test);
unregister_chrdev_region(dev_num,1);
return PTR_ERR(class_test);
}
// 4. 创建设备节点 /dev/device_test
device_test_dev = device_create(class_test, NULL, dev_num, NULL, DEV_NAME);
if (IS_ERR(device_test_dev)) {
printk(KERN_ERR "mychardev: device_create failed\n");
class_destroy(class_test);
cdev_del(&cdev_test);
unregister_chrdev_region(dev_num,1);
return -ENOMEM;
}
// 5. 在 sysfs 中创建 myparam 文件
device_create_file(device_test_dev, &dev_attr_myparam);
printk(KERN_INFO "mychardev: chrdev driver loaded successfully\n"); // 驱动加载成功
return 0;
}
// 模块卸载时自动调用的清理函数
static void __exit chrdev_fops_exit(void)
{
device_destroy(class_test, dev_num); // 删除设备节点
class_destroy(class_test); // 删除设备类
cdev_del(&cdev_test); // 注销 cdev
unregister_chrdev_region(dev_num, 1); // 释放设备号
device_remove_file(device_test_dev, &dev_attr_myparam); // 删除 sysfs 文件
printk(KERN_INFO "mychardev: chrdev driver unloaded\n"); // 卸载信息
}
// 指定模块的初始化和退出函数
module_init(chrdev_fops_init); // 加载模块时调用
module_exit(chrdev_fops_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
6、Makefile和编译
直接复用 《实现一个字符设备》 章节中的 05_char_device 中 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 += mychardev.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
CROSS_COMPILE:依旧是SDK中的编译器路径前缀。KDIR:依旧是内核源码目录。
编写好了之后直接运行 make 命令,生成 .ko 文件 :
五、属性文件的实际验证
我们将 mychardev.ko 复制到开发板中挂载,在对应 sysfs 路径下出现自定义属性文件:
我们查看下他的初始值:
cat /sys/class/class_test/device_test/myparam这就是我们写在驱动里面的初始值
42
因为我们在驱动里面使用的是DEVICE_ATTR_RW 这个宏,创建的的可读可写权限。
所以我们修改它的值为 456 试一下,然后再次读一下,看看修改成功了没:
# 写新参数
echo 456 | sudo tee /sys/class/class_test/device_test/myparam
# 再读,应该变成456
cat /sys/class/class_test/device_test/myparam2
3
4
5
六、小结与注意要点
- 属性文件可以有多种权限(只读 RO、只写 WO、读写 RW),按需选择宏(DEVICE_ATTR_RW/RO/WO)。
- show/store 回调在进程上下文运行,不可做过于耗时的操作。
- 建议确保访问变量的同步性(如多核或中断场合加锁)。
- 属性文件的命名应简短明了,便于日后调试和维护。