Module 4 · Unit 10 · Lesson 4 of 7
Directory Organization
A directory looks like a simple list of names, but under the surface it is a data structure with its own layout, its own lookup algorithm, and its own performance characteristics. As directories grow from a handful of entries to millions, the choice of structure determines whether a lookup is a microsecond or a millisecond. This lesson presents the classical linear-list directory and the modern alternatives — hash tables, HTrees, and B-trees — that scale to the workloads of the present.
01 Introduction
Lesson 10.1 introduced the directory as a mapping from names to files: an entry in a directory associates a human-readable name with an inode number, and the inode number identifies the file within the file system. Lesson 10.2 examined the metadata that the inode holds, and Lesson 10.3 presented the inode itself — its layout, its lifetime, and its role in path resolution. This lesson turns to the directory's internal structure: how the entries are stored, how a name is looked up, how the structure is updated, and how it scales.
The directory is easy to overlook. From the user's perspective, a directory is just a folder: a container for files, with operations like create, delete, rename, and list. From the file system's perspective, it is a data structure with a defined layout on disk, a lookup algorithm that runs on every path resolution, and a performance profile that matters more and more as directories grow. A directory with a handful of entries can be searched linearly with negligible cost; a directory with a million entries cannot. The choice of structure is therefore a scalability decision, and it is one of the file system's most visible performance characteristics.
The classical Unix directory is a simple file whose contents are a sequence of entries: each entry is a name and an inode number, and a lookup is a linear scan of the file. The structure is simple to implement and works well for small directories, but it degrades to O(n) for a directory with n entries. For a system with millions of small files — a mail spool, a build cache, a Git object store — the linear scan is the bottleneck. Modern file systems address this with indexed structures: ext4 uses the HTree (a B-tree-shaped hash table), XFS and Btrfs use B-trees, and the classical BSD file system uses a variant of the linear list with a cache. Each of these structures has its own trade-offs, and each is designed for a specific range of workloads.
This lesson presents the directory as a data structure. It defines the directory entry and the operations the file system performs on it. It presents the classical linear-list directory, its implementation, and its limitations. It presents the linked-list and hash-table alternatives, and it presents the indexed structures — HTree and B-tree — that modern file systems use to scale. It examines the directory entry cache (dcache) and explains how caching turns a slow lookup into a fast one. It closes with a discussion of the directory's performance characteristics and of the design choices that shape them.
Prerequisites
This lesson builds directly on Lessons 10.1, 10.2, and 10.3. The directory as a name-to-inode mapping, the inode table, and the distinction between a file's name and its identity are all assumed. The path-resolution process from Lesson 10.1 is the context in which directory lookups occur, and its discussion of the directory entry cache is extended here. The discussion of B-trees assumes the tree structures from Lesson 10.7 (which is later in the unit — read it as a preview if you have not yet covered it), but no new tree concepts are introduced. Familiarity with the Linux command line from Lesson 2.2 is helpful, since the interactive activity uses standard Unix tools.
02 Learning objectives
By the end of this lesson you will be able to
- Describe the directory entry and the operations the file system performs on a directory.
- Describe the classical linear-list directory and explain its performance characteristics.
- Explain the ext2/ext3/ext4 directory entry format and how entries are added, removed, and searched.
- Describe the HTree indexed directory and explain how it scales to millions of entries.
- Describe B-tree and B+tree directory structures and explain the role of the directory entry cache.
This lesson contributes to U10.4 and U10.5, and to course learning outcomes CLO1, CLO5, and CLO6.
03 Required reading
- Operating System Concepts — Chapter 14, Section 14.3, Directory Implementation. The standard treatment of the linear-list and hash-table directory structures.
- OSTEP — Chapter 40, File System Implementation, and Chapter 41, Locality and The Fast File System. OSTEP's treatment of the directory as a file of entries, and of the Fast File System's directory organization, is the clearest available account, and it is the basis for the worked example in this lesson.
- Tanenbaum & Bos — Modern Operating Systems, Chapter 4, Section 4.3.3, Directory Implementation. A complementary treatment that develops the linear list and the hashed directory from first principles.
- McKusick, M. K., Joy, W. N., Leffler, S. J., & Fabry, R. S. — A Fast File System for UNIX. ACM Transactions on Computer Systems, 1984. Section 4 discusses the directory implementation and the performance considerations.
- Bach, M. J. — The Design of the UNIX Operating System. Prentice Hall, 1986. Chapter 5, Section 5.3. The classical System V directory entry format and the directory operations.
-
Kerrisk, M. — The Linux Programming
Interface, Chapter 14, File Systems.
The directory-related interfaces:
opendir,readdir,seekdir,telldir, and thegetdentssystem call. - Love, R. — Linux Kernel Development, 3rd ed., Chapter 13, The Virtual File System. The VFS directory operations and the dentry cache.
-
Linux man pages:
man 3 opendir,man 3 readdir,man 3 seekdir,man 3 telldir,man 2 getdents,man 2 mkdir,man 2 rmdir,man 2 link,man 2 unlink,man 2 rename,man 5 proc(for the/proc/<pid>/fdinterface),man 8 tune2fs(for thedir_indexfeature),man 1 debugfs.
Reading strategy
Read the Silberschatz section first for the formal
classification of directory structures — linear list,
hash table, and the modern indexed forms. Then read
OSTEP's treatment of the directory as a file of
entries, which establishes the vocabulary and shows
how the structure is used in path resolution. The
Fast File System paper is worth reading for the
discussion of how directory layout interacts with the
file system's block layout. Finally, the Linux
kernel's directory implementation — the
ext4_dir_idx HTree and the XFS B-tree —
is the practical view, and it is what the interactive
activity will let you observe. Pay particular attention
to the difference between the directory's on-disk
structure and the directory entry cache in the kernel;
the cache is what makes lookups fast in practice, and
the on-disk structure matters when the cache is cold.
04 Core content
4.1 The directory entry
A directory entry is a record in a directory that associates a name with an inode number. The simplest possible entry has two fields:
- The name. A human-readable string, typically a sequence of bytes up to 255 bytes long (the classical Unix limit). The name is what the user types and what the file system looks up.
- The inode number. An integer that identifies the file within the file system's inode table (Lesson 10.3). The inode number is what the file system uses internally to refer to the file, and it is the link between the directory and the file's metadata.
Real directory entries also have fields that support the directory's data structure: a record length (or record size) that allows the entry to be padded to a specific length; a file type (a small code indicating whether the entry refers to a regular file, a directory, a symbolic link, and so on); and, on some file systems, additional metadata for the entry itself. The exact format varies between file systems, and it is one of the aspects of the file system that is not standardised across Unix-like systems.
The directory's entries are stored in the directory's
data blocks, which are located through the
directory's own inode — a directory is a file, and the
file system uses the same mechanisms (the inode's
multi-level pointer scheme, or extents) to locate the
directory's contents as for any other file. When a
program calls readdir, the kernel reads the
directory's data through its inode and returns the
entries one at a time.
4.2 Directory operations
The file system performs four operations on a directory, each of which interacts with the directory's internal structure.
- Lookup
-
Given a name, find the corresponding entry and return
its inode number. This is the most common operation:
every path resolution, every
open, everystat, and everylinkbegins with a lookup. The performance of the lookup dominates the performance of the directory. - Insert
-
Add a new entry with a given name and inode number.
Used when a file is created (
creat,openwithO_CREAT) or when a hard link is added (link). The operation must find space in the directory's data, or extend the data if necessary, and must preserve the directory's structure. - Delete
-
Remove the entry with a given name. Used when a file
is unlinked (
unlink) or when a directory is removed (rmdir). The operation must find the entry, mark it as free (typically by adjusting the previous entry's record length), and possibly update the directory's structure. - Rename
-
Change the name of an entry, or move it to a different
directory. Used by
rename. The operation must remove the entry from its old location and insert it in the new one, and must do so atomically (a concurrent lookup sees either the old state or the new, never an intermediate state).
The four operations have different performance characteristics depending on the directory's structure. In a linear list, a lookup is O(n), an insert is O(n) (to find the end of the list), a delete is O(n) (to find the entry), and a rename is O(n) plus the cost of the insert. In an indexed structure, the same operations are O(log n) or O(1), which is why indexed structures are used for large directories.
4.3 The classical linear-list directory
The linear-list directory is the simplest possible structure: the directory's data is a sequence of entries, and a lookup is a linear scan through the sequence, comparing the name of each entry with the name being searched for. The entry is found when the names match, or the lookup fails when the end of the sequence is reached without a match.
The classical implementation of the linear list is the linked-list directory, in which each entry contains the length of the entry and a pointer (or offset) to the next entry. The chain of pointers makes it easy to insert and delete entries: an insert can add the new entry at the front of the list (or in the first free slot), and a delete can adjust the previous entry's length to absorb the deleted entry's space. The linked-list structure is the one used by the classical Unix file system (System V, ext2) and by the BSD Fast File System, and it remains the basis of the modern ext2/ext3/ext4 directory.
The linear list's performance is straightforward:
- Lookup: O(n) in the worst case, O(n/2) on average. Every entry must be examined until the target is found.
- Insert: O(n) to find the end of the list, unless a free slot is maintained at the front (in which case it is O(1)).
- Delete: O(n) to find the entry, O(1) to absorb its space into the previous entry.
For a small directory — say, up to a few hundred entries — the linear scan is fast enough that the constant factor dominates: a scan of 100 entries is a few hundred nanoseconds on modern hardware, which is negligible compared to the cost of the other operations (the inode read, the data read, and the disk access). But for a large directory — say, 100,000 entries — the scan is expensive: 100,000 comparisons, each requiring a memory load and a comparison, plus the disk accesses if the directory's data spans multiple blocks. The linear list is therefore adequate for small directories but does not scale.
4.4 The ext2/ext3/ext4 directory entry format
The classical Unix file system's directory entry is a simple structure whose fields are worth examining, because the design illustrates the trade-offs of the linear-list approach.
struct ext2_dir_entry_2 {
__le32 inode; /* inode number (0 means "free") */
__le16 rec_len; /* length of this record, in bytes */
__u8 name_len; /* length of the name, in bytes */
__u8 file_type; /* file type code (regular, dir, ...) */
char name[]; /* the name, not null-terminated */
};
The structure has four fields plus the name:
-
inode— the inode number that the entry refers to. An inode number of 0 marks the entry as free. -
rec_len— the length of the record, in bytes. The record is padded to a multiple of the alignment (typically 4 bytes), so therec_lenmay be larger than the name's length would require. The padding leaves space that a later insert can use without extending the directory. -
name_len— the length of the name. The name is not null-terminated: its length is given byname_len. -
file_type— a small code indicating the file's type:EXT2_FT_REG_FILEfor a regular file,EXT2_FT_DIRfor a directory,EXT2_FT_SYMLINKfor a symbolic link, and so on. The file type is redundant with the type stored in the inode, but it allows the file system to return the type without reading the inode, which is a performance optimisation for readdir.
The record's length is variable, and the variable length
is the key to the structure's simplicity. When a new
entry is inserted, the file system finds the first
record with enough free space (either at the end of the
directory, or in a padded region of an existing record),
splits the space, and adds the new entry. When an entry
is deleted, the file system sets its inode
field to 0 and merges its space into the previous entry
by increasing that entry's rec_len. The
merge is what keeps the directory's free space available
for future inserts, and it is why a directory's size
grows but rarely shrinks: the space freed by deletes is
absorbed into neighbouring records, and the directory's
data blocks are not returned to the file system until
the directory is compressed (which most file systems do
not do).
A lookup in the ext2/ext3/ext4 directory is a linear scan of the records. The scan is fast because the records are small and the file system reads them sequentially, but the cost is still O(n). For a directory with a few hundred entries, the cost is negligible; for a directory with tens of thousands of entries, the cost becomes significant, and for a directory with millions of entries (as in a mail spool or a Git object store), the linear scan is a serious bottleneck.
Directory entry records are not self-describing
The ext2 directory entry's fields do not include a
pointer to the next record. The next record is found
by adding rec_len to the current entry's
offset. This is why the file system can merge a
deleted entry's space into the previous entry by
increasing the previous entry's rec_len:
the merged space becomes part of the previous record,
and the next entry is found by skipping the merged
space. The design is efficient in space (no pointer
per entry) but requires the file system to always
maintain the record lengths correctly — a corrupt
rec_len makes the directory unreadable
beyond the corrupt entry.
4.5 The HTree indexed directory
The linear-list directory's O(n) lookup became a practical problem in the 2000s, as file systems grew and workloads with many small files became common. Ext3 (and later ext4) addressed the problem by adding an indexed directory structure, called the HTree, that keeps the classical entry format but organizes the entries into a tree of blocks.
The HTree is a B-tree-shaped hash table. Each directory has a hash of its entries' names, and the entries are grouped into logical blocks (each of which is typically one file-system block, i.e., 4 KB). The tree has a root node that spans the hash values, and internal nodes that split the hash range into sub-ranges. A lookup hashes the name, traverses the tree to find the leaf block whose hash range contains the name's hash, and scans the entries in that block — a small number, because each block holds only the entries whose hashes fall in its range.
The HTree's structure is a compromise between the
classical entry format and a full hash table. The
directory's data blocks are still sequences of ext2-style
entries, so a program that reads the directory with
readdir sees the same format as before. But
the entries are not stored in insertion order:
they are stored in hash order, with the entries
for each hash range in its own block. The directory's
inode points to the tree's root, and the tree's internal
nodes point to the leaf blocks.
The HTree has three important properties:
- Lookup is O(log n) — typically two or three block reads, regardless of the directory's size. A directory with 10 million entries has a tree with three levels, so a lookup reads the root, one internal node, and one leaf — three disk accesses instead of 10 million.
- Insert and delete are O(log n) — the file system finds the leaf block whose hash range contains the new name's hash and inserts the entry there, splitting the block if it is full.
-
The directory's size grows but does not
shrink — like the classical ext2 directory,
the HTree does not return freed blocks to the file
system. The directory can be rebuilt (with
e2fsck -D) to reclaim space.
The HTree is enabled by default on ext3 and ext4 file
systems that are created with the dir_index
feature, which is the default on modern systems. It is
what allows ext4 to handle directories with millions of
entries without the performance collapse that a linear
list would suffer.
4.6 B-tree and B+tree directories
Other modern file systems use B-trees or B+trees as their directory structure. XFS uses a B+tree indexed by name (not by hash), and Btrfs uses a B-tree that is shared with the rest of the file system's metadata.
A B-tree is a balanced tree in which each node holds a sorted sequence of keys and pointers, and the tree's leaves hold the actual entries. The structure is self-balancing: when a node becomes full, it is split, and the tree's height grows logarithmically with the number of entries. A lookup traverses from the root to a leaf in O(log n) steps, comparing the target name with the keys at each node.
The B-tree's advantage over the HTree is that the entries
are stored in name order, not in hash order. This
allows efficient range queries: a
program can ask for all entries whose names fall in a
range (for example, all files whose names begin with
"config-"), and the file system can find the range's
start and iterate through the leaves. The HTree does not
support range queries efficiently, because the entries
are stored in hash order, which does not correspond to
name order. Linux exposes the range-query interface
through the getdents system call with the
d_type field and through the
readdir family, but the underlying file
system determines whether the query is efficient.
The B-tree's cost is that the names are compared in order, so each comparison is a string comparison rather than a hash comparison. The string comparison is more expensive than the hash comparison, but the difference is small for typical names, and the benefit of the range query is significant for the workloads that use it. XFS's directory B+tree is the classical example, and Btrfs's directory B-tree is the modern one.
4.7 Directory entry caching (the dcache)
Whatever the on-disk directory structure, the kernel maintains an in-memory cache of recently resolved directory entries. This cache is called the directory entry cache (the dcache), and it is the reason that most file operations are fast: once a path has been resolved, the resolution of the same path (or a path that shares a prefix) is served from memory, without any disk access.
The dcache is not a cache of names to inode numbers. It is a cache of directory entries: for each entry, the kernel stores the parent directory, the name, and the inode. The entries are organized in a hash table keyed by the parent-inode-and-name pair, and they are also linked into a tree that mirrors the directory hierarchy. The tree structure allows the kernel to walk a path from a cached component to the next, using the cached entries when possible and reading from disk only when a component is not cached.
The dcache's behaviour is similar to the other caches in the system (Lesson 9.3's TLB, Lesson 9.4's page cache). On a system with good locality — where programs repeatedly access the same directories and files — the cache hit rate is very high, and the cost of path resolution is dominated by the memory access rather than the disk access. On a system with poor locality — where programs traverse many directories and open many files — the cache hit rate is lower, and the cost of path resolution is dominated by the on-disk directory lookups.
The dcache's size is bounded by a tunable limit, and when the limit is reached, the kernel evicts the least recently used entries. An entry is in use if it is part of an open file's path (so the kernel cannot evict it), or if it is referenced by another cached entry; the kernel evicts only entries whose reference count is zero. On a modern system with gigabytes of memory, the dcache can hold hundreds of thousands of entries, and the eviction is rare.
The dcache and negative lookups
The dcache also caches negative lookups: when a
program asks for a name that does not exist, the kernel
records the absence in the cache, so a subsequent
lookup of the same name fails without re-reading the
directory. Negative caching is important for programs
that search for files by trying many names (for
example, a shell's $PATH search, or a
dynamic linker's library search): without negative
caching, each failed lookup would require a disk
access, and the cost of searching would dominate the
cost of the eventual successful lookup.
4.8 Scaling and the cost of directory operations
The choice of directory structure has a direct effect on the performance of the operations that use it. The following table summarizes the costs of the classical and modern structures for a directory with n entries:
| Structure | Lookup | Insert | Delete | Range query |
|---|---|---|---|---|
| Linear list | O(n) | O(n) | O(n) | O(n) |
| Hash table | O(1) avg, O(n) worst | O(1) avg | O(1) avg | O(n) |
| HTree (ext4) | O(log n) | O(log n) | O(log n) | O(n) — hash order |
| B-tree (XFS, Btrfs) | O(log n) | O(log n) | O(log n) | O(log n + k) — name order |
The table makes the trade-offs clear. The linear list is the simplest structure and is adequate for small directories, but it does not scale. The hash table gives O(1) lookups on average but does not support range queries. The HTree gives O(log n) lookups and inserts and is the standard for ext4, but it stores entries in hash order, so range queries are expensive. The B-tree gives O(log n) for all operations and supports range queries efficiently, which is why XFS and Btrfs use it.
In practice, the choice between the structures is made by the file system designer, and the user cannot change it. The user's lever is the directory's size: keeping directories reasonably small (a few thousand entries) keeps the cost of operations low regardless of the structure, and spreading a large set of files across many directories is the standard technique for managing a large collection. The classic example is Git's object store, which distributes its objects across 256 subdirectories (one per two-hex-digit prefix of the object's SHA-1 hash) precisely to avoid the scaling problems of a single large directory.
4.9 Directory operations in practice
The four operations — lookup, insert, delete, and rename — have specific implementations in the file system, and it is worth examining how they work on the linear-list structure and on the indexed structures.
On a linear list, the operations are direct:
- Lookup scans the entries, comparing names.
- Insert finds the first entry with enough free space (a deleted entry whose space has not been merged, or the end of the directory), splits the space, and writes the new entry.
- Delete sets the entry's inode field to 0 and merges the entry's space into the previous entry's record length. If the deleted entry is the first in the directory, the file system must handle the case specially (the entry remains as a free record, because there is no previous entry to merge into).
- Rename is a delete followed by an insert, but with the additional requirement that the operation be atomic. If the target directory is the same as the source directory, the rename can be done in place; if the directories differ, the entry is removed from the source and added to the target, and the two operations must be atomic with respect to concurrent lookups.
On an indexed structure, the operations are similar but use the index to find the right block first:
- Lookup hashes the name (for HTree) or compares it (for B-tree), traverses the tree to the appropriate leaf, and scans the entries in the leaf.
- Insert finds the leaf, inserts the entry, and if the leaf is full, splits it and updates the tree. The split may cascade up the tree, which is what makes the insert O(log n) in the worst case.
- Delete finds the entry, marks it free, and merges the space into the previous entry. If the leaf becomes sparse, the file system may merge it with a sibling, which also cascades up the tree.
- Rename is the same as on the linear list, but the file system must also update the tree's keys if the renamed entry's hash or name changes its position.
The operations must be crash-consistent: if the system crashes during an insert or a delete, the directory must be left in a valid state (either the old state or the new one, never an intermediate). This is the subject of journaling (Lesson 10.6), and it is one of the reasons directory operations are more complex than they first appear.
4.10 Common misconceptions
"Directories are just lists of files."
Directories are data structures with their own
internal layout, lookup algorithm, and performance
characteristics. The readdir interface
presents them as lists, but the underlying structure
can be a hash table or a B-tree.
"A directory's entries are stored in insertion order."
On ext2 and the classical Unix file system, yes — the entries are in the order they were inserted. On ext4's HTree and on XFS's B-tree, the entries are stored in hash order or name order, and the insertion order is not preserved.
"readdir returns entries in alphabetical
order."
POSIX does not specify the order in which
readdir returns entries. On most file
systems, the order is the order in which the entries
are stored in the directory's data, which is
insertion order (ext2) or hash order (HTree). A
program that needs alphabetical order must sort the
entries itself.
"Deleting a file reduces the directory's size on disk."
Deleting a file removes the directory entry and
marks the space as free, but the directory's data
blocks are not returned to the file system. The
directory's size stays the same, and the freed
space is used for future inserts. Reclaiming the
space requires rebuilding the directory (with
e2fsck -D on ext4).
"A large directory is always slow."
A large directory is slow on a file system with a linear-list directory, because the lookup is O(n). On a file system with an indexed directory (ext4's HTree, XFS's B-tree), the lookup is O(log n), and a directory with a million entries can be searched in a few microseconds.
"The dcache stores the directory's contents."
The dcache stores recently resolved entries
— parent directory, name, and inode — not the
directory's full contents. Reading a directory with
readdir still requires reading the
directory's data from disk (or from the page cache),
even if the dcache holds entries for some of the
names.
"The on-disk directory format is standardised."
The on-disk directory format is not standardised
across Unix-like systems. The readdir
interface is standardised, but the underlying
structure differs: ext4 uses a variant of the
classical ext2 format with an HTree index, XFS uses
a B+tree, and Btrfs uses a B-tree. A program that
reads a raw directory (bypassing readdir)
is not portable.
"Range queries are always supported."
Range queries — finding all entries whose names fall in a range — are supported efficiently only by name-ordered structures (B-trees). Hash-based structures (HTree) cannot support them efficiently, because the entries are stored in hash order, which does not correspond to name order.
05 Worked example
The following example traces a directory's linear-list structure through a sequence of inserts and deletes, computes the cost of a lookup for different directory sizes, and demonstrates the scaling of the HTree.
Tracing a directory's linear-list structure
Consider a directory that begins empty. The file
system creates it with a single block of data,
initialized with one entry: the . entry
that refers to the directory itself. The entry's
rec_len is the full block size (4 KB),
because there are no other entries yet.
| Step | Operation | Entries (in order) | Total space used |
|---|---|---|---|
| 0 | mkdir("d") | . (rec_len=4096) |
12 bytes |
| 1 | create "a" in "d" | . (rec_len=12), a (rec_len=4084) |
24 bytes |
| 2 | create "b" in "d" | . (rec_len=12), a (rec_len=12), b (rec_len=4072) |
36 bytes |
| 3 | create "c" in "d" | . (12), a (12), b (12), c (rec_len=4060) |
48 bytes |
| 4 | delete "b" in "d" | . (12), a (rec_len=24), c (rec_len=4060) |
48 bytes |
| 5 | create "b" in "d" | . (12), a (rec_len=24), b (rec_len=12), c (rec_len=4060) |
48 bytes |
The trace shows the linear list's behaviour. Each
insert finds the first record with enough space for the
new entry, splits it, and writes the new entry. The
record's length is reduced by the size of the new
entry, so the last entry in the list always has the
largest rec_len (the remaining free
space). When an entry is deleted, its space is merged
into the previous entry by increasing that entry's
rec_len, which is why the entry after the
deleted one (in this case c) does not
change its position. When a new entry is inserted after
the delete, it can reuse the merged space, so the
directory's total size does not grow.
The directory's total space usage grows only when the
free space in the existing records is exhausted. Once
the record space is used up, the file system allocates
a new block for the directory and continues the
entries in the new block. The directory's
st_size reflects the number of blocks
allocated, which grows monotonically until the
directory is rebuilt.
Cost of a linear-list lookup
The cost of a lookup in a linear-list directory is the cost of scanning the entries until the target is found. Let's estimate the cost for different directory sizes.
Assume each entry's record is 16 bytes (12 bytes of fixed fields plus an average name length of 4 bytes, aligned to 16), so a 4 KB block holds 256 entries. A lookup examines on average half the entries in the directory, and each examination requires a memory load and a string comparison — say, 10 nanoseconds per entry on a modern processor. The lookup's cost is:
| Directory size | Blocks | Average entries scanned | Cost (memory) | Cost (with disk) |
|---|---|---|---|---|
| 100 | 1 | 50 | 0.5 µs | 1 disk read |
| 1,000 | 4 | 500 | 5 µs | 4 disk reads |
| 10,000 | 40 | 5,000 | 50 µs | 40 disk reads |
| 100,000 | 400 | 50,000 | 500 µs | 400 disk reads |
| 1,000,000 | 4,000 | 500,000 | 5 ms | 4,000 disk reads |
The table is illustrative, but the pattern is what matters: the linear list's lookup cost grows linearly with the directory's size. For a directory with 100 entries, the lookup is negligible. For a directory with 100,000 entries, the lookup is 500 microseconds even with the directory cached in memory, and much more expensive with disk accesses. For a directory with a million entries, the lookup is 5 milliseconds per name, which is catastrophic for any program that opens many files.
The table also shows why the dcache matters. Even if the directory is on disk, the dcache holds the entries that have been looked up recently, so a program that opens the same files repeatedly pays the disk cost only on the first lookup. But a program that opens many different files (a build system, a file indexer, a backup tool) can miss in the dcache frequently, and the linear list's cost becomes visible.
Scaling with the HTree
Now suppose the directory uses ext4's HTree. The directory's data is organized as a tree, with the leaves holding the entries and the internal nodes holding hash ranges. A lookup hashes the target name, traverses the tree to find the leaf whose hash range contains the name's hash, and scans the leaf's entries. The cost is:
| Directory size | Tree levels | Blocks read | Cost (cached) | Cost (cold) |
|---|---|---|---|---|
| 100 | 1 | 1 | 0.5 µs | 1 disk read |
| 10,000 | 2 | 2 | 1 µs | 2 disk reads |
| 1,000,000 | 3 | 3 | 1.5 µs | 3 disk reads |
| 100,000,000 | 4 | 4 | 2 µs | 4 disk reads |
The HTree's cost grows logarithmically with the directory's size. A directory with 1 million entries requires three block reads (the root, one internal node, and one leaf), and a directory with 100 million entries requires four. The improvement over the linear list is dramatic: three disk reads versus 4,000 disk reads for a directory with a million entries, and the difference grows with the directory's size.
Directory compression with e2fsck -D
Because directories grow but do not shrink, a
long-running system accumulates wasted space in
directories that have had many files created and
deleted. The e2fsck -D command rebuilds
the directories, reclaiming the wasted space and
(optionally) converting them to use the HTree index.
# Check the directory's size
du -sh /var/spool/mail
# Rebuild the directories (requires unmounting the file system
# or running on a non-mounted device)
sudo umount /dev/sda1
sudo e2fsck -fD /dev/sda1
sudo mount /dev/sda1 /mnt
# Or, on a mounted file system, use tune2fs to enable
# the dir_index feature and e2fsck to rebuild the directories
sudo tune2fs -O dir_index /dev/sda1
sudo e2fsck -fD /dev/sda1
The -D option causes e2fsck
to optimize the directories: it removes the wasted
space and, for directories with enough entries,
builds the HTree index. The operation is not performed
automatically; the administrator must run it
explicitly, and it is typically done during a
maintenance window.
The need for e2fsck -D is a consequence of
the directory's design: the file system does not
track the directory's free space as a separate
quantity, so it cannot compact the directory
automatically. Modern file systems (Btrfs, ZFS) manage
the directory's space differently — typically as a
B-tree with space accounting — and do not need an
explicit compaction step.
06 Interactive activity
Activity · explore directory organization on a live system
The goal of this activity is to use the Linux command line to observe directory structures, to measure the cost of directory operations, and to demonstrate the scaling of the linear list versus the indexed HTree. Work through the steps in your course VM.
-
Observe a directory's structure.
Create a directory, add some entries, and observe
the directory's size with
statanddu.
The directory's size grows as the number of entries grows, but the growth is not linear: the file system allocates a new block when the existing space is exhausted. Note that the directory's size does not shrink when entries are removed.mkdir /tmp/dir-demo cd /tmp/dir-demo echo "hello" > file1 echo "hello" > file2 echo "hello" > file3 stat -c 'size=%s blocks=%b' . du -sh . # Add more entries for i in $(seq 1 100); do touch file$i; done stat -c 'size=%s blocks=%b' . du -sh . -
Measure the cost of directory lookups.
Use the
timecommand to measure the cost of creating files in a directory of different sizes, and use thetimecommand to measure the cost of looking up files in the directory.
The lookup cost depends on the file system's directory structure. On ext4 with the HTree index, the lookup is fast even for a large directory; on a file system without indexing, the lookup is slow.# Create a large directory mkdir /tmp/big-dir cd /tmp/big-dir for i in $(seq 1 100000); do touch file$i done # Time a lookup time ls -l file50000 > /dev/null # Time a full listing time ls -f . | wc -l -
Compare the HTree's behaviour.
Check whether the file system uses the HTree index
by using
tune2fs -land looking for thedir_indexfeature.
If the file system has thedf -T /tmp sudo tune2fs -l /dev/sda1 2>/dev/null | grep -i features # Check the directory's structure with debugfs sudo debugfs -R 'stat .' /dev/sda1 2>/dev/null | head -20dir_indexfeature, the directory uses the HTree. Thedebugfscommand can show the directory's internal blocks and the tree structure. -
Observe the dcache's effect.
Use the
timecommand to measure the cost of looking up the same file repeatedly, and compare it to the cost of looking up different files.
The first loop is much faster than the second, because the dcache holds the entry after the first lookup. This is the dcache's effect.# Look up the same file many times time for i in $(seq 1 1000); do ls -l /tmp/big-dir/file50000 > /dev/null; done # Look up different files time for i in $(seq 1 1000); do ls -l /tmp/big-dir/file$i > /dev/null; done -
Demonstrate the effect of a large
directory.
Create a directory with a very large number of
entries and observe how the cost of operations
changes.
The cost of each operation depends on the file system's directory structure. On a file system with the HTree, the creation and removal are efficient even for a million files; on a file system without indexing, they are much slower.mkdir /tmp/huge-dir cd /tmp/huge-dir # Time the creation of 1 million files (this may take a while) time for i in $(seq 1 1000000); do touch f$i; done # Time a lookup time ls f500000 # Time a listing time ls -f | wc -l # Remove the files time rm -rf /tmp/huge-dir -
Observe the ordering of
readdirresults. The POSIX standard does not specify the order in whichreaddirreturns entries. Observe the order on your system.
The# Create a directory with names in a specific order mkdir /tmp/order-demo cd /tmp/order-demo touch zebra.txt alpha.txt mike.txt bravo.txt yankee.txt # List the entries with readdir ls -f . ls -U .ls -fandls -Uoptions list entries in the order the file system returns them, without sorting. Compare the order to the order in which the files were created. On a file system with a linear-list directory, the order is insertion order; on a file system with an indexed directory, the order is hash order (ext4's HTree) or name order (XFS's B-tree). -
Observe the kernel's dcache statistics.
On Linux, the kernel's dcache statistics are
available in
/proc/sys/fs/dentry-state.
The first number is the number of dentries allocated, the second is the number of dentries not currently in use (i.e., candidates for eviction), and the remaining numbers are other statistics. Run a workload that accesses many files and observe the counters.cat /proc/sys/fs/dentry-state
The number of dentries should increase as the kernel caches more entries. The dcache's size is bounded by the kernel's memory management, and when memory is scarce, the kernel evicts unused entries.find /usr -type f 2>/dev/null | head -10000 cat /proc/sys/fs/dentry-state -
Read the man pages.
Read the man pages that document the directory
interface.
For each, write a sentence describing its main contribution to your understanding of directory operations. Theman 3 opendir man 3 readdir man 3 seekdir man 2 mkdir man 2 rmdir man 2 getdents man 5 procman 3 readdirpage documents the iteration interface; theman 2 getdentspage documents the underlying system call; theman 2 mkdirandman 2 rmdirpages document the operations that create and remove directories.
Reflect: the directory is one of those abstractions that is invisible most of the time. A user creates files, moves them around, and deletes them without thinking about the data structure that makes it all work. The directory's structure matters only when it is stressed — when a directory grows large, when a workload creates and deletes many files, when a program searches through many directories in a short time. Which workloads in your own experience stress directories the most? What would you do to make those workloads more efficient — would you change the directory structure, or would you change the workload's layout? What is the right balance between the directory's internal structure and the user's responsibility for organizing files?
07 Knowledge check
Check your understanding
Question 1
What is a directory entry? What fields does it contain, and what operations does the file system perform on it?
Show answer
Question 2
Describe the classical linear-list directory. What are its advantages and disadvantages, and why does it fail to scale?
Show answer
Question 3
Describe the ext2/ext3/ext4 directory entry format.
What is the role of the rec_len field,
and how does the file system use it to insert and
delete entries?
Show answer
inode field holds the inode number
(or 0 for a free entry). The rec_len
field holds the length of the record in bytes.
The name_len field holds the length
of the name. The file_type field
holds a small code indicating the file's type
(regular, directory, symbolic link, and so on).
The name itself follows the fixed fields and is
not null-terminated; its length is given by
name_len. The rec_len
field is the key to the structure's flexibility.
A record's space includes the entry itself plus
any padding, so the file system can write a new
entry in the padding without extending the
directory. To insert a new entry, the file system
scans the directory for the first record with
enough space, splits that record's space into two
parts — one for the existing entry, one for the
new entry — and writes the new entry in the second
part, updating the first record's
rec_len to reflect the split. To
delete an entry, the file system sets its
inode field to 0 and merges its space
into the previous record by adding the deleted
entry's rec_len to the previous
record's rec_len. The merge means
that the freed space is immediately available for
future inserts, which is why the directory's size
grows but rarely shrinks. The file system does not
track the directory's free space separately, so
the freed blocks are not returned to the file
system until the directory is rebuilt with
e2fsck -D.
Question 4
Describe the HTree indexed directory. How does it improve on the linear list, and what are its limitations compared to a B-tree?
Show answer
Question 5
What is the directory entry cache (dcache), and how does it affect the performance of path resolution? What is a negative lookup, and why does negative caching matter?
Show answer
$PATH search, a
dynamic linker's library search, a program that
tries several filenames to find the one it needs.
Without negative caching, each failed lookup would
require a disk access, and the cost of the search
would dominate the cost of the eventual successful
lookup. With negative caching, the failed lookups
are served from memory, and the search is fast.
Negative caching is one of the reasons the dcache
is a critical component of the file system's
performance: it is not just a cache of successful
lookups, but a cache of all lookups,
including the ones that failed.
08 Unit discussion
Discussion prompt
The directory is one of the oldest abstractions in computing, and one of the simplest to describe: a mapping from names to files. But under the surface it has evolved dramatically. The classical linear list was adequate for the file systems of the 1970s; the hash table was proposed in the 1980s; the indexed structures (HTree, B-tree) emerged in the 2000s and 2010s. Each generation of structure has been designed for the workloads of its time, and each has been replaced when those workloads changed.
The trend is unmistakable: the directory is becoming a database. The B-trees used by XFS and Btrfs are indistinguishable from the B-trees used by database systems; the HTree is a hash index with the same structure as a database's hash index; and the modern file systems' metadata operations are transaction-like, with journaling and copy-on-write providing the same crash consistency that a database provides for its tables. The directory is a small, specialized database that holds the file system's namespace.
If the directory is a database, should it be exposed as one? Some systems have proposed this: the semantic file system allows files to be queried by attribute rather than by name; the content-addressable storage systems identify files by their contents rather than their names; and the tag-based file systems (such as BeOS's BFS and modern systems' tag extensions) allow files to have multiple names and to be searched by tag. Each of these alternatives trades the classical name-to-inode mapping for a richer interface, and each has failed to displace the classical directory for general-purpose use.
Why has the classical directory been so durable? Is it because the name-to-inode mapping is the right abstraction, or because it is familiar, or because the alternatives are harder to implement efficiently? What would a directory look like if it were designed today, with the benefit of fifty years of hindsight and the demands of modern workloads? Post your response to the Unit 10 discussion forum and reply to at least two classmates. Supports CLO1, CLO5, and CLO6.
09 Practice exercise
This ungraded exercise consolidates the lesson and prepares you for Lesson 10.5. Aim to spend 60–90 minutes on it.
-
Trace a linear-list directory.
A directory begins empty and has a single 4 KB block.
Trace the following sequence of operations, showing
the entries and their
rec_lenvalues after each step. Assume each entry's fixed fields are 8 bytes, the name is padded to a multiple of 4, and the initial entry hasrec_lenequal to the remaining space.- create "a"
- create "bb"
- create "ccc"
- delete "bb"
- create "dddd"
- delete "a"
- Compare directory structures. Write a short paragraph (200–250 words) comparing the four directory structures: linear list, hash table, HTree, and B-tree. Your paragraph should address: (a) the lookup algorithm; (b) the worst-case and average-case costs for lookup, insert, and delete; (c) whether the structure supports range queries; (d) the workloads for which each structure is best suited.
-
Measure the cost of a large directory.
Create a directory with a million entries (or as
many as you can reasonably create), and measure
the cost of a single lookup. Compare it to the cost
of a lookup in a directory with a thousand entries.
Use the
timecommand and thestrace -ccommand to measure the system calls and the elapsed time.
What is the ratio of the two costs? Does it match the ratio of the directory sizes (a factor of 100)? If not, why not?# Create a small directory and a large one mkdir /tmp/small-dir /tmp/large-dir for i in $(seq 1 1000); do touch /tmp/small-dir/f$i; done for i in $(seq 1 100000); do touch /tmp/large-dir/f$i; done # Time a lookup in each time ls /tmp/small-dir/f500 > /dev/null time ls /tmp/large-dir/f50000 > /dev/null -
Observe the effect of the dcache.
Write a program that opens and closes the same file
a million times, and another that opens a million
different files. Compare the elapsed times. What
is the ratio? How much of the difference is
attributable to the dcache?
Run the program with the same file (dcache hit) and with different files (dcache miss). Compare the times.#include <fcntl.h> #include <stdio.h> #include <time.h> #include <unistd.h> int main(int argc, char **argv) { struct timespec t0, t1; clock_gettime(CLOCK_MONOTONIC, &t0); for (int i = 0; i < 1000000; i++) { int fd = open(argv[1], O_RDONLY); if (fd >= 0) close(fd); } clock_gettime(CLOCK_MONOTONIC, &t1); double elapsed = (t1.tv_sec - t0.tv_sec) + (t1.tv_nsec - t0.tv_nsec) / 1e9; printf("%.4f s\n", elapsed); return 0; } -
Investigate the file system's directory
structure.
Determine which directory structure your file system
uses. For ext4, check whether the
dir_indexfeature is enabled. For XFS, examine the directory's B-tree. For Btrfs, examine the file system's B-tree.
Write down what you find. Which structure does your file system use? What is the evidence?# Check the file system type df -T / # Check ext4's features sudo tune2fs -l /dev/sda1 2>/dev/null | grep -i features # Use debugfs to examine a directory sudo debugfs -R 'stat /tmp' /dev/sda1 2>/dev/null | head -30 -
Explore the dentry cache.
On Linux, the dentry cache statistics are available
in
/proc/sys/fs/dentry-state. Run a workload that accesses many files and observe the counters.
What does the first number represent? What does the second number represent? How do the counters change after the workload? Explain what you observe in terms of the dcache's behaviour.cat /proc/sys/fs/dentry-state find /usr -type f 2>/dev/null | head -20000 cat /proc/sys/fs/dentry-state -
Design a directory layout.
A system is being built to store a large number of
small files: 10 million documents, each identified
by a unique 16-character alphanumeric ID. Design a
directory layout for storing these documents.
Consider:
- How many directories would you use?
- How would you map an ID to a directory?
- What is the maximum number of files in any directory?
- How would the layout interact with the file system's directory structure (linear list, HTree, B-tree)?
- Reflect. In your own words, explain why the classical linear list directory does not scale, and describe the two approaches (hash-based and tree-based) that modern file systems use to address the problem. Then describe one situation in your own experience — a program you have written or a system you have used — where a large directory affected performance. What was the workload, and how would an indexed directory have helped?
10 Summary
- A directory entry is a record that associates a name with an inode number. The entry may also contain a record length, a file type, and other metadata. The file system performs four operations on a directory: lookup, insert, delete, and rename.
- The classical linear-list directory stores entries in a sequence in the directory's data blocks. A lookup scans the sequence, so the cost is O(n). The structure is simple and adequate for small directories, but it does not scale.
- The ext2/ext3/ext4 directory entry format is a variable-length record with an inode number, a record length, a name length, a file type, and the name. The record length allows the file system to insert and delete entries efficiently by splitting and merging records, but the directory's size grows but does not shrink.
- The HTree is ext4's indexed directory structure. It is a B-tree-shaped hash table that stores entries in hash order and reduces the lookup cost to O(log n) — typically two or three block reads. Its limitation is that it cannot support efficient range queries, because the entries are not stored in name order.
- A B-tree or B+tree directory stores entries in name order, which allows both O(log n) lookups and efficient range queries. XFS and Btrfs use B-trees for their directories, and the structure is the most general of the options.
- The directory entry cache (dcache) is the kernel's in-memory cache of recently resolved entries. It is keyed by the parent-inode-and-name pair and holds entries in a hash table and a tree. On a system with good locality, the dcache hit rate is very high, and the cost of path resolution is dominated by memory access rather than disk access.
-
Negative caching is the dcache's
caching of failed lookups. It matters for programs
that search for files by trying many names (a shell's
$PATHsearch, a dynamic linker's library search), because without it, each failed lookup would require a disk access. - The choice of directory structure determines the file system's performance on directory-heavy workloads. The linear list is adequate for small directories; the hash table is fast for lookups but cannot support range queries; the HTree scales to millions of entries but stores entries in hash order; the B-tree scales and supports range queries. The choice is made by the file system designer, and the user cannot change it.
-
Directory compression is necessary
on file systems whose directories grow but do not
shrink. The
e2fsck -Dcommand rebuilds ext2/ext3/ext4 directories, reclaiming the wasted space and (optionally) building the HTree index. - The directory is becoming a database: the B-trees and HTrees used by modern file systems have the same structure as the indexes used by database systems, and the file system's metadata operations are increasingly transaction-like. The classical name-to-inode mapping remains the interface, but the implementation has grown significantly more sophisticated.
Looking ahead
This lesson presented the directory as a data structure: the entry, the linear list, the ext2 format, the HTree, the B-tree, and the dcache. It referred several times to the allocation of the directory's data blocks, and to the file system's general strategies for laying out data on disk. Lesson 10.5 presents file allocation: the strategies for mapping a file's blocks to physical locations on the storage device. The lesson covers the classical approaches — contiguous allocation, linked allocation, and indexed allocation — and the modern approaches — extents, block groups, and allocation policies that place related blocks near each other. The allocation strategy is one of the file system's most important performance decisions, and it interacts directly with the directory structures of this lesson.
11 References
- Silberschatz, A., Galvin, P. B., & Gagne, G. Operating System Concepts, 10th ed., Chapter 14, Section 14.3.
-
Arpaci-Dusseau, R. H., & Arpaci-Dusseau, A. C.
Operating Systems: Three Easy Pieces, Chapters 40
and 41. Available online at
ostep.org. - Tanenbaum, A. S., & Bos, H. Modern Operating Systems, 4th ed., Chapter 4, Section 4.3.3.
- McKusick, M. K., Joy, W. N., Leffler, S. J., & Fabry, R. S. A Fast File System for UNIX. ACM Transactions on Computer Systems, 1984.
- Bach, M. J. The Design of the UNIX Operating System. Prentice Hall, 1986. Chapter 5.
- Kerrisk, M. The Linux Programming Interface. No Starch Press, 2010. Chapter 14.
- Love, R. Linux Kernel Development, 3rd ed., Chapter 13, The Virtual File System. The VFS directory operations and the dentry cache.
- Bovet, D. P., & Cesati, M. Understanding the Linux Kernel, 3rd ed. O'Reilly, 2005. Chapter 12.
- Mathur, A., Cao, M., Bhattacharya, S., Dilger, A., Tomas, A., & Vivier, L. The New Ext3 Directory Index. Ottawa Linux Symposium, 2007. The paper that describes the HTree directory.
-
Linux man-pages:
man 3 opendir,man 3 readdir,man 3 seekdir,man 3 telldir,man 2 getdents,man 2 mkdir,man 2 rmdir,man 2 link,man 2 unlink,man 2 rename,man 5 proc,man 8 tune2fs,man 1 debugfs,man 8 e2fsck.