The file_operations structure is the single most important data structure a Linux device driver author will ever touch. Every open, read, write, or ioctl a user process performs on your device file eventually lands on a function pointer inside this structure. In this free Linux device drivers course lecture, you will learn exactly how the file_operations structure works, how the kernel’s Virtual Filesystem Switch uses it to reach your driver, and how to correctly handle operations you choose not to support on a modern kernel.
If you have not yet gone through the first lecture on the misc character device driver framework, we recommend completing it before this one, since the examples here build directly on that foundation.
What Is the file_operations Structure?
The file_operations structure, defined in the kernel’s fs.h header, is essentially a table of function pointers, one for almost every file-related system call a process can issue: open, read, write, poll, mmap, release, ioctl, and many more. Think of it as an object-oriented “virtual method table” written in plain C. When you write a device driver, your real job is to fill in the entries of this table with your own functions, and leave the rest as NULL for operations you do not support.
This design is what makes the well-known Unix philosophy “everything is a file” actually work in practice. A device, a pipe, a socket, and a regular file all present the same file_operations-shaped interface to user space, even though what happens underneath each one is completely different.
A Minimal file_operations Example
Below is a small, self-contained file_operations table that only implements open and read, leaving every other operation unset. This is a perfectly valid and common pattern for simple drivers.
static int ep_open(struct inode *inode, struct file *filp)
{
pr_info("ep_driver: device opened\n");
return 0;
}
static ssize_t ep_read(struct file *filp, char __user *ubuf,
size_t count, loff_t *off)
{
/* driver-specific read logic goes here */
return 0;
}
static const struct file_operations ep_fops = {
.owner = THIS_MODULE,
.open = ep_open,
.read = ep_read,
};
How the Kernel Connects a Process to Your Driver
Understanding this connection is the real payoff of this lecture. When a user-space process calls open() on your device file, the kernel’s Virtual Filesystem Switch layer allocates an open-file object, commonly referred to internally as struct file, for that process. This structure holds a pointer to your driver’s file_operations table. From that point forward, every system call the process makes on that open file handle is routed through this pointer.
In simplified terms, whenever a process issues a file-related call, the VFS layer checks whether the corresponding pointer in your table is non-NULL, and if it is, calls it. If it is NULL, the VFS layer takes a fallback path instead of your driver code at all.
Why This Matters for Driver Authors
Because the VFS layer, not your driver, decides what happens when a pointer is missing, you must be deliberate about every entry you leave unset. An unset pointer is not simply “ignored”; it produces a specific, sometimes confusing, return code back to user space, which is exactly what the next section covers in detail.
Handling Unsupported file_operations Correctly
You are never required to implement every entry in the file_operations structure. Only fill in the operations your device genuinely supports. However, when a user-space process calls an operation you left NULL, the kernel does not fail silently; it returns a specific negative error code, and that code is not always the most intuitive one.
| Situation | Default Kernel Behaviour | Recommended Driver Practice |
|---|---|---|
| read left NULL | Returns EINVAL, which can be misleading | Implement it and return -ENOSYS explicitly if truly unsupported |
| llseek left NULL | May appear to “succeed” with a meaningless offset | Set llseek explicitly and mark the file non-seekable in open() |
| ioctl left NULL | Returns ENOTTY | This default is already clear; no extra handling needed |
The clearest, most unambiguous approach for an operation you deliberately do not support is to implement the function anyway and explicitly return -ENOSYS. This causes user space tools like perror() to report “Function not implemented,” which tells the calling developer precisely what happened instead of leaving them guessing.
The Seek Operation: A Special Case
Seeking rarely makes sense for hardware-backed devices, so most drivers should mark themselves explicitly as non-seekable rather than leaving the behaviour to chance. On modern kernels, the recommended approach in your open() callback is to call the kernel’s non-seekable helper so that any later seek attempt fails predictably with -ESPIPE instead of silently returning an unhelpful positive value.
static int ep_open(struct inode *inode, struct file *filp)
{
return nonseekable_open(inode, filp);
}
static const struct file_operations ep_fops = {
.owner = THIS_MODULE,
.open = ep_open,
.read = ep_read,
.llseek = no_llseek,
};
Current kernel trees already fall back to seek-rejecting behaviour by default when llseek is left unset, but explicitly marking your device as non-seekable in open() still remains the clearest, most portable, and most self-documenting choice, and is the pattern used across mainline drivers today.
Real-World Use Cases
Understanding the file_operations structure is essential far beyond simple demo drivers. Every real character driver in the mainline kernel, from input devices to sensor drivers to debugging interfaces, is built around this same table of function pointers. Network protocol drivers, tty drivers, and even filesystem implementations reuse the identical dispatch philosophy of exposing a table of operations that the VFS layer calls into.
Common Mistakes and Troubleshooting
- Assuming a NULL pointer means “no error.” A NULL entry still produces a real error code back to user space, and that code is not always intuitive.
- Leaving llseek unset without marking the device non-seekable. This can cause user tools to believe a seek succeeded when it did not.
- Confusing struct file with struct inode. struct file represents an open instance, while struct inode represents the underlying file identity; mixing them up is a frequent beginner error.
- Forgetting the owner field. Omitting
.owner = THIS_MODULEcan allow your module to be unloaded while it is still in use, leading to a crash.
Best Practices
- Only populate the file_operations entries your driver genuinely implements.
- Return
-ENOSYSexplicitly for operations you intentionally do not support. - Always mark non-seekable devices explicitly in your open() callback.
- Always set the owner field to THIS_MODULE.
- Keep each callback function short and focused on a single system call’s behaviour.
Performance Considerations
Dispatch through the file_operations table is a single indirect function call, which is extremely fast and adds negligible overhead compared to the work your driver actually performs. Performance in real drivers is almost always dominated by the logic inside your read, write, and ioctl callbacks, particularly around locking and data copying between kernel and user space, rather than by the dispatch mechanism itself.
Security Considerations
Every callback in your file_operations table is a trust boundary between user space and the kernel. Never trust buffer pointers, lengths, or offsets supplied by user space without validation. Always use the proper copy_to_user and copy_from_user helpers rather than direct pointer dereferencing, since user-supplied pointers must never be dereferenced directly inside kernel code.
Summary and Key Takeaways
- The file_operations structure is a table of function pointers mapping system calls to your driver’s implementation.
- The VFS layer checks each pointer before calling it, and a NULL pointer still produces a real error code to user space.
- Explicitly returning -ENOSYS gives the clearest possible signal to user-space developers.
- Seek behaviour should be explicitly configured rather than left to default assumptions.
Conclusion
The file_operations structure is the backbone connecting every device driver to the kernel’s Virtual Filesystem Switch, and understanding it deeply is what separates a driver author who copies examples from one who truly understands Linux device driver programming. With this foundation in place, you are ready to move on to more advanced topics in this free Linux kernel development course, including ioctl handling and blocking I/O.
Frequently Asked Questions
Q1. What is the purpose of the file_operations structure in Linux?
It maps file-related system calls like open, read, and write to the specific functions your device driver implements.
Q2. Do I have to implement every function in file_operations?
No. You only implement the operations your driver actually supports and leave the rest as NULL, or explicitly return -ENOSYS for clarity.
Q3. What happens if I leave the read pointer NULL?
The VFS layer returns a default error code, typically EINVAL, which can be misleading since it does not clearly indicate that the operation is unsupported.
Q4. What is the difference between struct file and struct inode?
struct file represents a single open instance of a file, while struct inode represents the underlying file’s identity on the filesystem, independent of how many times it is opened.
Q5. Why should I mark my device as non-seekable?
Most hardware devices have no meaningful concept of a seek position, so marking the device non-seekable prevents user space from being misled by a meaningless success return value.
Q6. Is the file_operations structure the same across all kernel versions?
Its overall purpose has stayed the same for decades, though specific member names and available fields have evolved slightly across kernel releases.
This lecture is part of EmbeddedPathashala’s free Linux kernel development and device driver course, built for students preparing for embedded systems and Linux kernel programming roles.
Next Lecture: Handling Unsupported Operations Back to Course Index
2 Comments