Contents

What a File Actually Is (Inodes, Not Names)

Contents

I once deleted a log file that a running service still had open, expecting the disk space to come back immediately, and watched df report the space as still occupied for another hour until I finally restarted the service. That behaviour looks like a bug the first time you see it. It isn’t. It’s the direct, correct consequence of a fact most people never have to think about: on a Linux filesystem, the filename you typed rm against was never the file. It was a label pointing at the actual file, and the actual file — the inode — was still very much alive, still open, still occupying disk space, because a process still held a reference to it that had nothing to do with the name I’d just removed.

What an inode actually holds

Advertisement

Every file on an ext4, XFS, or similar Linux filesystem is represented by an inode — a fixed-size structure holding all of the file’s metadata and, indirectly, the location of its actual data blocks on disk. Crucially, the inode does not contain the filename. Ownership, permissions, size, timestamps, a link count, and pointers to the data blocks — all of that lives in the inode, identified purely by a number. The filename lives somewhere else entirely: in a directory, which is itself just a special kind of file containing a list of name-to-inode-number mappings.

1
2
3
4
5
6
7
8
$ ls -i /var/log/syslog
1442891 /var/log/syslog

$ stat /var/log/syslog
  File: /var/log/syslog
  Size: 8421          Blocks: 24         IO Block: 4096   regular file
Device: 259,2   Inode: 1442891    Links: 1
Access: (0640/-rw-r-----)  Uid: (  0/    root)   Gid: (  4/     adm)

ls -i shows the mapping directly: the name syslog and the inode number 1442891 are two separate pieces of information, joined by exactly one entry in the directory that contains them. When you run rm syslog, you aren’t destroying the inode. You’re removing that one directory entry, decrementing the inode’s link count by one, and the filesystem only actually reclaims the inode and its data blocks once that link count hits zero and no running process still has the file open. My log-deletion problem above is exactly this: the service had the inode open, so its link count sat at zero but its “still referenced” count didn’t, and the filesystem correctly kept the space allocated until the process closed its file handle.

This model explains hard links immediately, once you see the directory-entry-versus-inode split clearly: a hard link isn’t a copy of a file or a shortcut to it, it’s a second directory entry pointing at the exact same inode number.

1
2
3
4
5
6
7
$ echo "original content" > file-a.txt
$ ln file-a.txt file-b.txt
$ ls -i file-a.txt file-b.txt
1048577 file-a.txt
1048577 file-b.txt
$ stat -c '%h' file-a.txt
2

Both names point at inode 1048577, and the link count is now 2. There’s no “original” and “copy” in any meaningful sense at the filesystem level — both names are equally real, equally valid, pointing at identical data, and editing the content through either name changes what both names see, because there was only ever one file. Deleting file-a.txt removes one directory entry and drops the link count to 1; the data persists, entirely intact, reachable through file-b.txt, and the space isn’t freed until the link count reaches zero. This is also why hard links can’t cross filesystem boundaries or point at directories in the general case — inode numbers are only meaningful within a single filesystem, and allowing directory hard links would make the directory tree’s structure (which assumes a strict parent-child hierarchy) potentially non-hierarchical.

Symbolic links are a completely different mechanism worth distinguishing clearly, because people conflate the two constantly: a symlink is its own inode, containing a stored path string, and following it means the filesystem reads that path and looks the name up again from scratch. Delete the target of a symlink and the symlink still exists, now pointing at nothing — a dangling link. Delete one side of a hard link pair and the other side is completely unaffected, because there was never a “target” in the symlink sense at all, just two equally valid names for one underlying inode.

Why this matters for backups and deduplication

Advertisement

Understanding inodes explains a category of storage tricks that look almost magical until you see the mechanism. Tools like rsync --link-dest and backup systems that implement space-efficient incremental snapshots frequently do so by hard-linking unchanged files between successive backup directories, rather than copying them — each snapshot directory looks, from the outside, like a complete independent copy of the whole tree, while unchanged files across snapshots actually share a single inode and a single copy of the underlying data. A hundred nightly snapshots of a mostly-static directory can occupy barely more disk space than one full copy, because only the files that actually changed get new inodes; everything else is just another name pointing at data that was already there.

1
$ rsync -a --link-dest=../2026-07-17 /srv/data/ /backups/2026-07-18/

This single flag is doing exactly the trick described above: any file in the source identical to its counterpart in the previous snapshot gets hard-linked rather than copied, and the space savings are a direct, mechanical consequence of the inode model, not a compression trick or anything filesystem-vendor-specific.

What actually happens when you delete something

The word “delete” is doing a lot of misleading work in everyday language, because what actually happens on a Linux filesystem when you remove a file’s last name is closer to “stop bothering to remember where this was” than “erase the data.” Removing the final directory entry drops the link count to zero, at which point the filesystem marks the inode and its data blocks as free — available to be reused by a future write — but it does not, in the general case, overwrite the data blocks with zeroes or random bytes. The bytes that made up your file are usually still sitting exactly where they were, physically, until something else happens to get allocated to that same space.

This is precisely the mechanism undelete tools and forensic recovery software exploit: immediately after an accidental rm, the actual content is very often still fully intact on disk, just no longer reachable through any directory entry, and no longer protected against being overwritten by the next write the filesystem happens to perform. It’s also exactly why “securely deleting” a file is a meaningfully different, harder operation than deleting it — a secure-delete tool has to explicitly overwrite the data blocks before releasing them, precisely because ordinary deletion was never designed to do that, only to release the space for reuse. On solid-state drives the picture gets murkier still, because the TRIM command and the drive’s own wear-levelling firmware can reclaim blocks in ways the operating system doesn’t fully control or observe, which is why secure-erase guarantees on SSDs rely on the drive’s own firmware support rather than the filesystem layer alone.

Directories are files too, and that’s not a metaphor

It’s worth being literal about something stated in passing above: a directory is not a special filesystem object with its own storage format invented separately from regular files — it’s a regular file, with a regular inode, whose contents happen to be a structured list of name-to-inode-number pairs that the kernel treats specially when resolving a path. This is why directories have a size at all (ls -ld on an empty directory typically reports 4096 bytes on ext4, the size of one allocation unit) even though they contain no user data of their own: that size is the space taken up by the list of names inside them, not by anything the names point at.

This also explains a specific, otherwise-strange piece of filesystem behaviour: a directory containing thousands of entries can be slow to search linearly in older filesystem designs, which is why modern ones (ext4’s HTree indexing, XFS’s B-trees) store directory contents as an actual searchable structure rather than a flat list, purely as a performance optimisation over what is, conceptually, still just “a file containing names and the inode numbers they point to.” Nothing about the inode model changes; only how quickly the kernel can look a name up within one directory’s contents does.

Troubleshooting the inode-shaped surprises

df shows the disk nearly full, but adding up file sizes in du doesn’t come close. Check for deleted-but-open files held by a running process: lsof +L1 lists open files with a link count of zero, which is exactly the “deleted but still occupying space” case. Restarting or signalling the holding process to close and reopen its file handle (log rotation software does this specifically to avoid this trap) reclaims the space immediately.

“No space left on device” but df shows free space available. This is very likely inode exhaustion, not block exhaustion — a filesystem has a fixed number of inodes decided at creation time, and a directory tree with an enormous number of tiny files (a mail spool, a cache directory, certain build artefact trees) can exhaust the inode table while data blocks remain mostly empty. df -i shows inode usage separately from space usage and will confirm this immediately if it’s the cause.

Copying a directory with hard links inside it “loses” the link relationship. A plain cp by default creates independent copies of each name, breaking the shared-inode relationship entirely — use cp -a or cp --preserve=links, which explicitly detects and recreates hard link relationships in the destination rather than silently duplicating the data under two separate inodes.

Moving a file across filesystems is slow, but moving it within one filesystem is instant. A move within the same filesystem is just a directory-entry rewrite — the inode number and data blocks don’t move at all, so it’s effectively free regardless of file size. A move across filesystems has to be a genuine copy followed by a delete, because inode numbers aren’t portable between filesystems, and that’s the entire reason mv behaves so differently depending on whether the source and destination happen to share a filesystem.

A snapshot-based backup tool claims a full copy but the disk usage doesn’t add up. This is the expected, correct behaviour if the tool uses hard links between snapshot directories, exactly as described above — verify by comparing inode numbers (ls -i) for the same filename across two snapshot directories; matching numbers confirm shared data, not a bug in either the backup tool or your usage math.

Why this is worth actually knowing

This is the mental model that makes an entire category of Linux behaviour predictable instead of mysterious: why deleting a file doesn’t always free space, why backup tools can claim to store a hundred full copies in the space of two, why a hard link genuinely isn’t a copy in any sense that matters, and why moving files sometimes takes zero time and sometimes takes an hour. The name was never the thing. It’s worth remembering that the next time du and df disagree, or RAID fails to save you from a mistake made at exactly this layer — because RAID protects the disk holding the inodes, leaving the directory entries pointing at them just as vulnerable to a mistaken rm across a hard-linked tree — a data-loss mode no amount of disk redundancy was ever going to catch. The same layer is also exactly what a file recovery attempt after an accidental delete is actually racing against: the freed blocks getting reused before you get to them, well before the data itself has any chance to disappear on its own.

Advertisement
Advertisement
Smarc
Written by Smarc

Founder and editor of vo.rs. A lifelong tinkerer who self-hosts far more than is sensible, hardens Linux boxes for fun, and prods the latest AI tools to see what they can really do. The how-to guides here are the notes Smarc wishes had existed the first time round.