第14章 内核空间与用户空间数据交互实验

在"第12章 字符设备驱动框架实验"中,已经对file_operations结构体的进行了填充,该结构体的每一个成员都对应着一个系统调用,例如read、write等,在对应的实验中,只是对调用函数进行了标志打印,并没有真正实现设备的读写功能,而在本章节将对内核空间与用户空间的数据交换功能进行实现。

14.1 内核空间与用户空间

Linux系统将可访问的内存空间分为了两个部分,一部分是内核空间,一部分是用户空间。操作系统和驱动程序运行在内核空间(内核态),应用程序运行在用户空间(用户态)。

那么为什么要区分用户空间和内核空间呢?

(1)内核空间中的代码控制了硬件资源,用户空间中的代码只能通过内核暴露的系统调用接口来使用系统中的硬件资源,这样的设计可以保证操作系统自身的安全性和稳定性。

(2)从另一方面来说,内核空间的代码更偏向于系统管理,而用户空间中的代码更偏重业务逻辑实现,俩者的分工不同。

硬件资源管理都是在内核空间完成的,应用程序无法直接对硬件进行操作,只能通过调用相应的内核接口来完成相应的操作。比如应用程序要对磁盘上的一个文件进行读取,应用程序可以向内核发起一个"系统调用"申请------我要读取磁盘上的文件。这个过程其实是通过一个特殊的指令让进程从用户态进入到了内核态。在内核空间中,CPU可以执行任何命令,包括从磁盘上读取数据,具体过程是先把数据读取到内核空间中,然后再把数据拷贝到用户空间并从内核态切换到用户态。此时应用程序已经从系统调用中返回并拿到了想要的数据,可以继续往下执行了。

进程只有从用户空间切换到内核空间才可以使用系统的硬件资源,切换的方式有三种:系统调用,软中断,硬中断,如下图(图 14-1)所示:

图 14-1

14.2 用户空间和内核空间数据交换

内核空间和用户空间的内存是不能互相访问的。但是很多应用程序都需要和内核进行数据的交换,例如应用程序使用read函数从驱动中读取数据,使用write函数向驱动中写数据,上述功能就需要使用copy_from_user和copy_to_user俩个函数来完成。copy_from_user函数是将用户空间的数据拷贝到内核空间。copy_to_user函数是将内核空间的数据拷贝到用户空间。

这俩个函数定义在了kernel/include/linux/uaccess.h文件下,如下所示:

copy_to_user

函数原型:

​ unsigned long copy_to_user_inatomic(void __user *to, const void *from, unsigned long n);

函数作用:

​ 把内核空间的数据复制到用户空间。

参数含义:

​ *to是用户空间的指针

​ *from是内核空间的指针

​ n是从内核空间向用户空间拷贝的字节数

copy_from_user

函数原型:

​ unsigned long copy_from_user(void *to, const void __user *from, unsigned long n)

函数作用:

​ 把用户空间的数据复制到内核空间。

参数含义:

​ *to是内核空间的指针

​ *from是用户空间的指针

​ n是从用户空间向内核空间拷贝的字节数

14.3 实验程序编写

14.3.1 驱动程序编写

本驱动程序对应的网盘路径为:iTOP-RK3568开发板【底板V1.7版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux驱动配套资料\04_Linux驱动例程\09\module。

在该实验中将实现内核空间和用户空间进行数据交换的功能。以12章编写的字符设备驱动框架实验为基础编写驱动程序,程序使用copy_to_user函数和copy_from_user函数来实现内核空间和用户空间互传数据的功能,编写完成的file.c代码如下所示:

c 复制代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>

static dev_t dev_num;  //设备号
static int major = 0;  //主设备号
static int minor = 0;  //次设备号
struct cdev cdev_test; // cdev

struct class *class;       //类
struct device *device;    //设备

/*打开设备函数*/
static int cdev_test_open(struct inode *inode, struct file *file)
{
    printk("This is cdev_test_open\r\n");
    return 0;
}

/*向设备写入数据函数*/
static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
    /*本章实验重点******/
    char kbuf[32] = {0};   //定义写入缓存区kbuf
    if (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 ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
    /*本章实验重点******/
char kbuf[32] = "This is cdev_test_read!";//定义内核空间数据
// 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 int cdev_test_release(struct inode *inode, struct file *file)
{
    printk("This is cdev_test_release\r\n");
    return 0;
}

/*设备操作函数,定义file_operations结构体类型的变量cdev_test_fops*/
struct file_operations cdev_test_fops = {
    .owner = THIS_MODULE, //将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
    .open = cdev_test_open, //将open字段指向chrdev_open(...)函数
    .read = cdev_test_read,  //将open字段指向chrdev_read(...)函数
    .write = cdev_test_write, //将open字段指向chrdev_write(...)函数
    .release = cdev_test_release, //将open字段指向chrdev_release(...)函数
};

static int __init chr_fops_init(void) //驱动入口函数
{
    /*注册字符设备驱动*/
    int ret;
/*1 创建设备号*/
//动态分配设备号
    ret = alloc_chrdev_region(&dev_num, 0, 1, "alloc_name");     if (ret < 0)
    {
        printk("alloc_chrdev_region is error\n");//打印动态分配设备号失败
    }
    printk("alloc_chrdev_region is ok\n");

    major = MAJOR(dev_num); //获取主设备号
    minor = MINOR(dev_num); //获取次设备号

    printk("major is %d \r\n", major); //打印主设备号
    printk("minor is %d \r\n", minor); //打印次设备号
     /*2 初始化cdev*/
    cdev_test.owner = THIS_MODULE;
    cdev_init(&cdev_test, &cdev_test_fops);

    /*3 添加一个cdev,完成字符设备注册到内核*/
    cdev_add(&cdev_test, dev_num, 1);

    /*4 创建类*/
    class = class_create(THIS_MODULE, "test");

    /*5  创建设备*/
    device = device_create(class, NULL, dev_num, NULL, "test");
    return 0;
}

static void __exit chr_fops_exit(void) //驱动出口函数
{
    /*注销字符设备*/
    unregister_chrdev_region(dev_num, 1); //注销设备号
    cdev_del(&cdev_test);               //删除cdev
    device_destroy(class, dev_num);       //删除设备
    class_destroy(class);                 //删除类
}
module_init(chr_fops_init);   //注册入口函数
module_exit(chr_fops_exit);  //注册出口函数
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("topeet");

以上代码在cdev_test_read函数中使用copy_to_user函数将内核数据拷贝到用户空间,在cdev_test_write函数中使用copy_from_user函数将用户空间数据拷贝到内核空间。

14.3.2 编写测试 APP

本应用程序对应的网盘路径为:iTOP-RK3568开发板【底板V1.7版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux驱动配套资料\04_Linux驱动例程\09\app。

编写测试APP其实是在编写Linux应用,编译完成的应用程序app.c代码如下所示:

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}; //定义读取缓存区buf1
    char buf2[32] = "nihao"; //定义写入缓存区buf2
    fd = 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;
}

14.4 运行测试

14.4.1 编译驱动程序

在上一小节中的file.c代码同一目录下创建 Makefile 文件,Makefile 文件内容如下 所示:

makefile 复制代码
export ARCH=arm64#设置平台架构
export CROSS_COMPILE=aarch64-linux-gnu-#交叉编译器前缀
obj-m +=file.o    #此处要和你的驱动源文件同名
KDIR :=/home/topeet/Linux/linux_sdk/kernel    #这里是你的内核目录                                                                                                                            
PWD ?= $(shell pwd)
all:
    make -C $(KDIR) M=$(PWD) modules    #make操作
clean:
    make -C $(KDIR) M=$(PWD) clean    #make clean操作

对于Makefile的内容注释已在上图添加,保存退出之后,来到存放file.c和Makefile文件目录下,如下图(图14-2)所示:

图 14-2

然后使用命令"make"进行驱动的编译,编译完成如下图(图14-3)所示:

图14-3

编译完生成 file.ko目标文件,如下图(图 14-4)所示:

图 14-4

至此我们的驱动模块就编译成功了,下面进行应用程序编译.

14.4.2 编译应用程序

因为测试APP是要在开发板上运行的,所以需要aarch64-linux-gnu-gcc来编译,输入以下命令,编译完成以后会生成一个app的可执行程序,如下图(图 14-5)所示:

aarch64-linux-gnu-gcc app.c -o app

图 14-5

下面进行驱动程序的测试。

14.4.3 运行测试

驱动模块file.ko和测试程序app都已经准备好了,接下来就是运行测试。首先输入以下命令加载驱动程序,如下图(图14--6)所示:

insmod file.ko

图 14-6

输入以下命令运行应用程序,如下图(图 14-7)所示

图 14-7

由上图可知,打印"This is cdev_test_open"信息说明成功打开了字符设备驱动。

打印" This is cdev_test_read"和"buf1 is This is cdev_test_read!"说明应用程序成功读取到内核的数据。

打印"This is cdev_test_write"和"kbuf is nihao"说明应用程序向内核写数据成功。

最后打印"This is cdev_test_release"说明卸载字符设备。

【最新驱动资料(文档+例程)】

链接 https://pan.baidu.com/s/1M4smUG2vw_hnn0Hye-tkog

提取码:hbh6

【B 站配套视频】

https://b23.tv/XqYa6Hm

【RK3568 购买链接】

https://item.taobao.com/item.htm?spm=a1z10.5-c-s.w4002-2245

This is cdev_test_write"和"kbuf is nihao"说明应用程序向内核写数据成功。

最后打印"This is cdev_test_release"说明卸载字符设备。

【最新驱动资料(文档+例程)】

链接 https://pan.baidu.com/s/1M4smUG2vw_hnn0Hye-tkog

提取码:hbh6

【B 站配套视频】

https://b23.tv/XqYa6Hm

【RK3568 购买链接】

https://item.taobao.com/item.htm?spm=a1z10.5-c-s.w4002-2245

2452613.11.2fec74a6elWNeA&id=669939423234

相关推荐
cxr82812 小时前
SPARC方法论在Claude Code基于规则驱动开发中的应用
人工智能·驱动开发·claude·智能体
sukalot19 小时前
window显示驱动开发—显示适配器的子设备
驱动开发
Evan_ZGYF丶1 天前
【RK3576】【Android14】如何在Android14下单独编译kernel-6.1?
linux·驱动开发·android14·rk3576
sukalot2 天前
window显示驱动开发—视频呈现网络简介
驱动开发
sukalot3 天前
window显示驱动开发—为头装载和专用监视器生成自定义合成器应用(二)
驱动开发
zwhSunday3 天前
Linux驱动开发(1)概念、环境与代码框架
linux·运维·驱动开发
sukalot3 天前
window显示驱动开发—为头装载和专用监视器生成自定义合成器应用(三)
驱动开发
sukalot3 天前
window显示驱动开发—为头装载和专用监视器生成自定义合成器应用(一)
驱动开发
cxr8285 天前
基于Claude Code的 规范驱动开发(SDD)指南
人工智能·hive·驱动开发·敏捷流程·智能体
zwhSunday5 天前
Linux驱动开发(2)进一步理解驱动
linux·驱动开发