Filesystems · Filesystem Fundamentals
Unlink While Open and the Life of an Orphaned Inode
Removing the last name does not free a file that is still open; how the link count and the description reference interact, and why deleted files keep consuming disk.
A directory shows no entry for a file, df shows no change, and the file’s blocks are still allocated. The name is gone, the inode is not, and the space returns only when the last process holding the file closes it. The mechanism is the interaction of two independent counters, and nearly every case of unexplained disk consumption on a long-running system comes down to one of them not reaching zero.
Two references, one reclamation decision
The inode carries a link count: the number of directory entries that name it. The open file description holds a reference to the inode, independent of any name. unlink decrements the link count and does nothing else, except that when the count reaches zero the filesystem is permitted to reclaim the inode and its blocks.
The permission is conditional on the second reference. Linux’s rule is that the inode is freed when the link count is zero and no description refers to it. The POSIX requirement is expressed differently: the file’s data is not accessible through any name, but a process that already holds it open may continue. The implementation that satisfies it is exactly this: keep the object alive, drop the name, and free at the last close.
Removing the last link to a file with no open descriptors therefore frees it immediately. The directory entry’s removal and the block reclamation happen in the same operation, which is why rm on a large file whose only copy is not in use returns at once and the space appears in df immediately.
Removing the last link to a file with an open descriptor leaves the inode in a state the filesystem must record: allocated, unreferenced by any directory, and referenced by a descriptor. Ext4 tracks such inodes on an orphan list, traditionally a singly linked list anchored in the superblock with each inode storing the number of the previous orphan. That list is a single global structure, so a newer feature replaces it with an orphan file: a dedicated inode holding blocks of 32-bit inode numbers, each block ending in a magic value and a checksum. Either form is written as part of the deletion transaction and read back at mount time, so after a crash the filesystem knows which inodes were mid-deletion and finishes the job rather than leaking them permanently.
The distinction between “deleted” and “reclaimed” is the whole subject. ls shows the first, df shows the second, and they disagree for as long as any process holds the file.
The descriptor keeps the data, not the name
A descriptor opened before the unlink continues to support read, write, lseek, fstat, and mmap. The inode’s size, times, and permission bits remain readable through fstat, and the owner is unchanged. What is lost is the ability to obtain a new descriptor: no path resolves to the inode, so open by name fails with ENOENT regardless of the old name.
Passing the descriptor to another process keeps the file alive across that process’s lifetime, because the receiving process’s descriptor is another reference to the same description. A descriptor passed over a Unix socket to a cooperating process therefore extends the file’s life beyond the sending process’s exit, and the space is not freed until the recipient closes it.
linkat with AT_EMPTY_PATH can give a name back to an inode that has none, provided the caller holds a descriptor for it and satisfies the privilege check the call imposes, which on Linux means CAP_DAC_READ_SEARCH for the general case. That is the only interface for resurrecting a deleted file that is still open, and it requires the descriptor, since there is no way to name an inode by number.
The inode number itself is not reusable while the inode is alive. fstat continues to report the number it always had, and a subsequent open of a newly created file cannot receive that number until the orphaned inode is freed. Filesystems that reuse inode numbers aggressively still respect this ordering, because reuse requires the record to be free.
Where the space goes and how to find it
The kernel exposes the open descriptors of every process under /proc, and each entry for a regular file is a symlink whose target is the path the file had when it was opened, with the literal string (deleted) appended once the last link is gone. A directory walk over those links produces the set of orphaned files and their sizes.
The size reported there is st_size, not allocated blocks, so a sparse orphan shows a length far larger than the space it holds. The number that matters for reclaiming space is st_blocks, and a tool that reports only sizes overstates the problem for sparse files and understates it for files with extended attributes stored in separate blocks.
A process that holds the descriptor has no obligation to close it, and a service that opens a log file, unlinks it during rotation, and keeps writing will hold the old generation’s blocks indefinitely. The pattern is deliberate in some designs: unlink-then-write gives the process a private file that no other process can open by name, which is a useful isolation primitive for scratch data. It is a leak when it happens by accident, which is what happens when a rotation script renames and deletes while the writer never reopens.
The same effect appears without an explicit unlink when a temporary file is created, unlinked immediately, and used through its descriptor. That idiom is safe precisely because the close is guaranteed by the process’s own lifetime, and the space is reclaimed when the process exits even if it never calls close.
Directories and the refusal to unlink
unlink on a directory fails with EISDIR on Linux and EPERM on some systems. Directories are removed with rmdir, which requires the directory to be empty apart from . and ...
The reason is the same invariant that forbids hard links to directories. A directory’s .. entry records one parent, and removing the directory while its subtree still has entries would leave those entries reachable by no path, since their parent has no name and no inode reference from above. The emptiness check walks the directory’s records and fails with ENOTEMPTY if any live entry other than the two reserved names is found.
The check and the removal are one operation under the directory’s lock, so a concurrent create inside the directory either happens before the check and causes the removal to fail, or after the removal and fails with ENOENT because the parent no longer exists. There is no window in which the create succeeds into a directory that is being removed.
Removing a directory that contains only an orphaned file is permitted: the file has no entry, so the directory is empty by the check, and the removal succeeds while the orphan’s inode stays alive under its descriptor. The tree’s shape and the set of live inodes are therefore not in one-to-one correspondence, which is why a filesystem check that reconciles them must consult the orphan list rather than the directory tree alone.
Crash behaviour and the orphan list
The interesting failure is a crash between the unlink and the last close. The directory entry removal must be durable before the inode is freed, because the reverse order would leave a name pointing at a freed inode. The filesystem journals the entry removal and the link count decrement together, and the inode’s blocks are freed only after that transaction commits.
If the crash lands before the commit, replay either applies the whole transaction or none of it. In the first case the inode is on the orphan list and its blocks are freed at the next mount’s cleanup; in the second the entry still exists and the file is intact. Both are consistent states, and neither requires the directory to be scanned for dangling references.
A filesystem without journaling or copy-on-write has no such record. A crash between the entry removal and the block free leaves blocks marked allocated with no inode referencing them, which fsck finds by comparing the block bitmap against the inode table. The scan is proportional to the filesystem’s size, and the repair is what makes a full check of a large filesystem slow. The journal exists in part to avoid that scan.
Counting links and the arithmetic that follows
link increments the count and creates an entry; unlink decrements it. The count is stored in the inode, so its width bounds how many names one file can carry, and the bound differs by filesystem: ext4’s classic inode field is 16 bits, and XFS allows a far larger count. A program that creates hard links in a loop will meet EMLINK at whatever limit its filesystem imposes, and the limit is not queryable through the interface.
The count for a directory includes its .. entry in each child, so a directory’s count is not the number of names that refer to it in the usual sense. The arithmetic is described in Directory Entries as a Name-to-Inode Map, and it is the reason st_nlink for a directory drops when a subdirectory is removed and rises when one is created.
rename between two names in the same directory leaves the count unchanged, since one entry replaces another. rename to a name that already exists decrements the replaced inode’s count and, if that count reaches zero with no descriptor open, frees it as part of the same operation. That case is the one that makes the atomic replacement idiom reclaim space immediately: the old content disappears at the instant the new name appears, unless someone still holds it open.
Reading a file that has no name
A process that holds an orphaned file can read it to the end, and the read is indistinguishable from reading a named file. The data is in the page cache or fetched from the block map exactly as before. The one operation that fails is anything that needs to name the file: stat by path, chmod by path, rename, and link without a descriptor.
fchmod and futimens work on the descriptor, because they act on the inode rather than on a name. ftruncate works too, and truncating an orphan to zero frees its blocks immediately while leaving the inode alive for the descriptor’s lifetime. That is the only way to release the space before closing, and it is a real technique for a long-lived process that must reclaim a large scratch file without closing its descriptor.
What cannot be done is to recover the file after the last descriptor closes. The inode is freed, the blocks return to the allocator, and any subsequent allocation may overwrite them. Recovery tools that scan a device for deleted file content work below the filesystem, looking for the remnants of block maps and data, and their success depends on nothing having reused the space in the interval. On a filesystem with a journal, the journal itself may contain copies of recently written data blocks, which is why recovery from a journal is possible for recent files and hopeless for old ones.
The interaction with the rest of the interface
Three consequences follow from the design and appear throughout the interface.
A file’s identity is its inode, and the inode survives a name’s removal. Code that uses the name as an identity, as a lock file or a cache key or a temporary name does, is using something that can vanish while the object it identifies remains. The pair of numbers described in What an Inode Stores, and Why It Stores No Name is the identity, and a descriptor is the handle that preserves it.
Durability and deletion are independent. An unlink that has returned does not guarantee the removal is on stable media; the transaction may still be in the journal. A crash can resurrect the file. Making the removal durable requires a sync on the directory, exactly as making a rename durable does, and the calls involved are covered in fsync, fdatasync, and What Survives a Power Loss.
Space accounting follows allocation, not names. A filesystem can be full of data that no directory mentions, and no tool that lists directories will find it. The check is over the open descriptor table, and the fix is a restart or a deliberate close. It is the one case where the leak is not a bug in the filesystem but a consequence of the contract it keeps.
References
- unlink(2) — Linux manual page — man7.org
- The Open Group Base Specifications Issue 7, 2018 edition — unlink() — The Open Group
- Ext4 Data Structures and Algorithms — Orphan File — Linux kernel documentation
- The Design of the UNIX Operating System — Maurice J. Bach, Prentice Hall, 1986