Character Device File Operations Explained
Free Linux Device Drivers Course — Chapter 4: Character Device Drivers
Topics Covered
struct file_operations linux kernel
struct inode vs struct file
free linux device drivers course
free linux kernel development course
free embedded systems course
What You Will Learn
This lecture is part of our free Linux device drivers course and continues the character device chapter. In earlier lectures you learned how major and minor numbers identify a device and how to sketch out a driver skeleton. Here we open up character device file operations in detail so that the next lecture’s registration code actually makes sense.
- Why every character driver needs a
struct file_operationstable - What each commonly used callback in that table is responsible for
- How the kernel walks from a user space system call down to your driver code
- The real difference between
struct inodeandstruct file, explained without jargon - Common mistakes beginners make when filling in a file operations table
Prerequisites
- Comfortable with basic loadable kernel module concepts (
module_init,module_exit) - A working Linux 6.x build environment with kernel headers installed
- Basic knowledge of C structures and function pointers
- Familiarity with major and minor numbers from the previous lecture in this free linux kernel development course
Why the File Operations Structure Exists
Linux follows the old UNIX idea that almost everything looks like a file: regular files on disk, directories, pipes, sockets, and hardware devices. A user program does not need to know whether read() is being served by an SSD or by a temperature sensor. It just calls read() on a file descriptor and expects bytes back. The piece that makes this possible on the driver side is struct file_operations. Think of it as a menu card your driver hands to the kernel: “here is what I know how to do, and here is the function to call for each of those things.”
Inside struct file_operations
The kernel defines struct file_operations in include/linux/fs.h. It is a large structure, but a beginner-friendly driver typically only fills in a handful of members. None of them are mandatory — if a callback is left as NULL, the kernel simply reports an error to user space for that particular operation instead of crashing.
| Member | Triggered By | Typical Purpose |
|---|---|---|
| open | open() | Prepare the device for use, allocate per-open state |
| release | close() | Clean up whatever open() set up |
| read | read() | Copy data from the device to user space |
| write | write() | Copy data from user space to the device |
| unlocked_ioctl | ioctl() | Handle device-specific control commands |
| mmap | mmap() | Map device memory directly into a process |
| poll | poll()/select() | Report whether the device is ready for I/O |
| fsync | fsync() | Flush any buffered data to the device |
On modern kernels the old plain ioctl field is gone; every character driver uses unlocked_ioctl instead, which is called without the old Big Kernel Lock so it can run concurrently on multiple CPUs. If you ever see a tutorial using .ioctl directly, treat it as outdated.
How the Kernel Dispatches a System Call
When a program calls a file-related system call, the kernel does not talk to your driver directly. It goes through a layer called the Virtual File System (VFS). Here is the simplified sequence for a read() call on an open character device:
- The C library traps into the kernel for the
readsystem call. - The VFS finds the
struct fileobject tied to that file descriptor. - It checks
file->f_op->read. - If the pointer is set, the VFS calls it with the file, a user buffer pointer, a length, and a position.
- If the pointer is
NULL, the VFS returns-EINVALto user space without ever entering driver code.
The exact error code returned for a missing callback depends on which operation was requested. A missing mmap callback commonly results in -ENODEV, while a missing write callback results in -EINVAL. This is why a minimal driver that only supports reading is still perfectly valid — user programs simply get a clean error if they try to write to it.
struct inode vs struct file: What Is The Difference?
Beginners often mix up these two structures because both seem to “represent a file.” They actually represent two different ideas.
- struct inode represents the file’s identity on the filesystem itself. It exists whether or not anyone currently has the file open. For a device node in
/dev, the inode is what carries the information that this file is a character device and which driver owns it. - struct file represents one particular instance of that file being open. It carries per-open state such as the current read/write position and a pointer back to your driver’s
file_operationstable.
If two different processes open the same device node, the kernel creates two separate struct file objects — each with its own read/write position — but both point back to the same underlying struct inode.
A Minimal File Operations Table
Below is an original, minimal example showing how a driver typically declares its file operations table. This is only the declaration — the next lecture in this free linux device drivers course wires it into a real, loadable driver using cdev_init() and device_create().
{
pr_info(“ep_pulse: device opened\n”);
return 0;
}static int ep_pulse_release(struct inode *inode, struct file *filp)
{
pr_info(“ep_pulse: device closed\n”);
return 0;
}static ssize_t ep_pulse_read(struct file *filp, char __user *buf,
size_t len, loff_t *pos)
{
/* real copy_to_user logic comes in the next lecture */
return 0;
}static const struct file_operations ep_pulse_fops = {
.owner = THIS_MODULE,
.open = ep_pulse_open,
.release = ep_pulse_release,
.read = ep_pulse_read,
};
Notice that ep_pulse_read receives buf as a char __user * pointer, not a plain char *. The __user annotation is a compile-time hint used by static analysis tools like sparse. It tells you, the driver author, that this pointer belongs to user space and must never be dereferenced directly from kernel code. We will use copy_to_user() and copy_from_user() to safely move data across that boundary in the next lecture.
Common Mistakes and Troubleshooting
| Mistake | Symptom | Fix |
|---|---|---|
Forgetting .owner = THIS_MODULE |
Module can be removed while a file is still open, causing a crash | Always set .owner in the fops table |
Using .ioctl instead of .unlocked_ioctl |
Build fails on kernel 6.x | Use .unlocked_ioctl only |
| Dereferencing the user buffer directly | Sparse warnings, possible kernel oops | Always use copy_to_user()/copy_from_user() |
Best Practices
- Declare your file operations table as
static const— it should never change at runtime. - Only implement the callbacks your device genuinely needs; leaving the rest
NULLis normal and expected. - Keep each callback function small and focused on one job.
Security Considerations
Every callback that touches a user-supplied pointer or length must validate it. Never trust the len argument passed to read() or write() blindly, and always route data across the kernel/user boundary through the dedicated copy functions rather than direct pointer access.
Summary and Key Takeaways
struct file_operationsis the table that connects user space system calls to your driver’s functions.- No callback is mandatory; unset ones simply return an error to user space.
struct inodeis the file’s permanent identity, whilestruct fileis one particular open instance of it.- The
__userannotation marks pointers that must be accessed only through safe copy functions.
Frequently Asked Questions
What happens if I don’t implement the write callback?
The kernel returns -EINVAL to any program that tries to write to your device, without ever calling your driver code.
Is unlocked_ioctl mandatory for every character driver?
No. It is only needed if your device supports custom control commands beyond simple read and write.
Can two processes open the same character device at once?
Yes. Each open() call creates a separate struct file with its own position, while both share the same underlying struct inode.
Why can’t I just cast the user buffer pointer and use it directly?
A user space pointer may not be valid or mapped the same way in kernel context. Direct access can crash the kernel or leak memory; copy_to_user() and copy_from_user() handle this safely.
Where is struct file_operations actually defined?
In include/linux/fs.h in the kernel source tree, alongside struct file and struct inode.
Does this free linux kernel development course cover ioctl in depth?
Yes, unlocked_ioctl is covered in a later lecture of this free linux device drivers course once the basic driver is registered and working.
Continue This Free Linux Device Drivers Course
Next, we register this file operations table with the kernel and create a real device node in /dev.
