file_operations and the Character Driver Interface
struct file_operationsis the table of function pointers that is a character device driver: it is the contract between the Virtual File System (VFS) layer and the driver, the set of callbacks the kernel invokes when userspace performsopen,read,write,lseek,ioctl,mmap,poll, orcloseon the device’s/devnode. The kernel’s own VFS documentation calls it “the operations associated with a file” and lists each member as the implementation of a specific syscall —readis “called byread(2)and related system calls,”pollbyselect(2)/poll(2), and so on (VFS docs, v6.12). A driver fills in the handful of members it supports, leaves the rest NULL (the kernel treats NULL as “not supported,” returning a sensible default error), and registers the table so the VFS can dispatch through it. This note dissects the v6.12 table member-by-member, the relationship tostruct fileandstruct inode, how the VFS routes a syscall to the right table via the inode’si_cdev, theowner = THIS_MODULErefcount tie, blocking vs non-blocking behavior, and thecopy_to_user/copy_from_userboundary. The archetype and the registration plumbing are owned by Character Device Drivers and struct cdev and Character Device Registration.
Mental Model: a vtable the VFS calls into
Object-oriented languages have virtual method tables — a struct of function pointers that lets a generic caller invoke type-specific behavior without knowing the concrete type. struct file_operations is exactly that, hand-rolled in C: the VFS holds a generic struct file and, for every operation, calls file->f_op->whatever(...). The driver is the “subclass”; its file_operations is the vtable; file->f_op is the pointer that connects an open file to its vtable. This is one of several such tables in the VFS — inode_operations, super_operations, address_space_operations, dentry_operations — all surveyed in VFS Operation Tables; file_operations is the one a character driver author touches almost exclusively.
The crucial property is late binding by inode. The same read(2) syscall, on two different descriptors, can land in two completely different functions, because each struct file carries its own f_op. For a character-special file, that f_op is established at open time by following the inode’s device number to the driver’s cdev, as the dispatch section below traces.
flowchart LR subgraph PROC["Process"] FD["fd 3"] end subgraph KERN["Kernel objects"] FILE["struct file<br/>f_op ──┐<br/>f_pos, f_flags<br/>f_mode, private_data<br/>f_inode ──┐"] FOPS["struct file_operations<br/>(driver's vtable)<br/>.owner=THIS_MODULE<br/>.read .write .open ..."] INODE["struct inode<br/>i_fop = def_chr_fops<br/>i_rdev = dev_t<br/>i_cdev ──┐"] CDEV["struct cdev<br/>ops ──┐<br/>owner"] end FD -->|"fdget"| FILE FILE -->|"f_op"| FOPS FILE -->|"f_inode"| INODE INODE -->|"i_cdev (cached after 1st open)"| CDEV CDEV -->|"ops"| FOPS
How a descriptor reaches the driver’s vtable. What it shows: a file descriptor resolves to a struct file; the file’s f_op points at the driver’s file_operations; the file’s f_inode points at the struct inode, whose i_cdev (cached on first open) points at the struct cdev, whose ops is that same file_operations. The insight: file->f_op and inode->i_cdev->ops converge on one table — the inode supplies it once (via the device number), and the struct file then caches the pointer so per-operation dispatch is a single indirection, not a lookup.
The v6.12 table, member by member
From include/linux/fs.h at tag v6.12, verbatim, the table begins:
struct file_operations {
struct module *owner;
fop_flags_t fop_flags;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iopoll)(struct kiocb *kiocb, struct io_comp_batch *, unsigned int flags);
int (*iterate_shared) (struct file *, struct dir_context *);
__poll_t (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
/* ... get_unmapped_area, splice_*, fallocate, copy_file_range,
uring_cmd, ... continue here ... */
};owner — not a method but a struct module *. It exists “for internal VFS use” and should be set to THIS_MODULE in virtually every driver (kernel docs). Its purpose is the refcount tie discussed below: it lets the kernel pin your module while a file is open.
fop_flags — a bitmask of type fop_flags_t (an unsigned int __bitwise) added in the 6.x era; the field replaced a scattering of older per-fops booleans. The flags defined at v6.12 (verified verbatim against fs.h) are exactly six: FOP_BUFFER_RASYNC (bit 0), FOP_BUFFER_WASYNC (bit 1), FOP_MMAP_SYNC (bit 2), FOP_DIO_PARALLEL_WRITE (bit 3), FOP_HUGE_PAGES (bit 4), and FOP_UNSIGNED_OFFSET (bit 5) — note that the later FOP_DONTCACHE is not present at this tag. Most character drivers leave fop_flags zero; FOP_UNSIGNED_OFFSET matters for devices that want loff_t treated as an unsigned offset (so positions above 2^63 are valid rather than errors).
llseek — “called when the VFS needs to move the file position index.” Receives the struct file, an offset, and a whence (SEEK_SET/SEEK_CUR/SEEK_END), and returns the new absolute position. A driver that supports seeking commonly points this at default_llseek (handles all three whences against a size) or no_llseek/noop_llseek to refuse or no-op it. If llseek is NULL the VFS clears FMODE_LSEEK so lseek(2) returns -ESPIPE (verified in do_dentry_open, fs/open.c).
read / write — the byte-stream core, “called by read(2)/write(2) and related system calls.” Signatures: ssize_t read(struct file *, char __user *buf, size_t count, loff_t *ppos). The __user annotation marks buf as an untrusted userspace pointer that must never be dereferenced directly — only via copy_to_user/copy_from_user. The callback returns the number of bytes transferred (which may be fewer than count), 0 for EOF on read, or a negative errno. *ppos is the position cursor the callback must advance.
read_iter / write_iter — “possibly asynchronous read/write with iov_iter as destination/source.” These take a struct kiocb (kernel I/O control block, which carries the file, position, and async flags) and a struct iov_iter (an abstraction over scattered userspace buffers, a single buffer, or kernel buffers). They are the modern, vectored, async-capable form. A driver may implement either the simple read/write or the iter forms; vfs_read prefers read and falls back to wrapping read_iter via new_sync_read (see dispatch below). Most simple char drivers implement read/write; high-performance ones (and anything wanting io_uring or O_DIRECT) implement the iter forms.
poll — returns __poll_t (a bitmask of EPOLLIN/EPOLLOUT/etc.), “called by the VFS when a process wants to check if there is activity on this file and (optionally) go to sleep until there is activity. Called by the select(2) and poll(2) system calls.” The implementation registers the file’s wait queue(s) with poll_wait() and returns the currently-ready mask; this is how a char device participates in select/poll/epoll.
unlocked_ioctl / compat_ioctl — unlocked_ioctl is “called by the ioctl(2) system call” (the “unlocked” name is historical: it replaced an older ioctl that ran under the Big Kernel Lock). compat_ioctl is “called by the ioctl(2) system call when 32-bit system calls are used on 64-bit kernels,” so the driver can translate 32-bit argument layouts. These are the primary out-of-band control channel for a character device — everything that is not a byte-stream transfer (configure the device, query status, trigger an action) goes through ioctl with a driver-defined command number and an arbitrary unsigned long argument (usually a pointer to a struct the driver copy_from_users).
mmap — “called by the mmap(2) system call.” Lets userspace map device memory (a framebuffer, a DMA buffer, MMIO registers) directly into its address space; the driver typically calls remap_pfn_range or vm_insert_page to install the mapping into the supplied struct vm_area_struct.
open — “called by the VFS when an inode should be opened.” For a char device this is where you allocate/look up per-open state and stash it in filp->private_data. Note: for character devices the first f_op->open the VFS sees is the generic chrdev_open, which then swaps in your table and calls your open (next section).
flush — “called by the close(2) system call to flush a file.” Called on every close, once per descriptor, before the final release; rarely needed by char drivers.
release — “called when the last reference to an open file is closed.” This is the counterpart to open: it fires once, when the final descriptor referring to this struct file is closed (after the last dup/fork-shared copy goes away), and is where you free private_data. The asymmetry with flush (per-close vs once-per-struct file) trips people up.
fsync — “called by the fsync(2) system call.” fasync — “called by the fcntl(2) system call when asynchronous (non-blocking) mode is enabled,” used to deliver SIGIO when the device becomes ready. lock/flock — advisory file locking, almost never implemented by char drivers. get_unmapped_area — lets a driver choose the virtual address for an mmap (used by devices needing specific alignment).
Mechanical Walk-through: how the VFS dispatches a syscall to the right f_op
This is the load-bearing mechanism and worth tracing exactly, against v6.12 source.
struct inode and struct file — the two anchors. A struct inode represents the file as a filesystem object (its identity, permissions, type); a struct file (the open file description) represents one act of opening it (cursor, flags, the chosen op table). The inode is shared by all opens of the same file; each open gets its own struct file. The inode’s relevant fields for a char device are i_mode (with the S_IFCHR type bits), i_rdev (the device number dev_t), i_fop (the op table to use on open), and i_cdev (a cache of the resolved cdev). The struct file’s relevant fields (from fs.h, v6.12) are f_op (the live op table), f_inode (back-pointer to the inode), f_mode/f_flags (access and status flags), f_pos (position), f_count (reference count), f_mapping, and private_data. Their full definitions and lifecycles are in VFS Inode Object and The struct file and Open File Description.
Step A — the inode is born pointing at the generic stub. When the VFS instantiates the inode for a char-special node, init_special_inode() (fs/inode.c, v6.12) runs:
if (S_ISCHR(mode)) {
inode->i_fop = &def_chr_fops; /* generic char stub */
inode->i_rdev = rdev; /* remember the device number */
}So i_fop is not the driver’s table yet — it is def_chr_fops, defined in fs/char_dev.c as:
const struct file_operations def_chr_fops = {
.open = chrdev_open,
.llseek = noop_llseek,
};Step B — open(2) calls the stub’s open. do_dentry_open() (fs/open.c, v6.12) sets f->f_op = fops_get(inode->i_fop) (= def_chr_fops), then open = f->f_op->open; … error = open(inode, f); — calling chrdev_open.
Step C — chrdev_open finds the driver’s table and swaps it in. Verbatim core (fs/char_dev.c, v6.12):
p = inode->i_cdev; /* cached cdev? */
if (!p) {
kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx); /* look up by dev_t */
new = container_of(kobj, struct cdev, kobj); /* recover the cdev */
inode->i_cdev = p = new; /* cache on inode */
list_add(&inode->i_devices, &p->list);
}
...
fops = fops_get(p->ops); /* the DRIVER's file_operations + module pin */
replace_fops(filp, fops); /* filp->f_op = fops (after fops_put old) */
if (filp->f_op->open)
ret = filp->f_op->open(inode, filp); /* call the DRIVER's ->open */kobj_lookup(cdev_map, inode->i_rdev, …) consults the global device-number map (populated by cdev_add) and returns the cdev’s embedded kobject; container_of recovers the surrounding struct cdev; replace_fops overwrites filp->f_op with the driver’s ops. The line replace_fops expands (from fs.h) to do { fops_put(__file->f_op); BUG_ON(!(__file->f_op = (fops))); } while(0) — it drops the reference on the old table (the stub) and installs the new one. From here, every operation on this descriptor dispatches into the driver.
Step D — subsequent reads dispatch directly. read(2) → ksys_read → vfs_read (fs/read_write.c, v6.12). vfs_read checks FMODE_READ/FMODE_CAN_READ, runs access_ok(buf, count), then:
if (file->f_op->read)
ret = file->f_op->read(file, buf, count, pos);
else if (file->f_op->read_iter)
ret = new_sync_read(file, buf, count, pos); /* wrap iter form */
else
ret = -EINVAL;This is why a NULL read with a present read_iter still works, and why a table with neither yields -EINVAL on read.
The owner = THIS_MODULE refcount tie
fops_get(fops) is not a plain dereference — it is (from fs.h, v6.12):
#define fops_get(fops) ({ \
const struct file_operations *_fops = (fops); \
(((_fops) && try_module_get((_fops)->owner) ? (_fops) : NULL)); })It calls try_module_get(fops->owner), incrementing the module’s reference count, and only returns the table if that succeeds. The matching cdev_get/cdev_put in chrdev_open likewise try_module_get(p->owner) / module_put(owner). The effect: while any file is open against your driver, your module’s refcount is nonzero, so rmmod is refused (rmmod fails with “Module … is in use”). This is the safety interlock that prevents the kernel from unloading the code that an in-flight read is about to return into. If you forget owner = THIS_MODULE, try_module_get(NULL) succeeds vacuously (it pins nothing), the module can be unloaded with files still open, and the next dispatch jumps to freed memory — a use-after-free oops. This is the single most important non-obvious field in the table.
Blocking vs non-blocking, and the copy_*_user boundary
Blocking semantics. A read on a device with no data should, by default, put the caller to sleep until data arrives — typically wait_event_interruptible(my_wq, data_available) on a wait queue the driver wakes from an interrupt or another writer. But if the file was opened with O_NONBLOCK (visible as filp->f_flags & O_NONBLOCK), the callback must not sleep; it returns -EAGAIN immediately. A correct char driver checks this flag at the top of read/write. poll is the third leg: it lets select/poll/epoll ask “would a read block?” without actually reading, by registering the wait queue with poll_wait and returning an EPOLLIN mask when data is ready.
The user/kernel boundary. Userspace pointers (char __user *buf) live in a different, untrusted, potentially-unmapped address space; the kernel must never dereference them directly. The transfer primitives are (from include/linux/uaccess.h, v6.12):
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);Both return the number of bytes that could not be copied — 0 means full success, nonzero means the user pointer faulted partway. The kernel-doc states “if copying succeeds, the return value must be 0.” On a partial copy_from_user, the unfilled tail of the kernel destination is zero-padded (defense against leaking stale kernel data). The driver pattern is therefore if (copy_to_user(ubuf, kbuf, n)) return -EFAULT;. These functions also perform the necessary access checks and may sleep (they can fault in the user page), so they must be called from a context that may sleep — never from an atomic/interrupt context. vfs_read pre-validates the range with access_ok(buf, count) before calling the driver, but the per-byte fault safety still comes from copy_*_user itself.
Failure Modes and Common Misunderstandings
unlocked_ioctl vs the removed ioctl. Old code and old books reference a .ioctl member; it was removed long ago. v6.12 has only unlocked_ioctl and compat_ioctl. Setting the wrong one (or expecting a BKL) is a porting bug.
flush is not release. flush runs on every close(2) (once per descriptor copy); release runs once when the last reference to the struct file drops. Freeing private_data in flush double-frees when a descriptor was dup’d; free it in release.
Returning the wrong thing from read. Returning count unconditionally (without honoring *ppos) gives an infinite stream; returning -EFAULT as a positive number corrupts the byte count. EOF is 0, an error is negative, partial success is a positive count < requested.
Sleeping while holding a spinlock or in O_NONBLOCK mode. Calling copy_*_user or wait_event under a spinlock, or sleeping when O_NONBLOCK was requested, are classic char-driver bugs — the former can deadlock, the latter violates the non-blocking contract.
Assuming mmap gives coherent device memory for free. A naive mmap that just remap_pfn_ranges without the right page protections (uncached/write-combining for MMIO) yields subtly wrong reads from registers. Device mmap needs the correct vma->vm_page_prot.
Alternatives and When to Choose Them
For a single-node device, the misc framework lets you supply just a file_operations pointer (plus name and MISC_DYNAMIC_MINOR) and skips the explicit cdev/class boilerplate — see The miscdevice Framework. For pseudo-filesystems exposing per-driver attributes, sysfs/debugfs attribute files use their own simpler show/store callbacks rather than a full file_operations (see Device Attributes and sysfs Files). And note file_operations is reused far beyond char devices: block devices, pipes, sockets, and procfs/sysfs all populate one — it is the universal “what to do on a syscall against this open file” table, surveyed alongside its siblings in VFS Operation Tables.
Production Notes
The discipline that separates a robust driver’s file_operations from a toy one: (1) always set .owner = THIS_MODULE — the most common omission and the most dangerous; (2) never trust __user pointers — always go through copy_*_user, never cast-and-dereference; (3) honor O_NONBLOCK and implement poll if the device can block, so it composes with event loops; (4) make ioctl command numbers with the _IO/_IOR/_IOW/_IOWR macros (which encode direction and size) so the kernel can sanity-check them and 32/64-bit compat is tractable. In-tree examples worth reading in v6.12 are drivers/char/mem.c (the archetypal read/write/mmap table for /dev/mem, /dev/null, /dev/zero) and any input or hwmon driver for unlocked_ioctl and poll patterns. The teaching reference remains LDD3’s char-driver chapter for the shape, but its concrete APIs predate unlocked_ioctl, read_iter, and fop_flags and must be cross-checked against current source.
See Also
- Character Device Drivers — the archetype this table implements; the four-step lifecycle and a complete worked driver
- struct cdev and Character Device Registration — how the
file_operationsgets bound to a device number viacdev - VFS Operation Tables — the family of VFS vtables (
inode_operations,super_operations, …) this is one of - VFS Inode Object —
struct inode, includingi_rdev,i_fop,i_cdev - The struct file and Open File Description —
struct file, wheref_op,f_pos, andprivate_datalive - VFS File Object — the VFS-side view of
struct fileandf_opdispatch - File Descriptors and the fd Table — how a numeric
fdresolves to thestruct file - Open Flags and Access Modes —
O_NONBLOCK,f_flags,f_modesemantics referenced here - Major and Minor Numbers — the
dev_tthati_rdevcarries - Linux Device Drivers and Device Model MOC — parent MOC (§5, The Driver Archetypes)