VFS File Object
The file object —
struct file, defined ininclude/linux/fs.h(torvalds/linux v6.12) — is the kernel’s in-memory representation of a single open instance of a file. It is created when a process callsopen(2)(and a handful of internal openers), it carries the cursor (f_pos), the access mode (f_mode), the open-time flags (f_flags), a pointer to the file’s method table (f_op), the page-cache anchor (f_mapping), and the(dentry, vfsmount)pair that locates the file in the mounted tree (f_path). It is reference-counted (f_count), shared acrossdup()andfork(), and torn down by the lastfput(). The crucial distinction: an inode is per-file (one per file on the filesystem, shared by every opener), whereas astruct fileis per-open — twoopen()calls on the same path yield two independentstruct files with independent positions.
This note is about the kernel object itself — what fields it holds and how read/write dispatch through it. The POSIX-level semantics of the same object (it is what POSIX calls the open file description, and how dup/fork make several file descriptors share one) are owned by The struct file and Open File Description; the small-integer file descriptors that point at struct files and the per-process table that holds them are owned by File Descriptors and the fd Table. Read those for the userspace-visible behavior; read this for the data structure.
Mental Model
Think of the open-file machinery as a three-level chain, narrowing from per-process to per-system:
flowchart LR subgraph P["Process A"] FDA["fd 3<br/>(int index)"] end subgraph Q["Process B (forked)"] FDB["fd 3<br/>(int index)"] end FDA --> FILE["struct file<br/>f_pos, f_flags, f_mode<br/>f_op, f_mapping, f_path<br/>f_count = 2"] FDB --> FILE FILE --> PATH["f_path<br/>(vfsmount + dentry)"] PATH --> DEN["dentry<br/>(name to inode)"] DEN --> INO["struct inode<br/>(per-file metadata,<br/>shared by all openers)"] FILE -.f_mapping.-> AS["address_space<br/>(page cache)"] AS -.-> INO FILE -.f_op.-> OPS["file_operations<br/>(read_iter, write_iter,<br/>llseek, mmap, ...)"]
The fd → file → inode chain. What it shows: small-integer file descriptors in each process index into a struct file; the struct file holds the per-open cursor and points (via f_path → dentry) at the single shared struct inode; f_op is the function-pointer table the VFS calls through, and f_mapping is the page-cache handle. The insight to take: the offset lives in struct file, not in the inode and not in the descriptor — that is exactly why a forked child shares its parent’s f_pos (both fds point at the same struct file, f_count == 2) but two independent open()s of the same path do not.
Anatomy of struct file (v6.12)
The v6.12 layout was deliberately reorganized for cache efficiency — the kernel-doc comment in fs.h annotates cache-line boundaries, and the structure is __randomize_layout-protected and 4-byte aligned. The verbatim definition (fs.h:1032):
struct file {
atomic_long_t f_count; /* reference count */
spinlock_t f_lock; /* protects f_ep, f_flags */
fmode_t f_mode; /* FMODE_* — hot-path access flags */
const struct file_operations *f_op; /* the method table */
struct address_space *f_mapping; /* page cache for this file's data */
void *private_data; /* fs/driver scratch pointer */
struct inode *f_inode; /* cached inode (== f_path.dentry->d_inode) */
unsigned int f_flags; /* O_* open flags */
unsigned int f_iocb_flags;/* precomputed iocb flags */
const struct cred *f_cred; /* credentials of the opener */
/* --- cacheline 1 boundary (64 bytes) --- */
struct path f_path; /* {vfsmount *mnt; dentry *dentry} */
union {
struct mutex f_pos_lock; /* serializes f_pos for FMODE_ATOMIC_POS + dirs */
u64 f_pipe; /* pipes reuse this space */
};
loff_t f_pos; /* current file position (cursor) */
void *f_security; /* LSM context (CONFIG_SECURITY) */
/* --- cacheline 2 boundary (128 bytes) --- */
struct fown_struct *f_owner; /* SIGIO/async owner (now a pointer)*/
errseq_t f_wb_err; /* per-open writeback error cursor */
errseq_t f_sb_err; /* per-open sb writeback error cursor */
struct hlist_head *f_ep; /* epoll hooks (CONFIG_EPOLL) */
union { /* overlapped: only one is live */
struct callback_head f_task_work;
struct llist_node f_llist;
struct file_ra_state f_ra; /* readahead state */
freeptr_t f_freeptr;/* SLAB_TYPESAFE_BY_RCU free pointer */
};
/* --- cacheline 3 boundary (192 bytes) --- */
} __randomize_layout __attribute__((aligned(4)));The fields that matter conceptually, walked one at a time:
f_count— anatomic_long_treference count. As long as it is non-zero the object is alive; the last decrement to zero triggers teardown.get_file()increments it (with aWARN_ONCEif it was already zero, which would mean a use-after-free), andfput()decrements it.file_count(x)reads it. Note that in v6.12 this is the first field in the struct — it was moved to the front and madeatomic_long_tas part of the lockless-fget(SLAB_TYPESAFE_BY_RCU) work; older kernels placed it differently and usedatomic_t. Pin this to v6.12.f_mode— anfmode_tbitmask ofFMODE_*flags that the kernel checks on every hot-path operation:FMODE_READ,FMODE_WRITE,FMODE_CAN_READ/FMODE_CAN_WRITE(set at open time iff the operation table actually provides a method),FMODE_LSEEK,FMODE_STREAM(no seekable position, e.g. pipes),FMODE_PATH(anO_PATHopen), andFMODE_OPENED. It is derived from the open flags byOPEN_FMODE(flags)and then refined infile_init_path().f_flags— the rawO_*open flags (O_RDONLY/O_WRONLY/O_RDWR,O_APPEND,O_NONBLOCK,O_DIRECT,O_CLOEXECsemantics live on the fd not here, etc.).f_modeis the kernel’s distilled, fast-to-test form;f_flagsis the userspace-supplied original.fcntl(fd, F_SETFL, ...)mutatesf_flagsafter open. See Open Flags and Access Modes.f_op—const struct file_operations *, the per-open method table. This is the heart of VFS dispatch: every operation on the open file (read,write,llseek,mmap,ioctl,poll,fsync,release, …) is a call through this pointer. The concrete filesystem (or device driver) supplies the table; the VFS never reaches into filesystem internals directly. Detailed in VFS Operation Tables.f_mapping—struct address_space *, the page-cache index for this file’s data. Normally it isf_inode->i_mapping, copied in at open time. Aread()that misses the page cache, awrite()that dirties pages, anmmap()of the file — all go throughf_mapping. This is the seam with The Page Cache and address_space.f_inode— a cached pointer to thestruct inode(identical tof_path.dentry->d_inode). It exists so hot paths can reach the inode without dereferencing throughf_path.dentry.f_path— astruct path, which is exactly{ struct vfsmount *mnt; struct dentry *dentry; }(path.h). The dentry binds the file to its name and inode in the dcache; the vfsmount records which mount the file was opened through. Both halves matter: a single inode can be reachable through several bind mounts, andf_path.mntrecords the specific one — which is what makes/proc/<pid>/fd/<n>symlinks resolve to the right mount-qualified path. The kernel takes a reference on both (path_get) at open and drops it (path_put, i.e.dput+mntput) at the finalfput.f_pos—loff_t(a signed 64-bit offset), the read/write cursor. This is the per-open piece of state: it lives here, not in the inode, which is the entire reason independent opens have independent positions. For regular files and directories opened withFMODE_ATOMIC_POS, concurrent updates are serialized by the embeddedf_pos_lockmutex (see below).f_cred— the credentials (struct cred) captured at open time. Later permission checks on the open file use these, not the caller’s current credentials — which is why an already-open fd keeps working even if the process later drops privileges.private_data— an opaquevoid *the filesystem or driver stashes per-open state in. Character drivers, for instance, almost universally allocate a per-open context in their->openmethod and hang it here.f_wb_err/f_sb_err—errseq_t“error cursors” for writeback-error reporting.fsync()/close()compare the file’s snapshot against the mapping’s current error sequence so that a writeback failure is reported exactly once to each open file description, even across multiplefsync()calls. This is the modern fix for the “lost write error after the first fsync” class of bugs (LWN: Improved block-layer error handling). See fsync fdatasync and Durability.- The trailing union (
f_task_work/f_llist/f_ra/f_freeptr) overlaps four mutually-exclusive uses to save space: the deferred-__fputwork item, the delayed-fput list node, the readahead state for files that actually read, and the SLAB free pointer used while the object sits on the type-safe-by-RCU free list.
How read/write dispatch through f_op
The single most important behavior of struct file is that it is the dispatch vehicle: the VFS turns a syscall into a method call through f_op. Tracing read(2) end to end in v6.12 (fs/read_write.c):
SYSCALL_DEFINE3(read, …)callsksys_read(fd, buf, count).ksys_readcallsfdget_pos(fd). This looks the fd up in the per-process descriptor table to get thestruct file *, takes a reference (or borrows one), and — for files with a real position — takesf_pos_lockso concurrentread/writeon the samestruct fileserialize on the cursor. This is the mechanism behind POSIX’s “concurrent reads/writes on a shared open file description are atomic w.r.t. the offset.”- It reads the cursor:
pos = *file_ppos(file).file_ppos()returns&file->f_posfor seekable files, orNULLforFMODE_STREAMfiles (pipes, sockets), in which case there is no position to advance. - It calls
vfs_read(file, buf, count, ppos). vfs_readenforces access:-EBADFif!(f_mode & FMODE_READ),-EINVALif!(f_mode & FMODE_CAN_READ)— note these are exactly thef_modebits, andFMODE_CAN_READwas set at open time only iff_op->read || f_op->read_iterexisted. It validates the user buffer and clampscounttoMAX_RW_COUNT.- The actual dispatch:
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(...); else ret = -EINVAL;. Modern filesystems implement onlyread_iter(the iterator-based form), so the common path isnew_sync_read, which wraps the buffer in aniov_iter, builds astruct kiocbinitialized from the file, and callsfilp->f_op->read_iter(&kiocb, &iter). - On success,
ksys_readwrites the advanced position back:file->f_pos = pos, thenfdput_pos(f)dropsf_pos_lockand the reference.
write(2) is the mirror image through vfs_write → f_op->write or new_sync_write → f_op->write_iter. The takeaway: the VFS supplies the generic scaffolding (lookup, locking, access checks, position management) and the concrete filesystem supplies only the f_op methods. ext4, XFS, a character device, a pipe, and /proc all present the same struct file shape to the upper layers; they differ only in which file_operations table f_op points at.
Lifecycle: birth at open(), death at the last fput()
A struct file is allocated from a dedicated slab cache, filp_cachep, created in files_init() with the SLAB_TYPESAFE_BY_RCU flag — meaning a freed struct file may be immediately reused for another struct file, which is what lets fget-by-RCU readers probe a descriptor locklessly and re-validate (fs/file_table.c).
Allocation. alloc_empty_file(flags, cred) first enforces the system-wide limit: if get_nr_files() >= files_stat.max_files and the caller lacks CAP_SYS_ADMIN, it does an expensive precise percpu_counter_sum_positive() check and returns -ENFILE (“file-max limit reached”) if truly over. Otherwise it kmem_cache_zallocs a zeroed object and calls init_file(), which stashes the credentials, runs security_file_alloc() (the LSM hook), initializes f_lock and f_pos_lock, sets f_flags = flags and f_mode = OPEN_FMODE(flags), and — deliberately last, because of SLAB_TYPESAFE_BY_RCU — does atomic_long_set(&f->f_count, 1). The comment is explicit: “initialize f_count last … fget-rcu pattern users need to be able to handle spurious refcount bumps.”
Binding to a path. Once the path has been resolved, file_init_path(file, path, fop) wires the object to its file: it copies f_path = *path, sets f_inode and f_mapping from the dentry’s inode, samples the writeback-error cursors, sets FMODE_CAN_READ/FMODE_CAN_WRITE only if the operation table provides the corresponding method, sets f_iocb_flags, marks FMODE_OPENED, and finally assigns file->f_op = fop. The driver/filesystem ->open method runs around here and may overwrite f_op or stash private_data.
Reference sharing. get_file() (or dup/fork installing the same struct file under a second fd) bumps f_count. Crucially, sharing the struct file means sharing f_pos — see fork dup and Shared File Offsets.
Teardown. fput(file) does atomic_long_dec_and_test(&file->f_count); if that was the last reference, the real cleanup must run, but not necessarily immediately. For a normal task, fput queues __fput as task work (task_work_add(..., TWA_RESUME)) so it runs when the task returns to userspace — this avoids doing the potentially-sleeping teardown in awkward contexts. For kernel threads / interrupt context it falls back to a per-CPU delayed_fput_list flushed by a workqueue. The actual guts, __fput(), run the cleanup chain in a fixed order: fsnotify_close(), eventpoll_release(), locks_remove_file(), security_file_release(), then f_op->release(inode, file) (the filesystem/driver’s chance to free private_data), fops_put(), dropping the f_owner, dput(dentry) and mntput(mnt) (releasing the f_path references), and finally file_free() back to the slab. The order is load-bearing — the comment in __fput insists eventpoll_release() must be first in the chain.
Uncertain
Verify: the exact field set and ordering shown above is from v6.12
include/linux/fs.h. Thestruct filelayout changed materially across recent releases (e.g.f_countmoved to the front and becameatomic_long_t;f_ownerbecame a pointer;f_versionwas removed; the readahead/task-work union was reworked) as part of the lockless-fgetand cache-line work. Reason: layout is volatile release-to-release. To resolve: diffinclude/linux/fs.hbetween the specific tag in question and v6.12 before quoting field offsets for any other kernel. uncertain
file vs inode: per-open vs per-file (the core distinction)
This is the single most common point of confusion, so it is worth stating precisely. A struct inode (VFS Inode Object) is the canonical in-memory representation of a file — its mode, owner, size, timestamps, link count, and the pointers into on-disk data. There is exactly one live inode per file on a given filesystem, no matter how many processes have it open. A struct file is the representation of one act of opening that file. Consequences:
- Position is per-open.
f_poslives instruct file. Two unrelatedopen("/x")calls produce twostruct files with independent cursors — reading from one does not move the other. Conversely, adup()’d orfork()’d descriptor shares the samestruct file, so the cursors move together. The offset is therefore neither in the descriptor (too narrow) nor in the inode (too broad). - Access mode is per-open. Opening read-only vs read-write is a property of the
struct file(f_mode), not the inode. The same inode can be open read-only in one process and read-write in another. - Flags are per-open.
O_APPEND,O_NONBLOCK,O_DIRECTlive inf_flags. Changing them withfcntl(F_SETFL)affects only that open file description. - Metadata is per-file. File size, timestamps, permissions, link count are in the inode and are shared by all openers —
ftruncate()on one fd is visible through every other open of the same file because they all reach the same inode.
A useful mnemonic: the inode answers “what is this file?”; the struct file answers “how is this process currently looking at it?”
Failure Modes and Diagnostics
ENFILE(“Too many open files in system”). The system-widestruct filecount hitfiles_stat.max_files(thefs.file-maxsysctl). This is distinct fromEMFILE(“Too many open files”), which is the per-process descriptor limit (RLIMIT_NOFILE) and is enforced in the fd-table layer, not here — see File Descriptors and the fd Table. Diagnose withcat /proc/sys/fs/file-nr(allocated / free / max) andsysctl fs.file-max.- Leaked
struct files. A descriptor that is never closed keepsf_countnon-zero, pinning the inode, dentry, and the mount. The classic symptom is “device or resource busy” onumount: an open file holds anmntgetreference onf_path.mnt, so the mount cannot be torn down until the finalfput.lsofand/proc/<pid>/fd/enumerate open files. - Use-after-free / spurious refcount bumps. Because
filp_cachepisSLAB_TYPESAFE_BY_RCU, a freedstruct filecan be instantly recycled. Locklessfgetreaders must therefore re-validate after taking a reference; theget_file()WARN_ONCE("incremented from zero")fires if code bumps a count that already reached zero, signalling a genuine bug. - Deferred-close surprises. Because
__fputruns as task-work on return to userspace (not synchronously insideclose()/the lastfput), a filesystem’s->releaseand anyfsync-on-close error can be observed slightly later than naively expected. The synchronous variant__fput_syncexists for the narrow cases (e.g. unmount paths in kernel threads) that must not defer.
Alternatives and Boundaries
There is no “alternative” to struct file inside Linux — it is the open-file abstraction, and every openable thing (regular files, directories, pipes, sockets, devices, eventfd/timerfd/signalfd, epoll instances, io_uring rings) is represented by one, differing only in its f_op. The interesting boundaries are conceptual:
- vs the file descriptor. The fd is a per-process
int; thestruct fileis the shared object it indexes.dupcreates a new fd pointing at the samestruct file; opening the same path twice creates twostruct files. Owned by File Descriptors and the fd Table and The struct file and Open File Description. - vs the inode. Per-open vs per-file, as above. Owned by VFS Inode Object.
- vs
address_space.f_mappingpoints at the page-cache index; the file is the handle, the address_space is where the data actually lives. Owned by The Page Cache and address_space.
Production Notes
Reasoning about struct file lifetime is routine in real kernel work. Two recurring themes: (1) the lockless fget path — the v6.12 SLAB_TYPESAFE_BY_RCU filp_cachep plus the “init f_count last” rule exist precisely so high-rate fd lookups (think a busy server doing millions of recv/read per second) do not bounce a cache line on a shared lock; the cost is that callers must tolerate seeing a recycled object and re-check, which is why the lockless fget helpers (get_file_rcu, get_file_active) exist alongside plain get_file. (2) The task-work-deferred __fput is why tools that strace a close() sometimes see the heavy work (writeback flush, ->release) attributed to a later point — it is the kernel choosing not to do sleeping cleanup at an inconvenient moment. The iterator-based read_iter/write_iter methods are the modern data path; the older read/write callbacks survive only for filesystems and drivers that never converted, which is exactly why vfs_read tries f_op->read first and falls back to new_sync_read → f_op->read_iter (fs/read_write.c).
See Also
- The struct file and Open File Description — the POSIX-level view of this same object;
dup/forksharing semantics - File Descriptors and the fd Table — the per-process
int→struct fileindirection - VFS Inode Object — the per-file counterpart (metadata, shared by all opens)
- VFS Operation Tables —
file_operationsand the other method tablesf_opis one of - VFS Superblock Object — the mounted-instance object that ultimately owns the inode this file points at
- The Page Cache and address_space — what
f_mappingindexes - Open Flags and Access Modes — how
f_flags/f_modeare derived and changed - fork dup and Shared File Offsets — the consequence of sharing one
struct file - The Virtual File System Layer — the layer this object lives in
- MOC: Linux Filesystems and VFS MOC