VFS File Object

The file objectstruct file, defined in include/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 calls open(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 across dup() and fork(), and torn down by the last fput(). The crucial distinction: an inode is per-file (one per file on the filesystem, shared by every opener), whereas a struct file is per-open — two open() calls on the same path yield two independent struct 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 — an atomic_long_t reference count. As long as it is non-zero the object is alive; the last decrement to zero triggers teardown. get_file() increments it (with a WARN_ONCE if it was already zero, which would mean a use-after-free), and fput() 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 made atomic_long_t as part of the lockless-fget (SLAB_TYPESAFE_BY_RCU) work; older kernels placed it differently and used atomic_t. Pin this to v6.12.
  • f_mode — an fmode_t bitmask of FMODE_* 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 (an O_PATH open), and FMODE_OPENED. It is derived from the open flags by OPEN_FMODE(flags) and then refined in file_init_path().
  • f_flags — the raw O_* open flags (O_RDONLY/O_WRONLY/O_RDWR, O_APPEND, O_NONBLOCK, O_DIRECT, O_CLOEXEC semantics live on the fd not here, etc.). f_mode is the kernel’s distilled, fast-to-test form; f_flags is the userspace-supplied original. fcntl(fd, F_SETFL, ...) mutates f_flags after open. See Open Flags and Access Modes.
  • f_opconst 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_mappingstruct address_space *, the page-cache index for this file’s data. Normally it is f_inode->i_mapping, copied in at open time. A read() that misses the page cache, a write() that dirties pages, an mmap() of the file — all go through f_mapping. This is the seam with The Page Cache and address_space.
  • f_inode — a cached pointer to the struct inode (identical to f_path.dentry->d_inode). It exists so hot paths can reach the inode without dereferencing through f_path.dentry.
  • f_path — a struct 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, and f_path.mnt records 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 final fput.
  • f_posloff_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 with FMODE_ATOMIC_POS, concurrent updates are serialized by the embedded f_pos_lock mutex (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 opaque void * the filesystem or driver stashes per-open state in. Character drivers, for instance, almost universally allocate a per-open context in their ->open method and hang it here.
  • f_wb_err / f_sb_errerrseq_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 multiple fsync() 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-__fput work 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):

  1. SYSCALL_DEFINE3(read, …) calls ksys_read(fd, buf, count).
  2. ksys_read calls fdget_pos(fd). This looks the fd up in the per-process descriptor table to get the struct file *, takes a reference (or borrows one), and — for files with a real position — takes f_pos_lock so concurrent read/write on the same struct file serialize 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.”
  3. It reads the cursor: pos = *file_ppos(file). file_ppos() returns &file->f_pos for seekable files, or NULL for FMODE_STREAM files (pipes, sockets), in which case there is no position to advance.
  4. It calls vfs_read(file, buf, count, ppos).
  5. vfs_read enforces access: -EBADF if !(f_mode & FMODE_READ), -EINVAL if !(f_mode & FMODE_CAN_READ) — note these are exactly the f_mode bits, and FMODE_CAN_READ was set at open time only if f_op->read || f_op->read_iter existed. It validates the user buffer and clamps count to MAX_RW_COUNT.
  6. 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 only read_iter (the iterator-based form), so the common path is new_sync_read, which wraps the buffer in an iov_iter, builds a struct kiocb initialized from the file, and calls filp->f_op->read_iter(&kiocb, &iter).
  7. On success, ksys_read writes the advanced position back: file->f_pos = pos, then fdput_pos(f) drops f_pos_lock and the reference.

write(2) is the mirror image through vfs_writef_op->write or new_sync_writef_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. The struct file layout changed materially across recent releases (e.g. f_count moved to the front and became atomic_long_t; f_owner became a pointer; f_version was removed; the readahead/task-work union was reworked) as part of the lockless-fget and cache-line work. Reason: layout is volatile release-to-release. To resolve: diff include/linux/fs.h between 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_pos lives in struct file. Two unrelated open("/x") calls produce two struct files with independent cursors — reading from one does not move the other. Conversely, a dup()’d or fork()’d descriptor shares the same struct 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_DIRECT live in f_flags. Changing them with fcntl(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-wide struct file count hit files_stat.max_files (the fs.file-max sysctl). This is distinct from EMFILE (“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 with cat /proc/sys/fs/file-nr (allocated / free / max) and sysctl fs.file-max.
  • Leaked struct files. A descriptor that is never closed keeps f_count non-zero, pinning the inode, dentry, and the mount. The classic symptom is “device or resource busy” on umount: an open file holds an mntget reference on f_path.mnt, so the mount cannot be torn down until the final fput. lsof and /proc/<pid>/fd/ enumerate open files.
  • Use-after-free / spurious refcount bumps. Because filp_cachep is SLAB_TYPESAFE_BY_RCU, a freed struct file can be instantly recycled. Lockless fget readers must therefore re-validate after taking a reference; the get_file() WARN_ONCE("incremented from zero") fires if code bumps a count that already reached zero, signalling a genuine bug.
  • Deferred-close surprises. Because __fput runs as task-work on return to userspace (not synchronously inside close()/the last fput), a filesystem’s ->release and any fsync-on-close error can be observed slightly later than naively expected. The synchronous variant __fput_sync exists 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:

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_readf_op->read_iter (fs/read_write.c).

See Also