Skip to main content
Linux concept: filesystems
On this page
# Topics

Linux concept: filesystems

07 September 2026

A disk does not contain files. It contains a very long row of numbered blocks, all the same size, with no names, no folders, and no idea which block belongs to which document. Everything you think of as a file, the name, the folder it sits in, the owner, the permissions, the modification date, exists because one piece of software writes bookkeeping into some of those blocks and reads it back. That software is the filesystem, and almost every confusing thing a Linux server does with storage becomes obvious once you know what it is writing down.

This article explains what a filesystem really is, where the word comes from, how ext4, XFS, Btrfs, ZFS, FAT and the virtual filesystems differ, and how to read your own with df, stat, findmnt, filefrag and dumpe2fs. It then goes underneath: inodes, blocks, extents, journals, delayed allocation, sparse files and copy-on-write.

From "what is mounted here" to "why is this file 4 KB when it holds one byte".

The goal: after reading this you can look at any Linux storage problem and say which layer it belongs to.

1. The Basics

A filesystem is the structure a system writes onto a storage device so that raw blocks can be used as named files in folders. It is both a format on disk and the kernel code that understands that format.

Start from what the hardware offers. A disk, an SSD, a partition, an LVM volume, all present the same simple thing to the kernel: a numbered sequence of fixed-size blocks that you can read and write. Block 0, block 1, block 2, up to a few hundred million of them. There is no other structure. If you want a file called invoice.pdf that belongs to you, is 240 KB long, and lives in /home/peter/admin/, every part of that sentence has to be stored in those blocks by somebody.

The filesystem is that somebody. It decides which blocks hold your content, writes a record saying "this file is 240 KB, owned by user 1000, and its content is in blocks 900,112 to 900,171", and writes a second record in the folder saying "the name invoice.pdf refers to that record". Everything else follows from those two ideas.

1.1 What a Filesystem Actually Is

It helps to see the word used in three different ways, because Linux uses all three and they are not the same thing:

The word meansExample
A format: the on-disk layout and the rules for it"ext4 is a journalling filesystem"
An instance: one formatted device, mounted somewhere"the filesystem on /dev/sda2 is 96% full"
The tree: everything reachable from /"the file is somewhere in the filesystem"

When somebody says "the filesystem is full", they mean the second one. When they say "check the filesystem", they usually mean run fsck on the second one too. When a programmer says "read it from the filesystem", they mean the third. Keeping the three apart removes a surprising amount of confusion from storage discussions.

1.2 Three Things, Not One: Data, Metadata, and Names

The single most useful idea in this article is that a file is not one object. It is three separate things that the filesystem stores in three separate places:

DIRECTORY ENTRY     "invoice.pdf"  →  inode 61743294       (the name)
        |
        v
INODE 61743294      size, owner, group, permissions, three
                    timestamps, link count, and a map of blocks   (the facts)
        |
        v
DATA BLOCKS         900112, 900113, 900114, ...                  (the content)

The directory entry is only a name plus a number. The inode holds every fact about the file except its name. The data blocks hold the bytes. A directory is not a container that holds files; it is a small table of names and inode numbers, and it is itself a file.

The right mental model: a filename is a pointer, not a file. The file is the inode plus its blocks. This one split explains hard links, why deleting is instant, why a full disk can stay full after rm, and why moving a file inside one filesystem takes no time at all.

1.3 One Tree, Many Filesystems

Windows gives each formatted volume its own letter. Linux does the opposite: there is exactly one tree, starting at /, and every other filesystem is grafted into it at a mount point, which is just an existing directory. After the graft, nothing in a path tells you where one filesystem ends and the next begins.

$ findmnt -o TARGET,SOURCE,FSTYPE
TARGET            SOURCE                             FSTYPE
/                 /dev/mapper/ubuntu--vg-ubuntu--lv  ext4
├─/boot           /dev/nvme0n1p2                     ext4
│ └─/boot/efi     /dev/nvme0n1p1                     vfat
├─/dev/shm        tmpfs                              tmpfs
└─/run            tmpfs                              tmpfs

Four different filesystems on three different kinds of storage, and one of them (tmpfs) is not on a disk at all. A user typing cd /boot/efi crosses two boundaries without noticing, which is exactly the point.

The part of the kernel that makes this work is the VFS, the Virtual File System. It defines one set of operations, open, read, write, rename, and each filesystem type implements them. Your program calls open() and never learns whether the answer came from ext4, from a USB stick formatted as FAT, from a network share, or from memory. The article on the kernel covers the VFS as one of the kernel's core jobs; here it matters because it is the reason all the commands below work the same way on every filesystem type.

1.4 What Sits Underneath

The filesystem is one layer in a stack, and storage problems are much easier to solve when you know which layer you are looking at. On the machine used for this article the stack is five deep:

$ lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT
NAME                         SIZE TYPE  FSTYPE       MOUNTPOINT
nvme0n1                      1.9T disk
├─nvme0n1p1                    1G part  vfat         /boot/efi
├─nvme0n1p2                    2G part  ext4         /boot
└─nvme0n1p3                  1.9T part  crypto_LUKS
  └─dm_crypt-0               1.9T crypt LVM2_member
    └─ubuntu--vg-ubuntu--lv  1.9T lvm   ext4         /

Read it from the bottom up: an ext4 filesystem lives on a logical volume, which lives in an LVM volume group, which lives inside an encrypted LUKS container, which is a partition on an NVMe disk. Each layer only sees numbered blocks from the layer below and hands numbered blocks to the layer above.

That layering is why "the disk is full" and "the filesystem is full" are different sentences, why you can grow a filesystem without touching the disk, and why encryption is invisible to ext4. It is also the order you must think in when something breaks: a disk error, a LUKS problem, an LVM problem and a filesystem problem all look like "I cannot read my files".

Back to top

2. Where the Name Comes From

The name is a compound of two ordinary words, and both halves were chosen carefully.

File comes from office work. A paper file was a set of documents kept in order, from the French fil, a thread, because papers were once literally strung on a wire or thread to keep them together. Early computing borrowed the office vocabulary wholesale: files, folders, records, filing. When Unix called a stream of bytes on disk a "file", it was making a promise that this thing behaves like the paper thing: it has a name, you can put it somewhere, and you can find it again.

System is the honest half. The point is not the individual file but the organised whole: the naming, the hierarchy, the bookkeeping and the rules that keep them consistent. A filesystem is the filing system.

Spelling varies and both forms are correct. Traditional Unix documentation writes file system as two words; the Linux kernel community, and this article, mostly write filesystem as one. You will meet both in the same manual page.

A few other words in this article have their own small histories, and knowing them makes the tools easier to remember:

WordWhere it comes from
inodeShort for index node. The inodes form a numbered index; the number is the file's real identity.
superblockThe one block "above" all the others, holding the description of the whole filesystem: size, block size, feature flags, state.
mountFrom the tape and disk-pack era, when an operator physically mounted a reel or platter on a drive before the system could read it.
blockThe smallest unit of space the filesystem hands out, almost always 4096 bytes today. Not the same as the 512-byte sector the hardware talks in.
journalFrom bookkeeping: a running log of what you are about to do, written before you do it.
extExtended file system. It extended the very limited Minix filesystem that early Linux was stuck with.

Note what "inode" implies. Because a file's identity is a number in an index, and its name is a separate entry pointing at that number, the two can be many-to-one. That is not a clever feature added later; it falls straight out of the naming.

Back to top

3. A Short History

Filesystem design has been driven by three pressures, in order: making disks usable at all, surviving a crash, and coping with disks that grew far faster than anyone planned for.

Unix from 1969 already had the inode idea, and it barely changed in fifty years. What changed was speed and safety. The original Unix filesystem scattered inodes and data across the disk and lost a great deal of time to seeking. In 1983 the Berkeley Fast File System introduced cylinder groups, larger blocks and the idea of keeping a file's metadata near its data, and made Unix disks several times faster. Nearly every filesystem since has copied that idea in some form.

Linux started with the Minix filesystem, which allowed filenames of only 14 characters and partitions of only 64 MB. That limit is why ext exists at all: Remy Card wrote the Extended File System in 1992 to escape it, then replaced it a year later with ext2, which stayed the Linux default for a decade.

The next pressure was crash recovery. On a large ext2 filesystem, an unclean shutdown meant a full fsck, and a full fsck on a big disk could take hours while the server stayed down. Journalling solved that, and ext3 (2001) added it to ext2 without changing the on-disk layout, so you could upgrade in place. ext4 (2006, stable in 2008) then added extents, delayed allocation, much larger limits and faster allocation, and is still the default on most Linux distributions today.

The third pressure was scale, and it produced a different kind of filesystem. XFS came from Silicon Graphics in 1994 for machines doing video work, was released as open source in 2000, and was built from the start for very large files and many parallel writers. ZFS (Sun, released 2005) and Btrfs (started at Oracle in 2007, in the mainline kernel from 2009) went further and merged the volume manager into the filesystem, adding checksums on everything, snapshots, and copy-on-write.

YearMilestone
1969Unix introduces inodes, directories as files, and one mounted tree
1977FAT appears at Microsoft; still the format of nearly every USB stick and SD card
1983The Berkeley Fast File System makes Unix disks fast, with cylinder groups and bigger blocks
1985Sun's vnode and VFS design lets one kernel mount several filesystem types at once, including NFS
1992ext arrives, freeing Linux from Minix's 14-character names and 64 MB limit
1993ext2 becomes the Linux default and stays there for ten years
1994SGI ships XFS on IRIX, designed for very large files and parallel I/O
1995VFAT adds long filenames to FAT, keeping an 8.3 short name beside each one
2001ext3 adds a journal to ext2, ending the multi-hour fsck after a power cut
2005ZFS is released, merging volume management, checksums and snapshots into one layer
2008ext4 is declared stable, bringing extents and delayed allocation to the default filesystem
2009Btrfs enters the mainline kernel as Linux's own copy-on-write filesystem
2013F2FS arrives from Samsung, designed for the flash chips in phones rather than for spinning disks
Todayext4 remains the safe default; XFS ships as default on Red Hat; Btrfs on SUSE and Fedora

One licence detail explains a practical annoyance. ZFS is excellent and cannot be shipped inside the Linux kernel, because its CDDL licence and the kernel's GPL are considered incompatible. That is why ZFS on Linux arrives as a separate module you install and rebuild for each kernel, while Btrfs, which aims at the same goals, is simply there.

Back to top

4. Simple Use Cases: Reading Your Own System

Four commands answer almost every everyday question about filesystems. Learn these first, because every deeper section below is an explanation of something one of them prints.

4.1 What Is Mounted, and What Type

The classic tool is df (short for "disk free"). Its -T flag (short for "print type") is the one people forget, and it is the useful one:

$ df -hT
Filesystem                        Type   Size  Used Avail Use% Mounted on
/dev/mapper/ubuntu--vg-ubuntu--lv ext4   1.9T  1.7T   77G  96% /
/dev/nvme0n1p2                    ext4   2.0G  186M  1.7G  11% /boot
/dev/nvme0n1p1                    vfat   1.1G   42M  1.1G   4% /boot/efi
tmpfs                             tmpfs   32G  492M   31G   2% /dev/shm

-h is short for "human readable" and turns blocks into G and M. Before you tune, defragment, or search for advice online, run this: the answer in the Type column decides which of the rest of this article applies to you.

findmnt shows the same mounts as a tree, with the options they were mounted with, which df never shows:

$ findmnt /boot/efi
TARGET    SOURCE         FSTYPE OPTIONS
/boot/efi /dev/nvme0n1p1 vfat   rw,relatime,fmask=0022,dmask=0022,codepage=437,...

And stat -f (the -f is short for "file system") asks the filesystem itself rather than reading the mount table:

$ stat -f /
  File: "/"
    ID: b147cbc2ef6dfddb Namelen: 255     Type: ext2/ext3
Block size: 4096       Fundamental block size: 4096
Blocks: Total: 491176740  Free: 44949837   Available: 19980980
Inodes: Total: 124829696  Free: 110260882

Three things in that output are worth reading twice. Namelen: 255 is the maximum filename length, and section 7.2 shows that the unit is bytes, not characters. Free and Available differ by 25 million blocks, which is about 95 GiB that exists, is unused, and is not yours; section 7.3 explains where it went. And Type: ext2/ext3 is not a mistake and not a downgrade: ext2, ext3 and ext4 share the magic number 0xEF53 in the superblock, so this generic view cannot tell them apart. Trust df -T for the real name.

4.2 How Full: Two Different Questions

A filesystem can run out of two separate resources, and only one of them is space. Blocks hold your content; inodes hold your files' identities, and on ext4 the number of inodes is fixed when the filesystem is created and can never grow:

$ df -h /                 # blocks: how much content fits
Filesystem                         Size  Used Avail Use% Mounted on
/dev/mapper/ubuntu--vg-ubuntu--lv  1.9T  1.7T   77G  96% /

$ df -i /                 # inodes: how many files fit
Filesystem                            Inodes    IUsed     IFree IUse% Mounted on
/dev/mapper/ubuntu--vg-ubuntu--lv  124829696 14568814 110260882   12% /

This filesystem has room for 124.8 million files and is holding 14.5 million of them. The two percentages move independently: 96% of the space is used but only 12% of the inodes. A mail spool or a session directory can reverse that completely and hit 100% inodes with the disk half empty, at which point every write fails with "No space left on device" while df -h insists there is plenty. Section 5.5 covers that failure in detail.

4.3 The Filesystem Zoo

You will meet perhaps a dozen filesystem types in normal Linux work. This is what each is actually for:

TypeWhat it is forWorth knowing
ext4The general-purpose default on most distributionsJournalled, extent-based, boring in the best sense. Fixed inode count. Can grow online, shrink offline.
XFSLarge files, many parallel writers; default on Red Hat and derivativesSplits the device into allocation groups that allocate independently, which is where its parallelism comes from. Inodes are created on demand, so it cannot run out of them. It can grow but never shrink.
BtrfsCopy-on-write with snapshots, checksums and built-in RAIDSubvolumes and snapshots are cheap. Free space is genuinely hard to report; see section 6.7.
ZFSThe same ideas, older and very well tested, popular for storage serversNot in the kernel for licence reasons, so it is an add-on module. Wants a lot of RAM.
F2FSFlash storage: phones, SD cards, some embedded systemsLog-structured: it appends writes into large segments instead of scattering them, which is what flash hardware is fastest at. See section 7.9.
vfat / exFATUSB sticks, SD cards, the EFI boot partitionNo ownership, no permissions, no symlinks. FAT32 cannot hold a file of 4 GiB or more; exFAT lifts that.
NTFSReading Windows disksThe modern in-kernel driver is ntfs3, added in Linux 5.15; older systems use the FUSE-based ntfs-3g.
tmpfsFiles that live in RAM and vanish at rebootBacks /run, /dev/shm and often /tmp. Sized in memory, and it can swap.
squashfsRead-only compressed images: snaps, live CDs, appliancesAlways 100% full, because it is exactly the size of its contents.
overlayfsStacking a writable layer on a read-only one; the basis of container imagesCovered in the Docker article, where the copy-up cost matters most.
APFS / HFS+Mac disksHFS+ is readable with the hfsplus module; APFS has no mainline driver at all. See section 7.8.
NFS / SMBStorage on another machine, mounted as a normal directorySame system calls, very different failure modes: a server that stops answering can hang every process touching the mount.
FUSEFilesystems written as ordinary programs, in user spaceNot one format but a bridge: sshfs, ntfs-3g, cloud storage and archive browsers all arrive this way. See section 4.4.
proc / sysfsKernel state presented as filesNo storage at all. Every read runs kernel code and produces the answer on the spot.

A network filesystem deserves a warning of its own, because it is the one row where the VFS abstraction is doing you a small disservice. open() and read() look identical on an NFS mount, but underneath they are network requests, and the questions that follow are distributed-systems questions rather than storage ones: what happens while the server is unreachable, how user IDs on two machines are supposed to match, whether locking works, and when a write is really durable. A hard NFS mount whose server disappears will block processes in state D until it comes back, which is exactly the load-average puzzle the article on top describes.

If you have no strong reason to choose, choose ext4. It is the one your distribution tests most, the one every recovery tool understands, and the one with the fewest surprises in its free-space accounting. Pick XFS when you write very large files from many processes at once, and pick Btrfs or ZFS when you specifically want snapshots and checksums and are willing to learn their tools.

4.4 Most of Your Filesystems Are Not on a Disk

Count the mounts on an ordinary desktop and the result is surprising:

$ awk '{print $3}' /proc/mounts | sort | uniq -c | sort -rn | head -6
     48 squashfs        # one per installed snap package, all read-only
      5 tmpfs           # /run, /dev/shm, /run/lock, per-user runtime dirs
      5 nsfs            # namespace references, not storage at all
      3 ext4            # the only real disks: /, /boot, and one bind mount
      1 vfat            # /boot/efi
      1 tracefs         # kernel tracing, presented as files

Three of those sixty-odd filesystems store bytes on a disk. The rest are compressed read-only images, memory, or kernel state dressed up as files. The kernel keeps the list of types it can mount in a file of its own:

$ grep -v nodev /proc/filesystems      # the ones that need a real device
        ext3
        ext2
        ext4
        squashfs
        vfat
        fuseblk

Two of those lines are worth stopping on, because they are not really filesystems at all. fuseblk and fuse are FUSE, Filesystem in Userspace: a kernel module that forwards every filesystem operation to an ordinary program instead of handling it itself. That program can answer with anything it likes, which is how you get a filesystem backed by an SSH connection, a Windows partition, a cloud bucket, a ZIP archive or an AppImage. Two of them are running on this desktop right now:

$ grep fuse /proc/mounts
portal     /run/user/1000/doc  fuse.portal      rw,nosuid,nodev,relatime,user_id=1000,...
gvfsd-fuse /run/user/1000/gvfs fuse.gvfsd-fuse  rw,nosuid,nodev,relatime,user_id=1000,...

The price is a round trip into user space and back for every operation, so a FUSE filesystem is slower than a kernel one. The payoff is that anybody can write one, in any language, without kernel code and without root, and a crash takes down a process rather than the machine. sshfs, ntfs-3g, rclone mount and the file manager's trash folder are all this.

Every other line in that file carries the marker nodev, meaning "needs no block device": proc, sysfs, tmpfs, cgroup2, overlay and about twenty more. That marker is the formal difference between a filesystem that stores your data and one that only presents something as files, and the ratio between the two lists is the cleanest way to see how far Unix took the idea that everything should look like a file.

Back to top

5. Moderate Use Cases: Inodes, Names and Blocks

This section takes the three-part model from section 1.2 and shows each part on a real system. Everything here works on ext4 and needs no root.

5.1 Reading an Inode with stat

ls -l shows a polished summary. stat shows the inode almost raw, and it is the tool to reach for whenever a file behaves strangely:

$ printf 'x' > tiny.txt        # a file containing exactly one byte

$ stat tiny.txt
  File: tiny.txt
  Size: 1          Blocks: 8          IO Block: 4096   regular file
Device: 252,1      Inode: 61743294    Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1000/   pe7er)   Gid: ( 1000/   pe7er)
Access: 2026-09-06 22:14:03.840817742 +0200
Modify: 2026-09-06 22:14:03.840817742 +0200
Change: 2026-09-06 22:14:03.840817742 +0200
 Birth: 2026-09-06 22:14:03.840817742 +0200

Read that field by field, because every line is a fact the filesystem had to write down somewhere:

FieldMeaning
Size: 1The content is one byte long.
Blocks: 8Eight 512-byte units, so 4096 bytes, are allocated to hold that one byte. See 5.3.
Inode: 61743294The file's real identity on this filesystem. The name is not in the inode.
Device: 252,1Which filesystem it lives on. An inode number is only unique within one device.
Links: 1How many names point here. The file disappears when this reaches zero.
Access (atime)Last read. Cheap to skip, and mostly skipped; see 5.5.
Modify (mtime)Last time the content changed. This is the one ls -l shows.
Change (ctime)Last time the inode changed, which includes a rename or a chmod. You cannot set it.
BirthCreation time. ext4 has always stored it; Linux only gained a way to read it in 2017 with statx.

Notice what is not there: the filename appears only on the first line, and only because you typed it. The inode itself does not know what it is called.

5.2 A Name Is Not a File

Give the same inode a second name and both names are equally real. Neither is the original:

$ ln tiny.txt hard.txt          # a hard link: a second name, same inode

$ stat -c '%n inode=%i links=%h' tiny.txt hard.txt
tiny.txt inode=61743294 links=2
hard.txt inode=61743294 links=2

Delete either one and the data survives, because rm does not delete files. It removes a name and decreases the link count; the filesystem frees the blocks only when the count reaches zero and nobody has the file open. That is the whole mechanism behind the disk that stays full after you delete a log file, which the article on du traces in detail.

A symbolic link is a different thing entirely: its own inode, holding a path as text.

$ ln -s tiny.txt soft.txt

$ stat -c '%n inode=%i size=%s type=%F' soft.txt
soft.txt inode=61743295 size=8 type=symbolic link

Its size is 8 bytes, which is exactly the length of the string tiny.txt. That is all a symlink is: a small file whose content is a pathname, resolved fresh every time you use it. It can point across filesystems and at things that do not exist; a hard link can do neither, because an inode number means nothing outside its own filesystem.

5.3 The Metadata That Is Not in the Inode

The inode has a fixed set of fields, and for forty years that was all the metadata a file could have. Modern filesystems add a second, open-ended store: extended attributes, arbitrary name and value pairs attached to a file. They are the ext_attr feature and the user_xattr mount option that appear in the dumpe2fs output in section 6.1.

The usual tools are setfattr and getfattr from the attr package, which many distributions do not install by default. The system calls are always there, so anything can use them:

$ python3 -c "
import os
os.setxattr('tiny.txt', 'user.author', b'Peter Martin')
print(os.listxattr('tiny.txt'), os.getxattr('tiny.txt', 'user.author'))
"
['user.author'] b'Peter Martin'

Attribute names live in four namespaces, and the prefix decides who may write them:

NamespaceUsed for
user.Anything you like. Applications store tags, checksums, origin URLs and comments here.
security.SELinux labels, and security.capability, which is how a program gets one root power without being setuid root.
system.Kernel use, including access control lists.
trusted.Visible only to processes with CAP_SYS_ADMIN.

Access control lists then turn out to be a user of this same machinery rather than a separate feature. The classic owner/group/other bits cannot express "and this one extra user may write", so ACLs add per-user and per-group entries. They are switched on by the acl mount option, which is an ext4 default:

$ setfacl -m u:root:rw tiny.txt      # give one named user write access

$ ls -l tiny.txt
-rw-rw-r--+ 2 pe7er pe7er 1 Sep  6 22:14 tiny.txt      # note the trailing +

$ getfacl -c tiny.txt
user::rw-
user:root:rw-                       # the extra entry
group::rw-
mask::rw-
other::r--

That + after the permission string is the only hint ls gives you, and it is easy to miss when you are wondering why a user can write to a file that appears to forbid it. Where is the ACL stored? In an extended attribute:

$ python3 -c "import os; print(os.listxattr('tiny.txt'))"
['user.author', 'system.posix_acl_access']

Two practical consequences. The mask entry is an upper bound on every named entry, and chmod on a file with an ACL changes the mask rather than the entries, which is why permissions can appear to change back on their own. And extended attributes are not copied by default: use cp -a (or --preserve=xattr), rsync -X, and tar --xattrs, or you will silently drop ACLs, SELinux labels and file capabilities while "copying everything".

5.4 Why a One-Byte File Costs Four Kilobytes

The filesystem hands out space in whole blocks, normally 4096 bytes, so a file of one byte occupies a full block and wastes 4095 of it:

$ du -h --apparent-size tiny.txt
1       tiny.txt              # the content

$ du -h tiny.txt
4.0K    tiny.txt              # the allocation

At one file this is a curiosity. At scale it is a budget item: 100,000 files of 100 bytes hold 10 MB of content and occupy 410 MB of disk. Directories full of PHP sessions, mail messages, thumbnails or Git objects routinely cost ten times what they weigh.

The block size is chosen when the filesystem is created and cannot be changed afterwards without reformatting. On Linux it is effectively always 4096, because that is the memory page size, and a filesystem block larger than a page has never been simple to support.

5.5 Running Out of Inodes While df Says There Is Room

Because ext4 fixes the inode count at creation, you can exhaust files while space remains. The count follows one setting in /etc/mke2fs.conf:

$ grep inode_ratio /etc/mke2fs.conf
        inode_ratio = 16384

One inode for every 16 KB of filesystem. On a 2 TB volume that is the 124.8 million inodes from section 4.2, which is generous for ordinary use. It is not generous for a mail server, and mkfs lets you say so in advance:

$ mkfs.ext4 -T news  /dev/sdb1     # one inode per 4 KB: many small files
$ mkfs.ext4 -i 4096  /dev/sdb1     # the same thing, said directly
$ mkfs.ext4 -T largefile /dev/sdb1 # one inode per 1 MB: video, backups, images

The difference is not subtle. The same 512 MB device, formatted three ways, gets 512, 32,768 or 131,072 inodes. Choose wrong in the direction of "largefile" on a mail spool and the filesystem fills at a few hundred messages while df -h shows it empty. Choose wrong in the other direction and you spend disk on an inode table nobody uses.

When a disk reports "No space left on device" and df -h disagrees, check inodes first and then find the directory responsible:

$ df -i /var
$ du --inodes -d 1 /var | sort -rn | head    # count files, not bytes

XFS avoids this class of problem entirely by allocating inodes as it goes, which is a genuine reason to pick it for a filesystem that will hold an unpredictable number of small files.

5.6 Mount Options That Matter

Options are set in /etc/fstab and shown by findmnt. A handful change behaviour enough to be worth knowing by name:

OptionEffect
relatimeThe default. Updates the access time only if it is older than the modify time or older than a day, so reading a file usually writes nothing.
noatimeNever update access times at all. A small, safe win on busy servers, as long as nothing depends on atime.
roRead-only. The kernel also switches a filesystem to this by itself when it detects corruption.
nosuid, nodev, noexecRefuse setuid bits, device nodes and program execution. Standard hardening for /tmp and removable media.
errors=remount-roWhat to do when the filesystem hits an error. The ext4 default, and the reason a failing disk often shows up as "read-only file system".
discardTell the SSD about freed blocks immediately. Most systems prefer a weekly fstrim.timer instead.

You can watch relatime being lazy. Read a freshly written file and its access time does not move, because it is not yet older than the modify time:

$ findmnt -n -o OPTIONS /
rw,relatime

$ stat -c '%x' tiny.txt
2026-09-06 22:16:06.891262059 +0200
$ cat tiny.txt > /dev/null       # read it
$ stat -c '%x' tiny.txt
2026-09-06 22:16:06.891262059 +0200      # unchanged: no write happened

That is a deliberate compromise: relatime arrived in 2007 and became the kernel default two years later. Strict atime turned every read into a write, which was expensive and pointless for almost everyone, while relatime keeps the access time useful enough for the few tools that read it.

5.7 fstab, UUIDs, and Why Device Names Are Not Trustworthy

Device names like /dev/sda2 are assigned in the order the kernel finds hardware. Add a disk, move a cable, or boot a virtual machine with a different controller, and yesterday's /dev/sdb is today's /dev/sdc. So filesystems are identified by a UUID written into their own superblock:

$ grep -v '^#' /etc/fstab
/dev/disk/by-uuid/e6894fe9-f40d-42ef-8183-6a31e0996127 /boot     ext4 defaults 0 1
/dev/disk/by-uuid/DAB0-BDC6                            /boot/efi vfat defaults 0 1

$ lsblk -f          # the same UUIDs, read from the filesystems themselves

The last two numbers on each line are worth a sentence. The first is for the ancient dump backup tool and is always 0. The second is the fsck order at boot: 1 for the root filesystem, 2 for the others, 0 for "never check". Set it to 0 on a filesystem that might not be present at boot, or on a filesystem, such as a network mount, where a boot-time check makes no sense.

One warning that costs people a lot of time: a broken fstab line stops the boot. Test a new entry with mount -a while the system is up and running, and only then reboot.

Back to top

6. Advanced Use Cases: How the Filesystem Really Works

Everything so far was visible from outside. This section opens the filesystem up: where a file's blocks are recorded, when they are chosen, what the journal protects, and what copy-on-write changes about all of it.

6.1 A Filesystem in a File

You do not need a spare disk to experiment, and you do not need root. mkfs is happy to format an ordinary file, and the ext tools are happy to inspect one:

$ truncate -s 512M test.img       # a 512 MB file full of nothing
$ mkfs.ext4 -q -F test.img        # -F: yes, I know it is not a block device

$ dumpe2fs -h test.img
Filesystem features:      has_journal ext_attr resize_inode dir_index filetype
                          extent 64bit flex_bg sparse_super large_file huge_file
                          dir_nlink extra_isize metadata_csum
Inode count:              32768
Block count:              131072
Reserved block count:     6553
Free blocks:              124719
Block size:               4096
Inodes per group:         8192
Inode size:               256
Journal inode:            8
Total journal size:       16M

That is a complete, real ext4 filesystem you can break, fill, and throw away. Mounting it does need root (sudo mount -o loop test.img /mnt), but the interesting parts do not: dumpe2fs reads the superblock, and debugfs reads and writes the structures directly. Every ext4 example below was produced this way.

Read the feature list at the top like a specification. has_journal is section 6.6, extent is section 6.2, metadata_csum means the filesystem checksums its own bookkeeping so corruption is detected rather than followed, and resize_inode is what makes online growing possible later.

6.2 Extents: How a File Records Where It Lives

The old ext2 and ext3 layout stored a list of block numbers, one entry per block, with indirect blocks once a file got large. A 1 GB file needed a quarter of a million entries. Extents replaced that with ranges: "1280 blocks, starting at block 2127". Put a file into the test image and ask debugfs what the inode says:

$ debugfs -w -R "write big.bin big.bin" test.img     # 5 MB of random data
Allocated inode: 12

$ debugfs -R "stat big.bin" test.img
Inode: 12   Type: regular    Mode:  0664   Flags: 0x80000
Links: 1   Blockcount: 10240
Size: 5242880
EXTENTS:
(0-1279):2127-3406

One line describes the whole file: logical blocks 0 to 1279 live in physical blocks 2127 to 3406. That is what Flags: 0x80000 means, the extents flag, and it is why lsattr shows an e on every file on a modern ext4 filesystem.

On a mounted filesystem the same information comes from filefrag, and it is a good habit to check it when a file feels slow to read:

$ filefrag /usr/bin/bash /var/log/syslog
/usr/bin/bash: 1 extent found
/var/log/syslog: 9 extents found

A program installed once, in one piece, needs one extent. A log file grown a few lines at a time over weeks, while other files were allocated around it, ends up in nine. This is fragmentation, and on ext4 it is mild enough that the answer is almost always "leave it alone". If a specific file really is badly fragmented, e4defrag can rewrite it, but a filesystem below about 80% full and not chronically starved of space keeps itself tidy.

6.3 Delayed Allocation: The File Is Not There Yet

Here is a result that looks like a bug and is not. Write a file, then immediately ask where its blocks are:

$ head -c 5M /dev/urandom > big.bin
$ filefrag -v big.bin
 ext:     logical_offset:   physical_offset: length:   expected: flags:
   0:        0..    1279:         0..      0:      0:            last,unknown_loc,delalloc,eof
big.bin: 1 extent found

Physical offset zero, length zero, and the flag delalloc. The file exists, the data is safe in memory, and the filesystem has not yet decided where on disk it goes. Force the write out and ask again:

$ sync
$ filefrag -v big.bin
 ext:     logical_offset:      physical_offset: length:   expected: flags:
   0:        0..    1023:  434518016..434519039:   1024:
   1:     1024..    1279:  434517760..434518015:    256:  434519040: last,eof

This is delayed allocation, and it is one of the biggest reasons ext4 is faster than ext3. By waiting until the data is actually flushed, the filesystem knows the final size and can pick one good range instead of guessing block by block as the writes arrive. Files written and deleted quickly, which is most temporary files, may never be allocated on disk at all.

The cost is the thing people notice after a power cut: a file can exist with the right size and contain zeroes, because the metadata reached the disk and the data did not. That is why a program that must not lose data calls fsync(), and why databases and package managers all do. The kernel flushes dirty pages within a few seconds anyway, and you can watch the queue:

$ cat /sys/fs/ext4/dm-1/delayed_allocation_blocks    # blocks waiting for a home
1

6.4 The Page Cache: Your Disk Is Rarely the Disk

Delayed allocation only makes sense because of a bigger idea underneath it. Almost nothing you read or write goes straight to the device. The kernel keeps file contents in spare RAM, the page cache, and serves what it can from there. Read a large file you have not touched since boot, and watch the cache grow by exactly its size:

$ ls -lh /usr/lib/balena-etcher/balena-etcher
-rwxr-xr-x 1 root root 172M Oct 10  2024 ...

$ grep ^Cached: /proc/meminfo
Cached:         10849064 kB

$ time cat /usr/lib/balena-etcher/balena-etcher > /dev/null
real    0m0.138s

$ grep ^Cached: /proc/meminfo
Cached:         11024544 kB          # 175,480 kB more: the whole file

$ time cat /usr/lib/balena-etcher/balena-etcher > /dev/null
real    0m0.020s                     # seven times faster, from memory

This is why free memory on a healthy Linux server looks alarmingly low. The kernel spends every spare page on cached file data and hands it back the moment a program needs it, so cache is not memory in use, it is memory doing something useful in the meantime. It is also why any timing you take of a filesystem operation is optimistic the second time you run it.

Writes are cached in the same way, and that is the part with consequences. A write() returns as soon as the data is in a dirty page, not when it is on the device:

$ grep -E '^(Dirty|Writeback):' /proc/meminfo
Dirty:                64 kB          # written by programs, not yet on disk
Writeback:            32 kB          # on its way to the device right now

The kernel flushes dirty pages within a few seconds, and sync forces the queue out now. But a program that must know its data is safe has to call fsync() on the file, and then fsync() on the directory if the file is new, because the name is a separate change. Databases, package managers and mail servers all do this; that is the difference between a program that survives a power cut and one that loses its last minute of work.

The kernel caches the naming layer too. The dentry cache remembers the result of pathname lookups, so walking /var/www/html/index.php a second time does not re-read four directories from disk. That cache is what makes a repeated du run so much faster than the first, and vm.vfs_cache_pressure (default 100) is the dial that decides how eagerly the kernel throws it away under memory pressure.

6.5 Sparse Files and Preallocation

A file's size and its cost are two different numbers, and they can differ in both directions. A sparse file claims a size it has never written:

$ dd if=/dev/zero of=sparse.bin bs=1 count=0 seek=100M   # seek, write nothing

$ ls -lh sparse.bin
-rw-rw-r-- 1 pe7er pe7er 100M ... sparse.bin

$ du -h sparse.bin
0       sparse.bin

$ filefrag sparse.bin
sparse.bin: 0 extents found

Zero extents: the file has no blocks at all. The filesystem simply records that the range is empty and returns zeroes when you read it. Virtual machine disks, container images, database files and preallocated logs are routinely sparse, which is why copying one carelessly can turn 100 MB into 100 GB. Use cp --sparse=always, rsync -S, or tar -S to keep the holes.

fallocate does the opposite. It reserves the blocks now, without writing anything, so the space cannot be taken by anyone else:

$ fallocate -l 50M prealloc.bin
$ du -h prealloc.bin                 50M    # allocated
$ du -h --apparent-size prealloc.bin 50M    # and claimed

Both numbers agree, which is the difference from the sparse file above. This is how a database guarantees room for its next segment before it needs it, and it is instant because no data is written.

6.6 The Journal: What It Protects and What It Does Not

Writing a file is never one operation. Creating invoice.pdf means allocating an inode, marking blocks used, writing the data, and adding a directory entry, and a power cut between any two of those leaves the filesystem inconsistent: blocks marked used that belong to nothing, or a directory entry pointing at an inode that was never written.

The journal fixes this by writing intent before action. The filesystem writes "I am about to make these metadata changes" into a reserved area, then makes them, then marks the entry done. After a crash the recovery reads the journal, redoes anything committed and discards anything incomplete. It is a small dedicated file, and you can look at it:

$ dumpe2fs -h test.img | grep -i journal
Journal inode:            8
Total journal size:       16M

$ debugfs -R "stat <8>" test.img | grep Size
Size: 16777216

Inode 8 is always the journal on an ext filesystem, and 16 MB of a 512 MB filesystem is spent on it before you store anything. That is the deal: a few percent of the disk in exchange for never waiting hours for a full fsck after a power cut.

The journal protects the filesystem's consistency, not your data. After recovery, ext4 guarantees that its own structures make sense. It does not guarantee that the last thing your application wrote is there. Only fsync() does that, and only for a program that calls it.

ext4 offers three journalling modes, set with the data= mount option:

ModeWhat is journalledTrade-off
data=orderedMetadata only, but data is always written before the metadata that points at itThe default. Safe and fast enough for almost everyone.
data=writebackMetadata only, in any orderSlightly faster, and a crash can leave a file pointing at somebody's old data.
data=journalMetadata and file contents bothSafest, and everything is written twice. Rarely worth it.

XFS and the copy-on-write filesystems solve the same problem differently: XFS journals metadata too, while Btrfs and ZFS avoid the question by never overwriting live data in the first place.

ext4 and XFS overwrite in place: change a byte in the middle of a file and the same block is rewritten. Btrfs and ZFS never do that. They write the change to a free block and then update the pointers, which is copy-on-write, and three useful features fall out of it almost for free.

Reflinks are copies that share blocks until one side changes. The copy is instant and costs no space:

$ cp --reflink=always big.bin clone.bin        # on Btrfs, XFS or ZFS

$ cp --reflink=always big.bin clone.bin        # on this machine, ext4
cp: failed to clone 'clone.bin' from 'big.bin': Operation not supported

Snapshots are the same idea applied to a whole subvolume or dataset. Taking one is instant, and it costs nothing until the original data starts changing. Compression is transparent, so a file can occupy fewer blocks than it contains.

All three break an assumption that every path-walking tool makes: that a block belongs to exactly one file. A snapshot holds blocks that no path leads to. A reflinked pair reports full size twice while occupying the space once. So on these filesystems the honest answer to "where did my space go" does not come from du at all, and the article on du ends at exactly this boundary. Use the filesystem's own accounting instead:

$ btrfs filesystem usage /mnt      # real free space, including metadata and RAID profile
$ btrfs subvolume list /mnt        # what snapshots exist
$ btrfs qgroup show -p /mnt        # per-subvolume usage, once quotas are enabled

$ zfs list -o space                # USED split into snapshots, children and data
$ zfs get compressratio tank       # what compression actually saved

Two practical warnings come with copy-on-write. Do not let one of these filesystems get very full: they need free blocks to write anything at all, including deletions, and they behave badly above roughly 80 to 90 percent. And a snapshot is not a backup. It lives on the same devices as the original, so it protects you from a mistake, not from a failed disk or a burnt building.

6.8 Checksums and Silent Corruption

Look back at the feature list in section 6.1 and read metadata_csum carefully, because the word doing the work is metadata. ext4 checksums its superblock, group descriptors, inodes, extent trees and directory blocks, so a corrupted structure is detected instead of followed into a crash. It does not checksum your file contents.

The practical meaning is uncomfortable. If a bit flips inside a data block, through a failing drive, a bad cable, a firmware bug or cosmic bad luck, ext4 and XFS hand the damaged bytes to your application without a word. Nothing reports an error, because as far as the filesystem is concerned nothing went wrong. This is what people mean by silent corruption, or bitrot, and it is silent in the exact sense that no layer is looking.

Btrfs and ZFS take the other position: every data block is checksummed, and the checksum is verified on every read. A mismatch is an error, not a surprise, and if the filesystem has a second copy (RAID1, a mirror, or ZFS copies=2) it returns the good one and repairs the bad one. Both can also check the whole filesystem in the background while it stays mounted:

$ sudo btrfs scrub start -B /mnt      # read and verify everything, and report
$ sudo btrfs device stats /mnt        # per-device error counters that persist

$ sudo zpool scrub tank
$ zpool status tank                   # CKSUM column: checksum errors found

Note what this does not do without redundancy. On a single disk, checksums turn silent corruption into a loud, specific error naming the damaged file, which is a real improvement but is still not a repair. Detection needs checksums; correction needs a second copy. And classic RAID gives you the second copy without the checksums, so it can see that two mirrors disagree but not which one is right. Integrity and redundancy are two different features, and you want both.

6.9 Growing, Shrinking and Checking

A filesystem is not fixed at its creation size, but the rules differ per type and the differences matter when you plan a volume:

Operationext4XFS
Growresize2fs, while mountedxfs_growfs, while mounted
Shrinkresize2fs, unmounted only, after e2fsck -fImpossible. Back up, recreate, restore.
Checke2fsck -f, unmounted onlyxfs_repair, unmounted only

Growing happens in two steps, and forgetting the first is the usual mistake: enlarge the layer underneath (the partition, or the LVM logical volume) and only then tell the filesystem to use the new room.

The rule about checking has no exceptions worth taking. Never run a repair tool on a mounted filesystem. The kernel holds structures in memory that the repair tool cannot see, and the two writing at once turns a small problem into a lost filesystem. Boot from rescue media, or unmount first.

Back to top

7. Something Most Users Do Not Know

7.1 Filenames Are Bytes, Not Text

Linux does not store filenames as text. It stores a sequence of bytes, and it enforces exactly two rules: the sequence may not contain a slash, because that separates path components, and it may not contain a zero byte, because that ends the string. Everything else is legal:

$ touch $'bad\xffname'          # 0xff is not valid UTF-8 at all
$ touch $'two\nlines'           # a newline inside the name

$ ls -b
bad\377name
two\nlines

Both files exist and behave normally. The kernel never checked whether the bytes spell anything; that is your terminal's job, and ls -b (short for "escape") is how you see what is really there.

This is not a curiosity but the reason for a whole family of scripting habits. A filename can contain a newline, so for f in $(ls) is broken by construction; a filename can start with a dash, so rm * can turn a file into a flag; a filename can hold invalid UTF-8, so a script that assumes decodable text will crash on it. It is why find -print0 and xargs -0 exist, and why careful scripts quote every variable.

7.2 The 255 Limit Is Bytes, So Accents Cost Double

The Namelen: 255 from section 4.1 is a byte count, not a character count. In pure ASCII the two are the same, and 255 characters fit exactly:

$ touch $(printf 'a%.0s' {1..255})     # 255 bytes: fine
$ touch $(printf 'a%.0s' {1..256})
touch: cannot touch 'aaaa...aaa': File name too long

Now use a letter that UTF-8 stores in two bytes, such as e with an acute accent, and the limit arrives at half the length:

$ touch $(printf '\xc3\xa9%.0s' {1..127})   # 127 letters, 254 bytes: fine
$ touch $(printf '\xc3\xa9%.0s' {1..128})   # 128 letters, 256 bytes
touch: cannot touch '...': File name too long

A Dutch, French or German filename can therefore be rejected at 128 characters while an English one of 255 is accepted, and an emoji, at four bytes each, gets you 63. The same arithmetic applies to the 4096-byte path limit, which is where deeply nested directories with long localised names occasionally hit a wall that looks like a bug in the application.

7.3 A New Filesystem Is Already Missing Space

Format a device and a noticeable part of it is gone before you write a single file. The 512 MB test image from section 6.1 says so plainly:

Block count:              131072       # 131072 x 4096 = 512 MiB
Free blocks:              124719       # 6353 blocks gone already = 24.8 MiB
Reserved block count:       6553       # another 25.6 MiB you cannot use
Inode count:              32768        # x 256 bytes each = 8 MiB of inode table
Total journal size:         16M        # the journal, before any data

The arithmetic closes almost exactly: 8 MiB of inode table plus a 16 MiB journal plus group bookkeeping is the 24.8 MiB that vanished. Then 5% is reserved for root on top of it, so about 10% of a fresh filesystem is spoken for.

Scale that to the 2 TB filesystem in section 4.2 and the numbers stop being cute. Its 124,829,696 inodes at 256 bytes each are 29.8 GiB of inode table, written at mkfs time and permanent. Its 5% root reserve is another 95 GiB, which is the gap between Free and Available in the stat -f output. That reserve is not waste: it keeps root able to log in and syslog able to write when the disk fills, which is precisely when you need both. On a pure data volume, where nothing depends on that safety margin, you can take most of it back:

$ sudo tune2fs -m 1 /dev/sdb1     # reduce the root reserve from 5% to 1%

Do that on a data disk. Do not do it on the root filesystem.

7.4 FAT Rounds Every Timestamp to Two Seconds

The FAT format stores a modification time in 16 bits, and spends only five of them on seconds, so it counts in units of two. You can prove it without a USB stick, using an image and the mtools package, which reads FAT without mounting:

$ truncate -s 64M fat.img && mkfs.vfat -F 32 -n TESTFAT fat.img
$ for s in 01 02 03; do
>   touch -d "2026-01-01 10:00:$s" t$s.txt
>   mcopy -m -o -i fat.img t$s.txt ::/      # -m preserves the timestamp
> done

Now read the raw 16-bit time field out of each directory entry, at offset 22, and decode it:

$ python3 -c "
d = open('fat.img','rb').read()
for n in (b'T01     TXT', b'T02     TXT', b'T03     TXT'):
    i = d.find(n); t = int.from_bytes(d[i+22:i+24], 'little')
    print(n.decode(), '%02d:%02d:%02d' % (t>>11, (t>>5)&63, (t&31)*2))
"
T01     TXT 10:00:00      # 10:00:01 rounded down
T02     TXT 10:00:02
T03     TXT 10:00:02      # 10:00:03 rounded down too

Three files written one second apart, two distinct timestamps. This is why a backup or sync tool that compares modification times across a FAT-formatted stick reports files as changed when nothing changed, and why rsync has a --modify-window=1 option that exists almost entirely for this.

The same directory entry shows the other FAT compromise. Every long name is stored twice, as a classic 8.3 name plus hidden extra entries holding the real one:

$ mdir -i fat.img ::/
MYLONG~1 TXT         6 2026-09-06  22:16  MyLongFileName.txt
tiny     txt         1 2026-09-06  22:16

And because FAT compares names case-insensitively, MyLongFileName.txt and mylongfilename.TXT are the same file there and two different files on ext4. Copy a directory from Linux to a USB stick and files can silently overwrite each other for that reason alone.

7.5 Your Filesystem Counts Every Byte Ever Written to It

ext4 keeps a lifetime write counter in its superblock and exposes it through /sys, no root needed:

$ cat /sys/fs/ext4/dm-1/lifetime_write_kbytes
19500766413                        # about 18.2 TiB since this disk was formatted

$ cat /sys/fs/ext4/dm-1/session_write_kbytes
6183360                            # about 5.9 GiB since the last mount

That first number is a genuinely useful figure on an SSD, where endurance is quoted in total bytes written. It is also a quiet reminder of how much writing an ordinary desktop does: eighteen terabytes through a filesystem whose owner has never deliberately written more than a few hundred gigabytes.

The same directory holds the rest of ext4's live knobs and counters, and /sys/fs/ext4/features/ lists what your kernel's ext4 driver can do, which is not the same as what your filesystem was created with.

7.6 ext4 Can Be Case-Insensitive, and ext3 Runs Out of Time in 2038

Two features that contradict what most people assume about ext4.

The first is casefolding. Since Linux 5.2 ext4 can compare names case-insensitively, per directory, which exists mainly so that Wine and Android can run software that expects Windows behaviour. It is off unless the filesystem was created with it:

$ mkfs.ext4 -q -F -O casefold cf.img
$ dumpe2fs -h cf.img | grep features
Filesystem features: has_journal ext_attr resize_inode dir_index filetype extent
                     64bit flex_bg casefold sparse_super large_file huge_file ...

The second is the year 2038 problem. A 32-bit signed timestamp counting seconds from 1970 overflows on 19 January 2038. ext4 escaped it by using 256-byte inodes with extra timestamp bits, which push the limit to the year 2446:

$ touch -d '2100-06-01 12:00:00' future.txt
$ stat -c '%y' future.txt
2100-06-01 12:00:00.000000000 +0200        # accepted without complaint

An old ext3 filesystem, or an ext4 one created long ago with 128-byte inodes, has no room for those bits and stops at 2038. The inode size is fixed at creation and cannot be changed, so the only fix is to recreate the filesystem. It is worth checking the Inode size line in dumpe2fs on any server whose disks were formatted more than a decade ago.

7.7 Your SSD May Never Be Told What You Deleted

An SSD cannot overwrite a block; it can only erase a large region and rewrite it. So the controller needs to know which blocks no longer hold anything useful, or it spends its life carefully preserving data that was deleted months ago. TRIM, also called discard, is the filesystem telling the device "these blocks are free now".

Whether that message arrives is another question, and the answer is per layer. lsblk -D prints the discard limits of every layer in the stack, and on this machine the chain breaks:

$ lsblk -D -o NAME,DISC-GRAN,DISC-MAX,MOUNTPOINTS -e7
NAME                        DISC-GRAN DISC-MAX MOUNTPOINTS
nvme0n1                          512B       2T
├─nvme0n1p1                      512B       2T /boot/efi
├─nvme0n1p2                      512B       2T /boot
└─nvme0n1p3                      512B       2T
  └─dm_crypt-0                   512B       0B
    └─ubuntu--vg-ubuntu--lv      512B       0B /

The disk and its partitions accept discards up to 2 TB. The LUKS layer on top of them reports 0B, and so does the logical volume above it. The root filesystem on this laptop has never trimmed anything, and never will while the stack looks like this, because LUKS drops discard requests unless it is opened with --allow-discards.

That default is deliberate rather than an oversight. Passing discards through encryption tells anyone who can read the raw device which blocks are unused, which leaks the filesystem's shape and how full it is, and that is information an encrypted volume is supposed to keep. The trade is real: pass discards through and the SSD stays fast, or keep them and give up a little of what encryption was for.

Where discards do get through, prefer the weekly batch job over the mount option: fstrim walks the free space once, while discard issues a command on every delete and can stutter on some hardware.

$ systemctl is-enabled fstrim.timer
enabled

$ sudo fstrim -av                 # trim every mounted filesystem that supports it

One more thing this makes clear: the filesystem does not decide where your data physically lands. The SSD's own translation layer does the placement, wear levelling and garbage collection, and it does not tell the filesystem about any of it. Every block number in section 6.2 is a fiction the device maintains for your convenience.

7.8 Crossing to Windows and macOS

Most Linux servers are administered from a Mac or a Windows laptop, and files move between the three all day. You do not need to know how NTFS or APFS work inside to do that safely, but you do need to know where they disagree with Linux, because each disagreement produces a bug that looks like something else entirely.

Case is the first one. Linux compares filenames byte for byte, so Logo.png and logo.png are two different files. NTFS and APFS are case-insensitive by default, so on a laptop they are one file. A web page that asks for logo.png while the repository contains Logo.png therefore works perfectly on the developer's machine and returns 404 on the server, and nothing on the laptop will ever warn about it. Section 7.4 showed the same collision on a FAT stick; this is the version that reaches production.

Unicode normalisation is the second, and it is nastier because you cannot see it. The letter e with an acute accent can be stored as one character, or as a plain e followed by a combining accent. macOS normalises filenames so both spellings land on the same file. Linux stores exactly the bytes it was given:

$ printf 'nfc' > "$(printf 'caf\xc3\xa9')"     # e-acute as one character
$ printf 'nfd' > "$(printf 'cafe\xcc\x81')"    # plain e plus a combining accent

$ ls -1 | grep caf | while IFS= read -r f; do
>   printf '%s  %s  %s\n' "$f" "$(printf '%s' "$f" | xxd -p)" "$(cat -- "$f")"
> done
café  63616665cc81  nfd
café  636166c3a9    nfc

Two files, one directory, identical on screen, different bytes. Copy a tree from a Mac to a Linux server and a filename typed on the server can fail to match the one that arrived, with the error "no such file" for a file you are looking straight at. It is also why git status on such a tree sometimes reports the same file as both deleted and untracked.

Legal names are the third. Linux forbids exactly two things in a filename, the slash and the zero byte. Windows forbids a good deal more, and Linux will happily create every one of them:

$ touch 'CON' 'aux.txt' 'a:b.txt' 'what?.txt' 'trailing.' 'space '
$ ls -b
a:b.txt
aux.txt
CON
space\ 
trailing.
what?.txt

All six are ordinary files here and none of them can exist on Windows. The characters < > : " / \ | ? * are forbidden there, as are control characters, a trailing dot and a trailing space, and so are the old device names CON, PRN, AUX, NUL, COM1 to COM9 and LPT1 to LPT9, with or without an extension. A backup tar from a Linux server can therefore refuse to extract on a Windows machine, and the file that stops it is usually a log or an export with a colon in a timestamp.

And metadata mostly does not survive at all. Ownership, permission bits, symlinks and the extended attributes from section 5.3 have no place to live on FAT or exFAT, and what ls -l shows on such a volume is invented by the mount options. macOS works around this by writing its own metadata into companion files, which is why a USB stick that has visited a Mac is full of ._filename entries and .DS_Store files that mean nothing to Linux.

Finally, what Linux can actually read. Three of the four foreign filesystems have a driver in the mainline kernel, and the missing one is the one people ask about most:

$ for m in ntfs3 exfat hfsplus apfs; do printf '%-8s ' $m; modinfo -F filename $m 2>/dev/null || echo "(no module)"; done
ntfs3    /lib/modules/6.11.0-29-generic/kernel/fs/ntfs3/ntfs3.ko.zst
exfat    /lib/modules/6.11.0-29-generic/kernel/fs/exfat/exfat.ko.zst
hfsplus  /lib/modules/6.11.0-29-generic/kernel/fs/hfsplus/hfsplus.ko.zst
apfs     (no module)
FilesystemComes fromOn Linux
exFATWindows, and every large SD cardRead and write, in the kernel. The safest choice for a stick that must work everywhere.
NTFSWindows system disksRead and write with ntfs3. Windows ACLs do not map to POSIX permissions, so ownership comes from mount options.
HFS+Macs before 2017Readable with hfsplus; writing is refused while the volume's journal is active.
APFSEvery Mac since 2017No mainline support. Third-party FUSE drivers are read-only and unofficial. Copy over the network instead.

All of which gives one boring rule that removes every problem in this section at once: for anything that will cross between the three, use lowercase ASCII names, no spaces, no colons, and no trailing dots. It looks like superstition until the day a deployment fails on a capital letter.

7.9 The Filesystem in Your Pocket

Here is the fact that reorganises this whole subject: Android is Linux. Not Linux-like, not Linux-inspired. A phone runs the same kernel, the same VFS, the same inodes, the same extents and the same page cache described in every section above, usually on ext4 or F2FS. The userspace on top is Android's own rather than GNU, but the filesystem layer is the one this article has been taking apart. If you understand a Linux server's storage, you already understand roughly ninety percent of a phone's.

The differences are worth the other ten percent, and they start with the flash chip. A phone stores data in eMMC or UFS memory, which cannot overwrite a block and erases in large regions, exactly like the SSD in section 7.7, and which hides that behind a translation layer in the same way. F2FS is the filesystem written for it: it is log-structured, meaning it does not scatter updates across the device but appends them sequentially into large segments and reclaims old segments in the background. That pattern is the one flash hardware is fastest at, and it is why a filesystem designed by Samsung in 2012 for phones is now common on the data partition of Android devices while ext4 remains widespread elsewhere on the same phone.

You do not need a phone to meet it. The driver is on your laptop:

$ modinfo -F filename f2fs
/lib/modules/6.11.0-29-generic/kernel/fs/f2fs/f2fs.ko.zst

And the traffic runs the other way too. Section 7.5 pointed at /sys/fs/ext4/features/ without saying what is in it. Look at three of the entries:

$ ls /sys/fs/ext4/features/ | grep -E 'casefold|encryption|verity'
casefold
encrypted_casefold
encryption
test_dummy_encryption_v2
verity

All three exist largely because of Android, and they are sitting in the ext4 driver of an ordinary Ubuntu laptop:

ext4 featureWhat Android does with it
encryptionfscrypt, per-directory encryption in the filesystem rather than on the whole device. It is what lets a phone encrypt each user's files with a different key, and why alarms and incoming calls work after a reboot but before the first unlock.
verityfs-verity, transparent read-time integrity checking against a hash tree. Android uses it so that a system file or an installed app cannot be altered on disk without the change being noticed on the next read.
casefoldCase-insensitive lookups per directory, from section 7.6. Android's emulated storage has behaved case-insensitively since it imitated a FAT-formatted SD card, and this is how the kernel provides that natively.

Three more differences change how a phone feels compared with a server, and each is a layer bolted on top of the filesystems in this article rather than a new kind of filesystem:

  • /sdcard is not an SD card. On a modern phone it is emulated storage that lives inside the data partition and is presented to apps through FUSE, the mechanism from section 4.4. That indirection is where per-app permission filtering happens, and it is why file access on Android is slower than the raw filesystem underneath it.
  • The system partition is read-only and verified. Android mounts it through dm-verity, a device-mapper target that checks every block against a signed hash tree as it is read. It is the same device-mapper layer that carries LUKS and LVM in the stack in section 1.4, used for integrity instead of encryption.
  • Every app is a Unix user. Android gives each installed application its own UID and relies on ordinary POSIX file permissions to keep app data private. The permission bits from section 5.1 are doing the work; the phone just never shows them to you.

Apple went the other way. iPhones and iPads have run APFS since iOS 10.3 in 2017, the same filesystem as the Macs in section 7.8, with the same copy-on-write clones and snapshots from section 6.7. The difference is that iOS presents no filesystem to the user at all: there is no path to type, and the Files app is a curated view rather than a window onto a tree.

So the honest summary of mobile storage is that there is much less of a difference than the marketing suggests. One of the two big phone platforms runs the filesystems in this article on hardware that behaves like a small SSD, and has pushed three features back into the ext4 driver on your desktop. The other runs the filesystem from the Mac section and simply hides it.

7.10 Knowing Where the Filesystem Stops

When you needReach for
What is mounted, and with which optionsfindmnt, lsblk -f, /proc/mounts
Which files are using the spacedu, ncdu, and the article on du
Facts about one filestat, filefrag, lsattr
Facts about the whole filesystemdumpe2fs -h, tune2fs -l, xfs_info, btrfs filesystem usage
Space held by deleted-but-open fileslsof -nP +L1: no filesystem tool can see them, because they have no name
Structures no mounted-filesystem tool will showdebugfs on ext, xfs_db on XFS
Per-user limits rather than reportsQuotas: quota, repquota, edquota
The layers below the filesystemlsblk, cryptsetup status, lvs, smartctl

That last row is the one people skip. When a filesystem reports errors, the cause is often a layer below it, and smartctl -a /dev/nvme0n1 answers a question no filesystem tool can.

Back to top

8. Best Practices

  • Know which filesystem you are on before you change anything. Run df -T first. Half the advice on the internet is correct for ext4 and wrong for Btrfs, or the other way around.
  • Choose ext4 unless you can name the reason not to. It is the best tested, the best understood by recovery tools, and the least surprising. XFS for very large files and heavy parallel writes, Btrfs or ZFS when you actually want snapshots and checksums.
  • Mount by UUID, never by /dev/sdX. Device names change when hardware changes. A UUID lives in the filesystem's own superblock and travels with it.
  • Test a new fstab line with mount -a before rebooting. A typo there stops the boot, and fixing it means rescue media.
  • Watch inodes as well as blocks. Put df -i next to df -h in your monitoring. On ext4 the inode count is fixed forever at mkfs time.
  • Size the inode ratio when you create a filesystem for many small files. mkfs.ext4 -i 4096 for mail spools and caches; the default 16 KB ratio assumes ordinary mixed use.
  • Leave the 5% root reserve alone on /. Reclaim it with tune2fs -m 1 on data volumes only. It is what keeps a full server manageable.
  • Do not run a copy-on-write filesystem near full. Btrfs and ZFS need free blocks to write anything, including deletions. Keep them below roughly 80%.
  • Never run fsck, e2fsck or xfs_repair on a mounted filesystem. Unmount, or boot rescue media. There is no version of this that is safe to try.
  • Remember that a snapshot is not a backup. It shares the same devices as the original. It protects you from your own mistake, not from a dead disk.
  • Copy metadata, not just contents. cp -a, rsync -X and tar --xattrs keep extended attributes, ACLs, SELinux labels and file capabilities. A plain cp silently drops all of them.
  • Scrub a checksumming filesystem on a schedule. Btrfs and ZFS only find silent corruption when something reads the data, so run btrfs scrub or zpool scrub monthly and read the error counters afterwards.
  • Check that discards actually reach the device. lsblk -D shows every layer. A 0B in DISC-MAX on a LUKS or LVM layer means the SSD below it is never told what you deleted.
  • Preserve sparse files when copying. cp --sparse=always, rsync -S, tar -S, or watch a 40 GB virtual disk become a full 40 GB.
  • Let fstrim.timer handle SSD trimming. A weekly batch trim is kinder to performance than the discard mount option on most hardware.
  • Experiment on an image file, not on a server. truncate plus mkfs gives you a real filesystem you can destroy, and dumpe2fs and debugfs read it without root.
  • Read the documentation. The filesystem manual pages are unusually good, and they answer version-specific questions no article can.
$ man 5 fstab         # the mount table, field by field
$ man 8 mount         # the options shared by every filesystem
$ man 5 ext4          # every ext4-specific mount option, in one place
$ man 8 mkfs.ext4     # creation options: -b, -i, -N, -T, -O
$ man 8 tune2fs       # what you can still change afterwards
$ man 8 fstrim        # SSD trimming, and why the timer is preferred
$ man 7 xattr         # the four extended-attribute namespaces
$ man 5 acl           # access control lists, and how the mask works
$ man 2 fsync         # the only promise that your data reached the device
Back to top

9. Common Mistakes

9.1 Myth versus Reality

MythReality
"A file is one object, and its name is part of it." A file is an inode plus its blocks. The name is a separate entry in a directory, and there can be several of them or, briefly, none.
"rm deletes a file." It removes a name and decreases a link count. The space returns when the count reaches zero and no process still has the file open.
"Deleting frees the space immediately." Not while a process holds the file open. This is the classic full disk that du cannot explain and lsof -nP +L1 can.
"A directory contains its files." A directory is a file containing a table of names and inode numbers. The data is not in it, which is why moving a file inside one filesystem copies nothing.
"No space left on device means the disk is full." It can equally mean the inodes are gone. df -h and df -i answer two different questions.
"Linux filesystems never fragment." They fragment, they just cope well. filefrag shows the truth, and a nine-extent log file is normal and harmless.
"The journal means I cannot lose data." The journal protects the filesystem's structures. Your application's last write is protected only by fsync().
"Filenames are text." They are bytes. Anything except a slash and a zero byte is legal, including newlines and invalid UTF-8.
"The file size is how much disk it uses." A one-byte file uses 4 KB, a sparse 100 MB file uses nothing, and a compressed or reflinked file uses less than it claims.
"You can shrink any filesystem." ext4 can shrink, unmounted. XFS cannot shrink at all, ever. That is a decision you make at mkfs time.
"ext4 protects my files with checksums." It checksums its own metadata. A flipped bit inside your data is handed to your application in silence. Only Btrfs and ZFS checksum content.
"cp copies everything about a file." Plain cp drops extended attributes, ACLs and file capabilities. That needs cp -a, and sparse files need --sparse=always.
"Almost no free memory means the server needs more RAM." Most of it is page cache holding file data, and the kernel gives it back instantly when a program asks. Read available, not free.
"write() returned, so the data is on disk." It is in a dirty page in memory. Only fsync() on the file, plus fsync() on its directory for a new file, promises more.
"My SSD gets trimmed because fstrim.timer is enabled." Only if every layer passes discards down. LUKS drops them by default, and lsblk -D shows it as DISC-MAX 0B.
"A filename that works on my Mac works on the server." Linux is case-sensitive and byte-exact where macOS and Windows are neither. The same name can be one file on the laptop and two, or none, on the server.
"Phones use some special mobile filesystem." Android is Linux, on ext4 or F2FS, with the same inodes and permissions. Its encryption, integrity and case-folding features are ext4 features your laptop also has.
"Snapshots are backups." They live on the same devices. They survive your mistakes, not a hardware failure or a fire.
"stat -f saying ext2/ext3 means the disk is old." ext2, ext3 and ext4 share the superblock magic number 0xEF53. Use df -T for the real type.

9.2 Other Traps to Avoid

  • Copying files onto a mount point that is not mounted. The write succeeds into the underlying directory, then the real filesystem mounts over it and the data vanishes from view while still using space. Check with findmnt before writing.
  • Formatting with the default inode ratio for a mail or cache volume. Inodes run out at a few million files and cannot be added afterwards.
  • Copying a sparse virtual disk without --sparse=always. The holes become real zeroes and a small image turns into a full-size one.
  • Assuming a FAT stick preserves ownership and permissions. It stores neither. What ls -l shows there comes from the uid, gid, fmask and dmask mount options, not from the disk.
  • Growing the filesystem without growing the volume underneath. The order is always: extend the partition or logical volume first, then resize2fs or xfs_growfs.
  • Running e2fsck "just to check" on a mounted root filesystem. It can destroy a filesystem that had nothing wrong with it.
  • Trusting du on a snapshotted or deduplicated volume. It walks names, and snapshots have none. Use btrfs filesystem usage or zfs list -o space.
  • Storing a database or VM image on a copy-on-write filesystem without thinking. Random rewrites into a CoW file fragment badly; use chattr +C on the directory, or a subvolume with CoW disabled.
  • Deploying a repository built on a case-insensitive laptop. Logo.png and logo.png are one file on macOS and Windows and two on Linux, so the 404 appears only in production. Fix the case in the repository, not on the server.
  • Assuming a Mac can be plugged in and read. APFS has no driver in the Linux kernel. Plan for a network copy rather than discovering it with the disk in your hand.
  • Missing the + in ls -l. It means an ACL grants access the permission bits do not show. Run getfacl before concluding that permissions are broken.
  • Benchmarking a filesystem twice and believing the second number. The page cache and the dentry cache make every repeat run faster. Compare cold with cold, or do not compare.
  • Deciding a file is missing because ls shows nothing. A leading dash, a trailing space or a newline in the name is common enough to check with ls -b before panicking.
Back to top

10. Summary

A filesystem is the bookkeeping that turns a numbered row of blocks into named files with owners, dates and permissions. Once you can see the three parts it keeps, the name in a directory, the inode holding the facts, and the blocks holding the bytes, the rest of Linux storage stops being mysterious: hard links, sparse files, full disks that will not empty, and the gap between two commands that both claim to measure size.

  • A directory entry is a name and an inode number, an inode holds every fact about a file except its name, and the data blocks hold the content.
  • Linux mounts every filesystem into one tree, and the kernel's VFS layer makes them all answer the same calls, whether they are on disk, in RAM, or generated on the spot like /proc.
  • The filesystem is one layer in a stack that may also include partitions, LUKS encryption and LVM. lsblk shows the whole stack, and knowing which layer failed is half of any repair.
  • A filesystem can run out of blocks or of inodes, and on ext4 the inode count is fixed at creation. Monitor df -h and df -i together.
  • Space is handed out in 4 KB blocks, so a one-byte file costs 4 KB and a directory of tiny files costs many times what it holds.
  • ext4 records file locations as extents, decides where to put them as late as it can (delayed allocation), and protects its own structures with a journal, not your last write.
  • Sparse files claim space they never used, fallocate reserves space it never wrote, and copy-on-write filesystems let two files share the same blocks. All three break the idea that size equals cost.
  • On Btrfs and ZFS, snapshots and reflinks hold blocks that no path leads to, so only the filesystem's own tools can account for free space.
  • Beyond the inode's fixed fields, files carry extended attributes, and POSIX ACLs are stored in one of them. Neither survives a plain cp.
  • The page cache sits between you and the device, so reads are served from RAM and a write() that returned is not yet on disk.
  • ext4 and XFS checksum their metadata only; Btrfs and ZFS checksum data too, which is the difference between detecting silent corruption and never knowing.
  • Filenames are bytes: anything but a slash and a zero byte, with a 255-byte limit that accented letters reach twice as fast.
  • Linux is case-sensitive and byte-exact where Windows and macOS are neither, which is why a filename that works on a laptop can fail on the server, and why APFS disks cannot be read on Linux at all.
  • A phone is not a different world: Android is Linux on ext4 or F2FS, and fscrypt, fs-verity and casefolding are ext4 features your own kernel carries.
  • ext4 grows online and shrinks offline; XFS never shrinks; and no filesystem should ever be checked or repaired while it is mounted.
df -hT                       # what is mounted, how full, and which type
df -i                        # the other way a filesystem fills up
findmnt /path                # the mount, its source and its options
lsblk -f                     # the whole stack: disk, crypt, LVM, filesystem
stat file                    # inode number, links, blocks, four timestamps
stat -f /path                # block size, name length, free vs available
ls -b                        # filenames as they really are
filefrag -v file             # the extents a file is stored in
du -h --apparent-size file   # content size, next to plain du for real cost
dumpe2fs -h /dev/sdX1        # superblock: features, inodes, journal, reserve
tune2fs -m 1 /dev/sdX1       # reclaim the root reserve on a data volume
resize2fs /dev/sdX1          # grow ext4 online, after growing the volume
getfacl file                 # the ACL behind the + in ls -l
lsblk -D                     # does discard reach the device, layer by layer
lsof -nP +L1                 # deleted files still holding space open
btrfs filesystem usage /mnt  # free space on a copy-on-write filesystem
zfs list -o space            # the same question, ZFS edition

And when a server reports "No space left on device" while df -h shows a disk that is half empty, the filesystem is not confused and neither are you: it has run out of inodes, or it is still holding a deleted log file that some process never closed.

Back to top
Linux concept: filesystems
Peter Martin
Peter Martin
Joomla Specialist

Peter is a Joomla specialist and a Linux admin for fast, secure and scalable websites.