LV06-03-chrdev-04-llseek的使用

本文主要是字符设备驱动——使用lseek函数实现字符设备定位读写的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
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源码

一、概述

1. 背景描述

假如现在有这样一个场景,将两个字符串依 次进行写入,并对写入完成的字符串进行读取,如果仍采用之前的方式,第二次的写入值会覆盖第一次写入值,那要如何来实现上述功能呢?

我们在应用层有用过lseek进行文件读写位置的定位,字符设备也是文件,那是不是一样的原理?当然是啦,我们这部分就实现一下驱动中的llseek函数。

2. 怎么做?

我们来看一下驱动程序中的read函数指针:fs.h - include/linux/fs.h - *read

1
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);

write函数指针在这里 fs.h - include/linux/fs.h - *write

1
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);

llseek函数指针在这里fs.h - include/linux/fs.h - *llseek

1
loff_t (*llseek) (struct file *, loff_t, int);

其实这里我就没有去深究为什么了,具体做法就是实现以下三个函数:

1
2
3
static ssize_t sdev_read(struct file *file, char __user *buf, size_t size, loff_t *off)
static ssize_t sdev_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
static loff_t sdev_llseek(struct file *file, loff_t offset, int whence)

在llseek函数中,更新file→f_pos成员的值,struct file结构体中 fs.h - include/linux/fs.h - *f_pos 成员用于记录当前的读写位置。每次读写操作后,f_pos 会根据读写的数据量进行更新(这个应该是内核去自动更新的)。读写位置的更新大概是这样一个过程:

(1)应用程序调用lseek()函数指定读写指针的位置;

(2)系统调用最终调用到sdev_llseek()函数,函数中更新file→f_pos的值。

(3)在调用到read/write函数的时候,经过系统调用,会调用sdev_read()/sdev_write()函数,这两个函数中这个参数 off 的值会被更新为file→f_pos的值。然后进行读写操作,读写完毕后,在sdev_read()/sdev_write()函数中更新 off 的值(可以看到是一个指针参数,所以完全可以通过指针更新)。

(4)内核更新file→f_pos成员的值。

为什么在成功拷贝到用户态数据之后,需要更新*off这个指针的数据,而不是 file→f_pos 的数据?

从最终目的上来讲应该是一致的,都是表示用户读写位置的指针,为什么要费劲多传一个 off 过来,还需要内核代码对这个值去修改呢,我们自己直接修改file→f_pos中的数据不好吗?

网上看到一个解释:这么做的原因是“时机”,系统要求我们更新 *off,给系统一个机会去选择。系统可以直接选择将 off 指向 &file→f_pos, 也可以选择指向一个临时区域,后者拥有更大的灵活性。

比如由于某种情况不能正确返回到用户态,或者不能立即返回,这时内核临时保存这个读文件之后的偏移,file→f_pos存储的信息还是用户读写之前的样子,这是符合逻辑的,因为用户这时确实还未读到数据,我们不能提前更新file→f_pos。最终内核会在一个合适的时机将*off中的数据写回给file→f_pos。

这里有一个帖子讨论了这个问题:c - Reason why use loff_t *offp instead of direct filp->f_pos usage - Stack Overflow

二、定位设备 llseek

1. 应用程序中使用的lseek

1.1 函数说明

在应用程序中使用 lseek 函数进行读写位置的调整,该函数的具体使用说明如下:

1
2
3
4
#include <sys/types.h>
#include <unistd.h>

off_t lseek(int fd, off_t offset, int whence);

这个函数用于移动文件的读写位置。

参数:

  • fd: 文件描述符;

  • off_t offset: 偏移量,单位是字节的数量,可以正负,如果是负值表示向前移动;如果是正值,表示向后移动。

  • whence:当前位置的基点,可以使用三组值。 SEEK_SET——把文件指针直接设置成 offset,SEEK_CUR——把文件指针设置成 当前位置 + offset 值,SEEK_END——把文件指针设置成 文件结束位置 + offset 值。

返回值:成功返回当前位移大小,失败返回-1。

1.2 使用实例

1
2
3
4
5
lseek(fd, 5, SEEK_SET);  // 文件位置指针设置为 5
lseek(fd, 0, SEEK_END); // 文件位置设置成文件末尾
lseek(fd, 0, SEEK_CUR); // 确定当前的文件位置

lseek(fd, -1, SEEK_CUR); // 文件位置设置成当前位置的前一个位置

2. 驱动程序中实现的llseek

2.1 sdev_llseek()

应用程序中调用lseek函数,会最终调用到我们驱动函数中实现的llseek函数:

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
static loff_t sdev_llseek(struct file *file, loff_t offset, int whence)
{
loff_t new_offset = 0; // 定义loff_t类型的新的偏移值
switch (whence) // 对lseek函数传递的whence参数进行判断
{
case SEEK_SET:
if (offset < 0 || offset > BUFSIZE)
{
return -EINVAL; // EINVAL=22 表示无效参数
}
new_offset = offset; // 如果whence参数为SEEK_SET,则新偏移值为offset
break;
case SEEK_CUR:
if ((file->f_pos + offset < 0) || (file->f_pos + offset > BUFSIZE))
{
return -EINVAL;
}
new_offset = file->f_pos + offset; // 如果whence参数为SEEK_CUR,则新偏移值为file->f_pos + offset,file->f_pos为当前的偏移值
break;
case SEEK_END:
if (file->f_pos + offset < 0)
{
return -EINVAL;
}
new_offset = BUFSIZE + offset; // 如果whence参数为SEEK_END,则新偏移值为BUFSIZE + offset,BUFSIZE为最大偏移量
break;
default:
break;
}
file->f_pos = new_offset; // 更新file->f_pos偏移值

return new_offset;
}

使用 switch 语句对传递的 whence 参数进行判断,whence 在这里可以有三个取值, 分别为 SEEK_SET、SEEK_CUR 和 SEEK_END。switch语句内部分别对三个参数所代表的功能进行实现,其中需要注意 的是 file→f_pos 指的是当前文件的偏移值。

Tips:这里的逻辑我们传入的位置甚至可以直接等于buf的大小,例如,buf为32时,写入的时候索引为0-31,但是这里支持定位到32,。有什么好处?方便知道buf总大小啊,直接一个SEEK_END,然后偏移0,就可以知道buf总大小了。

2.2 sdev_read()

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
static ssize_t sdev_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
int i = 0;
loff_t offset = *off; // 将读取数据的偏移量赋值给loff_t类型变量p
size_t count = size;

if (offset > BUFSIZE)
{
return 0;
}

if (count > BUFSIZE - offset)
{
count = BUFSIZE - offset;
}

if (copy_to_user(buf, mem + offset, count))
{
// 将mem中的值写入buf,并传递到用户空间
printk("copy_to_user error!\n");
return -1;
}

for (i = 0; i < BUFSIZE; i++)
{
printk("mem buf[%d] %c\n", i, mem[i]); // 将mem中的值打印出来
}
printk("read offset is %llu, count is %d\n", offset, count);
*off = *off + count; // 更新偏移值

return count;
}

注意这里我们更新off参数,而不直接更新file→f_ops。另外,此代码逻辑中,当缓冲区buf大小不够存下要读取的数据的时候,只会读取当前位置到buf结束的所有数据。

2.3 sdev_write()

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
static ssize_t sdev_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
int i = 0;
loff_t offset = *off; // 将读取数据的偏移量赋值给loff_t类型变量p
size_t count = size;
if (offset > BUFSIZE)
{
return 0;
}

if (count > BUFSIZE - offset)
{
count = BUFSIZE - offset;
}

if (copy_from_user(mem + offset, buf, count))
{
// 将buf中的值,从用户空间传递到内核空间
printk("copy_to_user error \n");
return -1;
}

for (i = 0; i < BUFSIZE; i++)
{
printk("mem buf[%d] %c\n", i, mem[i]); // 将mem中的值打印出来
}
printk("write offset is %llu, count is %d\n", offset, count); // 打印写入的值
*off = *off + count; // 更新偏移值

return count;
}

此代码逻辑中,当缓冲区buf大小不够存下要写入的数据的时候,要写入的数据会被截断。

三、使用实例

1. 源码编写

1.1 chrdev_llseek_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
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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
#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/ 下显示的节点名 */
#define BUFSIZE 32 /* 设置最大偏移量为32 */

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 char mem[BUFSIZE] = {0}; // 设置数据存储数组mem

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)
{
int i = 0;
loff_t offset = *off; // 将读取数据的偏移量赋值给loff_t类型变量p
size_t count = size;

if (offset > BUFSIZE)
{
return 0;
}

if (count > BUFSIZE - offset)
{
count = BUFSIZE - offset;
}

if (copy_to_user(buf, mem + offset, count))
{
// 将mem中的值写入buf,并传递到用户空间
printk("copy_to_user error!\n");
return -1;
}

for (i = 0; i < BUFSIZE; i++)
{
printk("mem buf[%d] %c\n", i, mem[i]); // 将mem中的值打印出来
}
printk("read offset is %llu, count is %d\n", offset, count);
*off = *off + count; // 更新偏移值

return count;
}

static ssize_t sdev_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
int i = 0;
loff_t offset = *off; // 将读取数据的偏移量赋值给loff_t类型变量p
size_t count = size;
if (offset > BUFSIZE)
{
return 0;
}

if (count > BUFSIZE - offset)
{
count = BUFSIZE - offset;
}

if (copy_from_user(mem + offset, buf, count))
{
// 将buf中的值,从用户空间传递到内核空间
printk("copy_to_user error \n");
return -1;
}

for (i = 0; i < BUFSIZE; i++)
{
printk("mem buf[%d] %c\n", i, mem[i]); // 将mem中的值打印出来
}
printk("write offset is %llu, count is %d\n", offset, count); // 打印写入的值
*off = *off + count; // 更新偏移值

return count;
}

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

static loff_t sdev_llseek(struct file *file, loff_t offset, int whence)
{
loff_t new_offset = 0; // 定义loff_t类型的新的偏移值
switch (whence) // 对lseek函数传递的whence参数进行判断
{
case SEEK_SET:
if (offset < 0 || offset > BUFSIZE)
{
return -EINVAL; // EINVAL=22 表示无效参数
}
new_offset = offset; // 如果whence参数为SEEK_SET,则新偏移值为offset
break;
case SEEK_CUR:
if ((file->f_pos + offset < 0) || (file->f_pos + offset > BUFSIZE))
{
return -EINVAL;
}
new_offset = file->f_pos + offset; // 如果whence参数为SEEK_CUR,则新偏移值为file->f_pos + offset,file->f_pos为当前的偏移值
break;
case SEEK_END:
if (file->f_pos + offset < 0)
{
return -EINVAL;
}
new_offset = BUFSIZE + offset; // 如果whence参数为SEEK_END,则新偏移值为BUFSIZE + offset,BUFSIZE为最大偏移量
break;
default:
break;
}
file->f_pos = new_offset; // 更新file->f_pos偏移值

return new_offset;
}

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

// 模块入口函数
static int __init chrdev_llseek_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_llseek_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_llseek_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_llseek_demo exit!\n");
}

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

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

1.2 chrdev_llseek_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
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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>

#define BUFSIZE 32 /* 设置最大偏移量为64, 方便打印完整的内存空间数据*/
static char usrdata[] = {"sumu"};

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 x # 从x位置读取 \n");
printf(" ./chrdev_data_exchange_demo_app.out /dev/sdevice 2 x # 从x位置写入 \n");
printf(" 驱动中buf最大为1024字节 \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;
unsigned int arg1 = 0;
unsigned int arg2 = 0;

char readbuf[BUFSIZE] = {0};
char writebuf[BUFSIZE] = {0};

unsigned int off1 = 0; // 定义读写偏移位置
unsigned int off2 = 0; // 定义读写偏移位置
unsigned int off = 0; // 定义读写偏移位置

printf("*** Build Time: %s %s,Git Version: %s Git Remote: %s***\n",
__DATE__, __TIME__, GIT_VERSION, GIT_PATH);
// ./xxx.out /dev/sdevice x x
if (argc != 4)
{
usage_info();
return -1;
}
// 解析参数
filename = argv[1];
arg1 = atoi(argv[2]);
arg2 = atoi(argv[3]);
printf("%s %s %d %d\n", argv[0], filename, arg1, arg2);

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

off1 = lseek(fd, 0, SEEK_END); // 读取字符设备文件可读写最大的大小
printf("%s mem buf size is %d\n", filename, off1);
if (arg1 == 1)
{
off = arg2;// 获取读写的偏移位置
/* 从驱动文件读取数据 */
ret = lseek(fd, off, SEEK_SET); // 将偏移量设置为距离起始地址 off的位置
if (ret < 0)
{
printf("lseek %s %d failed!\n", filename, off);
}

ret = read(fd, readbuf, BUFSIZE);
if (ret < 0)
{
printf("read file %s failed!\n", filename);
}
off2 = lseek(fd, 0, SEEK_CUR); // 读取当前位置的偏移量
/* 读取成功,打印出读取成功的数据 */
printf("read data \"%s\" from %s! off=%d off2=%d\n", readbuf, filename, off, off2);

}
else if (arg1 == 2)
{
off = arg2;// 获取读写的偏移位置
ret = lseek(fd, off, SEEK_SET); // 将偏移量设置为距离起始地址 off的位置
if (ret < 0)
{
printf("lseek %s %d failed!\n", filename, off);
}

/* 向设备驱动写数据 */
memcpy(writebuf, usrdata, sizeof(usrdata));
ret = write(fd, writebuf, sizeof(usrdata));
if (ret < 0)
{
printf("write file %s failed!\n", filename);
}
off2 = lseek(fd, 0, SEEK_CUR); // 读取当前位置的偏移量
printf("write \"%s\" to %s success!off=%d off2=%d\n", usrdata, filename, off, off2);
}

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

return 0;
}

2. 开发板测试

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

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

# 卸载驱动
rmmod xxx_demo.ko