COMP 3150Operating Systems

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 the getdents system 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>/fd interface), man 8 tune2fs (for the dir_index feature), 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, every stat, and every link begins 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, open with O_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.

Directory structures: linear list, hash table, and indexed tree Three panels comparing directory structures. The first panel shows a linear list: a sequence of directory entries with names and inode numbers, where a lookup scans from the beginning. The second panel shows a hash table: the directory's data is divided into buckets, and a lookup hashes the name to find the bucket, then scans the bucket. The third panel shows an indexed tree (HTree or B-tree): the directory's entries are organized into a tree, and a lookup traverses from the root to the leaf that contains the entry. DIRECTORY STRUCTURES Linear list lookup is a scan from the start entry 0: name=A inode=12 entry 1: name=B inode=27 entry 2: name=C inode=105 entry 3: name=D inode=42 entry 4: name=E inode=88 … Lookup for "C": examine entries 0, 1, 2 → found. Cost: O(n) in the worst case. Hash table lookup hashes the name to a bucket bucket 0: [A→12] [D→42] bucket 1: [C→105] bucket 2: [B→27] [E→88] bucket 3: (empty) bucket 4: (empty) hash("C") = 1 → bucket 1 Lookup for "C": hash → bucket 1 → examine 1 entry. Cost: O(1) on average, O(n) in the worst case. Indexed tree (HTree or B-tree) lookup traverses from root to leaf root (hash range 0–1023) range 0–340 range 341–680 range 681–1023 Each leaf node holds a range of entries. Lookup traverses two or three levels. Cost: O(log n) — two or three disk accesses even for millions of entries.
Figure 10.4 — Directory structures. The linear list stores entries in a sequence and requires a scan for each lookup. The hash table groups entries into buckets by a hash of the name, so a lookup examines only the entries in the matching bucket. The indexed tree (HTree or B-tree) organizes entries into a hierarchy, so a lookup traverses from the root to the leaf that contains the entry in a bounded number of steps. The first structure is simple but does not scale; the last scales to millions of entries.

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 the rec_len may 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 by name_len.
  • file_type — a small code indicating the file's type: EXT2_FT_REG_FILE for a regular file, EXT2_FT_DIR for a directory, EXT2_FT_SYMLINK for 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:

Directory structure performance comparison
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.

  1. Observe a directory's structure. Create a directory, add some entries, and observe the directory's size with stat and du.
    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 .
    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.
  2. Measure the cost of directory lookups. Use the time command to measure the cost of creating files in a directory of different sizes, and use the time command to measure the cost of looking up files in the directory.
    # 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
    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.
  3. Compare the HTree's behaviour. Check whether the file system uses the HTree index by using tune2fs -l and looking for the dir_index feature.
    df -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 -20
    If the file system has the dir_index feature, the directory uses the HTree. The debugfs command can show the directory's internal blocks and the tree structure.
  4. Observe the dcache's effect. Use the time command to measure the cost of looking up the same file repeatedly, and compare it to the cost of looking up different files.
    # 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
    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.
  5. 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.
    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
    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.
  6. Observe the ordering of readdir results. The POSIX standard does not specify the order in which readdir returns entries. Observe the order on your system.
    # 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 .
    The ls -f and ls -U options 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).
  7. Observe the kernel's dcache statistics. On Linux, the kernel's dcache statistics are available in /proc/sys/fs/dentry-state.
    cat /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.
    find /usr -type f 2>/dev/null | head -10000
    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.
  8. Read the man pages. Read the man pages that document the directory interface.
    man 3 opendir
    man 3 readdir
    man 3 seekdir
    man 2 mkdir
    man 2 rmdir
    man 2 getdents
    man 5 proc
    For each, write a sentence describing its main contribution to your understanding of directory operations. The man 3 readdir page documents the iteration interface; the man 2 getdents page documents the underlying system call; the man 2 mkdir and man 2 rmdir pages 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
A directory entry is a record in a directory that associates a name with an inode number. The simplest entry has two fields: the name (a human-readable string, up to 255 bytes) and the inode number (the identifier of the file within the file system). Real directory entries also have fields that support the directory's data structure: a record length that allows the entry to be padded to a specific length, a file type code that indicates whether the entry is a regular file, a directory, a symbolic link, and so on, and (on some file systems) additional metadata for the entry itself. The file system performs four operations on a directory. Lookup finds the entry with a given name and returns its inode number; it is the most common operation, and it is the one that determines the directory's performance. Insert adds a new entry with a given name and inode number, used when a file is created or a hard link is added. Delete removes the entry with a given name, used when a file is unlinked or a directory is removed. Rename changes the name of an entry or moves it to a different directory, and it must be atomic with respect to concurrent lookups. The cost of each operation depends on the directory's internal structure: a linear list is O(n), a hash table is O(1) on average, and a B-tree or HTree is O(log n).

Question 2

Describe the classical linear-list directory. What are its advantages and disadvantages, and why does it fail to scale?

Show answer
The linear-list directory stores the directory's entries in a sequence in the directory's data blocks. A lookup scans the sequence, comparing the name of each entry with the name being searched for, and returns the inode number of the first matching entry. The classical implementation is the linked-list form, in which each entry contains its own record length and the next entry is found by skipping the current entry's length. The advantages of the linear list are its simplicity (the structure is a simple sequence of records with no pointers or indices) and its efficiency for small directories (a scan of a few hundred entries is negligible). The disadvantages are its performance as the directory grows: a lookup is O(n), an insert is O(n) (to find the end of the list), and a delete is O(n) (to find the entry). For a directory with tens of thousands of entries, the scan takes hundreds of microseconds and requires reading many disk blocks; for a directory with a million entries, the scan takes milliseconds and requires reading thousands of blocks. The linear list fails to scale because the cost of a lookup grows linearly with the number of entries, and modern workloads — mail spools, source repositories, caches — often have directories with hundreds of thousands or millions of entries. The remedy is to use an indexed structure (a hash table, an HTree, or a B-tree) that reduces the lookup cost to O(1) or O(log n). Modern file systems use these indexed structures for large directories while retaining the linear list's entry format for compatibility.

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
The ext2/ext3/ext4 directory entry is a structure with four fields plus the name. The 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
The HTree is the indexed directory structure used by ext3 and ext4. It is a B-tree-shaped hash table: each directory has a hash of its entries' names, and the entries are grouped into logical blocks by their hash values. The tree has a root node that spans the hash values, internal nodes that split the hash range, and leaf nodes that hold the entries whose hashes fall in their range. A lookup hashes the name, traverses the tree to the appropriate leaf block, and scans the entries in that block. The cost is O(log n), typically two or three block reads regardless of the directory's size. The improvement over the linear list is dramatic: a directory with a million entries is searched with three block reads instead of a scan of millions. The HTree's limitation compared to a B-tree is that it stores the entries in hash order, not name order. This means that the HTree cannot support efficient range queries: a program that asks for all entries whose names fall in a range must scan the entire directory, because the entries are scattered across the leaf blocks in an order that does not correspond to name order. A B-tree stores entries in name order, so it can find the range's start and iterate through the leaves. The HTree is also less efficient for names that hash to the same value, because those names end up in the same leaf block and must be searched linearly — a problem that is mitigated by the hash function's quality but not eliminated. XFS and Btrfs use B-trees for their directory structures precisely because they support both efficient lookups and efficient range queries.

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
The directory entry cache (dcache) is the kernel's in-memory cache of recently resolved 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 linked into a tree that mirrors the directory hierarchy. The dcache affects path resolution's performance by holding the entries that have been resolved recently, so a subsequent resolution of the same path (or a path that shares a prefix) can be served from memory without any disk access. On a system with good locality — where programs access the same directories and files repeatedly — 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 different files — the hit rate is lower, and the cost of path resolution is dominated by the on-disk directory lookups. The dcache is bounded by a tunable limit, and when the limit is reached, the kernel evicts the least recently used entries that are not in use. A negative lookup is a lookup for a name that does not exist in the directory. The dcache records such lookups, so a subsequent lookup of the same name fails without re-reading the directory. Negative caching matters for programs that search for files by trying many names — a shell's $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.

  1. 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_len values 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 has rec_len equal to the remaining space.
    • create "a"
    • create "bb"
    • create "ccc"
    • delete "bb"
    • create "dddd"
    • delete "a"
    After the sequence, what is the total space used? What is the total free space? Is the free space contiguous?
  2. 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.
  3. 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 time command and the strace -c command to measure the system calls and the elapsed time.
    # 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
    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?
  4. 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?
    #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;
    }
    Run the program with the same file (dcache hit) and with different files (dcache miss). Compare the times.
  5. Investigate the file system's directory structure. Determine which directory structure your file system uses. For ext4, check whether the dir_index feature is enabled. For XFS, examine the directory's B-tree. For Btrfs, examine the file system's B-tree.
    # 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
    Write down what you find. Which structure does your file system use? What is the evidence?
  6. 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.
    cat /proc/sys/fs/dentry-state
    find /usr -type f 2>/dev/null | head -20000
    cat /proc/sys/fs/dentry-state
    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.
  7. 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)?
    Justify your design.
  8. 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 $PATH search, 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 -D command 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.
👤