内核空间与用户控件数据交换
前面了解的字符设备中对 file_operations 结构体的进行了填充, 该
结构体的每一个成员都对应着一个系统调用, 例如 read、 write 等, 在字符设备相关的文章中有实验过对
调用函数进行了标志打印, 并没有真正实现设备的读写功能。 这里需要实现的功能其实就是内核空间和用户控件之间的数据交换
文章目录
- 理解概念:内核空间与用户空间
- 参考资料
- 用户空间和内核空间数据交换
- 用户空间数据复制到内核空间 copy_from_user
- 数据从内核空间拷贝到用户空间copy_to_user
- 实验
- 源码程序 file.c
- 方法 copy_to_user
- 方法 copy_from_user
- 编译文件 Makefile
- 测试程序
- 源码程序分析
- 加载驱动 insmod file.ko
- 执行程序 ./appfile
- 总结
理解概念:内核空间与用户空间
Linux 系统将可访问的内存空间分为了两个部分, 一部分是内核空间, 一部分是用户空间。
操作系统和驱动程序运行在内核空间(内核态) , 应用程序运行在用户空间(用户态) 。
那么为什么要区分用户空间和内核空间呢?
(1) 内核空间中的代码控制了硬件资源, 用户空间中的代码只能通过内核暴露的系统调
用接口来使用系统中的硬件资源, 这样的设计可以保证操作系统自身的安全性和稳定性。
(2) 从另一方面来说, 内核空间的代码更偏向于系统管理, 而用户空间中的代码更偏重
业务逻辑实现, 俩者的分工不同。
硬件资源管理都是在内核空间完成的, 应用程序无法直接对硬件进行操作, 只能通过调用
相应的内核接口来完成相应的操作。 比如应用程序要对磁盘上的一个文件进行读取, 应用程序
可以向内核发起一个“系统调用” 申请——我要读取磁盘上的文件。 这个过程其实是通过一个
特殊的指令让进程从用户态进入到了内核态。 在内核空间中, CPU 可以执行任何命令, 包括
从磁盘上读取数据, 具体过程是先把数据读取到内核空间中, 然后再把数据拷贝到用户空间并
从内核态切换到用户态。 此时应用程序已经从系统调用中返回并拿到了想要的数据, 可以继续
往下执行了。
进程只有从用户空间切换到内核空间才可以使用系统的硬件资源, 切换的方式有三种: 系
统调用, 软中断, 硬中断, 如下图:
参考资料
字符设备的基础知识一定要了解,都是关联的知识点
申请字符设备号
注册字符设备
创建字符设备节点
字符设备驱动框架
杂项设备
用户空间和内核空间数据交换
内核空间和用户空间的内存是不能互相访问的。 但是很多应用程序都需要和内核进行数据
的交换, 例如应用程序使用 read 函数从驱动中读取数据, 使用 write 函数向驱动中写数据, 上
述功能就需要使用 copy_from_user 和 copy_to_user 俩个函数来完成。 copy_from_user 函数是将
用户空间的数据拷贝到内核空间。 copy_to_user 函数是将内核空间的数据拷贝到用户空间。
这俩个函数定义在了 kernel/include/linux/uaccess.h 文件下
用户空间数据复制到内核空间 copy_from_user
copy_from_user 用于将数据从用户空间复制到内核空间。
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
参数说明
- to: 内核空间的目标地址
- from: 用户空间的源地址
- n: 要复制的字节
使用示例
char kernel_buf[BUFSIZE];
if (copy_from_user(kernel_buf, user_buf, count)) {// 处理错误return -EFAULT;
}
数据从内核空间拷贝到用户空间copy_to_user
copy_to_user 用于将数据从内核空间复制到用户空间。 函数原型如下:
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);
- to: 用户空间的目标地址
- from: 内核空间的源地址
- n: 要复制的字节数
实验
源码程序 file.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>static dev_t dev_num;//定义dev_t类型(32位大小)的变量dev_num
static int major; //定义int类型的主设备号major
static int minor;//定义int类型的 次设备号minor
static struct cdev cdev_test; //定义struct cdev 类型结构体变量cdev_test,表示要注册的字符设备
struct class *class_test;//定于struct class *类型结构体变量class_test,表示要创建的类
struct device *device; //设备/*打开设备函数*/
static int chrdev_open(struct inode *inode, struct file *file)
{printk("This is chrdev_open \n");return 0;
}static ssize_t chrdev_read(struct file *file,char __user *buf, size_t size, loff_t *off)
{/*本章实验重点******/char kbuf[32] = "This is read from kernel";//定义内核空间数据// copy_to_user:内核空间向用户空间传数据if (copy_to_user(buf, kbuf, strlen(kbuf)) != 0) {printk("copy_to_user error\r\n"); //打印copy_to_user函数执行失败return -1;}printk("This is cdev_test_read\r\n");return 0;
}/*向设备写入数据函数*/
static ssize_t chrdev_write(struct file *file,const char __user *buf,size_t size,loff_t *off)
{/*本章实验重点******/char kbuf[32] = {0}; //定义写入缓存区kbufif (copy_from_user(kbuf, buf, size) != 0) // copy_from_user:用户空间向内核空间传数据{printk("copy_from_user error\r\n");//打印copy_from_user函数执行失败return -1;}printk("This is cdev_test_write\r\n");printk("kbuf is %s\r\n", kbuf);return 0;
}
static int chrdev_release(struct inode *inode, struct file *file)
{return 0;
}static struct file_operations cdev_test_ops = {.owner=THIS_MODULE,//将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块.open = chrdev_open,//将open字段指向chrdev_open(...)函数.read = chrdev_read,//将open字段指向chrdev_read(...)函数.write = chrdev_write,//将open字段指向chrdev_write(...)函数.release = chrdev_release,//将open字段指向chrdev_release(...)函数
};//定义file_operations结构体类型的变量cdev_test_opsstatic int __init chrdev_fops_init(void)//驱动入口函数
{int ret;//定义int类型的变量ret,用来判断函数返回值ret=alloc_chrdev_region(&dev_num,0,1,"chardev_num"); //通过动态方式进行设备号注册if(ret < 0){printk("alloc_chrdev_region is error\n");} printk("alloc_chrdev_region is ok\n");major=MAJOR(dev_num);//通过MAJOR()函数进行主设备号获取minor=MINOR(dev_num);//通过MINOR()函数进行次设备号获取printk("major is %d\n",major);printk("minor is %d\n",minor);使用cdev_init()函数初始化cdev_test结构体,并链接到cdev_test_ops结构体cdev_init(&cdev_test,&cdev_test_ops);cdev_test.owner = THIS_MODULE;//将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块 ret= cdev_add(&cdev_test,dev_num,1);if(ret < 0 ){printk("cdev_add is error\n");}printk("cdev_add is ok\n");class_test = class_create(THIS_MODULE,"class_test");//使用class_create进行类的创建,类名称为class_testdevice_create(class_test,NULL,dev_num,NULL,"device_test");//使用device_create进行设备的创建,设备名称为device_testreturn 0;
}
static void __exit chrdev_fops_exit(void)//驱动出口函数
{cdev_del(&cdev_test);//使用cdev_del()函数进行字符设备的删除unregister_chrdev_region(dev_num,1);//释放字符驱动设备号 device_destroy(class_test,dev_num);//删除创建的设备class_destroy(class_test);//删除创建的类printk("module exit \n");}
module_init(chrdev_fops_init);//注册入口函数
module_exit(chrdev_fops_exit);//注册出口函数
MODULE_LICENSE("GPL v2");//同意GPL开源协议
MODULE_AUTHOR("wang fang chen "); //作者信息
源码分析:
这里省略之前内容关联知识的内容,重点看 内核空间和用户控件数据交换部分内容。
方法 copy_to_user
读取内核空间数据到用户空间,定义了内核空间数据,其实就是驱动程序里面定义了字节数组
char kbuf[32] = “This is read from kernel”;//定义内核空间数据
copy_to_user 方法调用后, copy kbuf 数据到 buf里面去,buf 在方法里面的参数是一个用户空间态指针,这样就传递到用户空间了。
static ssize_t chrdev_read(struct file *file,char __user *buf, size_t size, loff_t *off)
{/*本章实验重点******/char kbuf[32] = "This is read from kernel";//定义内核空间数据// copy_to_user:内核空间向用户空间传数据if (copy_to_user(buf, kbuf, strlen(kbuf)) != 0) {printk("copy_to_user error\r\n"); //打印copy_to_user函数执行失败return -1;}printk("This is cdev_test_read\r\n");return 0;
}
方法 copy_from_user
从用户空间数据copy 数据到内核空间
buf 数据是用户态指针类型,调用 copy_from_user 方法后 kbuf 就有新的数据了,其实其实驱动程序里面的数据就是内核态数据,然后将其打印出来。
/*向设备写入数据函数*/
static ssize_t chrdev_write(struct file *file,const char __user *buf,size_t size,loff_t *off)
{/*本章实验重点******/char kbuf[32] = {0}; //定义写入缓存区kbufif (copy_from_user(kbuf, buf, size) != 0) // copy_from_user:用户空间向内核空间传数据{printk("copy_from_user error\r\n");//打印copy_from_user函数执行失败return -1;}printk("This is cdev_test_write\r\n");printk("kbuf is %s\r\n", kbuf);return 0;
}
编译文件 Makefile
#!/bin/bash
export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu-
obj-m += file.o
KDIR :=/home/wfc123/Linux/rk356x_linux/kernel
PWD ?= $(shell pwd)
all:make -C $(KDIR) M=$(PWD) modulesclean:make -C $(KDIR) M=$(PWD) clean
测试程序
appfile.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char *argv[]) // 主函数
{int fd; // 定义 int 类型的文件描述符char buf1[32] = {0}; // 定义读取缓存区 buf1char buf2[32] = "nihao"; // 定义写入缓存区 buf2fd = open("/dev/test", O_RDWR); // 打开字符设备驱动if (fd < 0){perror("open error \n");return fd;}read(fd, buf1, sizeof(buf1)); // 从/dev/test 文件读取数据printf("buf1 is %s \r\n", buf1); // 打印读取的数据write(fd, buf2, sizeof(buf2)); // 向/dev/test 文件写入数据close(fd);return 0;
}
源码程序分析
这里关注read 方法,buf1 用户态数据,读取后 经过系统调用
copy_to_user(buf, kbuf, strlen(kbuf)),内核态数据copy 了一份到用户态 buf1 中,然后打印出来。
需要 aarch64-linux-gnu-gcc 来编译, 输入以下命令, 编译完成以后会生成一个可执行程序
aarch64-linux-gnu-gcc appfile.c -o appfile
生成可执行程序 appfile
加载驱动 insmod file.ko
这里都是以前的基本知识,暂不扩展
执行程序 ./appfile
结果如下: 不正是内核和用户态数据交互结果吗?
总结
- copy_from_user 和 copy_to_user 是 Linux 内核中用于在用户空间和内核空间之间安全传输数据的两个重要函数。
- 内核和用户态数据传递就是通过两个方法调用来实现,回调到用户态其实就是指针传递。