一、基础概念
在 Linux 操作系统中,内核空间(Kernel Space)和用户空间(User Space)是完全隔离的两个内存区域。
- 用户空间:存放用户进程运行的代码和数据。
- 内核空间:操作系统内核及其模块(如驱动程序)运行的空间,有最高权限。
普通应用程序不能直接访问内核空间的数据,反之亦然。数据交换必须通过特定的接口或方法来完成。
二、典型的数据交换方式
常见的数据交换方式如下:
- 设备文件节点(如 /dev/mydevice)+ ioctl、read、write操作
- sysfs 文件系统(如 /sys/class/xxx/yyy)
- proc 文件系统(如 /proc/xxx)
- mmap 内存映射机制
- netlink 通信机制
- 字符设备、块设备驱动的特定接口
最常用的是通过字符设备文件结合 read、 write 、 ioctl 进行数据交换。
三、API讲解
最主要的两个函数copy_to_user() 与 copy_from_user()
这俩个函数定义在了
kernel-6.1/include/linux/uaccess.h文件下
这两个函数用于实现在内核空间与用户空间之间的安全数据传递。
copy_to_user(to, from, count):将数据从内核空间复制到用户空间。copy_from_user(to, from, count):将数据从用户空间复制到内核空间。 请务必不要直接用指针赋值,否则系统会崩溃!
1、copy_to_user
函数原型:
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;
}
- 函数定义:用于将内核中的数据复制到用户程序的内存区域。
- 参数说明:
to:目标地址,是应用程序内存中的一个位置。from:源地址,是内核内存中需要拷贝的数据位置。n:要拷贝的数据量,单位是字节。
简单总结:这个函数就像一个搬运工,把内核里的数据(比如文件内容或计算结果)搬去用户程序能用的地方,并指定搬多少数据。
2、copy_from_user
函数原型:
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;
}
- 函数定义:将用户程序中的数据复制到操作系统内核的内存区域。
- 参数说明:
to: 内核内存的地址,数据会被复制到这里。from: 用户程序内存的地址,数据来源。n: 需要复制的数据大小(单位为字节)。
简单总结:这个函数的作用就像一个搬运工,把用户程序(比如你运行的软件)里的数据,安全地搬移到操作系统内核(计算机的核心程序)的内存里。
三、字符设备驱动的数据交换
1. 编写基本驱动
首先,我们实现一个最简单的字符设备驱动,使用 mkdir 06_user_kernel_data/ 创建一个文件夹,然后进入目录,创建一个 data_exchange_driver.c 驱动文件,并编写以下内容:
#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 "mychardev" // 设备节点名称
#define CLASS_NAME "class_mychardev" // 设备类名称
static dev_t dev_num; // 保存设备号
static struct cdev mychardev; // 字符设备结构体
static struct class *class_mychardev; // 设备类指针
// 打开设备时调用的函数
// 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)
{
char data[] = "This is data from kernel space!"; // 设定一段内核数据
size_t data_len = sizeof(data) -1; // 数据长度
if (*off >= data_len) {
return 0; // 已读完,返回0表示EOF
}
// 只读剩余的未读部分,调整读取大小,防止越界
if (size > data_len - *off) {
size = data_len - *off;
}
// 复制数据到用户空间
if (copy_to_user(buf, data + *off, size)) {
printk(KERN_ERR "mychardev: copy_to_user failed\n");
return -EFAULT; // 复制失败,返回错误码
}
*off += size; // 更新偏移量
printk(KERN_INFO "mychardev: chrdev_read called\n"); // 打印读操作信息
// 返回实际读取成功的字节数
return size;
}
// 写设备时调用的函数
// file: 文件结构体指针
// buf: 用户空间的缓冲区指针
// size: 要写入的字节数
// off: 偏移量指针
static ssize_t chrdev_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
char kernel_buf[256]; // 内核缓冲区
size_t write_size = size;
if (write_size > sizeof(kernel_buf) - 1) {
write_size = sizeof(kernel_buf) - 1; // 限制写入大小,防止溢出
}
// 复制数据从用户空间到内核缓冲区
if (copy_from_user(kernel_buf, buf, write_size)) {
printk(KERN_ERR "mychardev: copy_from_user failed\n");
return -EFAULT; // 复制失败,返回错误码
}
kernel_buf[write_size] = '\0'; // 添加字符串结束符
printk(KERN_INFO "mychardev: Received from user: %s\n", kernel_buf); // 打印接收到的数据
return write_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_mychardev = {
.owner = THIS_MODULE, // 拥有者,一般为 THIS_MODULE
.open = chrdev_open, // open 操作
.read = chrdev_read, // read 操作
.write = chrdev_write, // write 操作
.release = chrdev_release, // release 操作
};
// 模块加载时自动调用的初始化函数
static int __init mychardev_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(&mychardev, &cdev_mychardev); // 初始化 cdev
ret = cdev_add(&mychardev, 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_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. 创建设备节点 /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"); // 驱动加载成功
return 0;
}
// 模块卸载时自动调用的清理函数
static void __exit mychardev_exit(void)
{
device_destroy(class_mychardev, dev_num); // 删除设备节点
class_destroy(class_mychardev); // 删除设备类
cdev_del(&mychardev); // 注销 cdev
unregister_chrdev_region(dev_num, 1); // 释放设备号
printk(KERN_INFO "mychardev: chrdev driver unloaded\n"); // 卸载信息
}
// 指定模块的初始化和退出函数
module_init(mychardev_init); // 加载模块时调用
module_exit(mychardev_exit); // 卸载模块时调用
MODULE_LICENSE("GPL"); // 模块许可证声明
- 设备号与cdev注册
-alloc_chrdev_region()申请主设备号和次设备号。
-cdev_init()、cdev_add()注册驱动到内核。
-class_create()、device_create()自动生成/dev/mychardev,方便用户直接访问。 - open/release(打开/关闭)
-chrdev_open():设备被打开时调用。这里只打印日志。
-chrdev_release():设备被关闭时调用。这里只做日志处理。 - read(读操作)
- 实现为:每次用户读设备时返回一句固定的内核消息。
- 使用了off偏移量防止内容重复输出,用户多次读取内容只返回一次,之后再读返回0,表示“文件读取完毕”。
- 用copy_to_user()函数安全地把内核数据送到用户空间。 - write(写操作)
- 用户写数据到设备时,驱动读取用户的数据到内核,并打印收到的内容。
- 用copy_from_user()函数安全地从用户空间复制数据。 - 模块加载/卸载
-mychardev_init():模块加载时自动运行,完成设备注册与节点创建。
-mychardev_exit():模块卸载时自动运行,清理设备和节点。
TIP
- 必须用
copy_to_user()和copy_from_user()操作用户数据,不能直接操作用户指针。 read返回0表示“文件结尾”,必须配合处理*off偏移,否则会造成死循环或无输出。write返回实际写入字节数,否则用户空间可能判定写失败。
Linux 怎么用 read() 读文件?
Linux用户程序用read()多次去读文件或者设备。- 驱动的
.read每次实际被调用:
- 如果返回的数据大于0,程序就以为还有数据没读完,会继续读。
- 如果返回值是0,程序才会停下来,认为“文件到结尾了”。
2. 编译与加载模块
(1)Makefile文件编写
创建一个文件并编写:
export ARCH=arm64
# 07.用户与内核数据交换
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 += data_exchange_driver.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
和之前编写的 Makefile 几乎一摸一样!
这不过这里变为了 data_exchange_driver.o
CROSS_COMPILE:依旧是SDK中的编译器路径前缀。KDIR:依旧是内核源码目录。
(2)编译
make
当前目录下会出现 data_exchange_driver.ko 文件,这就是我们要的。
(3)加载模块
将 data_exchange_driver.ko 复制到开发板中(U盘、TF卡或者是SSH都可以),并运行下面的命令加载模块:
sudo insmod data_exchange_driver.ko
系统会打印设备主设备号。

3. 设定读写权限
sudo chmod 666 /dev/mychardev

4. 用户空间测试
(1)写入测试
echo "hello kernel" > /dev/mychardev

(2)读取测试
cat /dev/mychardev

5. 卸载模块
sudo rmmod data_exchange_driver

四、APP调用测试
1、编写APP程序
在 06_user_kernel_data/ 目录下创建一个 data_exchange_app.c 文件,并编写如下的代码:
#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. 打开设备
fd = open(DEV_PATH, O_RDWR);
if (fd < 0) {
perror("open");
return 1;
}
printf("设备打开成功: %s\n", DEV_PATH);
// 2. 写入一段内容
ret = write(fd, wbuf, strlen(wbuf));
if (ret < 0) {
perror("write");
close(fd);
return 2;
}
printf("写入内容: %s (写了%zd字节)\n", wbuf, ret);
// 3. 读回设备内容
// 根据你的驱动实现,read会返回"This is data from kernel space!"
lseek(fd, 0, SEEK_SET); // 通常不需要,但为保险起见,移到开始
ret = read(fd, rbuf, sizeof(rbuf) - 1);
if (ret < 0) {
perror("read");
close(fd);
return 3;
}
rbuf[ret] = '\0';
printf("读回内容: %s (读了%zd字节)\n", rbuf, ret);
close(fd);
return 0;
}
2、编译APP
使用下面的命令进行编译:
/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_app
命令格式是:
<SDK的gcc交叉编译器> <源码.c文件> -o <最终生成的可执行文件名字>
-o重名的意思,后面紧跟着最终想要生成的名字。
- SDK的gcc交叉编译器:这个就和之前我们在Makefile中编写的路径一致只不过变为了
aarch64-none-linux-gnu-gcc,不单单是只有前缀了。
最终就是这样的:

3、APP运行测试
运行之前要确保
data_exchange_driver.ko模块已经挂载!!不然会报错。
将这个 data_exchange_app 复制到开发板中(U盘、TF卡、SSH都可以),并运行:
sudo ./data_exchange_app
最终的效果是这样的:

[ 1132.722204] mychardev: chrdev_open called
- APP调用
open("/dev/mychardev", O_RDWR)时,内核驱动的chrdev_open被执行,打印了这行log。 - 说明设备节点能被正常打开,驱动正常响应。
[ 1132.722437] mychardev: Received from user: Hello kernel from userspace!
- APP调用
write(fd, wbuf, strlen(wbuf)),内核驱动的chrdev_write被执行,并成功把内容从用户空间复制到内核,且打印了用户数据。 - 说明数据交换写通路正常!
设备打开成功: /dev/mychardev
- APP里的
printf,标志open顺利,用户空间能访问设备节点。
[ 1132.722469] mychardev: chrdev_read called
- APP调用
read(fd, rbuf, ...),内核的chrdev_read被正常调用。 - 说明数据交换读通路正常,驱动能把数据回传给用户进程。
写入内容: Hello kernel from userspace! (写了28字节)
- APP的
printf,表明驱动返回写入了28个字节。 - 这个数字实际是
write_size,test字符串长度。 - write路径没有错。
[ 1132.722512] mychardev: chrdev_release called
- APP关闭文件句柄(
close(fd)),驱动的chrdev_release被调用,log随即打印。
读回内容: This is data from kernel space! (读了31字节)
- APP的
printf,表明驱动返回了31个字节内容,这就是驱动里data数组传出来的那句话。 - 说明read通路也没问题,正常实现了用户空间和内核空间的数据交换。
五、注意事项
- 不能直接用指针传递用户空间和内核空间数据。用
copy_to_user/copy_from_user等安全方法。 - 所有向用户空间返回的数据都需要显式
copy_to_user。 - 驱动必须有权限,注意权限控制。
- 内核模块调试可查看
dmesg输出。


被折叠的 条评论
为什么被折叠?



