1. Why Create sysfs Attribute Files?
Linux sysfs not only automatically exports kernel device information but also allows driver developers to actively add custom attribute interfaces, used for:
- Driver debugging (such as displaying status, accessing registers, etc.)
- Configuring parameters (users can modify some driver behavior without restarting the device/system)
- Providing simple data exchange with user space
This greatly improves the convenience of driver development and maintenance.
2. Principle of sysfs Attribute Files
- Each struct device (device object) can have multiple "attribute files/nodes".
- Attribute files are essentially hooks for read/write functions. When user space accesses them through commands like
cat/echo, the driver code automatically runs the corresponding callback function.
3. Basic Implementation Methods
1. Define Read/Write Callback Functions for Attributes
Common function prototypes used in drivers:
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(): Called when user reads an attribute filestore(): Called when user writes to an attribute file
2. Declare and Initialize Attribute Objects
Through macro definitions DEVICE_ATTR or the more recommended DEVICE_ATTR_RW, DEVICE_ATTR_RO, DEVICE_ATTR_WO:
DEVICE_ATTR_RW(mymode);
// Equivalent to automatically generating mymode_show and mymode_store attributes2
Old method (still works):
static struct device_attribute dev_attr_mymode = __ATTR(mymode, 0664, mymode_show, mymode_store);3. Register and Unregister Attributes in the Driver
Attribute files need to be manually added and removed:
device_create_file(dev, &dev_attr_mymode); // Register (add) attribute
device_remove_file(dev, &dev_attr_mymode); // Unregister (remove) attribute2
Where dev is the device object pointer (struct device *).
4. Example Code
Here we directly reuse the mychardev.c driver source code from the 05_char_device in the "Implementing a Character Device" chapter, create a 10_touch_sysfs_file/ folder, and copy mychardev.c into it. Make some modifications:
1. Driver Structure and sysfs Association Review
- The driver creates a class device through
class_createanddevice_create- Path under sysfs:
/sys/class/class_test/device_test/ - This directory is the "device node" of the driver in the kernel
- Path under sysfs:
- Custom attribute files belonging to the device can be defined under this directory to achieve parameter read/write and debugging
2. Define Attribute Access Callback Functions
First, define an integer parameter and its corresponding show/store callback functions.
static int myparam = 42; // Parameter for experiment
// Read operation: called when cat myparam
static ssize_t myparam_show(struct device *dev, struct device_attribute *attr, char *buf)
{
// Format myparam value as string and write to buf buffer
return sprintf(buf, "%d\n", myparam);
}
// Write operation: called when echo 88 > myparam
static ssize_t myparam_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count)
{
int val;
// kstrtoint is a kernel helper function: used to parse strings (such as "88\n", "123", "007", etc.) into integer values.
if (kstrtoint(buf, 10, &val) == 0) {
// Successfully parsed, update myparam value
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;: Used to define an initial valuemyparam_show: Read current valuemyparam_store: Write new value (with input checking)
myparam_store explanation:
kstrtointis a kernel helper function: used to parse strings (such as"88\n","123","007", etc.) into integer values.- Parameter description:
buf: Pointer to the string to parse.10: Interpret the string as "decimal" (you can use 16 for hexadecimal).&val: Output the parsed integer.
- A return value of 0 indicates the string was successfully parsed into an integer.
3. Declare Attribute Objects
Recommended concise style (requires including <linux/device.h>):
This statement is generally placed directly after the callback functions (
myparam_show/myparam_store)
DEVICE_ATTR_RW(myparam);
// Or the old method:
// static struct device_attribute dev_attr_myparam = __ATTR(myparam, 0664, myparam_show, myparam_store);2
3
This automatically generates an object called dev_attr_myparam
【Special Note】
You don't need to manually declare dev_attr_myparam. As long as you use the macro DEVICE_ATTR_RW(myparam), it is automatically generated at compile time! We can directly reference this object later when calling dev_attr_myparam, and it won't cause errors!!!
DEVICE_ATTR_RW(myparam)actually automatically associatesmyparam_showandmyparam_storefunctions, and its execution mechanism is automatic concatenation and calling.After expansion, it is roughly equal to:
struct device_attribute dev_attr_myparam = {
.attr = { .name = "myparam", .mode = 0644 },
.show = myparam_show, // Automatically concatenated here
.store = myparam_store, // Automatically concatenated here
};2
3
4
5
That is to say:
Macro parameter
myparamautomatically concatenates_showand looks formyparam_showas the "read" functionMacro parameter
myparamautomatically concatenates_storeand looks formyparam_storeas the "write" functionTherefore, you need to declare these two functions in advance and ensure the naming convention
4. Register and Unregister Device Attributes
In chrdev_fops_init() (register attributes when driver loads), in chrdev_fops_exit() (remove attributes when driver unloads).
// Add at the end of initialization:
if (device_create_file(class_test->dev_kobj, &dev_attr_myparam)) {
printk(KERN_ERR "mychardev: device_create_file failed\n");
// Error handling omitted
}2
3
4
5
Note: Modern style should directly register attributes to the specific device node (recommended as follows):
- Declare a device pointer at the beginning of the file
// Return value of device_create, save a copy
static struct device *device_test_dev;2
- Receive the pointer when executing device_create in the driver load function
device_test_dev = device_create(class_test, NULL, dev_num, NULL, DEV_NAME);
if (IS_ERR(device_test_dev)) {
// Error handling
}2
3
4
5
- Register attributes to that device node
device_create_file(device_test_dev, &dev_attr_myparam);Unregister attributes when driver unloads:
device_remove_file(device_test_dev, &dev_attr_myparam);Special Note
Some people may have questions here. Why don't I declare dev_attr_myparam object, why do I reference it directly?
- In the Declare Attribute Objects section of this chapter, it was already explained that as long as you use the macro
DEVICE_ATTR_RW(myparam), it is automatically generated at compile time! No manual declaration needed.
5. Complete Code
The complete code is as follows:
#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>
// Device name and class name macro definitions
#define DEV_NAME "device_test" // Device node name
#define CLASS_NAME "class_test" // Device class name
static dev_t dev_num; // Save device number
static struct cdev cdev_test; // Character device structure
static struct class *class_test; // Device class pointer
static struct device *device_test_dev; // Device pointer
static int myparam = 42; // Parameter for experiment
// Read operation: called when cat myparam
static ssize_t myparam_show(struct device *dev, struct device_attribute *attr, char *buf)
{
// Format myparam value as string and write to buf buffer
return sprintf(buf, "%d\n", myparam);
}
// Write operation: called when echo 88 > myparam
static ssize_t myparam_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count)
{
int val;
// kstrtoint is a kernel helper function: used to parse strings (such as "88\n", "123", "007", etc.) into integer values.
if (kstrtoint(buf, 10, &val) == 0) {
// Successfully parsed, update myparam value
myparam = val;
}
// Return the number of bytes written
return count;
}
DEVICE_ATTR_RW(myparam); // Create read-write device attribute myparam
// Function called when opening the device
// inode: Pointer to file inode structure
// file: File structure pointer
static int chrdev_open(struct inode *inode, struct file *file)
{
printk(KERN_INFO "mychardev: chrdev_open called\n"); // Print open info to kernel log
return 0; // Return 0 for success
}
// Function called when reading the device
// file: File structure pointer
// buf: Pointer to user space buffer
// size: Expected number of bytes to read
// off: Offset pointer
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"); // Print read operation info
return 0; // Return 0 for no data to read
}
// Function called when writing to the device
// file: File structure pointer
// buf: Pointer to user space buffer
// size: Number of bytes to write
// off: Offset pointer
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"); // Print write operation info
return size; // Return the number of bytes written, indicating success
}
// Function called when closing the device
// inode: Pointer to file inode structure
// file: File structure pointer
static int chrdev_release(struct inode *inode, struct file *file)
{
printk(KERN_INFO "mychardev: chrdev_release called\n"); // Print close info
return 0; // Return 0 for success
}
// file_operations structure, specifying operations supported by this device
static struct file_operations cdev_fops_test = {
.owner = THIS_MODULE, // Owner, generally THIS_MODULE
.open = chrdev_open, // open operation
.read = chrdev_read, // read operation
.write = chrdev_write, // write operation
.release = chrdev_release, // release operation
};
// Initialization function automatically called when module loads
static int __init chrdev_fops_init(void)
{
int ret;
int major, minor;
// 1. Automatically request device number, major and minor numbers allocated by kernel
ret = alloc_chrdev_region(&dev_num, 0, 1, DEV_NAME);
if (ret < 0) {
printk(KERN_ERR "mychardev: alloc_chrdev_region failed\n"); // Request failed
return ret;
}
major = MAJOR(dev_num); // Get major device number
minor = MINOR(dev_num); // Get minor device number
printk(KERN_INFO "mychardev: alloc_chrdev_region ok: major=%d, minor=%d\n", major, minor);
// 2. Initialize cdev structure and add to kernel
cdev_init(&cdev_test, &cdev_fops_test); // Initialize cdev
ret = cdev_add(&cdev_test, dev_num, 1); // Register cdev to kernel
if (ret < 0) {
printk(KERN_ERR "mychardev: cdev_add failed\n");
unregister_chrdev_region(dev_num,1); // Release device number on failure
return ret;
}
// 3. Create device class, facilitating automatic device node creation
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. Create device node /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. Create myparam file in sysfs
device_create_file(device_test_dev, &dev_attr_myparam);
printk(KERN_INFO "mychardev: chrdev driver loaded successfully\n"); // Driver loaded successfully
return 0;
}
// Cleanup function automatically called when module unloads
static void __exit chrdev_fops_exit(void)
{
device_destroy(class_test, dev_num); // Remove device node
class_destroy(class_test); // Remove device class
cdev_del(&cdev_test); // Unregister cdev
unregister_chrdev_region(dev_num, 1); // Release device number
device_remove_file(device_test_dev, &dev_attr_myparam); // Remove sysfs file
printk(KERN_INFO "mychardev: chrdev driver unloaded\n"); // Unload info
}
// Specify module initialization and exit functions
module_init(chrdev_fops_init); // Call when loading module
module_exit(chrdev_fops_exit); // Call when unloading module
MODULE_LICENSE("GPL"); // Module license declaration2
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 and Compilation
Directly reuse the Makefile file from 05_char_device in the "Implementing a Character Device" chapter.
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 += mychardev.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
CROSS_COMPILE: Still the compiler path prefix in the SDK.KDIR: Still the kernel source directory.
After writing, directly run the make command to generate the .ko file:
5. Actual Verification of Attribute Files
We copy mychardev.ko to the development board and mount it. The custom attribute file appears under the corresponding sysfs path:
Let's check its initial value:
cat /sys/class/class_test/device_test/myparamThis is the initial value
42we wrote in the driver
Because we used the DEVICE_ATTR_RW macro in the driver, it creates read-write permissions.
So let's try modifying its value to 456, then read it again to see if the modification was successful:
# Write new parameter
echo 456 | sudo tee /sys/class/class_test/device_test/myparam
# Read again, should be 456
cat /sys/class/class_test/device_test/myparam2
3
4
5
6. Summary and Key Points
- Attribute files can have multiple permissions (read-only RO, write-only WO, read-write RW). Choose macros (DEVICE_ATTR_RW/RO/WO) as needed.
- show/store callbacks run in process context and should not perform time-consuming operations.
- Ensure synchronization of accessed variables (such as locking for multi-core or interrupt scenarios).
- Attribute file naming should be short and clear for easy debugging and maintenance in the future.