泰山派3M-RK3576-Linux内核驱动教程-Linux驱动基础-设备模型与sysfs-创建sysfs属性文件
2026/5/9 23:43:31 网站建设 项目流程

一、为什么要创建 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);
  • show():用户读属性文件时会被调用
  • store():用户写属性文件时会被调用

2、声明和初始化属性对象

通过宏定义DEVICE_ATTR或更推荐的DEVICE_ATTR_RWDEVICE_ATTR_RODEVICE_ATTR_WO

DEVICE_ATTR_RW(mymode); // 等价于自动生成 mymode_show 和 mymode_store 的属性

老写法(也能用):

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); // 注销(删除)属性

其中dev是设备对象指针(struct device *)。

四、实例代码

这里我们直接复用《实现一个字符设备》章节中的05_char_devicemychardev.c驱动源码,创建一个10_touch_sysfs_file/文件夹,并将mychardev.c复制到到其中。进行一些修改:

1、驱动结构与 sysfs 关联回顾

  • 驱动通过class_createdevice_create创建了 class 设备
  • sysfs下路径:/sys/class/class_test/device_test/
  • 这个目录就是该驱动在内核中的“设备节点
  • 此目录下可定义属于设备的自定义属性文件,实现参数读写与调试

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; }
  • 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);

这样自动生成了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, // 这里自动拼接 };
  • 也就是说:

  • 宏参数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"); // 处理错误略 }

注意:现代写法应直接将属性注册到具体设备节点(推荐如下):

  1. 在文件开头声明一个设备指针
// device_create 返回的 struct device*,保存一份 static struct device *device_test_dev;
  1. 在驱动加载函数执行device_create时接收一下指针
device_test_dev = device_create(class_test, NULL, dev_num, NULL, DEV_NAME); if (IS_ERR(device_test_dev)) { // 错误处理 }
  1. 注册属性到该设备节点
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"); // 模块许可证声明

6、Makefile和编译

直接复用 《实现一个字符设备》 章节中的05_char_deviceMakefile文件即可。

export ARCH=arm64 # 04.创建sysfs属性文件 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) clean
  • 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/myparam

六、小结与注意要点

  • 属性文件可以有多种权限(只读 RO、只写 WO、读写 RW),按需选择宏(DEVICE_ATTR_RW/RO/WO)。
  • show/store 回调在进程上下文运行,不可做过于耗时的操作。
  • 建议确保访问变量的同步性(如多核或中断场合加锁)。
  • 属性文件的命名应简短明了,便于日后调试和维护。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询