Character Device File Operations Explained- Free Linux Device Drivers Course

 

PREV_LEC | NEXT_LEC

Character Device File Operations Explained

Free Linux Device Drivers Course — Chapter 4: Character Device Drivers

Lecture 3 of Chapter 4
Kernel 6.x Ready
Beginner Friendly

Topics Covered

character device file operations
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_operations table
  • 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 inode and struct 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.”

From System Call To Driver Function
User calls read()
VFS layer
Looks up file->f_op
Calls f_op->read()
Your driver code runs

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:

  1. The C library traps into the kernel for the read system call.
  2. The VFS finds the struct file object tied to that file descriptor.
  3. It checks file->f_op->read.
  4. If the pointer is set, the VFS calls it with the file, a user buffer pointer, a length, and a position.
  5. If the pointer is NULL, the VFS returns -EINVAL to 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_operations table.

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.

One Inode, Multiple Open Files
struct inode (/dev/ep-mem0)
struct file — Process A, position 0
struct file — Process B, position 128

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().

ep_pulse.c — Declaring the Table
static int ep_pulse_open(struct inode *inode, struct file *filp)
{
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 NULL is 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_operations is 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 inode is the file’s permanent identity, while struct file is one particular open instance of it.
  • The __user annotation 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.

PREV_LEC | NEXT_LEC

 

Leave a Reply

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