Loading learning content...
Linux supports over 60 file systems out of the box—from disk-based systems like ext4, XFS, and btrfs, to network file systems like NFS and CIFS, to pseudo-filesystems like proc, sysfs, and tmpfs. Each of these file systems must somehow make itself known to the kernel, announcing its presence and capabilities so that when a user types mount -t ext4 /dev/sda1 /mnt, the kernel knows exactly which code to invoke.
This process is called file system registration. It's the mechanism by which file system drivers install themselves into the VFS layer, and by which VFS discovers and dispatches to the correct driver during mount operations.
This page explores the complete registration lifecycle: how file systems declare themselves, how the kernel maintains a registry of available file systems, how mounting triggers driver invocation, and how loadable kernel modules enable on-demand file system loading.
By the end of this page, you will understand the file_system_type structure, registration and unregistration functions, how mount() selects the correct driver, the kernel module loading mechanism for file systems, and how to inspect registered file systems on a running system.
Every file system that wants to integrate with VFS must define a struct file_system_type. This structure is the identity card of a file system—it tells VFS the file system's name, capabilities, and most importantly, how to mount instances of it.
Core Principle: A file_system_type represents the file system driver (e.g., ext4, NFS). A super_block represents a mounted instance of that file system. One driver can have multiple mounted instances.
1234567891011121314151617181920212223242526272829303132333435363738
struct file_system_type { /* Identification */ const char *name; /* File system name (e.g., "ext4") */ /* Flags describing capabilities */ int fs_flags; /* Flags (see below) */ /* Mount function - THE critical method */ int (*init_fs_context)(struct fs_context *); /* OR older interface: */ struct dentry *(*mount)(struct file_system_type *, int, const char *, void *); /* Context parameters */ const struct fs_parameter_spec *parameters; /* Unmount cleanup */ void (*kill_sb)(struct super_block *); /* Module owner */ struct module *owner; /* Module providing this FS */ /* Linked list of registered file systems */ struct file_system_type *next; /* List of all superblocks for this file system type */ struct hlist_head fs_supers; /* Locking */ struct lock_class_key s_lock_key; struct lock_class_key s_umount_key; struct lock_class_key s_vfs_rename_key; struct lock_class_key s_writers_key[SB_FREEZE_LEVELS]; struct lock_class_key i_lock_key; struct lock_class_key i_mutex_key; struct lock_class_key invalidate_lock_key; struct lock_class_key i_mutex_dir_key;};Key Fields Explained:
| Field | Purpose |
|---|---|
name | The type name passed to mount(). E.g., "ext4" for mount -t ext4 |
fs_flags | Capabilities and requirements (see below) |
init_fs_context | Modern mount interface - creates filesystem context |
mount | Legacy mount interface - returns root dentry |
kill_sb | Cleanup function called when last superblock is unmounted |
owner | The kernel module providing this file system (for refcounting) |
next | Links all registered file systems in a list |
fs_supers | List of all superblocks (mounted instances) of this type |
Filesystem Flags (fs_flags):
1234567891011121314151617
/* File system requires a real block device */#define FS_REQUIRES_DEV 1 /* Filesystem will handle binary mount data (not text options) */#define FS_BINARY_MOUNTDATA 2 /* Has subtype (like FUSE: fuse.sshfs) */#define FS_HAS_SUBTYPE 4 /* Can be mounted by user namespace root */#define FS_USERNS_MOUNT 8 /* Disable fanotify permission events */#define FS_DISALLOW_NOTIFY_PERM 16 /* File system is safe for RENAME_EXCHANGE */#define FS_RENAME_DOES_D_MOVE 32768File systems like ext4 and XFS set FS_REQUIRES_DEV—they need a block device. Pseudo-filesystems like procfs and sysfs don't set this flag because they're purely in-memory. Network file systems like NFS also don't require a local block device. This flag influences how mount() handles the source parameter.
File systems register themselves with VFS using register_filesystem() and unregister with unregister_filesystem(). These functions add or remove the file system type from the kernel's global list of known file systems.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
/* Global list of registered file systems */static struct file_system_type *file_systems;static DEFINE_RWLOCK(file_systems_lock); /** * register_filesystem - register a new file system * @fs: the file system structure to register * * Adds the file system to the list of known file systems. * Returns 0 on success, negative error on failure. */int register_filesystem(struct file_system_type *fs){ int res = 0; struct file_system_type **p; /* Sanity check: must have a name */ if (fs->next) return -EBUSY; write_lock(&file_systems_lock); /* Walk the list to check for duplicates and find end */ p = &file_systems; while (*p) { if (strcmp((*p)->name, fs->name) == 0) { /* Already registered with this name! */ res = -EBUSY; goto out; } p = &(*p)->next; } /* Add to end of list */ *p = fs; out: write_unlock(&file_systems_lock); return res;}EXPORT_SYMBOL(register_filesystem); /** * unregister_filesystem - unregister a file system * @fs: file system type to remove */int unregister_filesystem(struct file_system_type *fs){ struct file_system_type **p; int err = -EINVAL; write_lock(&file_systems_lock); /* Find and remove from list */ for (p = &file_systems; *p; p = &(*p)->next) { if (*p == fs) { *p = fs->next; fs->next = NULL; err = 0; break; } } write_unlock(&file_systems_lock); return err;}EXPORT_SYMBOL(unregister_filesystem);When Registration Happens:
initcall mechanisms:/* In fs/ext4/super.c */
static int __init ext4_init_fs(void)
{
/* ... initialization ... */
err = register_filesystem(&ext4_fs_type);
if (err)
goto out;
return 0;
}
module_init(ext4_init_fs); /* Called during kernel init or module load */
module_init function runs when the module is loaded. Loading can be:
modprobe ext4fs-ext4 module when mounting with -t ext4Unregistration:
Unregistration only succeeds when no superblocks of that type exist (nothing mounted). The owner field enables module refcounting—a module cannot be unloaded while its file system is mounted.
When a file system is mounted, VFS takes a reference on the file system type's owner module (try_module_get()). This prevents the module from being unloaded while in use. Only after all instances are unmounted and references released can the module be removed.
When you execute mount -t ext4 /dev/sda1 /mnt, the kernel must find the registered ext4 file system type and invoke its mount function. This lookup happens in get_fs_type().
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
/** * get_fs_type - find a file system type by name * @name: name of the file system (e.g., "ext4") * * Returns the file system type structure, or NULL if not found. * May trigger module autoload for "fs-<name>" or "fs-<base>". */struct file_system_type *get_fs_type(const char *name){ struct file_system_type *fs; const char *dot = strchr(name, '.'); int len = dot ? dot - name : strlen(name); /* First, try to find it in the registered list */ fs = __get_fs_type(name, len); /* If not found, try to load a module */ if (!fs && (request_module("fs-%.*s", len, name) == 0)) { /* Module loaded, try again */ fs = __get_fs_type(name, len); } if (dot && fs && !(fs->fs_flags & FS_HAS_SUBTYPE)) { put_filesystem(fs); fs = NULL; } return fs;} /* Actually search the registered list */static struct file_system_type *__get_fs_type(const char *name, int len){ struct file_system_type *fs; read_lock(&file_systems_lock); for (fs = file_systems; fs; fs = fs->next) { if (strncmp(fs->name, name, len) == 0 && !fs->name[len]) { if (!try_module_get(fs->owner)) fs = NULL; break; } } read_unlock(&file_systems_lock); return fs;}The Module Autoload Mechanism:
Notice request_module("fs-%.*s", len, name). If ext4 isn't found, the kernel requests loading module fs-ext4. This triggers:
modprobe fs-ext4 runs/lib/modules/<version>/modules.alias:
alias fs-ext4 ext4
/lib/modules/<version>/kernel/fs/ext4/ext4.koinit function runs, calling register_filesystem()Viewing Registered File Systems:
# List all currently registered file systems
$ cat /proc/filesystems
nodev sysfs
nodev tmpfs
nodev bdev
nodev proc
nodev cgroup
nodev devtmpfs
ext3
ext4
nodev fuse
nodev overlay
xfs
btrfs
nodev nfs
nodev nfs4
...
The nodev prefix indicates file systems that don't require a block device (FS_REQUIRES_DEV not set).
When you run 'mount /dev/sda1 /mnt' without specifying -t, mount tries to detect the file system type by reading the device's superblock signatures. It tries registered types in order, calling their 'mount' functions until one succeeds. This is why specific type (-t) mounts are faster—they skip the detection loop.
Once VFS finds the file system type, it invokes the mount callback. Modern kernels use a context-based mount API (init_fs_context), but understanding the legacy mount callback is still valuable:
Legacy Mount Callback Signature:
struct dentry *(*mount)(struct file_system_type *fs_type,
int flags,
const char *dev_name,
void *data);
Parameters:
fs_type: The file system type structureflags: Mount flags (MS_RDONLY, MS_NOEXEC, etc.)dev_name: Device name (e.g., "/dev/sda1") or special stringdata: Mount options string (e.g., "noatime,errors=remount-ro")Return Value:
Standard Mount Helpers:
VFS provides helper functions that simplify common mount patterns:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
/* For file systems requiring a block device (ext4, xfs, etc.) */static struct dentry *ext4_mount(struct file_system_type *fs_type, int flags, const char *dev_name, void *data){ return mount_bdev(fs_type, flags, dev_name, data, ext4_fill_super);} /* mount_bdev: Opens the block device, calls fill_super */struct dentry *mount_bdev(struct file_system_type *fs_type, int flags, const char *dev_name, void *data, int (*fill_super)(struct super_block *, void *, int)){ struct block_device *bdev; struct super_block *s; /* Open the block device */ bdev = blkdev_get_by_path(dev_name, mode, fs_type); if (IS_ERR(bdev)) return ERR_CAST(bdev); /* Get or create a superblock for this device */ s = sget(fs_type, test_bdev_super, set_bdev_super, flags, bdev); if (s->s_root) { /* Already mounted - return existing root */ return dget(s->s_root); } /* New mount - set up the superblock */ s->s_bdev = bdev; s->s_dev = bdev->bd_dev; /* Call file system's fill_super to initialize */ error = fill_super(s, data, flags & SB_SILENT ? 1 : 0); if (error) goto error_out; return dget(s->s_root);} /* For pseudo-filesystems (proc, sysfs, tmpfs) */static struct dentry *proc_mount(struct file_system_type *fs_type, int flags, const char *dev_name, void *data){ return mount_ns(fs_type, flags, data, pid_ns, proc_fill_super);} /* For single-instance filesystems */static struct dentry *sysfs_mount(struct file_system_type *fs_type, int flags, const char *dev_name, void *data){ return mount_single(fs_type, flags, data, sysfs_fill_super);}| Helper | Use Case | Block Device? | Superblock Sharing |
|---|---|---|---|
| mount_bdev() | Disk-based file systems (ext4, xfs) | Required | One per device |
| mount_nodev() | Memory/pseudo file systems (tmpfs) | None | One per mount |
| mount_single() | Global single-instance (sysfs, debugfs) | None | One globally |
| mount_ns() | Namespace-aware (procfs, mqueue) | None | One per namespace |
Each mount helper eventually calls the file system's 'fill_super' function. This is where the real work happens: reading on-disk structures, populating the superblock, creating the root inode and dentry. This callback is the file system's opportunity to initialize itself for this mount instance.
Linux 5.1 introduced a new mount API based on struct fs_context. This provides more flexibility, better parameter parsing, and supports new mount features like mount namespaces and bind mount configuration.
Why the New API?
12345678910111213141516171819202122232425262728293031323334353637
/* Forward declaration of fs_context */struct fs_context { const struct fs_context_operations *ops; struct file_system_type *fs_type; void *fs_private; /* FS-specific data */ struct super_block *root; /* Resulting superblock */ struct user_namespace *user_ns; /* User namespace */ struct net *net_ns; /* Network namespace */ const struct cred *cred; /* Credentials to use */ char *source; /* Source (device name) */ void *security; /* LSM context */ unsigned int sb_flags; /* Proposed superblock flags */ unsigned int sb_flags_mask; /* Flags being modified */ unsigned int purpose:8; /* Mount purpose */ /* ... more fields ... */}; /* Operations the file system provides */struct fs_context_operations { /* Free the fs_context */ void (*free)(struct fs_context *fc); /* Duplicate for fork of mount process */ int (*dup)(struct fs_context *fc, struct fs_context *src); /* Parse a single mount option */ int (*parse_param)(struct fs_context *fc, struct fs_parameter *param); /* Parse a monolithic mount data string (legacy compatibility) */ int (*parse_monolithic)(struct fs_context *fc, void *data); /* Actually perform the mount */ int (*get_tree)(struct fs_context *fc); /* Reconfigure (remount) an existing superblock */ int (*reconfigure)(struct fs_context *fc);};Example: ext4 with fs_context:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485
/* Define the mount parameters ext4 accepts */static const struct fs_parameter_spec ext4_param_specs[] = { fsparam_flag("bsddf", Opt_bsd_df), fsparam_flag("minixdf", Opt_minix_df), fsparam_flag("nogrpid", Opt_no_grpid), fsparam_u32("resgid", Opt_resgid), fsparam_u32("resuid", Opt_resuid), fsparam_u32("sb", Opt_sb), fsparam_enum("errors", Opt_errors, ext4_param_errors), fsparam_string("data", Opt_data), fsparam_flag_no("acl", Opt_acl), fsparam_flag_no("user_xattr", Opt_user_xattr), fsparam_u32("commit", Opt_commit), fsparam_u32("max_batch_time", Opt_max_batch_time), {} /* Terminator */}; /* Parse individual parameters */static int ext4_parse_param(struct fs_context *fc, struct fs_parameter *param){ struct ext4_fs_context *ctx = fc->fs_private; struct fs_parse_result result; int opt; /* Use generic parameter parser */ opt = fs_parse(fc, ext4_param_specs, param, &result); if (opt < 0) return opt; switch (opt) { case Opt_bsd_df: ctx->mount_opt |= EXT4_MOUNT_MINIX_DF; break; case Opt_resgid: ctx->s_resgid = make_kgid(fc->user_ns, result.uint_32); break; case Opt_commit: ctx->s_commit_interval = result.uint_32; break; /* ... handle each option ... */ } return 0;} /* Perform the actual mount */static int ext4_get_tree(struct fs_context *fc){ return get_tree_bdev(fc, ext4_fill_super);} static const struct fs_context_operations ext4_context_ops = { .free = ext4_fc_free, .parse_param = ext4_parse_param, .get_tree = ext4_get_tree, .reconfigure = ext4_reconfigure,}; /* Create an fs_context for ext4 */static int ext4_init_fs_context(struct fs_context *fc){ struct ext4_fs_context *ctx; ctx = kzalloc(sizeof(*ctx), GFP_KERNEL); if (!ctx) return -ENOMEM; /* Set defaults */ ctx->s_commit_interval = JBD2_DEFAULT_MAX_COMMIT_AGE; /* ... more defaults ... */ fc->fs_private = ctx; fc->ops = &ext4_context_ops; return 0;} /* Register with the new callback */static struct file_system_type ext4_fs_type = { .name = "ext4", .fs_flags = FS_REQUIRES_DEV, .init_fs_context = ext4_init_fs_context, .parameters = ext4_param_specs, .kill_sb = ext4_kill_sb, .owner = THIS_MODULE,};The fs_context API enables new system calls like fsopen(), fsconfig(), fsmount(), and move_mount(). These provide finer control than the monolithic mount() call, allowing userspace to configure mounts step-by-step before committing them.
Most file systems are built as loadable kernel modules. Understanding how they're loaded on demand is important for system administration and debugging.
Module Aliases:
Kernel modules declare aliases that map request names to actual module names. For file systems:
# In /lib/modules/$(uname -r)/modules.alias
alias fs-ext4 ext4
alias fs-ext3 ext4 # ext4 also handles ext3
alias fs-ext2 ext4 # ext4 also handles ext2
alias fs-xfs xfs
alias fs-btrfs btrfs
alias fs-nfs nfs
alias fs-nfs4 nfsv4
alias fs-cifs cifs
alias fs-vfat fat
alias fs-msdos fat
These aliases are generated from MODULE_ALIAS() declarations in the source:
/* In fs/ext4/super.c */
MODULE_ALIAS_FS("ext4");
MODULE_ALIAS_FS("ext3");
MODULE_ALIAS_FS("ext2");
/* Expands to: */
/* MODULE_ALIAS("fs-ext4"); */
/* MODULE_ALIAS("fs-ext3"); */
/* MODULE_ALIAS("fs-ext2"); */
The Loading Sequence:
Practical Commands:
# Load a file system module explicitly
$ sudo modprobe ext4
# Check if module is loaded
$ lsmod | grep ext4
ext4 786432 1
jbd2 131072 1 ext4
mbcache 16384 1 ext4
# View module information
$ modinfo ext4
filename: /lib/modules/6.5.0/kernel/fs/ext4/ext4.ko
license: GPL
description: Fourth Extended Filesystem
author: ... Mingming Cao, ...
alias: fs-ext4
alias: fs-ext3
alias: fs-ext2
depends: mbcache,jbd2
...
# Force unload (dangerous if in use!)
$ sudo modprobe -r ext4
modprobe: FATAL: Module ext4 is in use.
A file system module cannot be unloaded while any instance is mounted. The module reference count prevents rmmod from succeeding. You must unmount ALL instances of that file system type before the module can be removed.
Let's look at a complete, minimal file system registration to tie everything together. This example shows what a simple in-memory file system needs to integrate with VFS.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
/* * simplefs - A minimal example file system * Shows the essential VFS registration pattern */ #include <linux/module.h>#include <linux/fs.h>#include <linux/init.h> #define SIMPLEFS_MAGIC 0x534D504C /* 'SMPL' */ /* Super operations - what the file system provides for superblock */static const struct super_operations simplefs_super_ops = { .statfs = simple_statfs, /* Use generic implementation */ .drop_inode = generic_delete_inode,}; /* Fill superblock during mount */static int simplefs_fill_super(struct super_block *sb, void *data, int silent){ struct inode *root_inode; struct dentry *root_dentry; /* Configure superblock */ sb->s_magic = SIMPLEFS_MAGIC; sb->s_op = &simplefs_super_ops; sb->s_maxbytes = MAX_LFS_FILESIZE; sb->s_blocksize = PAGE_SIZE; sb->s_blocksize_bits = PAGE_SHIFT; sb->s_time_gran = 1; /* Create root inode */ root_inode = new_inode(sb); if (!root_inode) return -ENOMEM; root_inode->i_ino = 1; root_inode->i_sb = sb; root_inode->i_mode = S_IFDIR | 0755; root_inode->i_uid = KUIDT_INIT(0); root_inode->i_gid = KGIDT_INIT(0); root_inode->i_atime = root_inode->i_mtime = root_inode->i_ctime = current_time(root_inode); root_inode->i_op = &simple_dir_inode_operations; root_inode->i_fop = &simple_dir_operations; /* Create root dentry */ root_dentry = d_make_root(root_inode); if (!root_dentry) { iput(root_inode); return -ENOMEM; } sb->s_root = root_dentry; return 0;} /* Mount callback - uses nodev helper (no block device needed) */static struct dentry *simplefs_mount(struct file_system_type *fs_type, int flags, const char *dev_name, void *data){ return mount_nodev(fs_type, flags, data, simplefs_fill_super);} /* Cleanup on unmount */static void simplefs_kill_sb(struct super_block *sb){ kill_litter_super(sb); /* Clean up pseudo-FS superblock */} /* The file system type structure - our registration data */static struct file_system_type simplefs_type = { .name = "simplefs", .fs_flags = 0, /* No FS_REQUIRES_DEV - in-memory only */ .mount = simplefs_mount, .kill_sb = simplefs_kill_sb, .owner = THIS_MODULE,}; /* Module initialization - called when module loads */static int __init simplefs_init(void){ int ret; pr_info("simplefs: Registering file system\n"); ret = register_filesystem(&simplefs_type); if (ret) { pr_err("simplefs: Registration failed: %d\n", ret); return ret; } pr_info("simplefs: Successfully registered\n"); return 0;} /* Module cleanup - called when module unloads */static void __exit simplefs_exit(void){ pr_info("simplefs: Unregistering file system\n"); unregister_filesystem(&simplefs_type);} module_init(simplefs_init);module_exit(simplefs_exit); MODULE_LICENSE("GPL");MODULE_AUTHOR("Example Author");MODULE_DESCRIPTION("A minimal example file system");MODULE_ALIAS_FS("simplefs");Using This File System:
# Build and load the module
$ make
$ sudo insmod simplefs.ko
# Verify registration
$ cat /proc/filesystems | grep simple
nodev simplefs
# Mount it
$ mkdir /mnt/simple
$ sudo mount -t simplefs none /mnt/simple
# It's mounted!
$ mount | grep simple
none on /mnt/simple type simplefs (rw,relatime)
# Unmount
$ sudo umount /mnt/simple
# Unload module
$ sudo rmmod simplefs
This minimal example demonstrates the essential registration pattern:
file_system_type with name, mount callback, and kill_sbfill_super to set up superblock, root inode, root dentryregister_filesystem() in module initunregister_filesystem() in module exitWe've explored how file systems plug into the VFS layer. Here are the key concepts:
Module Complete:
With this page, we've completed our deep dive into the Virtual File System. You now understand:
This knowledge forms the foundation for understanding how operating systems manage files, debugging file system issues, and implementing custom file systems.
Congratulations! You've mastered the Virtual File System layer. You understand how VFS provides uniform file access across diverse storage technologies, the structures that make it possible, and how new file systems integrate with the kernel. This is fundamental systems knowledge that applies to all Unix-like operating systems.