In driver development, we usually create a device structure for each device to store that device's hardware information. This chapter explains the working principle of device structures and how to handle private data in files.
1. What is "Private Data"?
In Linux driver development, the struct file structure represents " an opened file object ". Linux specifically provides a field called void *private_data; in the struct file structure (domain) so that drivers can " record their own exclusive information ". This structure is defined in the linux/fs.h header file.
2. Why Use Private Data?
Each device in the driver may have different hardware information and states. Also, multiple processes or threads may open the same device simultaneously. We usually:
- Define our own structure (such as my_device) for each device.
- In
opendevice, put our structure pointer into file->private_data. - In subsequent operations, directly retrieve the device structure belonging to the current open from file->private_data.
This way, the driver is like object-oriented programming—each operation can distinguish its own "object" information.
- In open function: Assign the address of the device structure to
private_data; - In read/write functions: Get device information through
private_dataand execute specific operations.
This approach makes passing device information between different functions in the driver simple and direct:
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
In the code, we first created a device information structure named dev1. When the device is opened (in the open function), we set the device's private data pointer (private_data) to point to this dev1 structure. This way, in subsequent read operations (read function) and write operations (write function), we can directly access the data in the device's dev1 structure through this private data pointer.
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
3. Usage of Private Data
Let's design an experiment. First, write a driver program that saves data written by the user APP into the file→private_data structure using the driver write interface. Then, when the user APP performs read, it uses the driver read interface to return the data stored in file→private_data to the user.
1. Driver Writing
Create a 07_private_data/ folder, and create a private_data.c driver file in it with the following content:
#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 "mychardev" // Device node name
#define CLASS_NAME "class_mychardev" // Device class name
static dev_t dev_num; // Save device number
static struct cdev mychardev; // Character device structure
static struct class *class_mychardev; // Device class pointer
// private data structure definition
struct my_device {
uint16_t device_id; // Device ID
int buff_len; // Buffer length
char device_buff[32]; // Buffer
// Can extend more members, such as register base address, buffer, etc.
};
// Function called when opening device
// inode: pointer to inode structure of the file
// file: file structure pointer
static int chrdev_open(struct inode *inode, struct file *file)
{
struct my_device *mydev;
// Use kzalloc to apply for memory, allocate a structure for each 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; // Fill in a device ID
// Fill in buffer size
mydev->buff_len = sizeof(mydev->device_buff) - 1;
// Initialize data in device_buff field
memset(mydev->device_buff, 0, sizeof(mydev->device_buff));
// Important step: record structure pointer in file->private_data
file->private_data = mydev;
printk(KERN_INFO "mychardev: open, create private data %p\n", mydev);
return 0; // Return 0 for success
}
// Function called when reading device
// file: file structure pointer
// buf: user space buffer pointer
// 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)
{
// Since private_data structure is void by default, need type cast for actual use
struct my_device *mydev = (struct my_device *)file->private_data;
// No data to read or already read, return 0 (EOF)
if (*off >= mydev->buff_len)
return 0;
// Only return remaining part to avoid out-of-bounds
if (size > mydev->buff_len - *off)
size = mydev->buff_len - *off;
// Copy buffer data to user space
if (copy_to_user(buf, mydev->device_buff + *off, size)) {
printk(KERN_ERR "mychardev: copy_to_user failed\n");
return -EFAULT; // Copy failed, return error code
}
printk(KERN_INFO "mychardev: chrdev_read called\n"); // Print read operation info
*off += size; // Update offset
return size;
}
// Function called when writing device
// file: file structure pointer
// buf: user space buffer pointer
// 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)
{
// Since private_data structure is void by default, need type cast for actual use
struct my_device *mydev = (struct my_device *)file->private_data;
// Limit write length
if(size > sizeof(mydev->device_buff) - 1)
size = sizeof(mydev->device_buff) - 1;
// Copy data from user space to file->private_data buffer
if (copy_from_user(mydev->device_buff, buf, size)) {
printk(KERN_ERR "mychardev: copy_from_user failed\n");
return -EFAULT; // Copy failed, return error code
}
mydev->device_buff[size] = '\0'; // Add string terminator
printk(KERN_INFO "mychardev: Received from user [%s]\n", mydev->device_buff); // Print received data
return size;
}
// Function called when closing device
// inode: pointer to inode structure of the file
// file: file structure pointer
static int chrdev_release(struct inode *inode, struct file *file)
{
// Free private data structure allocated during open
kfree(file->private_data);
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_mychardev = {
.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 is loaded
static int __init mychardev_init(void)
{
int ret;
int major, minor;
// 1. Automatically apply for 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"); // Apply failed
return ret;
}
major = MAJOR(dev_num); // Get major number
minor = MINOR(dev_num); // Get minor 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(&mychardev, &cdev_mychardev); // Initialize cdev
ret = cdev_add(&mychardev, 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 for convenient auto-creation of device node
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. Create device node /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"); // Driver loaded successfully
return 0;
}
// Cleanup function automatically called when module is unloaded
static void __exit mychardev_exit(void)
{
device_destroy(class_mychardev, dev_num); // Delete device node
class_destroy(class_mychardev); // Delete device class
cdev_del(&mychardev); // Unregister cdev
unregister_chrdev_region(dev_num, 1); // Release device number
printk(KERN_INFO "mychardev: chrdev driver unloaded\n"); // Unload info
}
// Specify module's initialization and exit functions
module_init(mychardev_init); // Called when loading module
module_exit(mychardev_exit); // Called 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
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
- Whenever application layer
open("/dev/mychardev"), the driver useskzallocto allocate an independentstruct my_devicestructure for each file instance. - The structure, through
file->private_data, allows all subsequent operations (read/write/ioctl, etc.) to find their own "exclusive" data area. - Only
releasefrees this area, ensuring each file object is independent and non-conflicting.
Complete flow:
- open
kzallocallocatesstruct my_device- Assign to
file->private_data
- write
- Copy data from user space to
device_buff - Limit copy length, no overflow allowed
- Return actual bytes written
- Copy data from user space to
- read
- Copy from
my_device->device_buffstarting at*off - If
*off>=buff_len, no data, return0(EOF) copyto user space, then update*off- Return actual bytes read
- Copy from
- release
kfreecleanup private data
Detailed introduction of off parameter:
1. Background of off parameter
- The
offinread(file, buf, size, *off)andwrite(file, buf, size, *off)in the kernel is called " file offset ". offis essentially the "cursor" of the virtual file, representing where the user currently read to/wrote to.
2. Why maintain off?
- For regular files,
offis the read/write position; for character devices, if you need to implement something like "remaining multiple segment reads", you also need to properly updateoff. - This allows a program to read a few bytes at a time, with each
readcall automatically continuing from unread positions until the file (content) is read.
How to operate off in the driver?
read implementation key points:
Check if
*off >= data length. If yes, content has been read, return0(representingEOF, tools likecatwill stop because of this).Each time
readsuccessfully reads how many bytes,*offshould+=that amount (*off += size;), ensuring multiple reads don't return the same data repeatedly.Use
data start address + *offfor the read range, so next multiple reads can "continue from break point".This way, if cat/read and other tools use a small buffer for multiple reads, the driver can also correctly output content in segments, no infinite loops, no data loss.
2. Makefile Writing
Continue in the 07_private_data/ folder, create a Makefile file and write the following:
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 += private_data.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
It's almost exactly the same as the Makefile we wrote before!
The only difference is it became private_data.o
CROSS_COMPILE: Still the compiler path prefix from the SDK.KDIR: Still the kernel source directory.
3. Driver Compilation
makeThis will directly generate the .ko file.
4. APP Writing
Continue in the 07_private_data/ folder, create a private_data_app.c file and write the following:
#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. Open device
fd = open(DEV_PATH, O_RDWR);
if (fd < 0)
{
perror("open");
return 1;
}
printf("Device opened successfully: %s\n", DEV_PATH);
// 2. Write a piece of content
ret = write(fd, wbuf, strlen(wbuf));
if (ret < 0)
{
perror("write");
close(fd);
return 2;
}
printf("Content written: %s\n", wbuf);
// 3. Read back device content
// According to driver implementation, read will return previously written content
lseek(fd, 0, SEEK_SET); // Usually not needed, but for safety, move to start
ret = read(fd, rbuf, sizeof(rbuf) - 1);
if (ret < 0)
{
perror("read");
close(fd);
return 3;
}
rbuf[ret] = '\0';
printf("Content read back: %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
- Open device file
fd = open("/dev/mychardev", O_RDWR);- Try to open the character device node in read-write mode.
- After opening, the
.openmethod of the kernel driver will be called.
- Write data
write(fd, wbuf, strlen(wbuf));- Write a string to the device.
- The kernel driver's
.writemethod will be called. Usually data is copied into the driver's buffer (such as members in the structure pointed to by file->private_data).
- Read data
read(fd, rbuf, sizeof(rbuf) - 1);- Read content from the device back to the user space buffer.
- The kernel driver's
.readmethod will be called. It usually copies driver data (such as content written in the previous step) to user space.
- Close device
close(fd);- Close the device file. The kernel's
.releasemethod is called to clean up related resources.
5. APP Compilation
Use the following command to 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-gcc private_data_app.c -o private_data_appCommand format:
<SDK's gcc cross compiler> <source.c file> -o <final executable file name>
-omeans rename, followed directly by the name you want to generate.
- SDK's gcc cross compiler: This is the same as the path we wrote in the Makefile before, just changed to
aarch64-none-linux-gnu-gcc, not just the prefix.
The result is like this:
6. Running Test
Copy private_data.ko and private_data_app to the development board (USB drive, TF card or SSH all work).
First, mount the driver:
sudo insmod private_data.koRun APP program:
sudo ./private_data_appFrom the phenomenon, we can see that the Hello file private data show! we wrote is stored in private_data. When the APP reads, it is returned back.