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 | static ssize_t sdev_read(struct file *file, char __user *buf, size_t size, loff_t *off) |
在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 |
|
这个函数用于移动文件的读写位置。
参数:
fd: 文件描述符;
off_t offset: 偏移量,单位是字节的数量,可以正负,如果是负值表示向前移动;如果是正值,表示向后移动。
whence:当前位置的基点,可以使用三组值。 SEEK_SET——把文件指针直接设置成 offset,SEEK_CUR——把文件指针设置成 当前位置 + offset 值,SEEK_END——把文件指针设置成 文件结束位置 + offset 值。
返回值:成功返回当前位移大小,失败返回-1。
1.2 使用实例
1 | lseek(fd, 5, SEEK_SET); // 文件位置指针设置为 5 |
2. 驱动程序中实现的llseek
2.1 sdev_llseek()
应用程序中调用lseek函数,会最终调用到我们驱动函数中实现的llseek函数:
1 | static loff_t sdev_llseek(struct file *file, loff_t offset, int whence) |
使用 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 | static ssize_t sdev_read(struct file *file, char __user *buf, size_t size, loff_t *off) |
注意这里我们更新off参数,而不直接更新file→f_ops。另外,此代码逻辑中,当缓冲区buf大小不够存下要读取的数据的时候,只会读取当前位置到buf结束的所有数据。
2.3 sdev_write()
1 | static ssize_t sdev_write(struct file *file, const char __user *buf, size_t size, loff_t *off) |
此代码逻辑中,当缓冲区buf大小不够存下要写入的数据的时候,要写入的数据会被截断。
三、使用实例
1. 源码编写
1.1 chrdev_llseek_demo.c
1 |
|
1.2 chrdev_llseek_demo_app.c
1 |
|
2. 开发板测试
1 | 加载驱动 |