Linux内核与驱动:7.从应用层 lseek() 到驱动层 .llseek,Linux 字符设备偏移控制详解

很多初学 Linux 驱动的人,都会写 open/read/write/release,但一到 llseek 就容易糊涂:

  • 应用层调用的是 lseek() ,为什么驱动里实现的是 .llseek?
  • lseek() 改变的到底是什么?
  • read()/write 里的 offset 和 file->f_pos 是什么关系?

这篇就把这条链路完整讲清楚:从用户态 lseek(),一路走到驱动层 file_operations.llseek

1.应用层lseek()

lseek()本质上是在改"文件位置指针";

cpp 复制代码
lseek(fd, 10, SEEK_SET);

这行代码的含义是,把这个文件描述符的偏移量位置调整到起始位置偏移10的地方。

后续无论是read还是write这个fd,都是在调整之后的位置,同样的,read/write之后偏移量也会改变。

在内核里,这个"当前位置"通常对应:

cpp 复制代码
filp->f_pos

所以,lseek() 的核心作用,就是修改 struct file 中的 f_pos,而驱动层的 .llseek 就是专门干这件事的。

应用层怎么用lseek()?

标准原型:

cpp 复制代码
off_t lseek(int fd, off_t offset, int whence);

whence为基准位置,常见基准位置有三种:SEEK_SET,SEEK_CUR.SEEK_END,分别在文件开头位置,文件当前位置,文件末尾。

在字符设备里,"末尾"到底是什么?

对普通文件来说,SEEK_END 很自然,就是文件大小。但对字符设备来说,没有真正的磁盘文件长度。所以驱动开发时,通常需要自己定义设备的逻辑长度

比如你有一个 100 字节缓冲区:

cpp 复制代码
#define BUFSIZE 100
char kbuf[BUFSIZE];

那么通常就认为:

  • 设备起始位置:0
  • 设备末尾位置:BUFSIZE

也就是说,这个设备的可寻址范围就是 [0,BUFSIZE].

2.驱动层.llseek()

一个最简单、最典型的字符设备 llseek 如下:

cpp 复制代码
static loff_t cdev_test_llseek(struct file *filp, loff_t offset, int whence)
{
    loff_t new_pos;

    switch (whence) {
    case SEEK_SET:
        new_pos = offset;
        break;

    case SEEK_CUR:
        new_pos = filp->f_pos + offset;
        break;

    case SEEK_END:
        new_pos = BUFSIZE + offset;
        break;

    default:
        return -EINVAL;
    }

    if (new_pos < 0 || new_pos > BUFSIZE)
        return -EINVAL;

    filp->f_pos = new_pos;
    return new_pos;
}

这个函数做了 3 件事:

1)根据 whence 计算目标位置

SEEK_SET:直接等于 offset

SEEK_CUR:当前 f_pos + offset

SEEK_END:设备尾部 BUFSIZE + offset

2)做边界检查

如果越界,就返回:

cpp 复制代码
-EINVAL

3)更新 filp->f_pos

真正的偏移修改发生在这里:

cpp 复制代码
filp->f_pos = new_pos;

最后返回新的位置。

3..llseek 和 read/write 里的 offset 是什么关系

驱动中read的函数模板为:

cpp 复制代码
static ssize_t xxx_read(struct file *filp, char __user *buf,
                        size_t count, loff_t *offset)

这里的 offset ,其实就是:

cpp 复制代码
&filp->f_pos

所以逻辑关系可以总结为:

  • lseek() 通过 .llseek 修改 filp->f_pos
  • read()/write() 通过 *offset 读取和更新 filp->f_pos

驱动中read实现:

cpp 复制代码
static ssize_t cdev_test_read(struct file *filp, char __user *buf,
                              size_t count, loff_t *offset)
{
    struct cdev_test_dev *dev = filp->private_data;
    loff_t off = *offset;

    if (off >= BUFSIZE)
        return 0;

    if (count > BUFSIZE - off)
        count = BUFSIZE - off;

    if (copy_to_user(buf, dev->kbuf + off, count))
        return -EFAULT;

    *offset += count;
    return count;
}

驱动中write实现:

cpp 复制代码
static ssize_t cdev_test_write(struct file *filp, const char __user *buf,
                               size_t count, loff_t *offset)
{
    struct cdev_test_dev *dev = filp->private_data;
    loff_t off = *offset;

    if (off >= BUFSIZE)
        return 0;

    if (count > BUFSIZE - off)
        count = BUFSIZE - off;

    if (copy_from_user(dev->kbuf + off, buf, count))
        return -EFAULT;

    *offset += count;
    return count;
}

4.完整实验

驱动程序:

cpp 复制代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>  //copy_to_user copy_from_user
#include <linux/string.h>  //strlen
#include <linux/wait.h>  //等待队列头文件
#include <linux/time.h>

#define BUFSIZE 100


struct cdev_test_dev{
    dev_t dev_num;
    int major;
    int minor;
    struct cdev cdev_test;
    struct class* class_test;
    struct device* device_test;
    char kbuf[BUFSIZE];
};
struct cdev_test_dev dev1;




static int cdev_test_open(struct inode* inode,struct file* filp)
{
    printk("cdev_test_open\n");
    filp->private_data = &dev1; //将设备结构体指针保存在文件私有数据中
    printk("major = %d,minor = %d\n",dev1.major,dev1.minor);
    return 0;
}
static ssize_t cdev_test_read(struct file* filp,char __user* buf,size_t count,loff_t* offset)
{
    //offset参数是 filp->f_pos,内核自动传入的
    loff_t off = *offset;
    if(off > BUFSIZE)
    {
        return 0;
    }
    if(count > BUFSIZE -1 - off)
    {
        count = BUFSIZE - 1 - off;
    }
    copy_to_user(buf,dev1.kbuf + off,count);  //用于将内核空间的数据复制到用户空间,使用strlen而不是sizeof是为了去掉字符串末尾的'\0'
    printk("cdev_test_read\n");
    *offset += count;  // = "filp->f_pos = filp->f_pos + count"
    return count;
}
static ssize_t cdev_test_write(struct file* filp,const char __user* buf,size_t count,loff_t* offset)
{
    struct cdev_test_dev* dev = (struct cdev_test_dev*)filp->private_data;
    loff_t p = *offset;
    if(p > BUFSIZE)
    {
        return 0;
    }
    if(count > BUFSIZE -p)
    {
        count = BUFSIZE - p;
    }

    int ret = copy_from_user(dev->kbuf + p,buf,count);  //用于将用户空间的数据复制到内核空间
    if(ret != 0)
    {
        printk("write failed\n");
        return -1;
    }
    printk("cdev_test_write to dev1: %s\n",dev->kbuf);
    *offset += count;
    return count;
}
static int cdev_test_release(struct inode* inode,struct file* flip)
{
    printk("cdev_test_release\n");
    return 0;
}

loff_t cdev_test_llseek(struct file *filp, loff_t offset, int whence)
{
    loff_t new_pos = 0; // 建议改名为 new_pos 语义更准

    switch(whence)
    {
        case SEEK_SET: // 从开头开始偏移
            if (offset < 0 || offset > BUFSIZE) {
                return -EINVAL;
            }
            new_pos = offset;
            break;

        case SEEK_CUR: // 从当前位置开始偏移
            new_pos = filp->f_pos + offset;
            if (new_pos < 0 || new_pos > BUFSIZE) {
                return -EINVAL;
            }
            break;

        case SEEK_END: // 从末尾开始偏移
            new_pos = BUFSIZE + offset;
            if (new_pos < 0 || new_pos > BUFSIZE) {
                return -EINVAL;
            }
            break;

        default: // 处理非法 whence
            return -EINVAL;
    }

    // 更新文件指针的位置
    filp->f_pos = new_pos; 
    
    // 返回新的偏移位置给用户空间
    return new_pos;
}

static struct file_operations fops = {
    .owner = THIS_MODULE,
    .open = cdev_test_open,
    .read = cdev_test_read,
    .write = cdev_test_write,
    .release = cdev_test_release,
    .llseek = cdev_test_llseek,
}; //文件操作结构体

static int __init module_cdev_init(void)
{
    int ret;
    int major,minor;
    //1.申请设备号
    ret = alloc_chrdev_region(&dev1.dev_num,0,1,"cdev_test1");
    if(ret < 0)
    {
        goto err_alloc;
    }
    dev1.major = MAJOR(dev1.dev_num);
    dev1.minor = MINOR(dev1.dev_num);
    printk("major = %d,minor = %d\n",dev1.major,dev1.minor);
    //2.初始化字符设备结构体
    cdev_init(&dev1.cdev_test,&fops);
    dev1.cdev_test.owner = THIS_MODULE;
    //3.注册字符设备
    ret = cdev_add(&dev1.cdev_test,dev1.dev_num,1);
    if(ret < 0)
    {
        goto err_add;
    }else
    {
        printk("cdev_add success\n");
    }
    //4.自动创建设备节点(创建类和设备)
    dev1.class_test = class_create(THIS_MODULE,"cdev_test_class");
    if(IS_ERR(dev1.class_test))
    {
        ret = PTR_ERR(dev1.class_test);
        goto err_class;
    }
    dev1.device_test = device_create(dev1.class_test,NULL,dev1.dev_num,NULL,"cdev_test_device");
    if(IS_ERR(dev1.device_test))
    {
        ret = PTR_ERR(dev1.device_test);
        goto err_device;
    }
    return 0;
err_device:
    class_destroy(dev1.class_test);
err_class:
    cdev_del(&dev1.cdev_test);
err_add:
    unregister_chrdev_region(dev1.dev_num,1);
err_alloc:
    return ret;
}


static void __exit module_cdev_exit(void)
{
    //1.删除字符设备
    cdev_del(&dev1.cdev_test);
    //2.释放设备号
    unregister_chrdev_region(dev1.dev_num,1);
    //3.销毁设备节点和类
    device_destroy(dev1.class_test,dev1.dev_num);
    class_destroy(dev1.class_test);
    printk("cdev_test exit\n");
}

module_init(module_cdev_init);
module_exit(module_cdev_exit);

MODULE_LICENSE("GPL");

测试程序:

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main()
{
    int fd;
    char buf1[100];
    char buf2[100] = "hello world";

    fd = open("/dev/cdev_test_device",O_RDWR);  //阻塞,读写方式打开设备节点
    if(fd < 0)
    {
        perror("open failed");
        return -1;
    }
    printf("read from device...\n");
    write(fd,buf2,strlen(buf2));
    int ret = lseek(fd,0,SEEK_CUR);
    printf("cur = %d\n",ret);
    
    read(fd,buf1,strlen(buf2));
    printf("read buf : %s\n",buf1);

    ret = lseek(fd,0,SEEK_SET);
    read(fd,&buf1,strlen(buf2));  //从设备节点读取数据
    printf("read buf : %s\n",buf1);
    close(fd);
    return 0;
}

5.什么设备支持/不支持llseek

适合支持 llseek 的设备

  • 有内部缓冲区的字符设备
  • 模拟 EEPROM / Flash / RAM 区域的驱动
  • 有"逻辑地址空间"的设备
  • 需要随机访问的设备

不适合支持 llseek 的设备

  • 串口
  • 管道
  • FIFO
  • 流式采集设备
  • 一些只允许顺序读写的设备

因为这类设备本质上不是"存储介质",没有明确的"随机访问位置"。

比如串口驱动,你没法说"跳到串口流的第 100 个字节再读"。

相关推荐
xcbeyond2 小时前
Linux 磁盘挂载
linux·运维·服务器
steins_甲乙2 小时前
从0做一个小型内存泄露检测器(2): elf文件的动态链接
c++
charlie1145141912 小时前
通用GUI编程技术——图形渲染实战(二十八)——图像格式与编解码:PNG/JPEG全掌握
开发语言·c++·windows·学习·图形渲染·win32
LoneEon2 小时前
Kubernetes高可用集群部署教程
linux·docker·kubernetes
Ricky_Theseus2 小时前
C++静态库
开发语言·c++
洛水水2 小时前
【力扣100题】14.两数相加
c++·算法·leetcode
AlanW2 小时前
# Vcpkg使用总结2
c++
paeamecium2 小时前
【PAT甲级真题】- Insert or Merge (25)
数据结构·c++·算法·排序算法·pat考试·pat
小羽网安2 小时前
Linux 服务器如何进行安全加固?
linux·服务器·安全