1. Basic Concepts
In the Linux operating system, kernel space (Kernel Space) and user space (User Space) are two completely isolated memory regions.
- User space: Stores code and data of user processes.
- Kernel space: Space where the operating system kernel and its modules (such as drivers) run, with highest privileges.
Ordinary applications cannot directly access data in kernel space, and vice versa. Data exchange must be completed through specific interfaces or methods.
2. Typical Data Exchange Methods
Common data exchange methods are as follows:
- Device file nodes (such as /dev/mydevice) + ioctl, read, write operations
- sysfs file system (such as /sys/class/xxx/yyy)
- proc file system (such as /proc/xxx)
- mmap memory mapping mechanism
- netlink communication mechanism
- Character device, block device driver specific interfaces
The most commonly used is data exchange through character device files combined with read, write, ioctl.
3. API Explanation
The two most important functions are copy_to_user() and copy_from_user()
These two functions are defined in the
kernel-6.1/include/linux/uaccess.hfile
These two functions are used to implement safe data transfer between kernel space and user space.
copy_to_user(to, from, count): Copies data from kernel space to user space.copy_from_user(to, from, count): Copies data from user space to kernel space. Please do not use direct pointer assignment, otherwise the system will crash!
1. copy_to_user
Function prototype:
static __always_inline unsigned long __must_check
copy_to_user(void __user *to, const void *from, unsigned long n)
{
if (check_copy_size(from, n, true))
n = _copy_to_user(to, from, n);
return n;
}2
3
4
5
6
7
- Function definition: Used to copy data in the kernel to a user program's memory area.
- Parameter description:
to: Target address, a location in the application memory.from: Source address, the location of data in kernel memory that needs to be copied.n: Amount of data to copy, in bytes.
Simple summary: This function is like a mover, moving data from the kernel (such as file content or calculation results) to a place the user program can use, specifying how much data to move.
2. copy_from_user
Function prototype:
static __always_inline unsigned long __must_check
copy_from_user(void *to, const void __user *from, unsigned long n)
{
if (check_copy_size(to, n, false))
n = _copy_from_user(to, from, n);
return n;
}2
3
4
5
6
7
- Function definition: Copies data from the user program to the operating system kernel's memory area.
- Parameter description:
to: Address in kernel memory, data will be copied here.from: Address in user program memory, source of data.n: Size of data to copy, in bytes.
Simple summary: This function is like a mover, safely moving data from the user program (such as the software you run) to the memory of the operating system kernel (the core program of the computer).
3. Data Exchange in Character Device Drivers
1. Write Basic Driver
First, let's implement the simplest character device driver. Create a folder using mkdir 06_user_kernel_data/, then enter the directory and create a data_exchange_driver.c driver file 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
// 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)
{
printk(KERN_INFO "mychardev: chrdev_open called\n"); // Print open info to kernel log
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)
{
char data[] = "This is data from kernel space!"; // Set some kernel data
size_t data_len = sizeof(data) -1; // Data length
if (*off >= data_len) {
return 0; // Already read, return 0 for EOF
}
// Only read remaining unread part, adjust read size to prevent out-of-bounds
if (size > data_len - *off) {
size = data_len - *off;
}
// Copy data to user space
if (copy_to_user(buf, data + *off, size)) {
printk(KERN_ERR "mychardev: copy_to_user failed\n");
return -EFAULT; // Copy failed, return error code
}
*off += size; // Update offset
printk(KERN_INFO "mychardev: chrdev_read called\n"); // Print read operation info
// Return actual successfully read bytes
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)
{
char kernel_buf[256]; // Kernel buffer
size_t write_size = size;
if (write_size > sizeof(kernel_buf) - 1) {
write_size = sizeof(kernel_buf) - 1; // Limit write size to prevent overflow
}
// Copy data from user space to kernel buffer
if (copy_from_user(kernel_buf, buf, write_size)) {
printk(KERN_ERR "mychardev: copy_from_user failed\n");
return -EFAULT; // Copy failed, return error code
}
kernel_buf[write_size] = '\0'; // Add string terminator
printk(KERN_INFO "mychardev: Received from user: %s\n", kernel_buf); // Print received data
return write_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)
{
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
- Device number and cdev registration
alloc_chrdev_region()applies for major and minor device numbers.cdev_init(),cdev_add()register the driver with the kernel.class_create(),device_create()auto-generate/dev/mychardev, convenient for users to access directly.
- open/release (open/close)
chrdev_open(): Called when device is opened. Here only prints logs.chrdev_release(): Called when device is closed. Here only does log processing.
- read (read operation)
- Implemented to return a fixed kernel message each time user reads the device.
- Uses
offoffset to prevent duplicate content output. User reads multiple times but content only returned once, then returns 0, indicating "file read complete". - Uses
copy_to_user()function to safely deliver kernel data to user space.
- write (write operation)
- When user writes data to the device, the driver reads user's data to the kernel and prints the received content.
- Uses
copy_from_user()function to safely copy data from user space.
- Module load/unload
mychardev_init(): Runs automatically when module is loaded, completing device registration and node creation.mychardev_exit(): Runs automatically when module is unloaded, cleaning up devices and nodes.
TIP
- Must use
copy_to_user()andcopy_from_user()to operate user data, not directly operate user pointers. readreturns 0 to indicate "end of file", must cooperate with*offoffset handling, otherwise it may cause infinite loops or no output.writereturns actual bytes written, otherwise user space may determine write failed.
How does Linux use read() to read files?
Linuxuser programs useread()to read files or devices multiple times.The driver's
.readis actually called each time:- If returned data is greater than 0, the program thinks there is still data not read, continues reading.
- If return value is 0, the program stops, thinking "file has reached the end".
2. Compile and Load Module
(1) Makefile writing
Create a file and write:
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 += data_exchange_driver.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 data_exchange_driver.o
CROSS_COMPILE: Still the compiler path prefix from the SDK.KDIR: Still the kernel source directory.
(2) Compile
makeThe data_exchange_driver.ko file will appear in the current directory, which is what we need.
(3) Load module
Copy data_exchange_driver.ko to the development board (USB drive, TF card or SSH all work), and run the following command to load the module:
sudo insmod data_exchange_driver.koThe system will print the device major number.
3. Set Read/Write Permissions
sudo chmod 666 /dev/mychardev4. User Space Testing
(1) Write test
echo "hello kernel" > /dev/mychardev(2) Read test
cat /dev/mychardev5. Unload Module
sudo rmmod data_exchange_driver4. APP Call Test
1. Write APP Program
Create a data_exchange_app.c file in the 06_user_kernel_data/ directory and write the following code:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#define DEV_PATH "/dev/mychardev"
int main() {
int fd;
char wbuf[] = "Hello kernel from userspace!";
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 (wrote %zd bytes)\n", wbuf, ret);
// 3. Read back device content
// According to your driver implementation, read will return "This is data from kernel space!"
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 (read %zd bytes)\n", rbuf, ret);
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
2. Compile APP
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 data_exchange_app.c -o data_exchange_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:
3. APP Running Test
Before running, make sure the
data_exchange_driver.komodule is already mounted!! Otherwise it will report an error.
Copy this data_exchange_app to the development board (USB drive, TF card, SSH all work) and run:
sudo ./data_exchange_appThe final effect is like this:
[ 1132.722204] mychardev: chrdev_open called
- When APP calls
open("/dev/mychardev", O_RDWR), the kernel driver'schrdev_openis executed, printing this log. - Indicates the device node can be opened normally and the driver responds normally.
[ 1132.722437] mychardev: Received from user: Hello kernel from userspace!
- When APP calls
write(fd, wbuf, strlen(wbuf)), the kernel driver'schrdev_writeis executed, successfully copying content from user space to kernel and printing user data. - Indicates the data exchange write path is normal!
Device opened successfully: /dev/mychardev
- The APP's
printf, indicating open was successful and user space can access the device node.
[ 1132.722469] mychardev: chrdev_read called
- When APP calls
read(fd, rbuf, ...), the kernel'schrdev_readis called normally. - Indicates the data exchange read path is normal, and the driver can return data to the user process.
Content written: Hello kernel from userspace! (wrote 28 bytes)
- The APP's
printf, indicating the driver returned 28 bytes written. - This number is actually
write_size, the length of the test string. - The write path has no errors.
[ 1132.722512] mychardev: chrdev_release called
- When APP closes the file handle (
close(fd)), the driver'schrdev_releaseis called and the log is printed.
Content read back: This is data from kernel space! (read 31 bytes)
- The APP's
printf, indicating the driver returned 31 bytes of content. This is the sentence passed from the data array in the driver. - Indicates the read path is also normal, successfully implementing data exchange between user space and kernel space.
5. Precautions
- Cannot directly use pointers to pass user space and kernel space data. Use
copy_to_user/copy_from_userand other safe methods. - All data returned to user space needs explicit
copy_to_user**. - Drivers must have permissions. Pay attention to permission control.
- For kernel module debugging, check
dmesgoutput.