Skip to content

LV020-llseek定位

一、概述

1. 背景描述

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

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

2. 怎么做?

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

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

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

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

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

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

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

c
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 函数进行读写位置的调整,该函数的具体使用说明如下:

c
#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 使用实例

c
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 函数:

c
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()

c
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()

c
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

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

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. 开发板测试

shell
# 加载驱动
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