Linux Character Driver llseek Method- Free Linux Device Drivers Course

PREV_LEC | NEXT_LEC

Linux Character Driver llseek Method

Free Linux Kernel Development Course — Chapter 4.9

The linux character driver llseek method lets an application move the read/write cursor to any position inside your device, the same way lseek() works on a regular file. In this lecture of the free linux device drivers course, we finish the ep_chardev driver by adding a correct, modern llseek() implementation that works with SEEK_SET, SEEK_CUR, and SEEK_END.

Keywords covered in this lecture

llseek file operation
SEEK_SET SEEK_CUR SEEK_END
file position kernel
free linux device drivers course
kernel 6.x

What You Will Learn

  • The prototype of the llseek() file operation and its whence argument
  • The difference between SEEK_SET, SEEK_CUR, and SEEK_END
  • How to validate a new file position before accepting it
  • How to add a working llseek() method to the ep_chardev driver
  • How to test seeking behavior from the command line

Prerequisites

  • Lectures 4.7 and 4.8 of this free linux kernel development course, which added write() and read() to ep_chardev
  • A Linux machine on kernel 6.x with kernel headers installed

The llseek() Method Prototype

loff_t (*llseek) (struct file *filp, loff_t offset, int whence);

offset is a signed value describing how far to move, and whence tells the driver what that offset is relative to. The function returns the new absolute file position, or a negative error code such as -EINVAL if the resulting position would be invalid.

The Three whence Values

Seek Reference Points

SEEK_SET: from byte 0

SEEK_CUR: from current pos

SEEK_END: from last byte
Constant Meaning Example
SEEK_SET Position relative to the beginning of the device lseek(fd, 10, SEEK_SET) moves to byte 10
SEEK_CUR Position relative to the current offset lseek(fd, 5, SEEK_CUR) moves 5 bytes forward
SEEK_END Position relative to the end of stored data lseek(fd, -2, SEEK_END) moves to 2 bytes before the end

Steps Inside an llseek() Implementation

  1. Compute the candidate new position based on whence and the driver’s notion of “current” and “end”.
  2. Reject negative positions. A file position can never be negative.
  3. Reject unknown whence values by returning -EINVAL.
  4. Store the new position into filp->f_pos and return it.

Full Example: Adding llseek() to ep_chardev

This continues the same ep_chardev struct used in the write and read lectures, using data_len as the notion of “end of file”.

static loff_t ep_llseek(struct file *filp, loff_t offset, int whence)
{
struct ep_chardev *dev = filp->private_data;
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 = dev->data_len + offset;
break;
default:
return -EINVAL;
}if (new_pos < 0 || new_pos > EP_BUF_SIZE)
return -EINVAL;filp->f_pos = new_pos;
return new_pos;
}

Notice that the upper bound check uses EP_BUF_SIZE, the total capacity of the device, rather than data_len. This allows an application to seek past the currently written data and then write() at that new offset, exactly like a regular sparse file.

Registering the llseek Callback

static const struct file_operations ep_fops = {
.owner = THIS_MODULE,
.open = ep_open,
.release = ep_release,
.write = ep_write,
.read = ep_read,
.llseek = ep_llseek,
};

If you omit .llseek entirely, the kernel’s default handler is used, which for most simple character devices returns -ESPIPE (illegal seek). Explicitly assigning your own function is what makes the device seekable.

Building and Testing the Driver

$ make
$ sudo insmod ep_chardev.ko
$ sudo mknod /dev/ep_chardev c <major> 0
$ printf “ABCDEFGH” | sudo tee /dev/ep_chardev
$ sudo dd if=/dev/ep_chardev bs=1 skip=3 count=2 2>/dev/null

Expected output:

ABCDEFGH
DE
$ dmesg | tail -n 2
[ 210.114532] ep_chardev: llseek to offset 3 (SEEK_SET)
[ 210.114701] ep_chardev: read 2 bytes at offset 3

dd uses skip internally to call lseek() before reading, so seeing DE (the 4th and 5th bytes) confirms the seek moved the cursor correctly.

llseek() Return Values

Return Value Meaning
Non-negative loff_t The new absolute file position
-EINVAL Requested position is negative, out of range, or whence is unknown
-ESPIPE Default kernel behavior when a driver does not support seeking

Common Mistakes

  • Forgetting the default: return -EINVAL; case, silently accepting invalid whence values.
  • Allowing a negative final position, which corrupts later read/write calculations.
  • Bounding SEEK_END against data_len when writes should be allowed to extend the device.
  • Forgetting to assign .llseek in file_operations, leaving the device non-seekable.

Best Practices

  • Always validate the computed position before storing it.
  • Decide clearly whether your device’s capacity or its current data length is the correct upper bound, and be consistent across read(), write(), and llseek().
  • Return the new position on success, matching standard lseek() semantics that user-space tools expect.

Performance and Security Considerations

llseek() is a cheap, in-memory operation, so performance is rarely a concern. The real risk is security: without careful bounds checking, a crafted offset (including large or negative values causing integer overflow) can push f_pos outside the valid range, which later read or write calls might trust blindly. Always re-validate f_pos in read() and write() as well, rather than assuming llseek() was the only entry point that set it.

Summary

  • The llseek() file operation moves the file position cursor using SEEK_SET, SEEK_CUR, or SEEK_END.
  • Always validate the resulting position before storing it in filp->f_pos.
  • Without a registered llseek(), most character devices are treated as non-seekable.
  • Keep bounds checking consistent across read(), write(), and llseek().

FAQ

What happens if I don’t implement llseek() at all?

The kernel falls back to a default handler that typically returns -ESPIPE, meaning tools that try to seek on your device will get an “illegal seek” error.

What is the difference between SEEK_SET and SEEK_CUR?

SEEK_SET moves to an absolute position from the start of the device, while SEEK_CUR moves relative to wherever the cursor currently is.

Can offset be negative in llseek()?

Yes, especially with SEEK_CUR or SEEK_END, where a negative offset moves the cursor backward. The resulting absolute position must still not be negative.

Why does dd use llseek() internally?

Options like skip and seek in dd translate directly into lseek() calls before dd starts reading or writing, so a correct llseek() is required for those options to work.

Should SEEK_END use data_len or the full buffer size?

It depends on your device’s semantics. Using data_len treats the device like a regular file with an end-of-content marker, while using the full buffer size allows seeking into still-empty space.

Conclusion

With llseek() implemented, the ep_chardev driver now supports the full core file operation set: open, release, read, write, and seek, exactly like a real device driver used in production kernels. This completes the character device drivers series in this free embedded linux course. Later chapters in this free linux kernel development course move on to sysfs, procfs, and interrupt handling.

Continue the Free Linux Kernel Development Course

More free embedded systems course and free linux development course lectures are on the way.

PREV_LEC | NEXT_LEC

 

Leave a Reply

Your email address will not be published. Required fields are marked *