LV020-llseek定位
一、概述
1. 背景描述
假如现在有这样一个场景,将两个字符串依 次进行写入,并对写入完成的字符串进行读取,如果仍采用之前的方式,第二次的写入值会覆盖第一次写入值,那要如何来实现上述功能呢?
我们在应用层有用过 lseek 进行文件读写位置的定位,字符设备也是文件,那是不是一样的原理?当然是啦,我们这部分就实现一下驱动中的 llseek 函数。
2. 怎么做?
我们来看一下驱动程序中的 read 函数指针:fs.h - include/linux/fs.h - *read:
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);write 函数指针在这里 fs.h - include/linux/fs.h - *write:
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);llseek 函数指针在这里 fs.h - include/linux/fs.h - *llseek:
loff_t (*llseek) (struct file *, loff_t, int);其实这里我就没有去深究为什么了,具体做法就是实现以下三个函数:
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 函数进行读写位置的调整,该函数的具体使用说明如下:
#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 使用实例
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 函数:
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()
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()
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
#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
#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. 开发板测试
# 加载驱动
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