VFS Superblock Object

The superblock objectstruct super_block, defined in include/linux/fs.h (torvalds/linux v6.12) — is the kernel’s in-memory representation of one mounted filesystem instance. Where the on-disk “superblock” is a fixed block of bytes describing the filesystem’s geometry, the VFS struct super_block is the live runtime object the kernel builds at mount time to manage that instance: it points at the filesystem’s method table (s_op), the root dentry of the mounted tree (s_root), the backing block device (s_bdev) or pseudo-source, the filesystem’s private state (s_fs_info), the block size and maximum file size, and the lists of inodes belonging to the instance (s_inodes). One block device mounted twice at two paths normally yields one super_block (the second mount shares it); two different filesystems mounted yield two. It is created by the get_tree_* family calling the filesystem’s fill_super, and destroyed at unmount via kill_sbgeneric_shutdown_super.

This note is about the kernel object — its fields, how it is born at mount and how statfs and teardown work through it. The registration of a filesystem type and the broader mount mechanism (the fs_context/fsopen/fsmount path, mount propagation, vfsmount) are owned by Filesystem Registration and Mounting Internals and The New Mount API; the method tables in general are owned by VFS Operation Tables. Read those for the surrounding machinery; read this for the object that anchors a mounted instance.

Mental Model

A mounted filesystem is a small graph hanging off one struct super_block:

flowchart TB
  FST["file_system_type<br/>(e.g. ext4)<br/>.mount/.init_fs_context, .kill_sb"]
  FST -->|"fill_super at mount"| SB["struct super_block<br/>s_op, s_root, s_dev<br/>s_blocksize, s_maxbytes<br/>s_fs_info, s_inodes"]
  SB -->|s_root| ROOT["root dentry<br/>(top of mounted subtree)"]
  SB -->|s_op| SOP["super_operations<br/>(alloc_inode, statfs,<br/>sync_fs, put_super, ...)"]
  SB -->|s_bdev| BD["block_device<br/>(or NULL for pseudo fs)"]
  SB -->|s_fs_info| PRIV["fs-private state<br/>(e.g. ext4_sb_info)"]
  SB -->|s_inodes| INODES["all live inodes<br/>of this instance"]
  ROOT --> INO["root inode"]
  INODES -.-> INO
  BD -.-> DISK["on-disk superblock<br/>+ filesystem data"]

One mounted instance, anchored by its superblock. What it shows: a file_system_type (registered once per filesystem driver) produces a struct super_block at mount time via fill_super; the superblock then ties together the root dentry (s_root), the method table (s_op), the backing device (s_bdev), the driver’s private state (s_fs_info), and the list of all this instance’s inodes. The insight to take: the superblock is the per-mount-instance object — the place the kernel hangs everything that is true of “this ext4 on /dev/sda1 mounted here,” as opposed to everything true of “ext4 the driver” (which is the file_system_type) or “one file” (which is the inode).

Anatomy of struct super_block (v6.12)

The structure is large; the fields that carry the concept, walked one at a time (fs.h:1253):

struct super_block {
    struct list_head      s_list;        /* node on the global super_blocks list */
    dev_t                 s_dev;         /* identifying device number (search key) */
    unsigned char         s_blocksize_bits;
    unsigned long         s_blocksize;   /* block size in bytes */
    loff_t                s_maxbytes;    /* max file size this fs supports */
    struct file_system_type *s_type;     /* the driver that owns this instance */
    const struct super_operations *s_op; /* the method table */
    ...
    unsigned long         s_flags;       /* SB_RDONLY, SB_NOSUID, SB_ACTIVE, ... */
    unsigned long         s_magic;       /* fs magic number (e.g. EXT4_SUPER_MAGIC) */
    struct dentry         *s_root;       /* root dentry of the mounted subtree */
    struct rw_semaphore   s_umount;      /* serializes mount/unmount/remount */
    int                   s_count;       /* lifetime refcount */
    atomic_t              s_active;      /* active (mounted) refcount */
    ...
    struct list_head      s_mounts;      /* mounts using this sb (VFS-only) */
    struct block_device   *s_bdev;       /* backing block device, or NULL */
    struct file           *s_bdev_file;  /* file handle on the bdev (v6.12) */
    struct backing_dev_info *s_bdi;      /* writeback/readahead congestion info */
    struct hlist_node     s_instances;   /* node on s_type->fs_supers */
    ...
    void                  *s_fs_info;    /* FILESYSTEM PRIVATE info */
    u32                   s_time_gran;   /* timestamp granularity (ns) */
    ...
    char                  s_id[32];      /* informational name (e.g. "sda1") */
    uuid_t                s_uuid;        /* filesystem UUID */
    ...
    struct user_namespace *s_user_ns;    /* user-ns for uid/gid/xattr interpretation */
    struct list_lru       s_dentry_lru;  /* per-sb dentry shrinker list */
    struct list_lru       s_inode_lru;   /* per-sb inode shrinker list */
    ...
    spinlock_t            s_inode_list_lock ____cacheline_aligned_in_smp;
    struct list_head      s_inodes;      /* ALL inodes of this instance */
    spinlock_t            s_inode_wblist_lock;
    struct list_head      s_inodes_wb;   /* inodes under writeback */
} __randomize_layout;
  • s_dev — a dev_t that identifies the instance and serves as the lookup key for “do we already have a superblock for this device?” For block-backed filesystems it is the device number of s_bdev; for anonymous/pseudo filesystems it is an anonymous device number allocated by set_anon_super from a dedicated minor pool. It is what shows up as the st_dev of every file on the filesystem, and what makes inode numbers unique only within a device.
  • s_blocksize / s_blocksize_bits — the filesystem block size in bytes and its log2. Set by fill_super (commonly via sb_set_blocksize()), it governs how the page cache and block layer chunk I/O for this instance.
  • s_maxbytes — the largest file size the filesystem supports. alloc_super initializes it to MAX_NON_LFS (the non-large-file limit, ~2 GiB) and fill_super raises it to the real value (e.g. ext4’s multi-TiB limit). vfs_get_tree warns if a filesystem ever sets it negative.
  • s_typestruct file_system_type *, a back-pointer to the driver (ext4, xfs, tmpfs, …). The superblock is linked onto s_type->fs_supers via s_instances so the kernel can enumerate all instances of a given filesystem type.
  • s_opconst struct super_operations *, the method table the VFS calls through for whole-filesystem operations (allocate/free an inode, write a dirty inode, statfs, sync_fs, put_super, freeze/thaw). This is the superblock-level analogue of f_op on a file. Detailed below and in VFS Operation Tables. alloc_super initializes it to a default_op of all-NULL methods; fill_super overwrites it.
  • s_flags — mount-wide flags: SB_RDONLY (read-only), SB_NOSUID, SB_NODEV, SB_NOEXEC, SB_SYNCHRONOUS, SB_ACTIVE (the instance is live and usable — set after fill_super succeeds), SB_BORN (fully published — set by vfs_get_tree), and more. These are the kernel-internal SB_* form of the userspace MS_* mount flags.
  • s_magic — the filesystem’s magic number, e.g. EXT4_SUPER_MAGIC (0xEF53) or TMPFS_MAGIC (0x01021994). It is what statfs(2) returns in f_type, letting userspace identify the filesystem kind of a path.
  • s_rootstruct dentry *, the root dentry of the mounted subtree. This is the single most important field: it is the anchor from which the whole in-memory directory tree of this instance hangs, and the thing a mount grafts onto a mount point. A superblock with a non-NULL s_root has been fully filled in; sget_fc returns a partially-constructed superblock with s_root == NULL and the caller (vfs_get_super/fill_super) sets it.
  • s_bdev / s_bdev_file — the backing struct block_device (and, new in this era, the struct file handle on it). For pseudo filesystems (tmpfs, proc, sysfs, sockfs) this is NULL — they have no block device, only an anonymous s_dev.
  • s_bdistruct backing_dev_info *, the writeback/readahead-tuning object the flusher threads consult for this instance. alloc_super points it at noop_backing_dev_info until the real one is attached.
  • s_fs_infovoid *, the filesystem’s private superblock state. This is where the driver hangs its own parsed on-disk superblock and runtime bookkeeping — ext4_sb_info for ext4, xfs_mount for XFS, shmem_sb_info for tmpfs. The VFS never touches it; it belongs entirely to the filesystem.
  • s_inodes (+ s_inode_list_lock) — the list of all inodes currently belonging to this instance, used at unmount to evict them and by the s_inode_lru shrinker to reclaim them under memory pressure. s_inodes_wb is the subset currently under writeback.
  • s_user_ns — the owning user namespace, used to interpret on-disk uid/gid/quota/xattr values — the linchpin of unprivileged (“user-namespace”) mounts of filesystems flagged FS_USERNS_MOUNT.
  • s_umount — an rw_semaphore that serializes mount, unmount, and remount against each other and against concurrent operations that must see a stable mount state.
  • s_count / s_active — two refcounts with different meanings. s_active counts active (mounted) references; when it hits zero the filesystem is told to shut the instance down (kill_sb). s_count is the broader lifetime count keeping the memory alive while temporary lookups hold it. The split lets a brief lookup pin the structure (s_count) without keeping the filesystem mounted (s_active).

How a superblock is born: get_tree_* and fill_super

A superblock comes into existence at mount time, driven by the new mount API (Documentation/filesystems/mount_api.rst). The userspace mount(2) (or fsopen/fsconfig/fsmount) builds a struct fs_context describing what to mount and how, then vfs_get_tree(fc) invokes the filesystem’s get_tree operation. The filesystem almost always implements get_tree by calling one of the VFS-provided helpers, chosen by what kind of source it has (fs/super.c):

  • get_tree_bdev(fc, fill_super) — for a block-device-backed filesystem (ext4, XFS, Btrfs, …). It lookup_bdev()s the source path to a dev_t, then sget_dev(fc, dev) finds-or-creates a superblock keyed on that device. If s->s_root is already set, an existing instance is being shared (the second mount of the same device) — it only checks the RO/RW state matches. Otherwise it calls setup_bdev_super() (attaching s_bdev, setting s_dev) and then the filesystem’s fill_super, and on success sets SB_ACTIVE.
  • get_tree_nodev(fc, fill_super) — for a filesystem with no backing device and no sharing (a fresh, independent instance every mount). It calls sget_fc(fc, NULL, set_anon_super_fc), which always allocates a new superblock with an anonymous s_dev, then runs fill_super.
  • get_tree_single(fc, fill_super) — for a filesystem that should have exactly one shared instance system-wide (e.g. sysfs-like singletons): test_single_super always matches, so the first mount creates it and every later mount shares it.
  • get_tree_keyed(fc, fill_super, key) — share-or-create keyed on an arbitrary key stashed in s_fs_info (used when “the same instance” is defined by something other than a device, e.g. a network filesystem’s server+export tuple).

All four funnel through sget_fc(), the find-or-create core. sget_fc walks fc->fs_type->fs_supers looking for an existing superblock the test callback accepts; if none matches it alloc_super()s a fresh one, runs the set callback, copies in s_fs_info, sets s_type and s_id, publishes it onto the global super_blocks list and the per-type fs_supers hlist, and returns it in a partially constructed state — s_root is still NULL, SB_BORN/SB_ACTIVE not yet set (sget_fc, fs/super.c).

The filesystem-specific fill_super(sb, fc) is then where the real work happens: it reads the on-disk superblock off s_bdev, validates the magic, sets s_blocksize/s_maxbytes/s_magic/s_time_gran, installs the real s_op (and s_d_op, s_xattr, etc.), allocates and reads the root inode, builds s_root via d_make_root(), and stashes its parsed state in s_fs_info. When fill_super returns success, vfs_get_super does sb->s_flags |= SB_ACTIVE and the caller sets fc->root = dget(sb->s_root). Finally, back in vfs_get_tree, super_wake(sb, SB_BORN) publishes the instance as fully alive (with a barrier ordering the setup writes before the SB_BORN flag becomes visible). The order — publish empty → fill → mark ACTIVE → mark BORN — is what lets concurrent lookups safely wait for SB_BORN rather than racing a half-built superblock.

statfs: a method call through s_op

statfs(2)/fstatfs(2) are the clearest example of dispatching through the superblock’s method table. The syscall resolves the path (or fd) to a dentry, then statfs_by_dentry() calls, verbatim (fs/statfs.c):

if (!dentry->d_sb->s_op->statfs)
    return -ENOSYS;
...
retval = dentry->d_sb->s_op->statfs(dentry, buf);

That is: from the dentry, reach its superblock (d_sb), reach the method table (s_op), and call the filesystem’s statfs method, which fills a struct kstatfs (total/free/available blocks, free inodes, block size, filesystem magic in f_type, fragment size, max filename length). The VFS copies the kstatfs out to the userspace struct statfs. Every other whole-filesystem operation works the same way — sync_fs, freeze_super/thaw_super, remount_fs, the inode-lifecycle hooks alloc_inode/destroy_inode/write_inode/evict_inode — each is a s_op->method(sb, …) call, exactly as file operations are f_op->method(file, …) calls on the VFS File Object. The super_operations members and their contracts are documented in Documentation/filesystems/vfs.rst and enumerated in VFS Operation Tables.

How a superblock dies: kill_sbgeneric_shutdown_super

Unmount drops the last active reference. deactivate_locked_super(s) does atomic_dec_and_test(&s->s_active); when that reaches zero it frees the per-sb shrinker, calls the filesystem’s s->s_type->kill_sb(s), destroys the LRU lists, drops the file_system_type reference, and finally put_super() (fs/super.c). Almost every filesystem implements kill_sb by doing its own cleanup and then calling the common helper generic_shutdown_super(sb), which: shrinks the dcache for the instance (shrink_dcache_for_umount), runs a final sync_filesystem, clears SB_ACTIVE, evicts all zero-refcount inodes (evict_inodes), tears down fsnotify and security state, calls the filesystem’s s_op->put_super if present, destroys the fscrypt keyring, and — defensively — poisons any inodes still on s_inodes after all that, logging “VFS: Busy inodes after unmount” (a “you forgot to release an inode” bug in the filesystem). It then marks the superblock SB_DYING, drops the backing-device reference, and the memory is freed via RCU once s_count also reaches zero.

Failure Modes and Diagnostics

  • EBUSY on mount from an RO/RW mismatch. get_tree_bdev refuses to silently flip a shared instance’s read-only state: if you try to mount a device read-write that is already mounted read-only (or vice-versa) and they would share one superblock, it returns -EBUSY (“Can’t mount, would change RO state”). Use mount -o remount,rw to change the state of the existing instance instead.
  • EBUSY on umount (busy filesystem). s_active cannot reach zero while anything holds the instance: an open file (its f_path.mnt/dentry pin it), a process with a cwd inside it, or a bind mount. generic_shutdown_super’s “Busy inodes after unmount” poison is the kernel’s last-ditch guard against a filesystem that leaks inodes; lsof +f -- /mountpoint and fuser -m find the userspace culprits.
  • ENOSYS from statfs. A filesystem with no s_op->statfs method returns -ENOSYS; in practice every real filesystem implements it.
  • Anonymous-device exhaustion. Pseudo filesystems draw s_dev from a limited anonymous-minor pool via set_anon_superget_anon_bdev, which allocates from a 20-bit minor ID space (ida_alloc_range(..., (1 << MINORBITS) - 1, ...)). On exhaustion the allocator’s -ENOSPC is explicitly remapped to -EMFILE before returning (get_anon_bdev, fs/super.c). Pathological numbers of simultaneous pseudo mounts (e.g. systems creating huge numbers of mount namespaces) can in principle hit this; it is rare in practice.

Uncertain

Verify: the field set, ordering, and the s_bdev_file companion field shown above are from v6.12 include/linux/fs.h; struct super_block is large and gains/loses members across releases (the s_bdev_file handle and the per-sb shrinker s_shrink pointer are recent additions). Reason: layout is volatile release-to-release. To resolve: diff include/linux/fs.h against the specific target tag before quoting field offsets or the presence of any optional #ifdef-gated member. uncertain

Alternatives and Boundaries

There is no alternative to the superblock — it is the per-mounted-instance object in Linux. The useful distinctions are with its neighbours:

  • vs file_system_type. One file_system_type per registered driver (ext4); one super_block per mounted instance of it. The driver is the factory; the superblock is a product. Owned by Filesystem Registration and Mounting Internals.
  • vs vfsmount / struct mount. A super_block is the filesystem instance; a vfsmount is a placement of (part of) that instance into the namespace tree. One superblock can underlie many vfsmounts (bind mounts), which is exactly why f_path on a file carries both a vfsmount and a dentry. Owned by The Mount Tree and vfsmount.
  • vs the on-disk superblock. The on-disk superblock is a fixed byte layout (ext4’s is at a known offset, with magic 0xEF53); struct super_block is the runtime object fill_super builds from it. They are not the same thing, and a filesystem may keep many on-disk superblock copies (backups) for one runtime struct super_block.

Production Notes

The superblock is the natural granularity for filesystem-wide operations administrators actually invoke. sync_fs is what syncfs(2) and sync(8) drive; freeze_super/thaw_super back fsfreeze(8) and the FIFREEZE ioctl that LVM snapshots rely on to quiesce a filesystem to a consistent state before snapshotting. The per-sb s_inode_lru/s_dentry_lru shrinkers (registered in alloc_super) are how memory reclaim reclaims cached inodes and dentries per filesystem, which matters on machines with thousands of mounts — a single global LRU would be a scalability bottleneck. The migration to the fs_context/get_tree_* mount API (merged for 5.2, 2019) was a multi-year effort to replace the opaque, hard-to-extend mount(2) data page with an incremental, introspectable configuration object; the superblock-creation helpers above are its filesystem-facing half (LWN: Mount API, LWN: filesystem mounting).

See Also