很多初学 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 个字节再读"。