LV06-03-chrdev-03-内核空间与用户空间数据交互

本文主要是字符设备驱动——内核空间与用户空间数据交互的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
PC端开发环境 Windows Windows11
Ubuntu Ubuntu20.04.2的64位版本
VMware® Workstation 17 Pro 17.6.0 build-24238078
终端软件 MobaXterm(Professional Edition v23.0 Build 5042 (license))
Win32DiskImager Win32DiskImager v1.0
Linux开发板环境 Linux开发板 正点原子 i.MX6ULL Linux 阿尔法开发板
uboot NXP官方提供的uboot,使用的uboot版本为U-Boot 2019.04
linux内核 linux-4.19.71(NXP官方提供)
点击查看本文参考资料
分类 网址 说明
官方网站 https://www.arm.com/ ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档
https://www.nxp.com.cn/ NXP官方网站
https://www.nxpic.org.cn/NXP 官方社区
https://u-boot.readthedocs.io/en/latest/u-boot官网
https://www.kernel.org/linux内核官网
点击查看相关文件下载
分类 网址 说明
NXP https://github.com/nxp-imx NXP imx开发资源GitHub组织,里边会有u-boot和linux内核的仓库
nxp-imx/linux-imx/releases/tag/v4.19.71 NXP linux内核仓库tags中的v4.19.71
nxp-imx/uboot-imx/releases/tag/rel_imx_4.19.35_1.1.0 NXP u-boot仓库tags中的rel_imx_4.19.35_1.1.0
I.MX6ULL i.MX 6ULL Applications Processors for Industrial Products I.MX6ULL 芯片手册(datasheet,可以在线查看)
i.MX 6ULL Applications ProcessorReference Manual I.MX6ULL 参考手册(下载后才能查看,需要登录NXP官网)
Source Code https://elixir.bootlin.com/linux/latest/source linux kernel源码
https://elixir.bootlin.com/u-boot/latest/source uboot源码

一、内核空间与用户空间

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

1. 为什么分两部分?

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

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

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

进程只有从用户空间切换到内核空间才可以使用系统的硬件资源,切换的方式有三种:系统调用,软中断,硬中断:

image-20241204093654633

2. 用户空间和内核空间数据交换

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

2.1 copy_to_user函数

copy_to_user函数定义在uaccess.h - include/linux/uaccess.h - copy_to_user

1
2
3
4
5
6
7
static __always_inline unsigned long __must_check
copy_to_user(void __user *to, const void *from, unsigned long n)
{
if (likely(check_copy_size(from, n, true)))
n = _copy_to_user(to, from, n);
return n;
}

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

参数:

  • *to:指定目标地址,也就是数据存放的地址,在这里是用户空间的指针
  • *from:指定源地址,也就是数据的来源,在这里是内核空间的指针
  • n 是从内核空间向用户空间拷贝的字节数

返回值:内核空间向用户空间拷贝的字节数

2.2 copy_from_user函数

copy_from_user定义在uaccess.h - include/linux/uaccess.h - copy_from_user

1
2
3
4
5
6
7
static __always_inline unsigned long __must_check
copy_from_user(void *to, const void __user *from, unsigned long n)
{
if (likely(check_copy_size(to, n, false)))
n = _copy_from_user(to, from, n);
return n;
}

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

参数:

  • *to:指定目标地址,也就是数据存放的地址,在这里是内核空间的指针
  • *from:指定源地址,也就是数据的来源,在这里是用户空间的指针
  • n 是从用户空间向内核空间拷贝的字节数

返回值:用户空间向内核空间拷贝的字节数

二、使用实例

1. 源码编写

1.1 chrdev_data_exchange_demo.c

点击查看详情
1
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
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
#include <linux/init.h> /* module_init module_exit */
#include <linux/kernel.h>
#include <linux/module.h> /* MODULE_LICENSE */

#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>

#include "./timestamp_autogenerated.h"
#include "./version_autogenerated.h"

#define CHRDEV_NAME "sdev" /* 设备名,cat /proc/devices 查看与设备号的对应关系 */
#define CLASS_NAME "sclass" /* 类名,在 /sys/class 中显示的名称 */
#define DEVICE_NAME "sdevice" /* 设备节点名,在 /sys/class/class_name/ 中显示的名称以及 /dev/ 下显示的节点名 */

static dev_t dev_num; // 定义dev_t类型(32位大小)的变量dev_num,用来存放设备号
static struct cdev g_cdev_dev; // 定义cdev结构体类型的变量g_cdev_dev
static struct class *p_class_dev; // 定于struct class *类型结构体变量 p_class_dev,表示要创建的类

static int sdev_open(struct inode *inode, struct file *file)
{
printk("This is sdev_open!\n");
return 0;
}

static ssize_t sdev_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
char kbuf[32] = "hello sumu!"; // 定义内核空间数据

if (copy_to_user(buf, kbuf, strlen(kbuf)) != 0) // copy_to_user:内核空间向用户空间传数据
{
printk("copy_to_user error\n"); // 打印copy_to_user函数执行失败
return -1;
}

printk("This is sdev_read! kbuf is %s \n", kbuf);
return 0;
}

static ssize_t sdev_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\n"); // 打印copy_from_user函数执行失败
return -1;
}
printk("This is sdev_write! kbuf is %s \n", kbuf);

return 0;
}
static int sdev_release(struct inode *inode, struct file *file)
{
printk("This is sdev_release!\n");
return 0;
}

static struct file_operations g_cdev_dev_ops = {
.owner = THIS_MODULE,//将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
.open = sdev_open,
.read = sdev_read,
.write = sdev_write,
.release = sdev_release,
}; // 定义file_operations结构体类型的变量g_cdev_dev_ops

// 模块入口函数
static int __init chrdev_data_exchange_demo_init(void)
{
int ret; // 定义int类型的变量ret,用来判断函数返回值
int major, minor; // 定义int类型的主设备号major和次设备号minor
printk("*** [%s:%d]Build Time: %s %s, git version:%s ***\n", __FUNCTION__,
__LINE__, KERNEL_KO_DATE, KERNEL_KO_TIME, KERNEL_KO_VERSION);
printk("chrdev_data_exchange_demo module init!\n");

ret = alloc_chrdev_region(&dev_num, 0, 1, CHRDEV_NAME); // 自动获取设备号,设备名为chrdev_name
if (ret < 0)
{
printk("alloc_chrdev_region is error!\n");
}
printk("alloc_register_region is ok!\n");
major = MAJOR(dev_num); // 使用MAJOR()函数获取主设备号
minor = MINOR(dev_num); // 使用MINOR()函数获取次设备号
printk("major is %d, minor is %d !\n", major, minor);

cdev_init(&g_cdev_dev, &g_cdev_dev_ops); // 使用cdev_init()函数初始化g_cdev_dev结构体,并链接到g_cdev_dev_ops结构体
g_cdev_dev.owner = THIS_MODULE; // 将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
ret = cdev_add(&g_cdev_dev, dev_num, 1); // 使用cdev_add()函数进行字符设备的添加
if (ret < 0)
{
printk("cdev_add is error !\n");
}
printk("cdev_add is ok !\n");
p_class_dev = class_create(THIS_MODULE, CLASS_NAME); // 使用class_create进行类的创建,类名称为 class_dev
device_create(p_class_dev, NULL, dev_num, NULL, DEVICE_NAME); // 使用device_create进行设备的创建,设备名称为 device_dev

return 0;
}

// 模块出口函数
static void __exit chrdev_data_exchange_demo_exit(void)
{
// 需要注意的是, 字符设备的注册要放在申请字符设备号之后,
// 字符设备的删除要放在释放字符驱动设备号之前。
cdev_del(&g_cdev_dev); // 使用cdev_del()函数进行字符设备的删除
unregister_chrdev_region(dev_num, 1); // 释放字符驱动设备号
device_destroy(p_class_dev, dev_num); // 删除创建的设备
class_destroy(p_class_dev); // 删除创建的类
printk("chrdev_data_exchange_demo exit!\n");
}

module_init(chrdev_data_exchange_demo_init); // 将__init定义的函数指定为驱动的入口函数
module_exit(chrdev_data_exchange_demo_exit); // 将__exit定义的函数指定为驱动的出口函数

/* 模块信息(通过 modinfo chrdev_data_exchange_demo 查看) */
MODULE_LICENSE("GPL v2"); /* 源码的许可证协议 */
MODULE_AUTHOR("sumu"); /* 字符串常量内容为模块作者说明 */
MODULE_DESCRIPTION("Description"); /* 字符串常量内容为模块功能说明 */
MODULE_ALIAS("module's other name"); /* 字符串常量内容为模块别名 */

1.2 chrdev_data_exchange_demo_app.c

点击查看详情
1
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
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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>

static char usrdata[] = {"usr data!"};

void usage_info(void)
{
printf("\n");
printf("+++++++++++++++++++++++++++++++++++++++++\n");
printf("+ help information @sumu +\n");
printf("+++++++++++++++++++++++++++++++++++++++++\n");

printf("help:\n");
printf("use format: ./app_name /dev/device_name arg1 ... \n");
printf(" ./chrdev_data_exchange_demo_app.out /dev/sdevice 1 # 读取 \n");
printf(" ./chrdev_data_exchange_demo_app.out /dev/sdevice 2 # 写入 \n");
printf("\n");

printf("command info:\n");
printf(" (1)load module : insmod module_name.ko\n");
printf(" (2)unload module: rmmod module_name.ko\n");
printf(" (3)show module : lsmod\n");
printf(" (4)view device : cat /proc/devices\n");
printf(" (5)create device node: mknod /dev/device_name c major_num secondary_num \n");
printf(" (6)show device node : ls /dev/device_name \n");
printf(" (7)show device vlass : ls /sys/class \n");
printf("+++++++++++++++++++++++++++++++++++++++++\n");
}

int main(int argc, char *argv[])
{
int fd = -1;
int ret = 0;
char *filename = NULL;
char readbuf[100] = {0};
char writebuf[100] = {0};

if (argc != 3)
{
usage_info();
return -1;
}

filename = argv[1];

/* 打开驱动文件 */
fd = open(filename, O_RDWR);
if (fd < 0)
{
printf("can't open file %s !\n", filename);
return -1;
}


if (atoi(argv[2]) == 1)
{
/* 从驱动文件读取数据 */
ret = read(fd, readbuf, 32);
if (ret < 0)
{
printf("read file %s failed!\n", filename);
}

/* 读取成功,打印出读取成功的数据 */
printf("read data \"%s\" from %s !\n", readbuf, filename);

}

if (atoi(argv[2]) == 2)
{
/* 向设备驱动写数据 */
memcpy(writebuf, usrdata, sizeof(usrdata));
ret = write(fd, writebuf, 32);
if (ret < 0)
{
printf("write file %s failed!\n", filename);
}

printf("write \"%s\" to %s success!\n", usrdata, filename);
}

/* 关闭设备 */
ret = close(fd);
if (ret < 0)
{
printf("can't close file %s !\n", filename);
return -1;
}

return 0;
}

2. 开发板测试

2.1 app demo实现读写

1
2
3
4
5
6
7
8
9
# 加载驱动
insmod xxx_demo.ko

# app测试
./xxx_demo_app.out /dev/dev_node 1 # 读取
./xxx_demo_app.out /dev/dev_node 2 # 写入

# 卸载驱动
rmmod xxx_demo.ko

2.2 cat与echo

除了使用自己写的app demo测试,我们还可以使用cat命令查看字符设备缓冲区的内容

1
cat /dev/dev_node
image-20241205234035364

可以通过echo命令向字符设备缓冲区写入数据:

1
2
3
echo "hello world" > /dev/dev_node
# 或者
sudo sh -c "echo 'hello world' > /dev/dev_node"
image-20241205234215599

这个好像有点问题,不按下Ctrl + c 的话,会一直往里面写,具体原因还不知道,不过这里只是为了演示有这样的方式,后面知道原因了再补充。