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
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 itswhenceargument - The difference between
SEEK_SET,SEEK_CUR, andSEEK_END - How to validate a new file position before accepting it
- How to add a working
llseek()method to theep_chardevdriver - 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()andread()toep_chardev - A Linux machine on kernel 6.x with kernel headers installed
The llseek() Method Prototype
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_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
- Compute the candidate new position based on
whenceand the driver’s notion of “current” and “end”. - Reject negative positions. A file position can never be negative.
- Reject unknown whence values by returning
-EINVAL. - Store the new position into
filp->f_posand 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”.
{
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
.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
$ 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:
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 invalidwhencevalues. - Allowing a negative final position, which corrupts later read/write calculations.
- Bounding
SEEK_ENDagainstdata_lenwhen writes should be allowed to extend the device. - Forgetting to assign
.llseekinfile_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(), andllseek(). - 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 usingSEEK_SET,SEEK_CUR, orSEEK_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(), andllseek().
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.
